---
phase: 14-tts-dispatch--feature-gates
plan: 02
type: execute
wave: 2
depends_on: ["14-01"]
files_modified:
  - plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
autonomous: true
requirements: [TTS-10, TTS-11]

must_haves:
  truths:
    - "Tail Brief text is dispatched to TTS after summary generation with karaoke subtitles"
    - "TTS greeting says 'Hi Terry, you were working in {project}:' before the Tail Brief"
    - "CJK-heavy text uses Chinese voice (speakerId 45) instead of English voice (speakerId 3)"
    - "TTS dispatch is skipped when TBR_TTS_ENABLED=false"
    - "Telegram Arc Summary is skipped when SUMMARIZER_TG_ENABLED=false"
    - "Telegram Tail Brief is skipped when TBR_TG_ENABLED=false"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift"
      provides: "Feature-gated TTS dispatch with language detection and proper greeting"
      contains: "LanguageDetector.detect"
  key_links:
    - from: "TelegramBot.swift"
      to: "LanguageDetector.swift"
      via: "LanguageDetector.detect(text:)"
      pattern: "LanguageDetector\\.detect"
    - from: "TelegramBot.swift"
      to: "FeatureGates.swift"
      via: "FeatureGates.tbrTtsEnabled etc."
      pattern: "FeatureGates\\."
    - from: "TelegramBot.swift dispatchTTS"
      to: "TTSEngine.synthesizeWithTimestamps"
      via: "speakerId from LanguageDetector"
      pattern: "speakerId.*LanguageDetector"
---

<objective>
Wire language detection and feature gates into TelegramBot's notification and TTS dispatch pipeline.

Purpose: Complete the TTS dispatch flow so Tail Brief is spoken with the correct voice (English or Chinese based on content) and each outlet can be independently toggled.
Output: Updated TelegramBot.swift with gated outlets and language-aware TTS
</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/14-tts-dispatch--feature-gates/14-01-SUMMARY.md
@plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/TTSEngine.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift

<interfaces>
<!-- From Plan 01 (LanguageDetector.swift) -->
```swift
struct LanguageResult {
    let lang: String      // "en-us" or "cmn"
    let speakerId: Int32  // 3 (af_heart) or 45 (zf_xiaobei)
}

enum LanguageDetector {
static func detect(text: String) -> LanguageResult
}

````

<!-- From Plan 01 (FeatureGates.swift) -->
```swift
enum FeatureGates {
    static var summarizerTgEnabled: Bool  // SUMMARIZER_TG_ENABLED != "false"
    static var ttsEnabled: Bool           // TTS_ENABLED != "false"
    static var summaryTtsEnabled: Bool    // ttsEnabled && SUMMARY_TTS_ENABLED != "false"
    static var tbrTgEnabled: Bool         // TBR_TG_ENABLED != "false"
    static var tbrTtsEnabled: Bool        // ttsEnabled && TBR_TTS_ENABLED != "false"
    static var allOutletsDisabled: Bool
}
````

<!-- Existing TTSEngine API (speakerId parameter already exists) -->

```swift
func synthesizeWithTimestamps(
    text: String,
    speakerId: Int32 = Config.defaultSpeakerId,
    speed: Float = 1.0,
    completion: @escaping (Result<TTSResult, Error>) -> Void
)
```

<!-- Existing SummaryEngine API -->

```swift
struct SummaryResult {
    let narrative: String
    let promptSummary: String?
    let ttsGreeting: String?  // e.g. "Hi Terry, you were working in cc skills."
}
```

<!-- Current dispatchTTS signature -->

```swift
private func dispatchTTS(text: String, greeting: String?) async
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Wire feature gates into sendSessionNotification</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,
    ~/.claude/automation/claude-telegram-sync/src/claude-sync/notification-watcher.ts
  </read_first>
  <action>
    Modify `sendSessionNotification()` in TelegramBot.swift to add feature gate checks matching the legacy TypeScript notification-watcher.ts outlet pattern.

    **Changes to sendSessionNotification():**

    1. At the top of the method (after the `guard isWatching` check), add early-exit for all outlets disabled:
       ```swift
       guard !FeatureGates.allOutletsDisabled else {
           logger.info("All notification outlets disabled -- skipping")
           return
       }
       ```

    2. Gate the Arc Summary Telegram message send with `FeatureGates.summarizerTgEnabled`:
       - Wrap the existing `sendMessage(message)` block (lines ~83-90 in current code) in:
         ```swift
         if FeatureGates.summarizerTgEnabled {
             // existing send logic
         } else {
             logger.info("Arc Summary TG outlet disabled")
         }
         ```

    3. Gate the Tail Brief Telegram message with `FeatureGates.tbrTgEnabled`:
       - Wrap the existing `sendSilentMessage(tbrMessage)` block in:
         ```swift
         if FeatureGates.tbrTgEnabled {
             // existing send logic
         }
         ```

    4. Gate the TTS dispatch with `FeatureGates.tbrTtsEnabled`:
       - Wrap the existing `dispatchTTS(...)` call in:
         ```swift
         if FeatureGates.tbrTtsEnabled {
             // existing dispatch logic
         }
         ```

    5. **Fix the Tail Brief greeting (TTS-11):** Currently `tailBrief()` returns `ttsGreeting: nil` and `dispatchTTS` uses `arc.ttsGreeting`. The legacy system computes a separate greeting for TTS: "Hi Terry, you were working in {project}:". Fix by:
       - Compute the TTS greeting directly in `sendSessionNotification` using `SummaryEngine.formatProjectName(cwd)`:
         ```swift
         let projectName = summaryEngine.formatProjectName(cwd)
         let ttsGreeting = "Hi Terry, you were working in \(projectName):"
         ```
       - Pass this computed greeting to `dispatchTTS` instead of `arc.ttsGreeting`
       - This matches the legacy pattern where the greeting is "Hi Terry, you were working in {project}:" (note: "working in" not "in this session in")

  </action>
  <acceptance_criteria>
    - Arc Summary Telegram send is gated by FeatureGates.summarizerTgEnabled
    - Tail Brief Telegram send is gated by FeatureGates.tbrTgEnabled
    - TTS dispatch is gated by FeatureGates.tbrTtsEnabled
    - All outlets disabled causes early return
    - TTS greeting is "Hi Terry, you were working in {project}:" using formatProjectName
  </acceptance_criteria>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <done>All five feature gates are wired into sendSessionNotification matching legacy outlet pattern. TTS greeting uses "working in {project}:" format.</done>
</task>

<task type="auto">
  <name>Task 2: Wire language detection into dispatchTTS</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/TTSEngine.swift,
    ~/.claude/automation/claude-telegram-sync/src/tts/kokoro-client.ts
  </read_first>
  <action>
    Modify `dispatchTTS()` in TelegramBot.swift to detect language and pass the correct speaker ID to the TTS engine.

    **Changes to dispatchTTS():**

    1. After building `fullText` (greeting + text), detect language:
       ```swift
       let langResult = LanguageDetector.detect(text: fullText)
       logger.info("TTS language: \(langResult.lang), speakerId: \(langResult.speakerId)")
       ```

    2. Pass `speakerId` to `synthesizeWithTimestamps`:
       - Change the existing call from:
         ```swift
         ttsEngine.synthesizeWithTimestamps(text: fullText) { ... }
         ```
       - To:
         ```swift
         ttsEngine.synthesizeWithTimestamps(text: fullText, speakerId: langResult.speakerId) { ... }
         ```

    This is a minimal change because `synthesizeWithTimestamps` already accepts `speakerId` as a parameter with a default value. We just need to pass the detected value instead of relying on the default.

    **No other files need changes.** TTSEngine.synthesize() already passes speakerId through to `SherpaOnnxOfflineTtsGenerate(tts, text, speakerId, speed)`.

  </action>
  <acceptance_criteria>
    - dispatchTTS calls LanguageDetector.detect on the full text
    - English text uses speakerId 3 (af_heart)
    - CJK-heavy text (>20%) uses speakerId 45 (zf_xiaobei)
    - speakerId is passed to synthesizeWithTimestamps, not hardcoded
  </acceptance_criteria>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <done>dispatchTTS uses LanguageDetector to select voice. Chinese text routes to zf_xiaobei (45), English text to af_heart (3).</done>
</task>

</tasks>

<verification>
- `cd plugins/claude-tts-companion && swift build` compiles without errors
- grep confirms LanguageDetector.detect is called in dispatchTTS
- grep confirms FeatureGates checks in sendSessionNotification
- grep confirms speakerId parameter passed to synthesizeWithTimestamps
- grep confirms "working in" greeting pattern in sendSessionNotification
</verification>

<success_criteria>
TelegramBot.swift uses LanguageDetector for voice selection and FeatureGates for outlet toggling. Build succeeds. All 4 requirements (TTS-10 through TTS-13) are addressed.
</success_criteria>

<output>
After completion, create `.planning/phases/14-tts-dispatch--feature-gates/14-02-SUMMARY.md`
</output>
