phasegate 0.160.20 → 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 (68) hide show
  1. package/CHANGELOG.md +22 -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/hooks/pre-commit +16 -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 +1 -1
  11. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +54 -5
  12. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +24 -3
  13. package/scripts/harness/config-foundation/domain/harness-config.ts +9 -0
  14. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +2 -0
  15. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +20 -5
  16. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +25 -0
  17. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +26 -3
  18. package/scripts/harness/installation/application/usecases/run-install.ts +16 -3
  19. package/scripts/harness/installation/application/usecases/run-reconcile.ts +48 -5
  20. package/scripts/harness/main.ts +71 -19
  21. package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +6 -1
  22. package/scripts/harness/setup/skill-deployer.ts +47 -0
  23. package/scripts/harness/skill-quality/domain/services/skill-structure-validator.ts +21 -0
  24. package/scripts/harness/skill-quality/domain/value-objects/skill-structure.ts +2 -0
  25. package/scripts/harness/validator-system/application/dto/validation-result-contract.ts +1 -0
  26. package/scripts/harness/validator-system/application/mappers/validation-result-contract-mapper.ts +1 -0
  27. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +18 -5
  28. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +48 -6
  29. package/scripts/harness/validator-system/composition-root.ts +15 -1
  30. package/scripts/harness/validator-system/domain/ports/validator-config-port.ts +2 -0
  31. package/scripts/harness/validator-system/domain/services/l4/consistency-check-service.ts +78 -1
  32. package/scripts/harness/validator-system/domain/services/validator-language-capability-service.ts +64 -0
  33. package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +7 -0
  34. package/scripts/harness/validator-system/infrastructure/adapters/file-system-work-item-reflection-adapter.ts +102 -0
  35. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +7 -1
  36. package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +3 -0
  37. package/scripts/harness/validator-system/presentation/formatters/human-validation-result-formatter.ts +3 -0
  38. package/skills/cascade-updater/SKILL.md +1 -0
  39. package/skills/codebase-mapper/SKILL.md +1 -0
  40. package/skills/codex-delegator/SKILL.md +1 -0
  41. package/skills/consistency-checker/SKILL.md +1 -0
  42. package/skills/doc-freshness-checker/SKILL.md +1 -0
  43. package/skills/domain-designer/SKILL.md +1 -0
  44. package/skills/engineering-perspective/SKILL.md +1 -0
  45. package/skills/environment-designer/SKILL.md +1 -0
  46. package/skills/implementation-planner/SKILL.md +1 -0
  47. package/skills/implementation-readiness-checker/SKILL.md +1 -0
  48. package/skills/it-test-designer/SKILL.md +1 -0
  49. package/skills/it-test-logic-designer/SKILL.md +1 -0
  50. package/skills/logical-designer/SKILL.md +1 -0
  51. package/skills/mock-designer/SKILL.md +1 -0
  52. package/skills/phasegate-config-doctor/SKILL.md +1 -0
  53. package/skills/phasegate-toolkit-guide/SKILL.md +1 -0
  54. package/skills/pointer-validator/SKILL.md +1 -0
  55. package/skills/product-architect/SKILL.md +1 -0
  56. package/skills/quick-implementor/SKILL.md +1 -0
  57. package/skills/scenario-test-designer/SKILL.md +1 -0
  58. package/skills/scenario-test-logic-designer/SKILL.md +1 -0
  59. package/skills/skill-creator/SKILL.md +1 -0
  60. package/skills/story-implementor/SKILL.md +1 -0
  61. package/skills/story-mapper/SKILL.md +1 -0
  62. package/skills/story-writer/SKILL.md +1 -0
  63. package/skills/test-coverage-checker/SKILL.md +1 -0
  64. package/skills/uiux-designer/SKILL.md +1 -0
  65. package/skills/unit-designer/SKILL.md +1 -0
  66. package/skills/unit-test-designer/SKILL.md +1 -0
  67. package/skills/unit-test-logic-designer/SKILL.md +1 -0
  68. package/templates/.claude/settings.json +4 -0
@@ -5,9 +5,11 @@
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";
12
+ import { readModelDelegationPolicy, renderSkillForModelDelegation } from "../../../setup/skill-deployer.js";
11
13
  import { DeploymentEntry } from "../../domain/deployment-entry.js";
12
14
  import { DeploymentManifest } from "../../domain/deployment-manifest.js";
13
15
  import type { ManagedBlockInput } from "../../domain/managed-block.js";
@@ -96,13 +98,24 @@ 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
+ harnessRoot: string,
103
+ projectRoot: string,
104
+ targetRoot: string,
105
+ skills: readonly string[],
106
+ ): Promise<void> {
100
107
  await mkdir(targetRoot, { recursive: true });
108
+ const modelDelegationPolicy = await readModelDelegationPolicy(projectRoot);
101
109
  for (const skill of skills) {
102
110
  const source = join(harnessRoot, "skills", skill);
103
111
  const target = join(targetRoot, skill);
104
112
  await rm(target, { recursive: true, force: true });
105
113
  await copyDirectory(source, target);
114
+ if (modelDelegationPolicy === "none") {
115
+ const skillPath = join(target, "SKILL.md");
116
+ const content = await readFile(skillPath, "utf8");
117
+ await writeFile(skillPath, renderSkillForModelDelegation(content, modelDelegationPolicy), "utf8");
118
+ }
106
119
  }
107
120
  }
108
121
 
@@ -115,6 +128,24 @@ async function listSelectedBundledSkills(harnessRoot: string, skillSet: SkillSet
115
128
  .sort();
116
129
  }
117
130
 
131
+ async function hasRenderedSkillDrift(
132
+ harnessRoot: string,
133
+ projectRoot: string,
134
+ targetRoot: string,
135
+ skills: readonly string[],
136
+ ): Promise<boolean> {
137
+ const modelDelegationPolicy = await readModelDelegationPolicy(projectRoot);
138
+ for (const skill of skills) {
139
+ const current = await readTextOrNull(join(targetRoot, skill, "SKILL.md"));
140
+ if (current === null) return true;
141
+ const source = await readTextOrNull(join(harnessRoot, "skills", skill, "SKILL.md"));
142
+ if (source === null) continue;
143
+ const expected = renderSkillForModelDelegation(source, modelDelegationPolicy);
144
+ if (current !== expected) return true;
145
+ }
146
+ return false;
147
+ }
148
+
118
149
  function normalizeJsonEntry(value: unknown): string {
119
150
  return JSON.stringify(value);
120
151
  }
@@ -491,7 +522,13 @@ export class RunReconcileUseCase {
491
522
  }
492
523
  const missingManifest = manifest.findEntry(SHARED_SKILLS_VERSION_PATH) === null
493
524
  || skills.some((skill) => manifest.findEntry(`skills/${skill}`) === null);
494
- const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest;
525
+ const renderedSkillDrift = await hasRenderedSkillDrift(
526
+ input.harnessRoot,
527
+ input.projectRoot,
528
+ this.resolveProjectPath(input.projectRoot, "skills"),
529
+ skills,
530
+ );
531
+ const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest || renderedSkillDrift;
495
532
  return {
496
533
  item: this.item(
497
534
  "skills",
@@ -514,7 +551,7 @@ export class RunReconcileUseCase {
514
551
 
515
552
  private async deploySharedSkills(input: RunReconcileInput, skills: readonly string[], skillSet: SkillSet): Promise<void> {
516
553
  const targetRoot = this.resolveProjectPath(input.projectRoot, "skills");
517
- await copySelectedSkillDirectories(input.harnessRoot, targetRoot, skills);
554
+ await copySelectedSkillDirectories(input.harnessRoot, input.projectRoot, targetRoot, skills);
518
555
  await writeFile(
519
556
  join(targetRoot, ".harness-version"),
520
557
  `${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
@@ -541,7 +578,13 @@ export class RunReconcileUseCase {
541
578
  }
542
579
  const missingManifest = manifest.findEntry(`${relativePath}/.harness-version`) === null
543
580
  || skills.some((skill) => manifest.findEntry(`${relativePath}/${skill}`) === null);
544
- const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest;
581
+ const renderedSkillDrift = await hasRenderedSkillDrift(
582
+ input.harnessRoot,
583
+ input.projectRoot,
584
+ this.resolveProjectPath(input.projectRoot, relativePath),
585
+ skills,
586
+ );
587
+ const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest || renderedSkillDrift;
545
588
  return {
546
589
  item: this.item(
547
590
  relativePath,
@@ -557,7 +600,7 @@ export class RunReconcileUseCase {
557
600
  apply: async () => {
558
601
  if (!changed) return null;
559
602
  const targetRoot = this.resolveProjectPath(input.projectRoot, relativePath);
560
- await copySelectedSkillDirectories(input.harnessRoot, targetRoot, skills);
603
+ await copySelectedSkillDirectories(input.harnessRoot, input.projectRoot, targetRoot, skills);
561
604
  await writeFile(
562
605
  join(targetRoot, ".harness-version"),
563
606
  `${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
@@ -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
@@ -18,6 +19,8 @@
18
19
  * @work-item-id WI-205
19
20
  * @work-item-id WI-206
20
21
  * @work-item-id WI-213
22
+ * @work-item-id WI-217
23
+ * @work-item-id WI-219
21
24
  *
22
25
  * Phasegate CLI エントリポイント。
23
26
  * 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
@@ -121,6 +124,19 @@ function isJsonRecord(value: unknown): value is Record<string, unknown> {
121
124
  return typeof value === "object" && value !== null && !Array.isArray(value);
122
125
  }
123
126
 
127
+ async function isModelDelegationDisabled(rootDir: string): Promise<boolean> {
128
+ try {
129
+ const raw = await fsReadFile(join(rootDir, "phasegate.config.json"), "utf-8");
130
+ const parsed = JSON.parse(raw) as unknown;
131
+ if (!isJsonRecord(parsed) || !isJsonRecord(parsed.modelRouting)) {
132
+ return false;
133
+ }
134
+ return parsed.modelRouting.delegation === "none";
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+
124
140
  async function ensurePhasegatePackageDependency(rootDir: string, version: string): Promise<PackageDependencyResult> {
125
141
  const packageJsonPath = join(rootDir, "package.json");
126
142
  let pkg: PackageJsonDocument = {};
@@ -166,10 +182,10 @@ Setup:
166
182
  init Initialize project: deploy skills + design docs + phasegate.config.json
167
183
  (--name <project-name>, --preset <full|standard|minimal|custom>,
168
184
  --skills <core|all>, --agent <claude|codex|both>, --workflow <standard|strict>,
169
- --with-husky, --with-ci, --yes)
185
+ --language <language>, --with-husky, --with-ci, --yes)
170
186
  update-skills Alias for reconcile (kept for compatibility)
171
187
  doctor Diagnose silent installation failures (--json, --strict, --personal, --agent <claude|codex|both>, --report-out <path>)
172
- scaffold-wi <unit|_cross> <story|issue|chore>
188
+ scaffold-wi <unit|_cross> <story|issue|fix|refactor|chore>
173
189
  Create docs/inception/{unit}/WI-XXX/description.md
174
190
  emit-agent-rules Print AGENTS.md / CLAUDE.md WI workflow rules block
175
191
  install Install phasegate managed files (--dry-run|--apply, --force, --personal, --agent <claude|codex|both>)
@@ -280,14 +296,14 @@ function hasFlag(args: readonly string[], flag: string): boolean {
280
296
  }
281
297
 
282
298
  type WorkflowMode = "standard" | "strict";
283
- type ScaffoldWorkItemType = "story" | "issue" | "chore";
299
+ type ScaffoldWorkItemType = "story" | "issue" | "fix" | "refactor" | "chore";
284
300
 
285
301
  function parseWorkflowMode(value: string | undefined): WorkflowMode {
286
302
  return value === "strict" ? "strict" : "standard";
287
303
  }
288
304
 
289
305
  function parseScaffoldWorkItemType(value: string | undefined): ScaffoldWorkItemType | null {
290
- if (value === "story" || value === "issue" || value === "chore") return value;
306
+ if (value === "story" || value === "issue" || value === "fix" || value === "refactor" || value === "chore") return value;
291
307
  return null;
292
308
  }
293
309
 
@@ -319,8 +335,8 @@ async function listFilesRecursive(root: string): Promise<string[]> {
319
335
  }
320
336
  }
321
337
 
322
- async function nextWorkItemId(rootDir: string): Promise<string> {
323
- const files = await listFilesRecursive(join(rootDir, "docs", "inception"));
338
+ async function nextWorkItemId(rootDir: string, inceptionRoot = "docs/inception"): Promise<string> {
339
+ const files = await listFilesRecursive(join(rootDir, inceptionRoot));
324
340
  let max = 0;
325
341
  for (const file of files) {
326
342
  const match = file.match(/\/WI-(\d{3})\/description\.md$/);
@@ -336,19 +352,25 @@ async function countLegacyPlansWithoutWorkItems(rootDir: string): Promise<number
336
352
  return files.filter((file) => file.includes("/codding_plan/") || file.endsWith("_plan.md")).length;
337
353
  }
338
354
 
339
- async function scaffoldInceptionRoots(rootDir: string, unit: string | null = null): Promise<void> {
340
- await fsMkdir(join(rootDir, "docs", "inception", "_shared"), { recursive: true });
341
- await fsMkdir(join(rootDir, "docs", "inception", "_cross"), { recursive: true });
355
+ async function scaffoldInceptionRoots(rootDir: string, unit: string | null = null, inceptionRoot = "docs/inception"): Promise<void> {
356
+ await fsMkdir(join(rootDir, inceptionRoot, "_shared"), { recursive: true });
357
+ await fsMkdir(join(rootDir, inceptionRoot, "_cross"), { recursive: true });
342
358
  if (unit && unit !== "_cross" && unit !== "_shared") {
343
- await fsMkdir(join(rootDir, "docs", "inception", unit), { recursive: true });
344
- await fsWriteFile(join(rootDir, "docs", "inception", unit, ".gitkeep"), "", "utf8").catch(() => undefined);
359
+ await fsMkdir(join(rootDir, inceptionRoot, unit), { recursive: true });
360
+ await fsWriteFile(join(rootDir, inceptionRoot, unit, ".gitkeep"), "", "utf8").catch(() => undefined);
345
361
  }
346
362
  }
347
363
 
348
- async function scaffoldWorkItem(rootDir: string, unit: string, type: ScaffoldWorkItemType): Promise<string> {
349
- const id = await nextWorkItemId(rootDir);
350
- await scaffoldInceptionRoots(rootDir, unit);
351
- const targetBase = unit === "_cross" ? join(rootDir, "docs", "inception", "_cross") : join(rootDir, "docs", "inception", unit);
364
+ async function scaffoldWorkItem(
365
+ rootDir: string,
366
+ unit: string,
367
+ type: ScaffoldWorkItemType,
368
+ options: { readonly id?: string; readonly inceptionRoot?: string } = {},
369
+ ): Promise<string> {
370
+ const inceptionRoot = options.inceptionRoot ?? "docs/inception";
371
+ const id = options.id ?? await nextWorkItemId(rootDir, inceptionRoot);
372
+ await scaffoldInceptionRoots(rootDir, unit, inceptionRoot);
373
+ const targetBase = unit === "_cross" ? join(rootDir, inceptionRoot, "_cross") : join(rootDir, inceptionRoot, unit);
352
374
  const targetDir = join(targetBase, id);
353
375
  await fsMkdir(targetDir, { recursive: true });
354
376
  const descriptionPath = join(targetDir, "description.md");
@@ -598,6 +620,7 @@ Options:
598
620
  --skills <core|all> Skill set to deploy (default: "all")
599
621
  --agent <claude|codex|both> Agent integration target (default: "claude")
600
622
  --workflow <standard|strict> Workflow enforcement defaults (default: "standard")
623
+ --language <language> Project language declaration (default: "typescript")
601
624
  --with-husky Install Husky pre-commit hooks
602
625
  --with-ci Install GitHub Actions workflows
603
626
  --yes Skip confirmation prompts
@@ -754,15 +777,18 @@ Options:
754
777
  Examples:
755
778
  phasegate check-change-category --paths src/foo.ts,src/bar.ts
756
779
  phasegate check-change-category --paths src/foo.ts --format json`,
757
- "scaffold-wi": `Usage: phasegate scaffold-wi <unit|_cross> <story|issue|chore>
780
+ "scaffold-wi": `Usage: phasegate scaffold-wi <unit|_cross> <story|issue|fix|refactor|chore> [options]
758
781
 
759
782
  Create docs/inception/{unit}/WI-XXX/description.md.
760
783
 
761
784
  Arguments:
762
785
  <unit|_cross> Unit id or _cross for cross-cutting work items.
763
- <story|issue|chore> Work item type.
786
+ <story|issue|fix|refactor|chore>
787
+ Work item type.
764
788
 
765
789
  Options:
790
+ --id <work-item-id> Use the supplied id instead of allocating WI-XXX.
791
+ --root <path> Inception root. Defaults to docs/inception, or personal paths.inceptionDocs.
766
792
  --help, -h Show this help`,
767
793
  "scaffold-design": `Usage: phasegate scaffold-design --unit <id> --phase <phase> [options]
768
794
 
@@ -1730,6 +1756,7 @@ async function main(): Promise<void> {
1730
1756
  "--skills",
1731
1757
  "--agent",
1732
1758
  "--workflow",
1759
+ "--language",
1733
1760
  "--with-husky",
1734
1761
  "--with-ci",
1735
1762
  "--yes",
@@ -1770,6 +1797,11 @@ async function main(): Promise<void> {
1770
1797
  process.exit(2);
1771
1798
  }
1772
1799
  const workflow = parseWorkflowMode(workflowRaw);
1800
+ const language = parseFlag(args, "--language") ?? "typescript";
1801
+ if (language.trim() === "") {
1802
+ console.error("Invalid --language value: value must not be empty.");
1803
+ process.exit(2);
1804
+ }
1773
1805
  const deployClaude = agent === "claude" || agent === "both";
1774
1806
  const deployCodex = agent === "codex" || agent === "both";
1775
1807
  const result = await deploySkills(harnessRoot, rootDir, skillSet);
@@ -1782,6 +1814,7 @@ async function main(): Promise<void> {
1782
1814
  const configResult = await initHarnessConfig(rootDir, projectName, phasePreset, {
1783
1815
  ciEnabled: withCi,
1784
1816
  workflow,
1817
+ language,
1785
1818
  });
1786
1819
  if (workflow === "strict") {
1787
1820
  await scaffoldInceptionRoots(rootDir);
@@ -2035,13 +2068,25 @@ async function main(): Promise<void> {
2035
2068
  }
2036
2069
 
2037
2070
  case "scaffold-wi": {
2071
+ const KNOWN_SCAFFOLD_WI_FLAGS = ["--id", "--root"];
2072
+ const flagError = validateKnownFlags(args, KNOWN_SCAFFOLD_WI_FLAGS);
2073
+ if (flagError) {
2074
+ console.error(flagError);
2075
+ process.exit(2);
2076
+ }
2038
2077
  const unit = args[1];
2039
2078
  const type = parseScaffoldWorkItemType(args[2]);
2040
2079
  if (!unit || !type) {
2041
- console.error("Usage: phasegate scaffold-wi <unit|_cross> <story|issue|chore>");
2080
+ console.error("Usage: phasegate scaffold-wi <unit|_cross> <story|issue|fix|refactor|chore> [--id <work-item-id>] [--root <path>]");
2042
2081
  process.exit(2);
2043
2082
  }
2044
- const descriptionPath = await scaffoldWorkItem(rootDir, unit, type);
2083
+ const configuredPersonalRoot = resolvedConfig?.paths.inceptionDocs?.startsWith(".phasegate-local/")
2084
+ ? resolvedConfig.paths.inceptionDocs
2085
+ : undefined;
2086
+ const descriptionPath = await scaffoldWorkItem(rootDir, unit, type, {
2087
+ id: parseFlag(args, "--id"),
2088
+ inceptionRoot: parseFlag(args, "--root") ?? configuredPersonalRoot,
2089
+ });
2045
2090
  console.log(`Created ${descriptionPath}`);
2046
2091
  process.exit(0);
2047
2092
  break;
@@ -3111,6 +3156,13 @@ Examples:
3111
3156
  printSubcommandHelp("delegate-sonnet");
3112
3157
  process.exit(0);
3113
3158
  }
3159
+ if (!hasFlag(args, "--dry-run") && await isModelDelegationDisabled(rootDir)) {
3160
+ console.error(JSON.stringify({
3161
+ code: "MODEL_DELEGATION_DISABLED",
3162
+ message: "phasegate delegate-sonnet is disabled by modelRouting.delegation=none",
3163
+ }));
3164
+ process.exit(1);
3165
+ }
3114
3166
  const { spawn } = await import("node:child_process");
3115
3167
  const scriptPath = join(harnessRoot, "scripts/delegate-sonnet.sh");
3116
3168
  const forwardArgs = args.slice(1);
@@ -77,7 +77,12 @@ export class QuickModeConfig {
77
77
  }
78
78
 
79
79
  isMaintained(validatorId: string): boolean {
80
- return this.maintainedLayers.includes(validatorId);
80
+ // maintainedLayers の各エントリ P について、レイヤー名エントリ(例 "L2")は
81
+ // そのレイヤーの全 ID(例 "L2-002")にマッチし、ID 完全一致(例 "L2-002")も
82
+ // 引き続きサポートする。
83
+ return this.maintainedLayers.some(
84
+ (entry) => validatorId === entry || validatorId.startsWith(`${entry}-`)
85
+ );
81
86
  }
82
87
 
83
88
  isRelaxed(validatorId: string): boolean {
@@ -4,6 +4,8 @@
4
4
  // @work-item-id WI-127
5
5
  // @work-item-id WI-184
6
6
  // @work-item-id WI-202
7
+ // @work-item-id WI-212
8
+ // @work-item-id WI-219
7
9
  // Note: import.meta.url を使わず、呼び出し元 (main.ts) がパスを解決して渡す設計。
8
10
 
9
11
  import { promises as fs } from "node:fs";
@@ -151,6 +153,44 @@ async function copyDirectory(src: string, dest: string): Promise<number> {
151
153
  return count;
152
154
  }
153
155
 
156
+ type ModelDelegationPolicy = "delegate-sonnet" | "none";
157
+
158
+ export async function readModelDelegationPolicy(projectRoot: string): Promise<ModelDelegationPolicy> {
159
+ try {
160
+ const raw = await fs.readFile(join(projectRoot, HARNESS_CONFIG_FILE), "utf-8");
161
+ const config = JSON.parse(raw) as { modelRouting?: { delegation?: unknown } };
162
+ return config.modelRouting?.delegation === "none" ? "none" : "delegate-sonnet";
163
+ } catch {
164
+ return "delegate-sonnet";
165
+ }
166
+ }
167
+
168
+ export function renderSkillForModelDelegation(content: string, policy: ModelDelegationPolicy): string {
169
+ if (policy !== "none") {
170
+ return content;
171
+ }
172
+
173
+ return content
174
+ .replace(/^model:\s+\S+\n/gm, "")
175
+ .replace(/^review:\s+\S+\n/gm, "")
176
+ .replaceAll("Sonnet 4.6 に委任して成果物を生成する(`npx phasegate delegate-sonnet` 経由)", "メインセッションが成果物を生成する")
177
+ .replaceAll("Opus が", "メインセッションが")
178
+ .replaceAll("Sonnet 4.6", "メインセッション")
179
+ .replaceAll("`npx phasegate delegate-sonnet`", "メインセッション");
180
+ }
181
+
182
+ async function applyModelDelegationPolicyToSkill(skillDir: string, policy: ModelDelegationPolicy): Promise<void> {
183
+ if (policy !== "none") {
184
+ return;
185
+ }
186
+
187
+ const skillPath = join(skillDir, "SKILL.md");
188
+ try {
189
+ const content = await fs.readFile(skillPath, "utf-8");
190
+ await fs.writeFile(skillPath, renderSkillForModelDelegation(content, policy), "utf-8");
191
+ } catch {}
192
+ }
193
+
154
194
  export async function deploySkills(
155
195
  harnessRoot: string,
156
196
  projectRoot: string,
@@ -160,6 +200,7 @@ export async function deploySkills(
160
200
  const skillsTarget = join(projectRoot, SKILLS_TARGET_DIR);
161
201
  const version = await getHarnessVersion(harnessRoot);
162
202
  const deployedAt = new Date().toISOString();
203
+ const modelDelegationPolicy = await readModelDelegationPolicy(projectRoot);
163
204
 
164
205
  const skillDirs = await fs.readdir(skillsSource, { withFileTypes: true });
165
206
  const allSkills = skillDirs.filter((d) => d.isDirectory()).map((d) => d.name);
@@ -172,6 +213,7 @@ export async function deploySkills(
172
213
  const srcSkillDir = join(skillsSource, skill);
173
214
  const destSkillDir = join(skillsTarget, skill);
174
215
  await copyDirectory(srcSkillDir, destSkillDir);
216
+ await applyModelDelegationPolicyToSkill(destSkillDir, modelDelegationPolicy);
175
217
  }
176
218
 
177
219
  const versionInfo: VersionInfo = { version, deployedAt, skillSet };
@@ -456,6 +498,7 @@ export async function deployHookScripts(harnessRoot: string, projectRoot: string
456
498
  export interface InitHarnessConfigOptions {
457
499
  ciEnabled?: boolean;
458
500
  workflow?: "standard" | "strict";
501
+ language?: string;
459
502
  }
460
503
 
461
504
  export async function initHarnessConfig(
@@ -477,6 +520,7 @@ export async function initHarnessConfig(
477
520
  project: {
478
521
  name: projectName,
479
522
  preset: "standard",
523
+ languages: [options.language ?? "typescript"],
480
524
  },
481
525
  architecture: {
482
526
  preset: "clean",
@@ -497,6 +541,9 @@ export async function initHarnessConfig(
497
541
  default: "interactive",
498
542
  perPhase: {},
499
543
  },
544
+ modelRouting: {
545
+ delegation: "delegate-sonnet",
546
+ },
500
547
  harnesses: {},
501
548
  paths: {
502
549
  designDocs: "docs/product/construction",
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit skill-quality
4
+ * @work-item-id WI-212
4
5
  */
5
6
  import { SkillStructure } from '../value-objects/skill-structure.js';
6
7
  import { SkillValidationResult } from '../value-objects/skill-validation-result.js';
@@ -29,6 +30,9 @@ export class SkillStructureValidator {
29
30
  // Check for frontmatter (YAML between --- delimiters)
30
31
  if (lines[0]?.trim() === '---') {
31
32
  sections.push('frontmatter');
33
+ if (this.hasLanguageMetadata(lines)) {
34
+ sections.push('languageMetadata');
35
+ }
32
36
  }
33
37
 
34
38
  // Look for headings that correspond to section names
@@ -69,4 +73,21 @@ export class SkillStructureValidator {
69
73
 
70
74
  return sections;
71
75
  }
76
+
77
+ private hasLanguageMetadata(lines: readonly string[]): boolean {
78
+ const closingDelimiterIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
79
+ if (closingDelimiterIndex < 0) return false;
80
+
81
+ const frontmatterLines = lines.slice(1, closingDelimiterIndex);
82
+ const languageLineIndex = frontmatterLines.findIndex((line) => /^\s*languages\s*:/.test(line));
83
+ if (languageLineIndex < 0) return false;
84
+
85
+ const languageLine = frontmatterLines[languageLineIndex] ?? '';
86
+ if (/\[[^\]]*\S[^\]]*\]/.test(languageLine)) return true;
87
+
88
+ return frontmatterLines.slice(languageLineIndex + 1).some((line) => {
89
+ if (/^\S/.test(line)) return false;
90
+ return /^\s*-\s*\S+/.test(line);
91
+ });
92
+ }
72
93
  }
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit skill-quality
4
+ * @work-item-id WI-212
4
5
  */
5
6
  import type { SectionName } from '../types/section-name.js';
6
7
 
7
8
  const REQUIRED_SECTIONS: readonly SectionName[] = [
8
9
  'frontmatter',
10
+ 'languageMetadata',
9
11
  'purpose',
10
12
  'inputs',
11
13
  'outputs',
@@ -18,4 +18,5 @@ export interface ValidationResultContract {
18
18
  }[];
19
19
  readonly durationMs: number;
20
20
  readonly skipped?: boolean;
21
+ readonly skipReason?: string;
21
22
  }
@@ -24,6 +24,7 @@ export class ValidationResultContractMapper {
24
24
  }),
25
25
  durationMs: result.durationMs,
26
26
  skipped: result.skipped,
27
+ ...(result.skipReason ? { skipReason: result.skipReason } : {}),
27
28
  };
28
29
  }
29
30
 
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer application
3
3
  * @unit validator-system
4
+ * @work-item-id WI-212
4
5
  *
5
6
  * RunL3ValidatorsUseCase — H08-02: L3バリデータ実行
6
7
  */
@@ -8,6 +9,7 @@ import { ValidatorId } from '../../domain/value-objects/validator-id.js';
8
9
  import { ValidationResult } from '../../domain/value-objects/validation-result.js';
9
10
  import { ValidatorRegistry } from '../../domain/services/validator-registry.js';
10
11
  import { ValidatorExecutionService, ValidatorExecutionError } from '../../domain/services/validator-execution-service.js';
12
+ import { ValidatorLanguageCapabilityService } from '../../domain/services/validator-language-capability-service.js';
11
13
  import { ValidationResultContractMapper } from '../mappers/validation-result-contract-mapper.js';
12
14
  import type { ValidationResultContract } from '../dto/validation-result-contract.js';
13
15
  import type { RunL3ValidatorsInput } from '../dto/run-l3-validators-input.js';
@@ -43,6 +45,7 @@ export class RunL3ValidatorsUseCase {
43
45
  private readonly coverageReportPort?: RunL3ValidatorsUseCaseDeps['coverageReportPort'];
44
46
  private readonly securityScannerPort?: SecurityPatternScannerPort;
45
47
  private readonly performanceScannerPort?: PerformanceScannerPort;
48
+ private readonly languageCapabilityService = new ValidatorLanguageCapabilityService();
46
49
 
47
50
  constructor(deps: RunL3ValidatorsUseCaseDeps) {
48
51
  this.registry = deps.validatorRegistry;
@@ -77,8 +80,12 @@ export class RunL3ValidatorsUseCase {
77
80
  return [];
78
81
  }
79
82
 
83
+ const projectLanguages = await this.getProjectLanguages();
84
+ const { executableDefinitions, unsupportedResults, unsupportedValidatorIds } =
85
+ this.languageCapabilityService.splitDefinitions(definitions, projectLanguages);
86
+
80
87
  // coverageReportPort が存在する場合、カバレッジを取得して判定
81
- if (this.coverageReportPort) {
88
+ if (this.coverageReportPort && !unsupportedValidatorIds.has('L3-003')) {
82
89
  const coverageData = await this.coverageReportPort.getCoverage();
83
90
  const threshold = layerConfig.getThreshold('coverageThreshold');
84
91
 
@@ -87,9 +94,9 @@ export class RunL3ValidatorsUseCase {
87
94
  const deficit = threshold - coverageData.overallCoverage;
88
95
 
89
96
  // 他のバリデータを実行
90
- const otherDefs = definitions.filter((d) => d.validatorId.value !== 'L3-003');
97
+ const otherDefs = executableDefinitions.filter((d) => d.validatorId.value !== 'L3-003');
91
98
  const otherResults = this.executionService.execute(otherDefs, [layerConfig]);
92
- const otherContracts = this.mapper.toContracts(otherResults);
99
+ const otherContracts = this.mapper.toContracts([...unsupportedResults, ...otherResults]);
93
100
 
94
101
  return [
95
102
  ...otherContracts.filter((r) => r.validatorId !== 'L3-003'),
@@ -104,8 +111,10 @@ export class RunL3ValidatorsUseCase {
104
111
  }
105
112
  }
106
113
 
107
- const results = this.executionService.execute(definitions, [layerConfig]);
108
- const overrideMap = new Map<string, ValidationResult>(results.map((result) => [result.validatorId.value, result]));
114
+ const results = this.executionService.execute(executableDefinitions, [layerConfig]);
115
+ const overrideMap = new Map<string, ValidationResult>(
116
+ [...unsupportedResults, ...results].map((result) => [result.validatorId.value, result]),
117
+ );
109
118
 
110
119
  if (this.securityScannerPort) {
111
120
  const l3001Result = overrideMap.get('L3-001');
@@ -153,4 +162,8 @@ export class RunL3ValidatorsUseCase {
153
162
  );
154
163
  return this.mapper.toContracts(finalResults);
155
164
  }
165
+
166
+ private async getProjectLanguages(): Promise<readonly string[]> {
167
+ return this.configPort.getProjectLanguages ? await this.configPort.getProjectLanguages() : ['typescript'];
168
+ }
156
169
  }