Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 46 additions & 3 deletions docs/adr/adr-055-firing-telemetry-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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 判定の観測基盤)
14 changes: 14 additions & 0 deletions docs/weekly-review-notification-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<pid>.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 外の即時運用アクション (本計画とは独立、忘れず実施)
Expand Down
1 change: 1 addition & 0 deletions src/hooks-session-start/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
102 changes: 66 additions & 36 deletions src/hooks-session-start/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 サブセッション) の場合は上書きする
Expand Down Expand Up @@ -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<String> = 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<String> {
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)。
Expand Down
Loading