---
phase: 10-deployment-extras
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/claude-tts-companion/scripts/install.sh
  - plugins/claude-tts-companion/scripts/rollback.sh
  - plugins/claude-tts-companion/launchd/com.terryli.claude-tts-companion.plist
  - plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
autonomous: true
requirements: [DEP-01, DEP-02, DEP-03, DEP-04]

must_haves:
  truths:
    - "launchd plist loads the unified binary with auto-restart on crash"
    - "install script builds release binary, strips it, copies to /usr/local/bin, installs plist, stops old services"
    - "rollback script stops unified binary, re-enables old services in under 30 seconds"
    - "Config.swift kokoroModelPath defaults to canonical ~/.local/share/kokoro/models/ path"
  artifacts:
    - path: "plugins/claude-tts-companion/launchd/com.terryli.claude-tts-companion.plist"
      provides: "launchd service definition"
      contains: "com.terryli.claude-tts-companion"
    - path: "plugins/claude-tts-companion/scripts/install.sh"
      provides: "Build + install + service cutover"
      min_lines: 40
    - path: "plugins/claude-tts-companion/scripts/rollback.sh"
      provides: "Rollback to old services"
      min_lines: 20
  key_links:
    - from: "launchd plist"
      to: "/usr/local/bin/claude-tts-companion"
      via: "Program key"
      pattern: "/usr/local/bin/claude-tts-companion"
    - from: "install.sh"
      to: "launchd plist"
      via: "launchctl bootstrap"
      pattern: "launchctl bootstrap"
    - from: "rollback.sh"
      to: "old services"
      via: "launchctl bootstrap"
      pattern: "com.terryli.telegram-bot"
---

<objective>
Create the launchd service definition, install script, and rollback script for deploying claude-tts-companion as a managed macOS LaunchAgent.

Purpose: The unified binary needs to run as a persistent background service that auto-restarts on crash, with a safe rollback path to the old three-service setup.
Output: Plist file, install.sh, rollback.sh, updated Config.swift model path default.
</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
@plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
@plugins/claude-tts-companion/Package.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift

<interfaces>
<!-- Existing launchd plist patterns from the user's system -->

From ~/Library/LaunchAgents/com.terryli.telegram-bot.plist:

- EnvironmentVariables: HOME, PATH (with mise shims)
- KeepAlive: NetworkState + SuccessfulExit: false
- RunAtLoad: true
- StandardErrorPath/StandardOutPath to ~/.local/state/launchd-logs/{service}/
- ThrottleInterval: 30
- ExitTimeOut: 10
- Nice: 0, ProcessType: Background

From ~/Library/LaunchAgents/com.terryli.kokoro-tts-server.plist:

- KeepAlive: true (simpler — always restart)
- Nice: -10 (higher priority for TTS)
- ProcessType: Adaptive
- SoftResourceLimits/MemoryLimit: 4GB
- ExitTimeOut: 15

From Config.swift:

```swift
static let serviceLabel = "com.terryli.claude-tts-companion"
static let httpPort: UInt16 = 8780
static let miniMaxApiKey: String? = ProcessInfo.processInfo.environment["MINIMAX_API_KEY"]
static let telegramBotToken: String? = ProcessInfo.processInfo.environment["TELEGRAM_BOT_TOKEN"]
static let telegramChatId: String? = ProcessInfo.processInfo.environment["TELEGRAM_CHAT_ID"]
```

Env vars the binary reads: SHERPA_ONNX_PATH, KOKORO_MODEL_PATH, MINIMAX_API_KEY, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, CLAUDE_CLI_PATH, CLAUDE_NOTIFICATION_DIR, CLAUDE_PROJECTS_DIR, HOME
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Launchd plist + Config.swift model path fix</name>
  <files>
    plugins/claude-tts-companion/launchd/com.terryli.claude-tts-companion.plist,
    plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
  </files>
  <read_first>
    - plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
    - ~/Library/LaunchAgents/com.terryli.telegram-bot.plist
    - ~/Library/LaunchAgents/com.terryli.kokoro-tts-server.plist
  </read_first>
  <action>
    1. Create `plugins/claude-tts-companion/launchd/` directory.

    2. Create `com.terryli.claude-tts-companion.plist` XML plist with these keys:
       - Label: "com.terryli.claude-tts-companion"
       - Program: "/usr/local/bin/claude-tts-companion"
       - RunAtLoad: true
       - KeepAlive: dict with NetworkState=true (needs internet for Telegram/MiniMax) and SuccessfulExit=false (restart on crash, not on clean exit)
       - ProcessType: "Adaptive" (both background work + interactive TTS)
       - Nice: -5 (moderate priority — TTS needs low latency but not as aggressive as -10)
       - ExitTimeOut: 15 (give SIGTERM handler time to clean up bot/watchers)
       - ThrottleInterval: 30 (prevent rapid restart loops)
       - EnvironmentVariables dict with:
         - HOME: /Users/terryli
         - PATH: /Users/terryli/.local/share/mise/shims:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
         - SHERPA_ONNX_PATH: /Users/terryli/fork-tools/sherpa-onnx/build-swift-macos/install
         - KOKORO_MODEL_PATH: /Users/terryli/.local/share/kokoro/models/kokoro-int8-multi-lang-v1_0
         - TELEGRAM_BOT_TOKEN: (leave empty string — user fills in)
         - TELEGRAM_CHAT_ID: (leave empty string — user fills in)
         - MINIMAX_API_KEY: (leave empty string — user fills in)
       - SoftResourceLimits/MemoryLimit: 1073741824 (1GB — peak 561MB during TTS + headroom)
       - StandardErrorPath: /Users/terryli/.local/state/launchd-logs/claude-tts-companion/stderr.log
       - StandardOutPath: /Users/terryli/.local/state/launchd-logs/claude-tts-companion/stdout.log

    3. Update Config.swift `kokoroModelPath` default from the dev spike path to the canonical production path:
       Change default from `~/tmp/subtitle-spikes-7aqa/03-textream/models-int8/kokoro-int8-multi-lang-v1_0`
       to `~/.local/share/kokoro/models/kokoro-int8-multi-lang-v1_0`
       Keep the KOKORO_MODEL_PATH env var override pattern intact.
       This addresses DEP-04.

  </action>
  <verify>
    <automated>plutil -lint plugins/claude-tts-companion/launchd/com.terryli.claude-tts-companion.plist && grep -q "claude-tts-companion" plugins/claude-tts-companion/launchd/com.terryli.claude-tts-companion.plist && grep -q ".local/share/kokoro/models" plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift</automated>
  </verify>
  <acceptance_criteria>
    - Plist passes plutil lint with zero errors
    - Plist contains all required env vars, log paths, KeepAlive, RunAtLoad
    - Config.swift kokoroModelPath defaults to ~/.local/share/kokoro/models/kokoro-int8-multi-lang-v1_0
    - KOKORO_MODEL_PATH env var override still works
  </acceptance_criteria>
  <done>Valid launchd plist exists with all required configuration; Config.swift points to canonical model path</done>
</task>

<task type="auto">
  <name>Task 2: Install and rollback scripts</name>
  <files>
    plugins/claude-tts-companion/scripts/install.sh,
    plugins/claude-tts-companion/scripts/rollback.sh
  </files>
  <read_first>
    - plugins/claude-tts-companion/launchd/com.terryli.claude-tts-companion.plist
  </read_first>
  <action>
    1. Create `plugins/claude-tts-companion/scripts/` directory.

    2. Create `install.sh` (bash, executable) that does:
       a. set -euo pipefail
       b. Define variables: REPO_DIR (dirname of script, up two levels), BINARY_NAME, INSTALL_DIR=/usr/local/bin, PLIST_NAME, PLIST_SRC (relative to repo), PLIST_DEST=~/Library/LaunchAgents/, LOG_DIR=~/.local/state/launchd-logs/claude-tts-companion
       c. Print header: "Installing claude-tts-companion..."
       d. Build release binary: `cd "$REPO_DIR" && swift build -c release`
       e. Strip the binary: `strip .build/release/claude-tts-companion`
       f. Copy to install dir: `cp .build/release/claude-tts-companion "$INSTALL_DIR/"`
       g. Print binary size: `ls -lh "$INSTALL_DIR/claude-tts-companion"`
       h. Create log directory: `mkdir -p "$LOG_DIR"`
       i. Copy plist to LaunchAgents: `cp "$PLIST_SRC" "$PLIST_DEST/"`
       j. Stop old services (DEP-02) — use bootout, not unload (modern launchctl):
          - `launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.terryli.telegram-bot.plist 2>/dev/null || true`
          - `launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.terryli.kokoro-tts-server.plist 2>/dev/null || true`
          - Print: "Old services stopped (plists preserved on disk)"
          - Do NOT delete the old plist files — just stop the services
       k. Bootstrap new service: `launchctl bootstrap gui/$(id -u) "$PLIST_DEST/$PLIST_NAME"`
       l. Wait 2 seconds, check service is running: `launchctl print gui/$(id -u)/com.terryli.claude-tts-companion | head -5`
       m. Verify HTTP health endpoint: `curl -s http://localhost:8780/health | head -1`
       n. Print success message with log path

    3. Create `rollback.sh` (bash, executable) that does (DEP-03):
       a. set -euo pipefail
       b. Print header: "Rolling back to old services..."
       c. Record start time with `date +%s`
       d. Stop unified service: `launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.terryli.claude-tts-companion.plist 2>/dev/null || true`
       e. Wait 2 seconds for clean shutdown
       f. Re-enable old services:
          - `launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.terryli.telegram-bot.plist`
          - `launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.terryli.kokoro-tts-server.plist`
       g. Verify old services running: check `launchctl print gui/$(id -u)/com.terryli.telegram-bot` and `launchctl print gui/$(id -u)/com.terryli.kokoro-tts-server`
       h. Record end time, compute elapsed, print "Rollback completed in {N} seconds"
       i. Print reminder: old services restored, unified binary still at /usr/local/bin

    4. Make both scripts executable: `chmod +x install.sh rollback.sh`

  </action>
  <verify>
    <automated>bash -n plugins/claude-tts-companion/scripts/install.sh && bash -n plugins/claude-tts-companion/scripts/rollback.sh && test -x plugins/claude-tts-companion/scripts/install.sh && test -x plugins/claude-tts-companion/scripts/rollback.sh && grep -q "bootout" plugins/claude-tts-companion/scripts/rollback.sh && grep -q "telegram-bot" plugins/claude-tts-companion/scripts/rollback.sh</automated>
  </verify>
  <acceptance_criteria>
    - Both scripts pass bash -n syntax check
    - Both scripts are executable
    - install.sh builds, strips, copies binary, stops old services, bootstraps new service
    - rollback.sh stops new service, re-enables both old services
    - Old plist files are never deleted (DEP-02)
    - Rollback script prints elapsed time
  </acceptance_criteria>
  <done>install.sh and rollback.sh exist, pass syntax checks, implement full install/rollback lifecycle</done>
</task>

</tasks>

<verification>
1. `plutil -lint plugins/claude-tts-companion/launchd/com.terryli.claude-tts-companion.plist` — zero errors
2. `bash -n plugins/claude-tts-companion/scripts/install.sh` — valid syntax
3. `bash -n plugins/claude-tts-companion/scripts/rollback.sh` — valid syntax
4. `grep -q ".local/share/kokoro/models" plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift` — canonical path
5. `cd plugins/claude-tts-companion && swift build 2>&1 | tail -3` — compiles with updated Config.swift
</verification>

<success_criteria>

- DEP-01: Launchd plist exists with KeepAlive, RunAtLoad, correct binary path, env vars, log paths
- DEP-02: install.sh stops old services but preserves their plist files on disk
- DEP-03: rollback.sh re-enables old services and stops unified binary
- DEP-04: Config.swift kokoroModelPath defaults to ~/.local/share/kokoro/models/kokoro-int8-multi-lang-v1_0
  </success_criteria>

<output>
After completion, create `.planning/phases/10-deployment-extras/10-01-SUMMARY.md`
</output>
