phasegate 0.160.21 → 0.162.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 (96) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +6 -1
  3. package/docs/contracts/requirement-test-matrix.schema.json +1 -1
  4. package/docs/guide/cli-reference.md +1 -1
  5. package/docs/guide/configuration.md +31 -1
  6. package/docs/guide/skills-overview.md +2 -0
  7. package/docs/principles/model-routing.md +1 -0
  8. package/docs/templates/personal/phasegate-local-config.json +4 -1
  9. package/docs/templates/project/phasegate.config.json +4 -1
  10. package/package.json +6 -3
  11. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +91 -23
  12. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +4 -0
  13. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +54 -5
  14. package/scripts/harness/agent-integration/infrastructure/adapters/ci-governance-baseline-grandfather-adapter.ts +25 -2
  15. package/scripts/harness/agent-integration/infrastructure/adapters/phase-gate-query-adapter.ts +17 -2
  16. package/scripts/harness/biome-ast-engine/domain/value-objects/import-graph.ts +28 -1
  17. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +6 -1
  18. package/scripts/harness/config-foundation/domain/harness-config.ts +10 -0
  19. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +2 -0
  20. package/scripts/harness/config-foundation/domain/value-objects/l3-config.ts +4 -0
  21. package/scripts/harness/config-foundation/domain/value-objects/layers-config.ts +1 -1
  22. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +20 -5
  23. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -1
  24. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -1
  25. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -1
  26. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +39 -9
  27. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +39 -9
  28. package/scripts/harness/harness-api/infrastructure/adapters/phase-dependency-model-query-adapter.ts +18 -4
  29. package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +27 -4
  30. package/scripts/harness/installation/application/ports/model-delegation-port.ts +10 -0
  31. package/scripts/harness/installation/application/usecases/run-install.ts +67 -5
  32. package/scripts/harness/installation/application/usecases/run-reconcile.ts +53 -5
  33. package/scripts/harness/installation/composition-root.ts +4 -2
  34. package/scripts/harness/installation/infrastructure/adapters/skill-deployer-model-delegation-adapter.ts +16 -0
  35. package/scripts/harness/installation/presentation/cli/install-handler.ts +2 -1
  36. package/scripts/harness/main.ts +99 -13
  37. package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +9 -7
  38. package/scripts/harness/nyquist-validation/domain/services/matrix-validation-service.ts +27 -2
  39. package/scripts/harness/nyquist-validation/domain/services/requirement-intent-coverage-service.ts +4 -4
  40. package/scripts/harness/nyquist-validation/domain/value-objects/intent-coverage.ts +33 -0
  41. package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +3 -1
  42. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +8 -3
  43. package/scripts/harness/quick-mode/composition-root.ts +3 -0
  44. package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +6 -1
  45. package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-quick-mode-execution-adapter.ts +18 -0
  46. package/scripts/harness/quick-mode/presentation/handlers/ci-check-quick-mode-handler.ts +51 -5
  47. package/scripts/harness/setup/skill-deployer.ts +47 -0
  48. package/scripts/harness/skill-quality/domain/services/skill-structure-validator.ts +21 -0
  49. package/scripts/harness/skill-quality/domain/value-objects/skill-structure.ts +2 -0
  50. package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +12 -2
  51. package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +12 -2
  52. package/scripts/harness/traceability-model/application/usecases/apply-work-item-status-usecase.ts +6 -1
  53. package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +28 -9
  54. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +76 -25
  55. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +20 -8
  56. package/scripts/harness/validator-system/composition-root.ts +8 -1
  57. package/scripts/harness/validator-system/domain/ports/validator-config-port.ts +2 -0
  58. package/scripts/harness/validator-system/domain/services/validator-language-capability-service.ts +64 -0
  59. package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +16 -6
  60. package/scripts/harness/validator-system/infrastructure/adapters/file-system-work-item-reflection-adapter.ts +10 -1
  61. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +7 -1
  62. package/scripts/harness/validator-system/infrastructure/adapters/import-graph-source-analysis-adapter.ts +35 -3
  63. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +86 -14
  64. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +29 -3
  65. package/skills/cascade-updater/SKILL.md +1 -0
  66. package/skills/codebase-mapper/SKILL.md +1 -0
  67. package/skills/codex-delegator/SKILL.md +1 -0
  68. package/skills/consistency-checker/SKILL.md +1 -0
  69. package/skills/doc-freshness-checker/SKILL.md +1 -0
  70. package/skills/domain-designer/SKILL.md +1 -0
  71. package/skills/engineering-perspective/SKILL.md +1 -0
  72. package/skills/environment-designer/SKILL.md +1 -0
  73. package/skills/implementation-planner/SKILL.md +1 -0
  74. package/skills/implementation-readiness-checker/SKILL.md +1 -0
  75. package/skills/it-test-designer/SKILL.md +1 -0
  76. package/skills/it-test-logic-designer/SKILL.md +1 -0
  77. package/skills/logical-designer/SKILL.md +1 -0
  78. package/skills/mock-designer/SKILL.md +1 -0
  79. package/skills/phasegate-config-doctor/SKILL.md +1 -0
  80. package/skills/phasegate-toolkit-guide/SKILL.md +1 -0
  81. package/skills/pointer-validator/SKILL.md +1 -0
  82. package/skills/product-architect/SKILL.md +1 -0
  83. package/skills/quick-implementor/SKILL.md +1 -0
  84. package/skills/scenario-test-designer/SKILL.md +1 -0
  85. package/skills/scenario-test-logic-designer/SKILL.md +1 -0
  86. package/skills/skill-creator/SKILL.md +1 -0
  87. package/skills/story-implementor/SKILL.md +1 -0
  88. package/skills/story-mapper/SKILL.md +1 -0
  89. package/skills/story-writer/SKILL.md +1 -0
  90. package/skills/test-coverage-checker/SKILL.md +1 -0
  91. package/skills/uiux-designer/SKILL.md +1 -0
  92. package/skills/unit-designer/SKILL.md +1 -0
  93. package/skills/unit-test-designer/SKILL.md +1 -0
  94. package/skills/unit-test-logic-designer/SKILL.md +1 -0
  95. package/templates/.claude/scripts/deny-check.sh +73 -12
  96. package/templates/.claude/settings.json +4 -0
@@ -15,6 +15,7 @@
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";
@@ -25,6 +26,7 @@ import type { RepairMode } from "../../domain/repair-mode.js";
25
26
  import { getBundledSkillsForSet, type SkillSet } from "../bundled-skill-selection.js";
26
27
  import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
27
28
  import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
29
+ import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
28
30
 
29
31
  type InstallAction = "missing" | "will-merge" | "will-skip" | "will-overwrite";
30
32
  type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "text-managed" | "copy" | "copy-dir" | "symlink";
@@ -38,6 +40,10 @@ export interface InstallPlanItem {
38
40
  readonly summary: string;
39
41
  readonly diff: string;
40
42
  readonly skillHint: string | null;
43
+ // Non-null when a target could not be applied cleanly and the user must act
44
+ // (e.g. a pre-existing git hook that phasegate will not overwrite). Surfaces
45
+ // situations that would otherwise be silently skipped.
46
+ readonly warning: string | null;
41
47
  }
42
48
 
43
49
  export interface RunInstallInput {
@@ -132,13 +138,25 @@ async function copyDirectory(src: string, dest: string): Promise<void> {
132
138
  }
133
139
  }
134
140
 
135
- async function copySelectedSkillDirectories(harnessRoot: string, targetRoot: string, skills: readonly string[]): Promise<void> {
141
+ async function copySelectedSkillDirectories(
142
+ modelDelegation: ModelDelegationPort,
143
+ harnessRoot: string,
144
+ projectRoot: string,
145
+ targetRoot: string,
146
+ skills: readonly string[],
147
+ ): Promise<void> {
136
148
  await mkdir(targetRoot, { recursive: true });
149
+ const modelDelegationPolicy = await modelDelegation.readPolicy(projectRoot);
137
150
  for (const skill of skills) {
138
151
  const source = join(harnessRoot, "skills", skill);
139
152
  const target = join(targetRoot, skill);
140
153
  await rm(target, { recursive: true, force: true });
141
154
  await copyDirectory(source, target);
155
+ if (modelDelegationPolicy === "none") {
156
+ const skillPath = join(target, "SKILL.md");
157
+ const content = await readFile(skillPath, "utf8");
158
+ await writeFile(skillPath, modelDelegation.renderSkill(content, modelDelegationPolicy), "utf8");
159
+ }
142
160
  }
143
161
  }
144
162
 
@@ -152,8 +170,28 @@ async function listSelectedBundledSkills(harnessRoot: string, skillSet: SkillSet
152
170
  .sort();
153
171
  }
154
172
 
173
+ // Canonical, key-order-independent serialization used as a dedup key when
174
+ // merging hook arrays. Plain `JSON.stringify` is sensitive to object key order,
175
+ // so two structurally identical hook entries authored with different key orders
176
+ // would be treated as distinct and appended on every merge, breaking merge
177
+ // idempotency. Sorting keys recursively makes the key stable.
178
+ function canonicalize(value: unknown): unknown {
179
+ if (Array.isArray(value)) {
180
+ return value.map(canonicalize);
181
+ }
182
+ if (value !== null && typeof value === "object") {
183
+ const record = value as Record<string, unknown>;
184
+ const sorted: Record<string, unknown> = {};
185
+ for (const key of Object.keys(record).sort()) {
186
+ sorted[key] = canonicalize(record[key]);
187
+ }
188
+ return sorted;
189
+ }
190
+ return value;
191
+ }
192
+
155
193
  function normalizeJsonEntry(value: unknown): string {
156
- return JSON.stringify(value);
194
+ return JSON.stringify(canonicalize(value));
157
195
  }
158
196
 
159
197
  function mergeHookArrays(existing: unknown, incoming: unknown): unknown[] {
@@ -331,6 +369,7 @@ export class RunInstallUseCase {
331
369
  constructor(
332
370
  private readonly manifestRepository: ManifestRepositoryPort,
333
371
  private readonly hashCalculator: HashCalculatorPort,
372
+ private readonly modelDelegation: ModelDelegationPort,
334
373
  ) {}
335
374
 
336
375
  async execute(input: RunInstallInput): Promise<RunInstallResult> {
@@ -371,6 +410,18 @@ export class RunInstallUseCase {
371
410
  const next = unmanagedPersonalRuntimeFile ? before : this.merge(target, before, template, input.phasegateVersion);
372
411
  const didChange = before !== next;
373
412
  const action = this.actionFor(before, didChange, input.force);
413
+ // A `copy` target with a pre-existing, non-matching file is left untouched
414
+ // (merge returns `before`). For executable git hooks this means the
415
+ // phasegate hook is NOT wired in — warn instead of skipping silently.
416
+ const preservedExistingHook =
417
+ target.strategy === "copy"
418
+ && target.executable === true
419
+ && before !== null
420
+ && before.trim() !== template.trim()
421
+ && !input.force;
422
+ const warning = preservedExistingHook
423
+ ? `${target.path}: an existing hook was found and left unchanged; phasegate's checks are NOT wired in. Merge the phasegate hook manually (or rerun with --force to back up and overwrite).`
424
+ : null;
374
425
  const item: InstallPlanItem = {
375
426
  path: target.path,
376
427
  action,
@@ -379,9 +430,12 @@ export class RunInstallUseCase {
379
430
  changed: didChange,
380
431
  summary: unmanagedPersonalRuntimeFile
381
432
  ? `${target.path}: existing non-phasegate runtime path requires manual review`
382
- : didChange ? `${target.path}: ${action}` : `${target.path}: already up to date`,
433
+ : warning !== null
434
+ ? warning
435
+ : didChange ? `${target.path}: ${action}` : `${target.path}: already up to date`,
383
436
  diff: unmanagedPersonalRuntimeFile ? "manual review required" : this.diffSummary(before, next),
384
437
  skillHint: repairMode === "ai-assisted" ? SKILL_HINT : null,
438
+ warning,
385
439
  };
386
440
  plan.push(item);
387
441
 
@@ -445,7 +499,7 @@ export class RunInstallUseCase {
445
499
  plan.push(item);
446
500
  if (input.apply && item.changed && item.repairMode === "mechanical") {
447
501
  try {
448
- await copySelectedSkillDirectories(input.harnessRoot, join(input.projectRoot, skillPath), selectedPersonalSkills);
502
+ await copySelectedSkillDirectories(this.modelDelegation, input.harnessRoot, input.projectRoot, join(input.projectRoot, skillPath), selectedPersonalSkills);
449
503
  await writeFile(
450
504
  join(input.projectRoot, skillPath, ".harness-version"),
451
505
  `${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
@@ -535,6 +589,7 @@ export class RunInstallUseCase {
535
589
  summary: "~/.codex/config.toml: personal mode does not write user-level Codex feature flags; enable hooks manually when needed",
536
590
  diff: "manual Codex hooks feature enablement may be required",
537
591
  skillHint: null,
592
+ warning: null,
538
593
  });
539
594
  }
540
595
 
@@ -790,6 +845,7 @@ export class RunInstallUseCase {
790
845
  summary: `${relativePath}: existing non-directory skills path requires manual review`,
791
846
  diff: "manual review required",
792
847
  skillHint: null,
848
+ warning: null,
793
849
  };
794
850
  }
795
851
  } catch {}
@@ -817,6 +873,7 @@ export class RunInstallUseCase {
817
873
  summary: changed ? `${relativePath}: deploy bundled skills` : `${relativePath}: already up to date`,
818
874
  diff: changed ? `+ ${skills.length} bundled skills (${skillSet})` : "no changes",
819
875
  skillHint: null,
876
+ warning: null,
820
877
  };
821
878
  }
822
879
 
@@ -857,12 +914,13 @@ export class RunInstallUseCase {
857
914
  summary: changed ? "skills: deploy shared bundled skills" : "skills: already up to date",
858
915
  diff: changed ? `+ ${skills.length} bundled skills (${skillSet})` : "no changes",
859
916
  skillHint: null,
917
+ warning: null,
860
918
  };
861
919
  }
862
920
 
863
921
  private async deploySharedSkills(input: RunInstallInput, skills: readonly string[], skillSet: SkillSet): Promise<void> {
864
922
  const targetRoot = join(input.projectRoot, "skills");
865
- await copySelectedSkillDirectories(input.harnessRoot, targetRoot, skills);
923
+ await copySelectedSkillDirectories(this.modelDelegation, input.harnessRoot, input.projectRoot, targetRoot, skills);
866
924
  await writeFile(
867
925
  join(targetRoot, ".harness-version"),
868
926
  `${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
@@ -914,6 +972,7 @@ export class RunInstallUseCase {
914
972
  summary: `${relativePath}: already linked`,
915
973
  diff: "no changes",
916
974
  skillHint: null,
975
+ warning: null,
917
976
  };
918
977
  }
919
978
  return {
@@ -925,6 +984,7 @@ export class RunInstallUseCase {
925
984
  summary: `${relativePath}: existing non-phasegate path requires manual review`,
926
985
  diff: "manual review required",
927
986
  skillHint: null,
987
+ warning: null,
928
988
  };
929
989
  } catch {
930
990
  try {
@@ -939,6 +999,7 @@ export class RunInstallUseCase {
939
999
  summary: `${relativePath}: parent path exists and requires manual review`,
940
1000
  diff: "manual review required",
941
1001
  skillHint: null,
1002
+ warning: null,
942
1003
  };
943
1004
  }
944
1005
  } catch {}
@@ -951,6 +1012,7 @@ export class RunInstallUseCase {
951
1012
  summary: `${relativePath}: create symlink`,
952
1013
  diff: `+ symlink ${target}`,
953
1014
  skillHint: null,
1015
+ warning: null,
954
1016
  };
955
1017
  }
956
1018
  }
@@ -5,6 +5,7 @@
5
5
  // @work-item-id WI-198
6
6
  // @work-item-id WI-210
7
7
  // @work-item-id WI-216
8
+ // @work-item-id WI-219
8
9
 
9
10
  import { access, chmod, copyFile, lstat, mkdir, readFile, readlink, readdir, rm, symlink, writeFile } from "node:fs/promises";
10
11
  import { dirname, join, relative, resolve } from "node:path";
@@ -15,6 +16,7 @@ import type { RepairMode } from "../../domain/repair-mode.js";
15
16
  import { getBundledSkillsForSet, type SkillSet } from "../bundled-skill-selection.js";
16
17
  import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
17
18
  import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
19
+ import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
18
20
 
19
21
  type ReconcileAction = "missing-manifest" | "update" | "add" | "link" | "skip" | "refuse";
20
22
  type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "copy-dir" | "symlink" | "unknown";
@@ -96,13 +98,25 @@ async function copyDirectory(src: string, dest: string): Promise<void> {
96
98
  }
97
99
  }
98
100
 
99
- async function copySelectedSkillDirectories(harnessRoot: string, targetRoot: string, skills: readonly string[]): Promise<void> {
101
+ async function copySelectedSkillDirectories(
102
+ modelDelegation: ModelDelegationPort,
103
+ harnessRoot: string,
104
+ projectRoot: string,
105
+ targetRoot: string,
106
+ skills: readonly string[],
107
+ ): Promise<void> {
100
108
  await mkdir(targetRoot, { recursive: true });
109
+ const modelDelegationPolicy = await modelDelegation.readPolicy(projectRoot);
101
110
  for (const skill of skills) {
102
111
  const source = join(harnessRoot, "skills", skill);
103
112
  const target = join(targetRoot, skill);
104
113
  await rm(target, { recursive: true, force: true });
105
114
  await copyDirectory(source, target);
115
+ if (modelDelegationPolicy === "none") {
116
+ const skillPath = join(target, "SKILL.md");
117
+ const content = await readFile(skillPath, "utf8");
118
+ await writeFile(skillPath, modelDelegation.renderSkill(content, modelDelegationPolicy), "utf8");
119
+ }
106
120
  }
107
121
  }
108
122
 
@@ -115,6 +129,25 @@ async function listSelectedBundledSkills(harnessRoot: string, skillSet: SkillSet
115
129
  .sort();
116
130
  }
117
131
 
132
+ async function hasRenderedSkillDrift(
133
+ modelDelegation: ModelDelegationPort,
134
+ harnessRoot: string,
135
+ projectRoot: string,
136
+ targetRoot: string,
137
+ skills: readonly string[],
138
+ ): Promise<boolean> {
139
+ const modelDelegationPolicy = await modelDelegation.readPolicy(projectRoot);
140
+ for (const skill of skills) {
141
+ const current = await readTextOrNull(join(targetRoot, skill, "SKILL.md"));
142
+ if (current === null) return true;
143
+ const source = await readTextOrNull(join(harnessRoot, "skills", skill, "SKILL.md"));
144
+ if (source === null) continue;
145
+ const expected = modelDelegation.renderSkill(source, modelDelegationPolicy);
146
+ if (current !== expected) return true;
147
+ }
148
+ return false;
149
+ }
150
+
118
151
  function normalizeJsonEntry(value: unknown): string {
119
152
  return JSON.stringify(value);
120
153
  }
@@ -234,6 +267,7 @@ export class RunReconcileUseCase {
234
267
  constructor(
235
268
  private readonly manifestRepository: ManifestRepositoryPort,
236
269
  private readonly hashCalculator: HashCalculatorPort,
270
+ private readonly modelDelegation: ModelDelegationPort,
237
271
  ) {}
238
272
 
239
273
  async execute(input: RunReconcileInput): Promise<RunReconcileResult> {
@@ -491,7 +525,14 @@ export class RunReconcileUseCase {
491
525
  }
492
526
  const missingManifest = manifest.findEntry(SHARED_SKILLS_VERSION_PATH) === null
493
527
  || skills.some((skill) => manifest.findEntry(`skills/${skill}`) === null);
494
- const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest;
528
+ const renderedSkillDrift = await hasRenderedSkillDrift(
529
+ this.modelDelegation,
530
+ input.harnessRoot,
531
+ input.projectRoot,
532
+ this.resolveProjectPath(input.projectRoot, "skills"),
533
+ skills,
534
+ );
535
+ const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest || renderedSkillDrift;
495
536
  return {
496
537
  item: this.item(
497
538
  "skills",
@@ -514,7 +555,7 @@ export class RunReconcileUseCase {
514
555
 
515
556
  private async deploySharedSkills(input: RunReconcileInput, skills: readonly string[], skillSet: SkillSet): Promise<void> {
516
557
  const targetRoot = this.resolveProjectPath(input.projectRoot, "skills");
517
- await copySelectedSkillDirectories(input.harnessRoot, targetRoot, skills);
558
+ await copySelectedSkillDirectories(this.modelDelegation, input.harnessRoot, input.projectRoot, targetRoot, skills);
518
559
  await writeFile(
519
560
  join(targetRoot, ".harness-version"),
520
561
  `${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
@@ -541,7 +582,14 @@ export class RunReconcileUseCase {
541
582
  }
542
583
  const missingManifest = manifest.findEntry(`${relativePath}/.harness-version`) === null
543
584
  || skills.some((skill) => manifest.findEntry(`${relativePath}/${skill}`) === null);
544
- const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest;
585
+ const renderedSkillDrift = await hasRenderedSkillDrift(
586
+ this.modelDelegation,
587
+ input.harnessRoot,
588
+ input.projectRoot,
589
+ this.resolveProjectPath(input.projectRoot, relativePath),
590
+ skills,
591
+ );
592
+ const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest || renderedSkillDrift;
545
593
  return {
546
594
  item: this.item(
547
595
  relativePath,
@@ -557,7 +605,7 @@ export class RunReconcileUseCase {
557
605
  apply: async () => {
558
606
  if (!changed) return null;
559
607
  const targetRoot = this.resolveProjectPath(input.projectRoot, relativePath);
560
- await copySelectedSkillDirectories(input.harnessRoot, targetRoot, skills);
608
+ await copySelectedSkillDirectories(this.modelDelegation, input.harnessRoot, input.projectRoot, targetRoot, skills);
561
609
  await writeFile(
562
610
  join(targetRoot, ".harness-version"),
563
611
  `${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
@@ -26,6 +26,7 @@ import type { UninstallReverseStrategy } from "./domain/ports/uninstall-reverse-
26
26
  import { FileSystemManifestRepositoryAdapter } from "./infrastructure/adapters/file-system-manifest-repository-adapter.js";
27
27
  import { NodeCryptoHashAdapter } from "./infrastructure/adapters/node-crypto-hash-adapter.js";
28
28
  import { NodeFsFileInspectorAdapter } from "./infrastructure/adapters/node-fs-file-inspector-adapter.js";
29
+ import { SkillDeployerModelDelegationAdapter } from "./infrastructure/adapters/skill-deployer-model-delegation-adapter.js";
29
30
  import { DoctorHandler } from "./presentation/cli/doctor-handler.js";
30
31
  import { InstallHandler } from "./presentation/cli/install-handler.js";
31
32
  import { ReconcileHandler } from "./presentation/cli/reconcile-handler.js";
@@ -43,6 +44,7 @@ export function createInstallationModule() {
43
44
  const inspector = new NodeFsFileInspectorAdapter();
44
45
  const manifestRepository = new FileSystemManifestRepositoryAdapter();
45
46
  const hashCalculator = new NodeCryptoHashAdapter();
47
+ const modelDelegation = new SkillDeployerModelDelegationAdapter();
46
48
  const checks = [
47
49
  new ClaudeHookMissingCheck(),
48
50
  new ClaudeContextMissingCheck(),
@@ -58,8 +60,8 @@ export function createInstallationModule() {
58
60
  new WiWorkflowDriftCheck(),
59
61
  ];
60
62
  const runDoctorDiagnosticsUseCase = new RunDoctorDiagnosticsUseCase(checks, inspector, manifestRepository);
61
- const runInstallUseCase = new RunInstallUseCase(manifestRepository, hashCalculator);
62
- const runReconcileUseCase = new RunReconcileUseCase(manifestRepository, hashCalculator);
63
+ const runInstallUseCase = new RunInstallUseCase(manifestRepository, hashCalculator, modelDelegation);
64
+ const runReconcileUseCase = new RunReconcileUseCase(manifestRepository, hashCalculator, modelDelegation);
63
65
  const runUninstallUseCase = new RunUninstallUseCase(manifestRepository, hashCalculator);
64
66
  return {
65
67
  manifestRepository,
@@ -0,0 +1,16 @@
1
+ // @unit installation
2
+ // @layer infrastructure
3
+ // @work-item-id WI-219
4
+
5
+ import { readModelDelegationPolicy, renderSkillForModelDelegation } from "../../../setup/skill-deployer.js";
6
+ import type { ModelDelegationPolicy, ModelDelegationPort } from "../../application/ports/model-delegation-port.js";
7
+
8
+ export class SkillDeployerModelDelegationAdapter implements ModelDelegationPort {
9
+ readPolicy(projectRoot: string): Promise<ModelDelegationPolicy> {
10
+ return readModelDelegationPolicy(projectRoot);
11
+ }
12
+
13
+ renderSkill(content: string, policy: ModelDelegationPolicy): string {
14
+ return renderSkillForModelDelegation(content, policy);
15
+ }
16
+ }
@@ -44,7 +44,8 @@ export class InstallHandler {
44
44
  input.apply ? `phasegate install${input.personal ? " --personal" : ""} apply` : `phasegate install${input.personal ? " --personal" : ""} dry-run`,
45
45
  ...result.plan.map((item) => {
46
46
  const hint = item.skillHint ? `; hint: ${item.skillHint}` : "";
47
- return `- ${item.path}: ${item.action} (${item.repairMode}, ${item.strategy}); diff: ${item.diff}${hint}`;
47
+ const warn = item.warning ? `\n WARNING: ${item.warning}` : "";
48
+ return `- ${item.path}: ${item.action} (${item.repairMode}, ${item.strategy}); diff: ${item.diff}${hint}${warn}`;
48
49
  }),
49
50
  ];
50
51
  if (result.backupDir !== null) lines.push(`backups: ${result.backupDir}`);
@@ -10,6 +10,7 @@
10
10
  * @work-item-id WI-189
11
11
  * @work-item-id WI-191
12
12
  * @work-item-id WI-195
13
+ * @work-item-id WI-212
13
14
  * @work-item-id WI-196
14
15
  * @work-item-id WI-197
15
16
  * @work-item-id WI-200
@@ -19,6 +20,7 @@
19
20
  * @work-item-id WI-206
20
21
  * @work-item-id WI-213
21
22
  * @work-item-id WI-217
23
+ * @work-item-id WI-219
22
24
  *
23
25
  * Phasegate CLI エントリポイント。
24
26
  * 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
@@ -36,6 +38,7 @@ import {
36
38
  writeFile as fsWriteFile,
37
39
  } from "node:fs/promises";
38
40
  import { dirname, join, resolve } from "node:path";
41
+ import { fileURLToPath } from "node:url";
39
42
  import { createAdrFoundationModule } from "./adr-foundation/composition-root.js";
40
43
  import { createBiomeAstEngineModule } from "./biome-ast-engine/composition-root.js";
41
44
  import { buildCiGovernance } from "./ci-governance/composition-root.js";
@@ -122,6 +125,19 @@ function isJsonRecord(value: unknown): value is Record<string, unknown> {
122
125
  return typeof value === "object" && value !== null && !Array.isArray(value);
123
126
  }
124
127
 
128
+ async function isModelDelegationDisabled(rootDir: string): Promise<boolean> {
129
+ try {
130
+ const raw = await fsReadFile(join(rootDir, "phasegate.config.json"), "utf-8");
131
+ const parsed = JSON.parse(raw) as unknown;
132
+ if (!isJsonRecord(parsed) || !isJsonRecord(parsed.modelRouting)) {
133
+ return false;
134
+ }
135
+ return parsed.modelRouting.delegation === "none";
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+
125
141
  async function ensurePhasegatePackageDependency(rootDir: string, version: string): Promise<PackageDependencyResult> {
126
142
  const packageJsonPath = join(rootDir, "package.json");
127
143
  let pkg: PackageJsonDocument = {};
@@ -167,7 +183,7 @@ Setup:
167
183
  init Initialize project: deploy skills + design docs + phasegate.config.json
168
184
  (--name <project-name>, --preset <full|standard|minimal|custom>,
169
185
  --skills <core|all>, --agent <claude|codex|both>, --workflow <standard|strict>,
170
- --with-husky, --with-ci, --yes)
186
+ --language <language>, --with-husky, --with-ci, --yes)
171
187
  update-skills Alias for reconcile (kept for compatibility)
172
188
  doctor Diagnose silent installation failures (--json, --strict, --personal, --agent <claude|codex|both>, --report-out <path>)
173
189
  scaffold-wi <unit|_cross> <story|issue|fix|refactor|chore>
@@ -320,21 +336,42 @@ async function listFilesRecursive(root: string): Promise<string[]> {
320
336
  }
321
337
  }
322
338
 
323
- async function nextWorkItemId(rootDir: string, inceptionRoot = "docs/inception"): Promise<string> {
324
- const files = await listFilesRecursive(join(rootDir, inceptionRoot));
339
+ // Normalize path separators so scanning works on Windows (`\`) as well as POSIX.
340
+ export function toPosixPath(filePath: string): string {
341
+ return filePath.replace(/\\/g, "/");
342
+ }
343
+
344
+ // Derive the next WI id from an already-collected list of file paths. Exposed
345
+ // (with the file list injected) so the numbering rules — Windows separator
346
+ // handling and dynamic zero-pad width past WI-999 — can be unit tested without
347
+ // touching the filesystem.
348
+ export function computeNextWorkItemId(files: readonly string[]): string {
325
349
  let max = 0;
350
+ let maxWidth = 3;
326
351
  for (const file of files) {
327
- const match = file.match(/\/WI-(\d{3})\/description\.md$/);
328
- if (match) max = Math.max(max, Number(match[1]));
352
+ const match = toPosixPath(file).match(/\/WI-(\d+)\/description\.md$/);
353
+ if (match) {
354
+ max = Math.max(max, Number(match[1]));
355
+ maxWidth = Math.max(maxWidth, match[1].length);
356
+ }
329
357
  }
330
- return `WI-${String(max + 1).padStart(3, "0")}`;
358
+ const next = max + 1;
359
+ return `WI-${String(next).padStart(maxWidth, "0")}`;
360
+ }
361
+
362
+ async function nextWorkItemId(rootDir: string, inceptionRoot = "docs/inception"): Promise<string> {
363
+ const files = await listFilesRecursive(join(rootDir, inceptionRoot));
364
+ return computeNextWorkItemId(files);
331
365
  }
332
366
 
333
367
  async function countLegacyPlansWithoutWorkItems(rootDir: string): Promise<number> {
334
368
  const files = await listFilesRecursive(join(rootDir, "docs", "inception"));
335
- const hasWorkItem = files.some((file) => /\/WI-\d{3}\/description\.md$/.test(file));
369
+ const hasWorkItem = files.some((file) => /\/WI-\d+\/description\.md$/.test(toPosixPath(file)));
336
370
  if (hasWorkItem) return 0;
337
- return files.filter((file) => file.includes("/codding_plan/") || file.endsWith("_plan.md")).length;
371
+ return files.filter((file) => {
372
+ const posix = toPosixPath(file);
373
+ return posix.includes("/codding_plan/") || posix.endsWith("_plan.md");
374
+ }).length;
338
375
  }
339
376
 
340
377
  async function scaffoldInceptionRoots(rootDir: string, unit: string | null = null, inceptionRoot = "docs/inception"): Promise<void> {
@@ -605,6 +642,7 @@ Options:
605
642
  --skills <core|all> Skill set to deploy (default: "all")
606
643
  --agent <claude|codex|both> Agent integration target (default: "claude")
607
644
  --workflow <standard|strict> Workflow enforcement defaults (default: "standard")
645
+ --language <language> Project language declaration (default: "typescript")
608
646
  --with-husky Install Husky pre-commit hooks
609
647
  --with-ci Install GitHub Actions workflows
610
648
  --yes Skip confirmation prompts
@@ -1362,11 +1400,11 @@ function buildConfigPatchPreview(intent: ConfigChangeIntent, before: unknown | n
1362
1400
  const configIntents: Record<ConfigChangeIntent, readonly { readonly pointer: string; readonly path: readonly string[]; readonly value: unknown }[]> = {
1363
1401
  "l4-strict": [
1364
1402
  { pointer: "/layers/L4/enabled", path: ["layers", "L4", "enabled"], value: true },
1365
- { pointer: "/layers/L4/failOnWarning", path: ["layers", "L4", "failOnWarning"], value: true },
1403
+ { pointer: "/validate/failOnWarning", path: ["validate", "failOnWarning"], value: true },
1366
1404
  ],
1367
1405
  "ci-fail-on-warning": [
1368
1406
  { pointer: "/ci/enabled", path: ["ci", "enabled"], value: true },
1369
- { pointer: "/layers/L4/failOnWarning", path: ["layers", "L4", "failOnWarning"], value: true },
1407
+ { pointer: "/validate/failOnWarning", path: ["validate", "failOnWarning"], value: true },
1370
1408
  ],
1371
1409
  "quick-mode-strict": [
1372
1410
  { pointer: "/quickMode/allowedCategories", path: ["quickMode", "allowedCategories"], value: ["bugfix"] },
@@ -1467,7 +1505,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1467
1505
  readonly risks: readonly string[];
1468
1506
  }> = {
1469
1507
  "l4-strict": {
1470
- targets: ["phasegate.config.json: layers.L4.enabled", "phasegate.config.json: layers.L4.failOnWarning"],
1508
+ targets: ["phasegate.config.json: layers.L4.enabled", "phasegate.config.json: validate.failOnWarning"],
1471
1509
  managedTargets: ["phasegate.config.json"],
1472
1510
  externalActions: [],
1473
1511
  commands: ["phasegate config:plan --intent l4-strict --apply --json", "phasegate validate --layer L4 --fail-on-warning --format human"],
@@ -1740,6 +1778,7 @@ async function main(): Promise<void> {
1740
1778
  "--skills",
1741
1779
  "--agent",
1742
1780
  "--workflow",
1781
+ "--language",
1743
1782
  "--with-husky",
1744
1783
  "--with-ci",
1745
1784
  "--yes",
@@ -1780,6 +1819,11 @@ async function main(): Promise<void> {
1780
1819
  process.exit(2);
1781
1820
  }
1782
1821
  const workflow = parseWorkflowMode(workflowRaw);
1822
+ const language = parseFlag(args, "--language") ?? "typescript";
1823
+ if (language.trim() === "") {
1824
+ console.error("Invalid --language value: value must not be empty.");
1825
+ process.exit(2);
1826
+ }
1783
1827
  const deployClaude = agent === "claude" || agent === "both";
1784
1828
  const deployCodex = agent === "codex" || agent === "both";
1785
1829
  const result = await deploySkills(harnessRoot, rootDir, skillSet);
@@ -1792,6 +1836,7 @@ async function main(): Promise<void> {
1792
1836
  const configResult = await initHarnessConfig(rootDir, projectName, phasePreset, {
1793
1837
  ciEnabled: withCi,
1794
1838
  workflow,
1839
+ language,
1795
1840
  });
1796
1841
  if (workflow === "strict") {
1797
1842
  await scaffoldInceptionRoots(rootDir);
@@ -2743,9 +2788,28 @@ async function main(): Promise<void> {
2743
2788
  }
2744
2789
 
2745
2790
  case "phasegate:generate-matrix": {
2791
+ // REAL registry: 有効 storyId 一覧を traceability-model の StoryCatalog から取得する。
2792
+ // 空スタブだと生成マトリクスの storyId 整合性検査が事実上 no-op になるため配線する。
2793
+ // config・catalog が読めない環境(config 不在など)では空配列にフォールバックし、
2794
+ // マトリクス生成自体は継続する。
2795
+ const loadCatalogStoryIds = async (): Promise<readonly string[]> => {
2796
+ try {
2797
+ const { createConfigFoundationModule } = await import("./config-foundation/composition-root.js");
2798
+ const configModule = createConfigFoundationModule();
2799
+ const resolvedConfig = await configModule.usecases.loadResolvedConfigUseCase.execute();
2800
+ const { createTraceabilityModelModule } = await import("./traceability-model/composition-root.js");
2801
+ const traceModule = createTraceabilityModelModule(process.cwd(), {
2802
+ pathRoots: { designDocsRoot: resolvedConfig.config.paths.designDocs },
2803
+ });
2804
+ const catalogStoryIds = await traceModule.storyCatalog.getAllStoryIds();
2805
+ return catalogStoryIds.map((s) => s.value);
2806
+ } catch {
2807
+ return [];
2808
+ }
2809
+ };
2746
2810
  const { createNyquistValidationModule } = await import("./nyquist-validation/composition-root.js");
2747
2811
  const mod = createNyquistValidationModule({
2748
- getStoryIds: async () => [],
2812
+ getStoryIds: loadCatalogStoryIds,
2749
2813
  });
2750
2814
  const flags: Record<string, boolean | string> = {};
2751
2815
  if (json) flags.json = true;
@@ -3133,6 +3197,13 @@ Examples:
3133
3197
  printSubcommandHelp("delegate-sonnet");
3134
3198
  process.exit(0);
3135
3199
  }
3200
+ if (!hasFlag(args, "--dry-run") && await isModelDelegationDisabled(rootDir)) {
3201
+ console.error(JSON.stringify({
3202
+ code: "MODEL_DELEGATION_DISABLED",
3203
+ message: "phasegate delegate-sonnet is disabled by modelRouting.delegation=none",
3204
+ }));
3205
+ process.exit(1);
3206
+ }
3136
3207
  const { spawn } = await import("node:child_process");
3137
3208
  const scriptPath = join(harnessRoot, "scripts/delegate-sonnet.sh");
3138
3209
  const forwardArgs = args.slice(1);
@@ -3220,4 +3291,19 @@ Examples:
3220
3291
  }
3221
3292
  }
3222
3293
 
3223
- main();
3294
+ // Only run the CLI when this module is the process entry point. Guarding this
3295
+ // keeps `main()` from executing (and calling process.exit) when the module is
3296
+ // imported for unit testing of exported helpers such as computeNextWorkItemId.
3297
+ function isMainModule(): boolean {
3298
+ const entry = process.argv[1];
3299
+ if (!entry) return false;
3300
+ try {
3301
+ return resolve(entry) === fileURLToPath(import.meta.url);
3302
+ } catch {
3303
+ return false;
3304
+ }
3305
+ }
3306
+
3307
+ if (isMainModule()) {
3308
+ main();
3309
+ }
@@ -3,6 +3,13 @@
3
3
  // @work-item-id WI-125
4
4
  // @work-item-id WI-131
5
5
 
6
+ // Intent coverage 系の型は domain 層(value-objects/intent-coverage)を正準とし、
7
+ // application 層はそれを参照・再エクスポートする(domain ← application の正しい依存方向)。
8
+ import type {
9
+ IntentCoverageStatus as DomainIntentCoverageStatus,
10
+ IntentCoverageItem as DomainIntentCoverageItem,
11
+ } from '../../domain/value-objects/intent-coverage.js';
12
+
6
13
  export interface RequirementSourceDto {
7
14
  readonly storyId: string;
8
15
  readonly acIds: readonly string[];
@@ -48,14 +55,9 @@ export interface OrphanTestDto {
48
55
  readonly testName?: string;
49
56
  }
50
57
 
51
- export type IntentCoverageStatus = 'observed' | 'weakly-observed' | 'unobserved';
58
+ export type IntentCoverageStatus = DomainIntentCoverageStatus;
52
59
 
53
- export interface IntentCoverageItemDto {
54
- readonly storyId: string;
55
- readonly acId: string;
56
- readonly status: IntentCoverageStatus;
57
- readonly warnings: readonly string[];
58
- }
60
+ export type IntentCoverageItemDto = DomainIntentCoverageItem;
59
61
 
60
62
  export interface MatrixGenerationReportDto {
61
63
  readonly missingTests: readonly MissingTestDto[];