---
phase: 07-file-watching-auto-continue
plan: 02
type: execute
wave: 2
depends_on: [07-01]
files_modified:
  - plugins/claude-tts-companion/Sources/claude-tts-companion/AutoContinue.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
autonomous: true
requirements: [AUTO-01, AUTO-02, AUTO-03]

must_haves:
  truths:
    - "Stop hook evaluates session transcript via MiniMax and returns CONTINUE, SWEEP, REDIRECT, or DONE"
    - "Plan file discovery scans transcript text for .claude/plans/*.md references"
    - "SWEEP mode produces a 5-step review pipeline injected as the continue prompt"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/AutoContinue.swift"
      provides: "AutoContinueEvaluator with MiniMax-based session evaluation and plan discovery"
      exports: ["AutoContinueEvaluator", "ContinueDecision"]
      min_lines: 150
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift"
      provides: "Wiring of NotificationWatcher + AutoContinueEvaluator into the app lifecycle"
      contains: "NotificationWatcher"
  key_links:
    - from: "AutoContinue.swift"
      to: "MiniMaxClient.query"
      via: "Evaluation prompt asking for CONTINUE/SWEEP/REDIRECT/DONE"
      pattern: "client\\.query"
    - from: "AutoContinue.swift"
      to: "TranscriptParser.parse"
      via: "Parse transcript to extract session content for evaluation"
      pattern: "TranscriptParser\\.parse"
    - from: "main.swift"
      to: "NotificationWatcher"
      via: "Start watcher, callback sends notification via TelegramBot"
      pattern: "NotificationWatcher"
---

<objective>
Create the auto-continue evaluator that uses MiniMax to decide whether a completed session should continue, sweep, redirect, or stop. Wire NotificationWatcher and AutoContinueEvaluator into main.swift.

Purpose: Auto-continue is the intelligence layer that decides what happens after a Claude Code session finishes. Combined with file watching from Plan 01, this completes the event-driven pipeline: file change -> detection -> evaluation -> action.
Output: AutoContinue.swift with evaluator logic; updated main.swift with full file watching integration.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/07-file-watching-auto-continue/07-01-SUMMARY.md
@plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/MiniMaxClient.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/TranscriptParser.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/FileWatcher.swift

<interfaces>
<!-- Key types and contracts from Plan 01 and existing code -->

From FileWatcher.swift (Plan 01 output):

```swift
final class NotificationWatcher: @unchecked Sendable {
    init(directory: String = Config.notificationDir, callback: @escaping (String) -> Void)
    func start()
    func stop()
}

final class JSONLTailer: @unchecked Sendable {
    init(filePath: String, callback: @escaping ([String]) -> Void)
    func start()
    func stop()
}
```

From MiniMaxClient.swift:

```swift
final class MiniMaxClient: @unchecked Sendable {
    let circuitBreaker: CircuitBreaker
    func query(prompt: String, systemPrompt: String, maxTokens: Int) async throws -> MiniMaxResult
}
struct MiniMaxResult {
    let text: String
    let durationMs: Int
    let outputTokens: Int?
}
```

From TranscriptParser.swift:

```swift
enum TranscriptEntry: Sendable {
    case prompt(text: String, timestamp: Date?)
    case response(text: String, timestamp: Date?)
    case toolUse(name: String, timestamp: Date?)
    case toolResult(content: String, timestamp: Date?)
    case unknown(type: String)
}
enum TranscriptParser {
    static func parse(filePath path: String) -> [TranscriptEntry]
    static func parse(content: String) -> [TranscriptEntry]
}
```

From TelegramBot.swift:

```swift
final class TelegramBot: @unchecked Sendable {
    func sendNotification(_ text: String) async
    func sendSessionNotification(turns: [ConversationTurn], cwd: String?) async
}
```

From SummaryEngine.swift:

```swift
struct ConversationTurn {
    let prompt: String
    let response: String
    let timestamp: Date?
    let toolSummary: String?
    let toolResults: String?
}
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create AutoContinue.swift with MiniMax evaluation and plan discovery</name>
  <files>plugins/claude-tts-companion/Sources/claude-tts-companion/AutoContinue.swift</files>
  <read_first>
    - plugins/claude-tts-companion/Sources/claude-tts-companion/MiniMaxClient.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/TranscriptParser.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift
  </read_first>
  <action>
Create AutoContinue.swift with `import Foundation` and `import Logging`.

**ContinueDecision enum** (AUTO-01):

```swift
enum ContinueDecision: String, Sendable {
    case `continue` = "CONTINUE"  // Session has more work to do
    case sweep = "SWEEP"          // Run 5-step review pipeline
    case redirect = "REDIRECT"    // Switch to a different task
    case done = "DONE"            // Session is complete, no action needed
}
```

**AutoContinueEvaluator class** (AUTO-01, AUTO-02, AUTO-03):

- `final class AutoContinueEvaluator: @unchecked Sendable`
- Properties:
  - `private let logger = Logger(label: "auto-continue")`
  - `private let client: MiniMaxClient`
- `init(client: MiniMaxClient)` -- reuse the same MiniMaxClient instance (shared circuit breaker)

**`func evaluate(transcriptPath: String) async -> (decision: ContinueDecision, reason: String)`** (AUTO-01):

- Parse the transcript: `let entries = TranscriptParser.parse(filePath: transcriptPath)`
- Guard that entries are not empty, return `.done` with reason "Empty transcript" if so
- Extract the last few entries to build evaluation context (last 5 prompt/response pairs max, truncated to 3000 chars each)
- Build a MiniMax prompt that:
  - Provides the session's last exchange as context
  - Asks: "Based on this session transcript, what should happen next?"
  - Instructs the model to respond with EXACTLY one of: CONTINUE, SWEEP, REDIRECT, DONE
  - CONTINUE: "The session was working on a multi-step task and there's clearly more to do"
  - SWEEP: "The session completed a feature/plan and should run a review sweep"
  - REDIRECT: "The session should pivot to a different task mentioned in the conversation"
  - DONE: "The session completed its work and no further action is needed"
  - After the decision word, add a pipe `|` followed by a one-sentence reason
  - Example output format: `SWEEP | The session completed plan 07-01 and should verify the implementation`
- System prompt: "You evaluate Claude Code session transcripts and decide the next action. Respond with EXACTLY one decision word (CONTINUE, SWEEP, REDIRECT, DONE) followed by | and a reason."
- Call `client.query(prompt:systemPrompt:maxTokens: 256)`
- Parse the response: split on `|`, trim, match first word against ContinueDecision raw values
- On parse failure or API error, default to `.done` with reason describing the error
- Log the decision and reason at `.info` level

**`func discoverPlanFiles(transcriptPath: String) -> [String]`** (AUTO-02):

- Read the transcript file as a string
- Use a regex to find all references matching `.claude/plans/*.md` or similar plan file patterns:
  - Pattern: `\\.claude/plans/[\\w.-]+\\.md` and also `\\.planning/phases/[\\w.-/]+PLAN\\.md`
- Return deduplicated array of matched paths
- Log count of discovered plan files

**`func buildSweepPrompt(planFiles: [String]) -> String`** (AUTO-03):

- Build a 5-step review pipeline prompt string:

  ```
  Run this 5-step review pipeline:

  1. VERIFY: Run the build and any existing tests. Report pass/fail.
  2. DIFF AUDIT: Review all changes made in this session. Flag any TODOs, debug code, or hardcoded values.
  3. PLAN CHECK: Compare implementation against plan requirements: {planFiles joined}. List any gaps.
  4. INTEGRATION: Check that new code integrates correctly with existing modules (imports resolve, types match).
  5. SUMMARY: Write a brief summary of what was accomplished and any remaining work.

  Report each step's result clearly.
  ```

- If planFiles is empty, step 3 says "No plan files discovered -- skip this step."
- Return the prompt string (this is NOT sent to MiniMax -- it's the prompt that would be injected into a CONTINUE/SWEEP auto-resume of Claude Code)

Mark the class and enum with appropriate Sendable conformance for Swift 6 strict concurrency.
</action>
<verify>
<automated>cd /Users/terryli/eon/cc-skills/plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>AutoContinue.swift compiles. ContinueDecision has four cases. evaluate() calls MiniMax and parses response. discoverPlanFiles() uses regex. buildSweepPrompt() returns 5-step pipeline.</acceptance_criteria>
<done>AutoContinueEvaluator evaluates transcripts via MiniMax returning CONTINUE/SWEEP/REDIRECT/DONE. Plan discovery finds .claude/plans/\*.md references. SWEEP mode builds 5-step review prompt.</done>
</task>

<task type="auto">
  <name>Task 2: Wire NotificationWatcher and AutoContinueEvaluator into main.swift</name>
  <files>plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift</files>
  <read_first>
    - plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/FileWatcher.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/AutoContinue.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
  </read_first>
  <action>
Update main.swift to integrate file watching and auto-continue. Add after the Telegram bot setup block and before the SIGTERM handler:

1. **Create AutoContinueEvaluator** using the shared MiniMaxClient from SummaryEngine:
   - `let autoContinue = AutoContinueEvaluator(client: SummaryEngine().client)`
   - Wait -- SummaryEngine's client is not public. Instead, create a shared MiniMaxClient instance:
     - Move `let miniMaxClient = MiniMaxClient()` to before `let summaryEngine = SummaryEngine(client: miniMaxClient)`
     - Then: `let autoContinue = AutoContinueEvaluator(client: miniMaxClient)`
   - This shares the circuit breaker between summaries and auto-continue

2. **Create NotificationWatcher**:

   ```swift
   let notificationWatcher = NotificationWatcher { [weak telegramBot] filePath in
       guard let bot = telegramBot else { return }
       // Read the notification JSON file
       guard let data = FileManager.default.contents(atPath: filePath),
             let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
           logger.warning("Could not parse notification file: \(filePath)")
           return
       }

       let sessionId = json["session_id"] as? String ?? "unknown"
       let transcriptPath = json["transcript_path"] as? String
       let cwd = json["cwd"] as? String

       logger.info("Session notification: \(sessionId)")

       Task {
           // If we have a transcript, evaluate auto-continue
           if let tp = transcriptPath {
               let (decision, reason) = await autoContinue.evaluate(transcriptPath: tp)
               logger.info("Auto-continue decision: \(decision.rawValue) -- \(reason)")

               // Send decision notification to Telegram
               let decisionEmoji: String
               switch decision {
               case .continue: decisionEmoji = "CONTINUE"
               case .sweep: decisionEmoji = "SWEEP"
               case .redirect: decisionEmoji = "REDIRECT"
               case .done: decisionEmoji = "DONE"
               }
               await bot.sendNotification("<b>[\(decisionEmoji)]</b> \(reason)")
           }

           // Parse transcript for session notification
           if let tp = transcriptPath {
               let entries = TranscriptParser.parse(filePath: tp)
               let turns = entriesToTurns(entries)
               await bot.sendSessionNotification(turns: turns, cwd: cwd)
           }
       }
   }
   notificationWatcher.start()
   ```

3. **Add entriesToTurns helper** (private function at the bottom of main.swift or before `app.run()`):

   ```swift
   func entriesToTurns(_ entries: [TranscriptEntry]) -> [ConversationTurn] {
       var turns: [ConversationTurn] = []
       var currentPrompt: (text: String, timestamp: Date?)? = nil
       var toolNames: [String] = []
       var toolResults: [String] = []

       for entry in entries {
           switch entry {
           case .prompt(let text, let ts):
               // If we had a pending prompt without a response, flush it
               if let prompt = currentPrompt {
                   turns.append(ConversationTurn(
                       prompt: prompt.text, response: "",
                       timestamp: prompt.timestamp, toolSummary: nil, toolResults: nil
                   ))
               }
               currentPrompt = (text, ts)
               toolNames = []
               toolResults = []
           case .response(let text, let ts):
               if let prompt = currentPrompt {
                   let toolSummary = toolNames.isEmpty ? nil : toolNames.joined(separator: ", ")
                   let toolResultStr = toolResults.isEmpty ? nil : toolResults.joined(separator: "\n")
                   turns.append(ConversationTurn(
                       prompt: prompt.text, response: text,
                       timestamp: prompt.timestamp ?? ts, toolSummary: toolSummary, toolResults: toolResultStr
                   ))
                   currentPrompt = nil
               }
           case .toolUse(let name, _):
               toolNames.append(name)
           case .toolResult(let content, _):
               if !content.isEmpty { toolResults.append(String(content.prefix(500))) }
           case .unknown:
               break
           }
       }
       // Flush any trailing prompt
       if let prompt = currentPrompt {
           turns.append(ConversationTurn(
               prompt: prompt.text, response: "",
               timestamp: prompt.timestamp, toolSummary: nil, toolResults: nil
           ))
       }
       return turns
   }
   ```

4. **Add strong references** to prevent ARC deallocation (following existing keepAlive pattern):

   ```swift
   nonisolated(unsafe) var keepNotificationWatcher: NotificationWatcher? = notificationWatcher
   nonisolated(unsafe) var keepAutoContinue: AutoContinueEvaluator? = autoContinue
   ```

5. **Update SIGTERM handler** to stop the notification watcher:
   - Add `notificationWatcher.stop()` inside the SIGTERM event handler block, before `app.stop(nil)`

6. **Update SummaryEngine initialization** to use shared MiniMaxClient:
   - Change `let summaryEngine = SummaryEngine()` to `let miniMaxClient = MiniMaxClient()` then `let summaryEngine = SummaryEngine(client: miniMaxClient)`

IMPORTANT: The `SummaryEngine.init(client:)` already accepts a MiniMaxClient parameter (confirmed from reading the source). The `client` property on SummaryEngine is private, so we must create the shared client externally.
</action>
<verify>
<automated>cd /Users/terryli/eon/cc-skills/plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>main.swift creates NotificationWatcher with callback that evaluates auto-continue and sends session notifications. Shared MiniMaxClient is used by both SummaryEngine and AutoContinueEvaluator. Strong references prevent ARC deallocation. SIGTERM handler stops the watcher. swift build succeeds.</acceptance_criteria>
<done>File watching and auto-continue are wired into the app lifecycle. Notification events trigger MiniMax evaluation and Telegram notifications. All DispatchSource references are strongly held. Build passes.</done>
</task>

</tasks>

<verification>
1. `swift build` succeeds with zero errors
2. main.swift creates NotificationWatcher and AutoContinueEvaluator
3. AutoContinue.swift has evaluate() returning ContinueDecision
4. Auto-continue decisions are sent to Telegram
5. Strong references: `keepNotificationWatcher` and `keepAutoContinue` in main.swift
6. SIGTERM handler calls `notificationWatcher.stop()`
7. Shared MiniMaxClient between SummaryEngine and AutoContinueEvaluator
</verification>

<success_criteria>

- AutoContinueEvaluator evaluates transcripts via MiniMax API
- ContinueDecision enum has CONTINUE/SWEEP/REDIRECT/DONE cases
- Plan discovery finds .claude/plans/\*.md references in transcripts
- SWEEP mode builds 5-step review pipeline prompt
- NotificationWatcher callback triggers evaluation and notification
- All new objects have strong references preventing ARC deallocation
- swift build passes clean
  </success_criteria>

<output>
After completion, create `.planning/phases/07-file-watching-auto-continue/07-02-SUMMARY.md`
</output>
