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.
- package/CHANGELOG.md +22 -0
- package/README.md +6 -1
- package/docs/guide/cli-reference.md +1 -1
- package/docs/guide/configuration.md +31 -1
- package/docs/guide/skills-overview.md +2 -0
- package/docs/principles/model-routing.md +1 -0
- package/docs/templates/personal/hooks/pre-commit +16 -0
- package/docs/templates/personal/phasegate-local-config.json +4 -1
- package/docs/templates/project/phasegate.config.json +4 -1
- package/package.json +1 -1
- package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +54 -5
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +24 -3
- package/scripts/harness/config-foundation/domain/harness-config.ts +9 -0
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +2 -0
- package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +20 -5
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +25 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +26 -3
- package/scripts/harness/installation/application/usecases/run-install.ts +16 -3
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +48 -5
- package/scripts/harness/main.ts +71 -19
- package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +6 -1
- package/scripts/harness/setup/skill-deployer.ts +47 -0
- package/scripts/harness/skill-quality/domain/services/skill-structure-validator.ts +21 -0
- package/scripts/harness/skill-quality/domain/value-objects/skill-structure.ts +2 -0
- package/scripts/harness/validator-system/application/dto/validation-result-contract.ts +1 -0
- package/scripts/harness/validator-system/application/mappers/validation-result-contract-mapper.ts +1 -0
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +18 -5
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +48 -6
- package/scripts/harness/validator-system/composition-root.ts +15 -1
- package/scripts/harness/validator-system/domain/ports/validator-config-port.ts +2 -0
- package/scripts/harness/validator-system/domain/services/l4/consistency-check-service.ts +78 -1
- package/scripts/harness/validator-system/domain/services/validator-language-capability-service.ts +64 -0
- package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +7 -0
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-work-item-reflection-adapter.ts +102 -0
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +7 -1
- package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +3 -0
- package/scripts/harness/validator-system/presentation/formatters/human-validation-result-formatter.ts +3 -0
- package/skills/cascade-updater/SKILL.md +1 -0
- package/skills/codebase-mapper/SKILL.md +1 -0
- package/skills/codex-delegator/SKILL.md +1 -0
- package/skills/consistency-checker/SKILL.md +1 -0
- package/skills/doc-freshness-checker/SKILL.md +1 -0
- package/skills/domain-designer/SKILL.md +1 -0
- package/skills/engineering-perspective/SKILL.md +1 -0
- package/skills/environment-designer/SKILL.md +1 -0
- package/skills/implementation-planner/SKILL.md +1 -0
- package/skills/implementation-readiness-checker/SKILL.md +1 -0
- package/skills/it-test-designer/SKILL.md +1 -0
- package/skills/it-test-logic-designer/SKILL.md +1 -0
- package/skills/logical-designer/SKILL.md +1 -0
- package/skills/mock-designer/SKILL.md +1 -0
- package/skills/phasegate-config-doctor/SKILL.md +1 -0
- package/skills/phasegate-toolkit-guide/SKILL.md +1 -0
- package/skills/pointer-validator/SKILL.md +1 -0
- package/skills/product-architect/SKILL.md +1 -0
- package/skills/quick-implementor/SKILL.md +1 -0
- package/skills/scenario-test-designer/SKILL.md +1 -0
- package/skills/scenario-test-logic-designer/SKILL.md +1 -0
- package/skills/skill-creator/SKILL.md +1 -0
- package/skills/story-implementor/SKILL.md +1 -0
- package/skills/story-mapper/SKILL.md +1 -0
- package/skills/story-writer/SKILL.md +1 -0
- package/skills/test-coverage-checker/SKILL.md +1 -0
- package/skills/uiux-designer/SKILL.md +1 -0
- package/skills/unit-designer/SKILL.md +1 -0
- package/skills/unit-test-designer/SKILL.md +1 -0
- package/skills/unit-test-logic-designer/SKILL.md +1 -0
- 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(
|
|
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
|
|
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
|
|
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`,
|
package/scripts/harness/main.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
341
|
-
await fsMkdir(join(rootDir,
|
|
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,
|
|
344
|
-
await fsWriteFile(join(rootDir,
|
|
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(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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>
|
|
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
|
|
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
|
-
|
|
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',
|
|
@@ -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 =
|
|
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(
|
|
108
|
-
const overrideMap = new Map<string, ValidationResult>(
|
|
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
|
}
|