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

must_haves:
  truths:
    - "Arc Summary prompt matches legacy TypeScript verbatim including em dashes and exact wording"
    - "Tail Brief prompt matches legacy verbatim with arrow character and exact budgets 3000/8000"
    - "Single-exchange prompt matches legacy verbatim with ||| delimiter parsing"
    - "Prompts exceeding 800 chars are condensed to under 150 words via MiniMax"
    - "Arc Summary post-processing strips greeting, normalizes paragraphs, forces transition newlines"
    - "Tail Brief prior context uses arrow character and 200/300 char per-turn limits"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift"
      provides: "Exact legacy prompts, summarizePromptForDisplay, correct budgets and post-processing"
      exports:
        [
          "arcSummary",
          "tailBrief",
          "singleTurnSummary",
          "summarizePromptForDisplay",
        ]
  key_links:
    - from: "SummaryEngine.swift"
      to: "MiniMaxClient.swift"
      via: "client.query(prompt:systemPrompt:maxTokens:)"
      pattern: "client\\.query"
---

<objective>
Rewrite SummaryEngine.swift with exact legacy prompt templates from TypeScript summarizer.ts, matching verbatim text including em dashes, arrow characters, and exact char budgets. Add missing summarizePromptForDisplay method.

Purpose: The AI summarizer quality depends on the exact prompt wording tuned over months in the legacy pipeline. Any paraphrasing degrades output quality.
Output: SummaryEngine.swift with legacy-identical prompts and new prompt condensing method.
</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/12-ai-summary-prompts/12-01-SUMMARY.md
@plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/MiniMaxClient.swift

<interfaces>
From MiniMaxClient.swift:
```swift
struct MiniMaxResult {
    let text: String
    let durationMs: Int
    let outputTokens: Int?
}

func query(prompt: String, systemPrompt: String, maxTokens: Int) async throws -> MiniMaxResult

````

From TranscriptParser.swift (after Plan 01):
```swift
static func isRealPrompt(_ text: String) -> Bool
static func getLastUserPrompt(from prompts: [String]) -> String
````

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Rewrite SummaryEngine with exact legacy prompts and add prompt condensing</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>
Rewrite SummaryEngine.swift. Keep the existing structure (ConversationTurn, SummaryResult, SummaryEngine class, MiniMaxClient dependency, formatTimeAgo, formatProjectName) but replace the prompt text in all three methods to match legacy TypeScript EXACTLY. Also add the missing `summarizePromptForDisplay` method.

**CRITICAL**: The prompt strings below are the EXACT legacy text. Use Unicode em dash (U+2014) where shown as `—`, and Unicode right arrow (U+2192) where shown as `→`. Do NOT substitute with `--` or `->`.

## 1. singleTurnSummary — PROMPT-03

The current Swift is already very close. Verify these exact strings are used:

**User prompt** (must match legacy summarizer.ts lines 186-213 exactly):

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

Part 1 \u{2014} PROMPT SUMMARY (under 30 words):
Summarize ONLY the "Most recent request" below \u{2014} 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 \u{2014} RESPONSE SUMMARY (under 50 words):
Summarize what was accomplished. Focus on outcomes and key actions taken.

Output format \u{2014} 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" \u{2014} just describe the request directly.

Most recent request:
\"""
{promptText}
\"""

Final response:
\"""
{responseText}
\"""

Summary:
```

**System prompt**: `"You convert text to natural spoken language. ONLY process the text explicitly provided by the user between triple-quote delimiters."`

**maxTokens**: 2048

Verify the current Swift uses `\` (backslash) before `"""` in string interpolation to produce literal triple quotes. The legacy TS uses `${promptText}` inside `"""..."""` — in Swift multiline strings, use `\"""` to produce literal triple quotes.

Char budgets: `strippedPrompt.prefix(2000)` for prompt, `response.prefix(10000)` for response. These already match.

## 2. arcSummary — PROMPT-01

**User prompt** (must match legacy summarizer.ts lines 334-353 exactly):

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

Rules:
- Do NOT include any greeting or project name \u{2014} those are added separately
- Start directly with the first chronological step using transition words: "First,", "Then,", "Next,", "After that,", "Finally,"
- Cover EVERY turn \u{2014} 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 \u{2014} 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 \u{2014} it was NOT cut off or incomplete. Summarize based on what is shown

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

Narrative:
```

Note the last rule: legacy says "Summarize based on what is shown" — the current Swift says "it was NOT cut off or incomplete" and stops there. Add the trailing clause.

**System prompt**: `"You convert coding session transcripts into natural spoken summaries. ONLY process text between triple-quote delimiters."`

**maxTokens**: 4096

**Char budgets** (must match legacy):

- maxPromptChars = 2000
- maxResponseChars = 4000
- maxToolResultChars = 1500
- maxTranscriptChars = 102400

These already match. Verify they are present.

**Post-processing** (must match legacy summarizer.ts lines 369-374):

1. Strip greeting: `result.text.replacingOccurrences(of: "^Hi Terry[^:]*:\\s*", with: "", options: .regularExpression)`
2. Collapse double newlines: `.replacingOccurrences(of: "\\n{2,}", with: "\n", options: .regularExpression)`
3. Force newline before transitions: `.replacingOccurrences(of: "(?<!\\n)((?:Then|Next|After that|Finally|Last),)", with: "\n$1", options: .regularExpression)`

These already match. Verify they are present.

## 3. tailBrief — PROMPT-02

**User prompt** (must match legacy summarizer.ts lines 460-478 exactly):

```
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}
Final turn \u{2014} USER ASKED:
\"""
{lastPrompt}
\"""

Final turn \u{2014} 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 \u{2014} not robotic, not corporate. Describe what happened like you're telling a colleague."`

Note the em dash in the system prompt too.

**maxTokens**: 2048

**Char budgets** (must match legacy):

- maxLastPromptChars = 3000
- maxLastResponseChars = 8000
- Prior context: 200 chars per prompt, 300 chars per response, 4000 total cap

**Prior context line format** (must use Unicode right arrow `→`, not `->` which current Swift uses):

```swift
"Turn \(i + 1): User asked: \(p)\(pSuffix) \u{2192} Outcome: \(r)\(rSuffix)"
```

The current Swift uses `->` — change to `\u{2192}` (→) to match legacy TS line 446.

**Post-processing**: Strip greeting only (same as current).

## 4. summarizePromptForDisplay — PROMPT-04 (NEW METHOD)

Add this new public method to SummaryEngine, matching legacy summarizer.ts lines 522-551:

```swift
/// Condense a long user prompt for Telegram display (not TTS).
/// Returns original text if short enough or if MiniMax fails.
func summarizePromptForDisplay(
    rawPrompt: String,
    maxDisplayChars: Int = 800
) async -> String {
    if rawPrompt.count <= maxDisplayChars { return rawPrompt }

    // Circuit breaker check — fall back to truncation
    if client.circuitBreaker.isOpen {
        return String(rawPrompt.prefix(maxDisplayChars)) + "..."
    }

    do {
        let result = try await client.query(
            prompt: """
                Condense this user request into under 150 words for display. \
                Preserve the key intent and any specific technical details. \
                No greeting, no filler. Write in second person ("You asked to...").

                User request:
                \"""
                \(String(rawPrompt.prefix(3000)))
                \"""

                Condensed:
                """,
            systemPrompt: "You condense user requests into concise display summaries. Preserve intent and key details.",
            maxTokens: 512
        )

        if !result.text.isEmpty {
            logger.info("Prompt condensed: \(rawPrompt.count) -> \(result.text.count) chars")
            return result.text
        }
    } catch {
        logger.warning("Prompt condensing failed: \(error)")
    }

    // Fallback to truncation
    return String(rawPrompt.prefix(maxDisplayChars)) + "..."
}
```

**Prompt text** (must match legacy summarizer.ts lines 532-539 exactly):

```
Condense this user request into under 150 words for display. Preserve the key intent and any specific technical details. No greeting, no filler. Write in second person ("You asked to...").

User request:
"""
{rawPrompt sliced to 3000}
"""

Condensed:
```

**System prompt**: `"You condense user requests into concise display summaries. Preserve intent and key details."`

**maxTokens**: 512

## Summary of changes needed vs current Swift

1. **singleTurnSummary**: Replace `--` with em dash `\u{2014}` in prompt text (6 occurrences). Current Swift uses `\\` line continuation which may have `--` instead of `—`.
2. **arcSummary**: Replace `--` with em dash, add "Summarize based on what is shown" to last rule.
3. **tailBrief**: Replace `--` with em dash in prompt and system prompt, replace `->` with `→` in prior context lines.
4. **summarizePromptForDisplay**: New method — add entirely.
   </action>

<acceptance_criteria> - Arc Summary prompt uses em dashes (—) not double hyphens (--) - Arc Summary last rule ends with "Summarize based on what is shown" - Tail Brief prompt uses em dashes in headers and system prompt - Tail Brief prior context uses → (U+2192) not -> - Tail Brief char budgets: 3000 prompt, 8000 response, 200/300 per prior turn, 4000 prior cap - Arc Summary char budgets: 2000 prompt, 4000 response, 1500 tool results, 102400 transcript - Single-exchange char budgets: 2000 prompt, 10000 response - summarizePromptForDisplay exists with 800 char threshold and 150 word condensing - summarizePromptForDisplay maxTokens is 512 - All prompt text matches legacy TS verbatim (em dashes, arrows, exact wording)
</acceptance_criteria>
<verify>
<automated>cd /Users/terryli/eon/cc-skills/plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
</verify>
<done>All four summary methods use exact legacy prompt text with correct Unicode characters, char budgets match legacy, summarizePromptForDisplay added, swift build succeeds</done>
</task>

</tasks>

<verification>
- `swift build` succeeds
- `grep -c '\\u{2014}' SummaryEngine.swift` or `grep -c '—' SummaryEngine.swift` shows em dashes are present
- `grep '→' SummaryEngine.swift` confirms right arrow in tail brief prior context
- `grep 'summarizePromptForDisplay' SummaryEngine.swift` confirms new method exists
- `grep 'Summarize based on what is shown' SummaryEngine.swift` confirms arc summary last rule
- `grep '150 words' SummaryEngine.swift` confirms prompt condensing instruction
</verification>

<success_criteria>
All four prompt templates (Arc Summary, Tail Brief, Single-exchange, Prompt Condensing) match legacy TypeScript verbatim. Em dashes, arrow characters, char budgets, system prompts, maxTokens, and post-processing are all identical to the legacy pipeline. Build compiles cleanly.
</success_criteria>

<output>
After completion, create `.planning/phases/12-ai-summary-prompts/12-02-SUMMARY.md`
</output>
