---
phase: 04-ai-summaries
plan: 02
type: execute
wave: 2
depends_on: [04-01]
files_modified:
  - plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift
autonomous: true
requirements: [SUM-01, SUM-02, SUM-03]

must_haves:
  truths:
    - "Arc Summary produces a chronological full-session narrative from conversation turns"
    - "Tail Brief produces an end-weighted narrative with 20% context and 80% final turn detail"
    - "Single-turn summary produces a 'you prompted me X ago to...' narrative from the last exchange"
    - "All three summary types share the same MiniMaxClient and circuit breaker"
    - "Empty turn arrays return a safe fallback string without calling the API"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift"
      provides: "Three summary methods with MiniMax prompt templates"
      exports: ["SummaryEngine", "SummaryResult", "ConversationTurn"]
      min_lines: 150
  key_links:
    - from: "SummaryEngine.swift"
      to: "MiniMaxClient.swift"
      via: "client.query() async calls"
      pattern: "client\\.query"
    - from: "SummaryEngine.swift"
      to: "Config.swift"
      via: "Config.summaryMaxTokens for token limits"
      pattern: "Config\\.summary"
---

<objective>
Implement the three summary types (Arc Summary, Tail Brief, Single-turn) as prompt templates that call MiniMaxClient.

Purpose: Provide the actual session narrative generation that the Telegram bot (Phase 5) will invoke after sessions end.
Output: SummaryEngine.swift with arcSummary(), tailBrief(), singleTurnSummary() methods
</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/04-ai-summaries/04-01-SUMMARY.md

<interfaces>
<!-- From Plan 01: MiniMaxClient API -->
```swift
struct MiniMaxResult {
    let text: String
    let durationMs: Int
    let outputTokens: Int?
}

final class MiniMaxClient: @unchecked Sendable {
let circuitBreaker: CircuitBreaker
func query(prompt: String, systemPrompt: String, maxTokens: Int) async throws -> MiniMaxResult
}

````

<!-- From Plan 01: CircuitBreaker -->
```swift
final class CircuitBreaker: @unchecked Sendable {
    var isOpen: Bool
    func recordFailure()
    func recordSuccess()
}
````

<!-- From Plan 01: SummaryError -->

```swift
enum SummaryError: Error, CustomStringConvertible {
    case circuitBreakerOpen
    case missingAPIKey
    case apiError(statusCode: Int, body: String)
    case emptyResponse
    case decodingError(String)
}
```

<!-- Reference: TypeScript prompt templates (port to Swift) -->

From existing TS summarizer.ts:

Arc Summary system prompt: "You convert coding session transcripts into natural spoken summaries. ONLY process text between triple-quote delimiters."
Arc Summary rules: Cover EVERY turn, use transition words (First/Then/Next/Finally), each step its own paragraph, under 200 words, no code/paths/markdown, natural spoken language.

Tail Brief system prompt: "You narrate coding sessions for spoken audio. Natural storytelling voice."
Tail Brief rules: ~20% context + ~80% final turn detail, past tense, under 200 words, no greeting/project name.

Single-turn: two-part (prompt summary ||| response summary), prompt part starts with infinitive verb, response focuses on outcomes.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create SummaryEngine with data types and single-turn summary</name>
  <files>
    plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/claude-tts-companion/MiniMaxClient.swift,
    plugins/claude-tts-companion/Sources/claude-tts-companion/CircuitBreaker.swift,
    plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
  </read_first>
  <action>
Create SummaryEngine.swift with shared types and the single-turn summary method (SUM-03).

1. Define ConversationTurn struct:

   ```swift
   struct ConversationTurn {
       let prompt: String
       let response: String
       let timestamp: Date?
       let toolSummary: String?  // e.g. "Bash(3), Read(5), Write(2)"
       let toolResults: String?  // Key tool outputs for context
   }
   ```

2. Define SummaryResult struct:

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

3. Create `final class SummaryEngine: @unchecked Sendable`:
   - `private let client: MiniMaxClient`
   - `private let logger = Logger(label: "summary-engine")`
   - `init(client: MiniMaxClient = MiniMaxClient())`

4. Add helper `formatTimeAgo(_ date: Date) -> String`:
   - Returns conversational relative time: "just now" (< 5s), "N seconds ago" (< 60s), "N minutes ago" (< 60min), "N hours and M minutes ago" (>= 60min)
   - Use `Date().timeIntervalSince(date)` for elapsed calculation

5. Add helper `formatProjectName(_ cwd: String?) -> String`:
   - Extracts last path component from cwd
   - Prefixes "." dirs with "Dot " (e.g. ".claude" -> "Dot claude")
   - Replaces hyphens/underscores with spaces
   - Returns "unknown project" for nil

6. Implement `func singleTurnSummary(prompt: String, response: String, lastActivityTime: Date?, cwd: String?) async -> SummaryResult`:
   - Strip code-fenced blocks from prompt: `prompt.replacingOccurrences(of: "```[\\s\\S]*?```", with: "[quoted code block]", options: .regularExpression)`
   - Truncate prompt to 2000 chars, response to 10000 chars
   - Compute timeAgo from lastActivityTime (or "a while ago")
   - Compute ttsGreeting: `"Hi Terry, you were working in \(projectName)."`
   - Build prompt (port from TypeScript):

     ```
     Generate a two-part spoken summary for text-to-speech. Do NOT include any greeting or intro — that is added separately.

     Part 1 — PROMPT SUMMARY (under 30 words):
     Summarize ONLY the "Most recent request" below — ignore any earlier conversation history. This will be preceded by "you prompted me to", so start with an infinitive verb phrase (e.g. "get precise timestamps", "lower the reading speed", "add forensic logging").

     Part 2 — RESPONSE SUMMARY (under 50 words):
     Summarize what was accomplished. Focus on outcomes and key actions taken.

     Output format — produce EXACTLY this (no extra text):
     [prompt summary] ||| [response summary]

     The ||| delimiter is mandatory. It separates the two parts.

     Rules:
     - Never mention "Claude Code", "Claude", "Anthropic", or "the assistant".
     - Use natural spoken language. No code, file paths, markdown, or technical symbols.
     - Do NOT start with "You asked" or "The user" — just describe the request directly.

     Most recent request:
     """
     {truncatedPrompt}
     """

     Final response:
     """
     {truncatedResponse}
     """

     Summary:
     ```

   - System prompt: `"You convert text to natural spoken language. ONLY process the text explicitly provided by the user between triple-quote delimiters."`
   - Call `try await client.query(prompt:systemPrompt:maxTokens: 2048)`
   - Parse response: split on "|||", if 2+ parts: narrative = "You prompted me {timeAgo} to {part0}. Here's my response to you in summary: {part1}"
   - Fallback if no delimiter: narrative = "You prompted me {timeAgo} to {text}"
   - On any error (including circuit breaker): return `SummaryResult(narrative: "Session completed.", promptSummary: nil, ttsGreeting: nil)`
   - Log success/failure with model, duration, text length
     </action>

  <verify>
    <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - SummaryEngine.swift exists with `struct ConversationTurn`
    - SummaryEngine.swift contains `struct SummaryResult`
    - SummaryEngine.swift contains `class SummaryEngine`
    - SummaryEngine.swift contains `func singleTurnSummary`
    - SummaryEngine.swift contains `func formatTimeAgo`
    - SummaryEngine.swift contains the `|||` delimiter parsing logic
    - SummaryEngine.swift contains `You prompted me`
    - SummaryEngine.swift contains `client.query`
    - `swift build` succeeds with zero errors
  </acceptance_criteria>
  <done>SummaryEngine exists with ConversationTurn/SummaryResult types, singleTurnSummary generates "you prompted me X ago to..." narratives via MiniMax, error cases return safe fallback</done>
</task>

<task type="auto">
  <name>Task 2: Add Arc Summary and Tail Brief methods to SummaryEngine</name>
  <files>
    plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift,
    plugins/claude-tts-companion/Sources/claude-tts-companion/MiniMaxClient.swift
  </read_first>
  <action>
Add arcSummary() and tailBrief() methods to the existing SummaryEngine class.

1. Implement `func arcSummary(turns: [ConversationTurn], cwd: String?) async -> SummaryResult` (SUM-01):
   - Return `SummaryResult(narrative: "Session completed.", ...)` if turns is empty
   - If turns.count <= 2, delegate to singleTurnSummary using the last turn (MiniMax hallucinates on single-turn arc mode)
   - Build transcript: for each turn, format as "=== Turn N ===\nUser request:\n{prompt}\n\nOutcome:\n{response}{tools}{toolResults}"
   - Truncate per turn: prompt to 2000 chars, response to 4000 chars, tool results to 1500 chars
   - Cap total transcript at 102400 chars (append "[remaining turns omitted for length]" if exceeded)
   - Compute ttsGreeting: `"Hi Terry, in this session in \(projectName):"`
   - Build prompt:

     ```
     Summarize this entire coding session as a spoken narrative for text-to-speech. The session had {N} turns.

     Rules:
     - Do NOT include any greeting or project name — those are added separately
     - Start directly with the first chronological step using transition words: "First,", "Then,", "Next,", "After that,", "Finally,"
     - Cover EVERY turn — do not skip or merge turns. Each user request should appear in the narrative
     - IMPORTANT: Each step MUST be its own paragraph, separated by exactly one newline. Never combine multiple steps into one paragraph
     - Focus on OUTCOMES and FINAL ACTIONS — what was actually done, not what was considered
     - Keep it under 200 words total
     - No code, file paths, markdown, or technical symbols
     - Never mention "Claude", "the assistant", or "AI"
     - Use natural spoken language
     - Text marked [truncated] means the full response was too long to include here — it was NOT cut off or incomplete

     Session transcript:
     """
     {transcript}
     """

     Narrative:
     ```

   - System prompt: `"You convert coding session transcripts into natural spoken summaries. ONLY process text between triple-quote delimiters."`
   - Call with maxTokens: 4096 (higher than single-turn to avoid cutoff on large sessions)
   - Post-process: strip any "Hi Terry..." prefix the model may include, normalize double newlines to single, force newline before transition words (Then/Next/After that/Finally)
   - On error: return fallback `SummaryResult(narrative: "Session completed.", ...)`

2. Implement `func tailBrief(turns: [ConversationTurn], cwd: String?) async -> SummaryResult` (SUM-02):
   - Return `SummaryResult(narrative: "", ...)` if turns is empty
   - Extract last turn with generous limits: prompt to 3000 chars, response to 8000 chars
   - Build compressed prior context (turns before last): 1 line per turn, prompt to 200 chars, response to 300 chars
   - Cap prior context at 4000 chars total
   - Build prompt:

     ```
     Narrate this coding session for text-to-speech.

     STRUCTURE:
     - CONTEXT (1-2 sentences): Quick catch-up on what the session was about before the final turn.
     - LATEST (the bulk): What the user wanted in their final request, what specific changes were made, and how the outcome turned out. Be thorough and specific.
     {priorContext if turns > 1}
     Final turn — USER ASKED:
     """
     {lastPrompt}
     """

     Final turn — WHAT WAS DONE:
     """
     {lastResponse}
     """

     Rules: No greeting/project name (added separately). No code/paths/markdown. Never say "Claude"/"AI"/"assistant". Past tense. Under 200 words. ~20% context, ~80% final turn detail.

     Narrative:
     ```

   - System prompt: `"You narrate coding sessions for spoken audio. Natural storytelling voice — not robotic, not corporate. Describe what happened like you're telling a colleague."`
   - Call with maxTokens: 2048
   - Post-process: strip any "Hi Terry..." prefix
   - On error: return `SummaryResult(narrative: "", ...)`
     </action>

  <verify>
    <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - SummaryEngine.swift contains `func arcSummary(turns:`
    - SummaryEngine.swift contains `func tailBrief(turns:`
    - SummaryEngine.swift contains `=== Turn` for arc transcript formatting
    - SummaryEngine.swift contains `Cover EVERY turn` in arc prompt
    - SummaryEngine.swift contains `20%` or `80%` in tail brief prompt
    - SummaryEngine.swift contains `maxTokens: 4096` for arc summary
    - SummaryEngine.swift contains `maxTokens: 2048` for tail brief
    - SummaryEngine.swift contains fallback `Session completed.` for arc
    - SummaryEngine.swift has `turns.count <= 2` delegation check
    - `swift build` succeeds with zero errors
  </acceptance_criteria>
  <done>SummaryEngine has all three summary types: arcSummary covers full session chronologically, tailBrief weights 80% toward final turn, singleTurnSummary handles single exchanges. All share circuit breaker via MiniMaxClient. Project compiles.</done>
</task>

</tasks>

<verification>
- `swift build` succeeds in plugins/claude-tts-companion
- SummaryEngine has arcSummary(), tailBrief(), singleTurnSummary() methods
- All three methods use MiniMaxClient.query() for API calls
- Arc Summary formats turns chronologically with transition words
- Tail Brief weights 20% context / 80% final turn
- Single-turn produces "you prompted me X ago to..." pattern
- Empty/short inputs return safe fallbacks without API calls
</verification>

<success_criteria>

- Project compiles with `swift build`
- Three distinct prompt templates matching TypeScript originals
- Arc Summary handles multi-turn transcripts with truncation
- Tail Brief compresses prior turns and expands final turn
- Single-turn parses ||| delimiter for two-part response
- All methods gracefully handle errors and empty inputs
  </success_criteria>

<output>
After completion, create `.planning/phases/04-ai-summaries/04-02-SUMMARY.md`
</output>
