---
phase: 11-notification-formatting
plan: 02
type: execute
wave: 2
depends_on: ["11-01"]
files_modified:
  - plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
autonomous: true
requirements: [FMT-01, FMT-02, FMT-03]
must_haves:
  truths:
    - "Session notification uses renderSessionNotification() for rich HTML header with metadata"
    - "Arc Summary message shows last prompt (condensed if >800 chars) followed by AI narrative"
    - "Tail Brief is sent as a separate silent Telegram message after Arc Summary"
    - "Notification JSON fields (sessionId, cwd, gitBranch, startTime, lastActivity) are parsed and passed through"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift"
      provides: "Updated sendSessionNotification using new formatting engine"
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift"
      provides: "Updated notification handler extracting all JSON fields for rich formatting"
  key_links:
    - from: "TelegramBot.sendSessionNotification"
      to: "TelegramFormatter.renderSessionNotification"
      via: "Builds SessionNotificationData and calls formatter"
      pattern: "renderSessionNotification"
    - from: "TelegramBot.sendSessionNotification"
      to: "TelegramBot.sendSilentMessage"
      via: "Tail Brief sent as separate silent message"
      pattern: "sendSilentMessage.*tail"
    - from: "main.swift notificationWatcher"
      to: "TelegramBot.sendSessionNotification"
      via: "Passes sessionId, cwd, gitBranch, timestamps, turns"
      pattern: "sendSessionNotification"
---

<objective>
Wire the new TelegramFormatter formatting engine into TelegramBot and main.swift so session notifications use rich HTML formatting with metadata header, separate Tail Brief message, and all notification JSON fields.

Purpose: Connect the formatting engine from Plan 01 to the actual bot message sending, completing the notification formatting pipeline.

Output: Updated TelegramBot.swift with rich sendSessionNotification() and separate silent Tail Brief; updated main.swift parsing all notification JSON fields.
</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/11-notification-formatting/11-01-SUMMARY.md
@plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift

<interfaces>
<!-- From Plan 01's TelegramFormatter upgrade -->

From TelegramFormatter.swift (after Plan 01):

```swift
struct SessionNotificationData {
    let sessionId: String
    let cwd: String
    let gitBranch: String?
    let startTime: Date?
    let lastActivity: Date?
    let turnCount: Int
    let lastUserPrompt: String?
    let aiNarrative: String?
    let promptSummary: String?
}

enum TelegramFormatter {
    static func renderSessionNotification(_ data: SessionNotificationData) -> String
    static func escapeHtml(_ text: String) -> String
    static func chunkTelegramHtml(_ text: String, limit: Int = telegramMaxLength) -> [String]
    static func markdownToTelegramHtml(_ markdown: String) -> String
}
```

From SummaryEngine.swift:

```swift
struct SummaryResult {
    let narrative: String
    let promptSummary: String?
    let ttsGreeting: 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)
}
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Update TelegramBot.sendSessionNotification to use rich formatting and separate Tail Brief</name>
  <read_first>
    - plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift (current sendSessionNotification)
    - plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramFormatter.swift (after Plan 01, for SessionNotificationData struct)
    - ~/.claude/automation/claude-telegram-sync/src/claude-sync/notification-watcher.ts (lines 280-470: how arc summary and tail brief are sent as separate messages, tail brief is silent)
  </read_first>
  <files>plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift</files>
  <action>
**1. Expand sendSessionNotification signature (FMT-01, FMT-02, FMT-03)**

Change the signature to accept all metadata fields extracted from the notification JSON:

```swift
func sendSessionNotification(
    sessionId: String,
    turns: [ConversationTurn],
    cwd: String?,
    gitBranch: String?,
    startTime: Date?,
    lastActivity: Date?
) async {
```

**2. Build SessionNotificationData and use renderSessionNotification (FMT-01, FMT-02)**

Replace the manual message building with:

```swift
// Extract last user prompt from turns
let lastPrompt = turns.last(where: { !$0.prompt.isEmpty })?.prompt

// Build notification data
let notifData = SessionNotificationData(
    sessionId: sessionId,
    cwd: cwd ?? "unknown",
    gitBranch: gitBranch,
    startTime: startTime ?? turns.first?.timestamp,
    lastActivity: lastActivity ?? Date(),
    turnCount: turns.count,
    lastUserPrompt: lastPrompt,
    aiNarrative: arc.narrative != "Session completed." ? arc.narrative : nil,
    promptSummary: arc.promptSummary
)

let message = TelegramFormatter.renderSessionNotification(notifData)
await sendMessage(message)
```

**3. Send Tail Brief as separate silent message (FMT-03)**

After sending the Arc Summary message, send the Tail Brief as a separate silent notification:

```swift
// Send Tail Brief as separate silent message (FMT-03)
if !tail.narrative.isEmpty {
    let tbrMessage = "<b>Tail Brief</b>:\n\(TelegramFormatter.escapeHtml(tail.narrative))"
    await sendSilentMessage(tbrMessage)
}
```

**4. Add sendSilentMessage method**

Add a new method that sends with `disableNotification: true`:

```swift
/// Send a silent HTML message (no push notification). Used for Tail Brief.
func sendSilentMessage(_ text: String) async {
    guard let bot = bot else {
        logger.warning("Cannot send silent message: bot not started")
        return
    }
    let chunks = TelegramFormatter.chunkTelegramHtml(text)
    for chunk in chunks {
        do {
            let params = TGSendMessageParams(
                chatId: .chat(chatId),
                text: chunk,
                parseMode: .html,
                linkPreviewOptions: TGLinkPreviewOptions(isDisabled: true),
                disableNotification: true
            )
            try await bot.sendMessage(params: params)
        } catch {
            logger.warning("Failed to send silent HTML, retrying plain: \(error)")
            do {
                let plainParams = TGSendMessageParams(
                    chatId: .chat(chatId),
                    text: TelegramFormatter.stripHtmlTags(chunk),
                    disableNotification: true
                )
                try await bot.sendMessage(params: plainParams)
            } catch {
                logger.error("Failed to send silent plain text: \(error)")
            }
        }
    }
}
```

NOTE: Check if TGSendMessageParams has a `disableNotification` parameter. If the swift-telegram-sdk uses a different name (e.g., `disable_notification` or it may be a property), check the SDK source. The Telegram Bot API field is `disable_notification`. The SDK likely uses Swift naming convention `disableNotification`.

**5. Keep TTS dispatch logic unchanged**

The existing `dispatchTTS` call for Tail Brief stays as-is. Only the message formatting changes.
</action>
<verify>
<automated>cd /Users/terryli/eon/cc-skills/plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria> - sendSessionNotification accepts sessionId, turns, cwd, gitBranch, startTime, lastActivity - Arc Summary uses renderSessionNotification() for rich HTML header - Tail Brief sent as separate silent message via sendSilentMessage() - sendSilentMessage uses disableNotification: true - TTS dispatch for Tail Brief preserved
</acceptance_criteria>
<done>TelegramBot uses rich formatting for Arc Summary and sends Tail Brief as separate silent message.</done>
</task>

<task type="auto">
  <name>Task 2: Update main.swift notification handler to extract all JSON metadata fields</name>
  <read_first>
    - plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift (lines 96-147: notification watcher handler)
    - ~/.claude/automation/claude-telegram-sync/src/claude-sync/notification-watcher.ts (NotificationFile interface: sessionId, cwd, slug, timestamp, transcriptPath)
    - ~/.claude/automation/claude-telegram-sync/src/claude-sync/transcript-parser.ts (SessionSummary: gitBranch comes from transcript events, startTime/lastActivity from first/last events)
  </read_first>
  <files>plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift</files>
  <action>
**1. Extract additional fields from notification JSON (FMT-01)**

The notification JSON written by the stop hook contains `sessionId`, `cwd`, `transcriptPath`, `timestamp`. The git branch and timing info come from parsing the transcript. Update the notification handler (around line 96) to:

```swift
let notificationWatcher = NotificationWatcher { filePath in
    // ... existing file read logic ...

    let sessionId = json["sessionId"] as? String ?? json["session_id"] as? String ?? "unknown"
    let transcriptPath = json["transcriptPath"] as? String ?? json["transcript_path"] as? String
    let cwd = json["cwd"] as? String

    // ... existing logging ...

    Task {
        // ... existing auto-continue logic ...

        // Parse transcript for session notification
        if let tp = transcriptPath {
            let entries = TranscriptParser.parse(filePath: tp)
            let turns = entriesToTurns(entries)

            // Extract git branch from first transcript entry that has it
            // (JSONL events include gitBranch field)
            let gitBranch = extractGitBranch(from: tp)

            // Extract timing from transcript entries
            let startTime = extractFirstTimestamp(entries)
            let lastActivity = extractLastTimestamp(entries)

            if let bot = telegramBot {
                await bot.sendSessionNotification(
                    sessionId: sessionId,
                    turns: turns,
                    cwd: cwd,
                    gitBranch: gitBranch,
                    startTime: startTime,
                    lastActivity: lastActivity
                )
            }
        }
    }
}
```

**2. Add helper functions for git branch and timestamp extraction**

Add these near the `entriesToTurns` helper at the bottom of main.swift:

```swift
/// Extract git branch from JSONL transcript (first event with gitBranch field).
func extractGitBranch(from transcriptPath: String) -> String? {
    guard let data = FileManager.default.contents(atPath: transcriptPath),
          let content = String(data: data, encoding: .utf8) else { return nil }
    // Scan first 20 lines for gitBranch field (usually in early events)
    let lines = content.components(separatedBy: .newlines)
    for line in lines.prefix(20) {
        guard let lineData = line.data(using: .utf8),
              let json = try? JSONSerialization.jsonObject(with: lineData) as? [String: Any],
              let branch = json["gitBranch"] as? String ?? json["git_branch"] as? String else { continue }
        if !branch.isEmpty { return branch }
    }
    return nil
}

/// Extract the first timestamp from parsed transcript entries.
func extractFirstTimestamp(_ entries: [TranscriptEntry]) -> Date? {
    for entry in entries {
        switch entry {
        case .prompt(_, let ts): if let ts = ts { return ts }
        case .response(_, let ts): if let ts = ts { return ts }
        case .toolUse(_, let ts): if let ts = ts { return ts }
        case .toolResult(_, let ts): if let ts = ts { return ts }
        case .unknown: continue
        }
    }
    return nil
}

/// Extract the last timestamp from parsed transcript entries.
func extractLastTimestamp(_ entries: [TranscriptEntry]) -> Date? {
    for entry in entries.reversed() {
        switch entry {
        case .prompt(_, let ts): if let ts = ts { return ts }
        case .response(_, let ts): if let ts = ts { return ts }
        case .toolUse(_, let ts): if let ts = ts { return ts }
        case .toolResult(_, let ts): if let ts = ts { return ts }
        case .unknown: continue
        }
    }
    return nil
}
```

IMPORTANT: The `gitBranch` field is extracted from the raw JSONL (not from TranscriptEntry, which doesn't store it). Read the first 20 lines of the JSONL file looking for the `gitBranch` key. This matches how the legacy TS does it (transcript-parser.ts line 88: `gitBranch: data.gitBranch`).
</action>
<verify>
<automated>cd /Users/terryli/eon/cc-skills/plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria> - Notification handler extracts sessionId, cwd, transcriptPath (existing) plus gitBranch, startTime, lastActivity (new) - extractGitBranch reads first 20 JSONL lines looking for gitBranch field - extractFirstTimestamp and extractLastTimestamp scan entries for Date values - sendSessionNotification called with all 6 parameters - swift build succeeds with zero errors
</acceptance_criteria>
<done>main.swift passes all metadata to sendSessionNotification. swift build succeeds. The full notification pipeline is wired: JSON file -> metadata extraction -> rich HTML formatting -> separate Arc Summary and Tail Brief messages.</done>
</task>

</tasks>

<verification>
```bash
cd plugins/claude-tts-companion && swift build 2>&1 | tail -3
```
Build must succeed with zero errors. Verify the integration:
```bash
grep -n 'sendSessionNotification\|sendSilentMessage\|extractGitBranch\|renderSessionNotification' plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
```
</verification>

<success_criteria>
Full notification pipeline is wired: notification JSON -> metadata extraction -> rich HTML formatting -> separate Arc Summary and silent Tail Brief messages. swift build succeeds.
</success_criteria>

<output>
After completion, create `.planning/phases/11-notification-formatting/11-02-SUMMARY.md`
</output>
