diff --git a/.claude/custom-lint-rules.toml b/.claude/custom-lint-rules.toml index ecf1086..6b312ac 100644 --- a/.claude/custom-lint-rules.toml +++ b/.claude/custom-lint-rules.toml @@ -291,3 +291,49 @@ steps = [ # 説明文中で実際の trap pattern を直接書かないため、placeholder 形式で示す bad = "[ADR-036](DOTDOT/docs/adr/adr-036-...) " good = "[ADR-036](adr/adr-036-...)" + +# ─── ルール⑨: takt workflow yaml で persona: を持つ step に model: 必須 ─── +# +# 由来: Bundle Y2 (PR #98) で post-pr-review.yaml supervise step に model: が +# 未指定であることが post-merge-feedback で指摘された。persona: を持つ step で +# model: 未指定は default に落ちるため、Bundle Y2 ゴール (analyze 系 haiku / +# supervise・fix は sonnet) では現時点で偶然合致しているが、将来 default 変更や +# persona 追加で意図せぬモデル選択が混入しうる。custom-lint-rule で書いた瞬間に +# block すれば bug class が排除される。 +# +# Bundle Z #B-α と同じ「決定論的防止層」哲学 (ADR-007 の正規表現層)。 +# +# 検出ロジック (multi-line regex): +# persona: 行の直後の indented field 行が「model: 以外」の場合に flag する。 +# field 名 enumeration 方式で takt yaml の sibling field を列挙し、新規 field が +# 増えた際は明示的に rule 拡張する形を取る (= 学習機会化)。Rust regex は lookahead +# 非対応のため negation by enumeration が pragmatic。 +# +# Path filter (.takt/workflows/*.yaml 限定): +# 順位 102 (PR #148) で実装した paths filter で範囲を絞り、.github/workflows/ +# 等の他 yaml への誤発火を防ぐ。 + +[[rules]] +id = "takt-workflow-persona-without-model" +pattern = '(?m)^[ \t]+persona:[ \t]+[\w-]+[ \t]*\r?\n[ \t]+(?:policy|instruction|edit|provider_options|knowledge|condition|rules|inputs|outputs|allowed_tools|disallowed_tools|name|type|cmd|when|description|tool|tools|output_contracts|pass_previous_response|required_permission_mode|parallel):' +severity = "error" +message = "takt workflow で persona: の直後に model: が指定されていません。default 落ち防止のため model: を明示してください" +why = "PR #98 post-merge-feedback で post-pr-review.yaml supervise step の model: 未指定が指摘された。default 落ちは将来 default 変更や persona 追加で意図せぬモデル選択を生む。Bundle Z #B-α と同じ決定論的防止層 (ADR-007 の正規表現層)" +extensions = ["yaml"] +paths = [".takt/workflows/*.yaml"] + +[rules.fix] +strategy = "persona: の直後に model: を追加" +steps = [ + "supervisor 系 step: model: sonnet を追加 (deep reasoning が必要)", + "code-reviewer 系 analyze step: model: haiku を追加 (低 cost で十分)", + "coder 系 fix step: model: sonnet を追加 (Bundle Y2 既定)", + "judge / loop_monitor block の persona: にも明示推奨 (clean baseline 維持)", +] + +[rules.example] +bad = ''' persona: supervisor + instruction: loop-monitor-reviewers-fix''' +good = ''' persona: supervisor + model: sonnet + instruction: loop-monitor-reviewers-fix''' diff --git a/.takt/workflows/post-pr-review.yaml b/.takt/workflows/post-pr-review.yaml index 2582c29..e44f2c1 100644 --- a/.takt/workflows/post-pr-review.yaml +++ b/.takt/workflows/post-pr-review.yaml @@ -21,6 +21,7 @@ loop_monitors: threshold: 2 judge: persona: supervisor + model: sonnet instruction: loop-monitor-reviewers-fix rules: - condition: Healthy (progress being made) @@ -115,6 +116,7 @@ steps: - name: supervise edit: false persona: supervisor + model: sonnet policy: review provider_options: claude: diff --git a/.takt/workflows/pre-push-review.yaml b/.takt/workflows/pre-push-review.yaml index 8eb7bbd..8812767 100644 --- a/.takt/workflows/pre-push-review.yaml +++ b/.takt/workflows/pre-push-review.yaml @@ -24,6 +24,7 @@ loop_monitors: threshold: 2 judge: persona: supervisor + model: sonnet instruction: loop-monitor-reviewers-fix rules: - condition: Healthy (progress being made) diff --git a/docs/local-llm-offload-analysis.md b/docs/local-llm-offload-analysis.md index 2db13f4..c4cab79 100644 --- a/docs/local-llm-offload-analysis.md +++ b/docs/local-llm-offload-analysis.md @@ -2,7 +2,7 @@ > **位置づけ**: 本ファイルは「残作業の **次に何をするか** だけ」を持つ実行計画。完了済みの分析・実装・dogfood 計測・retrospective は [local-llm-offload-history.md](local-llm-offload-history.md) に切り出した。 > -> **状態**: 試験運用 (Phase a 完了 = PR #130 land / Phase b 完了 = conditional GO 2026-05-08, PR #131 / Phase c MVP 完了 = PR #132 land 2026-05-08 / **Phase c+ Bundle i 完了 = PR #135 land 2026-05-09 / §8.D 完了 = PR #136 land 2026-05-09 (num_ctx 8192、agreement 86.7% / verdict GO) / Phase d kickoff prep 完了 = 2026-05-10 ([docs/local-llm-offload-phase-d-guide.md](local-llm-offload-phase-d-guide.md) 参照)**、実 dogfood (3-5 PR、long-running) 待機)。 +> **状態**: 試験運用 (Phase a 完了 = PR #130 land / Phase b 完了 = conditional GO 2026-05-08, PR #131 / Phase c MVP 完了 = PR #132 land 2026-05-08 / **Phase c+ Bundle i 完了 = PR #135 land 2026-05-09 / §8.D 完了 = PR #136 land 2026-05-09 (num_ctx 8192、agreement 86.7% / verdict GO) / Phase d kickoff prep 完了 = 2026-05-10 / Phase d Round 1 完遂 = 2026-05-12 (D-1〜D-3、実 dogfood は D-3 単独 1 data point) / Phase d Round 2 着手 = 2026-05-13 (D-4〜D-7、累積 5 PR で Phase E gate 充足見込み)** ([docs/local-llm-offload-phase-d-guide.md](local-llm-offload-phase-d-guide.md) 参照))。 > > **引退条件**: 以下のいずれかで本ファイルを削除する (docs-governance.md retirement workflow 準拠)。`local-llm-offload-history.md` も同タイミングで判断する。 > - 残作業 (§8.D / §8.E / §8.F, §1 Phase b/c/d) が **すべて land または却下** された場合 → permanent value (採用された設計判断、却下理由) を ADR-038 に migrate して両ファイルを削除 @@ -229,11 +229,11 @@ Phase A 実装後、PR #141 (P-3 = 187 行 mixed diff) を replay → **`prompt_ `src/cli-push-runner/src/stages/lint_screen.rs` 改修: graceful fallback (exit 0) 時にも classifier stderr を `.takt/lint-screen-report.md` の `## Diagnostic` section に取込。Phase A 診断 warn log が **real pipeline 経由で visible** になる状態を確保。新 struct `ClassifierOutput { stdout, stderr }`、新 helper `render_diagnostic`、新規 smoke test 4 件 (TP / FP / edge case / parse-error path) で contract を seal。lint_screen tests 14/14 pass + workspace 全 cargo test pass。 -##### ✅ Phase D: Clean dogfood validation (real pipeline 経由、計画完遂 2026-05-12) +##### 🔄 Phase D: Clean dogfood validation (real pipeline 経由、Round 1 完遂 2026-05-12 / Round 2 D-4〜D-7 着手中 2026-05-13) Phase C fix + Phase D 前提整備 (順位 109) 完了で **real pipeline 経由 dogfood の必要十分条件が揃った**。D-1 着手時に session-only opt-in workflow が jj auto-snapshot と本質的に衝突する gap が判明したが、**順位 115 (`LINT_SCREEN_ENABLED` env var override) land で解消**。env var 経路 (`$env:LINT_SCREEN_ENABLED = "true"`) で `push-runner-config.toml` を編集せずに lint_screen を有効化できるため、D-3 で初の実 dogfood が成立し、計画 3 PR + prereq 1 PR がすべて land 完了。 -**Phase D 対象 PR 構成 (2026-05-12 完遂)**: +**Phase D Round 1 対象 PR 構成 (D-1〜D-3、2026-05-12 完遂)**: | Order | 構成 (todo-summary.md priority list より) | Effort | 実 diff 行 | Diff Profile | 状態 | |---|---|---|---|---|---| @@ -273,15 +273,41 @@ Phase C fix + Phase D 前提整備 (順位 109) 完了で **real pipeline 経由 4. **1 false positive は Phase b' agreement 75% (= 25% disagreement) と整合**: 想定範囲内、複数 PR 累積評価が前提 5. **副産物 (D-3 post-merge-feedback)**: `MAX_CUSTOM_VIOLATIONS` outer/inner loop break scope の explicit test 必要性を発見 (Tier 2-1 採用、順位 119)、rule⑧ への paths filter 適用範囲検討を順位 118 として backlog 化 -**Phase D 計画完遂後の Phase E 判定材料**: +**Phase D Round 2 対象 PR 構成 (D-4〜D-7、2026-05-13 追加)**: + +Round 1 で実 dogfood data point が **1 件のみ** (D-3) に留まり、ADR-038 採用条件「5 PR 以上」+ analysis.md「3-5 PR 累積」前提との乖離が判明。D-1 反省 (docs-only は lint_screen findings 0 件で metrics 価値低) + workflow gap 解消済 (順位 115 = `LINT_SCREEN_ENABLED` env var override land、PR #147) を踏まえ、**残 4 PR で Rust code 中心 + size ramp-up + 累積 5 PR (D-3 + D-4〜D-7) 達成** を狙う延長計画を策定。 + +| Order | 構成 (todo-summary.md priority list より) | Tier / Effort | 推定 diff 行 | Diff Profile | 状態 | +|---|---|---|---|---|---| +| **D-4** | 順位 39 単独 = takt workflow `model` 必須化 lint rule + 副次作業 (`.takt/workflows/*.yaml` の `persona:` 行 3 件に `model:` 明示追加で clean baseline 確保) | T1 / S | ~150-280 | Rust lint rule (yaml multi-line regex) + 3-5 unit tests + custom-lint-rules.toml entry + 3 yaml site touch | 未着手 | +| **D-5** | 順位 56 + 119 bundle = comment-lint hook test 拡充 + `MAX_CUSTOM_VIOLATIONS` outer/inner loop break scope explicit test | T2+T2 / S+S | ~200-350 | hooks-post-tool-linter test infra (UTF-8 multi-byte 5 パターン + block boundary 6 パターン + MAX cap regression test) | 未着手 | +| **D-6** | 順位 51 単独 = `.takt/review-diff.txt` を fix→review iteration 間で refresh | T1 / M | ~400-580 | cli-push-runner Rust impl + iteration logic refactor + integration tests | 未着手 | +| **D-7** | Bundle c-1 (順位 63 + 64 + 67) = cli-merge-pipeline Drop guard / signal handler + orphan run reaper + ADR-030 spec amendment | T1×2 + T3 / M+M+XS | ~600-800 | Rust impl + signal trap + reaper logic + ADR markdown | 未着手 | + +**size ramp-up 設計 (Round 2)**: small → small-mid → mid → mid-large で num_ctx 32768 容量限界に向け漸増、各 size 帯で fallback 発生率 / Phase A diagnostic warn log 出力有無を観測。D-3 (mid, 496 行) と組合せて 5 size 帯をカバー。 + +**D-1 反省の適用 (Round 2)**: + +- ❌ **docs-only PR を回避**: D-4〜D-7 すべて Rust impl/test 中心 (D-7 で ADR markdown 1 つを bundle 内で同梱するのみ、主成分は Rust) +- ✅ **workflow gap 解消済**: 順位 115 (`LINT_SCREEN_ENABLED` env var) で session-only opt-in が成立、`push-runner-config.toml` 編集不要 +- ✅ **dogfood 実施可否を事前確認**: 各 PR 着手時に (a) env var set 確認 / (b) Ollama 起動確認 / (c) PR が code change を含むこと を check + +**想定リスク (Round 2)**: + +- **D-7 (Bundle c-1) size 上限超過リスク**: M+M+XS が 800 行を超えた場合、c-1a (順位 63 単独) / c-1b (順位 64+67) の 2 PR 分割に switch。その場合は D-7 → D-7a/D-7b で 5 PR に拡張 (Phase E 判定材料が 1 件増える方向で副次的に valid) +- **D-4 size の下振れリスク**: 順位 39 単独 + 3 yaml site touch + tests は ~150-280 行を見込むが、focused single-purpose PR として 250 lower bound 未達も許容 +- **detail 見積もりの精度**: 各 todo`N`.md の詳細 (実装方針 / acceptance criteria) を未参照、着手時に scope 修正の必要あり +- **D-4 の re-pivot 経緯 (2026-05-13)**: 当初 D-4 = 順位 47 (`>` vs `>=` boundary lint) を予定していたが、着手直前 (memory rule `feedback_verify_task_not_already_done.md` 適用) で **PR #126 (commit `b677b9d4f54d`) で既に land 済 (`no-time-field-strict-greater` rule、custom-lint-rules.toml line 208-243)** を発見。D-5 から 順位 39 を D-4 に繰上げ、D-5 を 順位 56 + 119 bundle に再構成。stale todo7.md 順位 47 entry は同 PR の docs commit で削除 + +**Phase D Round 1 完遂 + Round 2 計画策定後の Phase E 判定材料**: - ✅ pipeline integration works end-to-end (D-1 #144 smoke test + D-3 real diff 完走) - ✅ num_ctx 32768 で 270 行 Rust diff overflow なし (Phase C reference values と整合) -- ✅ fallback rate < 50% (D-3 で 0/1) +- ✅ fallback rate < 50% (D-3 で 0/1、単発) - ⚠️ agreement: 1 false positive 観測 (Phase b' 75% 想定範囲内、単発観測) -- ⏳ **複数 PR 累積必要** (Phase E 採否判定は次回以降の通常 PR で dogfood data を蓄積) +- 🔄 **累積 PR data 充足中**: Round 1 で 1 PR (D-3) 取得済、Round 2 (D-4〜D-7) で 4 PR 追加観測予定 → **累積 5 PR で ADR-038 採用条件「5 PR 以上」+ analysis.md「3-5 PR 累積」前提を充足見込み** -Phase E 着手の前提条件は **3-5 PR 累積 dogfood**。D-3 で 1 PR データを取得済 = あと 2-4 PR 観測すれば判定可能。当面は通常 PR の push 時に `$env:LINT_SCREEN_ENABLED=true` を opt-in で set し、`.takt/lint-screen-report.md` を post-push で記録する運用に移行する (本 § Phase D row への追記は dogfood data 蓄積継続中の暫定方針、判定時に Phase E section へ統合)。 +Phase E 着手の前提条件は **3-5 PR 累積 dogfood**。D-3 (1 PR 取得済) + Round 2 (D-4〜D-7、4 PR 追加観測予定) で計画上 **累積 5 PR** に到達する見込み。各 PR push 時に `$env:LINT_SCREEN_ENABLED=true` を opt-in で set し、`.takt/lint-screen-report.md` を post-push で記録する運用を継続。 **Phase D 計測手順** (各 PR 共通): @@ -309,8 +335,9 @@ Phase E 着手の前提条件は **3-5 PR 累積 dogfood**。D-3 で 1 PR デー **別案 (棚上げ)**: D-1 を順位 110+111+104 (testing.md + docs-governance routing rule + ADR-007 amendment、mixed) に変更する案もあったが、ADR codify 優先で 112+113+114 を採用。 -##### 🎯 Phase E: 採否判定 + retirement (1 PR、analysis.md 削除を含む、未着手) +##### 🎯 Phase E: 採否判定 + retirement (1 PR、analysis.md 削除を含む、**Phase D Round 2 完遂後着手**) +- **着手前提**: Phase D Round 2 (D-4〜D-7) 完遂 + 累積 5 PR 分の dogfood data 揃い + 各 PR の metrics (latency p50/p95 / fallback rate / classification 妥当性) 集計済 - **採用 case**: ADR-038 を「採用」に昇格 + [docs/local-llm-offload-phase-d-guide.md](local-llm-offload-phase-d-guide.md) を削除 (試験運用ガイド役目終了) + 本 analysis.md を削除 + history.md は permanent record として保持判断 - **却下 case**: cli-finding-classifier crate revert + ADR-038 を「却下」に更新 + Phase d guide 削除 + 本 analysis.md 削除 - **継続 case**: Phase D で別問題判明等 (例: real pipeline で classifier preview と異なる挙動) なら判定延期 + 本 §「次に何をするか」を再 pivot @@ -320,7 +347,6 @@ Phase E 着手の前提条件は **3-5 PR 累積 dogfood**。D-3 で 1 PR デー | Task | Effort | 関連 | |---|---|---| | 順位 100-108 docs PR (8 entries の todo registration、bundle 1 PR で消化) | S | Phase A〜C と並行 land 可、commit chain 整理 | -| Bundle c-1 (順位 63+64+67、c-1a/c-1b 分割推奨) | L (M+M+XS、split 推奨) | Phase d とは独立、通常 Tier 1 として後で対応 | | Bundle j-2 (順位 95+96、`.github/workflows/lint.yml` 新設) | M (S+M) | 独立 | | Bundle f-1/f-2 (PR #120 feedback) | S+M | 独立 | | 順位 110-114 (PR #142/#143 post-merge-feedback 採用分) | XS-S 各 | Phase D の対象 PR 候補としても活用可能 | diff --git a/docs/todo-summary.md b/docs/todo-summary.md index 4480c2f..00874aa 100644 --- a/docs/todo-summary.md +++ b/docs/todo-summary.md @@ -40,7 +40,6 @@ | 44 | 💎 Tier 3 | **gh CLI 使用規則を `~/.claude/rules/common/git-workflow.md` に追記 (計画書 #D-1) ★ Bundle a Sub-PR 1** | todo4.md | XS | なし (Sub-PR 1、Sub-PR 2 でも `gh api` を使うため先行 land 推奨) | | 45 | 🔧 Tier 2 | **`check-ci-coderabbit --list-findings` Rust モード追加 (計画書 #D-3) ★ Bundle a Sub-PR 1** | todo4.md | M | なし (Sub-PR 1、cli-pr-monitor が消費する構造化 findings API を提供) | | 46 | 🔧 Tier 2 | **CodeRabbit rate-limit auto-retry の integration test (PR #100 T2-1) ★ Bundle a Sub-PR 2** | todo4.md | M | 順位 42 と同 PR (Sub-PR 2、rate-limit auto-retry 実装と一体) | -| 47 | 🚀 Tier 1 | **`>` vs `>=` boundary inconsistency lint rule (PR #101 T1-2)** | todo7.md | S | なし (PR #101 直接対策、同一ファイル内 3 関数で latent drift が実証済の高頻度問題) | | 49 | 🔧 Tier 2 | **`parse_findings` 系の error-path test infrastructure (PR #101 T2-1) ★ Bundle a Sub-PR 2** | todo7.md | M | 順位 42 / 43 / 46 と同 PR (Sub-PR 2、`unwrap_or_else(\|_\| empty)` silent fail 抑止 + cli-pr-monitor mock infra 流用) | | 51 | 🚀 Tier 1 | **`.takt/review-diff.txt` を fix→review iteration 間で refresh (PR #103 観測)** | todo7.md | M | なし (PR #103 で stale-diff false positive による wasted iter ×2 = ~10 分浪費を実観測、6-iter outlier の構造的根因対策、Bundle Z 3 層では塞げない独立改善) | | 52 | 💎 Tier 3 | **comment-lint hook の MultiEdit 対応 (順位 50 follow-up)** | todo7.md | S | なし (順位 50 で v1 = Edit のみ実装、MultiEdit は whole-file fallback で no-regression、利用頻度低く優先度は低) | diff --git a/docs/todo7.md b/docs/todo7.md index f08ee5e..ea23a5a 100644 --- a/docs/todo7.md +++ b/docs/todo7.md @@ -10,46 +10,6 @@ ## 現在進行中 -### `>` vs `>=` boundary inconsistency lint rule (PR #101 T1-2) - -> **動機**: PR #101 で `parse_listed_findings` の `created_at > push_time` が CodeRabbit から境界 inclusive (`>=`) への揃え修正を指摘された。auto-fix が同一ファイル内 `parse_new_comments` / `parse_findings` にも `>=` を適用 (= 3 関数 latent drift)。`parse_rate_limit` だけが既に `>=` で、後続関数を書くたびに著者が意識せず `>` を選ぶ構造的問題。custom-lint-rule で書いた瞬間に block すれば bug class が排除される。 -> -> **本タスクの位置づけ**: PR #101 post-merge-feedback Tier 1 #2 採用 (高頻度 finding)。Bundle Z #B-α (Rust comment lint hook) と同じ「決定論的防止層」哲学。AST 解析ではなく正規表現層 (ADR-007) で対応可能。 -> -> **参照**: `.claude/feedback-reports/101.md` Tier 1 #2、ADR-007 (custom lint rule の正規表現 / AST 層線引き)、CodeRabbit PR #101 round 1 Minor finding -> -> **実行優先度**: 🚀 **Tier 1** — Effort S。`.claude/custom-lint-rules.toml` への regex rule 追加。 - -#### 設計決定 (案) - -- **配置先**: `.claude/custom-lint-rules.toml` に新規 rule entry -- **検出パターン (正規表現案)**: - - 狭め: `\.(created_at|submitted_at|updated_at)\b.*\.map\(\|\w+\|\s*\w+\s*[><](?!=)\s*(push_time|since)` - - 広め: `\b(created_at|submitted_at|updated_at|comment_event_time|event_time)\b.*[><](?!=)` で時刻フィールドの strict inequality 全般を flag -- **適用対象**: `.rs` ファイル -- **rule 名 (案)**: `time-boundary-strict-inequality` -- **suppress マーカー**: `// SAFETY: <理由>` 行末付与で suppression (例: 意図的に exclusive 比較する場合) - -#### 作業計画 - -- [ ] 既存 `.claude/custom-lint-rules.toml` の rule 構造を確認 -- [ ] regex + path filter を新 rule として記述 -- [ ] PostToolUse hook の lint runner で synthetic test (修正前 `parse_findings` 系の `>` パターンを再現してマッチ確認) -- [ ] 既存 codebase で false positive 影響範囲をグレップして確認 -- [ ] 派生プロジェクト (techbook-ledger / auto-review-fix-vc) への deploy 確認 -- [ ] 本 todo7.md エントリを削除 - -#### 完了基準 - -- `.claude/custom-lint-rules.toml` に新 rule が追加され `.rs` ファイル内の時刻フィールド strict inequality を検出 -- 1〜2 PR で dogfood し false positive がないこと - -#### 詰まっている箇所 - -- false positive の評価 (時刻フィールド以外で legitimate な `>` が誤 block されないか)。着手時に実 codebase でグレップして影響範囲を確認。 - ---- - ### `parse_findings` 系の error-path test infrastructure (PR #101 T2-1) ★ Bundle a Sub-PR 2 > **動機**: PR #101 で `run_list_findings` が `unwrap_or_else(|_| "[]")` で gh api 失敗を `[]` に潰していて CR Major finding を受けた。99.md でも `silent fail` (Windows path mismatch で early return) として類似言及あり。**`unwrap_or_else(|_| empty)` の anti-pattern が複数 PR で再発**。test 層で機械検証することで未然に塞ぐ。本タスクは Bundle a Sub-PR 2 (cli-pr-monitor の rate-limit auto-retry) で同 API を消費するので、同一 PR land で test 二重投資なし。 diff --git a/src/hooks-post-tool-linter/src/main.rs b/src/hooks-post-tool-linter/src/main.rs index 9a3aa99..99731e0 100644 --- a/src/hooks-post-tool-linter/src/main.rs +++ b/src/hooks-post-tool-linter/src/main.rs @@ -2032,4 +2032,140 @@ extensions = ["ts", "js"] missing ); } + + fn takt_workflow_persona_without_model_rule() -> CustomRule { + make_test_rule( + "takt-workflow-persona-without-model", + r"(?m)^[ \t]+persona:[ \t]+[\w-]+[ \t]*\r?\n[ \t]+(?:policy|instruction|edit|provider_options|knowledge|condition|rules|inputs|outputs|allowed_tools|disallowed_tools|name|type|cmd|when|description|tool|tools|output_contracts|pass_previous_response|required_permission_mode|parallel):", + &["yaml"], + ) + } + + /// judge / loop_monitor block で persona: → instruction: が違反として検出される。 + /// PR #98 post-merge-feedback で post-pr-review.yaml loop_monitor の persona: 後続 + /// に model: が不在で指摘された pattern を再現。 + #[test] + fn takt_workflow_persona_detects_judge_block_violation() { + let dir = tempfile::tempdir().unwrap(); + let fixture = "loop_monitors:\n - cycle:\n - analyze\n - fix\n judge:\n persona: supervisor\n instruction: loop-monitor-reviewers-fix\n"; + let file = write_file(dir.path(), "post-pr-review.yaml", fixture); + let rules = compile_test_rules(vec![takt_workflow_persona_without_model_rule()]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + assert_eq!( + violations.len(), + 1, + "judge block persona: + instruction: は violation として 1 件検出されるべき" + ); + } + + /// steps の supervise step で persona: → policy: が違反として検出される。 + /// PR #98 で実際に指摘された post-pr-review.yaml supervise step の構造を再現。 + #[test] + fn takt_workflow_persona_detects_supervise_step_violation() { + let dir = tempfile::tempdir().unwrap(); + let fixture = "steps:\n - name: supervise\n edit: false\n persona: supervisor\n policy: review\n"; + let file = write_file(dir.path(), "post-pr-review.yaml", fixture); + let rules = compile_test_rules(vec![takt_workflow_persona_without_model_rule()]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + assert_eq!( + violations.len(), + 1, + "supervise step の persona: + policy: は violation として 1 件検出されるべき" + ); + } + + /// persona: の直後に model: がある場合は clean (violation 0 件)。 + /// PR #98 fix 後の post-pr-review.yaml supervise step の構造を再現。 + #[test] + fn takt_workflow_persona_skips_when_model_directly_follows() { + let dir = tempfile::tempdir().unwrap(); + let fixture = "steps:\n - name: supervise\n edit: false\n persona: supervisor\n model: sonnet\n policy: review\n"; + let file = write_file(dir.path(), "post-pr-review.yaml", fixture); + let rules = compile_test_rules(vec![takt_workflow_persona_without_model_rule()]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + assert!( + violations.is_empty(), + "persona: → model: 構造は clean、violation 0 件であるべき。実際: {:?}", + violations + ); + } + + /// 複数 violation が同 file 内にある場合、すべて検出される (judge block + supervise step)。 + #[test] + fn takt_workflow_persona_detects_multiple_violations_in_same_file() { + let dir = tempfile::tempdir().unwrap(); + let fixture = "loop_monitors:\n - cycle:\n - analyze\n judge:\n persona: supervisor\n instruction: monitor\nsteps:\n - name: supervise\n persona: supervisor\n policy: review\n"; + let file = write_file(dir.path(), "post-pr-review.yaml", fixture); + let rules = compile_test_rules(vec![takt_workflow_persona_without_model_rule()]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + assert_eq!( + violations.len(), + 2, + "判定ブロック + supervise step の両方が violation として検出されるべき" + ); + } + + /// PR #150 CR Major 採用: persona: 直後に `output_contracts` / `pass_previous_response` / + /// `required_permission_mode` / `parallel` が来た場合も violation として検出される。 + /// 当初列挙から漏れていた 4 fields の regression test。 + #[test] + fn takt_workflow_persona_detects_required_permission_mode_violation() { + let dir = tempfile::tempdir().unwrap(); + let fixture = "steps:\n - name: fix\n persona: coder\n required_permission_mode: edit\n pass_previous_response: false\n"; + let file = write_file(dir.path(), "pre-push-review.yaml", fixture); + let rules = compile_test_rules(vec![takt_workflow_persona_without_model_rule()]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + assert_eq!( + violations.len(), + 1, + "persona: + required_permission_mode: は violation として 1 件検出されるべき (PR #150 CR Major fix)" + ); + } + + /// extensions filter で yaml 以外 (md など) はスキップされる。 + /// rule の `extensions = ["yaml"]` 制約を検証 (paths filter は別途 PR #148 D-3 で検証済)。 + #[test] + fn takt_workflow_persona_skips_non_yaml_extension() { + let dir = tempfile::tempdir().unwrap(); + let fixture = "persona: supervisor\ninstruction: loop\n"; + let file = write_file(dir.path(), "fake.md", fixture); + let rules = compile_test_rules(vec![takt_workflow_persona_without_model_rule()]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + assert!( + violations.is_empty(), + "yaml 以外の extension では rule は fire しないべき" + ); + } + + /// 配布済 `.takt/workflows/*.yaml` が clean baseline を維持していることを assert。 + /// PR #126 の `deployed_custom_rules_pass_powershell_case_insensitive_validation` と + /// 同パターン: rule 追加と clean baseline 確保を同 commit で land した後、後続編集での + /// regression を test 層で防ぐ。 + #[test] + fn deployed_takt_workflows_have_clean_baseline_for_persona_model_rule() { + let workflows_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join(".takt") + .join("workflows"); + let rules = compile_test_rules(vec![takt_workflow_persona_without_model_rule()]); + let mut total_violations: Vec = Vec::new(); + for entry in std::fs::read_dir(&workflows_dir) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", workflows_dir.display())) + { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) == Some("yaml") { + let violations = run_custom_rules(path.to_str().unwrap(), &rules); + for v in violations { + total_violations.push(format!("{}: {}", path.display(), v)); + } + } + } + assert!( + total_violations.is_empty(), + ".takt/workflows/*.yaml で persona: → model: 不在 violation が検出されました。`model:` 行を追加してください。違反内容: {:?}", + total_violations + ); + } }