phasegate 0.120.0 → 0.122.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.122.0] - 2026-05-08
11
+
12
+ ### Added
13
+
14
+ - **WI-087 Phase C-2 — Stop hook strict mode (`agentIntegration.stopHook.enforce`)** — 外部レポーター nakataj-mti が GitHub Issue [#3](https://github.com/junpei-9898/phasegate/issues/3) finding #4 で報告した「`phasegate hook stop` が Complete Check 失敗時に exit 1 を返すが、Claude Code の Stop hook block には exit 2 が必要」問題を解消。新 config flag `agentIntegration.stopHook.enforce: boolean` (default `false`) を v3 schema に追加し、true セット時のみ Complete Check 失敗で **exit 2 + `{"decision":"block","reason":"Complete Check failed (exitCode=N)"}`** を出力する strict mode に切り替わる。default は従来挙動 (exit cliResult.exitCode、stderr メッセージのみ) を完全維持。
15
+ - **schema 拡張**: `scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json` に top-level `agentIntegration.stopHook.enforce: boolean` を追加 (`additionalProperties: false`)。boolean 以外 / 未定義 key は AJV validator が reject。
16
+ - **新ポートメソッド**: `ConfigQueryPort.getStopHookEnforce(): Promise<boolean>` を追加 (`scripts/harness/agent-integration/domain/ports/config-query-port.ts`)。
17
+ - **infrastructure 実装**: `HarnessConfigConfigQueryAdapter.getStopHookEnforce()` が `agentIntegration.stopHook.enforce === true` のときのみ true を返す strict 一致判定。
18
+ - **application 拡張**: `HandleStopOutput` DTO に `shouldEnforceFailure?: boolean` を追加。`HandleStopUseCase.execute` 内で `enforce && cliResult.exitCode !== 0` の合成条件を populate。reentry 検出時は populate されない (現行挙動保持)。
19
+ - **presentation 分岐拡張**: `stop-hook.ts` で `output.shouldEnforceFailure === true` のとき stdout に decision JSON 出力 + stderr に reason 付きメッセージ + exit 2。それ以外は現行通り cliResult.exitCode で抜ける。
20
+ - **テスト追加**: 11 ケース (`ajv-config-schema-validator-v3.test.ts` +5、`harness-config-config-query-adapter.test.ts` +2、`handle-stop-usecase.test.ts` +4)。全 3491 テスト (前回 3480 + 新規 11) グリーン。
21
+ - **dogfood 検証**: phasegate 自身の `phasegate.config.json` に `enforce: true` を一時注入し `stop-hook.ts` を手動実行 → exit 2 + decision JSON 出力を確認。enforce 削除後は exit 1 + decision JSON なしの後方互換挙動を確認。
22
+ - **既存テストへの影響**: `ConfigQueryPort` インターフェース拡張に伴い、`handle-pre-tool-use-usecase.test.ts` / `handle-post-tool-use-usecase.test.ts` / `handle-stop-usecase.test.ts` / `ci-governance-baseline-grandfather-adapter.test.ts` の mock literal に `getStopHookEnforce: vi.fn().mockResolvedValue(false)` を追加 (default 後方互換を維持しつつ TypeScript 型契約を満たす)。
23
+ - **ドキュメント整合**: `docs/guide/configuration.md` に `agentIntegration` セクションを追加 (sub-field 表 + 用途説明)、`docs/guide/hooks-integration.md` の Stop hook 説明に enforce オプションを追記。
24
+ - **互換性**: 既存 v2/v3 config (agentIntegration セクション不在) は default false で v0.121.0 以前と完全同一挙動。schema validator 経路 / ConfigQueryPort 既存メソッド経路への影響なし。
25
+
26
+ ## [0.121.0] - 2026-05-07
27
+
28
+ ### Fixed
29
+
30
+ - **WI-087 finding #3 + WI-086 docs: PreToolUse の Quick Mode 通過時の visibility 改善 + hook 責務分離をドキュメント化** — 外部レポーター nakataj-mti / junpei-9898 が GitHub Issue [#3](https://github.com/junpei-9898/phasegate/issues/3) finding #3 / [#2](https://github.com/junpei-9898/phasegate/issues/2) で報告した「Quick Mode で write が allow されるとき hook が完全 silent で、初見ユーザーが "hook が走っていない" と誤認する」問題と「pre-tool-use が L1 lint をしないことが暗黙仕様化していてレポーター期待と齟齬している」問題を解消。
31
+ - **Quick Mode visibility notice** (`scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts`): `HandlePreToolUseOutput.quickModeAllowed` がセットされた場合、stderr に `phasegate: write allowed (Quick Mode, category=<dominantCategory>)` を出力。exit code は 0 維持で **semantics は不変**、可視性のみ向上。
32
+ - **DTO 拡張** (`scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts`): `quickModeAllowed?: { dominantCategory?: string }` を追加。`HandlePreToolUseUseCase.execute` 内で `WRITE_TOOLS + fullModeRequirementQueryPort + requiresFullMode=false` の経路で populate。block 経路や WRITE_TOOLS 外 (`Bash` 等) では出力されず、後方互換維持。
33
+ - **テスト追加** (`scripts/harness/__tests__/integration/agent-integration/handle-pre-tool-use-usecase.test.ts`): 4 ケース — `quickModeAllowed` が `dominantCategory` 付きで返る / `dominantCategory` 未設定で返る / `fullModeRequirementQueryPort` 未指定時は出力なし / WRITE_TOOLS 外 (`Bash`) では出力なし。全 49 ケース (前回 45 + 新規 4) グリーン。
34
+ - **責務分離のドキュメント明文化** (`docs/guide/hooks-integration.md`): "Responsibility Separation" セクションを冒頭に追加し、pre = フェーズゲート / post = lint / Stop = complete-check の役割分担表と「pre-tool-use は意図的に lint を実行しない (lint は書き込み後の content が必要)」旨を明記。WI-086 で指摘された「`pre-tool-use` で違反 Write を exit 2 でブロックしてほしい」期待が現行設計と乖離する理由を ユーザー視点で説明。
35
+ - **互換性**: 既存テスト全てグリーン(`expect(actual).toEqual({ shouldBlock: false })` 形式の既存 assert は `quickModeAllowed: undefined` を含む結果でも一致)。`HandlePreToolUseOutput` への field 追加は外部 API ではなく application/presentation 内部 DTO のみで、外部消費者なし。
36
+ - **スコープ外** (Phase C-2 で対応予定): WI-087 finding #4 — Stop hook `--enforce` flag (`agentIntegration.stopHook.enforce` config 追加で Complete Check 失敗時に exit 2 + decision JSON `"deny"` を返す strict mode)。config schema 拡張のため story-implementor で別リリース予定。
37
+
10
38
  ## [0.120.0] - 2026-05-07
11
39
 
12
40
  ### Fixed
@@ -416,6 +416,28 @@ Introduced in ISSUE-007 Wave 1 (v0.65.0) and wired into the pre-tool-use hook by
416
416
 
417
417
  Generate or refresh the snapshot with `npx phasegate baseline` (`--dry-run` to inspect, `--force` to overwrite, `--paths <glob,glob,...>` to scope, `--json` for CI-friendly output). See the [Baseline section in CLI Reference](cli-reference.md#baseline-retrofit-grandfather) for details.
418
418
 
419
+ #### `agentIntegration` (Stop hook strict mode)
420
+
421
+ Controls how phasegate's agent-side hooks integrate with Claude Code. Currently only `stopHook.enforce` is exposed.
422
+
423
+ ```jsonc
424
+ {
425
+ "agentIntegration": {
426
+ "stopHook": {
427
+ "enforce": true
428
+ }
429
+ }
430
+ }
431
+ ```
432
+
433
+ | Sub-field | Type | Default | Description |
434
+ |--------------------|-----------|---------|----------------------------------------------------------------------------------------------------------------------|
435
+ | `stopHook.enforce` | `boolean` | `false` | When `true`, a non-zero exit from `phasegate:complete-check` causes the Stop hook to emit `{"decision":"block","reason":"Complete Check failed (exitCode=N)"}` on stdout and exit with code 2, blocking Claude Code's turn. When `false` (default), the hook exits with the inner CLI's exit code, which Claude Code surfaces only as a transcript warning. |
436
+
437
+ Use `enforce: true` when your team treats Complete Check failures as hard gates (e.g., disallow ending a session with failing tests or lint). Leave it as default `false` for an opt-in / advisory experience.
438
+
439
+ Reentry-detection cases (`REENTRY_DETECTED`) always exit with code 0 regardless of this setting; strict mode applies only to actual Complete Check failures.
440
+
419
441
  ---
420
442
 
421
443
  ### Quick Mode
@@ -42,9 +42,27 @@ Add the following to `.claude/settings.json`:
42
42
 
43
43
  ## Hook Behaviors
44
44
 
45
+ ### Responsibility Separation (important)
46
+
47
+ Phasegate's hooks are **strictly partitioned by phase**:
48
+
49
+ | Hook | What it checks | What it does NOT check |
50
+ |------|----------------|------------------------|
51
+ | **PreToolUse** | Phase Gate, protected files, Quick Mode category gating, story-reflection | L1 lint (`@unit` / `@layer` / `any` abuse) — these are post-write concerns |
52
+ | **PostToolUse** | L1 Biome AST rules, formatter, type-check (via `analyze-errors-hook.sh`) | Phase Gate (already checked before write) |
53
+ | **Stop** | Complete-check (L2-L4 full validation suite) | Per-edit lint (already done by PostToolUse) |
54
+
55
+ This separation is intentional:
56
+ - **Pre = "should this write happen at all?"** — concerns the caller's intent and design state.
57
+ - **Post = "is what was written valid?"** — concerns the resulting code's quality.
58
+ - **Stop = "is the session ready to end?"** — concerns the cumulative state across the session.
59
+
60
+ If you expect L1 lint (e.g., missing `@unit` annotation) to **block** a Write before it happens, that is by design **not** the case. The PreToolUse hook intentionally does not run lint, because lint requires the resulting file content (which only exists after the write). Lint violations surface as **PostToolUse** decision JSON (`decision: "block"`) and trigger Claude Code to retry.
61
+
45
62
  ### PreToolUse (before file write)
46
63
  - Enforces Phase Gate: blocks writing to source files if required design documents don't exist
47
64
  - Blocks writes to protected files (package.json, biome.json, tsconfig.json)
65
+ - Quick Mode: when a write is allowed because the change is classified within `quickMode.allowedCategories`, an informational notice is emitted to stderr (`phasegate: write allowed (Quick Mode, category=<...>)`) — exit code remains 0
48
66
  - Returns actionable error messages with:
49
67
  - Violation reason
50
68
  - Missing artifacts
@@ -73,7 +91,8 @@ Use /quick-implementor skill for version changes in package.json.
73
91
 
74
92
  ### Stop (before session end)
75
93
  - Runs `phasegate:complete-check` (L2-L4 full validation)
76
- - Session end is held if checks are not all green
94
+ - By default, the hook exits with the inner CLI's exit code, which Claude Code shows as a transcript warning but does not turn-block on.
95
+ - Set `agentIntegration.stopHook.enforce: true` in `phasegate.config.json` to enable **strict mode**: on Complete Check failure, the hook emits `{"decision":"block","reason":"Complete Check failed (exitCode=N)"}` on stdout and exits with code 2, hard-blocking Claude Code's turn end. Reentry-detection still exits 0 regardless of this setting. See `docs/guide/configuration.md` `agentIntegration` section for details.
77
96
 
78
97
  ## Optional Shell Script Hooks
79
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.120.0",
3
+ "version": "0.122.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",
@@ -19,4 +19,9 @@ export interface HandlePreToolUseOutput {
19
19
  fullModeRejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
20
20
  fullModeDominantCategory?: string;
21
21
  nextAction?: string;
22
+ // Quick Mode が write を許可した際に dominant カテゴリを presentation 層に伝える。
23
+ // shouldBlock=false のときのみセットされる。WI-087 finding #3。
24
+ quickModeAllowed?: {
25
+ dominantCategory?: string;
26
+ };
22
27
  }
@@ -14,4 +14,5 @@ export interface HandleStopOutput {
14
14
  executed: boolean;
15
15
  skipReason?: SkipReason;
16
16
  cliResult?: CliExecutionResult;
17
+ shouldEnforceFailure?: boolean;
17
18
  }
@@ -134,6 +134,7 @@ export class HandlePreToolUseUseCase {
134
134
  }
135
135
  }
136
136
 
137
+ let quickModeAllowed: HandlePreToolUseOutput["quickModeAllowed"];
137
138
  if (
138
139
  HandlePreToolUseUseCase.WRITE_TOOLS.has(input.toolName) &&
139
140
  this.fullModeRequirementQueryPort !== undefined &&
@@ -157,29 +158,31 @@ export class HandlePreToolUseUseCase {
157
158
  unitIdForGuidance,
158
159
  );
159
160
  }
161
+ } else {
162
+ quickModeAllowed = { dominantCategory: fullModeResult.dominantCategory };
160
163
  }
161
164
  }
162
165
  }
163
166
 
164
167
  const scope = this.resolveStoryReflectionScope(input);
165
168
  if (scope === null || this.storyReflectionQueryPort === undefined) {
166
- return { shouldBlock: false };
169
+ return { shouldBlock: false, quickModeAllowed };
167
170
  }
168
171
 
169
172
  if (grandfather.allGrandfathered) {
170
173
  this.grandfatherLogger("story-reflection", input.targetFilePaths);
171
- return { shouldBlock: false };
174
+ return { shouldBlock: false, quickModeAllowed };
172
175
  }
173
176
 
174
177
  const unitId = scope.unitId;
175
178
  if (unitId === undefined) {
176
- return { shouldBlock: false };
179
+ return { shouldBlock: false, quickModeAllowed };
177
180
  }
178
181
 
179
182
  const reflectionResult = await this.storyReflectionQueryPort.checkReflection(unitId);
180
183
 
181
184
  if (reflectionResult.skipped || reflectionResult.passed) {
182
- return { shouldBlock: false };
185
+ return { shouldBlock: false, quickModeAllowed };
183
186
  }
184
187
 
185
188
  return HandlePreToolUseUseCase.buildStoryReflectionBlockOutput(
@@ -69,9 +69,13 @@ export class HandleStopUseCase {
69
69
 
70
70
  try {
71
71
  const cliResult = await this.cliExecutorPort.execute('phasegate:complete-check', []);
72
+ // WI-087 finding #4: enforce=true かつ Complete Check 失敗時のみ shouldEnforceFailure=true
73
+ const enforce = await this.configQueryPort.getStopHookEnforce();
74
+ const shouldEnforceFailure = enforce && cliResult.exitCode !== 0;
72
75
  return {
73
76
  executed: true,
74
77
  cliResult,
78
+ shouldEnforceFailure,
75
79
  };
76
80
  } finally {
77
81
  // deactivate は成否問わず必ず実行(finally保証)
@@ -17,4 +17,7 @@ export interface ConfigQueryPort {
17
17
  getRelaxedGates(): Promise<readonly string[]>;
18
18
  getProjectPaths(): ProjectPaths;
19
19
  getBaselineConfig(): Promise<BaselineConfig>;
20
+ // WI-087 finding #4: Stop hook strict mode 設定。true なら Complete Check 失敗時に
21
+ // exit 2 + decision JSON を出して Claude Code セッションを block する。
22
+ getStopHookEnforce(): Promise<boolean>;
20
23
  }
@@ -48,12 +48,21 @@ interface BaselineSection {
48
48
  path?: string;
49
49
  }
50
50
 
51
+ interface AgentIntegrationStopHookSection {
52
+ enforce?: boolean;
53
+ }
54
+
55
+ interface AgentIntegrationSection {
56
+ stopHook?: AgentIntegrationStopHookSection;
57
+ }
58
+
51
59
  interface HarnessConfigDocument {
52
60
  harnesses?: HarnessesSection;
53
61
  project?: ProjectSection;
54
62
  protectedFiles?: ProtectedFilesSection;
55
63
  quickMode?: QuickModeSection;
56
64
  baseline?: BaselineSection;
65
+ agentIntegration?: AgentIntegrationSection;
57
66
  }
58
67
 
59
68
  export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
@@ -128,4 +137,11 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
128
137
  path: baseline.path ?? '.phasegate/baseline.json',
129
138
  };
130
139
  }
140
+
141
+ async getStopHookEnforce(): Promise<boolean> {
142
+ const config = this.loadConfig();
143
+ const enforce = config.agentIntegration?.stopHook?.enforce;
144
+ // type guard: boolean 以外(schema validator ですり抜ける null/undefined を含む)は false にフォールバック
145
+ return enforce === true;
146
+ }
131
147
  }
@@ -150,6 +150,14 @@ async function main(): Promise<void> {
150
150
  process.exit(2);
151
151
  }
152
152
 
153
+ // Quick Mode が write を許可した場合に visibility を上げる informational notice。
154
+ // exit 0 は維持し semantics は変えない。WI-087 finding #3。
155
+ if (output.quickModeAllowed !== undefined) {
156
+ const cat = output.quickModeAllowed.dominantCategory;
157
+ const suffix = cat !== undefined && cat !== '' ? `, category=${cat}` : '';
158
+ process.stderr.write(`phasegate: write allowed (Quick Mode${suffix})\n`);
159
+ }
160
+
153
161
  process.exit(0);
154
162
  } catch (error) {
155
163
  process.stderr.write(`実行エラー: ${String(error)}\n`);
@@ -88,6 +88,15 @@ async function main(): Promise<void> {
88
88
 
89
89
  if (output.executed && output.cliResult) {
90
90
  if (output.cliResult.exitCode !== 0) {
91
+ // WI-087 finding #4: enforce=true なら exit 2 + decision JSON で turn block
92
+ if (output.shouldEnforceFailure === true) {
93
+ const reason = `Complete Check failed (exitCode=${output.cliResult.exitCode})`;
94
+ process.stdout.write(`${JSON.stringify({ decision: 'block', reason })}\n`);
95
+ process.stderr.write(
96
+ `Complete Check失敗 (exitCode=${output.cliResult.exitCode}) — strict mode により turn を block します\n`,
97
+ );
98
+ process.exit(2);
99
+ }
91
100
  process.stderr.write(`Complete Check失敗 (exitCode=${output.cliResult.exitCode})\n`);
92
101
  }
93
102
  process.exit(output.cliResult.exitCode);
@@ -482,6 +482,21 @@
482
482
  }
483
483
  }
484
484
  },
485
+ "agentIntegration": {
486
+ "type": "object",
487
+ "additionalProperties": false,
488
+ "properties": {
489
+ "stopHook": {
490
+ "type": "object",
491
+ "additionalProperties": false,
492
+ "properties": {
493
+ "enforce": {
494
+ "type": "boolean"
495
+ }
496
+ }
497
+ }
498
+ }
499
+ },
485
500
  "architecture": {
486
501
  "type": "object",
487
502
  "additionalProperties": false,