# kizu v0.2 — scar + 背景色 diff + マルチエージェントフック層 (post-adversarial-review)

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

## Purpose / Big Picture

このマイルストーンを完了すると、ユーザーは Claude Code / Cursor / Codex CLI / Qwen Code / Cline / Gemini CLI と併走させながら、kizu から **キー一打で scar（`@kizu[ask|reject|free]:` インラインコメント）を刻み、エージェントに（エージェントが対応する範囲で）自動フィードバック**できるようになります。

エージェントごとの統合深度はバラつきがあります:

| **エージェント** | **PostToolUse 即時通知** | **Stop 最終チェック** | **備考** |
|:---|:---:|:---:|:---|
| Claude Code | ✅ | ✅ | 基準実装 |
| Cursor 1.7+ | ✅ | ✅ | `afterFileEdit` + `stop`、production |
| Qwen Code | ✅ | ✅ | Claude Code 互換 |
| Codex CLI | ⚠️ Bash matcher 限定で不可 | ✅ | Stop のみ有効 |
| Cline v3.36+ | ✅ (best-effort) | ❌ | **Stop 相当 hook が無い**。parity 非保証 |
| Gemini CLI | ❌ | ❌ | stream-json consumer 経由の書き込み側のみ |
| Windsurf | ⏸ | ⏸ | **v0.2 スコープ外**（スキーマ検証待ち、v0.2.1 以降） |

具体的な観察可能挙動:

- kizu を起動中に `a` を押すと、選択中 hunk の直上に言語のコメント構文で `// @kizu[ask]: explain this change` が書き込まれる。`r` は `// @kizu[reject]: revert this change`、`c` はミニ入力欄→任意テキスト (`// @kizu[free]: <ユーザー入力>`)
- `x` を押すと選択中 hunk が `git checkout -p` 相当で revert される (scar は挿入しない)
- `e` を押すと `$EDITOR +<line> <file>` が起動し、該当行にジャンプしたエディタが開く。終了後に kizu に戻る
- `Space` で「見た」マーク（TUI 内部状態のみ、ファイルには何も書かない）
- **diff 表示が delta 風の背景色ベースに刷新される**: `+`/`-` の prefix マーカー列が廃止され、追加行は緑の背景、削除行は赤の背景で全幅塗りつぶし表示になる
- `kizu init` を実行すると、PATH 上の AI コーディングエージェントを自動検出してインタラクティブ multi-select UI が表示され、選んだエージェントそれぞれの設定ファイルに PostToolUse/Stop hook を**既存設定とマージしながら**書き込む。各エージェントの対応範囲は UI 上に明示表示する (`Stop only` / `PostTool best-effort` 等)
- `kizu init --agent cursor,codex --non-interactive` のように CI 用に飛ばせる
- Claude Code / Cursor / Qwen Code / Cline が scar のあるファイルを編集しようとすると、`kizu hook-post-tool` が `additionalContext` で「未対応 scar が N 件あります」と通知する
- **Stop gate が効くエージェント (Claude Code / Cursor / Codex / Qwen Code)** ではターン終了時に未対応 scar があると `kizu hook-stop` が exit 2 で stop を阻止し、stderr にファイル名と行番号が出力される
- **Stop gate のスキャン範囲は tracked + untracked の両方**（`git diff --name-only` + `git status --porcelain --untracked-files=all` を合成）。新規作成ファイルに刻んだ scar も検出される
- Gemini CLI 併用時は `gemini -p "..." --output-format stream-json | kizu consume-gemini-stream` で tool_use/tool_result を tail し、scar は書き込み側のみサポート（逆方向フィードバックなし）
- `kizu teardown` で全エージェントの hook 設定を一括除去する（選択 UI なし、検出した全てから外す）
- PostToolUse イベントログは **sanitize されたメタデータのみ**を `$XDG_STATE_HOME/kizu/events/` (Linux) / `~/Library/Application Support/kizu/events/` (macOS) に `0700` パーミッションで保存。`tool_input` / `tool_response` の生本文は保存しない。TTL 24 時間で自動削除、上限 1000 エントリ（v0.3 ストリームモードのメタデータ源）

scar まわりの非同期 feedback パターンの背景は `docs/inline-scar-pattern.md`、Claude Code hook の落とし穴は `docs/claude-code-hooks.md`、マルチエージェント調査結果は `deep-research-ai-agent-hooks.md` に一次情報が集約されています。このマイルストーンはこれらを Rust 実装に落とす作業です。

v0.3 スコープ（`--attach` でのターミナル分割、`/kizu` スラッシュコマンド、ストリームモード UI、`~/.config/kizu/config.toml`、Windsurf installer、Aider `--lint-cmd` 近似統合）はこの ExecPlan には含めません。v0.2 完了時点では `<state_dir>/events/` への sanitized メタデータ書き込みまで完了し、**UI 側の可視化は v0.3 に持ち越し**ます。

## Progress

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

### M1: delta 風背景色への UI 刷新 — 完了

- [x] (2026-04-15 13:00:00Z) M1: `src/ui.rs` の `render_diff_line` / `render_diff_line_wrapped` から `+`/`-`/` ` の prefix 列を除去し、全幅背景色 (`BG_ADDED = Color::Rgb(10, 50, 10)` / `BG_DELETED = Color::Rgb(60, 10, 10)`) で塗る設計に変更
- [x] (2026-04-15 13:00:00Z) M1: 旧 unit test `render_scroll_lines_carry_added_and_deleted_colors` と `selected_hunk_diff_lines_render_at_full_color` を新しい `Style.bg` / `Modifier::DIM` ベースのアサーションに書き換え (`render_scroll_lines_use_background_color_for_added_and_deleted`, `render_scroll_lines_omit_plus_minus_prefix`, `nowrap_added_row_background_extends_to_viewport_edge`, `selected_hunk_is_bright_and_unselected_hunk_is_dim` の 4 本)
- [x] (2026-04-15 13:30:00Z) M1: **方針変更** — cursor 行の `Modifier::REVERSED` は採用しない。左バー (`▎`/`▶`) と DIM コントラストで focus を伝える方が可読性が高く、REVERSED は dim な bg をコードテキストの fg に流入させて可読性を逆に落とすと判明 (ADR-0014 参照)
- [x] (2026-04-15 13:30:00Z) M1: 行折り返し / nowrap で背景色が端まで伸びるよう、body を `body_width` / `wrap_body_width` まで space padding する (nowrap は `area.width - 5`、wrap は `area.width - 6`)
- [x] (2026-04-15 13:30:00Z) M1: focused / unfocused の DIM コントラストを復活 — focused hunk は add/delete フル輝度 + context 端末既定色、unfocused hunk は `+ DIM`。v0.1 UX と等価の視線誘導を維持
- [x] (2026-04-15 14:00:00Z) M1: `docs/adr/0014-delta-style-background-diff.md` を Accepted で起票
- [x] (2026-04-15 13:30:00Z) M1: tuistory の `colors.test.ts` を layout 契約 (「body に `+`/`-` prefix が付かない」) に書き換え。色そのものは Rust 単体テストで pin 留め — tuistory の `foreground` / `background` filter が ratatui + crossterm 出力の `Rgb` bg を正確にマッチできない既知制約 (ADR-0004 の分担戦略を延長)

### M2: 言語コメント構文判定 — 完了

- [x] (2026-04-15 14:30:00Z) M2: `src/scar.rs` を新設し、`CommentSyntax` 構造体 + `detect_comment_syntax(path: &Path) -> CommentSyntax` を TDD で実装
- [x] (2026-04-15 14:30:00Z) M2: 拡張子マッピングを table-driven test で網羅 (`.rs/.ts/.tsx/.js/.jsx/.java/.go/.c/.cpp/.cc/.h/.hpp/.swift/.kt/.kts/.scala/.dart → //`, `.rb/.py/.sh/.bash/.zsh/.fish/.yaml/.yml/.toml/.ini/.conf/.r/.pl/.ex/.exs → #`, `.html/.htm/.xml/.svg/.vue/.md → <!-- -->`, `.css/.scss/.sass/.less → /* */`, `.sql/.lua/.hs/.ada/.sqlite → --`, 未知拡張子 / 拡張子なし → `#` フォールバック)。extension match は ASCII case-insensitive
- [x] (2026-04-15 14:30:00Z) M2: `CommentSyntax::wrap(body)` で generic な comment wrap、`CommentSyntax::render_scar(kind, body)` で `// @kizu[ask]: text` / `<!-- @kizu[free]: text -->` 等の scar 行を組み立て。`ScarKind::{Ask, Reject, Free}` の 3 種カテゴリを bracket tag として出力、hook 層が正規表現 `@kizu\[(\w+)\]:` で category 別抽出できる形に

### M3: scar writer — 完了

- [x] (2026-04-15 14:45:00Z) M3: `src/scar.rs` に `insert_scar(path: &Path, line_number: usize, text: &str) -> Result<()>` を TDD で実装（10 本の test で clamp / idempotent / CRLF / LF / HTML / Python / missing file / unknown ext を固定）
- [x] (2026-04-15 14:45:00Z) M3: 変更行の直上に 1 行挿入する（`line_number` は 1-indexed、`split_inclusive('\n')` で既存の line ending を失わずに挿入位置を決める）。既に同一内容の `@kizu[*]:` が前行にあれば no-op でべき等化
- [x] (2026-04-15 14:45:00Z) M3: 既存ファイルの line ending を保存する（`\r\n` が file 内に 1 つでもあれば scar line も `\r\n`、なければ LF）
- [x] (2026-04-15 14:45:00Z) M3: 書き込み失敗時は `anyhow::Error` を返し panic しない。`App.last_error` に流し込むのは M4 の app.rs 層の責務 (insert_scar は `Result<()>` を返すだけ)

### M4: App キーバインディング（scar 5 種 + e） — 完了

- [x] (2026-04-16) M4: **picker の起動キーを `Space` → `s` に変更**。`J`/`K` を 1 行移動、`h`/`l` を hunk jump に再配置
- [x] (2026-04-16) M4: `a`/`r` canned scar, `c` ScarComment overlay (フッター上独立入力行 + 折り返し + CJK カーソル), `x` revert with y/N confirm, `e` external editor (vim +N vs zed file:N 自動判別), `Space` seen mark toggle
- [x] (2026-04-16) M4: J/K 1行移動のアニメーション抑制 (key-repeat jitter 修正), file view でも e キー対応
- [x] (2026-04-16) M4: bracketed paste + `Event::Paste` for IME input, `handle_paste` routes to active overlay
- [x] (2026-04-16) M4: 入力カーソル移動 (Left/Right/Home/End/Ctrl-A/E) + ブロックカーソル表示 + unicode-width
- [x] (2026-04-16) M4: 削除行 scar テスト (all-deleted hunk edge case 含む)

### M4b: vim スタイルキーワード検索 — 完了

- [x] (2026-04-16) M4b: `SearchState` + `SearchInputState` + `find_matches` (smart case) + `/` key + `n`/`N` wrap-around navigation
- [x] (2026-04-16) M4b: フッター上の検索入力行 (`/query_`) + Esc cancel + Enter confirm → first match jump

### M4c: Enter ファイルビュー zoom — 完了

- [x] (2026-04-16) M4c: `FileViewState` + Enter opens (reads worktree file + maps hunk Added lines to BG_ADDED) + Esc/Enter exits + scroll restore
- [x] (2026-04-16) M4c: j/k chunk scroll + J/K 1行移動 + g/G top/bottom + Ctrl-d/u half-page + e external editor
- [x] (2026-04-16) M4c: `render_file_view` with syntax highlighting, char-aware truncation (UTF-8 multibyte safe)

### M5: hook サブコマンド — 完了

- [x] (2026-04-16) M5: `src/hook.rs` — `AgentKind` (5 agents), `NormalizedHookInput` (stdin JSON parser), `scan_scars` (行頭コメント構文 regex で false positive 排除), `format_additional_context`, `format_stop_stderr`
- [x] (2026-04-16) M5: `kizu hook-post-tool --agent <name>` — stdin JSON → scar scan → additionalContext stdout
- [x] (2026-04-16) M5: `kizu hook-stop --agent <name>` — `stop_hook_active` check → session-baseline scoped file scan → exit 2 + stderr
- [x] (2026-04-16) M5: `kizu hook-pre-commit` — staged ファイルに scar があれば exit 1 で commit ブロック
- [x] (2026-04-16) M5: `src/paths.rs` — XDG state dir 解決 (`$KIZU_STATE_DIR` override 可)
- [x] (2026-04-16) M5: `src/session.rs` — session baseline file (SHA + PID) write/read/remove + `is_session_alive` (libc kill -0)
- [x] (2026-04-16) M5: Stop hook は `enumerate_session_files` で baseline 以降の変更ファイル + untracked のみスキャン (docs 例文の false positive 排除)
- [ ] M5 deferred: `hook-log-event` (event log sanitization + TTL prune) は v0.3 に先送り

### M6: エージェント検出 — 完了

- [x] (2026-04-16) M6: `src/init.rs` に `detect_agents()` — `which` + project/user config dir チェック、6 エージェント対応
- [x] (2026-04-16) M6: `SupportLevel` enum (Full / StopOnly / PostToolOnlyBestEffort / WriteSideOnly)、Windsurf は v0.2 対象外

### M7: `kizu init` インタラクティブ — 完了

- [x] (2026-04-16) M7: `dialoguer` MultiSelect (agent) + Select (scope) によるインタラクティブ init。`--agent`/`--scope`/`--non-interactive` フラグで CI 対応
- [x] (2026-04-16) M7: scope 3 種: `project-local` (settings.local.json, gitignored, デフォルト) / `project-shared` (settings.json) / `user` (~/.claude/settings.json)
- [x] (2026-04-16) M7: Claude Code hook schema: `matcher` + `hooks` 配列ネスト構造 (`[{matcher: "Edit|Write|MultiEdit", hooks: [{type: "command", command: ...}]}]`)
- [x] (2026-04-16) M7: 6 agent installer: Claude Code / Cursor / Codex (Stop only) / Qwen / Cline (PostToolUse file-based) / Gemini (README only)
- [x] (2026-04-16) M7: git pre-commit hook 設置 (既存フックに追記、冪等)
- [x] (2026-04-16) M7: カラフル UI (バナー、support level 色分け、検出ステータスアイコン)

### M8: `kizu teardown` — 完了

- [x] (2026-04-16) M8: 全 scope (settings.json + settings.local.json + user) から kizu hook entries を除去。ユーザーの他 hook は保持
- [x] (2026-04-16) M8: git pre-commit hook から kizu 行を除去 (空になれば削除)
- [x] (2026-04-16) M8: session file 削除
- [x] (2026-04-16) M8: カラフル UI (init と統一スタイル)

### M9: e2e + ドキュメント — 完了

- [x] (2026-04-16) M9: e2e `scar.test.ts` — `a` ask scar / `c` free scar / `x` revert / `e` EDITOR smoke (4 tests)
- [x] (2026-04-16) M9: e2e `init.test.ts` — non-interactive init / idempotency / teardown / interactive pty (4 tests)
- [x] (2026-04-16) M9: `CLAUDE.md` v0.2 実装メモ追加 (scar, hook, session baseline, init/teardown, diff view, file view, 検索, 入力フィールド)
- [x] (2026-04-16) M9: `README.md` ステータス + フェーズ記述を v0.2 に更新
- [x] (2026-04-16) M9: `just ci` 全 green (255 Rust + 19 e2e)

### 追加実装 (計画外)

- [x] (2026-04-16) シンタックスハイライト: `src/highlight.rs` — syntect lazy-init via OnceCell、diff view + file view 両方、base16-eighties.dark テーマ
- [x] (2026-04-16) `RowRenderCtx` struct: render_row の 9 引数を構造体に集約
- [x] (2026-04-16) hunk header scar 位置修正: leading context をスキップして最初の変更行直上に配置

## Surprises & Discoveries

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

### 着手前に既知の landmine（deep-research-ai-agent-hooks.md より先行記録）

- Observation: OpenAI Codex CLI の `PreToolUse` / `PostToolUse` matcher は現時点で **Bash ツール限定**で、Write/Edit 系ツールには刺さらない。
  Evidence: `developers.openai.com/codex/hooks` の "Matcher Patterns" セクションで `PostToolUse / PreToolUse: tool name (e.g., "Bash")` と明記。
  Impact: Codex 統合では `hook-post-tool` を諦め、`Stop` hook + git diff grep の最終チェックだけに絞る。M7 の `CodexInstaller` は file edit 用 hook を設置せず、Stop hook のみ書き込む。
- Observation: Cursor の hook は **production ステータス**で、`CLAUDE_PROJECT_DIR` 環境変数まで exposed している。
  Evidence: `cursor.com/docs/hooks` の Environment Variables 表に `CLAUDE_PROJECT_DIR: Alias for project dir` とある。
  Impact: Cursor と Claude Code は hook コマンド側のコードパスをほぼ共有できる。`CursorInstaller` は Claude Code 相当の JSON を `.cursor/hooks.json` (version: 1) に書くだけで良い。
- Observation: Cline は hook を **ファイル配置型**で設定する（`.clinerules/hooks/PostToolUse` というファイル名そのものが event binding）。
  Evidence: `cline.ghost.io/cline-v3-36-hooks/` の "Types of Hooks" セクション。
  Impact: `ClineInstaller` だけ他と実装形態が違う。JSON マージではなく、実行可能スクリプトを 1 本配置する形になる。kizu 本体を呼ぶ shell wrapper を生成する。
- Observation: Cline は **macOS / Linux のみ**対応、Windows 非対応。
  Evidence: 同上 "Platform Support" セクション。
  Impact: `ClineInstaller` を Windows でビルドはするが、install 時に `cfg(not(target_os = "windows"))` でガードするか、warning を出して skip する。
- Observation: Gemini CLI は hook 機構が無く、`--output-format stream-json` の JSONL stream tail が唯一の統合点。
  Evidence: `geminicli.com/docs/cli/headless/` の stream-json ドキュメント。
  Impact: `GeminiStreamInstaller` は設定ファイルを触らず、`kizu consume-gemini-stream` 使用方法を標準出力に表示するだけ。双方向フィードバックは v0.2 スコープ外。
- Observation: Roo Code / Zed / Continue.dev は現時点で hook 機構を持たない（Roo Code は issue #11504, #12025 で申請中）。
  Evidence: 各公式ドキュメント + GitHub issue 確認済み。
  Impact: v0.2 の自動検出には含めない。将来 hook が追加されたら ExecPlan v0.3 で取り込む。
- Observation: v0.1 の素直な hook-stop 設計（`git diff --name-only | xargs grep -ln '@kizu\['`）は **untracked ファイルを取りこぼす**。
  Evidence: `git diff --name-only` の man page、および `src/git.rs` で既に `git status --porcelain --untracked-files=all` を使って untracked 合成している事実の交差参照。
  Impact: AI エージェントが新規作成したファイルに刻んだ scar は Stop gate を素通りする。これは AI の最頻出 action に対する穴で、critical 扱い。M5 で `enumerate_repo_files_with_scars` を導入して tracked + untracked を合成する。Codex adversarial review #2 で指摘された事項。
- Observation: `/tmp` への raw hook JSON 保存は **他 local user から読める** + 本文に秘密情報が混ざりうる。
  Evidence: POSIX デフォルトの `/tmp` permission (`1777`) と、Claude Code hook の `tool_input.content` / `tool_response.output` に edit したファイル本文がそのまま載る事実。
  Impact: v0.2 のまま ship すると情報漏洩パスを作る。M5 で sanitize + private state dir + 0700 permission + TTL を 3 点セットで導入。Codex adversarial review #1 で指摘された事項。
- Observation: Cline は Stop 相当 hook を持たない（PreToolUse / PostToolUse / UserPromptSubmit / TaskStart のみ）。
  Evidence: `cline.ghost.io/cline-v3-36-hooks/` の "Types of Hooks" セクション、deep-research 結果で再確認済み。
  Impact: Cline を「Claude Code parity」と宣伝するのは false safety。PostToolUse wrapper のみ配置する best-effort 統合にとどめ、ユーザーに対して明示的に warning を出す必要がある。Codex adversarial review #3 で指摘された事項。
- Observation: M1 実装中に、v0.1 の素直な `PollWatcher + compare_contents=true + Recursive(worktree)` 設計が起動時 `target/` 42,647 files / 1.3 GB の initial content scan で **3.5 秒フリーズ**することが発覚した。
  Evidence: `KIZU_STARTUP_TIMING_FILE` 計測で `watcher::start` が単独で 3.515 秒、他の startup step は合計 42ms 未満。
  Impact: v0.1 時代から潜んでいた UX regression。ADR-0015 で `target` / `node_modules` 等 15 種の top-level dir を hardcoded exclusion list で skip し、worktree root は `NonRecursive` + top-level children の `Recursive` watch の 2 層構成に変更して解決。ADR-0005 の「kizu 側で .gitignore filter を持たない」は維持。修正本体は Codex rescue で実装 (145 → 147 Rust test + 新規 `worktree_watcher_skips_target_directory` / `worktree_watcher_still_sees_root_level_file_writes` 各 1 本)。
- Observation: M1 の初期実装で `Modifier::REVERSED` を focused hunk の body style に載せたところ、REVERSED が bg/fg を swap してコードテキストが `Rgb(10, 50, 10)` の濃い緑で描画され、読めなくなった。
  Evidence: `selected_hunk_diff_lines_render_at_full_color` 相当の手動確認で fg が BG_ADDED 値に swap されていることを観察。
  Impact: ExecPlan 起草時の「REVERSED で cursor 行を反転」戦略は不採用に。左バー (`▎`/`▶`) と DIM コントラストで focus を伝える方針に切り替え (ADR-0014 で明記)。
- Observation: M1 の最初の drop では `is_selected` を無視して全 add/delete 行を uniform `bg` / 全 context 行を `DarkGray + DIM` にしたところ、focused hunk の context コードまで暗くなり「差分以外が暗すぎる」UX regression が発生した。
  Evidence: ユーザーからの直接フィードバック (2026-04-15 実機)。
  Impact: v0.1 の focused/unfocused コントラスト (focused = bright, unfocused = DIM) を復活。context 行も focused なら端末既定色、unfocused なら dark-gray + DIM に戻す。ADR-0014 の Decision 4 に最終ルールとして記載。
- Observation: Windsurf の file-edit hook 名は公式ドキュメントで未確定。
  Evidence: `docs.windsurf.com/windsurf/cascade/hooks` を調べた deep-research セクションで「実装前に再確認」と明示、file edit 特化 event 名が出てこない。
  Impact: 推測スキーマで `.windsurf/hooks.json` を書き込むと、既存 hook を silently 破壊するか完全 no-op になる。v0.2 では installer 自体を作らず、`AgentKind::Windsurf` を feature-gate する。Codex adversarial review #4 で指摘された事項。

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

- Observation: …
  Evidence: …

## Decision Log

Record every decision made while working on the plan.

### 着手前に決定済み

- Decision: diff 表示を `+`/`-` prefix ではなく背景色（delta 風）に全面移行する
  Rationale: 視線移動のコストが下がり、シンタックスハイライト未実装でも視覚的情報量が増える。syntect の lazy-load は v0.1.1 で棚上げしていたが、背景色ベースに切り替えるとハイライトなしでも十分見やすくなるため、syntect 導入自体を v0.3 以降へ先送りする
  Date/Author: 2026-04-15 / Initial implementer
- Decision: `e` キーで `$EDITOR +<line> <file>` を spawn して外部エディタを開く
  Rationale: scar 挿入では表現しきれない複雑な介入（hunk 全体の書き換え、別ファイルの参照）を kizu から離脱せずに行えるようにする。`$EDITOR` は POSIX 的な最低共通項
  Date/Author: 2026-04-15 / Initial implementer
- Decision: マルチエージェント hook 層は **Claude Code 形式を正規系**として、他エージェントは shim で吸収する
  Rationale: Cursor / Qwen Code / Cline がほぼ Claude Code 形式をコピーしており、今後追加される後発エージェントもこの形式に収束する見込みが高い。内部データ構造を `NormalizedHookInput` で一本化すれば `hook-post-tool` 本体は 1 コードパスで済む
  Date/Author: 2026-04-15 / Initial implementer
- Decision: 先行統合対象は Claude Code / Cursor / Codex CLI / Qwen Code / Cline / Gemini CLI の **6 種**。Windsurf / Roo Code / Zed / Continue.dev / Aider は v0.2 では対応しない
  Rationale: Roo Code / Zed / Continue.dev は hook 機構を持たない。Aider は `--lint-cmd` 近似統合が exit code 意味論のズレで ExecPlan を汚すため v0.2.1 以降。**Windsurf は公式ドキュメントの file-edit hook 名が未確定**で、同じ deep-research 内でも「実装前に再確認」とマーク済み → 推測スキーマで書き込むとユーザーの editor 設定を静かに壊す可能性があるため v0.2 では外す（v0.2.1 以降で schema 検証 + fixture test を整えてから追加）
  Date/Author: 2026-04-15 / Initial implementer (ref: deep-research-ai-agent-hooks.md, Codex adversarial review #4)
- Decision: Cline 統合は **PostToolUse best-effort のみ**とし、「Claude Code と parity」の主張はしない
  Rationale: Cline は Stop 相当 hook を持たない（v3.36 時点で PreToolUse / PostToolUse / UserPromptSubmit / TaskStart のみ、`cline.ghost.io/cline-v3-36-hooks/` より確認済み）。最終チェック機構が無いため、scar を刻んでもエージェントがそれを無視して task を終えた場合、kizu は止められない。v0.2 では PostToolUse wrapper だけ配置し、`InstallReport.warnings` と `kizu init` UI で parity 非保証を明示する。Success Criteria から「全エージェント parity」の主張を外す
  Date/Author: 2026-04-15 / Initial implementer (ref: Codex adversarial review #3)
- Decision: `hook-stop` の scar scan は **tracked + untracked の両方**を対象にする。`git diff --name-only` 単独は使わない
  Rationale: `git diff --name-only` は untracked ファイルを返さないため、AI エージェントが新規作成したファイルに scar を刻んだ場合に最終チェックが素通りする false negative が発生する。これは AI の common action（新規ファイル作成）に対する致命的な穴。v0.1 の `src/git.rs` は既に `git status --porcelain --untracked-files=all` で untracked 合成を実装済みなので、そのロジックを hook-stop から再利用する。kizu 本体の diff view と hook-stop の scan 対象が**同じ file set**である不変条件を守る
  Date/Author: 2026-04-15 / Initial implementer (ref: Codex adversarial review #2)
- Decision: event log は **`/tmp/kizu-events/` ではなく private state dir** に保存し、**生 stdin JSON ではなく sanitize 済みメタデータ**のみを書き出す
  Rationale: `/tmp` は POSIX デフォルトで他 local user から読めるため、hook stdin に含まれる `tool_input.content` / `tool_response.output` 等の本文（ファイル内容、プロンプト、秘密情報を含みうる）が漏洩する。retention 無し設計では放置すると際限なく蓄積する。対策は 3 層: (1) 保存先を `$XDG_STATE_HOME/kizu/events/` 等の private state dir、dir `0700` / file `0600`、(2) `tool_input` / `tool_response` の本文フィールドを捨て、`SanitizedEvent { session_id, hook_event_name, tool_name, file_paths, cwd, timestamp_ms }` のみ保存、(3) TTL 24 時間 + 上限 1000 エントリで prune。v0.3 ストリームモードは metadata だけで動くよう設計する
  Date/Author: 2026-04-15 / Initial implementer (ref: Codex adversarial review #1)
- Decision: `kizu init` はインタラクティブ multi-select をデフォルトとし、`--agent` / `--non-interactive` で CI 用に飛ばせるようにする
  Rationale: 初めて触るユーザーが 7 エージェントの設定ファイル場所を覚える必要をなくす + CI で `kizu init --agent claude-code --non-interactive` のように冪等実行できる
  Date/Author: 2026-04-15 / Initial implementer
- Decision: 既存 hook 設定が存在するエージェントでは、kizu の hook を **既存設定とマージ**する（上書きしない）
  Rationale: ユーザーが既に prettier や lint を hook に積んでいる可能性があり、単純上書きは破壊的。kizu 由来 entry は `command` が `kizu hook-` で始まることで識別可能
  Date/Author: 2026-04-15 / Initial implementer
- Decision: `kizu teardown` はインタラクティブ選択なし、検出した全エージェントから **一括で** kizu 由来 hook を剥がす
  Rationale: teardown は install と対称にしない。ユーザーは「全部外したい」という意図で呼ぶことが大半であり、選択 UI はむしろ摩擦。個別外しは設定ファイルを手で編集する方が早い
  Date/Author: 2026-04-15 / Initial implementer
- Decision: picker の起動キーを v0.1 の `Space` から `s` に付け替える
  Rationale: SPEC.md で `Space` は scar の「見た」マークに予約されているため、v0.2 で scar を実装する段階で衝突が表面化する。`s` は v0.1 で未使用かつ "select" の連想で記憶しやすい。既存の popup picker の意味論（modal、fzf 互換ナビ、`Esc` でキャンセル）はそのまま流用
  Date/Author: 2026-04-15 / Initial implementer (ref: ADR-0006 / 新規 ADR-0015 予定)
- Decision: `J`/`K` を厳密な hunk jump から **visual row 1 行単位のカーソル移動**に作り替え、hunk jump は `h`/`l` に移設する
  Rationale: scar は行単位の punctuated 操作のため、adaptive `next_change` (`j`/`k`) では行の指差し精度が足りない。1 行移動専用キーが必要。`J`/`K` の従来役割である hunk jump は kizu の縦巻物 UI では使われない `h`/`l` に転用する。`j`/`k` の adaptive 挙動は巻物 navigation の体験として価値があり据え置く
  Date/Author: 2026-04-15 / Initial implementer (ref: ADR-0015 予定)
- Decision: vim スタイルの `/` 検索を導入し、`n`/`N` で next/prev、大文字小文字は smart case
  Rationale: AI エージェントが大きく書き換えた diff の中から「auth」「token」といった特定キーワードを含む変更だけを拾いたい場面が実運用で頻出する。巻物全体に対する substring 検索は scar の指差し精度をさらに高める。vim 互換の意味論は学習コストゼロ
  Date/Author: 2026-04-15 / Initial implementer
- Decision: `Enter` キーで選択中 hunk から **ファイルビュー** に zoom 入り、`Enter` / `Esc` で通常モードに戻る (4 段階 cycle は採用しない)
  Rationale: コンテキスト行を段階的に増やす 4 段階 cycle はキー連打と視覚的変化が噛み合わず、「その hunk がファイル全体のどこに位置しているか」を掴むには結局ファイル全体を見たくなる。Zoom-in モデルなら 1 打で最大情報量に到達し、1 打で戻れる。scar キーはファイルビュー中も有効にして、hunk 外の行にも scar を刻める。外部エディタ起動 (`e`) と棲み分ける: `e` は編集、ファイルビューは「読む」
  Date/Author: 2026-04-15 / Initial implementer
- Decision: `*` (word-under-cursor 検索) は v0.2 スコープから外す
  Rationale: visual row の byte offset からカーソル位置の word を切り出すロジックが scroll layout の折り返し規則と絡んで複雑化する。`/` で query をタイプする workflow が確立すれば `*` の必要性は相対的に下がる。将来必要になれば v0.3 以降で再評価
  Date/Author: 2026-04-15 / Initial implementer
- Decision: scar dedupe は `/tmp/kizu-scars/<session_id>.json` にハッシュを蓄積する方式にする
  Rationale: Claude Code hook は 1 ターン中に同じファイルを複数回編集する可能性があり、同じ scar を毎回 `additionalContext` として返すとノイズになる。`(path, line, message_hash)` で一意化する
  Date/Author: 2026-04-15 / Initial implementer
- Decision: Codex CLI 統合では `hook-post-tool` を設置せず `hook-stop` のみ書き込む
  Rationale: Codex の PreTool/PostTool matcher が Bash ツール限定で、file edit に刺さらない（Surprises セクション参照）。Stop hook + git diff grep で最終チェックすれば scar 機能は成立する
  Date/Author: 2026-04-15 / Initial implementer
- Decision: Cline は `.clinerules/hooks/PostToolUse` ファイルに kizu 本体を呼ぶ **shell wrapper** を 1 本配置する
  Rationale: Cline は他と異なりファイル配置型の hook なので、JSON マージではなく実行可能ファイルの設置になる。wrapper の中で `kizu hook-post-tool --agent cline` を呼ぶことで、本体ロジックは共通コードパスに戻せる
  Date/Author: 2026-04-15 / Initial implementer
- Decision: Gemini CLI は設定ファイルを触らず `kizu consume-gemini-stream` 使用方法の README 出力のみ
  Rationale: Gemini に hook 機構がないため強制的に pipe モードになる。install 時に設定ファイルを作ろうとしても適切な置き場が無い
  Date/Author: 2026-04-15 / Initial implementer

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

- Decision: `x` (hunk revert) は **確認ダイアログを 1 クッション経由**する
  Rationale: scar (`a`/`r`/`c`) はコメント行を追加するだけで元コードを失わないのに対し、revert は AI エージェントの書き換えを worktree から消す破壊的操作。単打キー 1 つで即実行するとホームポジションでの誤爆 (`x` は `c` の隣) が発生した時に取り返しがつかない。確認ダイアログを 1 段入れることで「本当に消していいか」を認知するワンクッションを強制する。picker / ScarComment と同じ `Option<RevertConfirmState>` オーバーレイ方式で管理し、`y`/`Y`/`Enter` で確定、`n`/`N`/`Esc`/他キーで no-op
  Date/Author: 2026-04-16 / Initial implementer (ref: user feedback during M4 implementation)

## Outcomes & Retrospective

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

- Big Picture で宣言した観察可能挙動が全て実装され、e2e / Rust test で守られているか
- deep-research で出した Tier A/B/C 戦略の 3 階層通りに分かれているか
- 各エージェント installer の実装コストが見積もり通りだったか、差分があれば原因
- scar dedupe / merge ロジックが運用に耐えたか、手戻りがあったか
- v0.3（`--attach` / ストリームモード UI / 設定ファイル）へ持ち越した事項

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

## Context and Orientation

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

kizu はカレントディレクトリの git worktree を監視する単一バイナリの TUI です。ファイル構成:

- `src/main.rs` (46 行) — `#[tokio::main(flavor = "current_thread")]` で `app::run` を呼ぶエントリポイント
- `src/app.rs` (4082 行) — TUI 状態機械、`App` 構造体、`handle_key`、watcher イベント統合
- `src/git.rs` (1445 行) — git CLI shell out、`compute_diff`、`parse_unified_diff`、`find_root`、`head_sha`、`git_dir`
- `src/watcher.rs` (1138 行) — `notify-debouncer-full` ラッパ、worktree 300ms / git_dir 100ms デバウンス
- `src/ui.rs` (1699 行) — ratatui フルスクリーン縦巻物 + popup picker

v0.1 では `a` / `r` / `c` / `x` / `e` の key dispatch は未実装。`a` `r` `c` は現在 picker の query 入力として吸収されるため、通常モードでは no-op。`src/app.rs` の `handle_key` 関数に 2 モード dispatch (通常 / picker) があるので、通常モード側で先に scar キーを捕捉する必要があります。

**重要**: v0.1 では **`Space` キーが picker の起動トリガ**に割り当てられています。v0.2 では scar の「見た」マーク (SPEC.md 既定) に `Space` を使うため、picker トリガを `Space` → `s` に付け替えます。`s` はまだ未使用のキーで、単打で picker をポップアップさせる既存意味論は保たれます。

同時にカーソル移動系のキー配置も変更します。v0.1 の現状は以下:

- `j`/`k` (および `↓`/`↑`): adaptive `next_change` / `prev_change` — 長い hunk 内では chunk scroll、短い hunk では hunk 単位の collapse 移動
- `J`/`K`: 厳密な `next_hunk` / `prev_hunk` — hunk サイズに関わらず次の hunk header へ飛ぶ

v0.2 ではこれを以下に変更します:

- `j`/`k` (および `↓`/`↑`): **据え置き**（adaptive `next_change` の挙動は触らない）
- `J`/`K`: **visual row 1 行単位のカーソル移動**に作り替え（新規追加メソッド）
- `h`/`l`: 現行の `J`/`K` の hunk jump 挙動を引き継ぐ

`J`/`K` を 1 行移動に落とすのは、scar 挿入時に「この行」を精密に指し示したいためです。adaptive 移動は巻物 navigation には最適ですが、scar は行単位の punctuated 操作であり、別キーでの単行移動が必要になります。`h`/`l` は vim の水平移動から転用する形ですが、kizu は縦巻物 UI のため水平移動は存在しないので、空いた 2 キーを hunk jump に転用します。

baseline SHA は起動時の `git rev-parse HEAD` で取得し、`App.baseline` に保持されます。初回コミットがない repo では空 tree SHA (`4b825dc642cb6eb9a060e54bf8d69288fbee4904`) にフォールバックします。`R` キーで再取得可能。

### 専門用語の定義

- **scar**: AI コーディングエージェントに対する非同期レビューとしてソースファイル内に直接書き込まれる `@kizu[ask|reject|free]: ...` コメント。コードコメント構文で書かれ、エージェントは次の Read/Edit でこれを認識する。bracket 部分の tag で category が一意に grep できる。`docs/inline-scar-pattern.md` 参照
- **hunk**: unified diff の 1 連続した変更ブロック。`@@ -L,S +L,S @@` ヘッダで始まる単位
- **PostToolUse hook**: AI エージェントが Write/Edit ツール実行直後に呼ぶ外部コマンド。stdin に JSON を流し込み、exit code と stdout でエージェントに feedback を返す
- **Stop hook**: AI エージェントがターンを閉じようとする瞬間に呼ぶ外部コマンド。exit 2 で stop を阻止すると次ターンに stderr 内容が指示として渡される
- **additionalContext**: Claude Code hook の出力 JSON フィールド (`hookSpecificOutput.additionalContext`)。次のターン冒頭にエージェントへ渡される追加コンテキスト文字列
- **stop_hook_active**: Stop hook 入力 JSON のフィールドで、これが true のときに Stop hook が exit 2 を返すと無限ループになるため、kizu 側で true を見たら即 exit 0 しなければならない
- **NormalizedHookInput**: kizu 内部でエージェント間差分を吸収した正規化済みデータ構造。`session_id` / `tool_name` / `file_paths` / `cwd` / `stop_hook_active: bool` を持つ
- **InputMode**: `App.input_mode: InputMode` の取る値。`Normal` / `Picker` / `ScarComment` / `Search { direction }` の 4 種（v0.2 で `ScarComment` と `Search` を追加）
- **smart case**: 検索 query が全小文字なら case-insensitive、大文字を 1 文字でも含めば case-sensitive として扱う matching 戦略。vim の `'smartcase'` と同じ意味論
- **file view**: `Enter` で遷移する zoom-in モード。1 ファイル全体を描画し、hunk 行には `BG_ADDED` / `BG_DELETED` を残したまま、diff に含まれていなかった行も context として表示する。`Enter` / `Esc` で通常モード (scroll view) に戻る。`App.input_mode::FileView { file_idx, cursor_line, scroll }` で保持

### 関連ドキュメント（ExecPlan 内で全て self-contained にする方針だが、一次情報の所在を記録）

- `docs/SPEC.md` — 全体仕様、v0.2 の scope 定義
- `docs/claude-code-hooks.md` — PostToolUse/Stop hook の入出力と落とし穴
- `docs/inline-scar-pattern.md` — scar パターンの設計理由
- `deep-research-ai-agent-hooks.md` — 10 ツールの hook 機構調査結果（この ExecPlan の hook 層設計の一次情報）

## Plan of Work

### M1（delta 風背景色）を最初に置く理由

UI の全幅レイアウトに手を入れる変更は scar 機能とは独立に完結するため、まずこれを終わらせて M2 以降と干渉しないようにします。`src/ui.rs` の `render_scroll_line` / `render_diff_row` で `+`/`-` prefix 列を組み立てている箇所を特定し、その列の幅を 0 に、代わりに `Line` 全体の `Style.bg` を `LineKind::Added → Color::Rgb(10, 50, 10)` / `LineKind::Deleted → Color::Rgb(60, 10, 10)` に設定します。背景色は dim 寄りの値にしてテキストとのコントラストを維持します（delta の `--dark` プロファイル参考値）。

visual cursor の選択行表示は現在 `Modifier::REVERSED` で実装しているので、そのまま流用可能です。REVERSED は fg/bg を swap するため、背景色ベースでも正しく反転します。

行折り返し時の背景色尾引きは ratatui の既知の挙動で、`Line` にした時点で末尾に到達したら背景色が途切れます。`Paragraph` の背景は行ごとではなく Block ごとに指定されるため、折り返し行も背景色を継続させるには `pad_line_to_width` のようなヘルパで末尾スペースを埋める必要があります。M1 のユニットテストでこの尾引きを検証する行を追加します。

### M2-M3（scar のコア）を pure function で切る

`src/scar.rs` を新設し、副作用の無い `detect_comment_syntax` と `render_comment` を最初に書きます。TDD の Red を極力長く取り、`compute_diff` が純粋関数だった v0.1 と同じ戦略を踏襲します。`insert_scar` だけは `std::fs` を触るので、テストは `tempfile::tempdir` で実 FS を使う integration style にします。

### M4（App へのキー統合）

`src/app.rs` の `handle_key` に通常モードの scar dispatch を挿入する場所は、既存の `j` / `k` / `R` / `q` ブロックの直前です。picker モード中は従来通り query 入力として吸収されるので、`InputMode::Picker` の case は触りません。

`c` の入力欄は新しい `InputMode::ScarComment` を追加して実装します。`ui.rs` のフッタ描画に 1 行追加し、`input_mode == ScarComment` のとき `> <current_text>_` を出します。`Enter` で確定、`Esc` でキャンセル、`Backspace` で 1 文字削除、それ以外の char は append。

`e` のエディタ起動は生々しい端末制御を伴います。`ratatui::DefaultTerminal` の `draw` を止めて、`crossterm` で alt screen を leave + raw mode を disable、`tokio::process::Command::new(editor).arg(format!("+{line}")).arg(path).status().await?` を await、復帰後に再度 alt screen enter + raw mode enable + `terminal.clear()?` で再描画します。これを `App::open_editor(&mut self, terminal: &mut Terminal) -> Result<()>` として切り出します。

`x` の hunk revert は git CLI shell out で行います。`git.rs` に `revert_hunk(root: &Path, patch: &str) -> Result<()>` を追加し、`git apply --reverse --` に hunk 本文を stdin で流し込みます。hunk 本文は `App.layout` の現在カーソル位置から逆算します。

### M5（hook サブコマンド）

`src/hook.rs` を新設し、`main.rs` の clap subcommand dispatch から呼び出します。全 hook サブコマンドの構造:

1. stdin を `serde_json::from_reader` で `serde_json::Value` に読み取り
2. `AgentKind` を `--agent` フラグから取得（default: `claude-code`）
3. エージェント別のフィールド名で `file_paths` を抽出（Claude Code は `tool_input.file_path`、Cursor は `tool_input.file_path`、Qwen Code は Claude Code 互換、Cline は `tool_input.path`、Windsurf は要再確認）
4. `scan_scars(&file_paths)` で未対応 scar を列挙
5. dedupe state を `/tmp/kizu-scars/<session_id>.json` から読み、既通知分を除外
6. エージェント別の envelope で JSON を stdout に出力、exit code を決定

`hook-log-event` は最もシンプルで、stdin をそのまま `/tmp/kizu-events/<ts>-<tool_name>.json` に atomic write するだけです。ファイル名衝突時は `-<nanos>` サフィックスで回避します。

### M6-M7（`kizu init` インタラクティブ）

`detect_agents` は `std::env::var("PATH")` を split して各バイナリ名を `which::which` でチェックするのと、`<project>/.claude/`、`<home>/.codex/` 等のディレクトリ存在チェックを並列で行います。7 エージェントだけなので `futures::future::join_all` で同時実行しても数十 ms 以内で終わるはずです。

`dialoguer::MultiSelect` は TTY を要求します。非 TTY 環境（CI）では `--non-interactive` 必須にし、指定なしで non-TTY を検出したらエラー終了します。

マージロジックは agent ごとに実装が違います:

- **Claude Code**: `.claude/settings.json` は JSON。`hooks.PostToolUse` / `hooks.Stop` 配列に kizu entry を push。既存 entry の `command` が `kizu hook-` で始まるものは skip（再 install 時のべき等）
- **Cursor**: `.cursor/hooks.json` は `{ version: 1, hooks: { afterFileEdit: [...], stop: [...] } }`。同様に push + dedupe
- **Codex**: `~/.codex/hooks.json` は Claude Code 相当。ただし PostToolUse entry は設置せず Stop のみ
- **Qwen Code**: `.qwen/settings.json` は Claude Code 相当
- **Cline**: `.clinerules/hooks/PostToolUse` に shell wrapper を配置。既存ファイルがあれば中に `kizu hook-post-tool --agent cline` 行を追加、無ければ新規作成
- **Windsurf**: `.windsurf/hooks.json` (Cascade Hooks 最新形式)。実装前に `docs.windsurf.com/windsurf/cascade/hooks` で event 名を再確認
- **Gemini CLI**: 何もしない、使用方法の stdout 出力のみ

`AgentInstaller` trait を定義して、各 installer を実装クラスとして持たせます。`run_init` はこの trait オブジェクトの Vec を順に呼ぶだけのシンプルなドライバに保ちます。

### M8（`kizu teardown`）

install の逆操作。`AgentInstaller::uninstall` で各設定ファイルから kizu 由来 entry を削除します。空になった `hooks.PostToolUse` 配列や `hooks` オブジェクトは削除し、元が空だった key は書き戻さない（完全に install 前の状態に戻す）。

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

Rust integration test は `tests/hook/*.rs` と `tests/init/*.rs` に置きます。`tests/e2e/` の TypeScript は既存の helpers (`createTempRepo`, `launchKizu`) を使って scar キー操作とファイル内容の assertion を追加します。

## Concrete Steps

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

### M1 着手

```bash
just rust                              # v0.1 の状態が green であることを確認
cargo test render_scroll_lines_carry   # 既存の色検証テストを 1 本走らせる
```

`src/ui.rs` の `render_scroll_line` を編集し、`prefix_char` 変数を削除、`Style::default().bg(...)` を追加。テストを Red → 修正 → Green。

```bash
cargo test ui::tests                   # Red を確認
# (編集)
cargo test ui::tests                   # Green を確認
just e2e                               # tuistory の color test が壊れる → background 方式で書き直し
```

期待される最終出力:

```
test result: ok. 42 passed; 0 failed
```

### M2-M3 着手

```bash
touch src/scar.rs
```

`src/lib.rs` (なければ `src/main.rs` の `mod scar;` 宣言) に登録。TDD で `detect_comment_syntax` から書き始める。

```bash
cargo test scar::                      # Red → Green のループ
```

### M4 着手

`src/app.rs` の `handle_key` 通常モード分岐に scar dispatch を追加。Rust test で `dispatch_key_inserts_scar_for_a_key` を書く（インメモリの `App` と mock `FileWriter` を使う）。

```bash
cargo test app::tests::dispatch_key
```

エディタ起動の `e` キーは端末制御が絡むため Rust test では mock しきれない。e2e で `EDITOR=true` を使い panic しないことだけ確認する (`e2e: editor-open.test.ts`)。

### M5 着手

`src/hook.rs` を新設、`main.rs` を `clap` に移行。

```bash
cargo add clap --features derive
cargo add dialoguer
cargo add which
```

`cargo add` 後:

```bash
cargo check                            # clap 派生が通ることを確認
cargo test hook::                      # hook ロジックの TDD
```

サンプル stdin JSON を用意してサブコマンド単体実行:

```bash
echo '{"session_id":"test","hook_event_name":"PostToolUse","tool_name":"Edit","tool_input":{"file_path":"/tmp/foo.rs"},"cwd":"/tmp"}' \
  | cargo run -- hook-post-tool --agent claude-code
```

期待される出力（scar が無い場合）:

```
{}
```

### M6-M7 着手

```bash
cargo run -- init                      # インタラクティブ UI が出る
cargo run -- init --agent claude-code,cursor --non-interactive
cargo run -- init --dry-run            # 書き込み先ファイルの一覧だけ表示
```

期待される出力（インタラクティブ、agent が 3 つ検出された場合）:

```
kizu will install hooks for the agents you select.

? Which agents to wire up? (Space to toggle, Enter to confirm)
  [x] Claude Code     (.claude/settings.json)        [detected]
  [x] Cursor          (.cursor/hooks.json)           [detected]
  [x] Codex CLI       (~/.codex/hooks.json)          [detected]
  [ ] Qwen Code       (.qwen/settings.json)
  [ ] Cline           (.clinerules/hooks/)
  [ ] Windsurf        (.windsurf/hooks.json)
  [ ] Gemini CLI      (pipe mode, no installer)

? Install scope? [project/user]
> project

Installing hooks...
  ✓ .claude/settings.json         (2 entries added, 0 skipped)
  ✓ .cursor/hooks.json            (2 entries added, 0 skipped)
  ✓ ~/.codex/hooks.json           (1 entry added, 0 skipped — Stop only; PreTool/PostTool Bash-only)

Done. Run `kizu teardown` to remove.
```

### M8 着手

```bash
cargo run -- teardown
cargo run -- teardown --dry-run
```

期待される出力:

```
Removing kizu hooks from detected agents...
  ✓ .claude/settings.json         (2 entries removed)
  ✓ .cursor/hooks.json            (2 entries removed)
  ✓ ~/.codex/hooks.json           (1 entry removed)

Done.
```

### M9 着手

```bash
just ci                                # 全 CI gate を順に実行
```

期待される出力:

```
cargo fmt --check: ok
cargo clippy: 0 warnings
cargo test: N passed
cargo build --release: ok
e2e: N tests passed
```

## Validation and Acceptance

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

以下のシナリオを手動で実行し、全て期待通りに動くこと:

**1. 背景色 diff の視覚確認**

1. `cargo run --release` で kizu 起動
2. 別ターミナルで `echo "let x = 1;" >> src/main.rs` を実行
3. kizu の右ペインに新しい行が **`+` 記号なしで緑の背景色で表示**されること
4. `git checkout src/main.rs` で元に戻すと、削除行が赤の背景で表示されること

**2. scar の挿入**

1. `cargo run --release` で kizu 起動
2. 別ターミナルで `echo "let x = 1;" >> src/main.rs` を実行
3. kizu の追加行にカーソルを合わせ、`a` キーを押す
4. `src/main.rs` を別ターミナルで開き、直前の行に `// @kizu[ask]: explain this change` が挿入されていることを確認
5. kizu の画面にもこの scar 行が追加として表示されること

**3. `c` キーのインタラクティブ入力**

1. 別の追加行にカーソルを合わせ `c` を押す
2. フッタに `> _` が出る
3. `ここ本当に必要？` と入力して Enter
4. 該当行の直上に `// @kizu[free]: ここ本当に必要？` が挿入される
5. Esc でキャンセルした場合はファイルに何も書かれない

**4. `e` キーのエディタ起動**

1. 追加行にカーソルを合わせ `e` を押す
2. `$EDITOR` に設定されているエディタが起動し、該当行にカーソルがある
3. エディタを終了すると kizu に戻る
4. kizu が panic せず、画面が正しく再描画される

**5. `x` キーの hunk revert**

1. 追加行にカーソルを合わせ `x` を押す
2. ファイルの該当 hunk が元に戻っている
3. kizu の diff 表示からも該当 hunk が消えている

**6. `kizu init` インタラクティブ**

```bash
cargo run -- init
```

1. 現在の PATH 上にある AI エージェントが detected マークで表示される
2. Space で toggle、Enter で確定
3. scope を `project` で選ぶ
4. 各設定ファイルが書き込まれる
5. 該当ファイルを `cat` して kizu 由来 hook が追加されていることを確認

**7. `kizu init --non-interactive`**

```bash
cargo run -- init --agent claude-code --non-interactive
```

1. インタラクティブ UI が出ずに即実行
2. `.claude/settings.json` に hook が追加される
3. 既存 hook がある場合はマージされる（上書きされない）

**8. PostToolUse hook の即時通知**

1. 上記で kizu hook をインストール済みの repo で、別ターミナルから Claude Code を起動
2. ファイルに scar を書き込む
3. Claude Code でそのファイルを別の箇所で編集させる
4. Claude Code の出力に「未対応 scar が 1 件あります」相当のメッセージが出る

**9. Stop hook の最終チェック**

1. 同じ repo で Claude Code にタスクを依頼
2. 作業中にファイルに scar を書き込む
3. Claude Code がターンを閉じようとすると exit 2 が返り、scar の内容が指示として次ターンに渡される

**9a. Untracked ファイルの scar が Stop gate で検出される**（regression）

1. Claude Code に「新規ファイル `src/new_feature.rs` を作って」と依頼
2. kizu でそのファイルの追加行にカーソルを合わせ `r` で reject scar を刻む
3. Claude Code がターンを閉じようとすると exit 2 が返り、stderr に `src/new_feature.rs:<line>: @kizu[reject]: revert this change` が出力される
4. `git diff --name-only` 単独では検出されないが、`git status --porcelain --untracked-files=all` 合成により検出される

**9b. Cline は Stop gate が効かないことの確認**

1. Cline を使っているセッションで scar を刻む
2. `.clinerules/hooks/PostToolUse` 経由で `additionalContext` は動くが、Cline には Stop 相当 hook が無いため、未対応 scar があってもタスクは閉じられる
3. `kizu init` の UI で `Cline [PostTool best-effort: no Stop gate]` とラベル表示されていることを確認

**9c. Sanitized event log**

1. `kizu init` 済みの repo で Claude Code にファイル編集を 1 回させる
2. `$XDG_STATE_HOME/kizu/events/` 配下に JSON ファイルが作成されている
3. `stat -c %a <dir>` でディレクトリが `700`、ファイルが `600` であること
4. 中身に `tool_input.content` や `tool_response.output` が **含まれていない**こと（`file_paths` / `session_id` / `tool_name` 等のメタデータのみ）
5. 24 時間以上古いエントリは次回 `hook-log-event` 実行時に prune される

**10. `kizu teardown`**

```bash
cargo run -- teardown
```

1. 全エージェントから kizu 由来 hook が除去される
2. ユーザーの他 hook は残っている
3. 設定ファイルを `cat` して確認

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

```bash
just ci
```

期待される最終行:

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

個別に走らせる場合:

```bash
cargo test scar::                       # N passed (TDD で追加したテスト)
cargo test hook::                       # N passed
cargo test init::                       # N passed
cargo test ui::tests::render_scroll_lines_use_background_color
                                        # 1 passed
cd tests/e2e && bun test scar-ask       # 1 passed
cd tests/e2e && bun test scar-comment   # 1 passed
cd tests/e2e && bun test editor-open    # 1 passed (EDITOR=true smoke)
```

## Idempotence and Recovery

- **M1（背景色移行）**: 完全に内部リファクタ。複数回実行されても影響なし。元に戻す場合は `git revert` で十分
- **M2-M4（scar 実装）**: `insert_scar` は同一 scar が直上にあれば no-op。`x` (revert) は複数回実行可能だが 2 回目は「既に revert 済み」なので no-op
- **M5（hook サブコマンド）**: 各 hook は純粋関数的に動作し、dedupe state `<state_dir>/scars/<session_id>.json` で重複通知を防ぐ。dedupe ファイルを削除すれば state は初期化される。event log は `<state_dir>/events/` で TTL 24 時間 + 上限 1000 エントリで自動 prune され、放置しても無制限成長しない
- **M6-M7（`kizu init`）**: 再実行しても既存 entry を `command` プレフィックスで判定して skip するため、重複 hook は入らない
- **M8（`kizu teardown`）**: 複数回実行しても初回以降は no-op。既に外されていればエラーにならない
- **失敗時のリカバリ**:
  - `init` 中のファイル書き込み失敗: tempfile → rename の atomic write なので元ファイルは破壊されない
  - `teardown` 中の書き込み失敗: 同様に atomic、失敗したファイルは原状のまま
  - hook サブコマンドが panic した場合: stdin JSON が malformed でも panic せず、`serde_json::Error` を stderr に出して exit 1 で終わる
- **環境のクリーンアップ**: `/tmp/kizu-scars/*.json` と `/tmp/kizu-events/*.json` は teardown では消さない（ユーザー判断）。完全クリーンアップコマンドが必要なら v0.3 で検討

## Artifacts and Notes

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

実装中に発見した decisive な証拠をここに添付します。現時点では空。

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

## Interfaces and Dependencies

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

| **クレート** | **version** | **用途** |
|:---|:---|:---|
| `clap` | 4 (derive feature) | サブコマンド dispatch |
| `dialoguer` | 0.11 | インタラクティブ MultiSelect/Select |
| `which` | 7 | PATH 上のバイナリ検出 |
| `dirs` | 5 | `$XDG_STATE_HOME` / macOS / Windows の private state dir 解決 |
| `serde_json` | 既存 | hook I/O JSON |
| `tempfile` | 既存 (dev-dependency) | init/teardown の atomic write テスト |

### 新設モジュール

- `src/scar.rs` — scar のコア（`CommentSyntax`, `detect_comment_syntax`, `insert_scar`）
- `src/hook.rs` — hook サブコマンドのロジック（`NormalizedHookInput`, `AgentKind`, `SupportLevel`, `SanitizedEvent`, `scan_scars`, `enumerate_repo_files_with_scars`, `format_hook_output`）
- `src/init.rs` — `kizu init` / `kizu teardown` のドライバと `AgentInstaller` trait 実装群
- `src/paths.rs` — platform 別 state dir 解決（`state_dir()`, `events_dir()`, `scars_dir()`）、`KIZU_STATE_DIR` override 対応

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

```rust
// src/scar.rs
pub struct CommentSyntax {
    pub open: &'static str,
    pub close: Option<&'static str>,
}

pub fn detect_comment_syntax(path: &Path) -> CommentSyntax;
pub fn render_comment(syntax: &CommentSyntax, text: &str) -> String;
pub fn insert_scar(path: &Path, line_number: usize, text: &str) -> Result<()>;

// src/hook.rs
#[derive(Clone, Copy, Debug)]
pub enum AgentKind {
    ClaudeCode,
    Cursor,
    Codex,
    QwenCode,
    Cline,
    GeminiStream,
    #[cfg(feature = "windsurf-experimental")]
    Windsurf,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SupportLevel {
    Full,                       // PostTool + Stop 両方動く
    StopOnly,                   // Stop のみ (Codex)
    PostToolOnlyBestEffort,     // PostTool のみ、Stop gate 無し (Cline)
    WriteSideOnly,              // scar 書き込みのみ、フィードバックパス無し (Gemini CLI)
}

pub struct NormalizedHookInput {
    pub session_id: String,
    pub tool_name: Option<String>,
    pub file_paths: Vec<PathBuf>,
    pub cwd: PathBuf,
    pub stop_hook_active: bool,
}

impl NormalizedHookInput {
    pub fn from_stdin_json(agent: AgentKind, reader: impl Read) -> Result<Self>;
}

pub struct ScarHit {
    pub path: PathBuf,
    pub line: usize,
    pub message: String,
}

pub struct SanitizedEvent {
    pub session_id: 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 scan_scars(paths: &[PathBuf]) -> Vec<ScarHit>;

/// tracked + untracked の両方を列挙する (v0.1 git.rs の untracked 合成を流用)
pub fn enumerate_repo_files_with_scars(root: &Path, baseline: &str) -> Result<Vec<PathBuf>>;

pub fn run_hook_post_tool(agent: AgentKind) -> Result<ExitCode>;
pub fn run_hook_stop(agent: AgentKind) -> Result<ExitCode>;
pub fn run_hook_log_event() -> Result<ExitCode>;
pub fn prune_event_log(ttl: Duration, max_entries: usize) -> Result<()>;

// src/paths.rs
pub fn state_dir() -> Result<PathBuf>;          // $XDG_STATE_HOME/kizu/ (or platform equivalent)
pub fn events_dir() -> Result<PathBuf>;         // state_dir()/events/
pub fn scars_dir() -> Result<PathBuf>;          // state_dir()/scars/
pub fn ensure_private_dir(path: &Path) -> Result<()>;  // 0700 で作成

// src/init.rs
#[derive(Clone, Copy, Debug)]
pub enum Scope { Project, User }

pub struct DetectedAgent {
    pub kind: AgentKind,
    pub binary_found: bool,
    pub config_dir_found: bool,
    pub support_level: SupportLevel,
}

pub fn detect_agents(project_root: &Path) -> Vec<DetectedAgent>;

pub struct InstallReport {
    pub agent: AgentKind,
    pub files_modified: Vec<PathBuf>,
    pub entries_added: usize,
    pub entries_skipped: usize,
    pub warnings: Vec<String>,
}

pub trait AgentInstaller {
    fn kind(&self) -> AgentKind;
    fn support_level(&self) -> SupportLevel;
    fn install(&self, project_root: &Path, scope: Scope) -> Result<InstallReport>;
    fn uninstall(&self, project_root: &Path, scope: Scope) -> Result<()>;
}

pub fn run_init(args: InitArgs) -> Result<()>;
pub fn run_teardown(args: TeardownArgs) -> Result<()>;

// src/main.rs (clap subcommand)
#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    command: Option<Command>,
}

#[derive(Subcommand)]
enum Command {
    Init(InitArgs),
    Teardown(TeardownArgs),
    HookPostTool { #[arg(long)] agent: AgentKind },
    HookStop { #[arg(long)] agent: AgentKind },
    HookLogEvent,
    ConsumeGeminiStream,
}
```

### 既存モジュールへの追加

- `src/app.rs`:
  - `App.input_mode: InputMode` 追加（`Normal` / `Picker` / `ScarComment`）
  - `App.scar_comment_buffer: String` 追加
  - `App.seen_hunks: HashSet<HunkId>` 追加
  - `handle_key` に scar dispatch 追加
  - `App::open_editor(&mut self, terminal: &mut Terminal) -> Result<()>` 追加
- `src/git.rs`:
  - `revert_hunk(root: &Path, patch: &str) -> Result<()>` 追加（`git apply --reverse` shell out）
- `src/ui.rs`:
  - `render_scroll_line` から `+`/`-` prefix 生成を削除、`Style.bg` ベースに書き換え
  - `render_footer` に `InputMode::ScarComment` 時の入力欄描画を追加

### Hook 設定ファイルのフォーマット例（各 installer が生成するもの）

**Claude Code (`.claude/settings.json`)**:

```json
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit|MultiEdit",
      "hooks": [{ "type": "command", "command": "kizu hook-post-tool --agent claude-code" }]
    }],
    "Stop": [{
      "hooks": [{ "type": "command", "command": "kizu hook-stop --agent claude-code" }]
    }]
  }
}
```

**Cursor (`.cursor/hooks.json`)**:

```json
{
  "version": 1,
  "hooks": {
    "afterFileEdit": [
      { "command": "kizu hook-post-tool --agent cursor", "timeout": 10 }
    ],
    "stop": [
      { "command": "kizu hook-stop --agent cursor", "timeout": 10 }
    ]
  }
}
```

**Codex CLI (`~/.codex/hooks.json`)** — Stop のみ:

```json
{
  "hooks": {
    "Stop": [{
      "hooks": [{ "type": "command", "command": "kizu hook-stop --agent codex" }]
    }]
  }
}
```

**Qwen Code (`.qwen/settings.json`)** — Claude Code 互換:

```json
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit|MultiEdit",
      "hooks": [{ "type": "command", "command": "kizu hook-post-tool --agent qwen-code" }]
    }],
    "Stop": [{
      "hooks": [{ "type": "command", "command": "kizu hook-stop --agent qwen-code" }]
    }]
  }
}
```

**Cline (`.clinerules/hooks/PostToolUse`)** — 実行可能 shell wrapper、**Stop wrapper は作らない**:

```sh
#!/bin/sh
exec kizu hook-post-tool --agent cline "$@"
```

Cline は Stop 相当 hook を持たないため、`InstallReport.warnings` に `"Cline lacks a Stop hook; unresolved scars cannot block task completion"` を載せて UI で警告する。

**Windsurf**: v0.2 では installer を提供しない。`AgentKind::Windsurf` enum バリアントは `#[cfg(feature = "windsurf-experimental")]` でガードし、standard build には含めない。`.windsurf/hooks.json` のスキーマが公式ドキュメントで確定し、fixture test が揃ってから v0.2.1 以降で追加する。

### event log のファイルフォーマット（sanitize 済み）

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

**生の stdin JSON ではなく、sanitize された `SanitizedEvent` のみを書き出す**:

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

**削除するフィールド**:

- `tool_input.content` / `tool_input.new_string` / `tool_input.old_string` — edit したファイル本文
- `tool_response.output` / `tool_response.stdout` / `tool_response.stderr` — エージェントが観測した実行結果
- `prompt` / `user_prompt` / `assistant_message` — 対話ログ

**保持するフィールド**:

- `tool_input.file_path` / `tool_input.path` のような path metadata のみ
- agent 固有フィールドを `NormalizedHookInput` に正規化した結果

**retention**: TTL 24 時間、上限 1000 エントリ。`hook-log-event` 実行時に `prune_event_log` を呼ぶ。`KIZU_EVENT_TTL_SECS` 環境変数で TTL を上書き可能。

### scar dedupe state のフォーマット

保存先: `<state_dir>/scars/<session_id>.json`（`0700` / `0600`）。

```json
{
  "notified": [
    { "path": "src/auth.rs", "line": 14, "hash": "a3f..." }
  ]
}
```
