---
phase: 05-telegram-bot-core
plan: 02
type: execute
wave: 2
depends_on: ["05-01"]
files_modified:
  - plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
autonomous: true
requirements: [BOT-03, BOT-04]

must_haves:
  truths:
    - "Bot sends session-end notifications containing Arc Summary and Tail Brief text"
    - "Bot dispatches TTS for Tail Brief text with synchronized karaoke subtitles"
    - "main.swift creates TelegramBot and starts it alongside existing subsystems"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift"
      provides: "sendSessionNotification method that generates summaries and dispatches TTS"
      contains: "sendSessionNotification"
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift"
      provides: "TelegramBot lifecycle integrated with NSApp run loop"
      contains: "TelegramBot"
  key_links:
    - from: "TelegramBot.swift"
      to: "SummaryEngine.swift"
      via: "arcSummary and tailBrief calls"
      pattern: "summaryEngine\\.arcSummary|summaryEngine\\.tailBrief"
    - from: "TelegramBot.swift"
      to: "TTSEngine.swift"
      via: "synthesizeWithTimestamps for Tail Brief narration"
      pattern: "ttsEngine\\.synthesizeWithTimestamps"
    - from: "TelegramBot.swift"
      to: "SubtitlePanel.swift"
      via: "showUtterance for karaoke display"
      pattern: "subtitlePanel\\.showUtterance"
    - from: "main.swift"
      to: "TelegramBot.swift"
      via: "creates and starts TelegramBot in async context"
      pattern: "telegramBot\\.start"
---

<objective>
Wire TelegramBot to SummaryEngine (Arc Summary + Tail Brief notifications) and TTSEngine + SubtitlePanel (TTS dispatch with karaoke). Integrate bot lifecycle into main.swift.

Purpose: Completes Phase 5 by connecting bot notifications to the AI summary and TTS subsystems built in Phases 3-4.
Output: Updated TelegramBot.swift with notification methods, updated main.swift with bot lifecycle
</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/05-telegram-bot-core/05-01-SUMMARY.md

<interfaces>
<!-- Existing APIs from Phases 2-4 that this plan wires together -->

From SummaryEngine.swift:

```swift
final class SummaryEngine: @unchecked Sendable {
    init(client: MiniMaxClient = MiniMaxClient())
    func arcSummary(turns: [ConversationTurn], cwd: String?) async -> SummaryResult
    func tailBrief(turns: [ConversationTurn], cwd: String?) async -> SummaryResult
    func singleTurnSummary(prompt: String, response: String, lastActivityTime: Date?, cwd: String?) async -> SummaryResult
}

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

struct SummaryResult {
    let narrative: String
    let promptSummary: String?
    let ttsGreeting: String?
}
```

From TTSEngine.swift:

```swift
final class TTSEngine: @unchecked Sendable {
    func synthesizeWithTimestamps(
        text: String,
        speakerId: Int32 = Config.defaultSpeakerId,
        speed: Float = 1.0,
        completion: @escaping (Result<TTSResult, Error>) -> Void
    )
    func play(wavPath: String, completion: (() -> Void)? = nil)
    func stopPlayback()
}

struct TTSResult {
    let wavPath: String
    let text: String
    let wordTimings: [TimeInterval]
    let audioDuration: TimeInterval
}
```

From SubtitlePanel.swift:

```swift
@MainActor
final class SubtitlePanel: NSPanel {
    func showUtterance(_ text: String, wordTimings: [TimeInterval])
    func show(text: String)
    func hide()
}
```

From MiniMaxClient.swift:

```swift
final class MiniMaxClient: @unchecked Sendable {
    let circuitBreaker: CircuitBreaker
    init(circuitBreaker: CircuitBreaker = CircuitBreaker())
}
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add session notification + TTS dispatch methods to TelegramBot</name>
  <files>
    plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
  </files>
  <read_first>
    @plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
    @plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift
    @plugins/claude-tts-companion/Sources/claude-tts-companion/TTSEngine.swift
    @plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitlePanel.swift
  </read_first>
  <action>
    Extend TelegramBot to accept SummaryEngine, TTSEngine, and SubtitlePanel references, then implement session notification with TTS dispatch.

    1. **Add dependencies to TelegramBot init:**
       ```swift
       private let summaryEngine: SummaryEngine
       private let ttsEngine: TTSEngine
       private let subtitlePanel: SubtitlePanel  // @MainActor -- access must dispatch to main

       init(botToken: String, chatId: Int64, summaryEngine: SummaryEngine, ttsEngine: TTSEngine, subtitlePanel: SubtitlePanel) {
           self.botToken = botToken
           self.chatId = chatId
           self.summaryEngine = summaryEngine
           self.ttsEngine = ttsEngine
           self.subtitlePanel = subtitlePanel
       }
       ```

    2. **Session notification method** (`sendSessionNotification`):
       ```swift
       /// Send session-end notification with Arc Summary + Tail Brief, then dispatch TTS.
       /// Called by file watcher (Phase 7) when a session ends.
       func sendSessionNotification(turns: [ConversationTurn], cwd: String?) async {
           guard isWatching else {
               logger.info("Skipping notification -- bot not watching")
               return
           }

           // Generate both summaries concurrently
           async let arcResult = summaryEngine.arcSummary(turns: turns, cwd: cwd)
           async let tailResult = summaryEngine.tailBrief(turns: turns, cwd: cwd)

           let arc = await arcResult
           let tail = await tailResult

           // Build notification message (HTML formatted)
           var message = "<b>Session Complete</b>"
           if let cwd = cwd {
               message += " — <code>\(TelegramFormatter.escapeHtml(cwd))</code>"
           }

           // Arc Summary section
           if !arc.narrative.isEmpty && arc.narrative != "Session completed." {
               message += "\n\n<b>Arc Summary:</b>\n\(TelegramFormatter.escapeHtml(arc.narrative))"
           }

           // Tail Brief section
           if !tail.narrative.isEmpty {
               message += "\n\n<b>Tail Brief:</b>\n\(TelegramFormatter.escapeHtml(tail.narrative))"
           }

           // If both summaries failed, send minimal notification
           if arc.narrative == "Session completed." && tail.narrative.isEmpty {
               message += "\n\n<i>Summary generation failed or was skipped.</i>"
           }

           // Send notification
           await sendMessage(message)

           // Dispatch TTS for Tail Brief with karaoke subtitles (BOT-04)
           if !tail.narrative.isEmpty {
               await dispatchTTS(text: tail.narrative, greeting: arc.ttsGreeting)
           }
       }
       ```

    3. **TTS dispatch method** (`dispatchTTS`):
       ```swift
       /// Synthesize text and play with synchronized karaoke subtitles.
       private func dispatchTTS(text: String, greeting: String?) async {
           // Build full TTS text with optional greeting
           let fullText: String
           if let greeting = greeting, !greeting.isEmpty {
               fullText = "\(greeting) \(text)"
           } else {
               fullText = text
           }

           logger.info("Dispatching TTS: \(fullText.count) chars")

           ttsEngine.synthesizeWithTimestamps(text: fullText) { [weak self] result in
               guard let self = self else { return }
               switch result {
               case .success(let ttsResult):
                   self.logger.info("TTS synthesis complete: \(String(format: "%.2f", ttsResult.audioDuration))s")

                   // Show karaoke subtitles on main thread
                   DispatchQueue.main.async {
                       self.subtitlePanel.showUtterance(ttsResult.text, wordTimings: ttsResult.wordTimings)
                   }

                   // Play audio concurrently with subtitle display
                   self.ttsEngine.play(wavPath: ttsResult.wavPath) {
                       self.logger.info("TTS playback complete")
                   }

               case .failure(let error):
                   self.logger.error("TTS dispatch failed: \(error)")
               }
           }
       }
       ```

    4. **Update /health command** to show SummaryEngine status:
       - Check `summaryEngine` circuit breaker state if accessible. Otherwise just check `Config.miniMaxAPIKey != nil`.
       - Show TTS engine availability.

    **Swift 6 concurrency notes:**
    - `subtitlePanel` is `@MainActor` so `showUtterance` must be called from `DispatchQueue.main.async` (or `await MainActor.run {}`)
    - `ttsEngine.synthesizeWithTimestamps` uses completion handler (runs on TTS serial queue), not async/await
    - `summaryEngine.arcSummary` and `.tailBrief` are `async` methods
    - The `async let` pattern runs both summary calls concurrently

  </action>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "sendSessionNotification" plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
    - grep -q "dispatchTTS" plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
    - grep -q "summaryEngine" plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
    - grep -q "ttsEngine" plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
    - grep -q "subtitlePanel" plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
    - grep -q "showUtterance" plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
    - grep -q "arcSummary" plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
    - grep -q "tailBrief" plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
  </acceptance_criteria>
  <done>TelegramBot sends session-end notifications with Arc Summary + Tail Brief, dispatches TTS with karaoke subtitles for Tail Brief narration. swift build succeeds.</done>
</task>

<task type="auto">
  <name>Task 2: Wire TelegramBot into main.swift and verify build</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/TelegramBot.swift
    @plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
  </read_first>
  <action>
    Update main.swift to create and start TelegramBot alongside existing SubtitlePanel and TTSEngine.

    1. **Replace the TTS demo block** (the `DispatchQueue.main.asyncAfter(deadline: .now() + 0.5)` demo synthesis block) with bot startup logic.

    2. **Add bot initialization after existing subsystem creation** (after `let ttsEngine = TTSEngine()`):
       ```swift
       // Create summary engine
       let summaryEngine = SummaryEngine()

       // Create and start Telegram bot (if token available)
       var telegramBot: TelegramBot?
       if let token = Config.telegramBotToken, let chatId = Config.telegramChatId {
           let bot = TelegramBot(
               botToken: token,
               chatId: chatId,
               summaryEngine: summaryEngine,
               ttsEngine: ttsEngine,
               subtitlePanel: subtitlePanel
           )
           telegramBot = bot

           // Start bot in background task (async context)
           Task {
               do {
                   try await bot.start()
                   logger.info("Telegram bot started successfully")
               } catch {
                   logger.error("Failed to start Telegram bot: \(error)")
               }
           }
       } else {
           logger.warning("TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID not set -- bot disabled")
       }
       ```

    3. **Update SIGTERM handler** to stop bot on shutdown:
       ```swift
       sigSource.setEventHandler {
           logger.info("SIGTERM received, shutting down")
           subtitlePanel.hide()
           if let bot = telegramBot {
               Task {
                   await bot.stop()
               }
           }
           // existing dummy event + app.stop code
       }
       ```

    4. **Update keepAlive references** to prevent ARC deallocation:
       ```swift
       nonisolated(unsafe) var keepBot: TelegramBot? = telegramBot
       nonisolated(unsafe) var keepSummary: SummaryEngine? = summaryEngine
       ```

    5. **Keep subtitle panel demo as fallback** -- If bot token is not set AND this is a dev run, keep the subtitle demo. Otherwise remove it (the bot will drive subtitle display via notifications).
       ```swift
       // Show subtitle demo only when bot is disabled (no token = dev mode)
       if telegramBot == nil {
           DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
               subtitlePanel.demo()
           }
       }
       ```

    6. **Verify build** with `swift build` (not release, just debug is fine for verification).

    **Important:** The bot token environment variable must be set for the bot to start. During development, use a TEST bot token (different from production) to avoid long-polling conflicts. This is documented in STATE.md as a known concern.

  </action>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "TelegramBot" plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
    - grep -q "SummaryEngine" plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
    - grep -q "TELEGRAM_BOT_TOKEN" plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
    - grep -q "bot.start" plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
    - grep -q "bot.stop" plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
    - swift build succeeds in plugins/claude-tts-companion/
  </acceptance_criteria>
  <done>main.swift creates TelegramBot with SummaryEngine, TTSEngine, SubtitlePanel references. Bot starts on launch if TELEGRAM_BOT_TOKEN is set, falls back to subtitle demo otherwise. SIGTERM stops bot cleanly. swift build succeeds.</done>
</task>

</tasks>

<verification>
1. `cd plugins/claude-tts-companion && swift build` succeeds with zero errors
2. TelegramBot.swift has sendSessionNotification calling both arcSummary and tailBrief
3. TelegramBot.swift has dispatchTTS wiring TTSEngine + SubtitlePanel
4. main.swift creates and starts TelegramBot with all subsystem references
5. main.swift gracefully handles missing bot token (falls back to demo mode)
6. SIGTERM handler stops bot before exiting
</verification>

<success_criteria>

- swift build succeeds
- Session notification path: TelegramBot -> SummaryEngine -> sendMessage (HTML) + TTSEngine -> SubtitlePanel (karaoke)
- Bot lifecycle managed in main.swift (start on launch, stop on SIGTERM)
- Graceful degradation when TELEGRAM_BOT_TOKEN not set
  </success_criteria>

<output>
After completion, create `.planning/phases/05-telegram-bot-core/05-02-SUMMARY.md`
</output>
