phasegate 0.160.21 → 0.161.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 (59) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +6 -1
  3. package/docs/guide/cli-reference.md +1 -1
  4. package/docs/guide/configuration.md +31 -1
  5. package/docs/guide/skills-overview.md +2 -0
  6. package/docs/principles/model-routing.md +1 -0
  7. package/docs/templates/personal/phasegate-local-config.json +4 -1
  8. package/docs/templates/project/phasegate.config.json +4 -1
  9. package/package.json +1 -1
  10. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +54 -5
  11. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
  12. package/scripts/harness/config-foundation/domain/harness-config.ts +9 -0
  13. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +2 -0
  14. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +20 -5
  15. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +25 -0
  16. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +25 -0
  17. package/scripts/harness/installation/application/usecases/run-install.ts +16 -3
  18. package/scripts/harness/installation/application/usecases/run-reconcile.ts +48 -5
  19. package/scripts/harness/main.ts +31 -1
  20. package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +6 -1
  21. package/scripts/harness/setup/skill-deployer.ts +47 -0
  22. package/scripts/harness/skill-quality/domain/services/skill-structure-validator.ts +21 -0
  23. package/scripts/harness/skill-quality/domain/value-objects/skill-structure.ts +2 -0
  24. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +18 -5
  25. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +16 -3
  26. package/scripts/harness/validator-system/domain/ports/validator-config-port.ts +2 -0
  27. package/scripts/harness/validator-system/domain/services/validator-language-capability-service.ts +64 -0
  28. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +7 -1
  29. package/skills/cascade-updater/SKILL.md +1 -0
  30. package/skills/codebase-mapper/SKILL.md +1 -0
  31. package/skills/codex-delegator/SKILL.md +1 -0
  32. package/skills/consistency-checker/SKILL.md +1 -0
  33. package/skills/doc-freshness-checker/SKILL.md +1 -0
  34. package/skills/domain-designer/SKILL.md +1 -0
  35. package/skills/engineering-perspective/SKILL.md +1 -0
  36. package/skills/environment-designer/SKILL.md +1 -0
  37. package/skills/implementation-planner/SKILL.md +1 -0
  38. package/skills/implementation-readiness-checker/SKILL.md +1 -0
  39. package/skills/it-test-designer/SKILL.md +1 -0
  40. package/skills/it-test-logic-designer/SKILL.md +1 -0
  41. package/skills/logical-designer/SKILL.md +1 -0
  42. package/skills/mock-designer/SKILL.md +1 -0
  43. package/skills/phasegate-config-doctor/SKILL.md +1 -0
  44. package/skills/phasegate-toolkit-guide/SKILL.md +1 -0
  45. package/skills/pointer-validator/SKILL.md +1 -0
  46. package/skills/product-architect/SKILL.md +1 -0
  47. package/skills/quick-implementor/SKILL.md +1 -0
  48. package/skills/scenario-test-designer/SKILL.md +1 -0
  49. package/skills/scenario-test-logic-designer/SKILL.md +1 -0
  50. package/skills/skill-creator/SKILL.md +1 -0
  51. package/skills/story-implementor/SKILL.md +1 -0
  52. package/skills/story-mapper/SKILL.md +1 -0
  53. package/skills/story-writer/SKILL.md +1 -0
  54. package/skills/test-coverage-checker/SKILL.md +1 -0
  55. package/skills/uiux-designer/SKILL.md +1 -0
  56. package/skills/unit-designer/SKILL.md +1 -0
  57. package/skills/unit-test-designer/SKILL.md +1 -0
  58. package/skills/unit-test-logic-designer/SKILL.md +1 -0
  59. package/templates/.claude/settings.json +4 -0
package/CHANGELOG.md CHANGED
@@ -7,9 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.161.0] - 2026-07-04
11
+
12
+ ### Fixed
13
+
14
+ - **WI-220 — phase-gate bypass and quick-mode relaxation defects** — closes four defects reproduced against the published `0.160.21` package:
15
+ - Claude Code の PreToolUse `Bash` matcher に `phasegate hook pre-tool-use` を配線し、Bash 経由(`cat >`, heredoc, `tee`, `sed -i`, `cp`, `mv` 等)の書き込みもフェーズゲート・保護ファイル判定を通すようにした(従来は Codex のみ配線され Claude では素通りしていた)。
16
+ - `WriteTargetScope.fromPath` の `normalize()` で `.`/`..` セグメントを解決し、`.../__tests__/../domain/*` トラバーサルによる保護回避を塞いだ(本物のテストファイルの除外は維持)。
17
+ - `WriteTargetScope` のプレフィックス一致を大文字小文字非依存にし、大小非依存ファイルシステム(macOS/Windows)での `Scripts/harness/...` 経由の保護回避を塞いだ。
18
+ - `QuickModeConfig.isMaintained` がレイヤー名エントリ(`"L2"`)を配下バリデータ ID(`"L2-002"` 等)にマッチさせるようにし、`maintainedLayers: ["L1","L2"]` で L2 が全件 skip されていた不具合を修正(ID 完全一致も後方互換)。
19
+
20
+ ## [0.160.22] - 2026-06-12
21
+
22
+ ### Added
23
+
24
+ - **WI-212 — language-aware validator and skill metadata** — adds `project.languages` configuration, `init --language`, unsupported-language skips for TypeScript-only validators, bundled skill `languages` frontmatter, and a supported languages matrix for adoption planning.
25
+ - **WI-219 — configurable model delegation policy** — adds `modelRouting.delegation` so projects can keep the existing `delegate-sonnet` skill behavior or disable fixed Sonnet delegation while preserving local review guidance.
26
+
10
27
  ### Fixed
11
28
 
12
29
  - **WI-217 — personal inception/product consistency** — makes L4-002 validate configured personal documentation roots, scopes L4-004 freshness to `paths.designDocs`, adds personal hook L4 backstop behavior for `.phasegate-local` docs, and lets `scaffold-wi` use custom IDs and personal inception roots.
30
+ - **WI-218 — WI description scope classification** — treats `docs/inception/**/WI-*/description.md` as a Phase 1 description artifact instead of Level 3, matching hook guidance and allowing description-only inception work without Full Mode.
13
31
 
14
32
  ## [0.160.16] - 2026-05-22
15
33
 
package/README.md CHANGED
@@ -413,6 +413,9 @@ For selection guidance and config examples see [Preset Selection Guide](docs/gui
413
413
  "preset": "standard",
414
414
  "storyReflection": { "enabled": true }
415
415
  },
416
+ "modelRouting": {
417
+ "delegation": "delegate-sonnet"
418
+ },
416
419
  "protectedFiles": {
417
420
  "exclude": ["tsconfig.json", "package.json"]
418
421
  },
@@ -433,6 +436,8 @@ For selection guidance and config examples see [Preset Selection Guide](docs/gui
433
436
 
434
437
  `paths` maps PhaseGate's design and reference documentation roots. Repositories that do not use `docs/` can point `designDocs`, `inceptionDocs`, `principlesDocs`, and `folderRulesDoc` at their own documentation layout; product-wide Level 1 artifacts are overridden separately with custom `phaseDependencies.gates[]`. <!-- @work-item-id WI-214 -->
435
438
 
439
+ `modelRouting.delegation` controls whether bundled skills may use the legacy `delegate-sonnet` workflow. Set it to `"none"` when the main agent session should do the work directly; skill deployment removes fixed model routing text and normal `delegate-sonnet` execution is refused. <!-- @work-item-id WI-219 -->
440
+
436
441
  `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. Since v0.71.0 the `baseline.enabled` flag defaults to `true`, so simply running `npx phasegate baseline` after `init` is enough — no manual config edit needed. For a step-by-step retrofit walkthrough see [Retrofit Adoption Guide](docs/guide/retrofit-adoption.md).
437
442
 
438
443
  ---
@@ -560,7 +565,7 @@ README keeps only the entry points most users need. The full public/compatibilit
560
565
  | `hook <pre-tool-use\|post-tool-use\|stop\|session-start\|user-prompt-submit>` | Run an agent hook (reads JSON from stdin; session-start/user-prompt-submit write JSON context) |
561
566
  | `pre-commit` | Run L2 pre-commit validators on staged files |
562
567
  | `bypass:audit --base <ref> [--head <ref>]` | Replay pre-commit validation over a push/CI range and require structured bypass evidence for gate failures |
563
- | `delegate-sonnet [...args]` | Delegate task to Sonnet 4.6 (transparent wrapper) |
568
+ | `delegate-sonnet [...args]` | Delegate task to Sonnet 4.6 when `modelRouting.delegation` allows it; `--help` and `--dry-run` remain available |
564
569
  | `migrate work-items --dry-run` / `--apply` | Migrate legacy `ISSUE-XXX` / `H{NN}-{NN}` directories under `docs/inception/` to the unified `WI-XXX` layout (frontmatter `type` / `legacy_id` / `affects` injected). Sequential allocator skips numbers already used by existing WIs. See [CLI Reference -- Work Item Migration](docs/guide/cli-reference.md#work-item-migration). |
565
570
  | `migrate --schema v3` | Upgrade `phasegate.config.json` to v3 schema by adding the `architecture` key (idempotent). |
566
571
 
@@ -22,7 +22,7 @@ Command names in this document are split into three surfaces:
22
22
 
23
23
  | Command | Description |
24
24
  |---|---|
25
- | `init --name <name>` | Legacy-compatible bootstrap for new projects: deploy skills, generate config, and optionally add hooks/CI. Options: `--preset <full\|standard\|minimal\|custom>`, `--skills <core\|all>`, `--agent <claude\|codex\|both>`, `--workflow <standard\|strict>`, `--with-husky`, `--with-ci`, `--yes`. |
25
+ | `init --name <name>` | Legacy-compatible bootstrap for new projects: deploy skills, generate config, and optionally add hooks/CI. Options: `--preset <full\|standard\|minimal\|custom>`, `--skills <core\|all>`, `--agent <claude\|codex\|both>`, `--workflow <standard\|strict>`, `--language <language>`, `--with-husky`, `--with-ci`, `--yes`. `--language` writes `project.languages: ["<language>"]`; omitted language keeps the TypeScript-compatible default. <!-- @work-item-id WI-212 --> |
26
26
  | `install --dry-run` / `--apply` | Idempotently merge PhaseGate into an existing project, preserve user content, add package scripts/devDependency, create selected `AGENTS.md` / `CLAUDE.md` managed sections, and write `.phasegate/manifest.json`. `--agent <claude\|codex\|both>`, `--skills <core\|all>`, and `--workflow <standard\|strict>` affect rendered agent context; `--force` replaces managed targets after backup. |
27
27
  | `doctor` | Diagnose silent or partial installations and report repair hints (`--json`, `--strict`, `--agent <claude\|codex\|both>`, `--report-out <path>`). `--agent` defaults to `both`; single-agent scopes keep shared targets applicable and mark the other agent's findings as not applicable. `--report-out` writes exactly to the supplied path, not to `reporting.outputDir`. |
28
28
  | `uninstall --dry-run` / `--apply` | Remove PhaseGate-managed files and managed blocks using `.phasegate/manifest.json`; `--force` handles managed conflict cases. |
@@ -24,7 +24,8 @@ The plan identifies target fields, managed artifacts, commands, validation, risk
24
24
  {
25
25
  "project": {
26
26
  "name": "my-project",
27
- "preset": "standard" // "minimal" | "standard" | "strict"
27
+ "preset": "standard", // "minimal" | "standard" | "strict"
28
+ "languages": ["typescript"]
28
29
  },
29
30
  "layers": {
30
31
  "L1": { "enabled": true },
@@ -56,6 +57,9 @@ The plan identifies target fields, managed artifacts, commands, validation, risk
56
57
  "default": "interactive", // "interactive" | "embedded-qa"
57
58
  "perPhase": {}
58
59
  },
60
+ "modelRouting": {
61
+ "delegation": "delegate-sonnet" // "delegate-sonnet" | "none"
62
+ },
59
63
  "harnesses": {
60
64
  "agentLessonCollection": false,
61
65
  "cascadeUpdate": false,
@@ -121,6 +125,24 @@ The `strict` preset also enables:
121
125
  |-----------|----------|--------------|----------------------------------------------------------------|
122
126
  | `name` | `string` | -- | Project identifier. Used in reports and validation output. |
123
127
  | `preset` | `string` | `"standard"` | One of `"minimal"`, `"standard"`, `"strict"`. Controls which layers and harnesses are active by default. Individual settings override the preset. |
128
+ | `languages` | `string[]` | `["typescript"]` | Project implementation languages used for validator dispatch and skill applicability. Unknown values are accepted as declarations; unsupported validator/language pairs are reported as skips with `unsupported-language` reasons. <!-- @work-item-id WI-212 --> |
129
+
130
+ #### Supported Languages
131
+
132
+ <!-- @work-item-id WI-212 -->
133
+
134
+ PhaseGate currently ships TypeScript-backed source, test, and coverage validators. Non-TypeScript declarations are supported as configuration metadata so architecture, phase-gate, documentation, pointer, and WI workflow checks can still run while TypeScript-only validators skip with a clear reason.
135
+
136
+ | Capability | TypeScript | Python / Go / Rust / other declared languages |
137
+ |---|---:|---:|
138
+ | `project.languages` config | Supported | Accepted |
139
+ | `init --language <lang>` bootstrap | Supported | Accepted |
140
+ | Architecture preset / phase-gate / WI workflow | Supported | Supported |
141
+ | Markdown document validators (`L4-002`, `L4-004`, `L4-005`, `L4-006`) | Supported | Supported |
142
+ | L3 performance AST validator (`L3-002`) | Supported | Skipped as `unsupported-language` |
143
+ | L3 coverage validator (`L3-003`) | Supported through Vitest coverage | Skipped as `unsupported-language` |
144
+ | L4 dead-code validator (`L4-003`) | Supported | Skipped as `unsupported-language` |
145
+ | Bundled skills | `languages: [typescript]` metadata present | Future language-specific skills can coexist by declaring their own `languages` metadata |
124
146
 
125
147
  #### `layers`
126
148
 
@@ -454,6 +476,14 @@ Quick Mode with `relaxedGates: ["phase-gate"]` relaxes `storyReflection` as well
454
476
  | `default` | `string` | `"interactive"` | `"interactive"` prompts the user at each decision point. `"embedded-qa"` embeds QA checks inline without prompting. |
455
477
  | `perPhase` | `object` | `{}` | Override the planning mode for specific phases. Keys are phase names, values are `"interactive"` or `"embedded-qa"`. |
456
478
 
479
+ #### `modelRouting`
480
+
481
+ <!-- @work-item-id WI-219 -->
482
+
483
+ | Sub-field | Type | Default | Description |
484
+ |--------------|----------|----------------------|-------------|
485
+ | `delegation` | `string` | `"delegate-sonnet"` | `"delegate-sonnet"` preserves the legacy skill routing and `phasegate delegate-sonnet` behavior. `"none"` deploys bundled skills without fixed `model` / `review` frontmatter or `delegate-sonnet` body instructions, and blocks normal `phasegate delegate-sonnet` execution. Help and `--dry-run` remain available. |
486
+
457
487
  #### `harnesses`
458
488
 
459
489
  | Sub-field | Type | Default | Description |
@@ -2,6 +2,8 @@
2
2
 
3
3
  Phasegate provides 30 skills covering the full AIDLC (AI-Driven Development Life Cycle). `npx phasegate init` and project `npx phasegate install` deploy skill bodies to root `skills/` and expose them to enabled agents through `.claude/skills/` / `.codex/skills/` links. Personal install instead writes real local-only per-agent skill directories. <!-- @work-item-id WI-210 -->
4
4
 
5
+ Bundled `SKILL.md` files include `languages: [typescript]` frontmatter so PhaseGate can distinguish current TypeScript-oriented guidance from future language-specific skill variants. The metadata is advisory for applicability and does not prevent non-TypeScript projects from installing the catalog. <!-- @work-item-id WI-212 -->
6
+
5
7
  ## AIDLC Process — Skill Execution Order
6
8
 
7
9
  The AIDLC follows a 3-level execution flow, each level producing design artifacts that gate the next.
@@ -17,6 +17,7 @@
17
17
  - 実装、テスト、リポジトリ内の整合修正は Implementor が担当する。
18
18
  - 委任結果は必ず親エージェントがレビューし、配置ルール、上位文書整合、スコープ逸脱を確認する。
19
19
  - ユーザーまたは実行環境の指示でサブエージェント利用が制限されている場合は、その指示を優先する。
20
+ - `phasegate.config.json` の `modelRouting.delegation` はPhaseGate配備物に対する委任可否の正本である。`"none"` の場合、配備済みスキルは固定モデル委任を指示せず、`delegate-sonnet` の通常実行も拒否される。@work-item-id WI-219
20
21
 
21
22
  ## 委任してよい作業
22
23
 
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "project": {
3
3
  "name": "personal-phasegate",
4
- "preset": "standard"
4
+ "preset": "standard",
5
+ "languages": [
6
+ "typescript"
7
+ ]
5
8
  },
6
9
  "layers": {
7
10
  "L1": {
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "project": {
3
3
  "name": "phasegate-project",
4
- "preset": "standard"
4
+ "preset": "standard",
5
+ "languages": [
6
+ "typescript"
7
+ ]
5
8
  },
6
9
  "architecture": {
7
10
  "preset": "clean"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.160.21",
3
+ "version": "0.161.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -75,11 +75,15 @@ export class WriteTargetScope {
75
75
 
76
76
  const inceptionMatch = matchPrefix(normalizedPath, inceptionPath);
77
77
  if (inceptionMatch !== null) {
78
- const [unitId, secondSegment] = inceptionMatch;
78
+ const [unitId, secondSegment, thirdSegment] = inceptionMatch;
79
79
 
80
80
  // 横断的 WI: docs/inception/_cross/{WI-XXX}/ → Level 3
81
81
  if (unitId === "_cross") {
82
82
  if (secondSegment !== undefined && WORK_ITEM_ID_PATTERN.test(secondSegment)) {
83
+ if (thirdSegment === "description.md") {
84
+ return WriteTargetScope.create({ level: 1 });
85
+ }
86
+
83
87
  return WriteTargetScope.create({ level: 3, unitId, storyId: secondSegment });
84
88
  }
85
89
 
@@ -88,6 +92,10 @@ export class WriteTargetScope {
88
92
 
89
93
  // Unit 所有 WI / 既存 US パス: docs/inception/{unit}/{storyId}/ → Level 3
90
94
  if (unitId !== undefined && secondSegment !== undefined && WORK_ITEM_ID_PATTERN.test(secondSegment)) {
95
+ if (secondSegment.startsWith("WI-") && thirdSegment === "description.md") {
96
+ return WriteTargetScope.create({ level: 1 });
97
+ }
98
+
91
99
  return WriteTargetScope.create({ level: 3, unitId, storyId: secondSegment });
92
100
  }
93
101
  }
@@ -121,24 +129,65 @@ export class WriteTargetScope {
121
129
  }
122
130
  }
123
131
 
124
- const normalize = (value: string): string =>
125
- value
132
+ const normalize = (value: string): string => {
133
+ const cleaned = value
126
134
  .replaceAll("\\", "/")
127
135
  .replace(/^\.\/+/, "")
128
136
  .replace(/\/+/g, "/")
129
137
  .replace(/\/$/, "");
130
138
 
139
+ return resolveTraversal(cleaned);
140
+ };
141
+
142
+ // posix セマンティクスで `.`/`..` セグメントを解決する(プロジェクト相対パス想定)。
143
+ // これにより `a/b/../c` → `a/c`。フェーズゲート保護回避(P-2)対策として、
144
+ // __tests__ 除外や prefix 一致判定はこの解決後のパスに対して適用される。
145
+ const resolveTraversal = (value: string): string => {
146
+ if (value === "") {
147
+ return "";
148
+ }
149
+
150
+ const segments = value.split("/");
151
+ const resolved: string[] = [];
152
+
153
+ for (const segment of segments) {
154
+ if (segment === "" || segment === ".") {
155
+ continue;
156
+ }
157
+
158
+ if (segment === "..") {
159
+ if (resolved.length > 0 && resolved[resolved.length - 1] !== "..") {
160
+ resolved.pop();
161
+ } else {
162
+ // プロジェクトルートより上への参照はそのまま残す(保護判定に影響させない)
163
+ resolved.push("..");
164
+ }
165
+ continue;
166
+ }
167
+
168
+ resolved.push(segment);
169
+ }
170
+
171
+ return resolved.join("/");
172
+ };
173
+
131
174
  const matchPrefix = (targetPath: string, basePath: string): string[] | null => {
132
175
  const normalizedBase = normalize(basePath);
133
176
 
134
- if (targetPath === normalizedBase) {
177
+ // フェーズゲートはセキュリティ境界のため、大小非依存 FS(macOS/Windows)での
178
+ // 保護回避(P-3)を防ぐべく prefix 一致は大文字小文字を無視して判定する。
179
+ const lowerTarget = targetPath.toLowerCase();
180
+ const lowerBase = normalizedBase.toLowerCase();
181
+
182
+ if (lowerTarget === lowerBase) {
135
183
  return [];
136
184
  }
137
185
 
138
- if (!targetPath.startsWith(`${normalizedBase}/`)) {
186
+ if (!lowerTarget.startsWith(`${lowerBase}/`)) {
139
187
  return null;
140
188
  }
141
189
 
190
+ // remainder(unitId 等の抽出結果)は元の大小を保持する。
142
191
  const remainder = targetPath.slice(normalizedBase.length + 1);
143
192
  return remainder === "" ? [] : remainder.split("/");
144
193
  };
@@ -3,6 +3,7 @@
3
3
  * @unit config-foundation
4
4
  * @work-item-id WI-133 / WI-156
5
5
  * @work-item-id WI-217
6
+ * @work-item-id WI-212
6
7
  */
7
8
  import type { HarnessConfigV2 } from '../../domain/harness-config.js';
8
9
 
@@ -33,7 +34,10 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
33
34
  : l4Validators;
34
35
 
35
36
  return {
36
- project: { preset: resolvedConfig.project.preset },
37
+ project: {
38
+ preset: resolvedConfig.project.preset,
39
+ languages: resolvedConfig.project.languages ?? ['typescript'],
40
+ },
37
41
  paths: {
38
42
  designDocs: resolvedConfig.paths.designDocs,
39
43
  inceptionDocs: resolvedConfig.paths.inceptionDocs,
@@ -2,6 +2,8 @@
2
2
  * @layer domain
3
3
  * @unit config-foundation
4
4
  * @work-item-id WI-012
5
+ * @work-item-id WI-212
6
+ * @work-item-id WI-219
5
7
  */
6
8
  import { ConfigFoundationDomainError } from './errors/config-foundation-domain-error.js';
7
9
  import { ConfigValidationError } from './errors/config-validation-error.js';
@@ -30,6 +32,7 @@ import type {
30
32
  export type LayerId = 'L1' | 'L2' | 'L3' | 'L4';
31
33
  export type PresetId = 'minimal' | 'standard' | 'strict';
32
34
  export type PlanningModeValue = 'interactive' | 'embedded-qa' | 'manual';
35
+ export type ModelDelegationPolicy = 'delegate-sonnet' | 'none';
33
36
  export type { PhaseDependenciesPresetId };
34
37
  type DeepPartial<T> = {
35
38
  [K in keyof T]?: T[K] extends Array<infer TItem>
@@ -43,6 +46,7 @@ export interface HarnessConfigSourceDocument {
43
46
  project: {
44
47
  name: string;
45
48
  preset: PresetId;
49
+ languages?: string[];
46
50
  };
47
51
  layers: DeepPartial<HarnessConfigResolvedDocument['layers']>;
48
52
  quickMode: Partial<HarnessConfigResolvedDocument['quickMode']>;
@@ -54,6 +58,7 @@ export interface HarnessConfigSourceDocument {
54
58
  ci?: HarnessConfigResolvedDocument['ci'];
55
59
  validate?: HarnessConfigResolvedDocument['validate'];
56
60
  preCommit?: Partial<HarnessConfigResolvedDocument['preCommit']>;
61
+ modelRouting?: HarnessConfigResolvedDocument['modelRouting'];
57
62
  phase2Extensions?: HarnessConfigResolvedDocument['phase2Extensions'];
58
63
  architecture?: ArchitectureConfigSource;
59
64
  }
@@ -62,6 +67,7 @@ export interface HarnessConfigResolvedDocument {
62
67
  project: {
63
68
  name: string;
64
69
  preset: PresetId;
70
+ languages?: string[];
65
71
  };
66
72
  layers: {
67
73
  L1: {
@@ -125,6 +131,9 @@ export interface HarnessConfigResolvedDocument {
125
131
  preCommit?: {
126
132
  implementationExtensions: string[];
127
133
  };
134
+ modelRouting?: {
135
+ delegation: ModelDelegationPolicy;
136
+ };
128
137
  phase2Extensions?: {
129
138
  initialCreationExpirationRules?: Array<{
130
139
  ruleId: string;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit config-foundation
4
+ * @work-item-id WI-212
4
5
  */
5
6
  import { ConfigFoundationDomainError } from '../errors/config-foundation-domain-error.js';
6
7
  import type {
@@ -152,6 +153,7 @@ export class PresetResolutionService {
152
153
  project: {
153
154
  name: sourceDocument.project.name,
154
155
  preset: sourceDocument.project.preset,
156
+ languages: sourceDocument.project.languages ?? ['typescript'],
155
157
  },
156
158
  layers: deepMerge(presetDefinition.layers, sourceDocument.layers, 'layers'),
157
159
  quickMode: deepMerge(
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit config-foundation
4
+ * @work-item-id WI-212
4
5
  *
5
6
  * ProjectConfig値オブジェクト - プロジェクト名とPresetを保持する
6
7
  */
@@ -10,35 +11,49 @@ import { Preset } from './preset.js';
10
11
  interface ProjectConfigProps {
11
12
  readonly name: string;
12
13
  readonly preset: Preset;
14
+ readonly languages?: readonly string[];
13
15
  }
14
16
 
15
17
  export class ProjectConfig {
16
18
  readonly name: string;
17
19
  readonly preset: Preset;
20
+ readonly languages: readonly string[];
18
21
 
19
22
  constructor(props: ProjectConfigProps) {
20
23
  if (!props.name || props.name.trim() === '') {
21
24
  throw new ConfigValidationError('ProjectConfig: name must not be empty');
22
25
  }
26
+ const sourceLanguages = props.languages ?? ['typescript'];
27
+ if (sourceLanguages.length === 0) {
28
+ throw new ConfigValidationError('ProjectConfig: languages must not be empty');
29
+ }
30
+ const normalizedLanguages = sourceLanguages.map((language) => language.trim()).filter((language) => language.length > 0);
31
+ if (normalizedLanguages.length !== sourceLanguages.length) {
32
+ throw new ConfigValidationError('ProjectConfig: languages must contain non-empty strings');
33
+ }
23
34
  this.name = props.name;
24
35
  this.preset = props.preset;
36
+ this.languages = Object.freeze([...normalizedLanguages]);
25
37
  Object.freeze(this);
26
38
  }
27
39
 
28
- static create(raw: { name: string; preset: string }): ProjectConfig {
40
+ static create(raw: { name: string; preset: string; languages?: readonly string[] }): ProjectConfig {
29
41
  const preset = Preset.create(raw.preset);
30
- return new ProjectConfig({ name: raw.name, preset });
42
+ return new ProjectConfig({ name: raw.name, preset, languages: raw.languages ?? ['typescript'] });
31
43
  }
32
44
 
33
45
  rename(name: string): ProjectConfig {
34
- return new ProjectConfig({ name, preset: this.preset });
46
+ return new ProjectConfig({ name, preset: this.preset, languages: this.languages });
35
47
  }
36
48
 
37
49
  changePreset(preset: Preset): ProjectConfig {
38
- return new ProjectConfig({ name: this.name, preset });
50
+ return new ProjectConfig({ name: this.name, preset, languages: this.languages });
39
51
  }
40
52
 
41
53
  equals(other: ProjectConfig): boolean {
42
- return this.name === other.name && this.preset.equals(other.preset);
54
+ return this.name === other.name
55
+ && this.preset.equals(other.preset)
56
+ && this.languages.length === other.languages.length
57
+ && this.languages.every((language, index) => language === other.languages[index]);
43
58
  }
44
59
  }
@@ -33,6 +33,15 @@
33
33
  "strict"
34
34
  ]
35
35
  },
36
+ "languages": {
37
+ "type": "array",
38
+ "items": {
39
+ "type": "string",
40
+ "minLength": 1
41
+ },
42
+ "minItems": 1,
43
+ "uniqueItems": true
44
+ },
36
45
  "paths": {
37
46
  "type": "object",
38
47
  "additionalProperties": false,
@@ -474,6 +483,22 @@
474
483
  }
475
484
  }
476
485
  },
486
+ "modelRouting": {
487
+ "type": "object",
488
+ "additionalProperties": false,
489
+ "required": [
490
+ "delegation"
491
+ ],
492
+ "properties": {
493
+ "delegation": {
494
+ "type": "string",
495
+ "enum": [
496
+ "delegate-sonnet",
497
+ "none"
498
+ ]
499
+ }
500
+ }
501
+ },
477
502
  "protectedFiles": {
478
503
  "type": "object",
479
504
  "additionalProperties": false,
@@ -43,6 +43,15 @@
43
43
  "strict"
44
44
  ]
45
45
  },
46
+ "languages": {
47
+ "type": "array",
48
+ "items": {
49
+ "type": "string",
50
+ "minLength": 1
51
+ },
52
+ "minItems": 1,
53
+ "uniqueItems": true
54
+ },
46
55
  "paths": {
47
56
  "type": "object",
48
57
  "additionalProperties": false,
@@ -492,6 +501,22 @@
492
501
  }
493
502
  }
494
503
  },
504
+ "modelRouting": {
505
+ "type": "object",
506
+ "additionalProperties": false,
507
+ "required": [
508
+ "delegation"
509
+ ],
510
+ "properties": {
511
+ "delegation": {
512
+ "type": "string",
513
+ "enum": [
514
+ "delegate-sonnet",
515
+ "none"
516
+ ]
517
+ }
518
+ }
519
+ },
495
520
  "protectedFiles": {
496
521
  "type": "object",
497
522
  "additionalProperties": false,
@@ -15,9 +15,11 @@
15
15
  // @work-item-id WI-214
16
16
  // @work-item-id WI-215
17
17
  // @work-item-id WI-216
18
+ // @work-item-id WI-219
18
19
 
19
20
  import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink, readdir, rm } from "node:fs/promises";
20
21
  import { dirname, join } from "node:path";
22
+ import { readModelDelegationPolicy, renderSkillForModelDelegation } from "../../../setup/skill-deployer.js";
21
23
  import { DeploymentEntry } from "../../domain/deployment-entry.js";
22
24
  import { DeploymentManifest } from "../../domain/deployment-manifest.js";
23
25
  import type { ManagedBlockInput } from "../../domain/managed-block.js";
@@ -132,13 +134,24 @@ async function copyDirectory(src: string, dest: string): Promise<void> {
132
134
  }
133
135
  }
134
136
 
135
- async function copySelectedSkillDirectories(harnessRoot: string, targetRoot: string, skills: readonly string[]): Promise<void> {
137
+ async function copySelectedSkillDirectories(
138
+ harnessRoot: string,
139
+ projectRoot: string,
140
+ targetRoot: string,
141
+ skills: readonly string[],
142
+ ): Promise<void> {
136
143
  await mkdir(targetRoot, { recursive: true });
144
+ const modelDelegationPolicy = await readModelDelegationPolicy(projectRoot);
137
145
  for (const skill of skills) {
138
146
  const source = join(harnessRoot, "skills", skill);
139
147
  const target = join(targetRoot, skill);
140
148
  await rm(target, { recursive: true, force: true });
141
149
  await copyDirectory(source, target);
150
+ if (modelDelegationPolicy === "none") {
151
+ const skillPath = join(target, "SKILL.md");
152
+ const content = await readFile(skillPath, "utf8");
153
+ await writeFile(skillPath, renderSkillForModelDelegation(content, modelDelegationPolicy), "utf8");
154
+ }
142
155
  }
143
156
  }
144
157
 
@@ -445,7 +458,7 @@ export class RunInstallUseCase {
445
458
  plan.push(item);
446
459
  if (input.apply && item.changed && item.repairMode === "mechanical") {
447
460
  try {
448
- await copySelectedSkillDirectories(input.harnessRoot, join(input.projectRoot, skillPath), selectedPersonalSkills);
461
+ await copySelectedSkillDirectories(input.harnessRoot, input.projectRoot, join(input.projectRoot, skillPath), selectedPersonalSkills);
449
462
  await writeFile(
450
463
  join(input.projectRoot, skillPath, ".harness-version"),
451
464
  `${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
@@ -862,7 +875,7 @@ export class RunInstallUseCase {
862
875
 
863
876
  private async deploySharedSkills(input: RunInstallInput, skills: readonly string[], skillSet: SkillSet): Promise<void> {
864
877
  const targetRoot = join(input.projectRoot, "skills");
865
- await copySelectedSkillDirectories(input.harnessRoot, targetRoot, skills);
878
+ await copySelectedSkillDirectories(input.harnessRoot, input.projectRoot, targetRoot, skills);
866
879
  await writeFile(
867
880
  join(targetRoot, ".harness-version"),
868
881
  `${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,