diff --git a/Cargo.lock b/Cargo.lock index 3e0d5312..9ee1cb7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,6 +431,7 @@ version = "0.1.0" dependencies = [ "lib-jj-helpers", "lib-subprocess", + "lib-telemetry", "proptest", "serde", "serde_json", diff --git a/docs/adr/adr-055-firing-telemetry-collection.md b/docs/adr/adr-055-firing-telemetry-collection.md index 004696f8..1a243504 100644 --- a/docs/adr/adr-055-firing-telemetry-collection.md +++ b/docs/adr/adr-055-firing-telemetry-collection.md @@ -68,9 +68,10 @@ warm-up 後に実データで棚卸し (step 2/3) を後続 PR で行う。本 A - **常時 ON の構造チェック** (comment-lint-rust の非 doc コメント / 関数長、post-tool-linter の file_size_check / utf8_integrity)。これらは編集のたびに発火するコア機構で削除候補に ならず、記録すると ROI 信号 (「発火 0 = 削除候補」) を希釈するノイズになるため。 -- **nudge-only hook** (session-start reminder / stop-feedback-dispatch / - user-prompt-feedback-recovery)。decision 語彙が block/warn の 2 値のため、nudge (助言 - 出力) は乗らない。将来 decision 語彙を拡張する際に再検討する。 +- **残りの nudge-only hook** (stop-feedback-dispatch / user-prompt-feedback-recovery)。 + 本 PR のスコープ外で、計装は各 hook を触る PR で個別に行う。session-start nudge は当初 + この除外に含めていたが、後述の Amendment (2026-07-19) で除外根拠 (「nudge は block/warn に + 乗らない」) を撤回し計装対象に加えた。 `decision` は「hook がツールを実際に停止したか」ではなく「発火の重み」を表す軸である。 custom rule / jj-op-verify は additionalContext の助言層で実際には block しないが、severity @@ -197,6 +198,47 @@ session_id と同性質) であり、ファイルパス・コマンド本文で ADR-057 / ADR-058 の採否判定 (期限 2026-08-15) と R5/R6 の after 計測。これらの効果検証が 「push 時コンソール出力の手動保存」に依存していたのを、機械集計可能な JSONL に置き換える。 +## Amendment (2026-07-19): session-start nudge 群の計装 (PR-N3) + +初版は §計装スコープ で **session-start reminder を含む nudge-only hook を除外**し、根拠を +「decision 語彙が block/warn の 2 値のため nudge (助言出力) は乗らない」とした。weekly-review +reminder が約 4 週間ユーザーに気付かれず発火し続けていた incident +([ADR-059](adr-059-hook-system-message-visibility.md)) を受け、**この除外根拠を撤回し +session-start hook の 5 nudge を firing 計装 (`firings-*.jsonl`) に加える**。 + +### 除外根拠の撤回 — warn は「発火の重み」であり nudge に整合する + +初版の「nudge は乗らない」判断は `decision` を「hook が実際に停止したか」と暗黙に捉えていた。 +本 ADR は §計装スコープ 末尾で既に **`decision` は「発火の重み」を表す軸**と定義しており、 +additionalContext の助言層で実際には block しない custom rule / jj-op-verify も warn/block を +記録している。nudge (助言出力) はこの warn (= 助言的発火) に自然に対応するため、語彙拡張を +待たず `warn` で記録できる。よって初版の除外根拠は不成立で撤回する。 + +### 計装対象と id + +| 対象 | hook | kind | decision | +|---|---|---|---| +| session-start nudge 群 (5 種) | hooks-session-start | hook | warn | + +`id` は nudge 種別の 5 値: `weekly_review_reminder` / `pr_monitor_catchup` / `reaper` / +`staleness` / `workspace_stale`。各 nudge が発火 (context 追記) した点で +`lib_telemetry::record` を 1 回呼ぶ (`hooks-session-start/src/main.rs` の `record_nudge_firing`)。 +session_id は SessionStart hook 入力から直接渡す。fail-open / opt-in / kill-switch / per-pid×日次 +partition は既存原則に相乗りする。 + +### 動機 — ADR-059 bounded lifetime の観測基盤 + +ADR-059 は systemMessage 可視化を weekly reminder 限定で dogfood し、行動要求系 nudge +(PR catch-up / post-merge recovery / failed marker) への段階展開の採否を発火実績で判定する +(期限 2026-08-16)。本計装が「どの nudge が実際に発火したか」を供給してその判定を支える。 +同時に WP-12 step 2 の ROI 棚卸し (発火 0 の機構を削除候補提示) にも寄与する。 + +### スコープ外に残す nudge-only hook + +stop-feedback-dispatch / user-prompt-feedback-recovery は本 PR では計装しない。撤回した根拠は +これらにも当てはまるが、計装は各 hook を触る PR で個別に行う (ADR-059 段階展開に連動)。 +§計装スコープ の除外リストは本 amendment に合わせて更新した。 + ## 関連 ADR - [ADR-039](adr-039-experimental-feature-standard-pattern.md) — 試験運用標準パターン (opt-in / kill-switch / bounded lifetime) @@ -207,3 +249,4 @@ ADR-057 / ADR-058 の採否判定 (期限 2026-08-15) と R5/R6 の after 計測 - [ADR-012](adr-012-src-naming-convention.md) — src/ 命名規約 (`lib-` prefix) - [ADR-026](adr-026-cargo-workspace.md) — Cargo workspace (新 crate の members 追記) - [ADR-041](adr-041-test-isolation-patterns.md) — テスト隔離 (env kill-switch テストの serial 化) +- [ADR-059](adr-059-hook-system-message-visibility.md) — systemMessage 可視化 (session-start nudge 計装が bounded lifetime 判定の観測基盤) diff --git a/docs/weekly-review-notification-plan.md b/docs/weekly-review-notification-plan.md index 8da10456..12a091ac 100644 --- a/docs/weekly-review-notification-plan.md +++ b/docs/weekly-review-notification-plan.md @@ -207,6 +207,20 @@ - 新セッション起動 → `.claude/telemetry/firings-*.jsonl` に `hooks-session-start` 行が append されること (削除条件 4)。 +### 作業記録 (2026-07-19 実装完了) + +- **実装済み**。コミット粒度 (レビューしやすさ優先で 3 分割): + 1. `feat(session-start)`: `Cargo.toml` に `lib-telemetry` 依存追加 + `main.rs` に `record_nudge_firing` ヘルパー + 5 発火点 (pr_monitor_catchup / reaper / staleness / workspace_stale / weekly_review_reminder) への配線。配線で `emit_session_start_output` が 50 行上限 (touch-trigger ratchet) を超えたため `append_pr_monitor_catchup_nudge` / `append_cwd_nudges` に責務分割 (挙動不変)。 + 2. `docs(adr)`: ADR-055 のスコープに session-start nudge 群を追記 + 除外根拠を撤回 (下記 設計判断) + Amendment (2026-07-19) セクション + 関連 ADR に ADR-059 追記。 + 3. `docs`: 本計画書に PR-N3 作業記録を反映 (本コミット)。 +- **設計判断 (ADR-055 除外根拠の撤回)**: ADR-055 初版は session-start reminder を含む nudge-only hook を「decision 語彙が block/warn の 2 値のため nudge (助言出力) は乗らない」として除外していた。しかし ADR-055 は `decision` を「発火の重み」を表す軸と定義済みで、additionalContext の助言層で実際には block しない custom rule / jj-op-verify も既に warn/block を記録している。nudge はこの warn (= 助言的発火) に自然に対応するため、除外根拠は不成立と判断し撤回した。全 nudge を `warn` で一括記録する (表示ノイズゼロのため systemMessage と違い段階展開不要、計画どおり)。 +- 検証結果: + - `cargo test -p hooks-session-start`: **93 passed** (PR-N2 と同数)。観測層の追加は挙動不変のため新規ユニットテストは追加せず。telemetry 本体の書き込み・opt-in・partition は lib-telemetry の 20+ テストが担保し、record wrapper に専用テストを持たない方針は sibling hook (hooks-post-tool-jj-op-verify / hooks-stop-tool-call-leak) の `record_*_firing` の前例に倣った (`record` は exe 隣 `.claude/` 解決 + `OnceLock` キャッシュのプロセスグローバル依存でユニットテストに不向き)。 + - `cargo clippy -p hooks-session-start --all-targets -- -D warnings`: クリーン。 + - `pnpm build:all`: 成功 (全 crate release ビルド + 更新 exe を `.claude/` に配布)。 + - **デプロイ済み exe を実 session_id で駆動して end-to-end 確認済み**: メイン workspace から `.claude/hooks-session-start.exe` を SessionStart 入力で駆動すると、`.claude/telemetry/firings-2026-07-19-.jsonl` に `pr_monitor_catchup` と `weekly_review_reminder` の **2 発火行** (`hook=hooks-session-start` / `kind=hook` / `decision=warn` / `session_id` 付き) が append されることを確認。発火した nudge のみ記録される (条件未成立の staleness / workspace_stale / reaper は非記録) ことも確認。実 session_id を渡すことで `.session-id` の冪等スキップを確認し、既存 session 状態を汚さないことも担保。 +- **残タスク (削除条件 4)**: land 後、**新セッション起動**で `.claude/telemetry/firings-*.jsonl` に session-start nudge 発火行が記録されることを目視確認 (本 E2E で pre-land 検証済み)。opt-in (`[telemetry] enabled = true`) は dogfood のため既に本 repo で有効。 + --- ## PR 外の即時運用アクション (本計画とは独立、忘れず実施) diff --git a/src/hooks-session-start/Cargo.toml b/src/hooks-session-start/Cargo.toml index 2a2d8043..6d6e6705 100644 --- a/src/hooks-session-start/Cargo.toml +++ b/src/hooks-session-start/Cargo.toml @@ -9,6 +9,7 @@ serde_json = "1.0" toml = "0.8" lib-subprocess = { path = "../lib-subprocess" } lib-jj-helpers = { path = "../lib-jj-helpers" } +lib-telemetry = { path = "../lib-telemetry" } [dev-dependencies] proptest = "1" diff --git a/src/hooks-session-start/src/main.rs b/src/hooks-session-start/src/main.rs index 725bcc4c..7ba909f8 100644 --- a/src/hooks-session-start/src/main.rs +++ b/src/hooks-session-start/src/main.rs @@ -9,6 +9,9 @@ //! 5. Working copy staleness nudge: `staleness` module //! 6. Weekly review reminder (ADR-031 Phase C): `weekly_review` module //! +//! 各 nudge の発火は `lib-telemetry` (ADR-055) に `warn` として記録され、ROI 棚卸しの +//! 観測基盤 (`.claude/telemetry/firings-*.jsonl`) に載る (fail-open)。 +//! //! .session-id ファイルは「同一 ID スキップ」方式: //! - 既に同じ session_id が書かれていれば何もしない (冪等) //! - 異なる ID (新セッション or サブセッション) の場合は上書きする @@ -102,57 +105,84 @@ fn main() { emit_session_start_output(&session_id); } -/// `additionalContext` (session_id + 任意の PR monitor catch-up nudge + 任意の reaper nudge) を -/// 組み立て、Claude Code に返す JSON を stdout に書き出す。 +/// `additionalContext` (session_id + 任意の nudge 群: PR monitor catch-up / reaper / staleness / +/// workspace_stale / weekly review) と任意の `systemMessage` を組み立て、Claude Code に返す JSON を +/// stdout に書き出す。各 nudge の追記と telemetry 記録はヘルパーに委譲する。 /// serde_json で組み立てることで session_id 内の特殊文字を安全にエスケープする。 fn emit_session_start_output(session_id: &str) { let mut context = format!("CLAUDE_CODE_SESSION_ID={}", session_id); let mut system_message: Option = None; let now_unix = current_unix_secs(); + append_pr_monitor_catchup_nudge(&mut context, session_id, now_unix); + if let Ok(cwd) = std::env::current_dir() { + system_message = append_cwd_nudges(&mut context, session_id, &cwd, now_unix); + } + let output = build_session_start_json(&context, system_message.as_deref()); + println!("{}", output); +} + +/// PR monitor catch-up nudge を `context` に追記し、発火時は telemetry に記録する。 +/// この nudge は cwd に依存せず parked state ファイルのみを見るため独立したヘルパーにする。 +fn append_pr_monitor_catchup_nudge(context: &mut String, session_id: &str, now_unix: i64) { if let Some(state) = read_parked_state(&pr_monitor_state_path()) { if let Some(nudge) = compute_catchup_nudge(&state, now_unix) { context.push_str("\n\n"); context.push_str(&nudge); + record_nudge_firing("pr_monitor_catchup", session_id); } } - if let Ok(cwd) = std::env::current_dir() { - if let Some(reaper_nudge) = compute_reaper_nudge(&cwd, now_unix) { +} + +/// cwd 依存の nudge 群 (reaper / staleness / workspace_stale / weekly review) を `context` に +/// 追記し、発火時は telemetry に記録する。weekly review のみユーザー可視の systemMessage を +/// 伴うため、それを戻り値として返す (発火しなければ `None`)。 +fn append_cwd_nudges( + context: &mut String, + session_id: &str, + cwd: &Path, + now_unix: i64, +) -> Option { + if let Some(reaper_nudge) = compute_reaper_nudge(cwd, now_unix) { + context.push_str("\n\n"); + context.push_str(&reaper_nudge); + record_nudge_firing("reaper", session_id); + } + let hooks_config = read_hooks_config(cwd); + let session_start = hooks_config.session_start.as_ref()?; + if let Some(staleness_config) = session_start.staleness.as_ref() { + if let Some(staleness_nudge) = compute_staleness_nudge(cwd, staleness_config) { context.push_str("\n\n"); - context.push_str(&reaper_nudge); - } - let hooks_config = read_hooks_config(&cwd); - if let Some(staleness_config) = hooks_config - .session_start - .as_ref() - .and_then(|s| s.staleness.as_ref()) - { - if let Some(staleness_nudge) = compute_staleness_nudge(&cwd, staleness_config) { - context.push_str("\n\n"); - context.push_str(&staleness_nudge); - } - if let Some(stale_nudge) = compute_workspace_stale_nudge(staleness_config) { - context.push_str("\n\n"); - context.push_str(&stale_nudge); - } + context.push_str(&staleness_nudge); + record_nudge_firing("staleness", session_id); } - if let Some(weekly_config) = hooks_config - .session_start - .as_ref() - .and_then(|s| s.weekly_review_reminder.as_ref()) - { - if let Some(weekly_nudge) = - compute_weekly_review_reminder_nudge(&cwd, weekly_config, now_unix) - { - context.push_str("\n\n"); - context.push_str(&weekly_nudge.additional_context); - if weekly_nudge.system_message.is_some() { - system_message = weekly_nudge.system_message; - } - } + if let Some(stale_nudge) = compute_workspace_stale_nudge(staleness_config) { + context.push_str("\n\n"); + context.push_str(&stale_nudge); + record_nudge_firing("workspace_stale", session_id); } } - let output = build_session_start_json(&context, system_message.as_deref()); - println!("{}", output); + let weekly_config = session_start.weekly_review_reminder.as_ref()?; + let weekly_nudge = compute_weekly_review_reminder_nudge(cwd, weekly_config, now_unix)?; + context.push_str("\n\n"); + context.push_str(&weekly_nudge.additional_context); + record_nudge_firing("weekly_review_reminder", session_id); + weekly_nudge.system_message +} + +/// nudge の発火を telemetry (ADR-055) に記録する (fail-open)。 +/// +/// `id` は nudge 種別 (`weekly_review_reminder` / `pr_monitor_catchup` / `reaper` / +/// `staleness` / `workspace_stale`)。nudge は助言出力のため decision は一律 `Warn` +/// (「発火の重み」軸であり、実際に停止したかではない。jj-op-verify の非 block warn と同性質)。 +/// 記録失敗・opt-in OFF は lib-telemetry 内部で握りつぶすため hook 本来の出力を妨げない。 +fn record_nudge_firing(id: &str, session_id: &str) { + lib_telemetry::record(&lib_telemetry::Firing { + hook: "hooks-session-start", + kind: lib_telemetry::FiringKind::Hook, + id, + decision: lib_telemetry::Decision::Warn, + session_id: Some(session_id), + }); } /// SessionStart hook の stdout JSON を組み立てる純粋関数 (ADR-059)。