---
phase: 07-file-watching-auto-continue
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/claude-tts-companion/Sources/claude-tts-companion/FileWatcher.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
autonomous: true
requirements: [WATCH-01, WATCH-02, WATCH-03, WATCH-04]

must_haves:
  truths:
    - "New .json files in the notification directory are detected and a callback fires"
    - "Growing JSONL transcript files are tailed via offset tracking, yielding only new lines"
    - "DispatchSource watchers survive beyond the setup scope (strong references)"
    - "File detection latency is under 100ms from write to callback invocation"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/FileWatcher.swift"
      provides: "NotificationWatcher and JSONLTailer using DispatchSource"
      exports: ["NotificationWatcher", "JSONLTailer"]
      min_lines: 120
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift"
      provides: "notificationDir and transcriptDir path constants"
      contains: "notificationDir"
  key_links:
    - from: "FileWatcher.swift (NotificationWatcher)"
      to: "DispatchSource.makeFileSystemObjectSource(.write)"
      via: "Directory file descriptor watching"
      pattern: "DispatchSource\\.makeFileSystemObjectSource"
    - from: "FileWatcher.swift (JSONLTailer)"
      to: "FileHandle.seekToEndOfFile / readDataToEndOfFile"
      via: "Offset-based read of new bytes"
      pattern: "seek|readData"
---

<objective>
Create the file watching subsystem: a NotificationWatcher that detects new .json files in the Claude Code notification directory, and a JSONLTailer that reads new bytes from growing transcript files using offset tracking.

Purpose: These are the event sources that drive session notifications and auto-continue evaluation. Without file watching, the binary cannot react to Claude Code session events.
Output: FileWatcher.swift with NotificationWatcher and JSONLTailer classes; updated Config.swift with directory paths.
</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/Sources/claude-tts-companion/main.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/TranscriptParser.swift

<interfaces>
<!-- Key types and contracts the executor needs -->

From Config.swift:

```swift
enum Config {
    static let appName = "claude-tts-companion"
    // Executor will ADD:
    // static let notificationDir: String  -- ~/.claude/notifications/
    // static let transcriptBaseDir: String -- ~/.claude/projects/
}
```

From TranscriptParser.swift:

```swift
enum TranscriptParser {
    static func parse(content: String) -> [TranscriptEntry]
    static func summarize(entries: [TranscriptEntry]) -> TranscriptSummary
}
```

From main.swift (ARC pattern for DispatchSource):

```swift
// Existing pattern -- store strong reference to prevent ARC deallocation
nonisolated(unsafe) var keepAlive: (any DispatchSourceSignal)? = sigSource
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add notification and transcript directory paths to Config.swift</name>
  <files>plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift</files>
  <read_first>
    - plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
  </read_first>
  <action>
Add a `// MARK: - File Watching` section to Config.swift with these constants:

1. `static let notificationDir: String` -- defaults to `~/.claude/notifications/` with `CLAUDE_NOTIFICATION_DIR` env override. Use the same `ProcessInfo.processInfo.environment["HOME"]` pattern already in Config.
2. `static let transcriptBaseDir: String` -- defaults to `~/.claude/projects/` with `CLAUDE_PROJECTS_DIR` env override. This is the root under which `{hash}/sessions/{id}/transcript.jsonl` files live.
3. `static let fileWatcherLatencyTarget: TimeInterval = 0.1` -- 100ms target for documentation/assertions.

Follow the existing pattern of lazy static lets with env var override (see `sherpaOnnxPath`, `kokoroModelPath`).
</action>
<verify>
<automated>cd /Users/terryli/eon/cc-skills/plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>Config.swift has notificationDir and transcriptBaseDir with env var overrides. swift build succeeds.</acceptance_criteria>
<done>Config.notificationDir and Config.transcriptBaseDir are accessible from other files. Build passes.</done>
</task>

<task type="auto">
  <name>Task 2: Create FileWatcher.swift with NotificationWatcher and JSONLTailer</name>
  <files>plugins/claude-tts-companion/Sources/claude-tts-companion/FileWatcher.swift</files>
  <read_first>
    - plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/main.swift
    - plugins/claude-tts-companion/Sources/claude-tts-companion/TranscriptParser.swift
  </read_first>
  <action>
Create FileWatcher.swift with two classes. Both use `import Foundation` and `import Logging`.

**NotificationWatcher** (WATCH-01, WATCH-03, WATCH-04):

- `final class NotificationWatcher: @unchecked Sendable` -- matches TTSEngine pattern for Swift 6
- Properties:
  - `private let logger = Logger(label: "notification-watcher")`
  - `private let directoryPath: String` (defaults to `Config.notificationDir`)
  - `private var source: DispatchSourceFileSystemObject?` -- STRONG reference (WATCH-03)
  - `private var knownFiles: Set<String>` -- tracks already-processed filenames to avoid re-firing
  - `private let lock = NSLock()` -- protects knownFiles
  - `private let callback: (String) -> Void` -- called with the full path of each new .json file
- `init(directory: String = Config.notificationDir, callback: @escaping (String) -> Void)`:
  - Store directory and callback
  - Scan directory for existing .json files and populate `knownFiles` so we only fire on NEW files
- `func start()`:
  - Create directory if it doesn't exist: `FileManager.default.createDirectory(atPath:withIntermediateDirectories:true)`
  - Open a file descriptor on the directory: `open(directoryPath, O_EVTONLY)`. Log and return if fd is -1.
  - Create `DispatchSource.makeFileSystemObjectSource(fileDescriptor: fd, eventMask: .write, queue: .global(qos: .userInitiated))`
  - Set event handler: scan directory for .json files, diff against `knownFiles`, fire callback for each new file, update `knownFiles`
  - Set cancel handler: `close(fd)`
  - `source?.resume()`
  - Log "Watching \(directoryPath) for new .json files"
- `func stop()`:
  - `source?.cancel()`
  - `source = nil`
- Private `scanForNewFiles()`:
  - `FileManager.default.contentsOfDirectory(atPath: directoryPath)` filtered to `.json` suffix
  - Diff against `knownFiles` under lock
  - For each new file, call `callback(directoryPath + "/" + filename)` and add to `knownFiles`

**JSONLTailer** (WATCH-02, WATCH-03, WATCH-04):

- `final class JSONLTailer: @unchecked Sendable`
- Properties:
  - `private let logger = Logger(label: "jsonl-tailer")`
  - `private let filePath: String`
  - `private var source: DispatchSourceFileSystemObject?` -- STRONG reference (WATCH-03)
  - `private var offset: UInt64 = 0` -- current read position
  - `private let callback: ([String]) -> Void` -- called with array of new complete lines
  - `private let lock = NSLock()`
- `init(filePath: String, callback: @escaping ([String]) -> Void)`:
  - Store filePath and callback
  - Set initial offset to current file size (only tail new content): `FileManager.default.attributesOfItem(atPath:)[.size] as? UInt64 ?? 0`
- `func start()`:
  - Open fd with `open(filePath, O_EVTONLY)`. Log and return if -1.
  - Create `DispatchSource.makeFileSystemObjectSource(fileDescriptor: fd, eventMask: .write, queue: .global(qos: .userInitiated))`
  - Event handler calls `readNewLines()`
  - Cancel handler: `close(fd)`
  - `source?.resume()`
  - Log "Tailing \(filePath) from offset \(offset)"
- `func stop()`:
  - `source?.cancel()`
  - `source = nil`
- Private `readNewLines()`:
  - Open file with `FileHandle(forReadingAtPath: filePath)`. Guard nil.
  - `fileHandle.seek(toFileOffset: offset)`
  - `let data = fileHandle.readDataToEndOfFile()`
  - Guard `!data.isEmpty`
  - Update `offset += UInt64(data.count)` under lock
  - Convert data to String (UTF-8), split by newlines, filter empty lines
  - If the last character is NOT a newline, save the partial line (subtract its bytes from offset so it gets re-read next time). This avoids yielding incomplete JSON lines.
  - Call `callback(completeLines)` with non-empty complete lines
  - `fileHandle.closeFile()`

CRITICAL implementation details:

- Both classes store their DispatchSource as a strong `var` property (WATCH-03). This prevents silent ARC deallocation that would kill the watcher.
- Use `O_EVTONLY` for the file descriptor -- this is read-only notification, does not block other writers.
- The event handler fires within the DispatchSource's queue latency (typically <50ms on macOS), well within the 100ms target (WATCH-04).
- NSLock protects mutable state, matching the project's established thread-safety pattern (TTSEngine, CircuitBreaker).
  </action>
  <verify>
  <automated>cd /Users/terryli/eon/cc-skills/plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>FileWatcher.swift exists with NotificationWatcher and JSONLTailer classes. Both use DispatchSource.makeFileSystemObjectSource with strong references. Build succeeds with zero errors.</acceptance_criteria>
  <done>NotificationWatcher detects new .json files via directory DispatchSource. JSONLTailer reads new bytes from growing files via offset tracking. Both store DispatchSource as strong property references. swift build passes.</done>
  </task>

</tasks>

<verification>
1. `swift build` succeeds with zero errors
2. FileWatcher.swift contains `DispatchSource.makeFileSystemObjectSource`
3. Both NotificationWatcher and JSONLTailer have `private var source: DispatchSourceFileSystemObject?` (strong reference)
4. Config.swift has `notificationDir` and `transcriptBaseDir` constants
5. JSONLTailer tracks offset via `UInt64` property and uses `seek(toFileOffset:)`
</verification>

<success_criteria>

- FileWatcher.swift compiles with NotificationWatcher and JSONLTailer
- DispatchSource watchers are strong references (not local variables)
- JSONL tailing uses offset-based reads (no full-file re-read)
- Config has notification and transcript directory paths with env overrides
- swift build passes clean
  </success_criteria>

<output>
After completion, create `.planning/phases/07-file-watching-auto-continue/07-01-SUMMARY.md`
</output>
