# kizu v0.3 — 統合: ストリームモード + ターミナル自動分割 + 設定ファイル + プラグイン

この ExecPlan はリビングドキュメントです。`Progress` / `Surprises & Discoveries` / `Decision Log` / `Outcomes & Retrospective` の各セクションは作業進行に合わせて継続的に更新します。

## Purpose / Big Picture

このマイルストーンを完了すると、ユーザーは以下の新しい体験を得ます:

1. **ストリームモード**: Claude Code が行った Write/Edit 操作を時系列で一覧し、git の hunk マージに汚されない「操作単位のdiff」を閲覧できる。メインの diff ビュー（「今どうなっているか」）と Tab で切り替えて、「Claude Code が何をしたか」を操作粒度で追跡できる
2. **ターミナル自動分割**: `kizu --attach` 一発で tmux / zellij / kitty / Ghostty の画面を分割し、片方に kizu TUI、もう片方にシェル（Claude Code を起動する場所）が立ち上がる。手動でターミナルを分割して kizu を起動する手間がなくなる
3. **設定ファイル**: `~/.config/kizu/config.toml` でキーバインドのリマップ、diff の背景色変更、デバウンス間隔の調整などが行える。`a` キーを `A` に変えたい、背景色をもっと明るくしたい、といったカスタマイズが可能になる
4. **Claude Code プラグイン**: kizu を Claude Code プラグインとしてインストールでき、`/kizu` スラッシュコマンドでステータス確認やセッション管理が Claude Code 内から直接行える。hook の自動登録もプラグインが担う

動作確認の代表例:

    # ストリームモード
    kizu                          # TUI 起動
    (別ターミナルで Claude Code がファイルを編集)
    Tab                           # ストリームモードに切り替え
    # → 14:03:22 Write src/auth.rs +12/-3 のような行が時系列で並ぶ
    # → 選択すると「その操作だけ」の diff が右ペインに表示される

    # ターミナル自動分割
    kizu --attach                 # tmux/zellij/kitty/Ghostty を自動検出して分割
    # → 左ペインに kizu TUI、右ペインにシェルが立ち上がる

    # 設定ファイル
    cat ~/.config/kizu/config.toml
    # [keys]
    # ask = "A"
    # reject = "R"
    # [colors]
    # bg_added = [20, 60, 20]
    kizu                          # カスタムキーバインドと色で起動

## Progress

作業を開始する前、完了直後、途中で停止する際に必ず更新してください。タイムスタンプは `(YYYY-MM-DD HH:MM:SSZ)` 形式。

### M1: `hook-log-event` サブコマンド + イベントディレクトリ基盤 — 完了

- [x] (2026-04-16) `SanitizedEvent` 構造体定義と JSON シリアライズ (`src/hook.rs`)
- [x] (2026-04-16) `events_dir()` を `src/paths.rs` に追加 (`<state_dir>/events/`)
- [x] (2026-04-16) `ensure_private_dir` (0700) + ファイル 0600 パーミッション (`src/paths.rs`)
- [x] (2026-04-16) `config_file()` を `src/paths.rs` に追加 (M2 で使用)
- [x] (2026-04-16) `hook-log-event` サブコマンド本体 (`main.rs` — stdin JSON → sanitize → atomic write)
- [x] (2026-04-16) `prune_event_log` + `prune_event_log_in` (TTL 24h + 上限 1000 エントリ、テスト用パス引数版)
- [x] (2026-04-16) `main.rs` の `unimplemented!` を実装に差し替え
- [x] (2026-04-16) Rust unit tests 11 本追加 (sanitize 2 / write 2 / prune 3 / paths 4)、合計 280 passed

### M2: 設定ファイル (`~/.config/kizu/config.toml`) — 完了

- [x] (2026-04-16) `toml` v1.1.2 crate 追加、`src/config.rs` 新設
- [x] (2026-04-16) `KizuConfig` 構造体（keys / colors / timing / editor / attach）+ `#[serde(default)]` で部分 TOML マージ
- [x] (2026-04-16) `load_config()` / `load_config_from()` — `$KIZU_CONFIG` → `$XDG_CONFIG_HOME/kizu/config.toml` → `~/.config/kizu/config.toml` → デフォルト
- [x] (2026-04-16) `Action` enum + `resolve_action()` for future full migration
- [x] (2026-04-16) `app.rs` の `handle_normal_key` でリマップ対象キー (ask/reject/comment/revert/editor/seen/follow/search/picker/reset/cursor_placement/wrap) を `config.keys` 経由に変更
- [x] (2026-04-16) `App` に `config: KizuConfig` フィールド追加（`bootstrap_with_diff` / `fake_app` で初期化）
- [x] (2026-04-16) `ui.rs` の `BG_ADDED`/`BG_DELETED` 定数を test-only に移動、production は `config.colors.bg_added_color()` / `bg_deleted_color()` を使用
- [x] (2026-04-16) `RowRenderCtx` に `bg_added`/`bg_deleted` フィールド追加、`render_diff_line`/`render_diff_line_wrapped` にパラメータ伝搬
- [x] (2026-04-16) `app.rs` の file view `line_bg` もConfig 参照に変更
- [x] (2026-04-16) Rust unit tests 16 本追加 (config 16)、合計 296 passed

### M3: ストリームモード TUI — 完了

- [x] (2026-04-16) `StreamEvent` 構造体（metadata + captured diff + add/del counts）
- [x] (2026-04-16) `ViewMode` enum (Diff/Stream) + `toggle_view_mode()` on App
- [x] (2026-04-16) `WatchEvent::EventLog(PathBuf)` バリアント追加
- [x] (2026-04-16) `handle_event_log()`: SanitizedEvent 読み込み → per-operation diff capture → stream_events push
- [x] (2026-04-16) `compute_operation_diff()`: 前回/今回の cumulative diff snapshot の set-difference
- [x] (2026-04-16) `git::diff_single_file()`: 単一ファイル diff helper
- [x] (2026-04-16) Tab キーでモード切り替え（handle_normal_key + stream mode key dispatch）
- [x] (2026-04-16) ストリームモードの render（`render_stream_view`: 左=時系列リスト、右=syntax-highlighted diff）
- [x] (2026-04-16) ストリームモードでの j/k/g/G ナビゲーション
- [x] (2026-04-16) フッターに `[stream]` モードインジケータ
- [x] (2026-04-16) Rust unit tests 6 本追加、合計 302 passed
- [x] (2026-04-16) events dir の live watcher 統合 (`spawn_events_dir_debouncer` → `new_kizu_debouncer` で events dir を NonRecursive 監視、100ms debounce)
- [x] (2026-04-16) 起動時の既存イベントロード (`load_existing_events()` — state_dir/events/ から timestamp 昇順でロード、diff_snapshot は None)
- [ ] ストリームモードでの scar 操作対応（v0.3.1 に先送り — render 層が安定してから追加）

### M4: `--attach` ターミナル自動分割 — 完了

- [x] (2026-04-16) `src/attach.rs` 新設
- [x] (2026-04-16) `TerminalKind` enum (Tmux/Zellij/Kitty/Ghostty) + `from_str`
- [x] (2026-04-16) `detect_terminal()`: $TMUX → $ZELLIJ → $KITTY_LISTEN_ON → $TERM_PROGRAM=ghostty
- [x] (2026-04-16) `split_and_launch()`: 各ターミナルの分割コマンド実行。Ghostty は macOS only (#[cfg])
- [x] (2026-04-16) `resolve_terminal()`: config override → auto-detect
- [x] (2026-04-16) `--attach` CLI フラグ追加 (main.rs early return)
- [x] (2026-04-16) Rust unit tests 3 本追加、合計 305 passed

### M5: Claude Code プラグイン — 完了

- [x] (2026-04-16) `plugin/` ディレクトリ構造
- [x] (2026-04-16) `plugin/plugin.json`: PostToolUse (hook-post-tool + hook-log-event async) + Stop (hook-stop)
- [x] (2026-04-16) `plugin/commands/kizu.md`: `/kizu` スラッシュコマンド（セッション状態確認）
- [x] (2026-04-16) `plugin/README.md`: インストール・使い方

### M6: テスト + ドキュメント + リリース準備 — 進行中

- [x] (2026-04-16) CLAUDE.md v0.3 実装メモ追加
- [ ] README.md 更新
- [ ] `just ci` 全 green
- [ ] version bump to 0.3.0
- [ ] e2e テスト追加（stream mode / attach）

## Surprises & Discoveries

実装中に発見した予期しない挙動、バグ、最適化、性能トレードオフを証拠とともに記録します。

### 着手前に既知の事項

- Observation: v0.2 の `hook-log-event` は `unimplemented!()` で残されている。v0.3 ストリームモードの前提として実装が必要。
  Evidence: `src/main.rs:80` — `Some(Command::HookLogEvent) => unimplemented!("v0.2: kizu hook-log-event")`
  Impact: M1 で最優先実装。

- Observation: ストリームモードで「操作単位の diff」を表示するには、各イベント発生時点のファイル差分を捕捉する必要があるが、`SanitizedEvent` にはコード本文を含めない（v0.2 のセキュリティ判断）。
  Evidence: v0.2 ExecPlan Decision Log — 「event log は private state dir に保存し、生 stdin JSON ではなく sanitize 済みメタデータのみを書き出す」
  Impact: M3 で「TUI が real-time に diff snapshot を memory 上に capture する」方式を採用。過去イベント（TUI 起動前）は metadata のみ表示。

- Observation: Ghostty の AppleScript による split は macOS 限定。他の OS では Ghostty 分割は使えない。
  Evidence: SPEC.md の対応ターミナル表、Ghostty AppleScript ドキュメント。
  Impact: M4 で `#[cfg(target_os = "macos")]` ガード。

実装開始後に発見した事項はここに追記します:

- Observation: …
  Evidence: …

## Decision Log

### 着手前に決定済み

- Decision: ストリームモードの per-operation diff は TUI がリアルタイムに memory 上でキャプチャする方式を採用する。イベントファイルには sanitized metadata のみ保存する
  Rationale: v0.2 で確立したセキュリティポリシー（`tool_input.content` / `tool_response.output` をディスクに書かない）を維持しつつ、操作単位の diff を実現する唯一の方法。TUI は watcher で filesystem の変更をリアルタイムに検知しており、新しい event が到着した時点で `git diff <baseline> -- <file>` を取り、前回の snapshot との差分を計算すれば per-operation diff が得られる。TUI 起動前のイベントは metadata のみ表示（file paths, timestamp, tool name）になるが、これは許容する。TUI を常時起動する想定のワークフロー（`--attach` で起動しっぱなし）では問題にならない
  Date/Author: 2026-04-16 / Initial implementer

- Decision: 設定ファイルは TOML 形式、パスは `~/.config/kizu/config.toml` (`$XDG_CONFIG_HOME/kizu/config.toml`)
  Rationale: Rust エコシステムでは TOML がデファクト。`toml` crate は安定しており、型付き deserialize が直感的。macOS では `~/.config/` が XDG 互換として広く使われているため、platform 別分岐は不要
  Date/Author: 2026-04-16 / Initial implementer

- Decision: `--attach` のターミナル検出優先順は tmux → zellij → kitty → ghostty
  Rationale: SPEC.md で規定済み。tmux が最も広く使われており、zellij は Rust エコシステムで伸びている。kitty は `kitty @ launch` でリモート制御可能。Ghostty は AppleScript 依存で macOS 限定のため最後
  Date/Author: 2026-04-16 / Initial implementer

- Decision: Tab キーでメインの diff ビューとストリームモードを切り替える
  Rationale: Tab は「タブ切り替え」の普遍的メタファー。vim の `gt`/`gT` は 2 ストロークで面倒。`Tab` は v0.2 で未使用キーであり衝突しない
  Date/Author: 2026-04-16 / Initial implementer

- Decision: Claude Code プラグインは kizu 本体リポジトリ内の `plugin/` サブディレクトリとして管理する
  Rationale: プラグインの hook コマンドは `kizu hook-post-tool` / `kizu hook-stop` を呼び出すだけで、kizu バイナリと同期が必要。別リポにするとバージョン不整合のリスクがある。`plugin/` ディレクトリは Cargo ビルドに影響しない
  Date/Author: 2026-04-16 / Initial implementer

- Decision: キーバインドリマップは 1 文字 → 1 文字のマッピングのみサポートする（コード入力は不可）
  Rationale: v0.3 の最小実装として十分。複雑なキーシーケンス（`gg`, `Ctrl-x Ctrl-s` 等）のマッピングは設定ファイルのスキーマが爆発する。ユーザーの 99% は `a` → `A` のような単純リマップで十分。将来必要になれば v0.4 で拡張する
  Date/Author: 2026-04-16 / Initial implementer

実装中に決定した事項はここに追記します:

- Decision: …
  Rationale: …
  Date/Author: …

## Outcomes & Retrospective

この ExecPlan が完了した時点で、以下を照合して記入します:

- Big Picture で宣言した 4 機能（ストリームモード、`--attach`、設定ファイル、プラグイン）が全て実装され、e2e テストで保護されているか
- ストリームモードの per-operation diff capture が実用的に動作したか、memory 方式の制約が問題になったか
- `--attach` の対応ターミナル 4 種全てで動作確認できたか
- 設定ファイルのスキーマが安定しているか、v0.3 リリース後に breaking change の予感があるか
- プラグインの自動 hook 登録が `kizu init` と比べてどれだけ体験が改善したか

[このセクションは作業進行に合わせて埋めていきます]

## Context and Orientation

### 現在の状態（v0.2 完了時点）

kizu は git worktree をリアルタイム監視する Rust 製 TUI で、v0.2 では scar（`@kizu[ask|reject|free]:` インラインコメント）によるレビューとマルチエージェント hook 層が実装済みです。主要ファイル:

- `src/main.rs` (177 行) — clap subcommand dispatch。`HookLogEvent` のみ `unimplemented!()` で残存
- `src/app.rs` (~4000 行) — TUI 状態機械。`App` 構造体、`handle_key`、watcher イベント統合、scar/revert/search/file-view 操作
- `src/git.rs` (~1400 行) — git CLI shell out。`compute_diff`、`parse_unified_diff`
- `src/watcher.rs` (~1100 行) — `notify-debouncer-full` ラッパ。worktree 300ms / git_dir 100ms デバウンス
- `src/ui.rs` (~1700 行) — ratatui 描画。diff view (delta 風背景色)、picker、scar comment overlay、search bar、file view
- `src/scar.rs` — scar 書き込み。`detect_comment_syntax` / `insert_scar`
- `src/hook.rs` — hook サブコマンドロジック。`NormalizedHookInput` / `scan_scars` / `format_additional_context`
- `src/init.rs` — `kizu init` / `kizu teardown`。6 エージェント対応
- `src/session.rs` — セッション管理 (baseline SHA + PID)
- `src/paths.rs` — XDG state dir 解決。`state_dir()` / `session_file()`
- `src/highlight.rs` — syntect シンタックスハイライト

テスト: Rust 269 本 + e2e (bun:test) 19 本。`just ci` で fmt-check → clippy → cargo test → release → e2e。

### 依存クレート（v0.2 時点）

| クレート | 用途 |
|:---|:---|
| ratatui 0.30 | TUI フレームワーク |
| crossterm 0.29 | ターミナル制御 |
| notify 8.2 + notify-debouncer-full 0.7 | ファイル監視 |
| tokio 1.52 (current_thread) | 非同期ランタイム |
| clap 4.6 (derive) | CLI パーサー |
| dialoguer 0.12 | インタラクティブ UI (init) |
| serde + serde_json | JSON シリアライズ |
| syntect 5.3 | シンタックスハイライト |
| regex 1 | scar パターンマッチ |
| dirs 6 | XDG ディレクトリ解決 |
| which 8 | バイナリ検出 |
| unicode-width 0.2 | CJK 幅計算 |
| libc 0.2 | PID 存在チェック (kill -0) |

### 専門用語の定義

- **ストリームモード**: PostToolUse hook が記録するイベントログ (`<state_dir>/events/`) をデータソースとする TUI ビュー。git の hunk ではなく、AI エージェントの個別 Write/Edit 操作を 1 行 1 エントリで時系列表示する。メイン diff ビュー（「今どうなっているか」＝状態）と対になる「何をしたか」＝操作履歴のビュー
- **SanitizedEvent**: hook-log-event が stdin の生 JSON から `tool_input.content` / `tool_response.output` 等のコード本文を削除し、metadata のみに絞ったデータ構造。`session_id` / `tool_name` / `file_paths` / `cwd` / `timestamp_ms` のフィールドを持つ
- **per-operation diff**: 1 回の Write/Edit 操作だけに対応する unified diff。git の hunk マージ（隣接する複数操作が 1 hunk に合体する）を受けない、操作粒度の差分。TUI が real-time に前回の diff snapshot と現在の diff snapshot の差分を計算して生成する
- **attach mode**: `kizu --attach` で起動するモード。ターミナルエミュレータの API を使って画面を自動分割し、片方のペインに kizu TUI、もう片方にユーザーのシェルを配置する
- **config file**: `~/.config/kizu/config.toml` に置かれる設定ファイル。キーバインドリマップ、diff 背景色のカスタマイズ、デバウンスタイミング調整などを TOML 形式で記述する
- **Claude Code プラグイン**: kizu の hook 登録と `/kizu` スラッシュコマンドを Claude Code のプラグインシステム経由で提供するパッケージ。ユーザーはプラグインをインストールするだけで hook が自動登録される

## Plan of Work

### M1 を最初に置く理由

`hook-log-event` は v0.2 から先送りされた唯一の未実装サブコマンドで、M3 ストリームモードのデータ源になる。ストリームモードなしでも単体で価値がある（イベントログが蓄積されるようになり、外部ツールからの分析が可能になる）。実装は stdin JSON を読んで sanitize して書くだけの純粋な I/O で、TUI 層に一切手を入れないため、他のマイルストーンとの干渉がない。

`src/paths.rs` に `events_dir()` を追加する。`state_dir()` の下に `events/` サブディレクトリを返す。ディレクトリ作成時に `0700` パーミッションを設定する `ensure_private_dir(path)` ヘルパも同じモジュールに追加する。

`src/hook.rs` に `SanitizedEvent` 構造体を追加する。`NormalizedHookInput` から `tool_name` / `file_paths` / `cwd` を抽出し、`timestamp_ms` は `SystemTime::now()` から計算する。`session_id` は `NormalizedHookInput.session_id` をそのまま渡す。

`hook-log-event` の本体は `main.rs` の `run_hook_log_event()` 関数。stdin JSON を `parse_hook_input` で読み、`SanitizedEvent` に変換し、`<events_dir>/<timestamp_ms>-<tool_name>.json` に atomic write（`tempfile::NamedTempFile` → `persist`）する。書き込み後に `prune_event_log` を呼び、TTL 超過と上限超過のファイルを削除する。

`prune_event_log` は `events_dir` 内のファイルを列挙し、ファイル名の timestamp prefix を解析して TTL 24 時間を超えたものを削除、残りが 1000 件を超えていれば古い順に削除する。`KIZU_EVENT_TTL_SECS` 環境変数で TTL を上書き可能にする。

### M2（設定ファイル）の位置づけ

設定ファイルは M3 以降で使う色やキーバインドの基盤なので、M3 より前に実装する。ただし M1 とは独立しているため、並行作業も可能。

`src/config.rs` を新設する。`toml` crate（`0.8`）を追加し、`KizuConfig` 構造体を定義する。

    [keys]
    ask = "a"
    reject = "r"
    comment = "c"
    revert = "x"
    editor = "e"
    seen = " "
    follow = "f"
    search = "/"
    search_next = "n"
    search_prev = "N"
    picker = "s"
    file_view = "\n"      # Enter
    reset_baseline = "R"
    cursor_placement = "z"
    wrap_toggle = "w"
    stream_mode = "\t"    # Tab

    [colors]
    bg_added = [10, 50, 10]
    bg_deleted = [60, 10, 10]

    [timing]
    debounce_worktree_ms = 300
    debounce_git_dir_ms = 100

    [editor]
    command = ""          # 空なら $EDITOR、設定ありならそちらを優先

    [attach]
    terminal = ""         # 空なら自動検出、"tmux"/"zellij"/"kitty"/"ghostty" で強制

`load_config()` は `$KIZU_CONFIG` → `$XDG_CONFIG_HOME/kizu/config.toml` → `~/.config/kizu/config.toml` の順にファイルを探し、見つかれば TOML を parse する。見つからなければ全フィールドがデフォルト値の `KizuConfig::default()` を返す。TOML の各セクションは `Option` で、未指定のフィールドはデフォルト値にフォールバックする。

`app.rs` の `handle_key` では、`KeyCode::Char('a')` のような直書きのパターンを `config.keys.ask` との比較に置き換える。キーバインドの解決は `config.rs` の `KeyAction::from_key(key, config) -> Option<Action>` で一元化し、`handle_key` は `Action` enum を match する形にリファクタする。

`ui.rs` の `BG_ADDED` / `BG_DELETED` 定数は `config.colors.bg_added` からの `Color::Rgb` 変換に置き換える。`Config` は `App` 構造体のフィールドとして保持し、`ui.rs` の render 関数に `&Config` を渡す。

### M3（ストリームモード）の設計

ストリームモードは kizu のもう 1 つのデータソースを可視化する機能で、v0.3 の中核。

`App` に `view_mode: ViewMode` フィールドを追加する。`ViewMode` は `Diff`（現行のメイン diff ビュー）と `Stream`（ストリームモード）の 2 値 enum。`Tab` キーで切り替える。

`StreamEvent` 構造体を `src/app.rs`（または `src/stream.rs` に分離）に定義する:

    pub struct StreamEvent {
        pub metadata: SanitizedEvent,      // hook-log-event が書いたメタデータ
        pub diff_snapshot: Option<String>,  // TUI が capture した per-operation diff (real-time のみ)
        pub add_count: usize,              // +行数 (diff から計算)
        pub del_count: usize,              // -行数
    }

`App.stream_events: Vec<StreamEvent>` にイベントを蓄積する。

events dir の監視は既存の watcher に追加する。`watcher.rs` の `WatchEvent` に `EventLog` バリアントを追加し、`<state_dir>/events/` ディレクトリへの create イベントを `EventLog` として emit する。debounce は 100ms（git_dir と同じ — イベントは 1 ファイルずつ個別に書き込まれるため、短いデバウンスで問題ない）。

新しいイベントファイルが検出されたとき、`App` は以下を行う:

1. イベントファイルを読み込み `SanitizedEvent` にデシリアライズ
2. `event.file_paths` の各ファイルに対して `git diff <baseline> -- <file>` を実行
3. 前回同ファイルの diff snapshot と今回の diff を比較し、差分（per-operation diff）を計算する。前回 snapshot がなければ今回の diff 全体が per-operation diff
4. `StreamEvent` を `stream_events` に push

per-operation diff の計算ロジック: 前回の diff snapshot を `old_lines: HashSet<(String, LineKind)>` として持ち、今回の diff の各行のうち `old_lines` に含まれないものが「この操作で追加された差分」。これは厳密な diff-of-diff ではないが、実用上十分な近似。厳密さが必要なら `similar` crate の text diff を使う。

ストリームモードの render は ui.rs に追加する。左ペインに時系列リスト（時刻 / tool_name / file_path / +N/-M）、右ペインに選択中イベントの per-operation diff（メイン diff ビューと同じシンタックスハイライト + 背景色スタイル）。scar 操作（a/r/c）はストリームモードでも使えるようにする — ストリームイベントの file_path と diff 行から scar の挿入位置を計算する。

### M4（`--attach`）は TUI に手を入れない

`--attach` は kizu 本体の TUI ロジックには影響しない。terminal detection + split command の実行のみ。

`src/attach.rs` を新設する。`detect_terminal() -> Option<TerminalKind>` は環境変数を順にチェック:
1. `$TMUX` が設定されていれば `TerminalKind::Tmux`
2. `$ZELLIJ` が設定されていれば `TerminalKind::Zellij`
3. `$KITTY_LISTEN_ON` が設定されていれば `TerminalKind::Kitty`
4. `$TERM_PROGRAM == "ghostty"` なら `TerminalKind::Ghostty` (macOS only)
5. いずれもなければ `None`

`split_and_launch(terminal: TerminalKind) -> Result<()>` は各ターミナルの分割コマンドを実行:
- tmux: `tmux split-window -h "kizu"`
- zellij: `zellij run --floating -- kizu`
- kitty: `kitty @ launch --type=window kizu`
- ghostty: AppleScript で split + kizu 起動

`main.rs` に `--attach` フラグを追加。`--attach` が指定された場合、TUI を起動する代わりに `split_and_launch` を呼ぶ。config の `[attach].terminal` が指定されていればそちらを優先し、なければ `detect_terminal()` で自動検出する。

### M5（Claude Code プラグイン）はリポジトリ内のサブディレクトリ

`plugin/` ディレクトリにプラグインのファイルを配置する。プラグインは `kizu` バイナリが PATH 上にある前提で動作する。

`plugin/plugin.json` にマニフェストを書く。hooks セクションで PostToolUse / Stop を宣言し、commands セクションで `/kizu` スラッシュコマンドを定義する。

`/kizu` コマンドは `kizu` のセッション状態を表示する: 現在のセッション baseline、未解決 scar の数、最新のストリームイベント。引数なしでステータス表示、`/kizu init` で hook 設置（プラグインの hook と重複しないよう注意）、`/kizu teardown` で hook 除去。

### M6（テスト + ドキュメント）

各マイルストーン完了時に unit test / e2e test を追加する。M6 は全体の最終確認として `just ci` を通し、README / CLAUDE.md を更新する。

## Concrete Steps

作業ディレクトリは全て `/Users/annenpolka/ghq/github.com/annenpolka/kizu` です。

### M1 着手

`toml` crate は M2 で使うが、M1 は serde_json のみで完結するため追加不要。

    just rust                              # v0.2 の状態が green であることを確認

`src/paths.rs` を編集して `events_dir()` を追加:

    cargo test paths::                     # 既存 + 新規テストが green

`src/hook.rs` に `SanitizedEvent` と sanitize ロジックを追加:

    cargo test hook::tests::sanitize       # TDD: Red → Green

`main.rs` の `run_hook_log_event()` を実装:

    echo '{"session_id":"test","hook_event_name":"PostToolUse","tool_name":"Edit","tool_input":{"file_path":"/tmp/foo.rs","content":"..."},"tool_response":{"output":"ok"},"cwd":"/tmp"}' \
      | KIZU_STATE_DIR=/tmp/kizu-test cargo run -- hook-log-event

期待される動作: `/tmp/kizu-test/events/<timestamp>-Edit.json` が作成され、中身に `tool_input.content` / `tool_response.output` が含まれない。

    cat /tmp/kizu-test/events/*.json

期待される出力:

    {"session_id":"test","hook_event_name":"PostToolUse","tool_name":"Edit","file_paths":["/tmp/foo.rs"],"cwd":"/tmp","timestamp_ms":1745000000000}

prune のテスト:

    cargo test hook::tests::prune_removes_old_entries
    cargo test hook::tests::prune_enforces_max_entries

    just rust                              # 全 green

### M2 着手

    cargo add toml --features parse

`src/config.rs` を新設:

    cargo test config::                    # TDD ループ

テスト内容:
- デフォルト設定が正しい値を返す
- TOML ファイルから部分的に設定を上書きできる
- 未知のキー名を指定するとエラーではなく無視する
- `$KIZU_CONFIG` 環境変数で設定ファイルパスを上書きできる
- キーバインドのリマップが `KeyAction::from_key` で正しく解決される

`app.rs` のリファクタ:

    cargo test app::tests::               # 既存テストが壊れないことを確認

`ui.rs` の色定数をConfigに:

    cargo test ui::tests::                 # 背景色テストが green

    just rust                              # 全 green

### M3 着手

`src/stream.rs` を新設（または `src/app.rs` に統合）:

    cargo test stream::                    # TDD ループ

watcher に events dir 監視を追加:

    cargo test watcher::tests::event_log_  # 新規テスト

App にストリームモード状態を追加:

    cargo test app::tests::stream_mode_    # Tab 切り替えテスト

ui.rs にストリーム描画を追加:

    cargo test ui::tests::render_stream_   # 描画テスト

e2e テスト:

    cd tests/e2e && bun test stream        # 新規 e2e

    just ci                                # 全 green

### M4 着手

`src/attach.rs` を新設:

    cargo test attach::                    # 検出ロジックの TDD

`main.rs` に `--attach` フラグを追加:

    # tmux 環境でテスト
    TMUX="/tmp/tmux-1000/default,1234,0" cargo run -- --attach
    # → tmux split-window -h "kizu" が実行される

    just rust                              # 全 green

### M5 着手

    mkdir -p plugin/commands plugin/hooks

`plugin/plugin.json`:

    {
      "name": "kizu",
      "description": "Realtime diff monitor + inline scar review TUI",
      "version": "0.3.0"
    }

`plugin/commands/kizu.md`:

    スラッシュコマンドの定義（YAML frontmatter + 本文）

    just ci                                # plugin ディレクトリが CI に影響しないこと

### M6 着手

    just ci                                # 最終確認

期待される出力:

    cargo fmt --check: ok
    cargo clippy: 0 warnings
    cargo test: N passed (269 + 新規分)
    cargo build --release: ok
    e2e: N tests passed (19 + 新規分)

## Validation and Acceptance

### 観察可能な成功条件

**1. hook-log-event がイベントを書き込む**

1. kizu init 済みの repo で `echo '...' | kizu hook-log-event` を実行
2. `~/Library/Application Support/kizu/events/` にファイルが作成される
3. `stat -f "%Lp" <dir>` でディレクトリが `700` であること
4. 中身に `tool_input.content` / `tool_response.output` が含まれていないこと
5. 1001 件書き込んだ後、最も古い 1 件が自動削除されていること

**2. 設定ファイルでキーバインドを変更する**

1. `~/.config/kizu/config.toml` に `[keys]\nask = "A"` を書く
2. `kizu` を起動
3. `a` を押しても何も起きない
4. `A` を押すと ask scar が挿入される

**3. 設定ファイルで背景色を変更する**

1. `~/.config/kizu/config.toml` に `[colors]\nbg_added = [0, 80, 0]` を書く
2. `kizu` を起動
3. 追加行の背景色がより明るい緑になっている

**4. ストリームモードで操作履歴を見る**

1. `kizu` を起動
2. 別ターミナルで Claude Code がファイルを数回編集する（各 Write/Edit で hook-log-event が実行される）
3. kizu で `Tab` を押す
4. 画面がストリームモードに切り替わり、各操作が時系列で表示される
5. 操作を選択すると、右ペインにその操作だけの diff が表示される
6. `a` を押すと、その操作の対象行に scar が挿入される

**5. `--attach` で自動分割する**

1. tmux セッション内で `kizu --attach` を実行
2. 画面が水平分割され、右ペインに kizu TUI が表示される
3. 左ペインにシェルが残り、`claude` コマンドを起動できる

**6. Claude Code プラグイン**

1. `claude plugin add ./plugin` で kizu プラグインをインストール
2. Claude Code セッション内で `/kizu` を実行
3. kizu のセッション情報が表示される
4. PostToolUse / Stop hook がプラグイン経由で自動的に有効になっている

### テストコマンドとその期待結果

    just ci

期待される最終行:

    ✓ fmt-check  ✓ clippy  ✓ test  ✓ release  ✓ e2e

個別に走らせる場合:

    cargo test hook::tests::sanitize_event_strips_content         # 1 passed
    cargo test hook::tests::prune_removes_old_entries              # 1 passed
    cargo test config::tests::default_config_has_correct_values    # 1 passed
    cargo test config::tests::toml_partial_override                # 1 passed
    cargo test config::tests::keybind_remap_resolves               # 1 passed
    cargo test attach::tests::detect_tmux                          # 1 passed
    cargo test stream::tests::stream_event_from_metadata           # 1 passed
    cd tests/e2e && bun test stream                                # N passed

## Idempotence and Recovery

- **M1 (hook-log-event)**: 同一イベントの再書き込みはファイル名の timestamp suffix で自然に重複回避。prune は複数回実行しても安全（削除済みファイルは再削除されない）。`events_dir` が存在しなければ作成する
- **M2 (設定ファイル)**: 設定ファイルが存在しなければデフォルト値で動作。不正な TOML はエラーメッセージを表示してデフォルトにフォールバック。既存の設定ファイルを壊すことはない
- **M3 (ストリームモード)**: TUI のメモリ上にのみ diff snapshot を保持。TUI を再起動すれば状態は初期化される。events dir のファイルは hook-log-event が管理し、ストリームモードは読み取り専用
- **M4 (`--attach`)**: 分割コマンドの失敗時はエラーメッセージを表示して exit 1。既に分割されている場合は追加分割が行われるが、kizu TUI は新しいペインで正常に起動する
- **M5 (プラグイン)**: プラグインのインストール/アンインストールは Claude Code のプラグインシステムが管理。kizu 側の hook 設定とプラグインの hook 宣言が重複した場合、hook は 2 回呼ばれるが、scar scan は冪等なので副作用なし
- **失敗時のリカバリ**:
  - hook-log-event の書き込み失敗: stderr にエラーを出力して exit 1。TUI は影響を受けない
  - 設定ファイルの parse 失敗: 警告を `last_error` に表示してデフォルトで起動
  - events dir の権限問題: `ensure_private_dir` でエラー → stderr 出力 → exit 1

## Artifacts and Notes

### 重要な transcript / diff / ログ（作業進行に合わせて追加）

[このセクションは作業進行に合わせて埋めていきます]

## Interfaces and Dependencies

### 追加する外部クレート

| クレート | version | 用途 |
|:---|:---|:---|
| `toml` | 0.8 (parse feature) | 設定ファイル TOML デシリアライズ |

v0.2 で追加済みの `serde` / `serde_json` / `dirs` / `tempfile` はそのまま流用。新規クレートは `toml` のみ。

### 新設モジュール

- `src/config.rs` — 設定ファイルのロード + `KizuConfig` 構造体 + `KeyAction` enum + `Action` enum
- `src/attach.rs` — ターミナル検出 + 分割コマンド実行
- `src/stream.rs` — `StreamEvent` + per-operation diff 計算（`app.rs` に統合する可能性あり）

### 新設ディレクトリ

- `plugin/` — Claude Code プラグインのファイル群
  - `plugin/plugin.json` — マニフェスト
  - `plugin/commands/kizu.md` — `/kizu` スラッシュコマンド
  - `plugin/hooks/` — hook 定義（必要に応じて）

### 主要な型と関数シグネチャ（マイルストーン終了時に存在するべきもの）

    // src/paths.rs (追加)
    pub fn events_dir() -> Option<PathBuf>;              // state_dir()/events/
    pub fn config_file() -> Option<PathBuf>;             // $XDG_CONFIG_HOME/kizu/config.toml
    pub fn ensure_private_dir(path: &Path) -> Result<()>; // 0700 で作成

    // src/hook.rs (追加)
    #[derive(Debug, Clone, Serialize, Deserialize)]
    pub struct SanitizedEvent {
        pub session_id: Option<String>,
        pub hook_event_name: String,
        pub tool_name: Option<String>,
        pub file_paths: Vec<PathBuf>,
        pub cwd: PathBuf,
        pub timestamp_ms: u64,
    }

    pub fn sanitize_event(input: &NormalizedHookInput) -> SanitizedEvent;
    pub fn write_event(event: &SanitizedEvent) -> Result<PathBuf>;
    pub fn prune_event_log(ttl: Duration, max_entries: usize) -> Result<usize>;

    // src/config.rs (新設)
    #[derive(Debug, Clone, Deserialize)]
    pub struct KizuConfig {
        pub keys: KeyConfig,
        pub colors: ColorConfig,
        pub timing: TimingConfig,
        pub editor: EditorConfig,
        pub attach: AttachConfig,
    }

    #[derive(Debug, Clone, Deserialize)]
    pub struct KeyConfig {
        pub ask: char,            // default: 'a'
        pub reject: char,         // default: 'r'
        pub comment: char,        // default: 'c'
        pub revert: char,         // default: 'x'
        pub editor: char,         // default: 'e'
        pub seen: char,           // default: ' '
        pub follow: char,         // default: 'f'
        pub search: char,         // default: '/'
        pub search_next: char,    // default: 'n'
        pub search_prev: char,    // default: 'N'
        pub picker: char,         // default: 's'
        pub reset_baseline: char, // default: 'R'
        pub cursor_placement: char, // default: 'z'
        pub wrap_toggle: char,    // default: 'w'
    }

    #[derive(Debug, Clone, Deserialize)]
    pub struct ColorConfig {
        pub bg_added: [u8; 3],    // default: [10, 50, 10]
        pub bg_deleted: [u8; 3],  // default: [60, 10, 10]
    }

    #[derive(Debug, Clone, Deserialize)]
    pub struct TimingConfig {
        pub debounce_worktree_ms: u64,  // default: 300
        pub debounce_git_dir_ms: u64,   // default: 100
    }

    pub enum Action {
        Ask, Reject, Comment, Revert, Editor, Seen, Follow,
        Search, SearchNext, SearchPrev, Picker, FileView,
        ResetBaseline, CursorPlacement, WrapToggle, StreamMode,
        ScrollDown, ScrollUp, LineDown, LineUp, HunkNext, HunkPrev,
        Top, Bottom, HalfPageDown, HalfPageUp, Quit,
    }

    pub fn load_config() -> KizuConfig;
    pub fn resolve_action(key: &KeyEvent, config: &KizuConfig) -> Option<Action>;

    // src/attach.rs (新設)
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum TerminalKind {
        Tmux,
        Zellij,
        Kitty,
        Ghostty,
    }

    pub fn detect_terminal() -> Option<TerminalKind>;
    pub fn split_and_launch(terminal: TerminalKind, kizu_bin: &Path) -> Result<()>;

    // src/stream.rs (新設、または app.rs 内)
    pub struct StreamEvent {
        pub metadata: SanitizedEvent,
        pub diff_snapshot: Option<String>,
        pub add_count: usize,
        pub del_count: usize,
    }

    // src/app.rs (追加)
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum ViewMode {
        Diff,    // メイン diff ビュー（現行）
        Stream,  // ストリームモード
    }

    // App 構造体に追加するフィールド:
    // pub view_mode: ViewMode,
    // pub stream_events: Vec<StreamEvent>,
    // pub stream_cursor: usize,
    // pub config: KizuConfig,
    // pub diff_snapshots: HashMap<PathBuf, String>,  // per-operation diff 計算用

### 既存モジュールへの変更

- `src/main.rs`:
  - `HookLogEvent` の `unimplemented!()` を `run_hook_log_event()` 呼び出しに差し替え
  - `--attach` フラグを `Cli` 構造体に追加
  - `--attach` 時の分岐処理
- `src/app.rs`:
  - `App` に `view_mode` / `stream_events` / `stream_cursor` / `config` フィールド追加
  - `handle_key` を `Action` enum ベースにリファクタ
  - `Tab` キーで `view_mode` を切り替える処理
  - watcher の `WatchEvent::EventLog` ハンドリング
- `src/ui.rs`:
  - `BG_ADDED` / `BG_DELETED` 定数を `Config` から読むように変更
  - ストリームモードの render 関数追加
  - Tab バーまたはモードインジケータの描画
- `src/watcher.rs`:
  - `WatchEvent::EventLog` バリアント追加
  - events dir の監視追加
- `src/paths.rs`:
  - `events_dir()` / `config_file()` / `ensure_private_dir()` 追加
- `src/hook.rs`:
  - `SanitizedEvent` / `sanitize_event` / `write_event` / `prune_event_log` 追加

### イベントログのファイルフォーマット

保存先: `<state_dir>/events/<unix_ms>-<tool_name>.json`。ディレクトリは `0700`、ファイルは `0600`。

    {
      "session_id": "01H...",
      "hook_event_name": "PostToolUse",
      "tool_name": "Edit",
      "file_paths": ["/path/to/foo.rs"],
      "cwd": "/path/to/repo",
      "timestamp_ms": 1745000000000
    }

### 設定ファイルのフルスキーマ例

    # ~/.config/kizu/config.toml

    [keys]
    ask = "a"
    reject = "r"
    comment = "c"
    revert = "x"
    editor = "e"
    seen = " "
    follow = "f"
    search = "/"
    search_next = "n"
    search_prev = "N"
    picker = "s"
    reset_baseline = "R"
    cursor_placement = "z"
    wrap_toggle = "w"

    [colors]
    bg_added = [10, 50, 10]
    bg_deleted = [60, 10, 10]

    [timing]
    debounce_worktree_ms = 300
    debounce_git_dir_ms = 100

    [editor]
    command = ""

    [attach]
    terminal = ""

### Claude Code プラグインの構造

    plugin/
    ├── plugin.json              # マニフェスト
    ├── commands/
    │   └── kizu.md              # /kizu スラッシュコマンド
    └── README.md                # プラグインの使い方
