phasegate 0.65.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.
Files changed (21) hide show
  1. package/README.ja.md +27 -7
  2. package/README.md +17 -2
  3. package/docs/guide/cli-reference.md +60 -0
  4. package/docs/guide/configuration.md +41 -6
  5. package/package.json +1 -1
  6. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +115 -23
  7. package/scripts/harness/agent-integration/domain/ports/baseline-grandfather-query-port.ts +12 -0
  8. package/scripts/harness/agent-integration/domain/ports/config-query-port.ts +6 -0
  9. package/scripts/harness/agent-integration/domain/ports/error-guidance-query-port.ts +20 -0
  10. package/scripts/harness/agent-integration/infrastructure/adapters/ci-governance-baseline-grandfather-adapter.ts +90 -0
  11. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +20 -1
  12. package/scripts/harness/agent-integration/infrastructure/adapters/harness-error-guidance-adapter.ts +49 -0
  13. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +11 -0
  14. package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -0
  15. package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -0
  16. package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -0
  17. package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +3 -0
  18. package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +12 -1
  19. package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +30 -0
  20. package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +31 -1
  21. package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +9 -0
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.65.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",
@@ -14,6 +14,14 @@ import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
14
14
  import type { PhaseGateQueryPort } from '../../domain/ports/phase-gate-query-port.js';
15
15
  import type { StoryReflectionQueryPort } from '../../domain/ports/story-reflection-query-port.js';
16
16
  import type { FullModeRequirementQueryPort } from '../../domain/ports/full-mode-requirement-query-port.js';
17
+ import type {
18
+ BaselineGrandfatherCheckResult,
19
+ BaselineGrandfatherQueryPort,
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';
17
25
  import { WriteTargetScope } from '../../domain/value-objects/write-target-scope.js';
18
26
  import type { HandlePreToolUseInput, HandlePreToolUseOutput } from '../dto/handle-pre-tool-use-dto.js';
19
27
 
@@ -22,6 +30,9 @@ export interface HandlePreToolUseUseCasePorts {
22
30
  phaseGateQueryPort: PhaseGateQueryPort;
23
31
  storyReflectionQueryPort?: StoryReflectionQueryPort;
24
32
  fullModeRequirementQueryPort?: FullModeRequirementQueryPort;
33
+ baselineGrandfatherQueryPort?: BaselineGrandfatherQueryPort;
34
+ grandfatherLogger?: (reason: string, targetFilePaths: readonly string[]) => void;
35
+ errorGuidanceQueryPort?: ErrorGuidanceQueryPort;
25
36
  }
26
37
 
27
38
  export class HandlePreToolUseInputValidationError extends Error {
@@ -41,11 +52,25 @@ export class HandlePreToolUseUseCase {
41
52
  private readonly configQueryPort: ConfigQueryPort;
42
53
  private readonly storyReflectionQueryPort?: StoryReflectionQueryPort;
43
54
  private readonly fullModeRequirementQueryPort?: FullModeRequirementQueryPort;
55
+ private readonly baselineGrandfatherQueryPort?: BaselineGrandfatherQueryPort;
56
+ private readonly errorGuidanceQueryPort?: ErrorGuidanceQueryPort;
57
+ private readonly grandfatherLogger: (
58
+ reason: string,
59
+ targetFilePaths: readonly string[],
60
+ ) => void;
44
61
 
45
62
  constructor(ports: HandlePreToolUseUseCasePorts) {
46
63
  this.configQueryPort = ports.configQueryPort;
47
64
  this.storyReflectionQueryPort = ports.storyReflectionQueryPort;
48
65
  this.fullModeRequirementQueryPort = ports.fullModeRequirementQueryPort;
66
+ this.baselineGrandfatherQueryPort = ports.baselineGrandfatherQueryPort;
67
+ this.errorGuidanceQueryPort = ports.errorGuidanceQueryPort;
68
+ this.grandfatherLogger =
69
+ ports.grandfatherLogger ??
70
+ ((reason, paths) =>
71
+ process.stderr.write(
72
+ `[baseline] grandfather skip (${reason}): ${paths.join(', ')}\n`,
73
+ ));
49
74
  this.translator = new AsyncHookToCliTranslator({
50
75
  configQueryPort: ports.configQueryPort,
51
76
  reentryGuard: { isActive: () => false } as never,
@@ -59,6 +84,8 @@ export class HandlePreToolUseUseCase {
59
84
  throw new HandlePreToolUseInputValidationError('toolNameは必須です(空文字不可)');
60
85
  }
61
86
 
87
+ const grandfather = await this.checkGrandfather(input.targetFilePaths);
88
+
62
89
  const hookEvent = HookEvent.createPreToolUse(input.toolName, input.targetFilePaths);
63
90
  const result = await this.translator.translate(hookEvent);
64
91
 
@@ -66,32 +93,44 @@ export class HandlePreToolUseUseCase {
66
93
  const metadata = result.blockMetadata;
67
94
  const blockedFilePath = metadata?.blockedFilePath ?? input.targetFilePaths[0];
68
95
 
69
- if (metadata?.reason === 'PHASE_GATE') {
70
- return HandlePreToolUseUseCase.buildPhaseGateBlockOutput(blockedFilePath, metadata);
71
- }
72
-
73
96
  if (metadata?.reason === 'PROTECTED_FILE') {
74
97
  return HandlePreToolUseUseCase.buildProtectedFileBlockOutput(blockedFilePath);
75
98
  }
76
99
 
77
- return {
78
- shouldBlock: true,
79
- blockedFilePath,
80
- error: {
81
- message: `ブロックされました: ${blockedFilePath ?? '不明なファイル'}`,
82
- },
83
- };
100
+ if (metadata?.reason === 'PHASE_GATE') {
101
+ if (grandfather.allGrandfathered) {
102
+ this.grandfatherLogger('phase-gate', input.targetFilePaths);
103
+ // fallthrough: continue to full-mode / story-reflection checks (which may also grandfather)
104
+ } else {
105
+ const guidance = await this.resolveGuidance('L2-001');
106
+ return HandlePreToolUseUseCase.buildPhaseGateBlockOutput(blockedFilePath, metadata, guidance);
107
+ }
108
+ } else {
109
+ return {
110
+ shouldBlock: true,
111
+ blockedFilePath,
112
+ error: {
113
+ message: `ブロックされました: ${blockedFilePath ?? '不明なファイル'}`,
114
+ },
115
+ };
116
+ }
84
117
  }
85
118
 
86
119
  if (HandlePreToolUseUseCase.WRITE_TOOLS.has(input.toolName)
87
120
  && this.fullModeRequirementQueryPort !== undefined
88
121
  && input.targetFilePaths.length > 0) {
89
- const fullModeResult = await this.fullModeRequirementQueryPort.check(input.targetFilePaths);
90
- if (fullModeResult.requiresFullMode) {
91
- return HandlePreToolUseUseCase.buildFullModeRequiredBlockOutput(
92
- input.targetFilePaths[0],
93
- fullModeResult,
94
- );
122
+ if (grandfather.allGrandfathered) {
123
+ this.grandfatherLogger('full-mode', input.targetFilePaths);
124
+ } else {
125
+ const fullModeResult = await this.fullModeRequirementQueryPort.check(input.targetFilePaths);
126
+ if (fullModeResult.requiresFullMode) {
127
+ const guidance = await this.resolveGuidance('L2-001');
128
+ return HandlePreToolUseUseCase.buildFullModeRequiredBlockOutput(
129
+ input.targetFilePaths[0],
130
+ fullModeResult,
131
+ guidance,
132
+ );
133
+ }
95
134
  }
96
135
  }
97
136
 
@@ -100,6 +139,11 @@ export class HandlePreToolUseUseCase {
100
139
  return { shouldBlock: false };
101
140
  }
102
141
 
142
+ if (grandfather.allGrandfathered) {
143
+ this.grandfatherLogger('story-reflection', input.targetFilePaths);
144
+ return { shouldBlock: false };
145
+ }
146
+
103
147
  const reflectionResult = await this.storyReflectionQueryPort.checkReflection(scope.unitId!);
104
148
 
105
149
  if (reflectionResult.skipped || reflectionResult.passed) {
@@ -113,6 +157,38 @@ export class HandlePreToolUseUseCase {
113
157
  );
114
158
  }
115
159
 
160
+ private async checkGrandfather(
161
+ targetFilePaths: readonly string[],
162
+ ): Promise<BaselineGrandfatherCheckResult> {
163
+ if (this.baselineGrandfatherQueryPort === undefined) {
164
+ return {
165
+ allGrandfathered: false,
166
+ baselineEnabled: false,
167
+ grandfatheredPaths: [],
168
+ };
169
+ }
170
+ try {
171
+ return await this.baselineGrandfatherQueryPort.check(targetFilePaths);
172
+ } catch {
173
+ return {
174
+ allGrandfathered: false,
175
+ baselineEnabled: false,
176
+ grandfatheredPaths: [],
177
+ };
178
+ }
179
+ }
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
+
116
192
  private static buildFullModeRequiredBlockOutput(
117
193
  blockedFilePath: string | undefined,
118
194
  result: {
@@ -121,6 +197,7 @@ export class HandlePreToolUseUseCase {
121
197
  rejectionReason?: string;
122
198
  dominantCategory?: string;
123
199
  },
200
+ guidance: ErrorGuidance | null,
124
201
  ): HandlePreToolUseOutput {
125
202
  const fp = blockedFilePath ?? '不明なファイル';
126
203
  const lines: string[] = [
@@ -135,7 +212,9 @@ export class HandlePreToolUseUseCase {
135
212
  if (result.rejectionReason) {
136
213
  lines.push(`理由: ${result.rejectionReason}`);
137
214
  }
138
- lines.push('次のアクション: /story-implementor スキルを使用して設計フェーズから開始してください。');
215
+ const suggestedSkill = guidance?.suggestedSkill ?? '/story-implementor';
216
+ lines.push(`次のアクション: ${suggestedSkill} スキルを使用して設計フェーズから開始してください。`);
217
+ HandlePreToolUseUseCase.appendGuidanceLines(lines, guidance);
139
218
 
140
219
  return {
141
220
  shouldBlock: true,
@@ -144,10 +223,20 @@ export class HandlePreToolUseUseCase {
144
223
  error: { message: lines.join('\n') },
145
224
  fullModeRejectionRule: result.rejectionRule,
146
225
  fullModeDominantCategory: result.dominantCategory,
147
- nextAction: '/story-implementor',
226
+ nextAction: suggestedSkill,
148
227
  };
149
228
  }
150
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
+
151
240
  private resolveStoryReflectionScope(input: HandlePreToolUseInput): WriteTargetScope | null {
152
241
  if (!HandlePreToolUseUseCase.WRITE_TOOLS.has(input.toolName)) {
153
242
  return null;
@@ -174,6 +263,7 @@ export class HandlePreToolUseUseCase {
174
263
  private static buildPhaseGateBlockOutput(
175
264
  blockedFilePath: string | undefined,
176
265
  metadata: BlockMetadata,
266
+ guidance: ErrorGuidance | null,
177
267
  ): HandlePreToolUseOutput {
178
268
  const levelLabel = metadata.scopeLevel
179
269
  ? HandlePreToolUseUseCase.LEVEL_LABELS[metadata.scopeLevel] ?? `Level ${metadata.scopeLevel}`
@@ -191,10 +281,12 @@ export class HandlePreToolUseUseCase {
191
281
  }
192
282
  }
193
283
 
194
- lines.push('次のアクション: /story-implementor スキルを使用して設計フェーズから開始してください。');
284
+ const suggestedSkill = guidance?.suggestedSkill ?? '/story-implementor';
285
+ lines.push(`次のアクション: ${suggestedSkill} スキルを使用して設計フェーズから開始してください。`);
195
286
  if (metadata.unitId) {
196
- lines.push(` 実行例: /story-implementor --unit ${metadata.unitId}`);
287
+ lines.push(` 実行例: ${suggestedSkill} --unit ${metadata.unitId}`);
197
288
  }
289
+ HandlePreToolUseUseCase.appendGuidanceLines(lines, guidance);
198
290
 
199
291
  return {
200
292
  shouldBlock: true,
@@ -203,8 +295,8 @@ export class HandlePreToolUseUseCase {
203
295
  error: { message: lines.join('\n') },
204
296
  phaseGateBlockers: [...blockers],
205
297
  nextAction: metadata.unitId
206
- ? `/story-implementor --unit ${metadata.unitId}`
207
- : '/story-implementor',
298
+ ? `${suggestedSkill} --unit ${metadata.unitId}`
299
+ : suggestedSkill,
208
300
  };
209
301
  }
210
302
 
@@ -0,0 +1,12 @@
1
+ // @unit agent-integration
2
+ // @layer domain
3
+
4
+ export interface BaselineGrandfatherCheckResult {
5
+ readonly allGrandfathered: boolean;
6
+ readonly baselineEnabled: boolean;
7
+ readonly grandfatheredPaths: readonly string[];
8
+ }
9
+
10
+ export interface BaselineGrandfatherQueryPort {
11
+ check(targetFilePaths: readonly string[]): Promise<BaselineGrandfatherCheckResult>;
12
+ }
@@ -5,10 +5,16 @@ import type { ProjectPaths } from '../value-objects/project-paths.js';
5
5
 
6
6
  export type HookType = 'pre-tool-use' | 'post-tool-use' | 'stop';
7
7
 
8
+ export interface BaselineConfig {
9
+ readonly enabled: boolean;
10
+ readonly path: string;
11
+ }
12
+
8
13
  export interface ConfigQueryPort {
9
14
  isHookEnabled(hookType: HookType): Promise<boolean>;
10
15
  getProtectedFilePatterns(): Promise<string[]>;
11
16
  getProtectedFileExclusions(): Promise<string[]>;
12
17
  getRelaxedGates(): Promise<readonly string[]>;
13
18
  getProjectPaths(): ProjectPaths;
19
+ getBaselineConfig(): Promise<BaselineConfig>;
14
20
  }
@@ -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,90 @@
1
+ // @unit agent-integration
2
+ // @layer infrastructure
3
+
4
+ import type {
5
+ BaselineGrandfatherQueryPort,
6
+ BaselineGrandfatherCheckResult,
7
+ } from '../../domain/ports/baseline-grandfather-query-port.js';
8
+ import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
9
+ import type { BaselineRepositoryPort } from '../../../ci-governance/domain/ports/baseline-repository-port.js';
10
+ import { BaselineJsonRepositoryAdapter } from '../../../ci-governance/infrastructure/adapters/baseline-json-repository-adapter.js';
11
+
12
+ export interface CiGovernanceBaselineGrandfatherAdapterDeps {
13
+ readonly baseDir: string;
14
+ readonly configQueryPort: ConfigQueryPort;
15
+ readonly baselineRepositoryFactory?: (
16
+ baseDir: string,
17
+ relativePath: string,
18
+ ) => BaselineRepositoryPort;
19
+ }
20
+
21
+ export class CiGovernanceBaselineGrandfatherAdapter
22
+ implements BaselineGrandfatherQueryPort
23
+ {
24
+ constructor(private readonly deps: CiGovernanceBaselineGrandfatherAdapterDeps) {}
25
+
26
+ async check(
27
+ targetFilePaths: readonly string[],
28
+ ): Promise<BaselineGrandfatherCheckResult> {
29
+ try {
30
+ const baselineConfig = await this.deps.configQueryPort.getBaselineConfig();
31
+
32
+ if (!baselineConfig.enabled) {
33
+ return {
34
+ allGrandfathered: false,
35
+ baselineEnabled: false,
36
+ grandfatheredPaths: [],
37
+ };
38
+ }
39
+
40
+ if (targetFilePaths.length === 0) {
41
+ return {
42
+ allGrandfathered: false,
43
+ baselineEnabled: true,
44
+ grandfatheredPaths: [],
45
+ };
46
+ }
47
+
48
+ const factory =
49
+ this.deps.baselineRepositoryFactory ??
50
+ ((baseDir, relativePath) =>
51
+ new BaselineJsonRepositoryAdapter(baseDir, relativePath));
52
+ const repository = factory(this.deps.baseDir, baselineConfig.path);
53
+
54
+ if (!(await repository.exists())) {
55
+ return {
56
+ allGrandfathered: false,
57
+ baselineEnabled: true,
58
+ grandfatheredPaths: [],
59
+ };
60
+ }
61
+
62
+ const snapshot = await repository.load();
63
+ if (snapshot === null) {
64
+ return {
65
+ allGrandfathered: false,
66
+ baselineEnabled: true,
67
+ grandfatheredPaths: [],
68
+ };
69
+ }
70
+
71
+ const grandfathered: string[] = [];
72
+ for (const p of targetFilePaths) {
73
+ if (snapshot.contains(p)) grandfathered.push(p);
74
+ }
75
+ const allGrandfathered = grandfathered.length === targetFilePaths.length;
76
+
77
+ return {
78
+ allGrandfathered,
79
+ baselineEnabled: true,
80
+ grandfatheredPaths: grandfathered,
81
+ };
82
+ } catch {
83
+ return {
84
+ allGrandfathered: false,
85
+ baselineEnabled: false,
86
+ grandfatheredPaths: [],
87
+ };
88
+ }
89
+ }
90
+ }
@@ -7,7 +7,11 @@
7
7
  */
8
8
 
9
9
  import * as fs from 'node:fs';
10
- import type { ConfigQueryPort, HookType } from '../../domain/ports/config-query-port.js';
10
+ import type {
11
+ BaselineConfig,
12
+ ConfigQueryPort,
13
+ HookType,
14
+ } from '../../domain/ports/config-query-port.js';
11
15
  import { ProjectPaths } from '../../domain/value-objects/project-paths.js';
12
16
 
13
17
  interface ProjectDocsSection {
@@ -39,11 +43,17 @@ interface QuickModeSection {
39
43
  relaxedGates?: string[];
40
44
  }
41
45
 
46
+ interface BaselineSection {
47
+ enabled?: boolean;
48
+ path?: string;
49
+ }
50
+
42
51
  interface HarnessConfigDocument {
43
52
  harnesses?: HarnessesSection;
44
53
  project?: ProjectSection;
45
54
  protectedFiles?: ProtectedFilesSection;
46
55
  quickMode?: QuickModeSection;
56
+ baseline?: BaselineSection;
47
57
  }
48
58
 
49
59
  export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
@@ -109,4 +119,13 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
109
119
  },
110
120
  );
111
121
  }
122
+
123
+ async getBaselineConfig(): Promise<BaselineConfig> {
124
+ const config = this.loadConfig();
125
+ const baseline = config.baseline ?? {};
126
+ return {
127
+ enabled: baseline.enabled ?? false,
128
+ path: baseline.path ?? '.phasegate/baseline.json',
129
+ };
130
+ }
112
131
  }
@@ -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
+ }
@@ -13,6 +13,8 @@ import { HarnessConfigConfigQueryAdapter } from '../infrastructure/adapters/harn
13
13
  import { PhaseGateQueryAdapter } from '../infrastructure/adapters/phase-gate-query-adapter.js';
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
+ import { CiGovernanceBaselineGrandfatherAdapter } from '../infrastructure/adapters/ci-governance-baseline-grandfather-adapter.js';
17
+ import { HarnessErrorGuidanceAdapter } from '../infrastructure/adapters/harness-error-guidance-adapter.js';
16
18
  import { createQuickModeCompositionRoot } from '../../quick-mode/composition-root.js';
17
19
  import * as path from 'node:path';
18
20
  import * as fs from 'node:fs/promises';
@@ -123,11 +125,20 @@ async function main(): Promise<void> {
123
125
  const fullModeRequirementQueryPort = new QuickModeFullModeRequirementAdapter({
124
126
  classifyUseCaseFactory: () => createQuickModeCompositionRoot().classifyUseCase,
125
127
  });
128
+ const baselineGrandfatherQueryPort = new CiGovernanceBaselineGrandfatherAdapter({
129
+ baseDir: path.dirname(configPath),
130
+ configQueryPort,
131
+ });
132
+ const errorGuidanceQueryPort = new HarnessErrorGuidanceAdapter({
133
+ rootDir: path.dirname(configPath),
134
+ });
126
135
  const useCase = new HandlePreToolUseUseCase({
127
136
  configQueryPort,
128
137
  phaseGateQueryPort,
129
138
  storyReflectionQueryPort,
130
139
  fullModeRequirementQueryPort,
140
+ baselineGrandfatherQueryPort,
141
+ errorGuidanceQueryPort,
131
142
  });
132
143
 
133
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',