From fc649b0b0f4835ccc161129fbb51447eb65a9d8a Mon Sep 17 00:00:00 2001 From: aloekun Date: Mon, 25 May 2026 13:44:41 +0900 Subject: [PATCH 1/5] =?UTF-8?q?docs(todo):=20=E9=A0=86=E4=BD=8D=20143=20+?= =?UTF-8?q?=20144=20=E6=96=B0=E8=A6=8F=E8=BF=BD=E5=8A=A0=20=E2=80=94=20PR?= =?UTF-8?q?=20#171=20post-merge-feedback=20=E6=8E=A1=E7=94=A8=202=20?= =?UTF-8?q?=E4=BB=B6=20(Bundle=20171:=20T2-#4=20fixture=20helper=20+=20T3-?= =?UTF-8?q?#8=20jj=20hook)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/todo-summary.md | 4 +- docs/todo8.md | 104 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/docs/todo-summary.md b/docs/todo-summary.md index f1a8906..81706db 100644 --- a/docs/todo-summary.md +++ b/docs/todo-summary.md @@ -72,7 +72,9 @@ | 139 | 💎 Tier 3 | **ADR-041: Test Isolation Patterns for Multi-Condition Guards (PR #168 T3-#2 採用) — 本 PR で land** | todo8.md | M | なし (PR #120 W-001 初発見 + PR #168 sentinel pattern 実装の 2 PR 横断で Frequency Medium、`feedback_no_unenforced_rules.md` 例外 = 既存実践の明文化 + project-specific 実装例 (poll.rs) + PR #120 W-001 history codify、順位 84 = code-review.md global checklist の補完 layer、順位 135 codified placeholder 番号 policy 適用 — 当初 ADR-NNN placeholder で entry 登録 → land 時 PR で ADR-041 確定取得、順位 78 を ADR-NNN に再 placeholder 化) | | 140 | 💎 Tier 3 | **順位 135「codified placeholder policy」を正式 ADR に昇格 (PR #169 T3-#2 採用)** | todo8.md | S | なし (順位 135 entry を retire し、ADR-NNN (採番未確定、land 時に確定): ADR Numbering Strategy として永続化。PR #111/#132/#169 の 3+ PR で適用実証済 — PR #169 で「ADR-038 → 041 → NNN」3 段振り直し dogfood が land、ephemeral todo entry 限りでは派生プロジェクトへの transferability 不足、`feedback_no_unenforced_rules.md` 例外 = 既存実践 (3 PR で実証) の明文化 + 後続 entry が同 policy を参照する際の永続 reference 確保) | | 141 | 🚀 Tier 1 | **CR rate-limit detection bug 修正 — fix_push_time 固定 + 早期 merge 判断 signal (PR #169 観測由来)** | todo8.md | S | なし (PR #169 セッションで systemic 観測 = wakeup ごとの push_time 更新で CR walkthrough overlay の updated_at が「過去扱い」になり parse_rate_limit の event_time >= push_time filter で除外される構造バグ、`feedback_pipeline_over_rules` 適用 = パイプライン側機械的修正で Claude 判断介入を排除、wall clock 短縮 = rate-limit 検出時に mergeable CLEAN なら 5-10 分で人間判断 (38 分 reset 待ちを bypass)、既存 auto-retry path は維持 (ユーザーが「待つ」選択時は通常 flow)、Bundle a Sub-PR 2 / Bundle f scope 外の独立 layer) | -| 142 | 💎 Tier 3 | **ADR-041 補強 — "State Preservation Invariant" pattern section 追加 (PR #170 T3-#1 採用)** | todo8.md | S | なし (PR #168/169/170 で連続観測の write-once 不変式 (once-set-never-overwritten) パターンを ADR-041 に追記、`state.fix_push_time.or_else(...)` 形式の 3 点セット test pattern (既存値あり / 新値提供 / preservation 確認) を明文化、参照実装 = poll.rs `finalize_*_preserves_existing_fix_push_time` + monitor.rs `resume_returns_fix_push_time_from_state_when_set`、ADR-041 既存 section (Multi-Condition Guards) とは別 pattern class、`feedback_no_unenforced_rules.md` 例外 = 既存実践 (3 PR で実証) の明文化 + 派生プロジェクト transferability 確保) | +| 142 | 💎 Tier 3 | **ADR-041 補強 — "State Preservation Invariant" pattern section 追加 (PR #170 T3-#1 採用) ★ Bundle 171** | todo8.md | S | なし (PR #168/169/170 で連続観測の write-once 不変式 (once-set-never-overwritten) パターンを ADR-041 に追記、`state.fix_push_time.or_else(...)` 形式の 3 点セット test pattern (既存値あり / 新値提供 / preservation 確認) を明文化、参照実装 = poll.rs `finalize_*_preserves_existing_fix_push_time` + monitor.rs `resume_returns_fix_push_time_from_state_when_set`、ADR-041 既存 section (Multi-Condition Guards) とは別 pattern class、`feedback_no_unenforced_rules.md` 例外 = 既存実践 (3 PR で実証) の明文化 + 派生プロジェクト transferability 確保) | +| 143 | 🔧 Tier 2 | **複言語 fixture helper 標準化 (hooks-post-tool-linter-tests) (PR #171 T2-#4 採用) ★ Bundle 171** | todo8.md | S | なし (PR #151/#171 の 2 PR 横断で multi-byte fixture 手動組み立てコストが Frequency Medium で観測、Japanese / emoji / combining chars helper 3 関数を標準化して新規 string-processing 関数追加時の boundary test コスト削減 + silent regression early detection、順位 142 + 144 と同 PR で land 推奨) | +| 144 | 💎 Tier 3 | **PreToolUse hook で `jj new` / `jj split` の -m 引数なしを block (PR #171 T3-#8 採用、hook 方針) ★ Bundle 171** | todo8.md | M | なし (PR #171 セッションで `jj new` 忘れによる混合 commit 事故を実観測、analyzer 原案 docs 化を `feedback_no_unenforced_rules.md` 適用でユーザー判断 hook 方針に変更、`BlockedPattern` 構造体に `exception: Option` field 追加 + 新 preset `jj-message-required` 追加で `-m`/`--message` 必須化を機械強制、設計判断 A-D ユーザー承認済、Bundle 171 の structural defense 主軸) | **戦略**: Tier 1 を 2〜3 セッションで片付け → Tier 2 で ADR-032 の前提 + rate-limit + convergence cost 削減を進める → Tier 3 で ADR-032 を land + ドキュメント整備。Tier 4-5 は cleanup / 外部展開で daily efficiency への直接効果は小さい。 diff --git a/docs/todo8.md b/docs/todo8.md index 2ff23d1..f0352db 100644 --- a/docs/todo8.md +++ b/docs/todo8.md @@ -562,6 +562,110 @@ analyzer report の `[ADR-041 追加 section 案]` をベースに、`docs/adr/a --- +### 複言語 fixture helper 標準化 (hooks-post-tool-linter-tests) (PR #171 T2-#4 採用) ★ Bundle 171 + +> **動機**: PR #151 (`byte_offset_to_line` char-boundary panic 発見) + PR #171 (`build_violation_json` defensive test 追加) の 2 PR 横断で multi-byte content fixture を手動で組み立てるコストが顕在化。Japanese / emoji / combining chars の各 sample を helper として標準化することで、新規 string-processing 関数追加時の boundary test 実装コストを低減し silent regression を early detection できる。 +> +> **本タスクの位置づけ**: PR #171 post-merge-feedback Tier 2 #4 採用 (Severity Medium / Frequency Medium / Effort S / Adoption Risk None)。Bundle 171 のコア (順位 142 ADR-041 補強 + 順位 144 jj hook と同 PR で land 推奨)。 +> +> **参照**: `.claude/feedback-reports/171.md` Tier 2 #4、`src/hooks-post-tool-linter/src/main.rs` (`run_custom_rules_line_number_correct_with_multibyte_content` を helper 化対象)、PR #151 / PR #171 +> +> **実行優先度**: 🔧 **Tier 2** — Effort S。Bundle 171 ペアタスク。 + +#### 設計決定 (案) + +- **helper API** (3 関数): + - `multibyte_fixture_japanese() -> &'static str` — 3 bytes/char (例: `// 日本語コメント`) + - `multibyte_fixture_emoji() -> &'static str` — 4 bytes/char (例: `// 🦀 rust`) + - `multibyte_fixture_combining() -> &'static str` — e + U+0301 結合文字 (例: `// caf\u{00e9}`) +- **配置先候補**: `src/hooks-post-tool-linter/src/main.rs` の test mod 内 (in-crate) vs 共有 test util crate (cross-crate 再利用)。本タスクでは前者を採用し、再利用ニーズが顕在化したタイミングで後者へ migrate +- **既存 test refactor**: PR #171 で追加した `run_custom_rules_line_number_correct_with_multibyte_content` を helper を呼ぶ形に書き換え + +#### 作業計画 + +- [ ] helper 配置先決定 (in-crate test mod を優先採用) +- [ ] 3 helper 関数を実装 (Japanese / emoji / combining) +- [ ] PR #171 で追加した既存 test を helper を使う形に refactor +- [ ] 派生プロジェクト (techbook-ledger / auto-review-fix-vc) への transferability 考慮 (in-crate なら porting 容易) +- [ ] 本エントリ削除 + todo-summary.md 行削除 + +#### 完了基準 + +- 3 helper 関数が公開され、test mod 内から呼べる +- 既存 test の refactor 完了 (動作不変、`cargo test` pass) +- 新規 string-processing 関数追加時に 1 行で multi-byte boundary test を書ける状態になる + +#### 詰まっている箇所 + +なし。Effort S、Bundle 171 内で 順位 142 + 順位 144 と並列実施可能。 + +--- + +### PreToolUse hook で `jj new` / `jj split` の -m 引数なしを block (PR #171 T3-#8 採用、hook 方針) ★ Bundle 171 + +> **動機**: PR #171 セッション中、`jj new` 忘れにより 順位 57 commit と 順位 91 commit の変更が混入する事故が発生 (後で `jj split -m` で recovery 済)。memory rule や docs 化では「次回ルールを読まなければ再発」する構造的脆弱性が残るため、PreToolUse hook で即座に block + feedback を返す機械強制層を導入する。`feedback_pipeline_over_rules.md` 適用 = パイプライン側機械的修正で Claude 判断介入を排除。 +> +> **本タスクの位置づけ**: PR #171 post-merge-feedback Tier 3 #8 採用 (Effort M / Adoption Risk None)。analyzer 原案は `~/.claude/rules/common/git-workflow.md` への docs 化だったが、ユーザー判断で **hook による mechanical enforcement** に方針変更 (2026-05-24 セッション、`feedback_no_unenforced_rules.md` 適用)。Bundle 171 の 3 タスク (順位 142 + 143 + 144) と同 PR で land 推奨。 +> +> **設計判断 (ユーザー承認済 2026-05-24)**: +> - **A**: `jj new` 引数なしも block (= `-m` を強制、混合 commit 事故の根因対策) +> - **B**: `jj new ` (例: `jj new master`) で `-m` なしも block (empty commit 防御層) +> - **C**: `jj split` interactive (= `-m` なし) は editor hang issue があるため strong block +> - **D**: scope は `jj` 直接呼び出しのみ (`pnpm jj-new` 等のラッパーは scope 外) +> +> **参照**: `.claude/feedback-reports/171.md` Tier 3 #8、`src/hooks-pre-tool-validate/src/main.rs` (`preset_jj_main_guard` / `preset_jj_push_guard` を template に追加)、`.claude/hooks-config.toml`、PR #171 session log (混合 commit recovery 経緯) +> +> **実行優先度**: 💎 **Tier 3** — Effort M。Bundle 171 の structural defense 主軸。 + +#### 設計決定 (案) + +- **`BlockedPattern` 構造体拡張**: + ```rust + struct BlockedPattern { + pattern: Regex, + exception: Option, // 新規: match しても exception が hit すれば allow + message: &'static str, + } + ``` + Rust 標準 `regex` crate は negative lookahead 非対応のため 2 段判定で実装する。 +- **`validate_command` ロジック拡張**: pattern match 後、exception 不一致時のみ block 返却 +- **新 preset `jj-message-required`**: + - block pattern: `(?im)(^|&&|;|\|\||\||&)\s*jj\s+(new|split)\b` + - exception regex: `\s(-m|--message)\b` + - message: `-m` または `--message` で message を明示するよう誘導 +- **`hooks-config.toml`** の preset リストに `jj-message-required` を追加 +- **test 拡充**: + - block: `jj new` / `jj new master` / `jj split file.rs` / `&& jj new` / 改行後 `jj split` 等 + - allow: `jj new -m "..."` / `jj split -m "..." file.rs` / `jj new --message "..."` / `pnpm jj-new` (scope 外) 等 + - 既存 preset (jj-main-guard / jj-push-guard) との non-regression + +#### 作業計画 + +- [ ] `BlockedPattern` 構造体に `exception: Option` field 追加 (既存 preset 全件に `exception: None` を補完) +- [ ] `validate_command` ロジック拡張 (~5 行) +- [ ] `preset_jj_message_required()` 関数追加 +- [ ] `apply_presets` の preset 名 dispatch に登録 +- [ ] `.claude/hooks-config.toml` の `[pre_tool_validate].presets` に `jj-message-required` 追加 +- [ ] test 拡充: block ケース 5+ / allow ケース 5+ / non-regression ケース 既存 preset 別 1+ +- [ ] 既存 pnpm scripts で `jj new` / `jj split` を内部使用していないか確認 (`pnpm jj-start-change` 等) +- [ ] 派生プロジェクト (techbook-ledger / auto-review-fix-vc) deploy 計画は本リポジトリ先行 dogfood 後判断 (breaking change リスクのため) +- [ ] 本エントリ削除 + todo-summary.md 行削除 + +#### 完了基準 + +- `jj new` / `jj split` を `-m` なしで実行すると即 block + 修正手順を含む feedback が返る +- `jj new -m "..."` / `jj split -m "..." ` は通過 +- 既存 preset (jj-main-guard / jj-push-guard / git / 等) の動作に regression なし +- `cargo test -p hooks-pre-tool-validate` pass + +#### 詰まっている箇所 + +- 設計判断 A-D は確認済 (2026-05-24 セッション、ユーザー明示承認) +- 派生プロジェクト breaking change リスクは scope 限定で軽減 (本リポジトリ先行 dogfood) +- 既存 `BlockedPattern` 構造体への field 追加は 7 preset 関数すべてに `exception: None` を補完する mechanical な edit になる (~20 箇所程度) + +--- + ## 既知課題 (記録のみ、本セッションで未対応) ### post-merge-feedback workflow が長時間 stale marker を残す問題 (PR #119 marker observed 2026-05-15) From a4496900c565d47d50f358734576a0963c14dec9 Mon Sep 17 00:00:00 2001 From: aloekun Date: Mon, 25 May 2026 13:44:41 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor(hooks-pre-tool-validate):=20Blocke?= =?UTF-8?q?dPattern=20=E3=81=AB=20exception=20field=20=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=20(=E9=A0=86=E4=BD=8D=20144=20Phase=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks-pre-tool-validate/src/main.rs | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/hooks-pre-tool-validate/src/main.rs b/src/hooks-pre-tool-validate/src/main.rs index cae58d0..586dfa3 100644 --- a/src/hooks-pre-tool-validate/src/main.rs +++ b/src/hooks-pre-tool-validate/src/main.rs @@ -47,6 +47,10 @@ struct PreToolValidateConfig { struct BlockedPattern { pattern: Regex, + /// 順位 144 (PR #171 T3-#8 採用): pattern match 後にこの regex が hit する場合は allow。 + /// Rust 標準 regex crate は negative lookahead 非対応のため 2 段判定で「pattern match + /// AND exception 不一致」の semantic を実現する。`None` の場合は従来通り pattern match で block。 + exception: Option, message: &'static str, } @@ -55,6 +59,7 @@ fn preset_default() -> Vec { vec![ BlockedPattern { pattern: Regex::new(r"(?i)rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)\s").unwrap(), + exception: None, message: r#"**rm -rf コマンドがブロックされました** このコマンドは再帰的に強制削除を行うため、重要なファイルを失う可能性があります。 @@ -67,6 +72,7 @@ fn preset_default() -> Vec { }, BlockedPattern { pattern: Regex::new(r"(?i)rm\s+(-[a-zA-Z]*\s+)*-[a-zA-Z]*r[a-zA-Z]*\s+(-[a-zA-Z]*\s+)*-[a-zA-Z]*f[a-zA-Z]*(\s|$)").unwrap(), + exception: None, message: r#"**rm -rf コマンドがブロックされました** このコマンドは再帰的に強制削除を行うため、重要なファイルを失う可能性があります。 @@ -79,6 +85,7 @@ fn preset_default() -> Vec { }, BlockedPattern { pattern: Regex::new(r"(?i)rm\s+(-[a-zA-Z]*\s+)*-[a-zA-Z]*f[a-zA-Z]*\s+(-[a-zA-Z]*\s+)*-[a-zA-Z]*r[a-zA-Z]*(\s|$)").unwrap(), + exception: None, message: r#"**rm -rf コマンドがブロックされました** このコマンドは再帰的に強制削除を行うため、重要なファイルを失う可能性があります。 @@ -91,6 +98,7 @@ fn preset_default() -> Vec { }, BlockedPattern { pattern: Regex::new(r"(?im)(^|&&|;|\|\||\||&)\s*cd\s+/d\s").unwrap(), + exception: None, message: r#"**cd /d コマンドがブロックされました** `cd /d` は Windows のコマンドプロンプト固有の構文で、Claude Code の bash 環境では動作しません。 @@ -114,6 +122,7 @@ fn preset_git() -> Vec { vec![ BlockedPattern { pattern: Regex::new(r#"(?i)\b(bash|sh)\s+-[a-zA-Z]*c[a-zA-Z]*\s+["'][^"']*\bgit\s+"#).unwrap(), + exception: None, message: r#"**git コマンドがブロックされました(シェルラッパー経由)** このプロジェクトでは Jujutsu (jj) をバージョン管理に使用しています。 @@ -123,6 +132,7 @@ fn preset_git() -> Vec { }, BlockedPattern { pattern: Regex::new(r#"(?im)(^|&&|;|\|\||\||&)\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+|command\s+|env\s+)*git(?:\s+|$)"#).unwrap(), + exception: None, message: r#"**git コマンドがブロックされました** このプロジェクトでは Jujutsu (jj) をバージョン管理に使用しています。 @@ -147,6 +157,7 @@ git コマンドを直接使用すると、バージョン履歴に不整合が fn preset_jj_immutable() -> Vec { vec![BlockedPattern { pattern: Regex::new(r"(?is)\bjj\b.*--ignore-immutable").unwrap(), + exception: None, message: r#"**jj --ignore-immutable がブロックされました** immutable commits(main 等)の書き換え保護を無効化するオプションのため、使用が禁止されています。 @@ -161,6 +172,7 @@ fn preset_jj_main_guard() -> Vec { BlockedPattern { pattern: Regex::new(r#"(?i)(jj\s+new|pnpm\s+jj-new)\s+(?:"main"|'main'|main)(?:\s|$)"#) .unwrap(), + exception: None, message: r#"**jj new main がブロックされました** ローカルの main ブックマークをベースに change を作成することは禁止されています。 @@ -178,6 +190,7 @@ pnpm jj-start-change r#"(?i)(jj\s+edit|pnpm\s+jj-edit)\s+(?:"main"|'main'|main)(?:\s|$)"#, ) .unwrap(), + exception: None, message: r#"**jj edit main がブロックされました** main ブックマークが指す commit を直接編集することは禁止されています。 @@ -198,6 +211,7 @@ fn preset_electron() -> Vec { vec![ BlockedPattern { pattern: Regex::new(r"(?i)(^|\s)(npm\s+(run\s+)?start|electron\b|npx\s+electron|yarn\s+start|npm\s+run\s+test:e2e:electron|pnpm\s+(run\s+)?start|pnpm\s+(run\s+)?test:e2e:electron)(\s|$)").unwrap(), + exception: None, message: r#"**Electron GUI 実行がブロックされました** Claude Code から Electron アプリを直接実行することはできません。 @@ -217,6 +231,7 @@ GUI アプリケーションは Claude Code のヘッドレス環境では動作 }, BlockedPattern { pattern: Regex::new(r"(?i)\b(npx|pnpm\s+exec)\s+playwright\s+test\b.*\belectron\b").unwrap(), + exception: None, message: r#"**Electron GUI 実行がブロックされました** Claude Code から Electron アプリを直接実行することはできません。 @@ -238,6 +253,7 @@ fn preset_jj_push_guard() -> Vec { vec![ BlockedPattern { pattern: Regex::new(r#"(?im)(^|&&|;|\|\||\||&)\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+|command\s+|env\s+)*jj\s+git\s+push(\s|$)"#).unwrap(), + exception: None, message: r#"**jj git push がブロックされました** 直接の push は禁止されています。push 前パイプライン(テスト・レビュー)を通す必要があります。 @@ -251,6 +267,7 @@ pnpm push }, BlockedPattern { pattern: Regex::new(r#"(?im)(^|&&|;|\|\||\||&)\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+|command\s+|env\s+)*jj\s+push(\s|$)"#).unwrap(), + exception: None, message: r#"**jj push がブロックされました** `jj push` は非推奨です。代わりに `jj git push` を使用しますが、 @@ -271,6 +288,7 @@ fn preset_gh_pr_create_guard() -> Vec { vec![ BlockedPattern { pattern: Regex::new(r#"(?im)(^|&&|;|\|\||\||&)\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+|command\s+|env\s+)*gh\s+(?:.*\s+)?pr\s+create(\s|$)"#).unwrap(), + exception: None, message: r#"**gh pr create がブロックされました** PR 作成は pnpm create-pr 経由で行ってください。 @@ -321,10 +339,12 @@ fn preset_polling_anti_pattern() -> Vec { vec![ BlockedPattern { pattern: Regex::new(r"(?is)\buntil\b.*?\bdo\b.*?\bsleep\s+\d").unwrap(), + exception: None, message: msg, }, BlockedPattern { pattern: Regex::new(r"(?is)\bwhile\s+!\s.*?\bdo\b.*?\bsleep\s+\d").unwrap(), + exception: None, message: msg, }, ] @@ -366,6 +386,7 @@ fn preset_exe_help_block() -> Vec { r#"(?im)(^|&&|;|\|\||\||&|\n)\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+|command\s+|env\s+)*(?:\S*?[/\\])?(?:cli-[\w-]+|hooks-[\w-]+|check-ci-[\w-]+)\.exe\s+(?:--help|-h|/\?)(\s|$)"#, ) .unwrap(), + exception: None, message: msg, }] } @@ -387,11 +408,13 @@ pnpm merge-pr // 直接実行パターン BlockedPattern { pattern: Regex::new(r#"(?im)(^|&&|;|\|\||\||&)\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+|command\s+|env\s+)*gh\s+(?:.*\s+)?pr\s+merge(\s|$)"#).unwrap(), + exception: None, message: msg, }, // シェルラッパー経由パターン (bash -c 'gh pr merge ...') BlockedPattern { pattern: Regex::new(r#"(?i)\b(bash|sh)\s+-[a-zA-Z]*c[a-zA-Z]*\s+["'][^"']*\bgh\s+(?:.*\s+)?pr\s+merge"#).unwrap(), + exception: None, message: msg, }, ] @@ -434,6 +457,7 @@ fn build_blocked_patterns(config: &Config) -> Vec { if let Ok(re) = Regex::new(custom) { patterns.push(BlockedPattern { pattern: re, + exception: None, message: "**カスタムパターンによりブロックされました**\n\nこのコマンドは hooks-config.toml のカスタムルールによりブロックされています。", }); } else { @@ -451,6 +475,11 @@ fn build_blocked_patterns(config: &Config) -> Vec { fn validate_command(command: &str, patterns: &[BlockedPattern]) -> Option<&'static str> { for pattern in patterns { if pattern.pattern.is_match(command) { + if let Some(exc) = &pattern.exception { + if exc.is_match(command) { + continue; + } + } return Some(pattern.message); } } From a635d3ef2958534f6e2ff1f18444e38ccba1d27b Mon Sep 17 00:00:00 2001 From: aloekun Date: Mon, 25 May 2026 13:47:59 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat(hooks-pre-tool-validate):=20jj-message?= =?UTF-8?q?-required=20preset=20=E8=BF=BD=E5=8A=A0=20(=E9=A0=86=E4=BD=8D?= =?UTF-8?q?=20144=20Phase=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/hooks-config.toml | 4 + src/hooks-pre-tool-validate/src/main.rs | 133 ++++++++++++++++-------- 2 files changed, 93 insertions(+), 44 deletions(-) diff --git a/.claude/hooks-config.toml b/.claude/hooks-config.toml index 7faa0f8..df0de0c 100644 --- a/.claude/hooks-config.toml +++ b/.claude/hooks-config.toml @@ -31,6 +31,9 @@ # をブロックして src//src/main.rs Read に誘導 # (PR #109 SIGPIPE 事故の直接 trigger を構造的に防止、順位 65 / Bundle c) # "electron" — Electron GUI 実行ブロック +# "jj-message-required" — jj new / jj split を -m / --message なしでブロック +# (混合 commit 事故 + jj split editor hang を構造的に防止、 +# 順位 144 / PR #171 T3-#8 採用) # プリセット名以外の文字列は正規表現としてカスタムパターン扱い blocked_patterns = [ "default", @@ -43,6 +46,7 @@ blocked_patterns = [ "polling-anti-pattern", "exe-help-block", "electron", + "jj-message-required", ] # 追加の保護ファイル (デフォルトリストに追加) diff --git a/src/hooks-pre-tool-validate/src/main.rs b/src/hooks-pre-tool-validate/src/main.rs index 586dfa3..3982e0c 100644 --- a/src/hooks-pre-tool-validate/src/main.rs +++ b/src/hooks-pre-tool-validate/src/main.rs @@ -420,56 +420,101 @@ pnpm merge-pr ] } -/// 設定ファイルに基づいてブロックパターンを構築 +/// プリセット: jj-message-required (jj new / jj split を `-m` / `--message` なしで block) +/// +/// 順位 144 (PR #171 T3-#8 採用): PR #171 セッションで `jj new` 忘れによる混合 commit 事故を +/// 実観測したのを契機に、message 必須化を機械強制する mechanical enforcement 層を導入。 +/// memory rule `feedback_pipeline_over_rules.md` 適用 = パイプライン側機械的修正で +/// Claude 判断介入を排除。 +/// +/// 設計判断 (2026-05-24 ユーザー承認済): +/// - A: `jj new` 引数なしも block (= `-m` を強制) +/// - B: `jj new ` (例: `jj new master`) で `-m` なしも block +/// - C: `jj split` interactive (= `-m` なし) は editor hang issue があるため strong block +/// - D: scope は `jj` 直接呼び出しのみ (`pnpm jj-new` 等のラッパーは scope 外) +/// +/// `BlockedPattern.exception` を活用し「pattern match + exception 不一致」の 2 段判定で +/// `-m`/`--message` 存在時の allow を実現する (Rust 標準 regex crate は negative lookahead 非対応)。 +fn preset_jj_message_required() -> Vec { + let msg = r#"**jj new / jj split に -m 引数なしがブロックされました** + +理由: +- `jj new` (引数なし or revision 指定) で message を省略すると description 未設定の commit が作成され、 + 後続の編集が意図しない commit に混入する事故が起こる (PR #171 で実観測) +- `jj split` を `-m` なしで実行すると interactive editor が起動し、Claude セッションが hang する + +**正しい使い方:** +``` +jj new -m "WIP: " # 新 commit 開始 +jj new master -m "WIP: " # revision 指定 +jj split -m "" # commit 分離 +``` + +設計判断 (順位 144、PR #171 T3-#8): `pnpm jj-new` 等の wrapper は scope 外。"#; + let exception = Regex::new(r"\s(-m|--message)\b").unwrap(); + vec![BlockedPattern { + pattern: Regex::new(r"(?im)(^|&&|;|\|\||\||&|\n)\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+|command\s+|env\s+)*jj\s+(new|split)\b").unwrap(), + exception: Some(exception), + message: msg, + }] +} + +fn default_preset_names() -> Vec { + vec![ + "default".to_string(), + "git".to_string(), + "jj-immutable".to_string(), + "jj-main-guard".to_string(), + "jj-push-guard".to_string(), + "electron".to_string(), + ] +} + +fn resolve_preset_or_custom(name: &str) -> Vec { + match name { + "default" => preset_default(), + "git" => preset_git(), + "jj-immutable" => preset_jj_immutable(), + "jj-main-guard" => preset_jj_main_guard(), + "jj-push-guard" => preset_jj_push_guard(), + "gh-pr-create-guard" => preset_gh_pr_create_guard(), + "gh-pr-merge-guard" => preset_gh_pr_merge_guard(), + "jj-message-required" => preset_jj_message_required(), + "polling-anti-pattern" => preset_polling_anti_pattern(), + "exe-help-block" => preset_exe_help_block(), + "electron" => preset_electron(), + custom => custom_regex_pattern(custom), + } +} + +fn custom_regex_pattern(custom: &str) -> Vec { + match Regex::new(custom) { + Ok(re) => vec![BlockedPattern { + pattern: re, + exception: None, + message: "**カスタムパターンによりブロックされました**\n\nこのコマンドは hooks-config.toml のカスタムルールによりブロックされています。", + }], + Err(_) => { + eprintln!( + "[validate-command] Warning: Invalid regex in blocked_patterns: {}", + custom + ); + Vec::new() + } + } +} + fn build_blocked_patterns(config: &Config) -> Vec { let preset_names: Vec = config .pre_tool_validate .as_ref() .and_then(|c| c.blocked_patterns.as_ref()) .cloned() - .unwrap_or_else(|| { - // 設定が無い場合: 全プリセット有効 (後方互換) - vec![ - "default".to_string(), - "git".to_string(), - "jj-immutable".to_string(), - "jj-main-guard".to_string(), - "jj-push-guard".to_string(), - "electron".to_string(), - ] - }); - - let mut patterns = Vec::new(); - for name in &preset_names { - match name.as_str() { - "default" => patterns.extend(preset_default()), - "git" => patterns.extend(preset_git()), - "jj-immutable" => patterns.extend(preset_jj_immutable()), - "jj-main-guard" => patterns.extend(preset_jj_main_guard()), - "jj-push-guard" => patterns.extend(preset_jj_push_guard()), - "gh-pr-create-guard" => patterns.extend(preset_gh_pr_create_guard()), - "gh-pr-merge-guard" => patterns.extend(preset_gh_pr_merge_guard()), - "polling-anti-pattern" => patterns.extend(preset_polling_anti_pattern()), - "exe-help-block" => patterns.extend(preset_exe_help_block()), - "electron" => patterns.extend(preset_electron()), - custom => { - // プリセット名以外はカスタム正規表現として扱う - if let Ok(re) = Regex::new(custom) { - patterns.push(BlockedPattern { - pattern: re, - exception: None, - message: "**カスタムパターンによりブロックされました**\n\nこのコマンドは hooks-config.toml のカスタムルールによりブロックされています。", - }); - } else { - eprintln!( - "[validate-command] Warning: Invalid regex in blocked_patterns: {}", - custom - ); - } - } - } - } - patterns + .unwrap_or_else(default_preset_names); + preset_names + .iter() + .flat_map(|name| resolve_preset_or_custom(name.as_str())) + .collect() } fn validate_command(command: &str, patterns: &[BlockedPattern]) -> Option<&'static str> { From 7dc7738e81212a11e5b52c50e0ff1c9f298f404b Mon Sep 17 00:00:00 2001 From: aloekun Date: Mon, 25 May 2026 13:50:21 +0900 Subject: [PATCH 4/5] =?UTF-8?q?test(hooks-pre-tool-validate):=20block/allo?= =?UTF-8?q?w/non-regression=20test=20=E6=8B=A1=E5=85=85=20(=E9=A0=86?= =?UTF-8?q?=E4=BD=8D=20144=20Phase=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks-pre-tool-validate/src/main.rs | 97 ++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/src/hooks-pre-tool-validate/src/main.rs b/src/hooks-pre-tool-validate/src/main.rs index 3982e0c..4e36378 100644 --- a/src/hooks-pre-tool-validate/src/main.rs +++ b/src/hooks-pre-tool-validate/src/main.rs @@ -798,12 +798,105 @@ mod tests { #[test] fn custom_regex_pattern() { - // カスタム正規表現パターン assert!(is_blocked_with("docker rm -f container", &[r"docker\s+rm"])); assert!(!is_blocked_with("docker ps", &[r"docker\s+rm"])); } - // --- git: direct commands (should block) --- + const JJ_MSG_REQ: &[&str] = &["jj-message-required"]; + + #[test] + fn jj_message_required_blocks_bare_jj_new() { + assert!(is_blocked_with("jj new", JJ_MSG_REQ)); + } + + #[test] + fn jj_message_required_blocks_jj_new_with_revision() { + assert!(is_blocked_with("jj new master", JJ_MSG_REQ)); + } + + #[test] + fn jj_message_required_blocks_jj_split_without_message() { + assert!(is_blocked_with("jj split file.rs", JJ_MSG_REQ)); + } + + #[test] + fn jj_message_required_blocks_jj_new_after_double_ampersand() { + assert!(is_blocked_with("cd /tmp && jj new", JJ_MSG_REQ)); + } + + #[test] + fn jj_message_required_blocks_jj_split_after_newline() { + assert!(is_blocked_with("echo ok\njj split src/main.rs", JJ_MSG_REQ)); + } + + #[test] + fn jj_message_required_allows_jj_new_with_m_flag() { + assert!(!is_blocked_with("jj new -m \"WIP: foo\"", JJ_MSG_REQ)); + } + + #[test] + fn jj_message_required_allows_jj_new_with_revision_and_m_flag() { + assert!(!is_blocked_with( + "jj new master -m \"WIP: foo\"", + JJ_MSG_REQ + )); + } + + #[test] + fn jj_message_required_allows_jj_new_with_long_message_flag() { + assert!(!is_blocked_with( + "jj new --message \"WIP: foo\"", + JJ_MSG_REQ + )); + } + + #[test] + fn jj_message_required_allows_jj_split_with_m_flag() { + assert!(!is_blocked_with( + "jj split -m \"split message\" file.rs", + JJ_MSG_REQ + )); + } + + #[test] + fn jj_message_required_allows_jj_split_with_long_message_flag() { + assert!(!is_blocked_with( + "jj split --message \"split message\" file.rs", + JJ_MSG_REQ + )); + } + + #[test] + fn jj_message_required_with_main_guard_still_blocks_jj_new_main_even_with_m() { + assert!(is_blocked_with( + "jj new main -m \"WIP\"", + &["jj-main-guard", "jj-message-required"] + )); + } + + #[test] + fn jj_message_required_does_not_affect_other_jj_subcommands() { + assert!(!is_blocked_with("jj status", JJ_MSG_REQ)); + assert!(!is_blocked_with("jj log", JJ_MSG_REQ)); + assert!(!is_blocked_with("jj describe -m \"x\"", JJ_MSG_REQ)); + } + + #[test] + fn jj_message_required_scope_excludes_pnpm_wrappers() { + assert!(!is_blocked_with("pnpm jj-new", JJ_MSG_REQ)); + assert!(!is_blocked_with("pnpm jj-start-change", JJ_MSG_REQ)); + } + + #[test] + fn jj_message_required_not_in_default_fallback_is_opt_in() { + let patterns = build_blocked_patterns(&Config::default()); + assert!( + validate_command("jj new", &patterns).is_none(), + "default fallback should NOT include jj-message-required (opt-in via hooks-config.toml)" + ); + } + + #[test] fn blocks_git_at_start() { From 122b378ade6ce7725a557ece43644f067c4579fe Mon Sep 17 00:00:00 2001 From: aloekun Date: Mon, 25 May 2026 13:53:24 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore(hooks):=20build=20+=20deploy=20hooks-?= =?UTF-8?q?pre-tool-validate=20exe=20+=20=E9=A0=86=E4=BD=8D=20144=20entry?= =?UTF-8?q?=20=E5=89=8A=E9=99=A4=20(Phase=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/todo-summary.md | 1 - docs/todo8.md | 65 -------------------------------------------- 2 files changed, 66 deletions(-) diff --git a/docs/todo-summary.md b/docs/todo-summary.md index 81706db..f99480d 100644 --- a/docs/todo-summary.md +++ b/docs/todo-summary.md @@ -74,7 +74,6 @@ | 141 | 🚀 Tier 1 | **CR rate-limit detection bug 修正 — fix_push_time 固定 + 早期 merge 判断 signal (PR #169 観測由来)** | todo8.md | S | なし (PR #169 セッションで systemic 観測 = wakeup ごとの push_time 更新で CR walkthrough overlay の updated_at が「過去扱い」になり parse_rate_limit の event_time >= push_time filter で除外される構造バグ、`feedback_pipeline_over_rules` 適用 = パイプライン側機械的修正で Claude 判断介入を排除、wall clock 短縮 = rate-limit 検出時に mergeable CLEAN なら 5-10 分で人間判断 (38 分 reset 待ちを bypass)、既存 auto-retry path は維持 (ユーザーが「待つ」選択時は通常 flow)、Bundle a Sub-PR 2 / Bundle f scope 外の独立 layer) | | 142 | 💎 Tier 3 | **ADR-041 補強 — "State Preservation Invariant" pattern section 追加 (PR #170 T3-#1 採用) ★ Bundle 171** | todo8.md | S | なし (PR #168/169/170 で連続観測の write-once 不変式 (once-set-never-overwritten) パターンを ADR-041 に追記、`state.fix_push_time.or_else(...)` 形式の 3 点セット test pattern (既存値あり / 新値提供 / preservation 確認) を明文化、参照実装 = poll.rs `finalize_*_preserves_existing_fix_push_time` + monitor.rs `resume_returns_fix_push_time_from_state_when_set`、ADR-041 既存 section (Multi-Condition Guards) とは別 pattern class、`feedback_no_unenforced_rules.md` 例外 = 既存実践 (3 PR で実証) の明文化 + 派生プロジェクト transferability 確保) | | 143 | 🔧 Tier 2 | **複言語 fixture helper 標準化 (hooks-post-tool-linter-tests) (PR #171 T2-#4 採用) ★ Bundle 171** | todo8.md | S | なし (PR #151/#171 の 2 PR 横断で multi-byte fixture 手動組み立てコストが Frequency Medium で観測、Japanese / emoji / combining chars helper 3 関数を標準化して新規 string-processing 関数追加時の boundary test コスト削減 + silent regression early detection、順位 142 + 144 と同 PR で land 推奨) | -| 144 | 💎 Tier 3 | **PreToolUse hook で `jj new` / `jj split` の -m 引数なしを block (PR #171 T3-#8 採用、hook 方針) ★ Bundle 171** | todo8.md | M | なし (PR #171 セッションで `jj new` 忘れによる混合 commit 事故を実観測、analyzer 原案 docs 化を `feedback_no_unenforced_rules.md` 適用でユーザー判断 hook 方針に変更、`BlockedPattern` 構造体に `exception: Option` field 追加 + 新 preset `jj-message-required` 追加で `-m`/`--message` 必須化を機械強制、設計判断 A-D ユーザー承認済、Bundle 171 の structural defense 主軸) | **戦略**: Tier 1 を 2〜3 セッションで片付け → Tier 2 で ADR-032 の前提 + rate-limit + convergence cost 削減を進める → Tier 3 で ADR-032 を land + ドキュメント整備。Tier 4-5 は cleanup / 外部展開で daily efficiency への直接効果は小さい。 diff --git a/docs/todo8.md b/docs/todo8.md index f0352db..efd9a66 100644 --- a/docs/todo8.md +++ b/docs/todo8.md @@ -601,71 +601,6 @@ analyzer report の `[ADR-041 追加 section 案]` をベースに、`docs/adr/a --- -### PreToolUse hook で `jj new` / `jj split` の -m 引数なしを block (PR #171 T3-#8 採用、hook 方針) ★ Bundle 171 - -> **動機**: PR #171 セッション中、`jj new` 忘れにより 順位 57 commit と 順位 91 commit の変更が混入する事故が発生 (後で `jj split -m` で recovery 済)。memory rule や docs 化では「次回ルールを読まなければ再発」する構造的脆弱性が残るため、PreToolUse hook で即座に block + feedback を返す機械強制層を導入する。`feedback_pipeline_over_rules.md` 適用 = パイプライン側機械的修正で Claude 判断介入を排除。 -> -> **本タスクの位置づけ**: PR #171 post-merge-feedback Tier 3 #8 採用 (Effort M / Adoption Risk None)。analyzer 原案は `~/.claude/rules/common/git-workflow.md` への docs 化だったが、ユーザー判断で **hook による mechanical enforcement** に方針変更 (2026-05-24 セッション、`feedback_no_unenforced_rules.md` 適用)。Bundle 171 の 3 タスク (順位 142 + 143 + 144) と同 PR で land 推奨。 -> -> **設計判断 (ユーザー承認済 2026-05-24)**: -> - **A**: `jj new` 引数なしも block (= `-m` を強制、混合 commit 事故の根因対策) -> - **B**: `jj new ` (例: `jj new master`) で `-m` なしも block (empty commit 防御層) -> - **C**: `jj split` interactive (= `-m` なし) は editor hang issue があるため strong block -> - **D**: scope は `jj` 直接呼び出しのみ (`pnpm jj-new` 等のラッパーは scope 外) -> -> **参照**: `.claude/feedback-reports/171.md` Tier 3 #8、`src/hooks-pre-tool-validate/src/main.rs` (`preset_jj_main_guard` / `preset_jj_push_guard` を template に追加)、`.claude/hooks-config.toml`、PR #171 session log (混合 commit recovery 経緯) -> -> **実行優先度**: 💎 **Tier 3** — Effort M。Bundle 171 の structural defense 主軸。 - -#### 設計決定 (案) - -- **`BlockedPattern` 構造体拡張**: - ```rust - struct BlockedPattern { - pattern: Regex, - exception: Option, // 新規: match しても exception が hit すれば allow - message: &'static str, - } - ``` - Rust 標準 `regex` crate は negative lookahead 非対応のため 2 段判定で実装する。 -- **`validate_command` ロジック拡張**: pattern match 後、exception 不一致時のみ block 返却 -- **新 preset `jj-message-required`**: - - block pattern: `(?im)(^|&&|;|\|\||\||&)\s*jj\s+(new|split)\b` - - exception regex: `\s(-m|--message)\b` - - message: `-m` または `--message` で message を明示するよう誘導 -- **`hooks-config.toml`** の preset リストに `jj-message-required` を追加 -- **test 拡充**: - - block: `jj new` / `jj new master` / `jj split file.rs` / `&& jj new` / 改行後 `jj split` 等 - - allow: `jj new -m "..."` / `jj split -m "..." file.rs` / `jj new --message "..."` / `pnpm jj-new` (scope 外) 等 - - 既存 preset (jj-main-guard / jj-push-guard) との non-regression - -#### 作業計画 - -- [ ] `BlockedPattern` 構造体に `exception: Option` field 追加 (既存 preset 全件に `exception: None` を補完) -- [ ] `validate_command` ロジック拡張 (~5 行) -- [ ] `preset_jj_message_required()` 関数追加 -- [ ] `apply_presets` の preset 名 dispatch に登録 -- [ ] `.claude/hooks-config.toml` の `[pre_tool_validate].presets` に `jj-message-required` 追加 -- [ ] test 拡充: block ケース 5+ / allow ケース 5+ / non-regression ケース 既存 preset 別 1+ -- [ ] 既存 pnpm scripts で `jj new` / `jj split` を内部使用していないか確認 (`pnpm jj-start-change` 等) -- [ ] 派生プロジェクト (techbook-ledger / auto-review-fix-vc) deploy 計画は本リポジトリ先行 dogfood 後判断 (breaking change リスクのため) -- [ ] 本エントリ削除 + todo-summary.md 行削除 - -#### 完了基準 - -- `jj new` / `jj split` を `-m` なしで実行すると即 block + 修正手順を含む feedback が返る -- `jj new -m "..."` / `jj split -m "..." ` は通過 -- 既存 preset (jj-main-guard / jj-push-guard / git / 等) の動作に regression なし -- `cargo test -p hooks-pre-tool-validate` pass - -#### 詰まっている箇所 - -- 設計判断 A-D は確認済 (2026-05-24 セッション、ユーザー明示承認) -- 派生プロジェクト breaking change リスクは scope 限定で軽減 (本リポジトリ先行 dogfood) -- 既存 `BlockedPattern` 構造体への field 追加は 7 preset 関数すべてに `exception: None` を補完する mechanical な edit になる (~20 箇所程度) - ---- - ## 既知課題 (記録のみ、本セッションで未対応) ### post-merge-feedback workflow が長時間 stale marker を残す問題 (PR #119 marker observed 2026-05-15)