phasegate 0.315.0 → 0.335.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 (56) hide show
  1. package/docs/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
  2. package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
  3. package/docs/guide/cli-reference.md +89 -7
  4. package/docs/guide/configuration.md +43 -2
  5. package/docs/guide/quick-vs-full-mode.md +36 -1
  6. package/package.json +2 -2
  7. package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +5 -1
  8. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +16 -8
  9. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +190 -2
  10. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +5 -0
  11. package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
  12. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +6 -3
  13. package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
  14. package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
  15. package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
  16. package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
  17. package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
  18. package/scripts/harness/ci-governance/composition-root.ts +55 -3
  19. package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
  20. package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
  21. package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
  22. package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
  23. package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
  24. package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
  25. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
  26. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
  27. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
  28. package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
  29. package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
  30. package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
  31. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -2
  32. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -2
  33. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -2
  34. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
  35. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
  36. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
  37. package/scripts/harness/main.ts +102 -12
  38. package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
  39. package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
  40. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +4 -1
  41. package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
  42. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +46 -17
  43. package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
  44. package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
  45. package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
  46. package/scripts/harness/quick-mode/index.ts +7 -1
  47. package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
  48. package/skills/product-architect/SKILL.md +19 -0
  49. package/skills/story-mapper/SKILL.md +11 -0
  50. package/skills/story-writer/SKILL.md +11 -0
  51. package/skills/unit-designer/SKILL.md +11 -0
  52. package/templates/product_overview.template.md +85 -0
  53. package/templates/product_overview_plan.template.md +55 -0
  54. package/templates/story_mapping_plan.template.md +61 -0
  55. package/templates/story_writer_plan.template.md +61 -0
  56. package/templates/unit_design_plan.template.md +63 -0
@@ -0,0 +1,74 @@
1
+ ---
2
+ adr_id: "039"
3
+ title: "hook の authorization 単位は観測可能な state とし、skill 名を伝播しない"
4
+ status: Accepted
5
+ date: 2026-08-06
6
+ ---
7
+
8
+ # hook の authorization 単位は観測可能な state とし、skill 名を伝播しない
9
+
10
+ <!-- @work-item-id WI-375 -->
11
+ <!-- @work-item-id WI-376 -->
12
+
13
+ ## Context
14
+
15
+ pre-tool-use hook には「呼び出し元 skill 名」を受け取る口が 2 つ実装されていた。
16
+
17
+ - hook input JSON の `caller_skill` フィールド(`scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts` の `PreToolUseHookInput`)
18
+ - 環境変数 `PHASEGATE_CALLER_SKILL`(同ファイルの `input.caller_skill ?? process.env.PHASEGATE_CALLER_SKILL`)
19
+
20
+ WI-202 はこれを「block guidance の出し分けにのみ使う optional context」として設計した。しかし **値を供給する producer はどこにも存在しない**。
21
+
22
+ 1. **Claude Code の PreToolUse payload に skill 情報が無い。** hook に渡る JSON は `session_id` / `transcript_path` / `cwd` / `hook_event_name` / `tool_name` / `tool_input` / `permission_mode` であり、実行中の skill を示すフィールドは規定されていない。`caller_skill` は phasegate が独自に期待しているだけの未定義キーである。
23
+ 2. **配布物の `.claude/settings.json` にも `.codex/hooks.json` にも `PHASEGATE_CALLER_SKILL` を設定する env 宣言が無い。** skill 側(`skills/*/SKILL.md`)にも export する手順は書かれていない。
24
+ 3. 結果として `callerSkill` は常に `undefined` で hook に届き、`callerSkill === "quick-implementor"` の分岐は **到達不能なデッドパス**だった。それでも `HandlePreToolUseUseCase` を直接 new して `callerSkill: "quick-implementor"` を渡す統合テストは緑になっており、「実運用では一度も通らない経路が、テスト上は被覆されている」という**偽の被覆**を生んでいた(GitHub #27 Defect C / #44 課題 1)。
25
+
26
+ WI-354 で guidance 分岐の一次条件を `dominantCategory`(Quick Mode の分類結果)に変更したため実害は解消済みだが、「カテゴリ未確定時のみ `callerSkill` を見る」フォールバックと受け口自体は残っていた。
27
+
28
+ さらに本質的な問題として、**skill 名は仮に producer を作っても認証されない自己申告値である**。hook を呼ぶのはエージェント自身であり、`caller_skill: "quick-implementor"` を名乗ることは誰にでもできる。ADR-030(injection threat model)の trust root の考え方に照らせば、エージェントが自由に制御できる文字列は防御判定の入力にできない。GitHub #26 で「skill 名で full mode を許可する」案を退け、`phasegate config:plan --apply` のような **managed command 経路**(phasegate 自身が実行し、その痕跡をファイルとして残す経路)に倒した結論と同根である。
29
+
30
+ ## Decision
31
+
32
+ ### 1. authorization / guidance の入力は「hook が自ら観測・検証できる state」に限る
33
+
34
+ pre-tool-use hook の判定と案内は、以下の観測可能な state のみを入力とする。
35
+
36
+ | state | 実体 | 観測方法 |
37
+ |-------|------|---------|
38
+ | 書き込み対象パスと変更カテゴリ | `tool_input` から抽出した対象パス、Quick Mode の `dominantCategory` | hook 自身が分類(`quick-mode` unit) |
39
+ | Full Mode session marker | `phasegate session begin --mode full --unit … --work-item …` が作るセッションファイル(WI / unit / 期限) | `FileSystemFullModeSessionQueryAdapter` がファイルを読む |
40
+ | 設計文書の存在 | `docs/product/construction/{unit}/logical_design.md` / `domain_model.md` | `PhaseGateQueryAdapter.checkDesignDocsExist` |
41
+ | WI の承認・reflection 状態 | inception 配下の WI 文書、story reflection | 各 query adapter がファイルを読む |
42
+ | 解決済み config | `phasegate.config.json` + 防御プリセット解決結果 | config-foundation 経由 |
43
+ | baseline / attestation | `.phasegate/baseline.json`、attestation マニフェスト | ci-governance / attestation unit |
44
+
45
+ これらはすべて「phasegate 自身の managed command が書いた痕跡」または「リポジトリの実ファイル」であり、hook 実行時点で独立に再検証できる。
46
+
47
+ ### 2. エージェントの自己申告 identity は受け取らない
48
+
49
+ skill 名・エージェント種別のような自己申告値は、**authorization にも guidance 分岐にも使わない**。よって受け口を削除する。
50
+
51
+ - `PreToolUseHookInput.caller_skill` フィールドを削除する
52
+ - 環境変数 `PHASEGATE_CALLER_SKILL` の参照を削除する
53
+ - `HandlePreToolUseInput.callerSkill` を削除する
54
+ - `shouldGuideQuickModeRelax` の `callerSkill === "quick-implementor"` フォールバックを削除する(判定は `dominantCategory` のみ)
55
+ - `callerSkill` を注入するだけで成立していた統合テストを削除し、代わりに **category ベースの分岐が WI-354 の挙動のまま不変であること**を回帰テストで固定する
56
+
57
+ 「使われていない口を念のため残す」ことは、偽の被覆を生み、将来「値さえ入れれば許可が広がる」という誤った拡張余地を残すため採らない。
58
+
59
+ ### 3. skill context が必要になった場合の唯一の入口は managed command
60
+
61
+ 将来「どの skill が書いているか」に応じて挙動を変える必要が生じた場合も、hook input に新しい自己申告フィールドを足すことはしない。skill 側に `phasegate session begin` 等の **managed command を実行させ、その結果として生まれた検証可能な state**(session marker のフィールド)を hook が読む方式のみを採る。この経路では phasegate が引数を検証し、期限・unit・WI を自ら記録するため、値の出所が hook から追跡できる。
62
+
63
+ ## Consequences
64
+
65
+ - pre-tool-use hook の入力契約から `caller_skill` が消える。未定義キーを送っていた呼び出し元があっても、hook は追加キーを無視するため互換性は壊れない(そもそも producer が存在しない)。
66
+ - Full Mode ブロック時の復旧案内は `dominantCategory` のみから決まる。`bugfix` / `docs` / `test` / `config` は quick-mode-relax 案内、`feature` / `domain` / `api` および**カテゴリ未確定**は `/story-implementor` 案内という WI-354 の挙動が唯一の仕様となる(カテゴリ未確定時に skill 名で分岐が変わる可能性が消える)。
67
+ - 「テストは緑だが実運用では到達しない」経路が 1 件減る。今後 hook の分岐を追加する際は、入力が上表の observable state に由来するかを ADR の基準として確認する。
68
+ - 経緯: WI-202(受け口の追加)→ WI-206(skill 名を許可条件にしない方針の調査)→ #26(managed command 経路への結論)→ WI-354(guidance の category ベース化)→ 本 ADR(受け口の削除と原則の明文化)。
69
+
70
+ ## Alternatives
71
+
72
+ 1. **producer を実装する(`.claude/settings.json` の env に `PHASEGATE_CALLER_SKILL` を埋める / skill 側で export させる)。** 値はエージェントが自由に設定できる自己申告であり、authorization の入力としては防御にならない。guidance 専用に限定しても、配布物の env 設定はユーザーの settings 編集に依存し、欠落時に静かに分岐が消えるため、category ベース判定より劣る。
73
+ 2. **現状維持(受け口を残したまま到達不能分岐を放置)。** デッドパスと偽の被覆が残り、`callerSkill` を「渡せば許可が変わるフック」と誤解した拡張を招く。
74
+ 3. **Claude Code 側の payload 拡張を待つ。** phasegate は AI 非依存(ADR-006)を掲げており、特定エージェントの payload 拡張に防御設計を依存させない。
@@ -0,0 +1,66 @@
1
+ ---
2
+ adr_id: "040"
3
+ title: "Quick Mode の実効設定を防御プリセット解決経由で決定する"
4
+ status: Accepted
5
+ date: 2026-08-06
6
+ ---
7
+
8
+ # Quick Mode の実効設定を防御プリセット解決経由で決定する
9
+
10
+ <!-- @work-item-id WI-375 -->
11
+ <!-- @work-item-id WI-377 -->
12
+ <!-- @work-item-id WI-378 -->
13
+
14
+ ## Context
15
+
16
+ ADR-007 は `phasegate.config.json` を品質設定の Single Source of Truth と定め、防御プリセット(`minimal` / `standard` / `strict`)の解決は config-foundation の `PresetResolutionService` + `PresetDefinitionStore` が担う。ところが Quick Mode の実効経路である `HarnessConfigQuickModeConfigAdapter`(`scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts`)は、**`phasegate.config.json` を `fs.readFile` + `JSON.parse` で直接読み、preset 解決を経由していなかった**。
17
+
18
+ その結果:
19
+
20
+ - `scripts/harness/config-foundation/infrastructure/presets/*.json` の `quickMode` セクションは**どこからも読まれないデッド宣言**だった。
21
+ - 実効既定値は adapter 内のハードコード定数 `DEFAULT_QUICK_MODE_CONFIG` にあり、宣言(presets)と実効値(adapter)が二重管理されていた。
22
+ - WI-353 で `allowedCategories` の乖離(presets が `["bugfix"]`、実効値が 4 カテゴリ)を是正し契約テストで再発を検知できるようにしたが、`maintainedLayers` / `relaxedGates` は依然乖離していた(presets: `["L1","L2"]` / `[]`、実効値かつ `docs/guide/configuration.md` の記載: `["L1","L2-002","L2-003","L2-014","L3-001"]` / `["L2-001","L3-002","L3-003","L3-004","L4"]`)。
23
+ - 「防御プリセットごとに Quick Mode の強度を変える」(例: `strict` では `allowedCategories` を絞る)という設計は、経路が繋がっていないため実現不能だった。
24
+
25
+ GitHub #44 課題 2。**既存プロジェクトの実効挙動を変えない移行**が絶対条件である(#27 の再発防止)。
26
+
27
+ ## Decision
28
+
29
+ ### 1. Quick Mode 設定は preset 解決結果(resolved config)から決定する
30
+
31
+ `HarnessConfigQuickModeConfigAdapter` は `PresetDefinitionStore` + `PresetResolutionService` を使い、`project.preset` に対応する preset 定義と `phasegate.config.json` の `quickMode` を config-foundation と同一の merge 規則(`deepMerge`: 配列はキー単位で全置換、未宣言キーは preset 値を継承)で解決した結果を使う。
32
+
33
+ これにより presets の `quickMode` 宣言が実際に読まれ、防御プリセットごとに Quick Mode 強度を変える設計が有効化される。
34
+
35
+ ### 2. 移行時は挙動不変を絶対条件とし、presets の宣言値を実効既定値に揃える
36
+
37
+ 経路の載せ替えだけを行うと、`quickMode` を書いていない既存プロジェクトの `maintainedLayers` / `relaxedGates` が preset の宣言値(`["L1","L2"]` / `[]`)に変わってしまう。これは「Quick Mode で L2-001 phase-gate が維持され、L3-001 が維持されなくなる」という実挙動の変化であり、許容しない。
38
+
39
+ したがって **3 プリセットの `quickMode` 宣言を実効既定値(= `docs/guide/configuration.md` が既定として記載している値)に揃える**。WI-353 が `allowedCategories` に対して行った是正を、`maintainedLayers` / `relaxedGates` にも適用する形になる。逆方向(実効値を preset 宣言に合わせる)は既存利用者への破壊的変更であり採らない。
40
+
41
+ 防御プリセットごとの Quick Mode 強度差(`strict` で絞る等)は**本 ADR では導入しない**。経路を繋ぐ変更と挙動を変える変更を同一コミットに混ぜないためである。今後差をつける場合は presets の該当ファイルのみを変更すれば実効値に反映される。
42
+
43
+ ### 3. 解決不能時は従来どおりの既定値に fail-open する
44
+
45
+ `project.preset` が未知・未宣言、または preset 解決が例外を投げる config(他セクションが壊れている等)では、従来と同じく adapter 内の既定値 + raw `quickMode` の per-key フォールバックで動作する。ADR-038 §3-1 の「hook は config がどんな状態でも完走する」原則を維持し、preset 解決の導入によって**新たな遮断経路を作らない**。
46
+
47
+ `HarnessConfigNotFoundError`(ENOENT)と `HarnessConfigParseError`(JSON 不正)は adapter の公開契約として維持する。
48
+
49
+ ### 4. WI-353 の契約テストは「宣言が実際に読まれること」の検証に昇格させる
50
+
51
+ WI-353 のテストは「preset 宣言値 == adapter のハードコード既定値」という**二重管理の整合**を検査していた。経路が繋がった後は、`quickMode` を持たない config に対する adapter の実効値が preset 宣言値そのものであることを 3 プリセット分検証する形に置き換える。これによりデッド宣言の解消自体が回帰テストで固定される。
52
+
53
+ あわせて「防御プリセット × `quickMode` キー有無 × 明示 override 有無」のマトリクス回帰テストで挙動不変を固定する。
54
+
55
+ ## Consequences
56
+
57
+ - presets の `quickMode` が実効値の所在(single source)になり、adapter 内の既定値は preset 解決不能時の fail-open 用フォールバックという位置づけに縮小する。
58
+ - `phasegate` が出力する解決済み config の `quickMode` と、Quick Mode 判定が実際に使う値が一致する(従来は前者だけが preset 由来で、後者と食い違っていた)。
59
+ - 挙動不変の担保はテストで行う。preset 宣言を変更すると Quick Mode の実挙動が変わるため、presets の `quickMode` 変更は破壊的変更として扱う。
60
+ - 経緯: WI-353(`allowedCategories` の宣言値是正・契約テスト)→ 本 ADR / WI-377(経路の載せ替えと残り 2 キーの是正)→ WI-378(契約テストの昇格)。
61
+
62
+ ## Alternatives
63
+
64
+ 1. **adapter の raw 読みを維持し、presets の `quickMode` セクションを削除する。** デッド宣言は消えるが、防御プリセットごとに Quick Mode 強度を変える設計余地を永久に失い、解決済み config から `quickMode` が消えて ADR-007 の Single Source of Truth と矛盾する。
65
+ 2. **経路を載せ替え、実効値を preset 宣言(`["L1","L2"]` / `[]`)に合わせる。** 既存プロジェクトの Quick Mode 緩和範囲が無告知で変わる破壊的変更であり、#27 と同種の事故を再発させる。
66
+ 3. **`LoadResolvedConfigUseCase` をそのまま呼ぶ(AJV schema 検証込みの完全経路)。** schema 違反 config で Quick Mode 判定が新たに例外を投げるようになり、ADR-038 の fail-open 表を変更してしまう。本 ADR は domain service(`PresetResolutionService`)と preset 定義のみを利用し、検証の厳格度は変えない。
@@ -127,7 +127,7 @@ npx phasegate baseline --paths "scripts/harness/**/*.ts,src/**/*.ts"
127
127
 
128
128
  | Command | Options | Description |
129
129
  |---|---|---|
130
- | `scaffold-design` | `--unit <id>` `--phase <logical\|domain\|uiux\|unit-test\|it-test>` `--force` `--json` | `templates/*.template.md` を読み取り `{{unit}}` を `--unit` 値で置換して `docs/product/construction/{unit}/*.md` を生成する。既存ファイルは `--force` なしでは保護(exit 2)。|
130
+ | `scaffold-design` | `--unit <id>` `--phase <logical\|domain\|uiux\|unit-test\|it-test>` `--force` `--json` | `templates/*.template.md` を読み取り `{{unit}}` を `--unit` 値で置換して `{paths.designDocs}/{unit}/*.md` を生成する。既存ファイルは `--force` なしでは保護(exit 2)。|
131
131
 
132
132
  ### `scaffold-design` の使い方
133
133
 
@@ -146,21 +146,103 @@ npx phasegate scaffold-design --unit harness-api --phase logical --force
146
146
  npx phasegate scaffold-design --unit harness-api --phase logical --json
147
147
  ```
148
148
 
149
- 生成先と対応テンプレ:
149
+ 生成先と対応テンプレ(生成先の `docs/product/construction` 部分は
150
+ `phasegate.config.json` の `paths.designDocs` に追従する。WI-369 以前は
151
+ ハードコードされており、`paths.designDocs` を移設した PJ では scaffold 先と
152
+ フェーズゲートの検査先がズレていた):
150
153
 
151
154
  | `--phase` | 生成先 | テンプレ |
152
155
  |---|---|---|
153
- | `logical` | `docs/product/construction/{unit}/logical_design.md` | `templates/logical_design.template.md` |
154
- | `domain` | `docs/product/construction/{unit}/domain_model.md` | `templates/domain_model.template.md` |
155
- | `uiux` | `docs/product/construction/{unit}/uiux_design.md` | `templates/uiux_design.template.md` |
156
- | `unit-test` | `docs/product/construction/{unit}/unit_test_design.md` | `templates/unit_test_design.template.md` |
157
- | `it-test` | `docs/product/construction/{unit}/it_test_design.md` | `templates/it_test_design.template.md` |
156
+ | `logical` | `{paths.designDocs}/{unit}/logical_design.md` | `templates/logical_design.template.md` |
157
+ | `domain` | `{paths.designDocs}/{unit}/domain_model.md` | `templates/domain_model.template.md` |
158
+ | `uiux` | `{paths.designDocs}/{unit}/uiux_design.md` | `templates/uiux_design.template.md` |
159
+ | `unit-test` | `{paths.designDocs}/{unit}/unit_test_design.md` | `templates/unit_test_design.template.md` |
160
+ | `it-test` | `{paths.designDocs}/{unit}/it_test_design.md` | `templates/it_test_design.template.md` |
158
161
 
159
162
  exit code は `0` = 生成成功 / 上書き成功、`2` = 既存ファイルあり(`--force` 無)
160
163
  または引数不正。
161
164
 
162
165
  ---
163
166
 
167
+ ## Scaffold Inception (Phase Gate Level-1 Templates)
168
+
169
+ | Command | Options | Description |
170
+ |---|---|---|
171
+ | `scaffold-inception` | `--kind <doc-kind>` `--dry-run`(既定)/ `--apply` `--force` `--json` | L2 の Level-1 フェーズゲートが要求する inception plan 文書と `product_overview.md` の雛形を生成する。既存ファイルは `--force` なしでは保護(exit 2)。|
172
+
173
+ WI-368(GitHub issue #42)で導入。`scaffold-design` は `--unit` を必須とする
174
+ construction 文書専用で、unit 軸を持たない `_shared/*_plan.md` や product 直下の
175
+ 文書を表現できないため、別コマンドとして分離している。
176
+
177
+ ```bash
178
+ # 何がどこに生成されるかを確認(既定は dry-run)
179
+ npx phasegate scaffold-inception --kind product-overview-plan
180
+
181
+ # 実際に書き込む
182
+ npx phasegate scaffold-inception --kind product-overview-plan --apply
183
+ npx phasegate scaffold-inception --kind product-overview --apply
184
+
185
+ # 既存ファイルを意図的に上書き
186
+ npx phasegate scaffold-inception --kind product-overview-plan --apply --force
187
+ ```
188
+
189
+ 生成先と対応テンプレ:
190
+
191
+ | `--kind` | 生成先 | テンプレ |
192
+ |---|---|---|
193
+ | `product-overview-plan` | `{paths.inceptionDocs}/_shared/product_overview_plan.md` | `templates/product_overview_plan.template.md` |
194
+ | `product-overview` | `{paths.designDocs}/../product_overview.md` | `templates/product_overview.template.md` |
195
+ | `story-writer-plan` | `{paths.inceptionDocs}/_shared/story_writer_plan.md` | `templates/story_writer_plan.template.md` |
196
+ | `story-mapping-plan` | `{paths.inceptionDocs}/_shared/story_mapping_plan.md` | `templates/story_mapping_plan.template.md` |
197
+ | `unit-design-plan` | `{paths.inceptionDocs}/_shared/unit_design_plan.md` | `templates/unit_design_plan.template.md` |
198
+
199
+ ### テンプレート = ゲート合格保証
200
+
201
+ plan テンプレートは QA セクション見出しを必ず含むため、`--apply` の生成物は
202
+ **無編集のまま** `planningMode: "interactive"`(既定)の Level-1 ゲートを通る。
203
+ この性質はラウンドトリップテストで機械的に検証されている。
204
+
205
+ `planningMode: "embedded-qa"` では `[Question]` と `[Answer]` の個数一致に加え
206
+ `[Answer]` に本文があることが要求されるため、生成直後の雛形は**通らない**。
207
+ これは意図した挙動であり、テンプレートが人間の承認証跡を偽造しないための境界。
208
+
209
+ ### スコープ外の doc-kind
210
+
211
+ `user_stories.md` / `user_story_mapping.md` / `units/{unit}_unit.md` /
212
+ `units/integration_contract.md` と、unit スコープの plan 文書
213
+ (`{unit}/domain_model_plan.md` 等、Level-2 ゲート)は未収録。段階投入とする。
214
+
215
+ ---
216
+
217
+ ## Templates (Bundled Template Access)
218
+
219
+ | Command | Options | Description |
220
+ |---|---|---|
221
+ | `templates list` | `--json` | 同梱テンプレート名を一覧表示する。|
222
+ | `templates show <name>` | — | テンプレート本文を stdout に出力する。|
223
+
224
+ WI-367(GitHub issue #42)で導入。consumer プロジェクトでは phasegate は
225
+ `node_modules/phasegate/templates/` に入り、エージェントの sandbox が
226
+ `node_modules` の Read を deny していると **テンプレート実体に到達できない**。
227
+ `skills list` / `skills info` と同型の stdout 経路でこれを解消する。
228
+
229
+ ```bash
230
+ npx phasegate templates list
231
+ npx phasegate templates show product_overview_plan
232
+
233
+ # 生成物としてそのまま保存できる(本文以外を混ぜない)
234
+ npx phasegate templates show logical_design > docs/product/construction/foo/logical_design.md
235
+ ```
236
+
237
+ `<name>` は同梱テンプレートの catalog(`templates/` の readdir 結果)と
238
+ **完全一致照合**する。ユーザー入力文字列がパス構成要素になる経路を持たないため、
239
+ `../../package.json` のようなパスは exit 2 で拒否され内容も出力されない。
240
+
241
+ exit code は `0` = 成功、`2` = name 未指定 / 不正な name / 未知の name /
242
+ 未知のサブコマンド。
243
+
244
+ ---
245
+
164
246
  ## Work Item Migration
165
247
 
166
248
  `docs/inception/` 配下の work item directory を **統一 `WI-XXX` レイアウト**へ移行する CLI。
@@ -41,7 +41,8 @@ The plan identifies target fields, managed artifacts, commands, validation, risk
41
41
  "mixedCategories": true,
42
42
  "newDomainFile": true,
43
43
  "apiContractChange": true
44
- }
44
+ },
45
+ "categoryOverrides": {} // Optional. e.g. { "docs": ["results/**", "notes/**"] }
45
46
  },
46
47
  "phaseDependencies": {
47
48
  "preset": "standard", // "full" | "standard" | "minimal" | "custom" ("default" -> "full")
@@ -198,10 +199,13 @@ The five layers are:
198
199
 
199
200
  | Sub-field | Type | Default | Description |
200
201
  |------------------------|------------|-----------------------------------------|-----------------------------------------------------------------------------|
201
- | `allowedCategories` | `string[]` | `["bugfix", "docs", "test", "config"]` | Change categories permitted under Quick Mode. Any category outside this list requires the full `story-implementor` workflow. |
202
+ | `allowedCategories` | `string[]` | `["bugfix", "docs", "test", "config"]` | Change categories permitted under Quick Mode. Any category outside this list requires the full `story-implementor` workflow. **Enum** — only `bugfix`, `docs`, `test`, `config`, `feature`, `domain`, `api` are accepted; anything else is a config error. |
202
203
  | `maintainedLayers` | `string[]` | `["L1", "L2-002", "L2-003", "L2-014", "L3-001"]` | Exact validator IDs that remain enforced in Quick Mode. `L1` is the only layer shorthand; `L2` is not expanded. |
203
204
  | `relaxedGates` | `string[]` | `["L2-001", "L3-002", "L3-003", "L3-004", "L4"]` | Validators/layers relaxed by Quick Mode. `L4` means all L4 validators are skipped. |
204
205
  | `fullModeRequiredWhen` | `object` | all flags `true` | Conditions that force a Quick Mode change to escalate to the full `/story-implementor` flow. See below. |
206
+ | `categoryOverrides` | `object` | `{}` | Maps a change category to a list of globs, so project-specific paths land in a category the built-in table cannot infer. See below. |
207
+
208
+ The defaults above come from the defense preset declared in `project.preset`; Quick Mode resolves its effective settings through the same preset resolution as the rest of the config (ADR-040). Sub-fields you declare in `phasegate.config.json` replace the preset value for that sub-field only (arrays are replaced wholesale, not merged); sub-fields you omit keep the preset value. All three shipped presets (`minimal` / `standard` / `strict`) currently declare the same Quick Mode values, so switching the defense preset does not change Quick Mode strength today. <!-- @work-item-id WI-377 -->
205
209
 
206
210
  ##### `fullModeRequiredWhen`
207
211
 
@@ -217,6 +221,43 @@ Introduced in ISSUE-006 Story A (v0.63.0) and wired into the pre-tool-use hook b
217
221
 
218
222
  Set a flag to `false` only when the project intentionally accepts the risk of merging that category of change without the design ceremony -- e.g. an early-stage prototype where new domain files are expected to churn.
219
223
 
224
+ ##### `allowedCategories` is an enum
225
+
226
+ <!-- @work-item-id WI-373 -->
227
+
228
+ `allowedCategories` accepts only the seven `ChangeCategory` values. A misspelling such as `"typoo"` is rejected as a config error rather than silently ignored, and case is not normalised — `"Docs"` is an error, not an alias for `"docs"`, because classification keys are always lowercase and a normalised spelling would only hide the same dead setting.
229
+
230
+ If the quickMode section is invalid, the pre-tool-use hook fails **closed**: writes are blocked with an explicit "quickMode 設定が不正なため" reason until the config is fixed. (A *missing* config still fails open, so greenfield adoption is unaffected.)
231
+
232
+ ##### `categoryOverrides`
233
+
234
+ <!-- @work-item-id WI-372 -->
235
+
236
+ ```jsonc
237
+ {
238
+ "quickMode": {
239
+ "categoryOverrides": {
240
+ "docs": ["results/**", "notes/**"]
241
+ }
242
+ }
243
+ }
244
+ ```
245
+
246
+ Keys are the seven `ChangeCategory` values; values are lists of globs matched against project-relative POSIX paths. Supported syntax is `**` (crosses `/`), `*` (does not cross `/`), and `?` (one non-`/` character); brace expansion and negation are not supported.
247
+
248
+ | Rule | Behaviour |
249
+ |------|-----------|
250
+ | Precedence | Overrides are evaluated **before** the built-in classification table, so an explicit declaration beats inference. |
251
+ | Downgrade guard | A path the built-in rules classify as `domain` or `api` cannot be moved to a lower-risk category; an override may only raise the risk. |
252
+ | Valid keys | All seven categories, including `domain` / `api` / `feature`. Assigning a path to those *hardens* the gate, since they sit outside the default `allowedCategories`. |
253
+ | Rejection rules | `NEW_DOMAIN` and `API_CONTRACT` remain path-based and ignore overrides entirely. Only `MIXED_CHANGES` uses the override-adjusted category. |
254
+ | Ties | If two categories match the same path, the higher-risk one wins, so the result never depends on JSON key order. |
255
+
256
+ Overrides apply identically to the pre-tool-use hook, `check-change-category`, and `ci-check --quick`. Leaving `categoryOverrides` unset reproduces the pre-existing classification exactly. See [Quick vs Full Mode](quick-vs-full-mode.md#overriding-the-table-for-project-specific-paths) for worked reasoning.
257
+
258
+ <!-- @work-item-id WI-380 -->
259
+ `categoryOverrides` goes through the same preset resolution as the other Quick Mode sub-fields (ADR-040), and merges one level deeper than they do: the merge unit is the **category key**, not the whole object. None of the three shipped presets declares `categoryOverrides`, so today whatever you write in `phasegate.config.json` is the effective value verbatim. If a preset does declare it, a category you also declare replaces that category's glob list wholesale, while categories you leave out keep the preset's globs.
260
+
220
261
  <!-- @work-item-id WI-159 -->
221
262
  Quick Mode uses exact validator IDs for `maintainedLayers`. To keep all L2 validators active, list `L2-001`, `L2-002`, `L2-003`, `L2-013`, `L2-014`, and `L2-015` explicitly. The default keeps metadata, test-quality, work-item status, and security checks active while skipping phase-gate, CLI E2E coverage, contract traceability coverage, performance, coverage, nyquist, and L4 scheduled validators.
222
263
 
@@ -77,6 +77,7 @@ The classifier is **path-based, not intent-based**. It never reads your commit m
77
77
 
78
78
  | Order | Match | Category |
79
79
  |---|---|---|
80
+ | 0 | `quickMode.categoryOverrides` glob match (see below) | the configured category |
80
81
  | 1 | `*.config.json`, `*.config.ts`, `phasegate.config.json` | `config` |
81
82
  | 2 | `.github/workflows/*.yml`, `.github/workflows/*.yaml` | `config` |
82
83
  | 3 | Repo-root bootstrap files: `.gitignore`, `.gitattributes`, `.editorconfig`, `.npmrc`, `.nvmrc`, `tsconfig.json`, `tsconfig.*.json`, anything under `.husky/` | `config` |
@@ -96,6 +97,38 @@ Two consequences worth internalising:
96
97
 
97
98
  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
99
 
100
+ ### Overriding the table for project-specific paths
101
+
102
+ <!-- @work-item-id WI-372 -->
103
+
104
+ The built-in table has no knowledge of your project's own conventions. A directory such as `results/` or `notes/` holds documents, but the classifier sees an unknown path: `MODIFY` falls through to `bugfix`, and `CREATE` falls through to `feature` — a category that cannot be added to `allowedCategories` at all, so the write is permanently Full Mode.
105
+
106
+ `quickMode.categoryOverrides` maps a category to a list of globs:
107
+
108
+ ```jsonc
109
+ {
110
+ "quickMode": {
111
+ "allowedCategories": ["bugfix", "docs", "test", "config"],
112
+ "categoryOverrides": {
113
+ "docs": ["results/**", "notes/**"]
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ Four rules govern how overrides interact with the table above:
120
+
121
+ 1. **Overrides are evaluated first — order 0 in the table.** They are an explicit declaration about your repository, so they beat inference. Without this, `notes/deploy.config.json` would still be caught by rule 1 and the setting would look broken.
122
+ 2. **Overrides cannot downgrade `domain` or `api`.** If the built-in rules classify a path as `domain` (rule 7) or `api` (rule 5), an override may only raise the risk, never lower it. `{"docs": ["scripts/**"]}` will *not* turn a domain file into a doc. The weakening vector is closed by construction, not by convention.
123
+ 3. **All seven categories are valid override keys**, including `domain`, `api`, and `feature`. Assigning a path to one of those only makes the gate harder to pass, since they sit outside the default `allowedCategories` — it is a hardening lever, e.g. `{"domain": ["packages/core/**"]}`.
124
+ 4. **`NEW_DOMAIN` and `API_CONTRACT` still apply.** The three rejection rules stay path-based and ignore overrides entirely, so a misconfigured override cannot let a new domain file or a port/adapter change slip through. Only `MIXED_CHANGES` consumes the override-adjusted category.
125
+
126
+ If two override categories match the same path, the higher-risk one wins, so the result never depends on JSON key order.
127
+
128
+ Supported glob syntax is deliberately small: `**` (crosses `/`), `*` (does not cross `/`), and `?` (one non-`/` character). Brace expansion and negation are not supported. Patterns are matched against project-relative POSIX paths.
129
+
130
+ Overrides apply identically to all three consumers — the PreToolUse hook, `check-change-category`, and `ci-check --quick` — because they share one config load. With `categoryOverrides` unset the classification is exactly what it was before the feature existed.
131
+
99
132
  ### Dry-running the classifier
100
133
 
101
134
  Use `check-change-category` to evaluate an arbitrary file list without actually starting an implementation:
@@ -193,7 +226,9 @@ Stop, commit nothing, and re-launch `/story-implementor`. Don't try to "finish i
193
226
  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.
194
227
 
195
228
  **Q. Can I add custom categories?**
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.
229
+ 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. The enum is enforced by both the config schema and `QuickModeConfig`, so a misspelling is a loud config error, not a setting that quietly never matches. <!-- @work-item-id WI-373 -->
230
+
231
+ What you *can* customise is which paths map to which of those seven categories — see [Overriding the table for project-specific paths](#overriding-the-table-for-project-specific-paths). If your workflow needs a category outside the enum, that is evidence the change is probably Full Mode material.
197
232
 
198
233
  ---
199
234
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.315.0",
3
+ "version": "0.335.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",
@@ -60,7 +60,7 @@
60
60
  "harness:check-phase": "pnpm phasegate:check-phase",
61
61
  "harness:check-ready": "pnpm phasegate:check-ready",
62
62
  "test": "vitest run --config scripts/harness/__tests__/vitest.config.forks.ts && vitest run --config scripts/harness/__tests__/vitest.config.ts",
63
- "coverage": "node -e \"require('node:fs').rmSync('coverage',{recursive:true,force:true})\" && vitest run --config scripts/harness/__tests__/vitest.config.coverage.forks.ts --reporter=blob --outputFile=coverage/.blob/forks.json && vitest run --config scripts/harness/__tests__/vitest.config.coverage.ts --reporter=blob --outputFile=coverage/.blob/threads.json && vitest run --config scripts/harness/__tests__/vitest.config.coverage.ts --merge-reports=coverage/.blob",
63
+ "coverage": "node -e \"require('node:fs').rmSync('coverage',{recursive:true,force:true})\" && vitest run --config scripts/harness/__tests__/vitest.config.coverage.forks.ts --reporter=blob --reporter=default --outputFile.blob=coverage/.blob/forks.json && vitest run --config scripts/harness/__tests__/vitest.config.coverage.ts --reporter=blob --reporter=default --outputFile.blob=coverage/.blob/threads.json && vitest run --config scripts/harness/__tests__/vitest.config.coverage.ts --merge-reports=coverage/.blob",
64
64
  "ci": "npm run coverage && npm run test"
65
65
  },
66
66
  "dependencies": {
@@ -3,10 +3,14 @@
3
3
  * @unit agent-integration
4
4
  */
5
5
 
6
+ /**
7
+ * WI-376 (ADR-039): 呼び出し元 skill 名を受け取るフィールドは持たない。
8
+ * 判定・案内の入力は hook が自ら観測・検証できる state(対象パス、変更カテゴリ、
9
+ * session marker、設計文書の存在、解決済み config 等)に限る。
10
+ */
6
11
  export interface HandlePreToolUseInput {
7
12
  toolName: string;
8
13
  targetFilePaths: string[];
9
- callerSkill?: string;
10
14
  targetChanges?: {
11
15
  filePath: string;
12
16
  beforeContent?: string | null;
@@ -8,6 +8,7 @@
8
8
  * @work-item-id WI-214
9
9
  * @work-item-id WI-349
10
10
  * @work-item-id WI-354
11
+ * @work-item-id WI-376
11
12
  *
12
13
  * HandlePreToolUseUseCase
13
14
  * PreToolUse Hook処理のオーケストレーション
@@ -187,7 +188,6 @@ export class HandlePreToolUseUseCase {
187
188
  fullModeResult,
188
189
  guidance,
189
190
  unitIdForGuidance,
190
- input.callerSkill,
191
191
  sessionResult,
192
192
  );
193
193
  }
@@ -284,7 +284,6 @@ export class HandlePreToolUseUseCase {
284
284
  },
285
285
  guidance: ErrorGuidance | null,
286
286
  unitId: string | undefined,
287
- callerSkill?: string,
288
287
  sessionResult?: FullModeSessionQueryResult,
289
288
  ): HandlePreToolUseOutput {
290
289
  const fp = blockedFilePath ?? "不明なファイル";
@@ -311,7 +310,7 @@ export class HandlePreToolUseUseCase {
311
310
  nextAction: `${dryRunCommand} && ${applyCommand}`,
312
311
  };
313
312
  }
314
- if (HandlePreToolUseUseCase.shouldGuideQuickModeRelax(result.dominantCategory, callerSkill)) {
313
+ if (HandlePreToolUseUseCase.shouldGuideQuickModeRelax(result.dominantCategory)) {
315
314
  const dryRunCommand = "phasegate config:plan --intent quick-mode-relax --dry-run --json";
316
315
  const applyCommand = "phasegate config:plan --intent quick-mode-relax --apply --json";
317
316
  const lines: string[] = [`Full mode 必須変更が検出されました: ${fp}`];
@@ -387,15 +386,18 @@ export class HandlePreToolUseUseCase {
387
386
  * 実運用で設定されず、この分岐は到達不能だった。カテゴリを一次条件にして
388
387
  * skill context なしでも実用的な復旧手順を出す。
389
388
  * feature / domain / api は従来どおり /story-implementor 誘導を維持する。
389
+ *
390
+ * WI-376 (ADR-039): 判定入力は hook が自ら観測できる state のみとする方針に従い、
391
+ * カテゴリ未確定時の callerSkill フォールバックを削除した。カテゴリが取れない場合は
392
+ * 一律 /story-implementor 誘導(従来のフォールバックも実運用では常にこの経路だった)。
390
393
  */
391
394
  private static readonly QUICK_MODE_SCOPE_CATEGORIES: readonly string[] = ["bugfix", "docs", "test", "config"];
392
395
 
393
- private static shouldGuideQuickModeRelax(dominantCategory: string | undefined, callerSkill?: string): boolean {
394
- if (dominantCategory !== undefined) {
395
- return HandlePreToolUseUseCase.QUICK_MODE_SCOPE_CATEGORIES.includes(dominantCategory);
396
+ private static shouldGuideQuickModeRelax(dominantCategory: string | undefined): boolean {
397
+ if (dominantCategory === undefined) {
398
+ return false;
396
399
  }
397
- // カテゴリ不明時は skill context だけが手掛かり
398
- return callerSkill === "quick-implementor";
400
+ return HandlePreToolUseUseCase.QUICK_MODE_SCOPE_CATEGORIES.includes(dominantCategory);
399
401
  }
400
402
 
401
403
  /**
@@ -575,6 +577,12 @@ export class HandlePreToolUseUseCase {
575
577
  message: (fp) =>
576
578
  `保護ファイルへの書き込みがブロックされました: ${fp}\nClaude Code の設定変更は /update-config スキルを使用してください。`,
577
579
  },
580
+ {
581
+ // WI-363: .husky/ 配下は L0 runtime の実施点。Quick Mode でも書き換えさせない。
582
+ pattern: /(?:^|\/)\.husky\//,
583
+ message: (fp) =>
584
+ `保護ファイルへの書き込みがブロックされました: ${fp}\n.husky/ 配下は L0 runtime(pre-commit / commit-msg / pre-push)の実施点であり、Quick Mode でも変更できません。\n未導入 hook の配置は npx phasegate setup:agent --apply --with-husky を使用してください。\n意図的に手編集する場合は phasegate.config.json の protectedFiles.exclude に対象パターンを明示してください。`,
585
+ },
578
586
  ];
579
587
 
580
588
  private async buildProtectedFileBlockOutput(blockedFilePath: string | undefined): Promise<HandlePreToolUseOutput> {