phasegate 0.283.0 → 0.315.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/docs/ADR/017-warning-severity-aggregation.md +17 -0
  3. package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
  4. package/docs/guide/layer-model.md +2 -0
  5. package/docs/guide/quick-vs-full-mode.md +28 -3
  6. package/docs/guide/troubleshooting.md +37 -0
  7. package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
  8. package/package.json +1 -1
  9. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +98 -18
  10. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +25 -3
  11. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
  12. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +18 -3
  13. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +51 -6
  14. package/scripts/harness/ci-governance/composition-root.ts +2 -2
  15. package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
  16. package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
  17. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
  18. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
  19. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
  20. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -3
  21. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
  22. package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
  23. package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
  24. package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
  25. package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
  26. package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
  27. package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
  28. package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
  29. package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
  30. package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
  31. package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
  32. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
  33. package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
  34. package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
  35. package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
  36. package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
  37. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
  38. package/scripts/harness/installation/application/usecases/run-install.ts +44 -11
  39. package/scripts/harness/installation/application/usecases/run-reconcile.ts +44 -11
  40. package/scripts/harness/installation/composition-root.ts +13 -3
  41. package/scripts/harness/installation/domain/check-id.ts +2 -0
  42. package/scripts/harness/installation/domain/config-status.ts +17 -0
  43. package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
  44. package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
  45. package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
  46. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
  47. package/scripts/harness/integrations/pre-commit.ts +17 -3
  48. package/scripts/harness/main.ts +57 -9
  49. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +60 -32
  50. package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
  51. package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
  52. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
  53. package/scripts/harness/quick-mode/composition-root.ts +25 -15
  54. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +72 -3
  55. package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
  56. package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
  57. package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
  58. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
  59. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
  60. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
  61. package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
  62. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +8 -0
  63. package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
  64. package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
  65. package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
  66. package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
  67. package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
  68. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
  69. package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
  70. package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
  71. package/skills/quick-implementor/SKILL.md +19 -0
package/CHANGELOG.md CHANGED
@@ -7,8 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - **WI-335 — エラー案内の機械往復保証(remediationType 分類 + round-trip テスト)** — suggested action が本当に「従えば直る」かを人間レビュー頼みにせず CI で保証する仕組み。HarnessError に optional `remediationType`(mechanical / ai-assisted / manual、未設定は manual 扱い)を導入し、代表 validator を分類(L2-002 metadata=mechanical、L3-003 レポート不在=mechanical・閾値未達=ai-assisted、L3-001=manual、L4-001/002=ai-assisted)。mechanical 宣言されたエラーは「エラー → suggestion 文言を機械適用 → 再実行 → pass」の往復テストで固定し、案内文言と実挙動が乖離すると(#37/#39 型の破れ)テストが落ちる構造にした。
13
+
14
+ - **WI-329 — 実分布リリースゲート(tarball release-smoke)** — 単体テスト全 green でも新規インストール即クラッシュ(#34)・非 TS リポで fail-closed(#37/#39)・dead flag(#36)がすり抜けた教訓から、`npm pack` した tarball を実分布相当 fixture(純 Python / Go monorepo / docs のみ)にクリーンインストールし install → doctor → validate → uninstall を実走する E2E を追加。通常 suite では skip(`PHASEGATE_RELEASE_SMOKE=1` ガードで hermetic 性維持)し、CI の新 job `release-smoke` が pack job の実バイト列 artifact に対して実行する。`--with-husky` のフラグ有効性 assert で dead-flag 再発も検知。
15
+
10
16
  ### Fixed
11
17
 
18
+ - **WI-334 — CI workflow ファイルの change-category 分類取りこぼしを修正** — `.github/workflows/*.yml` の新規作成が CREATE→feature フォールバックに落ちて pre-tool-use hook にブロックされ、かつ .github/ は unit を持たないため案内される story-implementor 経路が構造的に完遂不能だった(WI-329 のドッグフードで発見。MODIFY は bugfix で通るため防御としても不整合)。WI-261(skills/**/*.md → docs)と同型の明示ルールで `.github/workflows/` 配下の yml/yaml を config に分類(内容防御は L3-006 + integrity pin が担当)。併せて CLI `check-change-category` の changeKind をファイル存在で推定し hook 判定と一致させた(hook の Bash 経路は従来挙動を完全維持)。
19
+
20
+ - **WI-333 — config 不在時の hook 全遮断デッドロックを解消(ADR-038 G1 / GitHub #40 完全解消)** — config **不在**(ENOENT)で pre-tool-use hook が未捕捉例外→exit 2 となり、config を作成する Write すら遮断される自己修復デッドロックが missing 状態に残っていた(WI-314 は invalid のみ対応)。hook 用 config adapter で ENOENT を警告+既定値の fail-open に変更(EACCES 等の真の異常は従来どおり throw)。gated パスへの書き込みは phase-gate 側が config-foundation 経由の独立 load で fail-closed を維持することをテストで固定。ADR-038 の許可表と G1 を解消済みに更新。
21
+
22
+ - **WI-330 — doctor の config 状態可視化と許可ポリシーの仕様化(GitHub #40 恒久化)** — doctor が config 不在・不正でも他項目が揃っていれば GREEN を報告していた盲点を解消。新 check `config-status` を追加し、missing → warn(「既定 fail-open で動作中、`phasegate init` で生成可」)、invalid-json / invalid-schema → red(修復の一手つき)、JSON・human 出力に `configStatus` を明示。「config 状態 × 操作クラス」の許可表を ADR-038 として仕様化し、既知ギャップも正直に記録(**G1: config 不在時に pre-tool-use hook が ENOENT 未捕捉で全ツール遮断=自己修復デッドロックが missing 状態では現存**、G2: invalid-json の検査系が fail-open で原則より緩い)。G1 は WI-333 で修正。
23
+
24
+ - **WI-331 — CLAUDE.md の user-section を managed block の外へ移す構造修正(GitHub #35 根本修正)** — #35 の根本原因は CLAUDE.md テンプレートだけ user-section が managed block の内側にあり(AGENTS.md は外側)、block 置換のたびに WI-315 の「抽出→再注入」に頼っていたこと。テンプレートを AGENTS.md と対称の外側配置に変更し、install / reconcile は旧構造(内側)を検出すると本文を保持したまま外側へ移設する冪等 migration を実行(2 回目適用で byte 同値をテスト固定)。refresh 経路(claude-md-composer)は新テンプレート採用で自動移行、`$` シーケンス保護と空行保持も強化。新構造では uninstall 後も user-section が生存する副次改善あり。
25
+
26
+ - **WI-332 — severity 実効判定の単一ソース化と横断 regression(GitHub #38 恒久化)** — 実効 severity 判定(ADR-017 の isEffectivelyPassed)が harness-api と validator-system に複製され、pre-commit は独自集約で severity を見ておらず warning-only failure が exit 1 になっていた。判定を `validator-system/domain/services/effective-severity-policy.ts` に一本化し、validate 集約・CiCheckResult(ci-check / complete-check)・pre-commit の全経路が同一実装を通る形に統一(pre-commit の warning-only は ADR-017 どおり exit 0 へ)。同一結果セットを 3 経路に通して実効判定の一致を assert する横断 regression を追加し、将来の乖離を機械検知する。
27
+
12
28
  - **WI-328 — 実効言語と出所を phasegate:status に表示(GitHub #39 残課題)** — WI-319/320 の言語自動検出の結果がどこにも表示されず、どの validator が有効/SKIP になるか知る術がなかった。`phasegate:status` の JSON に `languages: { effective, source }`(source = `declared` / `detected` / `fallback`)を追加。解決ロジックは validator-system の `resolveProjectLanguages()`(WI-319 の検出テーブル)に一本化して再利用し、validator の有効/SKIP 判定と必ず同じ結果を表示する。ConfigQueryPort への追加は optional メソッドで後方互換。
13
29
 
14
30
  - **WI-327 — 最小 config(project のみ)で動作可能に** — 手書き `phasegate.config.json` のスキーマが top-level 8 項目を required とし、`{"project": {"name": ..., "preset": ...}}` の最小構成で L1-001 が連発していた。プリセット解決(`PresetResolutionService.deepMerge`)は省略セクションの補完を既に完備していたため、v3 **と v2**(architecture キーなしの最小 config は v2 検証に振られる)の top-level required を `["project"]` に緩和し、型定義を実態に一致させた。**検証は弱めていない**: セクション内の required・型・enum・additionalProperties は不変で、書かれているが不正なキーは従来どおりエラー(spawn E2E で exit 2 を固定)。最小 config の解決結果がプリセット定義と一致することも統合テストで固定。
@@ -212,3 +212,20 @@ human/agent/ci formatter で `[FAIL]` と `[WARN]` を分離:
212
212
  2. **(B) warning も exit 1 を default に保つ(CI 互換性優先)** — default を変えず移行コストはゼロだが、`defaultSeverity: warning` 宣言と「常に fail」実装の semantic 乖離が残るため不採用。
213
213
 
214
214
  採用理由の詳細は上記 Decision「採用理由」を参照。
215
+
216
+ ## 追記 (WI-332, 2026-07-18): 実効判定の共有実装と全経路の経由
217
+
218
+ 本 ADR の Decision(集計セマンティクス)自体は変更しない。実装配置に関する追記のみ。
219
+
220
+ github#38(`complete-check` だけが独自集約を持ち warning-only failure で exit 1 になった回帰。
221
+ WI-318 で修正)の恒久化として、WI-332 で「実効 severity 判定」を単一の共有実装に集約した:
222
+
223
+ - **共有実装(単一ソース)**: `scripts/harness/validator-system/domain/services/effective-severity-policy.ts` の `isEffectivelyPassed()`。本 ADR Decision の集計セマンティクス(skipped / passed=true は実質 pass、error 含みは fail、errors=[] は防御的に fail、warning-only は `failOnWarning=false` 既定で実質 pass)をそのまま実装する
224
+ - **全経路がこれを経由する**:
225
+ - `validate` 集約 — `validator-system/application/use-cases/aggregate-validation-results-usecase.ts`(WI-332 で複製実装を削除し共有実装を参照)
226
+ - `ci-check` / `complete-check` — `harness-api/domain/value-objects/ci-check-result.ts` の `CiCheckResult.fromResults()`(WI-318 で 2 コマンド共有化、WI-332 で複製実装を削除し共有実装を参照)
227
+ - `pre-commit` / `commit-msg` / `bypass-audit` — `integrations/pre-commit.ts` の `buildReport()` / `classifyValidatorFailure()`(WI-332 で手動集約 `failed === 0` を共有実装経由に変更。pre-commit 経路には `validate.failOnWarning` の config 配線が無いため、既定値 `false` に固定)
228
+ - **例外**: `status` コマンドは「failure を表示しつつ常に exit 0」という独立契約のため、本判定の対象外
229
+ - **回帰防御**: `scripts/harness/__tests__/integration/validator-system/severity-aggregation-consistency.test.ts` が同一の validator 結果セット(全 pass / warning-only / error / mixed / errors=[] / skipped)を 3 経路(validate 集約・CiCheckResult・pre-commit 集約)に通し、実効判定の一致を assert する。どれかの経路が独自判定に戻るとこのテストが落ちる
230
+
231
+ 新しい集約経路を追加する場合は、必ず `isEffectivelyPassed()` を経由し、上記横断テストに経路を追加すること(ADR-021 の格下げ禁止契約も併せて参照)。
@@ -0,0 +1,78 @@
1
+ ---
2
+ adr_id: "038"
3
+ title: "config 状態と操作クラスの許可ポリシー"
4
+ status: Accepted
5
+ date: 2026-07-18
6
+ ---
7
+
8
+ # config 状態と操作クラスの許可ポリシー
9
+
10
+ <!-- @work-item-id WI-330 -->
11
+ <!-- @work-item-id WI-333 -->
12
+
13
+ ## Context
14
+
15
+ GitHub #40 は「スキーマ違反の `phasegate.config.json` があると pre-tool-use hook が exit 2 で全ツールを遮断し、config を修復する経路自体が消える(自己修復デッドロック)」という障害だった。WI-314 で hook / doctor を fail-open にする応急処置を入れ、WI-323 で hook 側 adapter の JSON parse fail-open、WI-325 で `ConfigParseError` の導入(誤メッセージ修正)を行った。しかし、
16
+
17
+ - (a) doctor は config 不在・不正でも config 状態を diagnostics に含めず、他が揃っていれば GREEN を報告していた。
18
+ - (b) 「config がどの状態のとき、どの操作クラスが許可されるか」を定義した仕様書が存在せず、各修正がアドホックな穴埋めになっていた。
19
+ - (c) config **missing** 系のテストが欠落しており、missing 状態の実挙動は未検証だった。
20
+
21
+ WI-330 で許可表を実測に基づいて仕様化し、doctor に config 状態 check(`config-status`)を追加し、欠落マスをテストで固定した。
22
+
23
+ ## Decision
24
+
25
+ ### 1. config 状態は 4 値で分類する
26
+
27
+ `missing` / `invalid-json` / `invalid-schema` / `valid`。判定は config-foundation の実 load 経路(`FileSystemConfigRepository` の JSON parse + AJV schema 検証 + preset 解決)をそのまま使う。エラー型との対応:
28
+
29
+ | 状態 | 検出エラー型 | 定義箇所 |
30
+ |------|-------------|---------|
31
+ | missing | `ConfigNotFoundError` | `scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts:12` |
32
+ | invalid-json | `ConfigParseError`(`ConfigPersistenceError` のサブクラス、WI-325) | 同ファイル `:41` |
33
+ | invalid-schema | `ConfigValidationError`(AJV 違反・未知 preset を含む) | `scripts/harness/config-foundation/domain/errors/config-validation-error.ts:9`、throw 箇所は `load-resolved-config-use-case.ts:71-99` |
34
+ | valid | —(load 成功) | — |
35
+
36
+ config の解決順は project 直下 `phasegate.config.json` → `.phasegate-local/phasegate.config.json`(personal install)で、CLI は cwd から上方探索する(`file-system-config-repository.ts:58-81` `findNearestConfig`)。
37
+
38
+ ### 2. 許可表(config 状態 × 操作クラス)
39
+
40
+ 2026-07-18 時点の**実測**(`invalid-config-fail-open.integration.test.ts` で固定。missing 列は WI-333 で fail-open 化)。○ = 許可 / ● = 意図したゲート判定で通過・遮断 / × = 遮断。
41
+
42
+ | 操作クラス \ config 状態 | valid | missing | invalid-json | invalid-schema |
43
+ |---|---|---|---|---|
44
+ | **config 自身への編集**(hook 経由 Write/Edit) | ○ fail-open(既定では保護対象外。`protectedFiles.patterns` に含めた場合のみ CLI 誘導 block) | ○ fail-open(WI-333。自己修復経路) | ○ fail-open | ○ fail-open |
45
+ | **gated パス書込**(`scripts/harness/` 等、hook 経由) | ● phase-gate 判定(fail-closed) | ● 既定設定で phase-gate 判定(fail-closed 維持、WI-333) | ● 既定設定で phase-gate 判定(fail-closed 維持) | ● 既定設定で phase-gate 判定(fail-closed 維持) |
46
+ | **無関係パス書込**(hook 経由) | ○ | ○ fail-open(警告付き、WI-333) | ○ fail-open | ○ fail-open |
47
+ | **無関係 Bash**(書込抽出なし、hook 経由) | ○ | ○ fail-open(警告付き、WI-333) | ○ fail-open(警告付き) | ○ fail-open(警告付き) |
48
+ | **検査系コマンド**(`validate` / `ci-check` 等) | ○ 通常実行 | ○ fail-open(既定設定で実行、exit 0) | **○ fail-open(警告付き exit 0)【設計意図より緩い】** | × fail-closed(exit 2 + Recovery 手順) |
49
+ | **hook・doctor 起動** | ○ | ○ fail-open(doctor は `configStatus: missing` → warn。hook は警告 + 既定設定で完走、WI-333) | ○ fail-open(doctor は `configStatus: invalid-json` → red) | ○ fail-open(`CONFIG_FAIL_OPEN_COMMANDS`。doctor は `configStatus: invalid-schema` → red) |
50
+
51
+ コード根拠:
52
+
53
+ - **CLI dispatch 層の fail-open/fail-closed 分岐**: `scripts/harness/main.ts:1867` `CONFIG_FAIL_OPEN_COMMANDS = new Set(["hook", "doctor"])`、`main.ts:1869-1902` `loadResolvedConfig()` — `ConfigValidationError` は hook/doctor のみ fail-open・他は exit 2(`:1878-1889`)、`ConfigNotFoundError` は全コマンド silent fail-open(`:1891-1893`)、`ConfigParseError`(`instanceof ConfigPersistenceError`)は全コマンド警告付き fail-open(`:1894-1900`)。
54
+ - **hook 実行層の fail-open**: `scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts` `loadConfig()` — JSON parse 失敗と `fs.readFileSync` の ENOENT(missing、WI-333)はいずれも警告 + 空 config(既定値)で続行。ENOENT **以外**の fs エラー(EACCES / EISDIR 等の真の異常)は従来どおり throw し、`scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts` の outer catch で「実行エラー」exit 2 になる。fallback config path は `pre-tool-use-hook.ts:58-76` `findConfigPath()`(不在時は `startDir/phasegate.config.json` を返す)。
55
+ - **missing の gated パス fail-closed**: hook 側 config は既定値に縮退するが、gated パス書込は `phase-gate-query-adapter.ts` が config-foundation の `ConfigNotFoundError` を generic catch で「評価不能 = NOT passed」に落とすため、phase-gate block(フェーズゲート違反、exit 2)が維持される。
56
+ - **config 自身が gated/protected でない根拠**: 既定の保護パターンに `phasegate.config.json` は含まれない(`scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts:17-27` `DEFAULT_PATTERNS`)。ユーザーが `protectedFiles.patterns` で保護した場合のみ CLI 誘導付き block(`handle-pre-tool-use-usecase.ts:480-495`)。
57
+ - **doctor の config 状態可視化(WI-330 で追加)**: `scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts`(分類)、`installation/application/checks/config-status-check.ts`(missing → warn / invalid-* → red)、`installation/presentation/formatters/diagnostic-report-formatter.ts`(JSON `configStatus` フィールドと human `Config:` 行)。
58
+
59
+ ### 3. 設計原則
60
+
61
+ 1. **自己修復例外は復旧経路の保証であり、検査系の fail-closed は緩めない。** hook(エージェントのツール遮断点)と doctor(自己診断)は config がどんな状態でも起動・完走できなければならない。逆に `validate` / `ci-check` 等の検査系は、設定が壊れた状態の検査結果を「合格」として流通させないため fail-closed を原則とする。
62
+ 2. **fail-open は必ず可視化とセットにする。** fail-open で既定設定に落ちた事実は stderr 警告(CLI/hook)と doctor の `configStatus` + `config-status` finding(missing = warn、invalid-* = red)で必ず表面化させる。「不正 config でも doctor GREEN」は本 ADR をもって仕様違反である。
63
+ 3. **gated スコープの fail-closed は config 状態に依存しない。** config が壊れていても、gated パスへの書込は既定設定による phase-gate 判定で引き続きブロックされる(fail-open は「遮断の解除」ではなく「既定設定への縮退」)。
64
+
65
+ ### 4. 既知ギャップ(本 ADR は現状を固定し、修正は別 WI)
66
+
67
+ | # | ギャップ | 現状 | あるべき姿 |
68
+ |---|---------|------|-----------|
69
+ | G1 | **missing 状態で pre-tool-use hook が全ツール遮断**(config 自身への Write も遮断 = 自己修復デッドロック復活) | **WI-333 で解消**: `harness-config-config-query-adapter.ts` `loadConfig()` が ENOENT を捕捉し「警告 + 既定値で続行」の fail-open。config 自身への Write / 無関係 Bash は exit 0、gated パス書込は既定設定の phase-gate 判定で fail-closed 維持(`invalid-config-fail-open.integration.test.ts` の WI-333 テストで固定) | invalid-json と同じ「警告 + 既定値で続行」 — 達成済み |
70
+ | G2 | **invalid-json の検査系が fail-open**(invalid-schema より緩い) | `ConfigParseError` が `ConfigPersistenceError` 経路で全コマンド fail-open(exit 0) | 検査系は invalid-schema と同様 fail-closed が原則に忠実。ただし挙動変更は breaking のため要判断 |
71
+
72
+ いずれも `scripts/harness/__tests__/integration/harness-api/invalid-config-fail-open.integration.test.ts` のテストで挙動を固定済み(G1 は fail-open 期待に反転済み)。挙動を修正する際はテストの期待値反転とこの表の更新を同一変更で行うこと。
73
+
74
+ ## Consequences
75
+
76
+ - doctor は config 状態を常に報告する(JSON: `configStatus`、human: `Config:` 行、finding: `config-status`)。config 不在・不正のまま GREEN を報告することはなくなる(missing は warn、invalid-* は red で exit 1)。
77
+ - 許可表が仕様となったため、fail-open/fail-closed の変更は本 ADR の改訂を伴う。
78
+ - 経緯: WI-314(hook/doctor fail-open 化)→ WI-323(hook adapter の JSON parse fail-open)→ WI-325(`ConfigParseError` 導入)→ WI-330(本 ADR・doctor 可視化・missing 系テスト固定)→ WI-333(G1 解消: hook adapter の ENOENT fail-open、github#40 完全解消)。
@@ -43,6 +43,8 @@ npx phasegate hook post-tool-use < payload.json
43
43
  npx phasegate hook stop < payload.json
44
44
  ```
45
45
 
46
+ > **Config 状態と許可ポリシー**: `phasegate.config.json` が不在・破損している状態で各 hook / コマンドがどう振る舞うか(fail-open / fail-closed の別、自己修復経路、doctor の `configStatus` 報告)は [ADR-038](../ADR/038-config-state-operation-permission-policy.md) の許可表で仕様化されている。
47
+
46
48
  ### L0-B: Husky git hooks
47
49
 
48
50
  Deployed by `phasegate init --with-husky` into `.husky/`.
@@ -71,6 +71,31 @@ npx phasegate session end --work-item <WI-XXX>
71
71
 
72
72
  The PreToolUse hook validates `.phasegate/session.json` by TTL, unit, and dominant category. The `/story-implementor` skill is still the design/TDD guide; the session marker is the authorization the hook can actually observe. <!-- @work-item-id WI-206 -->
73
73
 
74
+ ### How a path becomes a category
75
+
76
+ The classifier is **path-based, not intent-based**. It never reads your commit message or your stated purpose; it looks at the write target's path and its change kind (`CREATE` / `MODIFY` / `DELETE`). Rules are evaluated in this order, first match wins:
77
+
78
+ | Order | Match | Category |
79
+ |---|---|---|
80
+ | 1 | `*.config.json`, `*.config.ts`, `phasegate.config.json` | `config` |
81
+ | 2 | `.github/workflows/*.yml`, `.github/workflows/*.yaml` | `config` |
82
+ | 3 | Repo-root bootstrap files: `.gitignore`, `.gitattributes`, `.editorconfig`, `.npmrc`, `.nvmrc`, `tsconfig.json`, `tsconfig.*.json`, anything under `.husky/` | `config` |
83
+ | 4 | Comment/whitespace-only diff | `docs` |
84
+ | 5 | `*port.ts`, `*adapter.ts` | `api` |
85
+ | 6 | `__tests__/`, `*.test.ts`, `*.spec.ts` | `test` |
86
+ | 7 | Anything under a `domain/` directory | `domain` |
87
+ | 8 | Anything under `docs/` | `docs` |
88
+ | 9 | `skills/**/*.md` | `docs` |
89
+ | 10 | Any other `CREATE` | `feature` |
90
+ | 11 | Any other `MODIFY` / `DELETE` | `bugfix` |
91
+
92
+ Two consequences worth internalising:
93
+
94
+ - The `config` rules are an **explicit allow-list**, not a wildcard. `.github/dependabot.yml`, `packages/app/.gitignore`, or a `Dockerfile` are *not* `config` — created fresh they fall through to `feature` and are blocked. This is deliberate fail-closed behaviour: widening the list is a config decision, not an inference the hook makes for you.
95
+ - `feature` is the fallback for **every new non-test source file**. "It's only a tiny helper" does not change the classification.
96
+
97
+ When the hook blocks a write, the message now names the deciding category and change kind per path (`foo.ts (category=feature, changeKind=CREATE)`), and — for `bugfix` / `docs` / `test` / `config` blocks — points at `allowedCategories` and `config:plan --intent quick-mode-relax` rather than at `/story-implementor`. <!-- @work-item-id WI-352 --> <!-- @work-item-id WI-354 -->
98
+
74
99
  ### Dry-running the classifier
75
100
 
76
101
  Use `check-change-category` to evaluate an arbitrary file list without actually starting an implementation:
@@ -159,16 +184,16 @@ Codify the split in your head (or in a project CLAUDE.md note) — "`domain/**`
159
184
  ## FAQ
160
185
 
161
186
  **Q. I have a one-line bugfix in `domain/`. Does "any file under `domain/`" auto-trigger Full?**
162
- Not mechanically Quick Mode does not inspect paths. A one-line bugfix inside an existing domain method (fixing an off-by-one, null check, wrong comparator) is still Quick-eligible. What triggers Full is *adding or reshaping* a domain concept, not editing an existing one.
187
+ Yes mechanically. The classifier is path-based (see [How a path becomes a category](#how-a-path-becomes-a-category)), so *any* write under a `domain/` directory lands in the `domain` category, which is outside the default `allowedCategories`, and the hook blocks it with `MIXED_CHANGES`. Judgement about "it's only an off-by-one" does not enter into it. If the fix is genuinely trivial, the sanctioned routes are a Full Mode session (`phasegate session begin --mode full --unit <unit> ...`) or, when the project has decided domain edits are routinely Quick-eligible, widening `allowedCategories` via `config:plan`. Do not expect the hook to infer intent.
163
188
 
164
189
  **Q. What if Quick discovers the change is bigger than expected mid-flight?**
165
190
  Stop, commit nothing, and re-launch `/story-implementor`. Don't try to "finish in Quick" — you'll skip the design gate that would otherwise catch the scope creep.
166
191
 
167
192
  **Q. Can I disable Quick Mode entirely?**
168
- Yesremove all entries from `quickMode.allowedCategories` in `phasegate.config.json`. All changes will route through Full Mode. This is reasonable for high-compliance projects. Alternatively, leave the categories as-is and set every `quickMode.fullModeRequiredWhen.*` flag to `true` (the default) so any non-trivial scope automatically escalates.
193
+ Not by emptying the list `allowedCategories: []` is rejected as invalid config (`allowedCategories must not be empty`), so it breaks the hook rather than tightening it. Narrow it instead: `allowedCategories: ["docs"]` routes effectively everything through Full Mode while staying valid. Alternatively, leave the categories as-is and set every `quickMode.fullModeRequiredWhen.*` flag to `true` (the default) so any non-trivial scope automatically escalates.
169
194
 
170
195
  **Q. Can I add custom categories?**
171
- No. `allowedCategories` is a fixed enum (`bugfix`, `docs`, `test`, `config`). If your workflow needs a fifth category, that is evidence the change is probably Full Mode material.
196
+ No. `allowedCategories` accepts only values from a fixed enum — the seven `ChangeCategory` values `bugfix`, `docs`, `test`, `config`, `feature`, `domain`, `api`. The default allow-list is the first four; the remaining three exist so that a project can *deliberately* widen the gate (rarely a good idea), not so you can invent a new label. If your workflow needs a category outside this enum, that is evidence the change is probably Full Mode material.
172
197
 
173
198
  ---
174
199
 
@@ -27,6 +27,43 @@ Each finding has a severity, a repair mode, and optional next-step fields.
27
27
  | `ci-workflow-missing` | `npx phasegate setup:agent --intent ci-only --with-ci --dry-run --json` |
28
28
  | agent context drift | `npx phasegate reconcile --dry-run` |
29
29
 
30
+ ## Phase Gate Blocked a Write and I Do Not Know What the Document Should Contain
31
+
32
+ <!-- @work-item-id WI-356 -->
33
+
34
+ Phase Gate blocks (`L2-001`, `FULL_MODE_REQUIRED`) mean a required inception or product design document is missing. Two things are needed: the file skeleton, and the section structure to fill in.
35
+
36
+ **1. Generate the skeleton.** `scaffold-design` writes a placeholder document for the missing phase:
37
+
38
+ ```bash
39
+ npx phasegate scaffold-design --unit <unit-id> --phase logical --dry-run
40
+ npx phasegate scaffold-design --unit <unit-id> --phase logical --apply
41
+ ```
42
+
43
+ Accepted `--phase` values: `logical`, `domain`, `uiux`, `unit-test`, `it-test`.
44
+
45
+ **2. Read the section structure from the skill definition.** PhaseGate does not ship fill-in templates into your repo; the authoritative structure of every plan and inception artifact lives in the skill markdown that produces it. `install` / `init` copy these into `skills/` in your repository:
46
+
47
+ | Document | Skill definition |
48
+ |---|---|
49
+ | product architecture / unit decomposition | `skills/product-architect/SKILL.md` |
50
+ | story map | `skills/story-mapper/SKILL.md` |
51
+ | story descriptions / acceptance criteria | `skills/story-writer/SKILL.md` |
52
+ | unit plan | `skills/unit-designer/SKILL.md` |
53
+ | `logical_design.md` | `skills/logical-designer/SKILL.md` |
54
+ | `domain_model.md` | `skills/domain-designer/SKILL.md` |
55
+ | UI/UX design | `skills/uiux-designer/SKILL.md` |
56
+ | unit / IT / scenario test plans | `skills/unit-test-designer/SKILL.md`, `skills/it-test-designer/SKILL.md`, `skills/scenario-test-designer/SKILL.md` |
57
+
58
+ If `skills/` is not present in your repository — the personal install path does not copy it — read the same content from PhaseGate's own package instead:
59
+
60
+ ```bash
61
+ npx phasegate skills list
62
+ npx phasegate skills info logical-designer
63
+ ```
64
+
65
+ `skills info` prints the full SKILL.md to stdout, so it works from a sandboxed agent that cannot read `node_modules/`.
66
+
30
67
  ## Refused Managed Target
31
68
 
32
69
  If `install`, `reconcile`, or `uninstall` refuses a target, do not immediately force it. Inspect the diff and ask the agent to explain:
@@ -1,6 +1,6 @@
1
1
  # CLAUDE.md
2
2
 
3
- <!-- @work-item-id WI-174, WI-176, WI-177 -->
3
+ <!-- @work-item-id WI-174, WI-176, WI-177, WI-331 -->
4
4
 
5
5
  <!-- phasegate:managed-section:start -->
6
6
  ## 必読ドキュメント
@@ -38,13 +38,13 @@ If `setup:agent --apply --json` or `install --apply --json` fails with a structu
38
38
 
39
39
  {{PHASEGATE_SKILLS}}
40
40
 
41
+ ## Agent Context Refresh
42
+
43
+ Run `phasegate ci:auto-refresh-agent-context --dry-run` to preview updates and `phasegate ci:auto-refresh-agent-context --apply` to write AGENTS.md / CLAUDE.md.
44
+ <!-- phasegate:managed-section:end -->
45
+
41
46
  ## User Section
42
47
 
43
48
  <!-- phasegate:user-section:start -->
44
49
  {{PHASEGATE_USER_SECTION}}
45
50
  <!-- phasegate:user-section:end -->
46
-
47
- ## Agent Context Refresh
48
-
49
- Run `phasegate ci:auto-refresh-agent-context --dry-run` to preview updates and `phasegate ci:auto-refresh-agent-context --apply` to write AGENTS.md / CLAUDE.md.
50
- <!-- phasegate:managed-section:end -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.283.0",
3
+ "version": "0.315.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -6,6 +6,8 @@
6
6
  * @work-item-id WI-202 / WI-204
7
7
  * @work-item-id WI-206
8
8
  * @work-item-id WI-214
9
+ * @work-item-id WI-349
10
+ * @work-item-id WI-354
9
11
  *
10
12
  * HandlePreToolUseUseCase
11
13
  * PreToolUse Hook処理のオーケストレーション
@@ -17,11 +19,11 @@ import type {
17
19
  } from "../../domain/ports/baseline-grandfather-query-port.js";
18
20
  import type { ConfigQueryPort } from "../../domain/ports/config-query-port.js";
19
21
  import type { ErrorGuidance, ErrorGuidanceQueryPort } from "../../domain/ports/error-guidance-query-port.js";
22
+ import type { FullModeRequirementQueryPort } from "../../domain/ports/full-mode-requirement-query-port.js";
20
23
  import type {
21
24
  FullModeSessionQueryPort,
22
25
  FullModeSessionQueryResult,
23
26
  } from "../../domain/ports/full-mode-session-query-port.js";
24
- import type { FullModeRequirementQueryPort } from "../../domain/ports/full-mode-requirement-query-port.js";
25
27
  import type { PhaseGateQueryPort } from "../../domain/ports/phase-gate-query-port.js";
26
28
  import type { StoryReflectionQueryPort } from "../../domain/ports/story-reflection-query-port.js";
27
29
  import { AsyncHookToCliTranslator } from "../../domain/services/hook-to-cli-translator.js";
@@ -186,6 +188,7 @@ export class HandlePreToolUseUseCase {
186
188
  guidance,
187
189
  unitIdForGuidance,
188
190
  input.callerSkill,
191
+ sessionResult,
189
192
  );
190
193
  }
191
194
  } else {
@@ -282,21 +285,20 @@ export class HandlePreToolUseUseCase {
282
285
  guidance: ErrorGuidance | null,
283
286
  unitId: string | undefined,
284
287
  callerSkill?: string,
288
+ sessionResult?: FullModeSessionQueryResult,
285
289
  ): HandlePreToolUseOutput {
286
290
  const fp = blockedFilePath ?? "不明なファイル";
287
291
  if (result.dominantCategory === "config" && /(?:^|\/)phasegate\.config\.json$/.test(fp)) {
288
292
  const dryRunCommand = "phasegate config:plan --intent quick-mode-relax --dry-run --json";
289
293
  const applyCommand = "phasegate config:plan --intent quick-mode-relax --apply --json";
290
- const lines = [
291
- `Full mode 必須変更が検出されました: ${fp}`,
292
- "カテゴリ: config",
293
- ];
294
+ const lines = [`Full mode 必須変更が検出されました: ${fp}`, "カテゴリ: config"];
294
295
  if (result.rejectionRule) {
295
296
  lines.push(`判定ルール: ${result.rejectionRule}`);
296
297
  }
297
298
  if (result.rejectionReason) {
298
299
  lines.push(`理由: ${result.rejectionReason}`);
299
300
  }
301
+ HandlePreToolUseUseCase.appendJudgmentContextLines(lines, sessionResult);
300
302
  lines.push(`次のアクション: ${dryRunCommand} で差分を確認し、承認後に ${applyCommand} を実行してください。`);
301
303
 
302
304
  return {
@@ -309,22 +311,24 @@ export class HandlePreToolUseUseCase {
309
311
  nextAction: `${dryRunCommand} && ${applyCommand}`,
310
312
  };
311
313
  }
312
- if (
313
- callerSkill === "quick-implementor" &&
314
- result.dominantCategory !== undefined &&
315
- ["bugfix", "docs", "test", "config"].includes(result.dominantCategory)
316
- ) {
314
+ if (HandlePreToolUseUseCase.shouldGuideQuickModeRelax(result.dominantCategory, callerSkill)) {
317
315
  const dryRunCommand = "phasegate config:plan --intent quick-mode-relax --dry-run --json";
318
316
  const applyCommand = "phasegate config:plan --intent quick-mode-relax --apply --json";
319
317
  const lines: string[] = [`Full mode 必須変更が検出されました: ${fp}`];
320
- lines.push(`カテゴリ: ${result.dominantCategory}`);
318
+ if (result.dominantCategory) {
319
+ lines.push(`カテゴリ: ${result.dominantCategory}`);
320
+ }
321
321
  if (result.rejectionRule) {
322
322
  lines.push(`判定ルール: ${result.rejectionRule}`);
323
323
  }
324
324
  if (result.rejectionReason) {
325
325
  lines.push(`理由: ${result.rejectionReason}`);
326
326
  }
327
- lines.push(`次のアクション: Quick Mode の許可カテゴリを確認してください。緩和する場合は ${dryRunCommand} で差分を確認し、承認後に ${applyCommand} を実行してください。`);
327
+ HandlePreToolUseUseCase.appendJudgmentContextLines(lines, sessionResult);
328
+ lines.push(
329
+ `次のアクション: Quick Mode の許可カテゴリを確認してください。緩和する場合は ${dryRunCommand} で差分を確認し、承認後に ${applyCommand} を実行してください。`,
330
+ );
331
+ lines.push(` 分類の確認: npx phasegate check-change-category --paths ${fp}`);
328
332
 
329
333
  return {
330
334
  shouldBlock: true,
@@ -346,9 +350,15 @@ export class HandlePreToolUseUseCase {
346
350
  if (result.rejectionReason) {
347
351
  lines.push(`理由: ${result.rejectionReason}`);
348
352
  }
353
+ HandlePreToolUseUseCase.appendJudgmentContextLines(lines, sessionResult);
349
354
  const suggestedSkill = guidance?.suggestedSkill ?? "/story-implementor";
350
355
  lines.push(`次のアクション: ${suggestedSkill} スキルを使用して設計フェーズから開始してください。`);
351
356
  if (unitId !== undefined && unitId !== "") {
357
+ if (HandlePreToolUseUseCase.isSessionActiveButRejected(sessionResult)) {
358
+ lines.push(
359
+ ` 既存 session は上記理由で今回の書き込みに使えません。張り直す場合: phasegate session end --work-item ${sessionResult?.workItemId ?? "<WI-XXX>"} を実行してから下記を実行してください。`,
360
+ );
361
+ }
352
362
  lines.push(
353
363
  ` 実装フェーズ開始時: phasegate session begin --mode full --unit ${unitId} --work-item <WI-XXX> --reason "<reason>" --duration 1h`,
354
364
  );
@@ -367,6 +377,56 @@ export class HandlePreToolUseUseCase {
367
377
  };
368
378
  }
369
379
 
380
+ /**
381
+ * Quick Mode スコープのカテゴリ(bugfix / docs / test / config)は quick-implementor で
382
+ * 完遂できる変更であり、遮断の実体は「allowedCategories が絞られている」ことである。
383
+ * ここで /story-implementor を案内すると、設計フェーズからやり直せという誤った指示になる。
384
+ *
385
+ * WI-354: 従来は callerSkill === "quick-implementor" を条件にしていたが、
386
+ * callerSkill を供給する producer(hook input の caller_skill / PHASEGATE_CALLER_SKILL)は
387
+ * 実運用で設定されず、この分岐は到達不能だった。カテゴリを一次条件にして
388
+ * skill context なしでも実用的な復旧手順を出す。
389
+ * feature / domain / api は従来どおり /story-implementor 誘導を維持する。
390
+ */
391
+ private static readonly QUICK_MODE_SCOPE_CATEGORIES: readonly string[] = ["bugfix", "docs", "test", "config"];
392
+
393
+ private static shouldGuideQuickModeRelax(dominantCategory: string | undefined, callerSkill?: string): boolean {
394
+ if (dominantCategory !== undefined) {
395
+ return HandlePreToolUseUseCase.QUICK_MODE_SCOPE_CATEGORIES.includes(dominantCategory);
396
+ }
397
+ // カテゴリ不明時は skill context だけが手掛かり
398
+ return callerSkill === "quick-implementor";
399
+ }
400
+
401
+ /**
402
+ * WI-349: ブロック理由に判定根拠を明示する。
403
+ *
404
+ * - 判定対象は「今回の書き込み対象パス」だけであり、ワークツリー上の未コミット変更は
405
+ * 一切含まれない。この誤解が「無関係な変更のせいでブロックされている」という
406
+ * 誤った原因究明を招いていた(issue #41 症状②)。
407
+ * - アクティブな Full Mode session がありながら不許可だった場合は、その理由
408
+ * (unit 不一致 / category 不一致 / 期限切れ 等)を提示する。これがないと
409
+ * 「session begin せよ」と案内されながら session は既に有効、という最悪の混乱になる。
410
+ */
411
+ private static appendJudgmentContextLines(
412
+ lines: string[],
413
+ sessionResult: FullModeSessionQueryResult | undefined,
414
+ ): void {
415
+ lines.push("判定対象: 今回の書き込み対象パスのみです(ワークツリー上の他の未コミット変更は判定に含まれません)。");
416
+ if (!HandlePreToolUseUseCase.isSessionActiveButRejected(sessionResult)) {
417
+ return;
418
+ }
419
+ const workItemId = sessionResult?.workItemId ?? "<unknown>";
420
+ const unit = sessionResult?.unit ?? "<unknown>";
421
+ const expiresAt = sessionResult?.expiresAt ?? "<unknown>";
422
+ lines.push(`アクティブな Full Mode session: ${workItemId} (unit=${unit}, expiresAt=${expiresAt})`);
423
+ lines.push(`session が書き込みを許可しなかった理由: ${sessionResult?.reason ?? "<unknown>"}`);
424
+ }
425
+
426
+ private static isSessionActiveButRejected(sessionResult: FullModeSessionQueryResult | undefined): boolean {
427
+ return sessionResult !== undefined && sessionResult.active && !sessionResult.allowed;
428
+ }
429
+
370
430
  private static appendGuidanceLines(lines: string[], guidance: ErrorGuidance | null, unitId?: string): void {
371
431
  if (guidance === null) return;
372
432
  if (guidance.scaffoldCommand !== null) {
@@ -377,10 +437,25 @@ export class HandlePreToolUseUseCase {
377
437
  lines.push(` scaffold: ${command}`);
378
438
  }
379
439
  if (guidance.templatePath !== null) {
380
- lines.push(` テンプレ: ${guidance.templatePath}`);
440
+ lines.push(` 構成リファレンス: ${HandlePreToolUseUseCase.describeStructureReference(guidance.templatePath)}`);
381
441
  }
382
442
  }
383
443
 
444
+ /**
445
+ * WI-356 (issue #29): 参照先が skill 定義(skills/<name>/SKILL.md)の場合、
446
+ * skills/ をユーザー repo に配置しない personal install 経路でも読めるよう
447
+ * stdout 経路(phasegate skills info)を併記する。
448
+ */
449
+ private static readonly SKILL_REFERENCE_PATTERN = /^skills\/([^/]+)\/SKILL\.md$/;
450
+
451
+ private static describeStructureReference(templatePath: string): string {
452
+ const skillMatch = HandlePreToolUseUseCase.SKILL_REFERENCE_PATTERN.exec(templatePath);
453
+ if (skillMatch === null) {
454
+ return templatePath;
455
+ }
456
+ return `${templatePath}(未配置なら: npx phasegate skills info ${skillMatch[1]})`;
457
+ }
458
+
384
459
  private deriveUnitIdFromPaths(targetFilePaths: readonly string[]): string | undefined {
385
460
  const projectPaths = this.configQueryPort.getProjectPaths();
386
461
  for (const targetFilePath of targetFilePaths) {
@@ -419,8 +494,15 @@ export class HandlePreToolUseUseCase {
419
494
  }
420
495
 
421
496
  private static isUnderInception(targetFilePath: string, inceptionPath: string): boolean {
422
- const normalizedTarget = targetFilePath.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
423
- const normalizedBase = inceptionPath.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/").replace(/\/$/, "");
497
+ const normalizedTarget = targetFilePath
498
+ .replaceAll("\\", "/")
499
+ .replace(/^\.\/+/, "")
500
+ .replace(/\/+/g, "/");
501
+ const normalizedBase = inceptionPath
502
+ .replaceAll("\\", "/")
503
+ .replace(/^\.\/+/, "")
504
+ .replace(/\/+/g, "/")
505
+ .replace(/\/$/, "");
424
506
  return normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`);
425
507
  }
426
508
 
@@ -577,9 +659,7 @@ export class HandlePreToolUseUseCase {
577
659
  lines.push("");
578
660
  lines.push("修正方法:");
579
661
  lines.push(" 1. cascade-updater を実行して product 文書を更新");
580
- lines.push(
581
- ` 2. または手動で該当 product 文書に @${annotationKey} ${details?.storyId ?? "<WORK-ITEM-ID>"} を追加`,
582
- );
662
+ lines.push(` 2. または手動で該当 product 文書に @${annotationKey} ${details?.storyId ?? "<WORK-ITEM-ID>"} を追加`);
583
663
  lines.push("");
584
664
  lines.push("参照: ADR-XXX");
585
665
 
@@ -140,6 +140,28 @@ function getCommandName(tokens: Token[]): string | undefined {
140
140
  return undefined;
141
141
  }
142
142
 
143
+ /** リダイレクト演算子の右辺を解釈し、fd 複製の場合は書き込み先なしとして扱う。 */
144
+ function getRedirectTarget(tokens: Token[], redirectIndex: number): string | undefined {
145
+ const next = tokens[redirectIndex + 1];
146
+ if (next === undefined) return undefined;
147
+
148
+ if (next.quoted === 'none' && /^&\d+$/.test(next.value)) {
149
+ return undefined;
150
+ }
151
+
152
+ if (next.quoted === 'none' && next.value === '&') {
153
+ const afterAmpersand = tokens[redirectIndex + 2];
154
+ if (afterAmpersand === undefined) return undefined;
155
+ if (afterAmpersand.quoted === 'none' && /^\d+$/.test(afterAmpersand.value)) {
156
+ return undefined;
157
+ }
158
+ // `>& file` は csh 形式の実ファイル書き込みとして安全側で抽出する。
159
+ return afterAmpersand.value;
160
+ }
161
+
162
+ return next.value;
163
+ }
164
+
143
165
  /**
144
166
  * 1 コマンド分のトークン列から書き込み先を抽出する。
145
167
  * リダイレクト先はコマンド種別によらず検出する (全コマンド共通)。
@@ -151,9 +173,9 @@ function extractFromSingleCommand(tokens: Token[]): string[] {
151
173
  for (let i = 0; i < tokens.length; i += 1) {
152
174
  const t = tokens[i];
153
175
  if (t.quoted === 'none' && (t.value === '>' || t.value === '>>' || t.value === '>|')) {
154
- const next = tokens[i + 1];
155
- if (next !== undefined) {
156
- results.push(next.value);
176
+ const target = getRedirectTarget(tokens, i);
177
+ if (target !== undefined) {
178
+ results.push(target);
157
179
  }
158
180
  }
159
181
  }