diff --git a/docs/adr/adr-035-doc-evaluation-policy.md b/docs/adr/adr-035-doc-evaluation-policy.md
index 0b16dec..6afec2e 100644
--- a/docs/adr/adr-035-doc-evaluation-policy.md
+++ b/docs/adr/adr-035-doc-evaluation-policy.md
@@ -73,6 +73,7 @@ executable code logic への変更が **無い** こと (= AST 上の関数 body
| function length / nesting depth / complexity metrics | docs に関数は無い |
| DRY (code logic 視点) | docs hierarchy は意図的な再記述 (summary + detail) を含む。例外列挙は本 ADR で集約 |
| YAGNI (code logic 視点) | 計画文書の "future candidates" / "Phase 2 検討" / "rejected alternatives" セクションは speculative ではなく **保管すべき意思決定履歴** |
+| Magic number / hardcoded value | docs 中の数値 (閾値説明 / Phase 番号 / バージョン / 行番号引用等) は説明的記述であり code 内の magic value とは性質が異なるため適用しない。code 例として埋め込まれた数値が `const` 化推奨に該当するか否かは、対応する実 code 側 PR で判定する |
### facet instructions への反映方針
diff --git a/docs/adr/adr-040-local-llm-context-size.md b/docs/adr/adr-040-local-llm-context-size.md
index 4aeb447..5279353 100644
--- a/docs/adr/adr-040-local-llm-context-size.md
+++ b/docs/adr/adr-040-local-llm-context-size.md
@@ -45,7 +45,13 @@ ephemeral artifact (旧 analysis.md) には permanent data を残さない原則
- Phase b' (8K): 180s で 12 件 mistral invoke (`cargo test --ignored`) を完走
- Phase C (32K): 269s 観測 (= 180s 超過、cargo test 全体) → 600s に拡大
-- per-invoke latency が num_ctx に対して概ね線形に拡大する経験則 (overflow 解消後の純粋な inference time)
+- per-invoke latency は num_ctx に対して**ほぼ線形**だが、KV cache locality 効果でわずかに sublinear (`22 ms/token` → `18.3 ms/token`、17% 改善)
+
+**実測値 vs 線形 derivation の使い分け** (派生プロジェクトでの porting 時の判断指針):
+
+- **実測値 (600s) を正規採択**: Phase C cargo test で 269s 観測 → 2x safety margin で 600s。本 ADR が定義する canonical 値。
+- **線形 derivation (= 720s) は保守上限見積もり**: per-token 不変仮定 (`22 ms/token × 32768 = 721s`) は KV cache locality を無視するため過大評価。新規 model / 未測定環境での fallback ceiling として使う。
+- sublinear 性の根拠は KV cache locality 効果 (推定) で model-specific。別 model (llama2:13b 等) では再 calibration 必須。
**reference 値** (派生プロジェクトでの derivation 用):
diff --git a/docs/todo-summary.md b/docs/todo-summary.md
index 015337b..fd7234f 100644
--- a/docs/todo-summary.md
+++ b/docs/todo-summary.md
@@ -11,11 +11,10 @@
| 順位 | Tier | タスク | ファイル | 工数 | 依存 |
|---|---|---|---|---|---|
-| 1 | 🚀 Tier 1 | push 前 untracked `__*` ファイル警告 hook (PR #85 T1-4) | todo2.md | Small | なし (PR #85 直接対策) |
| 2 | 🚀 Tier 1 | `cli-push-runner` jj bookmark 未設定 early-exit (PR #85 T1-3) | todo2.md | S | なし |
| 5 | 🚀 Tier 1 | **AI 生成一時スクリプト pattern の pre-push 検出 (PR #88 T1-2)** | todo3.md | Small | 順位 1 と関連 (要擦り合わせ) |
| 6 | 🚀 Tier 1 | ADR-032 PR-pre: GitHub Branch Protection 整備 | todo2.md | 設定のみ | なし (依存タスクは完了済) |
-| 8 | 🔧 Tier 2 | 週次レビュー (ADR-031) Phase B 実装 | todo.md | 中-高 | なし (順位 20 の compensating check 前提) |
+| 8 | 🔧 Tier 2 | 週次レビュー (ADR-031) Phase B 実装 — 7 観点責務 mapping 確定 (① ハーネス遵守 + ⑥ テストロジック を MVP 優先、2026-05-26 ユーザー合意) | todo.md | 中-高 | 順位 20 compensating check 前提 + 順位 136 land 先行推奨 (観点 ⑤ 責務分離) |
| 10 | 🔧 Tier 2 | ADR-032 PR-broken-link: broken-link-check + 内部アンカー検査 統合 | todo2.md | Small-中 | なし (clean baseline 確立済) |
| 11 | 🔧 Tier 2 | `cli-pr-monitor` プロセス正常終了の integration test (PR #85 T2-2) | todo2.md | S | なし |
| 16 | 🔧 Tier 2 | **`vitest` を devDependencies に固定 (PR #88 T2-3)** | todo3.md | Small | なし |
@@ -59,12 +58,10 @@
| 108 | 💎 Tier 3 | **CLAUDE.md に「Tier 2 偽装検知 + 却下パターン」table (PR #141 T3-#3 採用)** | todo6.md | S | なし (`~/.claude/CLAUDE.md` に memory `feedback_no_unenforced_rules` の policy をユーザー可視 table として公開、Tier 2 と称した必須化ルール提案を新セッションでも一貫して却下できる構造、memory ファイル閉鎖を補完) |
| 110 | 💎 Tier 3 | **pure function test pattern template を `testing.md` に追記 (PR #142 T2-#3 採用)** | todo6.md | S | なし (Phase A の `overflow_hint()` をモデル例とし「境界値 / None / 閾値未満」3 パターンの test テンプレを `~/.claude/rules/common/testing.md` に追記、副作用分離の促進、Rust lib 全般で再利用) |
| 111 | 💎 Tier 3 | **`docs-governance.md` に todo5/todo6 routing rule 明文化 (PR #142 T3-#1 採用)** | todo6.md | S | なし (Phase/bundle 関連 → todo6、global rules/lint → todo5 等の routing rule を `~/.claude/rules/common/docs-governance.md` に追記、PR #142 で実証された file pointer bifurcation の構造的予防、CR Minor #2 と同根) |
-| 116 | 💎 Tier 3 | **ADR-040 `step_timeout` 説明に sublinear / KV cache locality clarification 追記 (PR #145 T3-#1 採用)** | todo8.md | XS | なし (L42-48 で「sublinear (3.33x)」と「per-invoke latency が概ね線形」が並存し reference table 600s と formula 720s が乖離。実測値 600s 採択 + 保守上限 720s + sublinear 性の KV cache locality 根拠を 2-3 行追記して整合化、永続 ADR の数値正確性確保) |
| 117 | 💎 Tier 3 | **`coding-style.md § Cross-File Reference Lifecycle` に ephemeral → permanent 知識移管 edit order 追記 (PR #145 T3-#3 採用)** | todo8.md | S | なし (PR #145 で lib.rs L128-139 → ADR-040 移管 + Phase C/D empirical data 移管の 2 観測。既存ルール (参照方向制約) と complementary な「① permanent target 先行作成・validate → ② 参照追加 → ③ 参照元削除」3 ステップ原則を `~/.claude/rules/common/coding-style.md` に codify、次回 ephemeral 計画書 retire 時の checklist として再利用) |
| 118 | 💎 Tier 3 | **rule⑧ への paths filter 適用範囲検討 (順位 102 land 時の意図的保留、follow-up)** | todo8.md | XS | 順位 102 (PR #148 land 済、Phase D D-3) で paths filter は実装済だが、rule⑧ への `paths = ["docs/**/*.md"]` migration は D-2 (PR #146、順位 101) で追加した root-level MD fire intent を壊すため保留。4 案 (保留継続 / broader glob / explicit list / rule split) の trade-off 評価を ADR-007 amendment (順位 104) と整合させて結論を出す |
| 128 | 💎 Tier 3 | **CLAUDE.md § Cross-File Reference Lifecycle に多ファイル同時削除 retirement condition checklist を追加 (PR #153 T3-#2 採用)** | todo8.md | XS | なし (PR #133 (todo.md 分割) + PR #153 (analysis.md 分割) の successful pattern を明文化、`feedback_no_unenforced_rules.md` 例外 = 既存実践の明文化 + guide 効果、順位 122 / 127 と同じロジック、`~/.claude/` global 配下で派生プロジェクトに自動波及) |
| 133 | 💎 Tier 3 | **docs-governance §Retirement Workflow に「diff context 由来 false alarm 防止 = grep hit は実ファイル Read で確認」明記 (PR #156 T3 #1 採用)** | todo8.md | XS | なし (PR #156 で 5 件以上の false alarm 発生、`feedback_no_unenforced_rules.md` 例外 = 既存実践の明文化 + guide 効果、順位 122 / 127 と同じロジック、`~/.claude/` global 配下で派生プロジェクトに自動波及) |
-| 134 | 💎 Tier 3 | **ADR-035 に docs-only PR 評価の適用外基準リスト追加 (mutation / error handling / DRY / YAGNI / function length / test coverage / magic-number 等) (PR #156 T3 #2 採用)** | todo8.md | S | なし (Severity Medium = reviewer の criteria 誤適用による unnecessary review overhead / 開発体験劣化、ADR-035 は分類基準のみ定義済で適用外基準が未明示、`feedback_no_unenforced_rules.md` 例外 = ADR への追加で機械強制ではなく reviewer / Claude の judgment 補助) |
| 135 | 💎 Tier 3 | **todo entry の ADR 番号 hardcode 撤廃 — 「ADR-NNN (採番未確定、land 時に確定)」placeholder 採用 (順位 78 番号 conflict 2026-05-16 観測由来)** | todo8.md | XS | なし (順位 78 (旧 ADR-038 → ADR-041) で番号 conflict が顕在化、queue 滞留 entry の hardcode が後発 PR の採番と衝突する構造リスクを convention で予防、`~/.claude/rules/common/docs-governance.md` に 2-3 行追記。採番予約簿は管理コスト過剰のため見送り、land 時 PR で空き番号確定の軽量運用に統一) |
| 136 | 🚀 Tier 1 | **working copy staleness 検出 hook 2 段構え + stale todo entry 既実装 grep 提示 (PR cleanup-stale-rank-39 由来 + PR #150 T3-#1 統合 2026-05-25)** | todo8.md | M-L | なし (本セッション実証 failure mode の structural enforcement + 旧 順位 122 機能統合。案 A SessionStart で jj fetch + lineage 報告、案 B PreToolUse で docs/todo*.md edit 時の stale block + 既実装 grep 自動実行で関連 commit を warning 提示、rule 追加 (= 順位 122 当初案) を仕組み化に切替で session 跨ぎ品質一定化、ADR-039 experimental pattern 適用) |
| 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 確保) |
@@ -77,6 +74,9 @@
| 149 | 🔧 Tier 2 | **Long-running subprocess pipe truncate hook 拡張 — `development-workflow.md` § subprocess pipe truncate 禁止 移管 (PR #172 仕組み化方針切替由来) ★ Bundle 既存ルール仕組み化** | todo9.md | S | なし (既存 `exe-help-block` preset を `cli-*.exe ... \| (head\|tail\|awk)` 等の副作用ある subprocess 出力 truncate にも拡張 or 新 `subprocess-pipe-truncate-block` preset 追加、PR #109 SIGPIPE 事故 root cause の構造化、順位 44 (gh-token-efficiency) との scope 境界整理必要、development-workflow.md § 該当 section 縮小) |
| 150 | 🔧 Tier 2 | **Magic number lint 追加 — `coding-style.md` § Magic Numbers 移管 (PR #172 仕組み化方針切替由来、ユーザー判断 2026-05-25 = source folder 限定) ★ Bundle 既存ルール仕組み化** | todo9.md | M | なし (`.claude/custom-lint-rules.toml` に `no-magic-number` rule 追加、source folder paths filter で test/config 除外、時間定数 / リトライ回数 / threshold の 3 category MVP、severity warning で reviewer 判断補助、順位 102 paths filter + 順位 118 適用範囲検討と整合、coding-style.md § Magic Numbers 削除可否は dogfood 後判断) |
| 151 | 🔧 Tier 2 | **PR diff lines check 追加 — `git-workflow.md` § Multi-PR chaining 移管 (PR #172 仕組み化方針切替由来、ユーザー判断 2026-05-25 = 条件付き block 3 段階) ★ Bundle 既存ルール仕組み化** | todo9.md | S | なし (`src/cli-push-runner/src/stages/pr_size_check.rs` 新 stage 追加、`push-runner-config.toml` `[pr_size_check]` section で threshold 設定可能化 (default: block 1500 / warning 800)、jj diff stat 計測、大型 refactoring 時の override は config 編集、git-workflow.md § Multi-PR chaining 縮小) |
+| 152 | 🔧 Tier 2 | **todo entry 削除時の事前 land 確認手順 — 順位 136 hook 拡張 or 独立 follow-up (PR #173 T2-1 採用、2026-05-26)** | todo9.md | XS-S | 順位 136 (working copy staleness + 既実装 grep) と同型機械強制、lifecycle 補完 = 順位 136 (add/edit 時) + 本タスク (delete 時)。PreToolUse hook で `docs/todo*.md` 削除時に対応 land commit を `jj log` で grep 検証、land 確認なら allow + 証跡出力、未確認なら warning (block しない)。順位 136 hook 統合 (~+15 行) or 独立 (~40 行) のいずれか、ADR-042 § Decision matrix 適用 (mechanizable + FP 低 + Adoption Risk None) |
+| 153 | 🔧 Tier 2 | **`review-harness-whole` facet 追加 — 観点 ① 独立 facet 化 (順位 8 follow-up、Phase B+1、2026-05-26 ユーザー合意) ★ 週次拡張** | todo9.md | S | 順位 8 Phase B land + 2-3 週 dogfood 後に着手判断 (extract 不要なら close)、順位 146-151 Bundle 既存ルール仕組み化の継続的発見源、architecture-whole から ① 観点を extract して context 圧迫回避 |
+| 154 | 🔧 Tier 2 | **`review-todo-whole` facet + aggregate 前 file size pre-step — 観点 ⑤ ⑦ 拡張 (順位 8 follow-up、Phase B+1、2026-05-26 ユーザー合意) ★ 週次拡張** | todo9.md | M | 順位 136 land + Phase B 2-3 週 dogfood 完了後着手、順位 95 / 147 と scope 整理必要 (CI 即時 vs 週次 batch)、ADR-031 3 層分離原則で file size は LLM 不要の Rust pre-step に分離 |
**戦略**: 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/todo.md b/docs/todo.md
index 2ad893d..8a20cdf 100644
--- a/docs/todo.md
+++ b/docs/todo.md
@@ -279,6 +279,22 @@ SessionStart hook (hooks-session-start.exe 拡張)
| 失敗ポリシー | **best-effort** (`.failed` marker + SessionStart hook reminder で再実行誘導。must-run ではないので決定論ゲート不要) |
| アンチパターン | **whole-tree 用 facet を diff 用 facet と共通化しない** (ADR-027 で diff 局所が本質要件のため separation 必須) |
+#### 7 観点責務 mapping (2026-05-26 ユーザー合意、AskUserQuestion 経由)
+
+ユーザー希望 7 観点を ADR-031 設計の 3 facets に **prompt 重点配分** で対応。facet 数は増やさず YAGNI + context 圧迫リスク回避 (MVP 維持)。MVP 優先観点は **① ハーネス遵守 + ⑥ テストロジック** で、各 facet prompt の筆頭 criteria に組み込む。
+
+| 観点 | 担当 facet | prompt 重点 |
+|---|---|---|
+| ① ハーネス遵守 (rule < pipeline < hook 重複) | architecture-whole | **MVP 最優先** — facet criteria の筆頭、rule/pipeline/hook 重複検出、順位 146-151 Bundle 既存ルール仕組み化の継続的発見源 |
+| ② docs 内整合性 | architecture-whole の sub criterion | ADR 間 supersedes / cross-reference / todo routing、順位 10 / 95 / 96 と補完 |
+| ③ docs-source 矛盾 | architecture-whole の sub criterion | 重要 ADR 限定リスト (ADR-007 / 012 / 021 / 022 等) で context 圧迫回避 |
+| ④ セキュリティ | security-whole | ADR-031 設計通り、変更なし |
+| ⑤ Todo 妥当性 | **MVP 対象外** (順位 136 hook へ委譲) | hook = 編集時 immediate guard / 週次 = batch 棚卸し で責務分離、Phase B+1 で順位 154 facet として再評価 |
+| ⑥ テストロジック (振る舞い vs 実装詳細、境界) | simplicity-whole | **MVP 最優先** — facet criteria の筆頭、TDD anti-pattern + 境界欠落、順位 38 (cargo-mutants L3 weekly) と cross-validate |
+| ⑦ ファイルサイズ (50KB) | aggregate 前の Rust 機械 pre-step (Phase B+1) | facet 不要、機械検査で十分。順位 154 で順位 95 / 147 と scope 整理 |
+
+**Bundle 戦略**: **Phase B 単体で land** (順位 38 / 95 / 96 は別 PR、PR diff 250-800 行に収める方針)。Phase B+1 で観点 ① ⑤ ⑦ を独立 facet / pre-step に extract する余地を残す (順位 153 / 154 を follow-up 登録済)。
+
#### 作業計画
##### Phase B: takt workflow + facets + persona (PR 2)
diff --git a/docs/todo2.md b/docs/todo2.md
index 6b2c169..a72d283 100644
--- a/docs/todo2.md
+++ b/docs/todo2.md
@@ -362,40 +362,6 @@ Phase 観測 (4-6 週)
Phase 2 (任意、段階的緩和)
```
-### push 前 untracked `__*` ファイル警告 hook (PR #85 T1-4)
-
-> **動機**: PR #85 で `__parse_transcripts.ps1` が jj auto-snapshot 経由で commit に意図せず混入。`.gitignore` への `__*` 追加で当面の再発は防止できたが、将来 `.gitignore` 漏れの可能性は残る。push 前に `__*` 命名の untracked file が working directory に残っていないか機械的に検出する安全網が必要。
->
-> **本タスクの位置づけ**: jj 環境では staging area が無く `.gitignore` が唯一のフィルタ。push 前 hook で `__*` パターンの untracked file を検出し警告すれば、`.gitignore` 漏れがあっても気付ける。
->
-> **参照**: `.claude/feedback-reports/85.md` Tier 1 #4
->
-> **実行優先度**: 🚀 **Tier 1** — Small 工数、直近インシデントの直接対策。同種事故 (PR scope 外ファイル混入) の再発防止で、混入時の追加コスト (force-push + 再 review) を回避。
-
-#### 設計決定 (案)
-
-- 配置先: `cli-push-runner` の早期段階 (bookmark check の隣)、または独立 hooks binary
-- 検出方法: `jj status` 出力から `Untracked` セクションを parse、`__*` パターンとマッチング
-- 失敗時挙動: warning + ユーザー確認待ち (本人が意図的に scratch を残している場合の override を許容)
-- config: `[scratch_file_warning] patterns = ["__*"]` で将来の拡張性確保
-
-#### 作業計画
-
-- [ ] 検出ロジックを `cli-push-runner` または共通ライブラリに実装
-- [ ] config に `[scratch_file_warning]` セクション追加
-- [ ] dogfood: `__test.ps1` を意図的に作って push し、警告を確認
-- [ ] 派生プロジェクトへ deploy
-- [ ] 本 todo2.md エントリを削除
-
-#### 完了基準
-
-- push 前に `__*` 命名の untracked file が存在すると警告が出る
-- override コマンド (env var or flag) で意図的バイパスが可能
-
-#### 詰まっている箇所
-
-なし
-
### `cli-push-runner` jj bookmark 未設定 early-exit (PR #85 T1-3)
> **動機**: PR #85 の初回 `pnpm push` で bookmark 未設定 → `jj git push` が `Nothing changed` で終了し、158s かけて走った Quality Gate + takt review がすべて無駄になった。jj 環境特有の落とし穴で、決定論的に防止可能。
diff --git a/docs/todo8.md b/docs/todo8.md
index 55a8c43..36c6715 100644
--- a/docs/todo8.md
+++ b/docs/todo8.md
@@ -10,29 +10,6 @@
## 現在進行中
-### ADR-040 `step_timeout` 説明に sublinear / KV cache locality clarification 追記 (PR #145 T3-#1 採用)
-
-> **動機**: ADR-040 L42-48 の `step_timeout` 説明は「sublinear (3.33x)」と記述したが、本文中に「per-invoke latency が num_ctx に対して概ね線形に拡大する経験則」も併記しており、両者の関係が不明瞭。派生プロジェクトが reference table から 32K = 600s を読む際、なぜ formula `(num_ctx/8192)*180` で導出される 720s と乖離するかが直感的に分からない。clarification として「実測値 600s を正規値として採択、computed 720s は保守上限の目安、sublinear 性の根拠は KV cache locality 効果 (大規模 context で per-token efficiency 向上)」の 2-3 行追記が必要。
->
-> **本タスクの位置づけ**: PR #145 post-merge-feedback Tier 3 #1 採用 (Severity Low / Frequency Low / Effort XS / Adoption Risk None)。永続 ADR の数値整合性確保。
->
-> **参照**: `.claude/feedback-reports/145.md` Tier 3 #1、`docs/adr/adr-040-local-llm-context-size.md` L42-48
-
-#### 作業計画
-
-- [ ] ADR-040 § `step_timeout` 比例係数の根拠 に 2-3 行追記:
- - 実測値 600s を正規採択、computed 720s は保守上限見積もり
- - sublinear 性 (3.33x vs context 4x) の根拠 = KV cache locality 効果 (推定)
- - 派生プロジェクトでの derivation 時は実測 cargo test 経過時間の 2x margin を採用
-- [ ] 本エントリ削除 + todo-summary.md 行削除
-
-#### 完了基準
-
-- ADR-040 の reference table と本文の formula が矛盾なく解釈可能になる
-- 派生プロジェクトの porting 時に sublinear の根拠が永続記録から逆引きできる
-
----
-
### rule⑧ への paths filter 適用範囲検討 (順位 102 land 時に意図的保留、follow-up)
> **動機**: 順位 102 (PR #148 想定で land 中、Phase D D-3) で paths filter が lint runner に実装されたが、当初計画した rule⑧ への `paths = ["docs/**/*.md"]` migration は **意図的に保留**。理由: D-2 (PR #146、順位 101) で追加した「root-level MD (CLAUDE.md / README.md) からの `../docs/` 参照を fire = true positive で扱う」design intent が、`paths = ["docs/**/*.md"]` 適用で scope narrow されて壊れる (root-level MD の実 path が docs/ 配下ではないため rule 対象外になり、broken link 検出を失う)。本タスクで以下のいずれを採用するか検討する:
@@ -142,51 +119,6 @@
---
-### ADR-035 に docs-only PR 評価の明示的な適用外基準リストを追加 (PR #156 T3 #2 採用)
-
-> **動機**: ADR-035 は docs-only PR の **分類基準** (どの PR が docs-only か) は定義しているが、**除外される評価観点** (docs-only PR で適用すべきでない code-logic 系評価項目) が明示されていない。PR #156 (Phase E、docs-only) で reviewer が mutation / error handling / test coverage 等の code-logic criteria を docs-only PR に適用しかけて unnecessary review overhead が発生する潜在リスクが観測された。明示することで将来セッションでの reviewer による criteria 誤適用を防止できる。
->
-> **本タスクの位置づけ**: PR #156 post-merge-feedback Tier 3 #2 採用 (Severity Medium / Frequency Low / Effort S / Adoption Risk None)。Severity Medium の根拠 = 誤適用による unnecessary review overhead / 開発体験劣化。`feedback_no_unenforced_rules.md` 例外条件 = ADR (= 設計判断 doc) への追加で機械強制ではなく reviewer / Claude の judgment 補助。
->
-> **参照**: `.claude/feedback-reports/156.md` Tier 3 #2、`docs/adr/adr-035-doc-evaluation-policy.md`
-
-#### 設計決定 (案)
-
-- **配置先**: `docs/adr/adr-035-doc-evaluation-policy.md` 内に新 section 「docs-only PR で適用しない評価観点」を追加
-- **適用外基準リスト (案)**:
- - **Mutation / immutability**: docs に code mutation は存在しないため適用しない
- - **Error handling**: docs に error path は存在しないため適用しない
- - **Test coverage**: docs に test は不要なため適用しない (test 文言の追加自体は除く)
- - **Function length / complexity**: docs に関数は存在しないため適用しない
- - **DRY / YAGNI**: docs では intentional な重複・冗長な記述が reader にとって有益な場合があるため適用しない (例: 同じ概念を複数 section で説明する)
- - **Magic number / hardcoded value**: docs 中の数値は説明的記述で magic ではないため適用しない
-- **適用される評価観点** (既存 ADR-035 で定義済みのものを再確認):
- - Cross-reference lifecycle (permanent → ephemeral 禁止)
- - Markdown syntax / lint
- - Anchor link validity
- - Retirement workflow 整合
- - 内容の正確性 / typo
-
-#### 作業計画
-
-- [ ] `docs/adr/adr-035-doc-evaluation-policy.md` の構造確認 (既存 section header の慣習)
-- [ ] 「適用外基準リスト」section を追加
-- [ ] 既存 ADR の評価観点 section との整合性確認 (重複説明の有無、cross-reference の追加)
-- [ ] markdownlint clean 確認
-- [ ] 本エントリ削除 + todo-summary.md 行削除
-
-#### 完了基準
-
-- docs-only PR の reviewer / Claude が「mutation / DRY 等は適用しない」を ADR から逆引きできる
-- 将来の docs-only PR 評価で criteria 誤適用が systemic に発生しなくなる
-- markdownlint clean
-
-#### 詰まっている箇所
-
-なし。Effort S、ADR への追記のみで副作用最小。
-
----
-
### todo entry の ADR 番号 hardcode 撤廃 — 「ADR-NNN (採番未確定、land 時に確定)」placeholder 採用 (順位 78 番号 conflict 2026-05-16 観測由来)
> **動機**: 順位 78 (旧 ADR-038 Rust timestamp arithmetic safety、PR #115 T3-1) は entry 登録時 (2026 年序盤) に新規 ADR として ADR-038 を予約のつもりで hardcode していたが、queue 滞留中に Bundle Z 系列の連続採用で `ADR-037 / 038 / 039 / 040` がすべて占有され、2026-05-16 セッションで番号 conflict が顕在化 (ADR-041 へ振り直し)。さらに 2026-05-22 に順位 139 (PR #168 follow-up) が ADR-041 を取得したため順位 78 を再 placeholder 化 = **同一 entry が 3 回 (038 → 041 → NNN) 番号変更を経た実証ベース**で、queue 深度と滞留期間の積に比例して同型 conflict が再発する構造リスクを convention で予防する必要がある。
@@ -233,6 +165,8 @@
>
> **本タスクの位置づけ**: 本セッション post-merge-feedback 相当の structural defense + 旧 順位 122 機能統合。`feedback_no_unenforced_rules.md` 例外条件 = **2 つの hook で機械強制可能**。案 A (予防層 = session 起動時に状況認識) + 案 B (最終 backstop = stale 状態での編集を hard block + 既実装 grep 提示) のセット二段構え。
>
+> **週次レビュー (ADR-031) 観点 ⑤ Todo 妥当性 との責務分離 (2026-05-26 ユーザー合意)**: **本 hook = 編集時 immediate guard / 週次 = 全 entry 横断 batch 棚卸し**。本 hook land 後の週次レビュー Phase B+1 (順位 154 `review-todo-whole` facet) は hook が拾えない broad な観点 (経年劣化 entry / cross-file 重複 / preamble drift) に focus する設計。順位 8 entry の「7 観点責務 mapping」表参照。
+>
> **参照**: 本セッション (2026-05-18) PR cleanup-stale-rank-39 root cause 分析 (ユーザー対話)、PR #150 post-merge-feedback Tier 3 #1 (旧 順位 122 由来)、memory `feedback_verify_task_not_already_done.md`、ADR-039 (Experimental feature 標準パターン)、PR #172 (順位 144 hook 化 dogfood 事例)
>
> **実行優先度**: 🚀 **Tier 1** — Effort Medium-Large (案 A ~80 行 + 案 B ~50 行 = 既実装 grep 拡張で +~20 行)。本セッションの実観測 failure mode に対する直接対策で、並列セッション運用が常態化している現状で再発確率が高い。
diff --git a/docs/todo9.md b/docs/todo9.md
index 9cba1ee..278c086 100644
--- a/docs/todo9.md
+++ b/docs/todo9.md
@@ -305,6 +305,148 @@
---
+### todo entry 削除時の事前 land 確認手順 — 順位 136 hook 拡張 or 独立 follow-up (PR #173 T2-1 採用、2026-05-26)
+
+> **動機**: PR #173 で land 済 entry (順位 125 / 139 / 141) を todo8.md から削除した際、削除前の land 状態確認は実装 grep ベースの「事後 verify」で実施し全て land 確認できたが、「事前確認」の機械強制はなかった。post-merge-feedback analyzer (T2-1) で「rank 125 / 141 の actual land status を `jj log` で確認、未実装なら todo に復帰」採用判定が成立 (Severity Medium / Frequency Low / Effort XS / Adoption Risk None)。今回 false alarm (実装は全 land 済) だったが、将来「削除前に land 確認」を機械強制すれば誤削除を構造的に防止できる。
+>
+> **本タスクの位置づけ**: 順位 136 (working copy staleness hook + stale todo entry 既実装 grep 提示) と **同型の機械強制タスク**、lifecycle 補完関係:
+>
+> - 順位 136: **add / edit 時**に既実装の commit を grep 提示 (= 「既に実装済では?」warning)
+> - **本タスク (順位 152)**: **delete 時** に対応 land commit を grep 検証 (= 「本当に land 済?」warning)
+>
+> 順位 136 hook 実装時に統合検討 (= 同一 PreToolUse hook で add/edit/delete の edit 種別を判定して分岐)、または独立 hook (= shared utility 経由) で別 task 化のいずれか。ADR-042 § Decision matrix 適用 = **mechanizable + FP 低 + Adoption Risk None** で仕組み化 zone。
+>
+> **参照**: `.claude/feedback-reports/173.md` Tier 2 #1、順位 136 entry (本ファイル内)、PR #173 セッションで実施した実装 grep 検証 (rank 125 = `run_custom_rules_line_number_correct_with_multibyte_content` test 存在 / rank 139 = `docs/adr/adr-041-test-isolation-patterns.md` 存在 / rank 141 = `fix_push_time` + `RATE_LIMIT_BUT_MERGEABLE` シグナル存在)、ADR-042 (rule vs mechanism boundary)、memory `feedback_pipeline_over_rules.md`
+>
+> **実行優先度**: 🔧 **Tier 2** — Effort XS-S。順位 136 に統合する場合は追加 ~15 行 (edit 種別判定 + delete branch)、独立 hook の場合は ~40 行 (構造的に分離)。
+
+#### 設計決定 (案)
+
+- **検出条件**: `docs/todo*.md` への Edit/Write で `### 順位 N ` セクション (or `###
` headed entry) が削除されたパターン
+ - Edit tool の `old_string` に `### ` で始まる entry header が含まれ、`new_string` に含まれない場合 = 削除と判定
+ - Write tool で全文書き換えの場合は old/new file の `### ` header 数を比較
+- **動作**: 削除対象 entry の keyword (見出し title から抽出、順位 prefix / 句読点 除去) を `jj log --limit 30` で grep
+- **判定**:
+ - 関連 commit (= 「順位 N land」「PR #XXX」「 land 済」等の description) を検出 → 削除を **allow** + 検出 commit を additional context に出力 (削除証跡として残る)
+ - 関連 commit なし → **warning** (block ではなく feedback) + 「削除前に land 確認推奨。defer / withdraw の場合は commit message に明記推奨」を出力
+- **scope**: 順位 136 hook (PreToolUse on docs/todo*.md edit) に統合する case が推奨。共通の `jj log` grep utility を共有
+- **block vs warning 設計判断**: AI が大量 land 済 entry を一括削除するケース (本 PR #173 でも 3 件削除) を考慮し、warning にとどめる。block にすると mass cleanup PR で UX 阻害
+
+#### 作業計画
+
+- [ ] 順位 136 hook 実装時に edit 種別判定ロジック (add / edit / delete) を含める設計検討
+- [ ] delete 検出: `old_string` に `### ` entry header あり / `new_string` になし pattern
+- [ ] keyword 抽出 (順位 prefix / 句読点 除去) + `jj log --limit 30` grep
+- [ ] 結果出力フォーマット (land 確認時 = additional context に commit 列挙 / 未確認時 = warning)
+- [ ] test fixture (4 ケース): delete + land あり / delete + land なし / add + 既実装あり / add + 既実装なし
+- [ ] 派生プロジェクト deploy 検討 (順位 136 と同タイミング)
+- [ ] 本エントリ削除 + todo-summary.md 行削除
+
+#### 完了基準
+
+- `docs/todo*.md` への delete 操作時、対応 land commit が grep で検出されれば allow + 証跡 output
+- land commit なし時は warning (block しない) で AI に再確認を促す
+- 順位 136 (add/edit 時 既実装 grep) と統合 or 独立で lifecycle カバレッジ完成
+- 派生プロジェクト transferability
+
+#### 詰まっている箇所
+
+- **edit 種別判定の複雑性**: Edit tool の old/new 比較で削除を判定可能だが、部分削除 + 他箇所改修の混在 edit で false negative リスク。最小単位は「順位 N entry 全体の削除」のみ対象とする MVP が現実的
+- **keyword 抽出の精度**: 順位 prefix 除去後の title 残りで grep するが、title に表記揺れ (例: "ADR-041 Test Isolation Patterns" vs "Test Isolation Patterns ADR") があると false negative。順位 N をそのまま grep する case も併用検討
+- **mass cleanup PR との両立**: 本 PR #173 のように 3 件以上の land 済 entry を一括削除する PR では各削除で warning が累積し UX 阻害。1 PR 内で同 file の delete N 件目以降は output 抑制 等の noise 軽減策必要
+
+---
+
+### `review-harness-whole` facet 追加 — 観点 ① 独立 facet 化 (順位 8 follow-up、Phase B+1、2026-05-26 ユーザー合意)
+
+> **動機**: 順位 8 (週次レビュー Phase B) の MVP は 3 facets (simplicity / security / architecture) 構成で start し、観点 ① ハーネス遵守 (rule < pipeline < hook 重複検出) は architecture-whole facet の prompt 重点 criteria として組込。Phase B dogfood で「① 観点が architecture-whole の他 criteria (ADR 整合性 / モジュール境界 / 命名規約 / 循環依存) と context 圧迫」が観測されたら、独立 facet `review-harness-whole` に extract する。
+>
+> **本タスクの位置づけ**: 順位 8 の follow-up、Phase B+1。Phase B dogfood 結果を見てから着手判断 (extract 不要なら本 entry close)。順位 146-151 (Bundle 既存ルール仕組み化) の **継続的発見源** として機能し、新 rule → hook 昇格候補を週次で systemic に拾う構造を強化する。
+>
+> **参照**: 順位 8 entry (todo.md 「7 観点責務 mapping」表)、順位 146-151 Bundle 既存ルール仕組み化、`feedback_no_unenforced_rules.md`、`feedback_pipeline_over_rules.md`、ADR-031 (週次レビュー設計)
+>
+> **実行優先度**: 🔧 **Tier 2** — Effort S。順位 8 Phase B land + 2-3 週 dogfood 後に着手判断。
+
+#### 設計決定 (案)
+
+- 配置: `.takt/facets/instructions/review-harness-whole.md` 新規 facet (allowed_tools: Read/Glob/Grep のみ)
+- 観点: `~/.claude/rules/common/*.md` の各 rule を全文走査 + `.claude/custom-lint-rules.toml` / `.claude/hooks-config.toml` / `push-runner-config.toml` と突き合わせ → rule docs に記載があるが hook / pipeline 未実装の項目を finding として抽出
+- aggregate-weekly 側で finding category `harness-rule-coverage-gap` として独立 group 化
+- Phase B+1 着手判断条件: Phase B dogfood で architecture-whole の output から ① 観点 finding 数が多く他 criteria の finding 質が劣化、または ① 観点が見落とされていると観測された場合
+
+#### 作業計画
+
+- [ ] Phase B (順位 8) land + 2-3 週 dogfood 運用 → ① 観点 finding の context 圧迫 / 見落としを観測
+- [ ] facet extract 判断 (extract 不要なら本 entry close)
+- [ ] `review-harness-whole.md` instruction 設計 (順位 146-151 land 済 / 未済の状況を踏まえた rule-vs-hook gap 検出ロジック)
+- [ ] takt workflow weekly-review.yaml に facet 追加 + `parallel:` block 拡張
+- [ ] aggregate-weekly facet 拡張 (新 category) + pending JSON schema 拡張
+- [ ] dogfood + 本エントリ削除 + todo-summary.md 行削除
+
+#### 完了基準
+
+- ① ハーネス遵守 観点が独立 facet で週次検出される
+- architecture-whole は ADR 整合性 / モジュール境界 / 命名規約 / 循環依存 に集中
+- 新規 rule 追加時の hook 昇格候補が systemic に提案される
+
+#### 詰まっている箇所
+
+- Phase B dogfood 結果次第 (extract 不要なら本 entry close)
+- ① 観点と ② docs 内整合性の境界判断 (rule docs 整合 vs その他 docs 整合の cross-cut)
+
+---
+
+### `review-todo-whole` facet + aggregate 前 file size pre-step — 観点 ⑤ ⑦ 拡張 (順位 8 follow-up、Phase B+1、2026-05-26 ユーザー合意)
+
+> **動機**: 順位 8 (週次レビュー Phase B) の MVP では観点 ⑤ Todo 妥当性 は順位 136 (todo hook 2 段構え) に委譲し、観点 ⑦ ファイルサイズ も対象外とした。順位 136 hook land 後、hook が拾えない broad な観点 (全 todo entry 横断の dead pattern 検出 / cross-todo file の重複 entry / docs/todo*.md preamble drift) を週次の `review-todo-whole` facet で補完する。並行して観点 ⑦ ファイルサイズ (50KB / 800 行) は aggregate-weekly facet 直前の Rust 機械 pre-step で計測し、LLM context を浪費せず ADR-031 の 3 層分離 (Rust 機械 / takt AI / skill ask) に整合させる。
+>
+> **本タスクの位置づけ**: 順位 8 の follow-up、Phase B+1。順位 136 hook land 後に着手判断 (= hook の immediate guard が機能している前提で、週次は batch 棚卸しに focus)。`feedback_pipeline_over_rules.md` 適用で、機械検査可能な観点 (file size) を LLM facet に乗せず分離する設計。
+>
+> **参照**: 順位 8 entry (todo.md 「7 観点責務 mapping」表)、順位 136 entry (todo8.md、todo hook 2 段構え)、順位 95 (preamble file count CI 自動照合)、順位 147 (file length lint 800 行)、ADR-031 (3 層分離 = Rust 機械 / takt AI / skill ask)、`feedback_pipeline_over_rules.md`
+>
+> **実行優先度**: 🔧 **Tier 2** — Effort M (facet 新規 + Rust pre-step ~80 行)。順位 136 land + Phase B 2-3 週 dogfood 完了後に着手。
+
+#### 設計決定 (案)
+
+**`review-todo-whole` facet (観点 ⑤ 補完):**
+
+- 配置: `.takt/facets/instructions/review-todo-whole.md` 新規 facet (allowed_tools: Read/Glob/Grep のみ)
+- 観点: 全 todo*.md entry を横断走査 → dead pattern (= 半年以上 stale + 関連 commit なし + 依存 task land 済) / cross-file 重複 entry / preamble routing drift を finding として抽出
+- 順位 136 hook が拾えない範囲: 編集していない entry の経年劣化 / file 跨ぎの重複 / preamble file count drift
+
+**aggregate 前 Rust 機械 pre-step (観点 ⑦):**
+
+- 配置: takt workflow weekly-review.yaml の aggregate-weekly facet 直前に新 step 追加 (or aggregate facet 自身が呼び出す Rust binary)
+- 計測対象:
+ - `docs/todo*.md` の file size (50KB 閾値、PR #88 / #96 / #101 / #123 / #172 で実証された分割 trigger)
+ - `src/**/*.rs` の line count (800 行閾値、順位 147 file length lint と整合)
+- 出力: 閾値超過 / 接近 (90% 等) のファイル一覧を aggregate facet の入力として渡す
+- 機械検査のため LLM context を浪費しない (ADR-031 3 層分離原則)
+
+#### 作業計画
+
+- [ ] 順位 136 hook land 待ち
+- [ ] Phase B 2-3 週 dogfood 完了 + 観点 ⑤ ⑦ の必要性再評価 (順位 95 / 147 land 状況も確認)
+- [ ] `review-todo-whole.md` instruction 設計 (順位 136 hook が拾える範囲との境界明示)
+- [ ] aggregate 前 Rust pre-step 実装 (新 binary `cli-weekly-review-prep` or aggregate facet 内 step)
+- [ ] takt workflow weekly-review.yaml に facet + pre-step 追加
+- [ ] aggregate-weekly facet 拡張 (新 category) + pending JSON schema 拡張
+- [ ] dogfood + 本エントリ削除 + todo-summary.md 行削除
+
+#### 完了基準
+
+- 全 todo*.md entry の dead pattern / cross-file 重複 / preamble drift が週次検出される
+- file size 閾値超過 / 接近が aggregate facet input として通知される
+- 順位 136 hook と責務分離 (hook = 編集時 immediate / 週次 = batch 棚卸し) が機能
+
+#### 詰まっている箇所
+
+- 順位 136 hook 実装次第 (hook が拾える範囲が確定後に週次の補完範囲を確定)
+- Phase B dogfood 結果次第 (有用な finding が出るかは運用観察)
+- 順位 95 (preamble count CI 自動照合) との scope 重複整理: CI = 機械検査即時 / 週次 pre-step = aggregate 入力、両立可能だが integration 検討
+
+---
+
## 既知課題 (記録のみ、本セッションで未対応)
(現時点で本ファイルへの既知課題は無し。docs/todo8.md 末尾の post-merge-feedback workflow stale marker 問題を参照。)
diff --git a/push-runner-config.toml b/push-runner-config.toml
index cbcdac9..d5090aa 100644
--- a/push-runner-config.toml
+++ b/push-runner-config.toml
@@ -3,6 +3,29 @@
# pnpm push で起動される push-runner.exe がこのファイルを読み込む。
# カレントディレクトリ (リポジトリルート) を優先的に検索する。
+# ---------------------------------------------------------------------------
+# [scratch_file_warning] — 順位 1 (PR #85 T1-4): scratch ファイル混入の防御層。
+# `@` commit に `__*` 等の scratch ファイルが含まれていないか push 前に検査し、
+# 検出時は push を block する。jj auto-snapshot 環境で .gitignore 漏れがあると
+# scratch ファイルが PR に混入する事故 (PR #85 で実証) の構造的予防。
+#
+# ADR-039 (Experimental feature 標準パターン) 3 点セット:
+# - **Config opt-in**: 試験運用のため default OFF。本リポジトリでは明示的に
+# `enabled = true` で dogfood を開始。section 不在 / enabled 未設定では完全 skip。
+# - **Kill-switch**: `enabled = false` で完全停止。env `SCRATCH_FILE_WARNING_OVERRIDE=1`
+# で個別 push の意図的バイパスも可能。
+# - **Bounded lifetime**: 本リポジトリで 3-5 PR の dogfood 後 (false positive 観測 /
+# 検出効果 / override 使用頻度) に default-ON 昇格 or 却下を判定。判定結果は
+# `src/cli-push-runner/src/stages/scratch_file_warning.rs` module doc + 本 section
+# コメントに反映する。
+#
+# 順位 5 (AI 生成一時スクリプト pattern) で `_tmp_*` 等の追加 pattern を本 section
+# の `patterns` に追加して拡張する (補完アプローチ、別 PR Bundle 3 で実施)。
+# ---------------------------------------------------------------------------
+[scratch_file_warning]
+enabled = true
+patterns = ["__*"]
+
[quality_gate]
parallel = true
# step_timeout 履歴: 120s (Phase b 当初) → 180s (PR #132、Phase b' で 12 件 mistral invoke が 120s 境界) →
diff --git a/src/cli-push-runner/src/config.rs b/src/cli-push-runner/src/config.rs
index 597a389..57d822c 100644
--- a/src/cli-push-runner/src/config.rs
+++ b/src/cli-push-runner/src/config.rs
@@ -32,6 +32,24 @@ pub(crate) struct Config {
pub(crate) lint_screen: Option,
pub(crate) takt: TaktConfig,
pub(crate) push: PushConfig,
+ pub(crate) scratch_file_warning: Option,
+}
+
+/// 順位 1 (PR #85 T1-4) — scratch ファイル (`__*` 等) が `@` commit に
+/// 混入していないか push 前に検査する stage の config。
+///
+/// ADR-039 (Experimental feature 標準パターン) § 1 Config opt-in 準拠:
+/// `[scratch_file_warning]` section 不在 / `enabled` 未設定 / `enabled = false`
+/// のいずれも検査を **skip** (= default `enabled = false`)。明示的に `enabled = true`
+/// にしたときのみ検査実行 (3-5 PR の dogfood 後に default-ON 昇格 or 却下を判定)。
+///
+/// `patterns` は順位 5 (AI 生成一時スクリプト pattern の pre-push 検出) で
+/// `_tmp_*` 等の追加 pattern を config-driven で拡張可能 (= 補完アプローチ)。
+/// `patterns` 未設定時の default は `["__*"]` (= stage 側 `DEFAULT_PATTERN`)。
+#[derive(Deserialize)]
+pub(crate) struct ScratchFileWarningConfig {
+ pub(crate) enabled: Option,
+ pub(crate) patterns: Option>,
}
/// Phase c (§8.E lint screen facet) — pre-push 時に diff を mistral:7b に流して
@@ -543,6 +561,7 @@ command = "echo push"
},
diff: None,
lint_screen: None,
+ scratch_file_warning: None,
takt: TaktConfig {
workflow: "w".into(),
task: "t".into(),
@@ -615,6 +634,7 @@ command = "echo push"
},
diff: None,
lint_screen: None,
+ scratch_file_warning: None,
takt: TaktConfig {
workflow: "w".into(),
task: "t".into(),
@@ -697,6 +717,82 @@ command = "echo push"
);
}
+ #[test]
+ fn config_parses_with_scratch_file_warning_full() {
+ let toml_str = r#"
+[quality_gate]
+[[quality_gate.groups]]
+name = "test"
+commands = ["echo ok"]
+
+[scratch_file_warning]
+enabled = true
+patterns = ["__*", "_tmp_*"]
+
+[takt]
+workflow = "w"
+task = "t"
+
+[push]
+command = "echo push"
+"#;
+ let config: Config = toml::from_str(toml_str).unwrap();
+ let s = config
+ .scratch_file_warning
+ .expect("[scratch_file_warning] should parse to Some");
+ assert_eq!(s.enabled, Some(true));
+ assert_eq!(
+ s.patterns.unwrap(),
+ vec!["__*".to_string(), "_tmp_*".to_string()]
+ );
+ }
+
+ #[test]
+ fn config_parses_with_scratch_file_warning_only_enabled_false() {
+ let toml_str = r#"
+[quality_gate]
+[[quality_gate.groups]]
+name = "test"
+commands = ["echo ok"]
+
+[scratch_file_warning]
+enabled = false
+
+[takt]
+workflow = "w"
+task = "t"
+
+[push]
+command = "echo push"
+"#;
+ let config: Config = toml::from_str(toml_str).unwrap();
+ let s = config.scratch_file_warning.unwrap();
+ assert_eq!(s.enabled, Some(false));
+ assert!(s.patterns.is_none());
+ }
+
+ #[test]
+ fn config_scratch_file_warning_absent_yields_none() {
+ let toml_str = r#"
+[quality_gate]
+[[quality_gate.groups]]
+name = "test"
+commands = ["echo ok"]
+
+[takt]
+workflow = "w"
+task = "t"
+
+[push]
+command = "echo push"
+"#;
+ let config: Config = toml::from_str(toml_str).unwrap();
+ assert!(
+ config.scratch_file_warning.is_none(),
+ "absent [scratch_file_warning] should yield None (default-ON 動作は stage 側で解決)"
+ );
+ }
+
#[test]
fn validate_rejects_empty_commands() {
let config = Config {
@@ -711,6 +807,7 @@ command = "echo push"
},
diff: None,
lint_screen: None,
+ scratch_file_warning: None,
takt: TaktConfig {
workflow: "w".into(),
task: "t".into(),
diff --git a/src/cli-push-runner/src/main.rs b/src/cli-push-runner/src/main.rs
index 800583c..6c3b46a 100644
--- a/src/cli-push-runner/src/main.rs
+++ b/src/cli-push-runner/src/main.rs
@@ -1,6 +1,7 @@
//! Push Runner — takt ベースの pre-push パイプライン
//!
//! pnpm push から呼び出され、以下のステージを実行する:
+//! Stage 0: scratch_file_warning — `__*` 等の scratch ファイル混入を検査 (順位 1)
//! Stage 1: quality_gate — TOML で定義されたコマンド群をグループ間で並列実行
//! Stage 1.5: diff — jj diff を取得しファイルに書き出し(reviewers が Read で参照)
//! Stage 2: takt — AI レビュー(reviewers → fix loop)
@@ -15,6 +16,7 @@
//! 3 - push 失敗
//! 4 - 設定エラー
//! 5 - diff 取得失敗
+//! 6 - scratch_file_warning 検出 (override env で bypass 可能)
mod config;
mod log;
@@ -25,7 +27,10 @@ use std::time::Instant;
use config::load_config;
use log::log_info;
-use stages::{run_diff, run_lint_screen, run_push, run_quality_gate, run_takt, DiffResult};
+use stages::{
+ run_diff, run_lint_screen, run_push, run_quality_gate, run_scratch_file_warning, run_takt,
+ DiffResult,
+};
const EXIT_SUCCESS: i32 = 0;
const EXIT_QUALITY_GATE_FAILURE: i32 = 1;
@@ -33,6 +38,7 @@ const EXIT_TAKT_FAILURE: i32 = 2;
const EXIT_PUSH_FAILURE: i32 = 3;
const EXIT_CONFIG_ERROR: i32 = 4;
const EXIT_DIFF_FAILURE: i32 = 5;
+const EXIT_SCRATCH_FILE_WARNING: i32 = 6;
/// diff stage を実行し lint-screen を呼び出す。
/// Ok(skip_takt) で成功、 Err(exit_code) で pipeline 中断。
@@ -70,11 +76,19 @@ fn run_pipeline() -> i32 {
let has_diff = config.diff.is_some();
log_info(&format!(
- "パイプライン開始: quality_gate → {} takt ({}) → push",
+ "パイプライン開始: scratch → quality_gate → {} takt ({}) → push",
if has_diff { "diff →" } else { "" },
config.takt.workflow,
));
+ if !run_scratch_file_warning(config.scratch_file_warning.as_ref()) {
+ log_info(
+ "パイプライン中断: scratch ファイル検出。.gitignore 修正 / ファイル削除 / \
+ SCRATCH_FILE_WARNING_OVERRIDE=1 のいずれかで再実行してください。",
+ );
+ return EXIT_SCRATCH_FILE_WARNING;
+ }
+
if !run_quality_gate(&config.quality_gate) {
log_info("パイプライン中断: quality_gate 失敗。問題を修正して再実行してください。");
return EXIT_QUALITY_GATE_FAILURE;
diff --git a/src/cli-push-runner/src/stages/mod.rs b/src/cli-push-runner/src/stages/mod.rs
index a366853..9dace47 100644
--- a/src/cli-push-runner/src/stages/mod.rs
+++ b/src/cli-push-runner/src/stages/mod.rs
@@ -3,10 +3,12 @@ mod lint_screen;
mod push;
mod push_jj_bookmark;
mod quality_gate;
+mod scratch_file_warning;
mod takt;
pub(crate) use diff::{run_diff, DiffResult};
pub(crate) use lint_screen::run_lint_screen;
pub(crate) use push::run_push;
pub(crate) use quality_gate::run_quality_gate;
+pub(crate) use scratch_file_warning::run_scratch_file_warning;
pub(crate) use takt::run_takt;
diff --git a/src/cli-push-runner/src/stages/scratch_file_warning.rs b/src/cli-push-runner/src/stages/scratch_file_warning.rs
new file mode 100644
index 0000000..2ccf9a9
--- /dev/null
+++ b/src/cli-push-runner/src/stages/scratch_file_warning.rs
@@ -0,0 +1,514 @@
+//! Scratch file warning stage — 順位 1 (PR #85 T1-4)
+//!
+//! `@` commit に scratch-pattern ファイル (default pattern: `__*`) が含まれていないか
+//! 検査し、検出時は warning + block で push を停止する。jj は auto-snapshot で
+//! working tree を即 commit に取り込むため、`.gitignore` 漏れがあると scratch
+//! ファイルが PR に意図せず混入する (PR #85 で `__parse_transcripts.ps1` 実例)。
+//!
+//! ADR-039 (Experimental feature 標準パターン) 準拠の 3 点セット:
+//! - **Config opt-in**: 試験運用のため default `enabled = false`、`[scratch_file_warning]`
+//! section で明示的に `enabled = true` にしないと検査は走らない。section 不在 /
+//! enabled 未指定の場合も skip (= 完全 no-op)。
+//! - **Kill-switch**: `enabled = false` (TOML) または env override
+//! `SCRATCH_FILE_WARNING_OVERRIDE=1` で意図的バイパス可能。
+//! - **Bounded lifetime**: 3-5 PR の dogfood で false positive / 検出効果を観測後、
+//! default-ON 昇格 or 却下を判定 (詳細は push-runner-config.toml の
+//! `[scratch_file_warning]` section コメント参照)。
+//!
+//! Stage 配置: `run_pipeline` の最早期 (quality_gate より前)。検出時は quality_gate
+//! や takt review を無駄に走らせず即停止する。
+//!
+//! Config-driven pattern: `[scratch_file_warning]` section で `patterns` を拡張可能。
+//! 順位 5 (AI 生成一時スクリプト pattern の pre-push 検出) は本 stage の patterns
+//! 拡張 (例: `_tmp_*`) + ADR-007 連携で補完的に実装する。
+
+use std::process::Command;
+
+use crate::config::ScratchFileWarningConfig;
+use crate::log::{log_info, log_stage};
+
+const JJ_TIMEOUT_SECS: u64 = 30;
+const OVERRIDE_ENV_VAR: &str = "SCRATCH_FILE_WARNING_OVERRIDE";
+const DEFAULT_PATTERN: &str = "__*";
+
+/// `[scratch_file_warning]` config の有無に応じて検査を実行し、
+/// push を続行してよいか (= violation なし or override active) を返す。
+///
+/// ADR-039 § 1 Config opt-in 準拠: default `enabled = false` (試験運用)。
+/// section 不在 / `c.enabled = None` / `c.enabled = Some(false)` のいずれも skip。
+/// 明示的に `c.enabled = Some(true)` のときのみ検査を実行。
+///
+/// fail-open: jj 不調 (timeout / 起動失敗) 時は warning ログのみで true を返し、
+/// push 自体は止めない。
+pub(crate) fn run_scratch_file_warning(config: Option<&ScratchFileWarningConfig>) -> bool {
+ let enabled = config.and_then(|c| c.enabled).unwrap_or(false);
+ if !enabled {
+ return true;
+ }
+ let patterns = effective_patterns(config);
+ let files = match list_files_in_at() {
+ Ok(f) => f,
+ Err(e) => {
+ log_info(&format!(
+ "scratch_file_warning: jj file list 失敗、検査を skip して push を続行します: {}",
+ e
+ ));
+ return true;
+ }
+ };
+ let violations = find_violations(&files, &patterns);
+ if violations.is_empty() {
+ log_stage("scratch", "scratch ファイル検出なし");
+ return true;
+ }
+ log_stage(
+ "scratch",
+ &format!(
+ "scratch ファイル候補 ({} 件) が @ commit に含まれます:",
+ violations.len()
+ ),
+ );
+ for v in &violations {
+ log_info(&format!(" - {}", v));
+ }
+ let raw = std::env::var(OVERRIDE_ENV_VAR).ok();
+ if parse_override_env(raw.as_deref()) {
+ log_info(&format!(
+ " {}={} により続行します (意図的バイパス)",
+ OVERRIDE_ENV_VAR,
+ raw.as_deref().unwrap_or("")
+ ));
+ true
+ } else {
+ log_info(&format!(
+ " 対処:\n \
+ (a) `.gitignore` に該当 pattern を追加 + `jj abandon @ && jj new` で再記述\n \
+ (b) ファイル自体を削除\n \
+ (c) 意図的 commit なら env {}=1 を設定して再実行",
+ OVERRIDE_ENV_VAR
+ ));
+ false
+ }
+}
+
+fn effective_patterns(config: Option<&ScratchFileWarningConfig>) -> Vec {
+ config
+ .and_then(|c| c.patterns.as_ref())
+ .map(|patterns| {
+ patterns
+ .iter()
+ .map(|p| p.trim().to_string())
+ .filter(|p| !p.is_empty())
+ .collect::>()
+ })
+ .filter(|patterns| !patterns.is_empty())
+ .unwrap_or_else(|| vec![DEFAULT_PATTERN.to_string()])
+}
+
+fn list_files_in_at() -> Result, String> {
+ let output = run_jj_file_list_at()?;
+ Ok(parse_file_list_output(&output))
+}
+
+fn parse_file_list_output(raw: &str) -> Vec {
+ raw.lines()
+ .map(|line| line.trim().to_string())
+ .filter(|s| !s.is_empty())
+ .collect()
+}
+
+fn extract_basename(path: &str) -> &str {
+ match path.rfind(['/', '\\']) {
+ Some(idx) => &path[idx + 1..],
+ None => path,
+ }
+}
+
+/// 簡易 glob: `*` (任意長文字列、空マッチ含む) のみサポート。`?` 等は未対応。
+/// パターンに `*` が含まれない場合は完全一致。
+fn matches_glob(name: &str, pattern: &str) -> bool {
+ if !pattern.contains('*') {
+ return name == pattern;
+ }
+ let parts: Vec<&str> = pattern.split('*').collect();
+ match_pattern_parts(name, &parts)
+}
+
+fn match_pattern_parts(name: &str, parts: &[&str]) -> bool {
+ let Some(after_prefix) = consume_prefix(name, parts.first().copied().unwrap_or("")) else {
+ return false;
+ };
+ let middle_parts = pattern_middle_slice(parts);
+ let Some(after_middle) = consume_middle(after_prefix, middle_parts) else {
+ return false;
+ };
+ if parts.len() > 1 {
+ let suffix = parts.last().copied().unwrap_or("");
+ check_suffix(after_middle, suffix)
+ } else {
+ true
+ }
+}
+
+fn pattern_middle_slice<'a>(parts: &'a [&'a str]) -> &'a [&'a str] {
+ if parts.len() > 2 {
+ &parts[1..parts.len() - 1]
+ } else {
+ &[]
+ }
+}
+
+fn consume_prefix<'a>(name: &'a str, prefix: &str) -> Option<&'a str> {
+ if prefix.is_empty() {
+ Some(name)
+ } else if name.starts_with(prefix) {
+ Some(&name[prefix.len()..])
+ } else {
+ None
+ }
+}
+
+fn consume_middle<'a>(name: &'a str, middle_parts: &[&str]) -> Option<&'a str> {
+ let mut remaining = name;
+ for part in middle_parts {
+ if part.is_empty() {
+ continue;
+ }
+ let idx = remaining.find(part)?;
+ remaining = &remaining[idx + part.len()..];
+ }
+ Some(remaining)
+}
+
+fn check_suffix(name: &str, suffix: &str) -> bool {
+ suffix.is_empty() || name.ends_with(suffix)
+}
+
+fn find_violations(files: &[String], patterns: &[String]) -> Vec {
+ let mut violations = Vec::new();
+ for file in files {
+ let name = extract_basename(file);
+ for pattern in patterns {
+ if matches_glob(name, pattern) {
+ violations.push(file.clone());
+ break;
+ }
+ }
+ }
+ violations
+}
+
+fn parse_override_env(raw: Option<&str>) -> bool {
+ let Some(value) = raw else {
+ return false;
+ };
+ matches!(
+ value.trim().to_ascii_lowercase().as_str(),
+ "1" | "true" | "yes" | "on"
+ )
+}
+
+fn run_jj_file_list_at() -> Result {
+ use std::process::Stdio;
+
+ let mut child = Command::new("jj")
+ .args(["file", "list", "-r", "@"])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()
+ .map_err(|e| format!("jj file list 起動失敗: {}", e))?;
+
+ let stdout_handle =
+ crate::runner::drain_pipe(child.stdout.take().expect("stdout must be piped"));
+ let stderr_handle =
+ crate::runner::drain_pipe(child.stderr.take().expect("stderr must be piped"));
+
+ let status = crate::runner::wait_with_timeout("jj file list", &mut child, JJ_TIMEOUT_SECS)
+ .map_err(|e| format!("jj file list wait 失敗: {}", e))?;
+
+ let stdout = stdout_handle.join().unwrap_or_default();
+ let stderr = stderr_handle.join().unwrap_or_default();
+
+ match status {
+ None => Err(format!("jj file list タイムアウト ({}s)", JJ_TIMEOUT_SECS)),
+ Some(s) if s.success() => Ok(stdout),
+ Some(_) => Err(stderr.trim().to_string()),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parse_file_list_basic() {
+ let raw = "src/main.rs\nsrc/lib.rs\n";
+ assert_eq!(
+ parse_file_list_output(raw),
+ vec!["src/main.rs", "src/lib.rs"]
+ );
+ }
+
+ #[test]
+ fn parse_file_list_skips_empty_lines() {
+ let raw = "src/main.rs\n\n\nsrc/lib.rs\n";
+ assert_eq!(
+ parse_file_list_output(raw),
+ vec!["src/main.rs", "src/lib.rs"]
+ );
+ }
+
+ #[test]
+ fn parse_file_list_trims_whitespace() {
+ let raw = " src/main.rs \n\tsrc/lib.rs\t\n";
+ assert_eq!(
+ parse_file_list_output(raw),
+ vec!["src/main.rs", "src/lib.rs"]
+ );
+ }
+
+ #[test]
+ fn parse_file_list_empty_returns_empty() {
+ assert_eq!(parse_file_list_output(""), Vec::::new());
+ }
+
+ #[test]
+ fn extract_basename_forward_slash() {
+ assert_eq!(extract_basename("src/foo/bar.rs"), "bar.rs");
+ }
+
+ #[test]
+ fn extract_basename_backslash() {
+ assert_eq!(extract_basename(r"src\foo\bar.rs"), "bar.rs");
+ }
+
+ #[test]
+ fn extract_basename_no_separator() {
+ assert_eq!(extract_basename("foo.rs"), "foo.rs");
+ }
+
+ #[test]
+ fn extract_basename_mixed_separators() {
+ assert_eq!(extract_basename(r"src/foo\bar.rs"), "bar.rs");
+ assert_eq!(extract_basename(r"src\foo/bar.rs"), "bar.rs");
+ }
+
+ #[test]
+ fn extract_basename_trailing_separator_returns_empty() {
+ assert_eq!(extract_basename("src/foo/"), "");
+ }
+
+ #[test]
+ fn matches_glob_prefix_wildcard() {
+ assert!(matches_glob("__foo", "__*"));
+ assert!(matches_glob("__", "__*"));
+ assert!(!matches_glob("foo__", "__*"));
+ assert!(!matches_glob("_foo", "__*"));
+ }
+
+ #[test]
+ fn matches_glob_suffix_wildcard() {
+ assert!(matches_glob("foo.tmp", "*.tmp"));
+ assert!(matches_glob(".tmp", "*.tmp"));
+ assert!(!matches_glob("foo.tmpx", "*.tmp"));
+ }
+
+ #[test]
+ fn matches_glob_prefix_and_suffix_wildcards() {
+ assert!(matches_glob("_tmp_file.ps1", "_tmp_*"));
+ assert!(matches_glob("__file.py", "__*.py"));
+ assert!(!matches_glob("__file.ps1", "__*.py"));
+ }
+
+ #[test]
+ fn matches_glob_single_middle_wildcard() {
+ assert!(matches_glob("foobazbar", "foo*bar"));
+ assert!(matches_glob("foobar", "foo*bar"));
+ assert!(!matches_glob("fooXY", "foo*bar"));
+ }
+
+ #[test]
+ fn matches_glob_three_part_pattern() {
+ assert!(matches_glob("mytest_x.ps1", "*test*.ps1"));
+ assert!(matches_glob("test.ps1", "*test*.ps1"));
+ assert!(!matches_glob("foo.ps1", "*test*.ps1"));
+ }
+
+ #[test]
+ fn matches_glob_no_wildcard_exact() {
+ assert!(matches_glob("foo", "foo"));
+ assert!(!matches_glob("foo.bar", "foo"));
+ assert!(!matches_glob("foo", "bar"));
+ }
+
+ #[test]
+ fn matches_glob_only_wildcard_matches_anything() {
+ assert!(matches_glob("anything", "*"));
+ assert!(matches_glob("", "*"));
+ }
+
+ #[test]
+ fn matches_glob_empty_pattern_exact() {
+ assert!(matches_glob("", ""));
+ assert!(!matches_glob("foo", ""));
+ }
+
+ #[test]
+ fn find_violations_detects_default_pattern() {
+ let files = vec![
+ "src/main.rs".to_string(),
+ "__test.ps1".to_string(),
+ "docs/__draft.md".to_string(),
+ "src/__scratch.rs".to_string(),
+ ];
+ let patterns = vec!["__*".to_string()];
+ let violations = find_violations(&files, &patterns);
+ assert_eq!(
+ violations,
+ vec![
+ "__test.ps1".to_string(),
+ "docs/__draft.md".to_string(),
+ "src/__scratch.rs".to_string()
+ ]
+ );
+ }
+
+ #[test]
+ fn find_violations_empty_when_no_match() {
+ let files = vec!["src/main.rs".to_string(), "Cargo.toml".to_string()];
+ let patterns = vec!["__*".to_string()];
+ assert!(find_violations(&files, &patterns).is_empty());
+ }
+
+ #[test]
+ fn find_violations_multiple_patterns() {
+ let files = vec![
+ "__test.ps1".to_string(),
+ "_tmp_log.txt".to_string(),
+ "src/main.rs".to_string(),
+ ];
+ let patterns = vec!["__*".to_string(), "_tmp_*".to_string()];
+ let violations = find_violations(&files, &patterns);
+ assert_eq!(violations.len(), 2);
+ assert!(violations.contains(&"__test.ps1".to_string()));
+ assert!(violations.contains(&"_tmp_log.txt".to_string()));
+ }
+
+ #[test]
+ fn find_violations_reports_file_only_once_when_matching_multiple_patterns() {
+ let files = vec!["__test.tmp".to_string()];
+ let patterns = vec!["__*".to_string(), "*.tmp".to_string()];
+ let violations = find_violations(&files, &patterns);
+ assert_eq!(violations.len(), 1);
+ }
+
+ #[test]
+ fn find_violations_matches_basename_in_any_subdirectory() {
+ let files = vec![
+ "subdir/__hidden.txt".to_string(),
+ r"win\path\__hidden.txt".to_string(),
+ "__top.txt".to_string(),
+ ];
+ let patterns = vec!["__*".to_string()];
+ assert_eq!(find_violations(&files, &patterns).len(), 3);
+ }
+
+ #[test]
+ fn find_violations_ignores_dirname_prefix_match_when_basename_does_not_match() {
+ let files = vec!["__src/main.rs".to_string()];
+ let patterns = vec!["__*".to_string()];
+ assert!(find_violations(&files, &patterns).is_empty());
+ }
+
+ #[test]
+ fn parse_override_env_truthy() {
+ for v in [
+ "1", "true", "TRUE", "yes", "YES", "on", "On", " true ", "\tyes\n",
+ ] {
+ assert!(parse_override_env(Some(v)), "'{}' should be truthy", v);
+ }
+ }
+
+ #[test]
+ fn parse_override_env_falsy() {
+ for v in ["0", "false", "no", "off", "", " ", "maybe", "enable"] {
+ assert!(!parse_override_env(Some(v)), "'{}' should be falsy", v);
+ }
+ }
+
+ #[test]
+ fn parse_override_env_none_is_false() {
+ assert!(!parse_override_env(None));
+ }
+
+ #[test]
+ fn effective_patterns_default_when_none() {
+ let p = effective_patterns(None);
+ assert_eq!(p, vec!["__*".to_string()]);
+ }
+
+ #[test]
+ fn effective_patterns_default_when_no_patterns_field() {
+ let config = ScratchFileWarningConfig {
+ enabled: Some(true),
+ patterns: None,
+ };
+ assert_eq!(effective_patterns(Some(&config)), vec!["__*".to_string()]);
+ }
+
+ #[test]
+ fn effective_patterns_default_when_empty_list() {
+ let config = ScratchFileWarningConfig {
+ enabled: Some(true),
+ patterns: Some(vec![]),
+ };
+ assert_eq!(effective_patterns(Some(&config)), vec!["__*".to_string()]);
+ }
+
+ #[test]
+ fn effective_patterns_uses_config_when_provided() {
+ let config = ScratchFileWarningConfig {
+ enabled: Some(true),
+ patterns: Some(vec!["__*".to_string(), "_tmp_*".to_string()]),
+ };
+ assert_eq!(
+ effective_patterns(Some(&config)),
+ vec!["__*".to_string(), "_tmp_*".to_string()]
+ );
+ }
+
+ #[test]
+ fn effective_patterns_default_when_only_blank_entries() {
+ let config = ScratchFileWarningConfig {
+ enabled: Some(true),
+ patterns: Some(vec!["".to_string(), " ".to_string()]),
+ };
+ assert_eq!(effective_patterns(Some(&config)), vec!["__*".to_string()]);
+ }
+
+ #[test]
+ fn effective_patterns_filters_blank_entries_and_keeps_valid_ones() {
+ let config = ScratchFileWarningConfig {
+ enabled: Some(true),
+ patterns: Some(vec![
+ "".to_string(),
+ "__*".to_string(),
+ " ".to_string(),
+ "_tmp_*".to_string(),
+ ]),
+ };
+ assert_eq!(
+ effective_patterns(Some(&config)),
+ vec!["__*".to_string(), "_tmp_*".to_string()]
+ );
+ }
+
+ #[test]
+ fn effective_patterns_trims_whitespace_in_pattern_values() {
+ let config = ScratchFileWarningConfig {
+ enabled: Some(true),
+ patterns: Some(vec![" __* ".to_string()]),
+ };
+ assert_eq!(effective_patterns(Some(&config)), vec!["__*".to_string()]);
+ }
+}