---
phase: 12-ai-summary-prompts
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/claude-tts-companion/Sources/claude-tts-companion/TranscriptParser.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
autonomous: true
requirements: [PROMPT-05]

must_haves:
  truths:
    - "System-injected noise patterns are stripped from transcripts before summarization"
    - "Noise-only prompts are excluded from conversation turns"
    - "Skill expansions appended after user prompts are stripped"
    - "Turn extraction finds the LONGEST assistant response per turn (not first)"
    - "Tool counts are aggregated per turn as 'Edit x3, Bash x2' format"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/TranscriptParser.swift"
      provides: "Noise filtering, isRealPrompt, isSystemNoise, stripSkillExpansion, improved entriesToTurns"
      contains: "NOISE_PATTERNS"
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift"
      provides: "Updated to use TranscriptParser.entriesToTurns instead of local function"
  key_links:
    - from: "TranscriptParser.swift"
      to: "SummaryEngine.swift"
      via: "ConversationTurn struct and filtered turns"
      pattern: "entriesToTurns"
---

<objective>
Port legacy noise filtering and turn extraction from TypeScript transcript-parser.ts into Swift TranscriptParser.swift. This ensures the summarizer receives clean, noise-free conversation turns with proper tool aggregation.

Purpose: Summarization quality depends on clean input. Without noise filtering, system-injected prompts (task notifications, command expansions, statusline pastes) pollute the transcript and cause hallucinated summaries.
Output: TranscriptParser.swift with noise filtering + improved entriesToTurns; main.swift updated to use it.
</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/TranscriptParser.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift

<interfaces>
<!-- ConversationTurn is defined in SummaryEngine.swift and consumed by arcSummary/tailBrief/singleTurnSummary -->
From SummaryEngine.swift:
```swift
struct ConversationTurn {
    let prompt: String
    let response: String
    let timestamp: Date?
    let toolSummary: String?
    let toolResults: String?
}
```

From TranscriptParser.swift:

```swift
enum TranscriptEntry: Sendable {
    case prompt(text: String, timestamp: Date?)
    case response(text: String, timestamp: Date?)
    case toolUse(name: String, timestamp: Date?)
    case toolResult(content: String, timestamp: Date?)
    case unknown(type: String)
}
```

From TelegramFormatter.swift (already ported in Phase 11):

```swift
static func stripMetaTags(_ text: String) -> String
static func stripSkillExpansion(_ text: String) -> String
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add noise filtering and improved turn extraction to TranscriptParser</name>
  <files>plugins/claude-tts-companion/Sources/claude-tts-companion/TranscriptParser.swift, plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift</files>
  <read_first>
    - plugins/claude-tts-companion/Sources/claude-tts-companion/TranscriptParser.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift (lines 227-272, entriesToTurns function)
    - plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramFormatter.swift (lines 188-220, stripMetaTags and stripSkillExpansion)
    - plugins/claude-tts-companion/Sources/claude-tts-companion/SummaryEngine.swift (lines 1-13, ConversationTurn struct)
  </read_first>
  <action>
**In TranscriptParser.swift**, add the following after the existing `TranscriptSummary` struct and before `enum TranscriptParser`:

1. Add the exact legacy NOISE_PATTERNS array (from TypeScript transcript-parser.ts lines 137-157). These are string-contains checks:

```swift
/// System-injected content patterns -- never real user prompts.
/// Ported verbatim from legacy TypeScript transcript-parser.ts.
private let noisePatterns: [String] = [
    "<command-name>",
    "<local-command",
    "<local-command-caveat>",
    "<task-notification>",
    "<system-reminder>",
    "<bash-stdout>",
    "<bash-stderr>",
    "<bash-input>",
    "<summary>",
    "<task-id>",
    "<status>",
    "<output-file>",
    "<teammate-message",
    "This session is being continued from a previous conversation",
    "Stop hook feedback",
    "fd out from online",
    "Implement the following plan",
    "Tool loaded",
    "[Request interrupted by user",
]
```

Note: These must be defined as a stored property inside `TranscriptParser` enum (use `static let`).

1. Add regex patterns for noise that can't be matched by simple string inclusion (from TS lines 160-163):

```swift
/// Regex noise patterns (statusline pastes, interrupted requests).
private static let noiseRegexPatterns: [String] = [
    "M:\\d+ D:\\d+ S:\\d+",        // statusline paste (e.g. "M:4 D:0 S:0 U:6")
    "^\\[Request interrupted",      // interrupted requests (bracket form)
]
```

1. Add `isSystemNoise(_ content: String) -> Bool` static function matching legacy TS `isSystemNoise`:

- Returns true if `content` contains any item in `noisePatterns`
- OR if any regex in `noiseRegexPatterns` matches `content`

1. Add `isRealPrompt(_ text: String) -> Bool` static function matching legacy TS `isRealPrompt` from summarizer.ts line 49-54:

- Returns `text.trimmingCharacters(in: .whitespacesAndNewlines).count > 10 && !isSystemNoise(text)`

1. Add `stripSkillExpansion(_ text: String) -> String` static function matching legacy TS (transcript-parser.ts lines 123-131):

- Find the first `\n\n` in text
- If found, check if the text after the double newline starts with a pattern: `^(\*\*[A-Z]|#{1,4}\s|---\s*\n|>\s|-\s\[|TRIGGERS\b)`
- If it matches, return everything before the `\n\n` (trimmed)
- Otherwise return the original text
- NOTE: TelegramFormatter already has a `stripSkillExpansion` but it lives in TelegramFormatter which is a display concern. TranscriptParser needs its own copy for transcript-level filtering (matching the legacy TS architecture where transcript-parser.ts had its own copy).

1. **Replace the `entriesToTurns` function** currently in main.swift (lines 227-272) with a new static method `TranscriptParser.entriesToTurns(_ entries: [TranscriptEntry]) -> [ConversationTurn]` that matches the legacy TS `extractSessionSummary` turn-building logic (transcript-parser.ts lines 297-372):

Key differences from current implementation:

- **Filter noise**: Skip entries where `isSystemNoise(text)` or `text.count <= 10`
- **Strip skill expansions**: Apply `stripSkillExpansion()` to prompt text
- **Find LONGEST response**: For each user prompt, scan forward collecting all assistant responses until the next real user prompt. Keep the LONGEST text response (not the first one). Legacy TS: "Preferring the longest response ensures we capture the actual answer" (line 258).
- **Aggregate tool counts**: Build a `[String: Int]` dictionary of tool name -> count. Format as "Edit x3, Bash x2, Read x5" (count > 1 gets "x{count}" suffix).
- **Collect tool results**: Capture substantial tool results (content.count > 50), truncated to 300 chars each, capped at 3000 total chars. Join with "\n---\n".
- **Skip empty turns**: If bestResponse is empty AND no tools were used, skip the turn entirely (line 363).

1. **In main.swift**, delete the local `entriesToTurns` function (lines 227-272) and replace all call sites with `TranscriptParser.entriesToTurns(entries)`.

2. Also add a `getLastUserPrompt(from prompts: [String]) -> String` static method matching legacy TS `getLastUserPrompt` (summarizer.ts lines 556-568):

- Walk backwards through the array
- Return the first prompt where `isRealPrompt(p)` is true
- Truncate to 1000 chars with "..." suffix if longer
- Return "(no prompts)" if none found
  </action>
  <acceptance_criteria>
  - All 18 noise patterns from legacy TS present in noisePatterns array
  - Both regex patterns (statusline, interrupted) present
  - isSystemNoise checks both string patterns and regex patterns
  - entriesToTurns filters noise, finds longest response, aggregates tools
  - main.swift no longer has local entriesToTurns function
  - stripSkillExpansion matches legacy regex pattern exactly
    </acceptance_criteria>
    <verify>
    <automated>cd /Users/terryli/eon/cc-skills/plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
    </verify>
    <done>TranscriptParser has full noise filtering matching legacy TS, entriesToTurns produces clean turns with tool aggregation and longest-response selection, swift build succeeds</done>
    </task>

</tasks>

<verification>
- `swift build` succeeds with no errors
- TranscriptParser.swift contains all 18 noise patterns from legacy TS
- TranscriptParser.entriesToTurns exists as a static method
- main.swift no longer contains a local entriesToTurns function
- grep confirms: `grep -c "noisePatterns" TranscriptParser.swift` returns at least 1
</verification>

<success_criteria>
Noise filtering and turn extraction match legacy TypeScript behavior. System-injected prompts are excluded, skill expansions stripped, longest response selected per turn, tool counts aggregated. Build compiles cleanly.
</success_criteria>

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