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.
- package/README.ja.md +27 -7
- package/README.md +17 -2
- package/docs/guide/cli-reference.md +60 -0
- package/docs/guide/configuration.md +41 -6
- package/package.json +1 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +115 -23
- package/scripts/harness/agent-integration/domain/ports/baseline-grandfather-query-port.ts +12 -0
- package/scripts/harness/agent-integration/domain/ports/config-query-port.ts +6 -0
- package/scripts/harness/agent-integration/domain/ports/error-guidance-query-port.ts +20 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/ci-governance-baseline-grandfather-adapter.ts +90 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +20 -1
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-error-guidance-adapter.ts +49 -0
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +11 -0
- package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -0
- package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -0
- package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -0
- package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +3 -0
- package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +12 -1
- package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +30 -0
- package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +31 -1
- 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
|
-
|
|
348
|
-
|
|
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-
|
|
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
|
|
109
|
-
|
|
110
|
-
| `allowedCategories`
|
|
111
|
-
| `maintainedLayers`
|
|
112
|
-
| `relaxedGates`
|
|
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
package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
284
|
+
const suggestedSkill = guidance?.suggestedSkill ?? '/story-implementor';
|
|
285
|
+
lines.push(`次のアクション: ${suggestedSkill} スキルを使用して設計フェーズから開始してください。`);
|
|
195
286
|
if (metadata.unitId) {
|
|
196
|
-
lines.push(` 実行例:
|
|
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
|
-
?
|
|
207
|
-
:
|
|
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 {
|
|
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
|
}
|
package/scripts/harness/agent-integration/infrastructure/adapters/harness-error-guidance-adapter.ts
ADDED
|
@@ -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 });
|
|
@@ -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.
|
|
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',
|