diff --git a/.claude/settings.local.json.template b/.claude/settings.local.json.template index 83cc5f02..2e1434a2 100644 --- a/.claude/settings.local.json.template +++ b/.claude/settings.local.json.template @@ -13,7 +13,7 @@ "hooks": [ { "type": "command", - "command": "\"{{PROJECT_DIR}}\\.claude\\hooks-session-start.exe\"", + "command": "\"{{PROJECT_DIR}}/.claude/hooks-session-start{{EXE_SUFFIX}}\"", "timeout": 5 } ] @@ -24,7 +24,7 @@ "hooks": [ { "type": "command", - "command": "\"{{PROJECT_DIR}}\\.claude\\hooks-user-prompt-feedback-recovery.exe\"", + "command": "\"{{PROJECT_DIR}}/.claude/hooks-user-prompt-feedback-recovery{{EXE_SUFFIX}}\"", "timeout": 5 } ] @@ -36,7 +36,7 @@ "hooks": [ { "type": "command", - "command": "\"{{PROJECT_DIR}}\\.claude\\hooks-pre-tool-validate.exe\"", + "command": "\"{{PROJECT_DIR}}/.claude/hooks-pre-tool-validate{{EXE_SUFFIX}}\"", "timeout": 5 } ] @@ -48,7 +48,7 @@ "hooks": [ { "type": "command", - "command": "\"{{PROJECT_DIR}}\\.claude\\hooks-post-tool-linter.exe\"", + "command": "\"{{PROJECT_DIR}}/.claude/hooks-post-tool-linter{{EXE_SUFFIX}}\"", "timeout": 30 } ] @@ -58,7 +58,7 @@ "hooks": [ { "type": "command", - "command": "\"{{PROJECT_DIR}}\\.claude\\hooks-post-tool-comment-lint-rust.exe\"", + "command": "\"{{PROJECT_DIR}}/.claude/hooks-post-tool-comment-lint-rust{{EXE_SUFFIX}}\"", "timeout": 10 } ] @@ -68,7 +68,7 @@ "hooks": [ { "type": "command", - "command": "\"{{PROJECT_DIR}}\\.claude\\hooks-post-tool-jj-op-verify.exe\"", + "command": "\"{{PROJECT_DIR}}/.claude/hooks-post-tool-jj-op-verify{{EXE_SUFFIX}}\"", "timeout": 10 } ] @@ -79,17 +79,17 @@ "hooks": [ { "type": "command", - "command": "\"{{PROJECT_DIR}}\\.claude\\hooks-stop-tool-call-leak.exe\"", + "command": "\"{{PROJECT_DIR}}/.claude/hooks-stop-tool-call-leak{{EXE_SUFFIX}}\"", "timeout": 5 }, { "type": "command", - "command": "\"{{PROJECT_DIR}}\\.claude\\hooks-stop-quality.exe\"", + "command": "\"{{PROJECT_DIR}}/.claude/hooks-stop-quality{{EXE_SUFFIX}}\"", "timeout": 300 }, { "type": "command", - "command": "\"{{PROJECT_DIR}}\\.claude\\hooks-stop-feedback-dispatch.exe\"", + "command": "\"{{PROJECT_DIR}}/.claude/hooks-stop-feedback-dispatch{{EXE_SUFFIX}}\"", "timeout": 10 } ] diff --git a/docs/adr/adr-005-hooks-path-resolution-with-template.md b/docs/adr/adr-005-hooks-path-resolution-with-template.md index 75c3c2ab..ba756d60 100644 --- a/docs/adr/adr-005-hooks-path-resolution-with-template.md +++ b/docs/adr/adr-005-hooks-path-resolution-with-template.md @@ -104,8 +104,37 @@ ADR-003 の以下の記述はこの ADR により supersede される: - 「`settings.local.json` での参照: `"%CLAUDE_PROJECT_DIR%\\.claude\\<機能名>.exe"`」→ テンプレートの `{{PROJECT_DIR}}` に変更 - 「バージョン管理するもの: `settings.local.json`」→ テンプレートを管理し、生成物は `.gitignore` +### 追記: `{{EXE_SUFFIX}}` 変数 + パス区切り `/` 統一 (2026-07-20、WP-13) + +`harness-improvement-plan.md` WP-13「EXE_SUFFIX 抽象化」(全クラウド対応の土台) の一環として、 +本 ADR のテンプレート機構を OS 非依存化した。 + +**変更点:** + +1. **`{{EXE_SUFFIX}}` プレースホルダーを追加**: テンプレートの exe パス末尾を + `hooks-session-start.exe` から `hooks-session-start{{EXE_SUFFIX}}` に変更し、生成時に OS 依存の + 実行ファイル拡張子 (Windows: `.exe` / それ以外: 空文字) へ置換する。`{{PROJECT_DIR}}` と同じ + ビルド時置換の枠組みに乗せた。 +2. **パス区切りを `/` に統一**: テンプレートの `\\.claude\\` を `/.claude/` に変更し、 + `{{PROJECT_DIR}}` も forward-slash 正規化した値で置換する。**forward-slash の絶対パス exe は + Windows でも実行可能**であることを実測で確認済み (`& "C:/…/x.exe"` / `cmd /c "C:/…/x.exe"` の + 双方が exit 0。加えて配布後の session で forward-slash パスの PreToolUse hook が実発火を確認)。 + これにより JSON エスケープ (`\\`) が不要になり、Linux でもそのまま通る。 +3. **生成ロジックを `scripts/build-hooks-settings.mjs` へ切り出し**: 従来 `package.json` に + インラインで書いていた `node -e "…"` を独立スクリプト化し、`{{EXE_SUFFIX}}` 置換と + **生成物の JSON 妥当性検証 (fail-closed)** を追加した (壊れた settings で hooks が無言で + 無効化される本 ADR 冒頭の事故を防ぐため)。 +4. **`deploy-hooks.ts` も同一解決に追従**: 派生プロジェクト配布時の template 解決も + forward-slash + `{{EXE_SUFFIX}}` に揃え、コピー対象 exe リストも crate 名 + `EXE_SUFFIX` で + 組み立てるよう変更した。 + +**スコープ外 (WP-15 の Linux config 生成に委ねる)**: `hooks-config.toml` の quality_gate `cmd` +(cmd.exe 経由 + `.\.claude\….exe`) や `push-runner-config.toml` の `exe_path` 明示指定は、 +ソースの hardcode ではなくデプロイ時 config であり、cmd.exe 依存の解消を含むため本 WP では変更しない。 + ## References - ADR-003 — hooks の配置規則とビルド戦略(本 ADR で部分的に supersede) - `.claude/settings.local.json.template` — テンプレートの実体 -- `package.json` の `build:hooks-settings` — 生成スクリプト +- `scripts/build-hooks-settings.mjs` — 生成スクリプト (`package.json` の `build:hooks-settings` が起動) +- `scripts/deploy-hooks.ts` — 派生プロジェクト配布時の同一 template 解決 diff --git a/docs/harness-improvement-plan.md b/docs/harness-improvement-plan.md index a1c16350..ba047645 100644 --- a/docs/harness-improvement-plan.md +++ b/docs/harness-improvement-plan.md @@ -51,7 +51,7 @@ Anthropic 公式のハーネスエンジニアリング指針(決定論的基 - **ADR-028**: `pnpm create-pr` / `pnpm merge-pr` は permissions.ask ゲート対象。自動実行しない。 - **PreToolUse hook が `gh` の直呼びを block する**。GitHub 操作は既存の pnpm scripts / cli-* 経由で行うこと(hook のフィードバックに従う)。 - **ADR-043**: fail-closed はゲート関数のみに適用。助言層(本計画の local_review 等)は fail-open(graceful skip)が正しい。この線引きを新規 ADR に明記すること。 -- **Windows ビルドの既知の罠**: `pnpm build:all` は Git for Windows の `usr/bin`(`cp` 等)が PATH に必要。WP-13 完了でこの依存自体が解消される。 +- **Windows ビルドの既知の罠(WP-13 で解消済み)**: 従来 `pnpm build:all` は Git for Windows の `usr/bin`(`cp` 等)を PATH に要求していたが、WP-13 で `cp` を Node 製 `scripts/deploy-artifacts.mjs` に置換したため、この PATH 依存は解消された。 - **本ファイルを含む md 編集時に発火するカスタムルール**: 個人ユーザーパスの記載禁止(rule②・error)、`](../docs/` 形式のバックリンク禁止(rule⑧・error)、非 ASCII 見出しへのアンカーリンク警告(rule⑤)。markdownlint は MD028 / MD040(コードフェンスに言語必須)/ MD058(table 前後に空行)のみ有効。 - **takt はバージョン固定**(ADR-017)。Linux 対応時も同一バージョンの Linux バイナリを取得する。 - 派生プロジェクト(techbook-ledger / auto-review-fix-vc)への配布(`pnpm deploy:hooks`)を壊さないこと(WP-13 で特に注意)。 @@ -72,7 +72,7 @@ Anthropic 公式のハーネスエンジニアリング指針(決定論的基 | WP-10 | 1-C | 自律境界ポリシー ADR(ADR-028 の 2 段化) | S | なし | 実装済(ADR-052 起票: 自律 actor 限定の 2 クラス分類〔自動実行可: docs-only / Tier3 cleanup / `claude/` push / draft PR 作成、ゲート必須: ready 化 / マージ / master push〕+ 分類不能は fail-closed〔ADR-043〕。ADR-028 のゲートを commitment 点へ移設するのが 2 段化の本質。試験運用。Rust 分類関数は呼び手〔自律実行経路〕不在で今回見送り= WP-17/18 着手時に gate.rs の docs-only 判定を lib 切り出しで実装) | | WP-11 | 2 | prompt injection 信頼境界の 3 層防御 | M-L | WP-08 | 実装済([ADR-054](adr/adr-054-prompt-injection-trust-boundary-defense.md): 分類/指示/決定論の 3 層 + security facet + fixture。決定論層は default OFF opt-in、本リポジトリは observe で dogfood 開始。誤検知ゼロ確認後 enforce 昇格が採否判定〔3-5 PR〕) | | WP-12 | 2 | 発火テレメトリ + ハーネス ROI 棚卸し | M | なし | 実装済(step1 収集層のみ: ADR-055 + lib-telemetry + 6 hook 計装。step2-3〔集計 pre-step / 卒業判定機械化〕は 28 日 warm-up 後着手のため todo 順位 307/308 へ移管) | -| WP-13 | 3 | EXE_SUFFIX 抽象化 | M | なし | 未着手 | +| WP-13 | 3 | EXE_SUFFIX 抽象化 | M | なし | 実装済(build/実行 scripts を deploy-artifacts.mjs / run-artifact.mjs 経由に、settings を `/` 区切り + `{{EXE_SUFFIX}}` 化、Rust の機能的 exe 解決を EXE_SUFFIX 化。ADR-005 amendment。cargo test 全 pass・build:all/deploy:hooks/lint:docs 退行なし実測。config TOML の cmd.exe 依存は WP-15 へ。`完了` は初回 push/PR で launcher 経路の実走確認後) | | WP-14 | 3 | PowerShell 3 本の Rust 化 | S-M ×2 | なし | 未着手 | | WP-15 | 3 | Linux バイナリビルド + クラウド setup script | M | WP-13, 14 | 未着手 | | WP-16 | 3 | CI matrix(移植退行防止) | S | WP-13, 14 | 未着手 | @@ -232,6 +232,8 @@ Anthropic 公式のハーネスエンジニアリング指針(決定論的基 ### WP-13: EXE_SUFFIX 抽象化 +> **実装済 (2026-07-20)**: `.exe` ハードコードを OS 非依存化する 5 コミットで実装。① build コピー `cp target/release/*.exe .claude/` を Node 製 `scripts/deploy-artifacts.mjs` に置換(`cp` の PATH 依存も構造解消)。② 実行系 pnpm scripts(push / create-pr / mark-notified / merge-pr / check-ci / lint:docs)を `scripts/run-artifact.mjs` ランチャー経由に(suffix 解決 + 引数忠実転送で cli-pr-monitor の `--body` 再結合も透過)。③ `settings.local.json.template` を `/` 区切り + `{{EXE_SUFFIX}}` に統一(forward-slash 絶対パス exe が Windows でも実行可能なことを実測、配布後 session の PreToolUse hook 実発火も確認)、生成を `scripts/build-hooks-settings.mjs` へ切り出し + JSON 妥当性検証(fail-closed)、`deploy-hooks.ts` も同一解決に追従([ADR-005](adr/adr-005-hooks-path-resolution-with-template.md) amendment)。④ Rust の機能的 exe 解決を `std::env::consts::EXE_SUFFIX` 化。**スコープ補正**: プラン策定(2026-07-04)後のコード進化で当初想定の「hooks-pre-tool-validate の protected_files / polling_exe」は stale(protected_files に `.exe` 無し)となっており、実際の機能的 `.exe`(cli-pr-monitor の classifier/checker exe 解決・wakeup fallback 3 箇所、cli-push-runner の lint_screen デフォルト、hooks-pre-tool-validate の exe-help-block regex を `.exe` optional 化して Linux バイナリも block)を精査して置換した。**スコープ外(WP-15 の Linux config 生成へ委譲)**: `hooks-config.toml` の quality_gate `cmd`(cmd.exe 経由 + `.\.claude\….exe`)と `push-runner-config.toml` の `exe_path` 明示指定はソース hardcode ではなくデプロイ時 config であり、cmd.exe 依存の解消を含むため本 WP では変更しない。**受け入れ基準の実測**: cargo test 全 pass(248/256/232)・clippy clean・`pnpm build:all` / `deploy:hooks`(一時ターゲットで smoke)/ `lint:docs` 退行なし。**`完了` 条件**: 本変更を含む初回 push/PR サイクルで push / create-pr 経路の launcher 実走に退行が無いことを確認したら `完了`(create-pr / merge-pr は副作用のため本セッションでは launcher の引数透過性を設計で担保するに留めた)。以下は当初ステップ(記録用)。 + - **目的**: `.exe` ハードコード(package.json の build/実行 scripts 15 箇所以上 + Rust コード 2 箇所)の解消。全クラウド対応の土台。 - **ステップ**: 1. `scripts/deploy-artifacts.mjs`(Node 製・クロスプラットフォーム)を新設し、package.json の全 `cp target/release/*.exe .claude/` を置換(`process.platform` で suffix 判定)。**副次効果: Git usr/bin の `cp` PATH 依存(既知の罠)が構造的に解消される。** diff --git a/docs/todo-summary.md b/docs/todo-summary.md index fdc94521..f7bd51f5 100644 --- a/docs/todo-summary.md +++ b/docs/todo-summary.md @@ -177,6 +177,8 @@ | 331 | 🔧 Tier 2 | **hooks-session-start に systemMessage を含む JSON 出力の exe-spawn E2E テスト追加 (#299 post-merge feedback 採用)** | todo13.md | S | なし (現状 pure function レベルのみ、実 config パース込み exe 駆動の検証なし。ADR-049 exe-spawn E2E 先例流用。UI 実描画確認は別途 dogfood) | | 332 | 🔧 Tier 2 | **`pnpm build:all` 前に git usr/bin (cp.exe) PATH 未設定を自動検出・追加 (#301 post-merge feedback 採用)** | todo13.md | S | なし (Windows で pnpm が cmd.exe 経由実行のため `cp` 解決失敗。memory 既記録だが再発2回目。Windows 限定 additive 分岐、他OS非影響) | | 333 | 🔧 Tier 2 | **VSCode 拡張が hook `systemMessage` を UI 描画するかの調査 (ADR-059 dogfood / 削除条件 2、2026-07-19 週次レビュー観測)** | todo14.md | S | なし (2026-07-19 dogfood で VSCode UI での systemMessage 独立描画が未確認 = additionalContext 経由のみ観測。ターミナル CLI との挙動差を切り分け。ADR-059 bounded-lifetime 判定 期限 2026-08-16 の blocker。描画なしでも defense-in-depth backstop あり revert 不要) | +| 334 | 🚀 Tier 1 | **docs/todo*.md 本文の順位番号表記を検出する custom lint rule (ADR-033 使用禁止の仕組み化、#303 post-merge feedback 採用)** | todo14.md | S | なし (ADR-033 が禁止規定 + 将来の展望で lint 検討済みだが未実装 約3ヶ月。検証 grep 実証済み、literal-ban rule⑥/⑪ と同型) | +| 335 | 🔧 Tier 2 | **post-merge-feedback の transcript 分析を cli-merge-pipeline 生成 summary index に置換 (#303 post-merge feedback 採用)** | todo14.md | M | なし (session-analysis facet が約 1.5MB transcript で 25K token limit 衝突・避難措置を自己観測。既存 filter の自然な拡張、Frequency High) | **戦略**: 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/todo14.md b/docs/todo14.md index 81632d27..d9c73c57 100644 --- a/docs/todo14.md +++ b/docs/todo14.md @@ -30,3 +30,47 @@ #### 完了基準 - VSCode 拡張 (と CLI) で hook `systemMessage` が描画されるかが切り分けられ、ADR-059 削除条件 2 の判定 (計画書 `docs/weekly-review-notification-plan.md` の削除可否) が下せること。 + +--- + +### docs/todo*.md 本文の順位番号表記を検出する custom lint rule (ADR-033 使用禁止の仕組み化) + +> **動機**: [ADR-033](adr/adr-033-todo-numbering-simplification.md) (2026-04-29 試験運用) が「絶対番号は table のみに保持し、本文中の順位番号表記は使用禁止」と規定し、「将来の展望」節で pre-push hook の custom_lint_rule 追加を検討済みと明記したが、未実装のまま約 3 ヶ月経過。#303 の CodeRabbit 対応でも本文参照の drift が問題化した文脈。#303 post-merge feedback で採用。 +> +> **対処案**: `.claude/custom-lint-rules.toml` に regex rule を追加し、`docs/todo*.md` の本文 (table 行を除く) に残る順位番号の literal 表記を検出する。ADR-033 の検証用 grep が既に動作実証済みのため rule 化の Effort は S。既存の literal-ban 系 custom rule (rule⑥/⑪) と同型。 +> +> **参照**: `.claude/feedback-reports/303.md` Tier1 #2、[ADR-033](adr/adr-033-todo-numbering-simplification.md) (§ 将来の展望)、`.claude/custom-lint-rules.toml`。 +> +> **実行優先度**: 🚀 Tier 1 — Severity Medium / Frequency Medium / Effort S / Adoption Risk None (ADR-033 で既に禁止規定 + 検証 grep 実証済み)。 + +#### 作業計画 + +- [ ] `.claude/custom-lint-rules.toml` に `docs/todo*.md` 本文の順位番号表記を検出する regex rule を追加 (table 行を除外) +- [ ] 既存本文の違反を洗い出し修正 (ADR-033 の grep を流用) +- [ ] 本エントリ削除 + todo-summary.md 行削除 + +#### 完了基準 + +- `docs/todo*.md` 本文に順位番号表記が混入した場合、pre-push / PostToolUse で決定論的に検出されること (ADR-033 の規定が仕組みで強制される)。 + +--- + +### post-merge-feedback の transcript 分析を cli-merge-pipeline 生成の summary index に置換 + +> **動機**: post-merge-feedback の session-analysis facet が、大きな transcript (#303 マージ時は約 1.5MB / 427 行) で 25K token limit に衝突し、Grep + 手動パースの避難措置を要した (aggregate 工程の自己観測)。cli-merge-pipeline は既に transcript filter を実施済みのため、index 出力の追加は自然な拡張。#303 post-merge feedback で採用。 +> +> **対処案**: cli-merge-pipeline の Phase 0 (transcript filter) で summary index (timestamp / message_type / tool_name / outcome) を事前生成し、session-analysis facet の入力を raw transcript からこの index に置換する。token limit 衝突を構造的に回避。 +> +> **参照**: `.claude/feedback-reports/303.md` Tier2 #1、`src/cli-merge-pipeline` (Phase 0 transcript filter 出力)、`.takt/facets/instructions/analyze-session.md` (消費側 facet)。 +> +> **実行優先度**: 🔧 Tier 2 — Severity Medium / Frequency High (毎回のマージ feedback で発生し得る) / Effort M / Adoption Risk None (既存 filter の自然な拡張)。 + +#### 作業計画 + +- [ ] cli-merge-pipeline の Phase 0 で transcript summary index を生成 (timestamp / message_type / tool_name / outcome) +- [ ] session-analysis facet の入力を index に切替 + token 消費が threshold 内に収まることを確認 +- [ ] 本エントリ削除 + todo-summary.md 行削除 + +#### 完了基準 + +- 大きな transcript の PR でも session-analysis facet が token limit に衝突せず、Grep 避難措置なしで分析が完了すること。 diff --git a/package.json b/package.json index adca1aea..58dcd84e 100644 --- a/package.json +++ b/package.json @@ -5,32 +5,32 @@ "scripts": { "lint": "npx oxlint src/", "lint:md": "npx --no-install markdownlint-cli2 \"**/*.md\"", - "lint:docs": ".\\.claude\\cli-docs-lint.exe", + "lint:docs": "node scripts/run-artifact.mjs cli-docs-lint", "test": "npx vitest run", "test:e2e": "npx tsx scripts/e2e.ts", "build": "npx --no-install tsc --noEmit --pretty", - "build:hooks-pre-tool-validate": "cargo build --release -p hooks-pre-tool-validate && cp target/release/hooks-pre-tool-validate.exe .claude/hooks-pre-tool-validate.exe", - "build:hooks-post-tool-linter": "cargo build --release -p hooks-post-tool-linter && cp target/release/hooks-post-tool-linter.exe .claude/hooks-post-tool-linter.exe", - "build:hooks-post-tool-comment-lint-rust": "cargo build --release -p hooks-post-tool-comment-lint-rust && cp target/release/hooks-post-tool-comment-lint-rust.exe .claude/hooks-post-tool-comment-lint-rust.exe", - "build:hooks-post-tool-jj-op-verify": "cargo build --release -p hooks-post-tool-jj-op-verify && cp target/release/hooks-post-tool-jj-op-verify.exe .claude/hooks-post-tool-jj-op-verify.exe", - "build:hooks-stop-quality": "cargo build --release -p hooks-stop-quality && cp target/release/hooks-stop-quality.exe .claude/hooks-stop-quality.exe", - "build:hooks-stop-feedback-dispatch": "cargo build --release -p hooks-stop-feedback-dispatch && cp target/release/hooks-stop-feedback-dispatch.exe .claude/hooks-stop-feedback-dispatch.exe", - "build:hooks-stop-tool-call-leak": "cargo build --release -p hooks-stop-tool-call-leak && cp target/release/hooks-stop-tool-call-leak.exe .claude/hooks-stop-tool-call-leak.exe", - "build:hooks-user-prompt-feedback-recovery": "cargo build --release -p hooks-user-prompt-feedback-recovery && cp target/release/hooks-user-prompt-feedback-recovery.exe .claude/hooks-user-prompt-feedback-recovery.exe", - "build:cli-push-runner": "cargo build --release -p cli-push-runner && cp target/release/cli-push-runner.exe .claude/cli-push-runner.exe", - "build:cli-pr-monitor": "cargo build --release -p cli-pr-monitor && cp target/release/cli-pr-monitor.exe .claude/cli-pr-monitor.exe", - "build:check-ci-coderabbit": "cargo build --release -p check-ci-coderabbit && cp target/release/check-ci-coderabbit.exe .claude/check-ci-coderabbit.exe", - "build:cli-finding-classifier": "cargo build --release -p cli-finding-classifier && cp target/release/cli-finding-classifier.exe .claude/cli-finding-classifier.exe", - "build:hooks-session-start": "cargo build --release -p hooks-session-start && cp target/release/hooks-session-start.exe .claude/hooks-session-start.exe", - "build:cli-merge-pipeline": "cargo build --release -p cli-merge-pipeline && cp target/release/cli-merge-pipeline.exe .claude/cli-merge-pipeline.exe", - "build:cli-docs-lint": "cargo build --release -p cli-docs-lint && cp target/release/cli-docs-lint.exe .claude/cli-docs-lint.exe", - "build:hooks-settings": "node -e \"const fs=require('fs');const t=fs.readFileSync('.claude/settings.local.json.template','utf8');const p=process.cwd().replace(/\\\\/g,'\\\\\\\\');fs.writeFileSync('.claude/settings.local.json',t.replace(/\\{\\{PROJECT_DIR\\}\\}/g,p))\" && echo settings.local.json generated", + "build:hooks-pre-tool-validate": "cargo build --release -p hooks-pre-tool-validate && node scripts/deploy-artifacts.mjs hooks-pre-tool-validate", + "build:hooks-post-tool-linter": "cargo build --release -p hooks-post-tool-linter && node scripts/deploy-artifacts.mjs hooks-post-tool-linter", + "build:hooks-post-tool-comment-lint-rust": "cargo build --release -p hooks-post-tool-comment-lint-rust && node scripts/deploy-artifacts.mjs hooks-post-tool-comment-lint-rust", + "build:hooks-post-tool-jj-op-verify": "cargo build --release -p hooks-post-tool-jj-op-verify && node scripts/deploy-artifacts.mjs hooks-post-tool-jj-op-verify", + "build:hooks-stop-quality": "cargo build --release -p hooks-stop-quality && node scripts/deploy-artifacts.mjs hooks-stop-quality", + "build:hooks-stop-feedback-dispatch": "cargo build --release -p hooks-stop-feedback-dispatch && node scripts/deploy-artifacts.mjs hooks-stop-feedback-dispatch", + "build:hooks-stop-tool-call-leak": "cargo build --release -p hooks-stop-tool-call-leak && node scripts/deploy-artifacts.mjs hooks-stop-tool-call-leak", + "build:hooks-user-prompt-feedback-recovery": "cargo build --release -p hooks-user-prompt-feedback-recovery && node scripts/deploy-artifacts.mjs hooks-user-prompt-feedback-recovery", + "build:cli-push-runner": "cargo build --release -p cli-push-runner && node scripts/deploy-artifacts.mjs cli-push-runner", + "build:cli-pr-monitor": "cargo build --release -p cli-pr-monitor && node scripts/deploy-artifacts.mjs cli-pr-monitor", + "build:check-ci-coderabbit": "cargo build --release -p check-ci-coderabbit && node scripts/deploy-artifacts.mjs check-ci-coderabbit", + "build:cli-finding-classifier": "cargo build --release -p cli-finding-classifier && node scripts/deploy-artifacts.mjs cli-finding-classifier", + "build:hooks-session-start": "cargo build --release -p hooks-session-start && node scripts/deploy-artifacts.mjs hooks-session-start", + "build:cli-merge-pipeline": "cargo build --release -p cli-merge-pipeline && node scripts/deploy-artifacts.mjs cli-merge-pipeline", + "build:cli-docs-lint": "cargo build --release -p cli-docs-lint && node scripts/deploy-artifacts.mjs cli-docs-lint", + "build:hooks-settings": "node scripts/build-hooks-settings.mjs", "build:all": "pnpm build:hooks-session-start && pnpm build:hooks-pre-tool-validate && pnpm build:hooks-post-tool-linter && pnpm build:hooks-post-tool-comment-lint-rust && pnpm build:hooks-post-tool-jj-op-verify && pnpm build:hooks-stop-quality && pnpm build:hooks-stop-feedback-dispatch && pnpm build:hooks-stop-tool-call-leak && pnpm build:hooks-user-prompt-feedback-recovery && pnpm build:cli-push-runner && pnpm build:cli-pr-monitor && pnpm build:cli-merge-pipeline && pnpm build:check-ci-coderabbit && pnpm build:cli-finding-classifier && pnpm build:cli-docs-lint && pnpm build:hooks-settings", - "push": ".\\.claude\\cli-push-runner.exe && .\\.claude\\cli-pr-monitor.exe --monitor-only", - "create-pr": ".\\.claude\\cli-pr-monitor.exe", - "mark-notified": ".\\.claude\\cli-pr-monitor.exe --mark-notified", - "merge-pr": ".\\.claude\\cli-merge-pipeline.exe", - "check-ci": ".\\.claude\\check-ci-coderabbit.exe", + "push": "node scripts/run-artifact.mjs cli-push-runner && node scripts/run-artifact.mjs cli-pr-monitor --monitor-only", + "create-pr": "node scripts/run-artifact.mjs cli-pr-monitor", + "mark-notified": "node scripts/run-artifact.mjs cli-pr-monitor --mark-notified", + "merge-pr": "node scripts/run-artifact.mjs cli-merge-pipeline", + "check-ci": "node scripts/run-artifact.mjs check-ci-coderabbit", "prepare-pr-body": "pwsh -NoProfile -File scripts/prepare-pr-body.ps1", "prepare-pr-body:cleanup": "pwsh -NoProfile -File scripts/prepare-pr-body.ps1 -Cleanup", "deploy:hooks": "npx tsx scripts/deploy-hooks.ts", diff --git a/scripts/build-hooks-settings.mjs b/scripts/build-hooks-settings.mjs new file mode 100644 index 00000000..ead6a685 --- /dev/null +++ b/scripts/build-hooks-settings.mjs @@ -0,0 +1,41 @@ +/** + * .claude/settings.local.json.template から settings.local.json を生成する + * (ADR-005 / WP-13: EXE_SUFFIX 抽象化)。 + * + * 置換: + * {{PROJECT_DIR}} → リポジトリ絶対パス (forward-slash 正規化) + * {{EXE_SUFFIX}} → OS 依存の実行ファイル拡張子 (Windows: ".exe" / それ以外: "") + * + * パス区切りを `/` に統一することで、Windows でも JSON エスケープ (`\\`) 不要で + * 動作し (forward-slash 絶対パスの exe は cmd.exe / 直接起動の双方で実行可能)、 + * かつ Linux 移植の土台になる。従来 package.json にインラインで書いていた node -e + * を本スクリプトへ切り出し、{{EXE_SUFFIX}} 置換を追加した。 + */ + +import { readFileSync, writeFileSync } from "node:fs"; +import { resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** OS 依存の実行ファイル拡張子 (Windows: ".exe" / それ以外: "")。 */ +const EXE_SUFFIX = process.platform === "win32" ? ".exe" : ""; + +const SCRIPTS_DIR = fileURLToPath(new URL(".", import.meta.url)); +const ROOT = resolve(SCRIPTS_DIR, ".."); +const TEMPLATE = join(ROOT, ".claude", "settings.local.json.template"); +const OUTPUT = join(ROOT, ".claude", "settings.local.json"); + +const projectDir = ROOT.replace(/\\/g, "/"); +const template = readFileSync(TEMPLATE, "utf8"); +// 置換値はリテラルとして扱う (関数リプレーサ)。文字列置換だと projectDir 中の +// `$&` / `$'` / `` $` `` / `$1` 等が特殊解釈され、パスが破損して hooks が +// 無言で無効化される (fail-open) 恐れがあるため。 +const resolved = template + .replace(/\{\{PROJECT_DIR\}\}/g, () => projectDir) + .replace(/\{\{EXE_SUFFIX\}\}/g, () => EXE_SUFFIX); + +// 生成物が壊れて hooks が無効化される事故 (ADR-005 の背景) を防ぐため、 +// 書き出す前に JSON として妥当か検証する (fail-closed)。 +JSON.parse(resolved); + +writeFileSync(OUTPUT, resolved); +console.log("settings.local.json generated"); diff --git a/scripts/deploy-artifacts.mjs b/scripts/deploy-artifacts.mjs new file mode 100644 index 00000000..56ff1d89 --- /dev/null +++ b/scripts/deploy-artifacts.mjs @@ -0,0 +1,48 @@ +/** + * ビルド済み Rust 成果物を target/release/ から .claude/ へコピーする + * クロスプラットフォームスクリプト (WP-13: EXE_SUFFIX 抽象化)。 + * + * 従来 package.json の build:* スクリプトは `cp target/release/.exe .claude/` + * を使っていたが、これは (1) `.exe` を Windows 決め打ち (2) Git for Windows の + * usr/bin (`cp.exe`) を PATH に要求する、という 2 つの可搬性の壁があった。 + * 本スクリプトは `process.platform` から実行ファイル拡張子を解決し、Node の + * copyFileSync でコピーするため、両方の壁を構造的に解消する。 + * + * 使い方: node scripts/deploy-artifacts.mjs [ ...] + * 例: node scripts/deploy-artifacts.mjs hooks-stop-quality + */ + +import { copyFileSync, existsSync } from "node:fs"; +import { resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** OS 依存の実行ファイル拡張子 (Windows: ".exe" / それ以外: "")。 */ +const EXE_SUFFIX = process.platform === "win32" ? ".exe" : ""; + +const SCRIPTS_DIR = fileURLToPath(new URL(".", import.meta.url)); +const ROOT = resolve(SCRIPTS_DIR, ".."); +const RELEASE_DIR = join(ROOT, "target", "release"); +const CLAUDE_DIR = join(ROOT, ".claude"); + +function main() { + const names = process.argv.slice(2); + if (names.length === 0) { + console.error("usage: node scripts/deploy-artifacts.mjs [ ...]"); + process.exit(2); + } + + for (const name of names) { + const fileName = `${name}${EXE_SUFFIX}`; + const src = join(RELEASE_DIR, fileName); + const dest = join(CLAUDE_DIR, fileName); + if (!existsSync(src)) { + console.error(`error: build artifact not found: ${src}`); + console.error(" run the corresponding `cargo build --release -p ` first"); + process.exit(1); + } + copyFileSync(src, dest); + console.log(`deployed: ${fileName} -> .claude/`); + } +} + +main(); diff --git a/scripts/deploy-hooks.ts b/scripts/deploy-hooks.ts index 6232016e..e1b63966 100644 --- a/scripts/deploy-hooks.ts +++ b/scripts/deploy-hooks.ts @@ -19,18 +19,22 @@ const __dirname = fileURLToPath(new URL(".", import.meta.url)); const ROOT = resolve(__dirname, ".."); const CLAUDE_DIR = join(ROOT, ".claude"); -const EXE_FILES = [ - "hooks-pre-tool-validate.exe", - "hooks-post-tool-linter.exe", - "hooks-post-tool-comment-lint-rust.exe", - "hooks-stop-quality.exe", - "hooks-stop-feedback-dispatch.exe", - "hooks-user-prompt-feedback-recovery.exe", - "cli-push-runner.exe", - "cli-pr-monitor.exe", - "cli-merge-pipeline.exe", - "check-ci-coderabbit.exe", - "cli-docs-lint.exe", +/** OS 依存の実行ファイル拡張子 (Windows: ".exe" / それ以外: "")。WP-13: EXE_SUFFIX 抽象化。 */ +const EXE_SUFFIX = process.platform === "win32" ? ".exe" : ""; + +// crate 名 (拡張子なし)。コピー時に EXE_SUFFIX を付与する。 +const EXE_BASENAMES = [ + "hooks-pre-tool-validate", + "hooks-post-tool-linter", + "hooks-post-tool-comment-lint-rust", + "hooks-stop-quality", + "hooks-stop-feedback-dispatch", + "hooks-user-prompt-feedback-recovery", + "cli-push-runner", + "cli-pr-monitor", + "cli-merge-pipeline", + "check-ci-coderabbit", + "cli-docs-lint", ]; const SETTINGS_TEMPLATE = "settings.local.json.template"; @@ -91,7 +95,8 @@ function deployTo(targetDir: string): boolean { mkdirSync(targetClaude, { recursive: true }); } - for (const exe of EXE_FILES) { + for (const base of EXE_BASENAMES) { + const exe = `${base}${EXE_SUFFIX}`; const src = join(CLAUDE_DIR, exe); if (!existsSync(src)) { logger.warn(` ${exe} not found (run pnpm build:all first)`); @@ -121,10 +126,12 @@ function deployTo(targetDir: string): boolean { const templateSrc = join(CLAUDE_DIR, SETTINGS_TEMPLATE); if (existsSync(templateSrc)) { const template = readFileSync(templateSrc, "utf8"); - const resolved = template.replace( - /\{\{PROJECT_DIR\}\}/g, - targetDir.replace(/\\/g, "\\\\") - ); + // 置換値はリテラルとして扱う (関数リプレーサ)。文字列置換だと projectDir 中の + // `$&` / `$'` 等が特殊解釈され、生成される settings のパスが破損する恐れがある。 + const projectDir = targetDir.replace(/\\/g, "/"); + const resolved = template + .replace(/\{\{PROJECT_DIR\}\}/g, () => projectDir) + .replace(/\{\{EXE_SUFFIX\}\}/g, () => EXE_SUFFIX); let newSettings: Record; try { newSettings = JSON.parse(resolved); diff --git a/scripts/run-artifact.mjs b/scripts/run-artifact.mjs new file mode 100644 index 00000000..0aa053d8 --- /dev/null +++ b/scripts/run-artifact.mjs @@ -0,0 +1,60 @@ +/** + * .claude/ 配下のビルド済み実行ファイルを OS 非依存に起動するランチャー + * (WP-13: EXE_SUFFIX 抽象化)。 + * + * 従来 package.json の実行系スクリプト (push / create-pr / merge-pr / check-ci / + * lint:docs 等) は `.\.claude\.exe` を直接呼んでいたが、`.exe` を Windows + * 決め打ちしていた。本ランチャーは process.platform から実行ファイル拡張子を解決し、 + * spawnSync で子プロセスを起動して終了コードをそのまま伝播する。 + * + * 引数は忠実に子プロセスへ転送する (spawnSync は shell を介さず配列で渡すため、 + * cli-pr-monitor 側の `--body` 再結合ロジック等はそのまま機能する)。 + * + * 使い方: node scripts/run-artifact.mjs [args...] + * 例: node scripts/run-artifact.mjs cli-pr-monitor --monitor-only + */ + +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** OS 依存の実行ファイル拡張子 (Windows: ".exe" / それ以外: "")。 */ +const EXE_SUFFIX = process.platform === "win32" ? ".exe" : ""; + +const SCRIPTS_DIR = fileURLToPath(new URL(".", import.meta.url)); +const ROOT = resolve(SCRIPTS_DIR, ".."); +const CLAUDE_DIR = join(ROOT, ".claude"); + +const [name, ...forwarded] = process.argv.slice(2); +if (!name) { + console.error("usage: node scripts/run-artifact.mjs [args...]"); + process.exit(2); +} + +// このリポジトリの Rust 製 exe は --help を未実装で、help フラグを転送すると実体 +// (例: cli-merge-pipeline は merge 本体) が起動し PR #109 / ADR-030 の SIGPIPE 事故 +// ベクタになる。exe-help-block hook (polling_exe) と同じ意図で、ランチャー経由の +// 呼び出しでも fail-closed に拒否する (ガード迂回の防止)。 +const HELP_FLAGS = new Set(["--help", "-h", "/?"]); +if (forwarded.some((arg) => HELP_FLAGS.has(arg))) { + console.error(`error: ${name} does not implement a help flag; forwarding it would execute the tool itself (PR #109 / ADR-030 SIGPIPE vector).`); + console.error(` read src/${name}/src/main.rs for its arguments instead.`); + process.exit(2); +} + +const exePath = join(CLAUDE_DIR, `${name}${EXE_SUFFIX}`); +if (!existsSync(exePath)) { + console.error(`error: artifact not found: ${exePath}`); + console.error(" run `pnpm build:all` to build and deploy it first"); + process.exit(1); +} + +const result = spawnSync(exePath, forwarded, { stdio: "inherit" }); +if (result.error) { + console.error(`error: failed to launch ${name}: ${result.error.message}`); + process.exit(1); +} +// シグナルで終了した場合 status は null になるため非ゼロに正規化する。 +// それ以外は子プロセスの終了コードをそのまま伝播し、`&&` チェーンを維持する。 +process.exit(result.status === null ? 1 : result.status); diff --git a/src/cli-pr-monitor/src/classifier_runner.rs b/src/cli-pr-monitor/src/classifier_runner.rs index 2b8c3123..caea191d 100644 --- a/src/cli-pr-monitor/src/classifier_runner.rs +++ b/src/cli-pr-monitor/src/classifier_runner.rs @@ -35,15 +35,20 @@ pub(crate) struct ClassifiedFinding { pub(crate) fallback_reason: Option, } -/// cli-finding-classifier.exe のパスを解決する。 +/// cli-finding-classifier 実行ファイルのパスを解決する。 /// -/// 通常は cli-pr-monitor.exe と同 dir に置かれる (.claude/ 配下デプロイ前提)。 +/// 通常は cli-pr-monitor と同 dir に置かれる (.claude/ 配下デプロイ前提)。 +/// 実行ファイル拡張子は OS 依存 (Windows: `.exe` / それ以外: なし) のため +/// `std::env::consts::EXE_SUFFIX` で解決する (WP-13: EXE_SUFFIX 抽象化)。 pub(crate) fn classifier_exe_path() -> PathBuf { std::env::current_exe() .unwrap_or_default() .parent() .unwrap_or(Path::new(".")) - .join("cli-finding-classifier.exe") + .join(format!( + "cli-finding-classifier{}", + std::env::consts::EXE_SUFFIX + )) } /// findings を classifier に流して enrich する。 @@ -223,7 +228,8 @@ mod tests { #[test] fn classifier_exe_path_resolves_to_sibling_of_current_exe() { let p = classifier_exe_path(); - assert!(p.to_string_lossy().ends_with("cli-finding-classifier.exe")); + let expected = format!("cli-finding-classifier{}", std::env::consts::EXE_SUFFIX); + assert!(p.to_string_lossy().ends_with(&expected)); } #[test] diff --git a/src/cli-pr-monitor/src/runner.rs b/src/cli-pr-monitor/src/runner.rs index 5d023d1f..c7c0aa81 100644 --- a/src/cli-pr-monitor/src/runner.rs +++ b/src/cli-pr-monitor/src/runner.rs @@ -258,12 +258,16 @@ pub(crate) fn run_cmd_inherit(label: &str, program: &str, args: &[&str]) -> bool } } +/// check-ci-coderabbit 実行ファイルのパスを解決する (cli-pr-monitor と同 dir 前提)。 +/// +/// 実行ファイル拡張子は OS 依存 (Windows: `.exe` / それ以外: なし) のため +/// `std::env::consts::EXE_SUFFIX` で解決する (WP-13: EXE_SUFFIX 抽象化)。 pub(crate) fn checker_exe_path() -> PathBuf { std::env::current_exe() .unwrap_or_default() .parent() .unwrap_or(Path::new(".")) - .join("check-ci-coderabbit.exe") + .join(format!("check-ci-coderabbit{}", std::env::consts::EXE_SUFFIX)) } #[cfg(test)] diff --git a/src/cli-pr-monitor/src/stages/poll/rate_limit_signal.rs b/src/cli-pr-monitor/src/stages/poll/rate_limit_signal.rs index 3b613db2..8852f697 100644 --- a/src/cli-pr-monitor/src/stages/poll/rate_limit_signal.rs +++ b/src/cli-pr-monitor/src/stages/poll/rate_limit_signal.rs @@ -156,7 +156,7 @@ fn collect_posted_retrigger_park_fields( let wait_secs = (wakeup_unix - now_unix).max(0); let exe = std::env::current_exe() .map(|p| p.display().to_string()) - .unwrap_or_else(|_| "cli-pr-monitor.exe".into()); + .unwrap_or_else(|_| format!("cli-pr-monitor{}", std::env::consts::EXE_SUFFIX)); let cwd = std::env::current_dir() .map(|p| p.display().to_string()) .unwrap_or_else(|_| ".".into()); @@ -250,7 +250,7 @@ pub(crate) fn format_park_signal( let wait_total_secs = rl.wait_minutes * 60 + rl.wait_seconds; let exe = std::env::current_exe() .map(|p| p.display().to_string()) - .unwrap_or_else(|_| "cli-pr-monitor.exe".into()); + .unwrap_or_else(|_| format!("cli-pr-monitor{}", std::env::consts::EXE_SUFFIX)); let cwd = std::env::current_dir() .map(|p| p.display().to_string()) .unwrap_or_else(|_| ".".into()); diff --git a/src/cli-pr-monitor/src/stages/poll/review_recheck_signal.rs b/src/cli-pr-monitor/src/stages/poll/review_recheck_signal.rs index ad132d92..0faaa0aa 100644 --- a/src/cli-pr-monitor/src/stages/poll/review_recheck_signal.rs +++ b/src/cli-pr-monitor/src/stages/poll/review_recheck_signal.rs @@ -84,7 +84,7 @@ fn collect_review_park_fields( let wait_secs = (wakeup_unix - now_unix).max(0); let exe = std::env::current_exe() .map(|p| p.display().to_string()) - .unwrap_or_else(|_| "cli-pr-monitor.exe".into()); + .unwrap_or_else(|_| format!("cli-pr-monitor{}", std::env::consts::EXE_SUFFIX)); let cwd = std::env::current_dir() .map(|p| p.display().to_string()) .unwrap_or_else(|_| ".".into()); diff --git a/src/cli-push-runner/src/config/lint_screen.rs b/src/cli-push-runner/src/config/lint_screen.rs index 42d7e2cf..6fce7037 100644 --- a/src/cli-push-runner/src/config/lint_screen.rs +++ b/src/cli-push-runner/src/config/lint_screen.rs @@ -6,7 +6,10 @@ pub(crate) const DEFAULT_LINT_SCREEN_TIMEOUT_SECS: u64 = 60; pub(crate) const DEFAULT_LINT_SCREEN_MAX_DIFF_LINES: usize = 5000; pub(crate) const DEFAULT_LINT_SCREEN_MODEL: &str = "mistral:7b"; pub(crate) const DEFAULT_LINT_SCREEN_ENDPOINT: &str = "http://localhost:11434"; +#[cfg(windows)] pub(crate) const DEFAULT_LINT_SCREEN_EXE_PATH: &str = ".claude/cli-finding-classifier.exe"; +#[cfg(not(windows))] +pub(crate) const DEFAULT_LINT_SCREEN_EXE_PATH: &str = ".claude/cli-finding-classifier"; pub(crate) const DEFAULT_LINT_SCREEN_OUTPUT_PATH: &str = ".takt/lint-screen-report.md"; /// `LINT_SCREEN_ENABLED` env var の名前 (順位 115、Phase D D-1 workflow gap 解消)。 diff --git a/src/hooks-pre-tool-validate/src/presets/safety/polling_exe.rs b/src/hooks-pre-tool-validate/src/presets/safety/polling_exe.rs index 1f850c93..4db2c518 100644 --- a/src/hooks-pre-tool-validate/src/presets/safety/polling_exe.rs +++ b/src/hooks-pre-tool-validate/src/presets/safety/polling_exe.rs @@ -74,13 +74,15 @@ pub(crate) fn preset_polling_anti_pattern() -> Vec { /// `.failed` marker 未生成 → ADR-030 仕様違反、という連鎖の起点。 /// /// 設計: -/// - `?.exe` + 単独 `--help|-h|/?` (subcommand 形式 `exe foo --help` は対象外) +/// - `?[.exe]` + 単独 `--help|-h|/?` (subcommand 形式 `exe foo --help` は対象外) +/// - `.exe` 拡張子は OS 依存のため optional にする (Windows は `.exe` 付き、Linux は拡張子なしの +/// 同名バイナリを同じく block、WP-13: EXE_SUFFIX 抽象化) /// - 引数 `--version` 等は block 対象外 (本 preset の責務は --help 系の trigger のみ) /// - 順位 65 (PR #109 post-merge-feedback 採用、Bundle c) pub(crate) fn preset_exe_help_block() -> Vec { vec![BlockedPattern { pattern: Regex::new( - 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|$)"#, + 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, @@ -303,6 +305,30 @@ mod tests { )); } + #[test] + fn exe_help_block_blocks_linux_binary_without_exe_suffix() { + assert!(is_blocked_with( + "cli-merge-pipeline --help", + &["exe-help-block"] + )); + } + + #[test] + fn exe_help_block_blocks_linux_path_prefixed_binary() { + assert!(is_blocked_with( + "./.claude/cli-merge-pipeline --help", + &["exe-help-block"] + )); + } + + #[test] + fn exe_help_block_allows_linux_subcommand_help() { + assert!(!is_blocked_with( + "cli-merge-pipeline foo --help", + &["exe-help-block"] + )); + } + #[test] fn exe_help_block_allows_cargo_run_help() { assert!(!is_blocked_with("cargo run --help", &["exe-help-block"]));