phasegate 0.66.0 → 0.67.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/README.ja.md CHANGED
@@ -196,7 +196,12 @@ npx phasegate ci:generate-template --type consistency-check --render > .github/w
196
196
  "quickMode": {
197
197
  "allowedCategories": ["bugfix", "docs", "test", "config"],
198
198
  "maintainedLayers": ["L1", "L2"],
199
- "relaxedGates": ["phase-gate", "2-phase-execution"]
199
+ "relaxedGates": ["phase-gate", "2-phase-execution"],
200
+ "fullModeRequiredWhen": {
201
+ "mixedCategories": true,
202
+ "newDomainFile": true,
203
+ "apiContractChange": true
204
+ }
200
205
  },
201
206
  "phaseDependencies": {
202
207
  "preset": "standard",
@@ -204,10 +209,18 @@ npx phasegate ci:generate-template --type consistency-check --render > .github/w
204
209
  },
205
210
  "protectedFiles": {
206
211
  "exclude": ["package.json"]
212
+ },
213
+ "baseline": {
214
+ "enabled": true,
215
+ "path": ".phasegate/baseline.json"
207
216
  }
208
217
  }
209
218
  ```
210
219
 
220
+ `quickMode.fullModeRequiredWhen` は **「Quick Mode で進めようとした変更を Full Mode に強制エスカレートする条件」** を宣言します(v0.63.0 / ISSUE-006 Story A で導入、v0.64.0 / Story B で pre-tool-use hook に統合)。3 トリガー(`mixedCategories` / `newDomainFile` / `apiContractChange`)はいずれも安全側のデフォルト `true`。プロジェクトが意図的にリスクを受け入れる場合のみ個別に `false` にできます。
221
+
222
+ `baseline` は **Phase A-2 リトロフィット grandfather** をオン/オフします(v0.65.0 / ISSUE-007 Wave 1 で導入、v0.66.0 / Wave 2 で pre-tool-use hook に統合)。`.phasegate/baseline.json` に登録済みのファイルは、構造的に編集されるまで `phase-gate` 対象から除外されます。既存リポジトリへの導入時に `npx phasegate baseline` でスナップショットを生成してください。
223
+
211
224
  ### project.preset -- レイヤー厳密度
212
225
 
213
226
  | プリセット | 有効レイヤー | カバレッジ | 用途 |
@@ -343,10 +356,15 @@ Quick Mode は以下の方法で発動します:
343
356
 
344
357
  `bugfix`, `docs`, `test`, `config` カテゴリの変更では、Phase Gate と 2-Phase Execution を緩和し L1/L2 のみ維持します。
345
358
 
346
- **Quick Mode が拒否される条件**(フルチェックが強制されます):
347
- - `domain/` 配下に新規ファイルを追加した場合
348
- - `*port.ts` や `*adapter.ts`(API 契約)を変更した場合
349
- - 新機能追加・新ドメインモデル追加に該当する変更
359
+ **Quick Mode が拒否される条件**(`fullModeRequiredWhen` で設定駆動 / フルチェックが強制されます):
360
+
361
+ | 条件 | `fullModeRequiredWhen.*` フラグ |
362
+ |---|---|
363
+ | 複数カテゴリが混在する変更(例: `bugfix` + `api`) | `mixedCategories` |
364
+ | `domain/` 配下に新規ファイルを追加 | `newDomainFile` |
365
+ | `*port.ts` / `*adapter.ts`(API 契約)を変更 | `apiContractChange` |
366
+
367
+ 事前に判定したい場合は `npx phasegate check-change-category --paths <csv>` を使います。CI で gate にしたい場合は `--fail-on-full-required` を付与してください。
350
368
 
351
369
  ### protectedFiles -- AI 書き込み保護
352
370
 
@@ -398,6 +416,8 @@ npx phasegate <command> [options]
398
416
  | `ci-check` | CI フルチェック (L2-L4) | `--quick` `--dry-run` `--fail-on-reject` |
399
417
  | `check-phase-gate` | フェーズゲートチェック | `--level 1\|2\|3` |
400
418
  | `validate-metadata <files>` | メタデータ検証 | |
419
+ | `check-change-category` | 変更ファイルを Quick Mode カテゴリに分類し、`quickMode.fullModeRequiredWhen` 評価結果(Full Mode 強制が必要か)を返す(v0.63.0 / ISSUE-006 Story A) | `--paths <csv>` `--format human\|json` `--fail-on-full-required` |
420
+ | `baseline` | `.phasegate/baseline.json` スナップショットを生成(Phase A-2 grandfather)。登録済みファイルは構造的に編集されるまで `phase-gate` 対象から除外される(v0.65.0 / ISSUE-007 Wave 1) | `--dry-run` `--force` `--paths <glob,glob,...>` `--json` |
401
421
 
402
422
  ### phasegate コマンド
403
423
 
@@ -568,7 +588,7 @@ export class ConfigSchema { ... }
568
588
 
569
589
  | Hook | タイミング | 動作 |
570
590
  |---|---|---|
571
- | **PreToolUse** | Write/Edit/Bash 実行前 | フェーズゲート違反・保護ファイルへの書き込み・Bash 経由の書き込み(`sed -i`, `tee` 等)をブロック |
591
+ | **PreToolUse** | Write/Edit/Bash 実行前 | フェーズゲート違反・保護ファイルへの書き込み・Bash 経由の書き込み(`sed -i`, `tee` 等)をブロック。`quickMode.fullModeRequiredWhen` トリガー時は Quick Mode → Full Mode へエスカレート(v0.64.0)。`.phasegate/baseline.json` 登録済みかつ未編集のファイルは grandfather として `phase-gate` をスキップ(v0.66.0) |
572
592
  | **PostToolUse** | Write/Edit 実行後 | Biome AST ルールを自動実行、違反を即時フィードバック |
573
593
  | **Stop** | セッション終了前 | L2-L4 全チェックを実行、全グリーンでないと終了を保留 |
574
594
 
@@ -787,4 +807,4 @@ phasegate 自体の開発(内部アーキテクチャ、回帰テスト、リ
787
807
 
788
808
  ---
789
809
 
790
- *Last updated: 2026-04-07 -- v0.33.0*
810
+ *Last updated: 2026-04-22 -- v0.66.0*
package/README.md CHANGED
@@ -189,7 +189,12 @@ Skills cover the full **AIDLC (AI-Driven Development Life Cycle)**, enforcing ph
189
189
  "quickMode": {
190
190
  "allowedCategories": ["bugfix", "docs", "test", "config"],
191
191
  "maintainedLayers": ["L1", "L2"],
192
- "relaxedGates": ["phase-gate", "2-phase-execution"]
192
+ "relaxedGates": ["phase-gate", "2-phase-execution"],
193
+ "fullModeRequiredWhen": {
194
+ "mixedCategories": true,
195
+ "newDomainFile": true,
196
+ "apiContractChange": true
197
+ }
193
198
  },
194
199
  "phaseDependencies": {
195
200
  "preset": "standard",
@@ -197,10 +202,18 @@ Skills cover the full **AIDLC (AI-Driven Development Life Cycle)**, enforcing ph
197
202
  },
198
203
  "protectedFiles": {
199
204
  "exclude": ["tsconfig.json", "package.json"]
205
+ },
206
+ "baseline": {
207
+ "enabled": true,
208
+ "path": ".phasegate/baseline.json"
200
209
  }
201
210
  }
202
211
  ```
203
212
 
213
+ `quickMode.fullModeRequiredWhen` declares which conditions force a Quick Mode change to escalate to the full `/story-implementor` flow. All three triggers default to `true` so retrofits stay safe; flip individual flags to `false` only when a project intentionally accepts the risk.
214
+
215
+ `baseline` opts in to the **Phase A-2 retrofit grandfather**: pre-existing files captured in `.phasegate/baseline.json` are exempted from `phase-gate` until they are structurally modified. Generate the snapshot with `npx phasegate baseline` before introducing the harness to an existing repository.
216
+
204
217
  ---
205
218
 
206
219
  ## Configurable Phase Gates
@@ -246,7 +259,7 @@ Phasegate integrates natively with Claude Code via hooks in `.claude/settings.js
246
259
 
247
260
  | Hook | Trigger | Behavior |
248
261
  |---|---|---|
249
- | `PreToolUse` | `Write`, `Edit`, or `Bash` (write operations detected) | Blocks writes to source files without design docs; protects configured files; detects Bash write operations (`sed -i`, `tee`, `cp`, etc.) |
262
+ | `PreToolUse` | `Write`, `Edit`, or `Bash` (write operations detected) | Blocks writes to source files without design docs; enforces `quickMode.fullModeRequiredWhen` (escalates Quick Mode → Full when triggered); skips files captured in the `.phasegate/baseline.json` snapshot until they are modified; protects configured files; detects Bash write operations (`sed -i`, `tee`, `cp`, etc.) |
250
263
  | `PostToolUse` | `Write` or `Edit` | Auto-formats and validates metadata |
251
264
  | `Stop` | Session end | Runs full test suite to ensure all tests pass |
252
265
 
@@ -299,6 +312,8 @@ npx phasegate <command> [options]
299
312
  | `update-skills` | Update skills to latest version |
300
313
  | `phasegate:status` | Display overall harness health summary |
301
314
  | `phasegate:check-phase --unit <id>` | Check current phase for a Unit |
315
+ | `check-change-category --paths <csv>` | Classify changed files into Quick Mode categories and report whether Full Mode is required (`--format json`, `--fail-on-full-required`) |
316
+ | `baseline` | Create `.phasegate/baseline.json` snapshot for Phase A-2 retrofit grandfather (`--dry-run`, `--force`, `--paths <glob,glob,...>`, `--json`) |
302
317
  | `list-errors --layer <L0-L4>` | List error definitions with fix examples |
303
318
  | `hook <pre-tool-use\|post-tool-use\|stop>` | Run a Claude Code hook (reads JSON from stdin) |
304
319
  | `pre-commit` | Run L2 pre-commit validators on staged files |
@@ -32,6 +32,66 @@ npx phasegate <command> [options]
32
32
 
33
33
  ---
34
34
 
35
+ ## Quick Mode
36
+
37
+ | Command | Options | Description |
38
+ |---|---|---|
39
+ | `check-change-category` | `--paths <csv>` `--format human\|json` `--fail-on-full-required` | Classify changed file paths into Quick Mode categories (`api` / `domain` / `feature` / `bugfix` / `test` / `config` / `docs`) and report whether `quickMode.fullModeRequiredWhen` forces escalation to Full Mode. |
40
+
41
+ ### `check-change-category` の使い方
42
+
43
+ ISSUE-006 Story A で導入。Quick Mode で取り扱おうとしている変更が
44
+ `quickMode.fullModeRequiredWhen` のいずれかをトリガーするか事前に確認したいときに使う。
45
+
46
+ ```bash
47
+ # JSON 出力 (CI で消費しやすい)
48
+ npx phasegate check-change-category --paths src/foo.ts,src/bar.ts --format json
49
+
50
+ # Full Mode が必要なら exit 1 (PR チェック等に使える)
51
+ npx phasegate check-change-category \
52
+ --paths "$(git diff --name-only origin/main...HEAD | paste -sd, -)" \
53
+ --fail-on-full-required
54
+ ```
55
+
56
+ `--fail-on-full-required` を指定しない場合、Full Mode が必要と判定されても exit 0 を返す
57
+ (情報提供のみ)。CI で gate にしたいときは必ず付与すること。
58
+
59
+ ---
60
+
61
+ ## Baseline (Retrofit Grandfather)
62
+
63
+ | Command | Options | Description |
64
+ |---|---|---|
65
+ | `baseline` | `--dry-run` `--force` `--paths <glob,glob,...>` `--json` | Create / refresh `.phasegate/baseline.json` snapshot. Files in the snapshot are exempted from `phase-gate` until they are structurally modified (sha1 mismatch). |
66
+
67
+ ### `baseline` の使い方
68
+
69
+ ISSUE-007 Wave 1 で導入。既存リポジトリに phasegate を後付けする際、現状のコード資産を
70
+ "Phase A-2 grandfather" として一度だけ凍結する。
71
+
72
+ ```bash
73
+ # 現在のリポジトリ全体をスナップショット
74
+ npx phasegate baseline
75
+
76
+ # 何が含まれるかだけ確認 (ファイルは書かない)
77
+ npx phasegate baseline --dry-run --json
78
+
79
+ # 既存スナップショットを上書きして再生成
80
+ npx phasegate baseline --force
81
+
82
+ # 特定ディレクトリだけ含める
83
+ npx phasegate baseline --paths "scripts/harness/**/*.ts,src/**/*.ts"
84
+ ```
85
+
86
+ スナップショットに含まれるファイルは sha1 ハッシュで照合される。ファイルを構造的に
87
+ 編集した瞬間に grandfather が外れ、通常の `phase-gate` 対象に戻る。新規ファイルは
88
+ 最初から `phase-gate` の対象。
89
+
90
+ `baseline.enabled = false` (デフォルトは `true`) を `phasegate.config.json` に書くと
91
+ 仕組み全体を無効化できる。スナップショットの保存先は `baseline.path` で変更可能。
92
+
93
+ ---
94
+
35
95
  ## Harness API
36
96
 
37
97
  Commands exposed as npm scripts (`npm run <command>`).
@@ -24,7 +24,12 @@ This file is the **Single Source of Truth** for all quality configuration in a P
24
24
  "quickMode": {
25
25
  "allowedCategories": ["bugfix", "docs", "test", "config"],
26
26
  "maintainedLayers": ["L1", "L2"],
27
- "relaxedGates": ["phase-gate", "2-phase-execution"]
27
+ "relaxedGates": ["phase-gate", "2-phase-execution"],
28
+ "fullModeRequiredWhen": {
29
+ "mixedCategories": true,
30
+ "newDomainFile": true,
31
+ "apiContractChange": true
32
+ }
28
33
  },
29
34
  "phaseDependencies": {
30
35
  "preset": "standard", // "full" | "standard" | "minimal" | "custom" ("default" -> "full")
@@ -53,6 +58,10 @@ This file is the **Single Source of Truth** for all quality configuration in a P
53
58
  "reporting": {
54
59
  "format": "json",
55
60
  "outputDir": "reports"
61
+ },
62
+ "baseline": {
63
+ "enabled": true,
64
+ "path": ".phasegate/baseline.json"
56
65
  }
57
66
  }
58
67
  ```
@@ -105,11 +114,26 @@ The five layers are:
105
114
 
106
115
  #### `quickMode`
107
116
 
108
- | Sub-field | Type | Default | Description |
109
- |---------------------|------------|-----------------------------------------|-----------------------------------------------------------------------------|
110
- | `allowedCategories` | `string[]` | `["bugfix", "docs", "test", "config"]` | Change categories permitted under Quick Mode. Any category outside this list requires the full `story-implementor` workflow. |
111
- | `maintainedLayers` | `string[]` | `["L1", "L2"]` | Layers that remain fully enforced even in Quick Mode. |
112
- | `relaxedGates` | `string[]` | `["phase-gate", "2-phase-execution"]` | Gates that are relaxed (not skipped) when Quick Mode is active. |
117
+ | Sub-field | Type | Default | Description |
118
+ |------------------------|------------|-----------------------------------------|-----------------------------------------------------------------------------|
119
+ | `allowedCategories` | `string[]` | `["bugfix", "docs", "test", "config"]` | Change categories permitted under Quick Mode. Any category outside this list requires the full `story-implementor` workflow. |
120
+ | `maintainedLayers` | `string[]` | `["L1", "L2"]` | Layers that remain fully enforced even in Quick Mode. |
121
+ | `relaxedGates` | `string[]` | `["phase-gate", "2-phase-execution"]` | Gates that are relaxed (not skipped) when Quick Mode is active. |
122
+ | `fullModeRequiredWhen` | `object` | all flags `true` | Conditions that force a Quick Mode change to escalate to the full `/story-implementor` flow. See below. |
123
+
124
+ ##### `fullModeRequiredWhen`
125
+
126
+ Introduced in ISSUE-006 Story A (v0.63.0) and wired into the pre-tool-use hook by Story B (v0.64.0). Each flag is a hard escalation rule -- when triggered, the change cannot proceed under Quick Mode regardless of the file's category.
127
+
128
+ | Flag | Default | Trigger |
129
+ |---------------------|---------|-----------------------------------------------------------------------------------------------|
130
+ | `mixedCategories` | `true` | The change set spans more than one Quick Mode category (e.g. a `bugfix` file + an `api` file).|
131
+ | `newDomainFile` | `true` | The change creates a new file under any `domain/` directory. |
132
+ | `apiContractChange` | `true` | The change modifies a Port (`*port.ts`) or Adapter (`*adapter.ts`) file. |
133
+
134
+ **Use `npx phasegate check-change-category --paths <csv>`** to dry-run the classifier against an arbitrary file list (see [CLI Reference](cli-reference.md#check-change-category-の使い方)). Combining `--fail-on-full-required` with a CI job makes "Quick Mode PR that should have been Full" a hard build failure.
135
+
136
+ 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.
113
137
 
114
138
  #### `phaseDependencies`
115
139
 
@@ -381,6 +405,17 @@ Quick Mode with `relaxedGates: ["phase-gate"]` relaxes `storyReflection` as well
381
405
  | `format` | `string` | `"json"` | Output format for validation reports. |
382
406
  | `outputDir` | `string` | `"reports"` | Directory where reports are written. |
383
407
 
408
+ #### `baseline` (retrofit grandfather)
409
+
410
+ Introduced in ISSUE-007 Wave 1 (v0.65.0) and wired into the pre-tool-use hook by Wave 2 (v0.66.0). When phasegate is added to an existing repository, the `baseline` block lets you snapshot the current state of the codebase so legacy files do not trip `phase-gate` on first edit. Files in the snapshot are exempted **until they are structurally modified** (sha1 mismatch); new files are subject to `phase-gate` from the start.
411
+
412
+ | Sub-field | Type | Default | Description |
413
+ |-----------|-----------|-------------------------------|----------------------------------------------------------------------------------------------|
414
+ | `enabled` | `boolean` | `true` | Master switch. Set to `false` to disable retrofit grandfather and treat every file as new. |
415
+ | `path` | `string` | `".phasegate/baseline.json"` | Snapshot location. Override only if `.phasegate/` conflicts with an existing path in the repo.|
416
+
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
+
384
419
  ---
385
420
 
386
421
  ### Quick Mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.66.0",
3
+ "version": "0.67.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": "Apache-2.0",
@@ -18,6 +18,10 @@ import type {
18
18
  BaselineGrandfatherCheckResult,
19
19
  BaselineGrandfatherQueryPort,
20
20
  } from '../../domain/ports/baseline-grandfather-query-port.js';
21
+ import type {
22
+ ErrorGuidance,
23
+ ErrorGuidanceQueryPort,
24
+ } from '../../domain/ports/error-guidance-query-port.js';
21
25
  import { WriteTargetScope } from '../../domain/value-objects/write-target-scope.js';
22
26
  import type { HandlePreToolUseInput, HandlePreToolUseOutput } from '../dto/handle-pre-tool-use-dto.js';
23
27
 
@@ -28,6 +32,7 @@ export interface HandlePreToolUseUseCasePorts {
28
32
  fullModeRequirementQueryPort?: FullModeRequirementQueryPort;
29
33
  baselineGrandfatherQueryPort?: BaselineGrandfatherQueryPort;
30
34
  grandfatherLogger?: (reason: string, targetFilePaths: readonly string[]) => void;
35
+ errorGuidanceQueryPort?: ErrorGuidanceQueryPort;
31
36
  }
32
37
 
33
38
  export class HandlePreToolUseInputValidationError extends Error {
@@ -48,6 +53,7 @@ export class HandlePreToolUseUseCase {
48
53
  private readonly storyReflectionQueryPort?: StoryReflectionQueryPort;
49
54
  private readonly fullModeRequirementQueryPort?: FullModeRequirementQueryPort;
50
55
  private readonly baselineGrandfatherQueryPort?: BaselineGrandfatherQueryPort;
56
+ private readonly errorGuidanceQueryPort?: ErrorGuidanceQueryPort;
51
57
  private readonly grandfatherLogger: (
52
58
  reason: string,
53
59
  targetFilePaths: readonly string[],
@@ -58,6 +64,7 @@ export class HandlePreToolUseUseCase {
58
64
  this.storyReflectionQueryPort = ports.storyReflectionQueryPort;
59
65
  this.fullModeRequirementQueryPort = ports.fullModeRequirementQueryPort;
60
66
  this.baselineGrandfatherQueryPort = ports.baselineGrandfatherQueryPort;
67
+ this.errorGuidanceQueryPort = ports.errorGuidanceQueryPort;
61
68
  this.grandfatherLogger =
62
69
  ports.grandfatherLogger ??
63
70
  ((reason, paths) =>
@@ -95,7 +102,8 @@ export class HandlePreToolUseUseCase {
95
102
  this.grandfatherLogger('phase-gate', input.targetFilePaths);
96
103
  // fallthrough: continue to full-mode / story-reflection checks (which may also grandfather)
97
104
  } else {
98
- return HandlePreToolUseUseCase.buildPhaseGateBlockOutput(blockedFilePath, metadata);
105
+ const guidance = await this.resolveGuidance('L2-001');
106
+ return HandlePreToolUseUseCase.buildPhaseGateBlockOutput(blockedFilePath, metadata, guidance);
99
107
  }
100
108
  } else {
101
109
  return {
@@ -116,9 +124,11 @@ export class HandlePreToolUseUseCase {
116
124
  } else {
117
125
  const fullModeResult = await this.fullModeRequirementQueryPort.check(input.targetFilePaths);
118
126
  if (fullModeResult.requiresFullMode) {
127
+ const guidance = await this.resolveGuidance('L2-001');
119
128
  return HandlePreToolUseUseCase.buildFullModeRequiredBlockOutput(
120
129
  input.targetFilePaths[0],
121
130
  fullModeResult,
131
+ guidance,
122
132
  );
123
133
  }
124
134
  }
@@ -168,6 +178,17 @@ export class HandlePreToolUseUseCase {
168
178
  }
169
179
  }
170
180
 
181
+ private async resolveGuidance(errorCode: string): Promise<ErrorGuidance | null> {
182
+ if (this.errorGuidanceQueryPort === undefined) {
183
+ return null;
184
+ }
185
+ try {
186
+ return await this.errorGuidanceQueryPort.getGuidance(errorCode);
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
191
+
171
192
  private static buildFullModeRequiredBlockOutput(
172
193
  blockedFilePath: string | undefined,
173
194
  result: {
@@ -176,6 +197,7 @@ export class HandlePreToolUseUseCase {
176
197
  rejectionReason?: string;
177
198
  dominantCategory?: string;
178
199
  },
200
+ guidance: ErrorGuidance | null,
179
201
  ): HandlePreToolUseOutput {
180
202
  const fp = blockedFilePath ?? '不明なファイル';
181
203
  const lines: string[] = [
@@ -190,7 +212,9 @@ export class HandlePreToolUseUseCase {
190
212
  if (result.rejectionReason) {
191
213
  lines.push(`理由: ${result.rejectionReason}`);
192
214
  }
193
- lines.push('次のアクション: /story-implementor スキルを使用して設計フェーズから開始してください。');
215
+ const suggestedSkill = guidance?.suggestedSkill ?? '/story-implementor';
216
+ lines.push(`次のアクション: ${suggestedSkill} スキルを使用して設計フェーズから開始してください。`);
217
+ HandlePreToolUseUseCase.appendGuidanceLines(lines, guidance);
194
218
 
195
219
  return {
196
220
  shouldBlock: true,
@@ -199,10 +223,20 @@ export class HandlePreToolUseUseCase {
199
223
  error: { message: lines.join('\n') },
200
224
  fullModeRejectionRule: result.rejectionRule,
201
225
  fullModeDominantCategory: result.dominantCategory,
202
- nextAction: '/story-implementor',
226
+ nextAction: suggestedSkill,
203
227
  };
204
228
  }
205
229
 
230
+ private static appendGuidanceLines(lines: string[], guidance: ErrorGuidance | null): void {
231
+ if (guidance === null) return;
232
+ if (guidance.scaffoldCommand !== null) {
233
+ lines.push(` scaffold: ${guidance.scaffoldCommand}`);
234
+ }
235
+ if (guidance.templatePath !== null) {
236
+ lines.push(` テンプレ: ${guidance.templatePath}`);
237
+ }
238
+ }
239
+
206
240
  private resolveStoryReflectionScope(input: HandlePreToolUseInput): WriteTargetScope | null {
207
241
  if (!HandlePreToolUseUseCase.WRITE_TOOLS.has(input.toolName)) {
208
242
  return null;
@@ -229,6 +263,7 @@ export class HandlePreToolUseUseCase {
229
263
  private static buildPhaseGateBlockOutput(
230
264
  blockedFilePath: string | undefined,
231
265
  metadata: BlockMetadata,
266
+ guidance: ErrorGuidance | null,
232
267
  ): HandlePreToolUseOutput {
233
268
  const levelLabel = metadata.scopeLevel
234
269
  ? HandlePreToolUseUseCase.LEVEL_LABELS[metadata.scopeLevel] ?? `Level ${metadata.scopeLevel}`
@@ -246,10 +281,12 @@ export class HandlePreToolUseUseCase {
246
281
  }
247
282
  }
248
283
 
249
- lines.push('次のアクション: /story-implementor スキルを使用して設計フェーズから開始してください。');
284
+ const suggestedSkill = guidance?.suggestedSkill ?? '/story-implementor';
285
+ lines.push(`次のアクション: ${suggestedSkill} スキルを使用して設計フェーズから開始してください。`);
250
286
  if (metadata.unitId) {
251
- lines.push(` 実行例: /story-implementor --unit ${metadata.unitId}`);
287
+ lines.push(` 実行例: ${suggestedSkill} --unit ${metadata.unitId}`);
252
288
  }
289
+ HandlePreToolUseUseCase.appendGuidanceLines(lines, guidance);
253
290
 
254
291
  return {
255
292
  shouldBlock: true,
@@ -258,8 +295,8 @@ export class HandlePreToolUseUseCase {
258
295
  error: { message: lines.join('\n') },
259
296
  phaseGateBlockers: [...blockers],
260
297
  nextAction: metadata.unitId
261
- ? `/story-implementor --unit ${metadata.unitId}`
262
- : '/story-implementor',
298
+ ? `${suggestedSkill} --unit ${metadata.unitId}`
299
+ : suggestedSkill,
263
300
  };
264
301
  }
265
302
 
@@ -0,0 +1,20 @@
1
+ // @unit agent-integration
2
+ // @layer domain
3
+
4
+ /**
5
+ * phase-gate 等のエラーに紐づく actionable なガイダンス情報
6
+ * (harness-error Unit の ErrorDefinition.defaultSuggestedSkill 等から供給される)
7
+ */
8
+ export interface ErrorGuidance {
9
+ readonly suggestedSkill: string | null;
10
+ readonly scaffoldCommand: string | null;
11
+ readonly templatePath: string | null;
12
+ }
13
+
14
+ /**
15
+ * エラーコード → actionable guidance の lookup を行う port
16
+ * 実装は harness-error Unit の ErrorDefinitionRegistry を参照する
17
+ */
18
+ export interface ErrorGuidanceQueryPort {
19
+ getGuidance(errorCode: string): Promise<ErrorGuidance | null>;
20
+ }
@@ -0,0 +1,49 @@
1
+ // @unit agent-integration
2
+ // @layer infrastructure
3
+
4
+ import type {
5
+ ErrorGuidance,
6
+ ErrorGuidanceQueryPort,
7
+ } from '../../domain/ports/error-guidance-query-port.js';
8
+
9
+ /**
10
+ * harness-error Unit の ErrorDefinitionRegistry を lookup して
11
+ * actionable guidance を返す adapter
12
+ *
13
+ * Wave 2 の CiGovernanceBaselineGrandfatherAdapter と同じ
14
+ * infrastructure-to-infrastructure クロス Unit パターン
15
+ */
16
+ export class HarnessErrorGuidanceAdapter implements ErrorGuidanceQueryPort {
17
+ private readonly rootDir: string;
18
+
19
+ constructor(options: { rootDir: string }) {
20
+ this.rootDir = options.rootDir;
21
+ }
22
+
23
+ async getGuidance(errorCode: string): Promise<ErrorGuidance | null> {
24
+ try {
25
+ const [{ createHarnessErrorModule }, { ErrorCode }] = await Promise.all([
26
+ import('../../../harness-error/composition-root.js'),
27
+ import('../../../harness-error/domain/value-objects/error-code.js'),
28
+ ]);
29
+ const mod = createHarnessErrorModule(this.rootDir);
30
+ const definition = mod.errorDefinitionRegistry.getDefinition(
31
+ ErrorCode.create(errorCode),
32
+ );
33
+ if (
34
+ definition.defaultSuggestedSkill === null
35
+ && definition.defaultScaffoldCommand === null
36
+ && definition.defaultTemplatePath === null
37
+ ) {
38
+ return null;
39
+ }
40
+ return {
41
+ suggestedSkill: definition.defaultSuggestedSkill,
42
+ scaffoldCommand: definition.defaultScaffoldCommand,
43
+ templatePath: definition.defaultTemplatePath,
44
+ };
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+ }
@@ -14,6 +14,7 @@ import { PhaseGateQueryAdapter } from '../infrastructure/adapters/phase-gate-que
14
14
  import { FileSystemStoryReflectionQueryAdapter } from '../infrastructure/adapters/file-system-story-reflection-query-adapter.js';
15
15
  import { QuickModeFullModeRequirementAdapter } from '../infrastructure/adapters/quick-mode-full-mode-requirement-adapter.js';
16
16
  import { CiGovernanceBaselineGrandfatherAdapter } from '../infrastructure/adapters/ci-governance-baseline-grandfather-adapter.js';
17
+ import { HarnessErrorGuidanceAdapter } from '../infrastructure/adapters/harness-error-guidance-adapter.js';
17
18
  import { createQuickModeCompositionRoot } from '../../quick-mode/composition-root.js';
18
19
  import * as path from 'node:path';
19
20
  import * as fs from 'node:fs/promises';
@@ -128,12 +129,16 @@ async function main(): Promise<void> {
128
129
  baseDir: path.dirname(configPath),
129
130
  configQueryPort,
130
131
  });
132
+ const errorGuidanceQueryPort = new HarnessErrorGuidanceAdapter({
133
+ rootDir: path.dirname(configPath),
134
+ });
131
135
  const useCase = new HandlePreToolUseUseCase({
132
136
  configQueryPort,
133
137
  phaseGateQueryPort,
134
138
  storyReflectionQueryPort,
135
139
  fullModeRequirementQueryPort,
136
140
  baselineGrandfatherQueryPort,
141
+ errorGuidanceQueryPort,
137
142
  });
138
143
 
139
144
  const output = await useCase.execute({ toolName: effectiveToolName, targetFilePaths });
@@ -12,4 +12,7 @@ export interface CreateHarnessErrorInput {
12
12
  readonly adrRef?: string;
13
13
  readonly fixExample?: string;
14
14
  readonly validatorId: string;
15
+ readonly suggestedSkill?: string;
16
+ readonly scaffoldCommand?: string;
17
+ readonly templatePath?: string;
15
18
  }
@@ -11,4 +11,7 @@ export interface HarnessErrorContract {
11
11
  readonly suggestion: string;
12
12
  readonly adr_ref?: string;
13
13
  readonly fix_example?: string;
14
+ readonly suggested_skill?: string;
15
+ readonly scaffold_command?: string;
16
+ readonly template_path?: string;
14
17
  }
@@ -20,6 +20,15 @@ export class HarnessErrorContractMapper {
20
20
  ...(harnessError.fixExample !== null
21
21
  ? { fix_example: harnessError.fixExample.toString() }
22
22
  : {}),
23
+ ...(harnessError.suggestedSkill !== null
24
+ ? { suggested_skill: harnessError.suggestedSkill }
25
+ : {}),
26
+ ...(harnessError.scaffoldCommand !== null
27
+ ? { scaffold_command: harnessError.scaffoldCommand }
28
+ : {}),
29
+ ...(harnessError.templatePath !== null
30
+ ? { template_path: harnessError.templatePath }
31
+ : {}),
23
32
  };
24
33
 
25
34
  return Object.freeze(contract);
@@ -34,6 +34,9 @@ export class CreateHarnessErrorUseCase {
34
34
  requestedSeverity: input.severity,
35
35
  adrRef: input.adrRef,
36
36
  fixExample: input.fixExample,
37
+ suggestedSkill: input.suggestedSkill,
38
+ scaffoldCommand: input.scaffoldCommand,
39
+ templatePath: input.templatePath,
37
40
  });
38
41
 
39
42
  return this.contractMapper.toReadonlyContract(harnessError);
@@ -29,6 +29,9 @@ export interface CreateHarnessErrorParams {
29
29
  readonly requestedSeverity?: 'error' | 'warning';
30
30
  readonly adrRef?: string | null;
31
31
  readonly fixExample?: string | null;
32
+ readonly suggestedSkill?: string | null;
33
+ readonly scaffoldCommand?: string | null;
34
+ readonly templatePath?: string | null;
32
35
  }
33
36
 
34
37
  export interface HarnessErrorFactoryDeps {
@@ -119,7 +122,12 @@ export class HarnessErrorFactory {
119
122
  }
120
123
  }
121
124
 
122
- // 10. HarnessError 生成と凍結
125
+ // 10. actionable フィールドの解決(ErrorDefinition の default と input 引数の合成)
126
+ const resolvedSuggestedSkill = definition.resolveSuggestedSkill(input.suggestedSkill);
127
+ const resolvedScaffoldCommand = definition.resolveScaffoldCommand(input.scaffoldCommand);
128
+ const resolvedTemplatePath = definition.resolveTemplatePath(input.templatePath);
129
+
130
+ // 11. HarnessError 生成と凍結
123
131
  const harnessError = new HarnessError({
124
132
  code: errorCode,
125
133
  severity: effectiveSeverity,
@@ -127,6 +135,9 @@ export class HarnessErrorFactory {
127
135
  suggestion: input.suggestion,
128
136
  adrRef: resolvedAdrRef,
129
137
  fixExample: resolvedFixExample,
138
+ suggestedSkill: resolvedSuggestedSkill,
139
+ scaffoldCommand: resolvedScaffoldCommand,
140
+ templatePath: resolvedTemplatePath,
130
141
  });
131
142
 
132
143
  return Object.freeze(harnessError);
@@ -31,6 +31,9 @@ export interface ErrorDefinitionProps {
31
31
  readonly fixExampleRequired: boolean;
32
32
  readonly defaultFixExample: FixExample | null;
33
33
  readonly ownerValidatorId: string;
34
+ readonly defaultSuggestedSkill?: string | null;
35
+ readonly defaultScaffoldCommand?: string | null;
36
+ readonly defaultTemplatePath?: string | null;
34
37
  }
35
38
 
36
39
  export class ErrorDefinition {
@@ -43,6 +46,9 @@ export class ErrorDefinition {
43
46
  readonly fixExampleRequired: boolean;
44
47
  readonly defaultFixExample: FixExample | null;
45
48
  readonly ownerValidatorId: string;
49
+ readonly defaultSuggestedSkill: string | null;
50
+ readonly defaultScaffoldCommand: string | null;
51
+ readonly defaultTemplatePath: string | null;
46
52
 
47
53
  private constructor(props: ErrorDefinitionProps) {
48
54
  this.code = props.code;
@@ -54,6 +60,9 @@ export class ErrorDefinition {
54
60
  this.fixExampleRequired = props.fixExampleRequired;
55
61
  this.defaultFixExample = props.defaultFixExample;
56
62
  this.ownerValidatorId = props.ownerValidatorId;
63
+ this.defaultSuggestedSkill = props.defaultSuggestedSkill ?? null;
64
+ this.defaultScaffoldCommand = props.defaultScaffoldCommand ?? null;
65
+ this.defaultTemplatePath = props.defaultTemplatePath ?? null;
57
66
  Object.freeze(this);
58
67
  }
59
68
 
@@ -98,6 +107,27 @@ export class ErrorDefinition {
98
107
  return this.defaultFixExample;
99
108
  }
100
109
 
110
+ resolveSuggestedSkill(explicit?: string | null): string | null {
111
+ if (explicit) {
112
+ return explicit;
113
+ }
114
+ return this.defaultSuggestedSkill;
115
+ }
116
+
117
+ resolveScaffoldCommand(explicit?: string | null): string | null {
118
+ if (explicit) {
119
+ return explicit;
120
+ }
121
+ return this.defaultScaffoldCommand;
122
+ }
123
+
124
+ resolveTemplatePath(explicit?: string | null): string | null {
125
+ if (explicit) {
126
+ return explicit;
127
+ }
128
+ return this.defaultTemplatePath;
129
+ }
130
+
101
131
  equals(other: ErrorDefinition): boolean {
102
132
  const adrRefEqual =
103
133
  this.defaultAdrRef === null && other.defaultAdrRef === null
@@ -17,6 +17,9 @@ export interface HarnessErrorContract {
17
17
  readonly suggestion: string;
18
18
  readonly adr_ref?: string;
19
19
  readonly fix_example?: string;
20
+ readonly suggested_skill?: string;
21
+ readonly scaffold_command?: string;
22
+ readonly template_path?: string;
20
23
  }
21
24
 
22
25
  export interface HarnessErrorProps {
@@ -26,6 +29,9 @@ export interface HarnessErrorProps {
26
29
  readonly suggestion: string;
27
30
  readonly adrRef: AdrRef | null;
28
31
  readonly fixExample: FixExample | null;
32
+ readonly suggestedSkill?: string | null;
33
+ readonly scaffoldCommand?: string | null;
34
+ readonly templatePath?: string | null;
29
35
  }
30
36
 
31
37
  export class HarnessError {
@@ -35,6 +41,9 @@ export class HarnessError {
35
41
  readonly suggestion: string;
36
42
  readonly adrRef: AdrRef | null;
37
43
  readonly fixExample: FixExample | null;
44
+ readonly suggestedSkill: string | null;
45
+ readonly scaffoldCommand: string | null;
46
+ readonly templatePath: string | null;
38
47
 
39
48
  constructor(props: HarnessErrorProps) {
40
49
  this.code = props.code;
@@ -43,6 +52,9 @@ export class HarnessError {
43
52
  this.suggestion = props.suggestion;
44
53
  this.adrRef = props.adrRef;
45
54
  this.fixExample = props.fixExample;
55
+ this.suggestedSkill = props.suggestedSkill ?? null;
56
+ this.scaffoldCommand = props.scaffoldCommand ?? null;
57
+ this.templatePath = props.templatePath ?? null;
46
58
  }
47
59
 
48
60
  equals(other: HarnessError): boolean {
@@ -66,7 +78,10 @@ export class HarnessError {
66
78
  this.message === other.message &&
67
79
  this.suggestion === other.suggestion &&
68
80
  adrRefEqual &&
69
- fixExampleEqual
81
+ fixExampleEqual &&
82
+ this.suggestedSkill === other.suggestedSkill &&
83
+ this.scaffoldCommand === other.scaffoldCommand &&
84
+ this.templatePath === other.templatePath
70
85
  );
71
86
  }
72
87
 
@@ -78,6 +93,18 @@ export class HarnessError {
78
93
  return this.fixExample !== null;
79
94
  }
80
95
 
96
+ hasSuggestedSkill(): boolean {
97
+ return this.suggestedSkill !== null;
98
+ }
99
+
100
+ hasScaffoldCommand(): boolean {
101
+ return this.scaffoldCommand !== null;
102
+ }
103
+
104
+ hasTemplatePath(): boolean {
105
+ return this.templatePath !== null;
106
+ }
107
+
81
108
  toContract(): Readonly<HarnessErrorContract> {
82
109
  const contract: HarnessErrorContract = {
83
110
  code: this.code.toString(),
@@ -88,6 +115,9 @@ export class HarnessError {
88
115
  ...(this.fixExample !== null
89
116
  ? { fix_example: this.fixExample.toString() }
90
117
  : {}),
118
+ ...(this.suggestedSkill !== null ? { suggested_skill: this.suggestedSkill } : {}),
119
+ ...(this.scaffoldCommand !== null ? { scaffold_command: this.scaffoldCommand } : {}),
120
+ ...(this.templatePath !== null ? { template_path: this.templatePath } : {}),
91
121
  };
92
122
  return Object.freeze(contract);
93
123
  }
@@ -14,6 +14,9 @@ function createDefinition(input: {
14
14
  category: 'phase_gate' | 'architecture' | 'dependency' | 'quality' | 'security' | 'performance' | 'consistency' | 'metadata';
15
15
  ownerValidatorId: string;
16
16
  defaultFixExample: string;
17
+ defaultSuggestedSkill?: string;
18
+ defaultScaffoldCommand?: string;
19
+ defaultTemplatePath?: string;
17
20
  }): ErrorDefinition {
18
21
  return ErrorDefinition.create({
19
22
  code: ErrorCode.create(input.code),
@@ -25,6 +28,9 @@ function createDefinition(input: {
25
28
  fixExampleRequired: true,
26
29
  defaultFixExample: FixExample.create(input.defaultFixExample),
27
30
  ownerValidatorId: input.ownerValidatorId,
31
+ defaultSuggestedSkill: input.defaultSuggestedSkill ?? null,
32
+ defaultScaffoldCommand: input.defaultScaffoldCommand ?? null,
33
+ defaultTemplatePath: input.defaultTemplatePath ?? null,
28
34
  });
29
35
  }
30
36
 
@@ -36,6 +42,9 @@ export const L2_ERROR_DEFINITIONS = Object.freeze([
36
42
  ownerValidatorId: 'phase-gate',
37
43
  defaultFixExample:
38
44
  "const requiredPlanPath = 'docs/inception/harness-error/it_test_logic_plan.md';",
45
+ defaultSuggestedSkill: '/story-implementor',
46
+ defaultScaffoldCommand: 'npx phasegate scaffold-design --unit <unit-id> --phase logical',
47
+ defaultTemplatePath: 'docs/templates/logical_design.template.md',
39
48
  }),
40
49
  createDefinition({
41
50
  code: 'L2-002',