phasegate 0.32.0 → 0.44.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 +87 -1
- package/README.ja.md +498 -660
- package/README.md +8 -11
- package/docs/folder_management_rules.md +251 -0
- package/docs/guide/cli-reference.md +15 -0
- package/package.json +3 -1
- package/scripts/delegate-sonnet.sh +105 -0
- package/scripts/harness/ci-governance/application/usecases/generate-ci-template-usecase.ts +6 -2
- package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +11 -2
- package/scripts/harness/integrations/pre-commit.ts +64 -130
- package/scripts/harness/main.ts +134 -9
- package/scripts/harness/phase2-extensions/infrastructure/adapters/git-log-document-age-adapter.ts +7 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/git-diff-changed-files-adapter.ts +20 -1
- package/scripts/harness/regression-suite/composition-root.ts +4 -1
- package/scripts/harness/setup/skill-deployer.ts +99 -0
- package/scripts/harness/validator-system/application/dto/run-full-validation-input.ts +5 -0
- package/scripts/harness/validator-system/application/use-cases/run-full-validation-usecase.ts +40 -12
- package/scripts/harness/validator-system/composition-root.ts +2 -0
- package/scripts/harness/validator-system/domain/services/l4/drift-detection-service.ts +29 -2
- package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +17 -0
- package/scripts/harness/validator-system/infrastructure/adapters/markdown-design-document-adapter.ts +65 -5
- package/scripts/harness/validator-system/presentation/handlers/run-validators-handler.ts +9 -0
- package/skills/environment-designer/SKILL.md +1 -1
- package/skills/implementation-planner/SKILL.md +1 -1
- package/skills/it-test-designer/SKILL.md +3 -3
- package/skills/it-test-logic-designer/SKILL.md +2 -2
- package/skills/mock-designer/SKILL.md +1 -1
- package/skills/scenario-test-designer/SKILL.md +3 -3
- package/skills/scenario-test-logic-designer/SKILL.md +2 -2
- package/skills/story-mapper/SKILL.md +1 -1
- package/skills/story-writer/SKILL.md +1 -1
- package/skills/unit-designer/SKILL.md +1 -1
- package/skills/unit-test-designer/SKILL.md +3 -3
- package/skills/unit-test-logic-designer/SKILL.md +2 -2
- package/templates/.claude/settings.json +3 -3
- package/templates/.husky/pre-commit +1 -1
- package/templates/phasegate.config.json +0 -30
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
// @layer infrastructure
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
2
|
+
* @unit harness-api
|
|
3
|
+
* @layer presentation
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Pre-commit CLI entry.
|
|
6
|
+
* Runs L2 validators (phase-gate / metadata / test-quality) against staged
|
|
7
|
+
* TypeScript files. Invoked from `.husky/pre-commit` or `npx phasegate pre-commit`.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* Exit codes:
|
|
10
|
+
* 0 = pass (or nothing to check)
|
|
11
|
+
* 1 = validation failure (commit blocked)
|
|
12
|
+
* 2 = runtime error
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
|
-
import { execSync } from
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import type {
|
|
16
|
-
import {
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
import { createValidatorSystemModule } from '../validator-system/composition-root.js';
|
|
17
|
+
import { HumanValidationResultFormatter } from '../validator-system/presentation/formatters/human-validation-result-formatter.js';
|
|
18
|
+
import type { AggregatedValidationReport } from '../validator-system/application/dto/aggregated-validation-report.js';
|
|
19
|
+
import type { ValidationResultContract } from '../validator-system/application/dto/validation-result-contract.js';
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const RED = "\x1b[31m";
|
|
24
|
-
const RESET = "\x1b[0m";
|
|
25
|
-
|
|
26
|
-
// ─── Staged File Detection ───
|
|
21
|
+
const GREEN = '\x1b[32m';
|
|
22
|
+
const RED = '\x1b[31m';
|
|
23
|
+
const BOLD = '\x1b[1m';
|
|
24
|
+
const DIM = '\x1b[2m';
|
|
25
|
+
const RESET = '\x1b[0m';
|
|
27
26
|
|
|
28
27
|
function getStagedFiles(): string[] {
|
|
29
28
|
try {
|
|
30
|
-
const output = execSync(
|
|
31
|
-
encoding:
|
|
29
|
+
const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
|
|
30
|
+
encoding: 'utf-8',
|
|
31
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
32
32
|
});
|
|
33
33
|
return output
|
|
34
|
-
.split(
|
|
34
|
+
.split('\n')
|
|
35
35
|
.map((f) => f.trim())
|
|
36
36
|
.filter((f) => f.length > 0);
|
|
37
37
|
} catch {
|
|
@@ -39,126 +39,60 @@ function getStagedFiles(): string[] {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{ name: "dependency", harnessKey: "dependency", category: "dependency", importPath: "../validators/dependency.js", exportName: "runDependencyCheck" },
|
|
64
|
-
{ name: "test-quality", harnessKey: "testQuality", category: "quality", importPath: "../validators/test-quality.js", exportName: "runTestQualityCheck" },
|
|
65
|
-
{ name: "security", harnessKey: "security", category: "security", importPath: "../validators/security.js", exportName: "runSecurityCheck" },
|
|
66
|
-
{ name: "performance", harnessKey: "performance", category: "performance", importPath: "../validators/performance.js", exportName: "runPerformanceCheck" },
|
|
67
|
-
{ name: "consistency", harnessKey: "consistency", category: "consistency", importPath: "../validators/consistency.js", exportName: "runConsistencyCheck" },
|
|
68
|
-
{ name: "metadata", harnessKey: "metadata", category: "quality", importPath: "../validators/metadata.js", exportName: "runMetadataCheck" },
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
async function loadValidator(entry: ValidatorEntry): Promise<ValidatorFn | null> {
|
|
72
|
-
try {
|
|
73
|
-
const mod = await import(entry.importPath);
|
|
74
|
-
return mod[entry.exportName] ?? null;
|
|
75
|
-
} catch {
|
|
76
|
-
// Validator not yet implemented, skip gracefully
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ─── Convert Validator Errors to HarnessErrors ───
|
|
82
|
-
|
|
83
|
-
function toHarnessErrors(
|
|
84
|
-
validatorErrors: Array<{ rule: string; severity: "error" | "warning"; file: string; message: string; suggestion?: string }>,
|
|
85
|
-
entry: ValidatorEntry,
|
|
86
|
-
): HarnessError[] {
|
|
87
|
-
return validatorErrors.map((err) =>
|
|
88
|
-
createError({
|
|
89
|
-
code: `HARNESS-${err.rule.toUpperCase().replace(/[^A-Z0-9]/g, "-")}`,
|
|
90
|
-
severity: err.severity,
|
|
91
|
-
category: entry.category,
|
|
92
|
-
location: { file: err.file },
|
|
93
|
-
message: {
|
|
94
|
-
short: err.message,
|
|
95
|
-
detailed: err.message,
|
|
96
|
-
agentInstruction: err.suggestion ?? "",
|
|
97
|
-
},
|
|
98
|
-
metadata: { timestamp: new Date().toISOString(), validator: entry.name, layer: "L2" },
|
|
99
|
-
}),
|
|
100
|
-
);
|
|
42
|
+
function buildReport(results: readonly ValidationResultContract[]): AggregatedValidationReport {
|
|
43
|
+
const passed = results.filter((r) => r.passed && !r.skipped).length;
|
|
44
|
+
const failed = results.filter((r) => !r.passed && !r.skipped).length;
|
|
45
|
+
const skipped = results.filter((r) => r.skipped).length;
|
|
46
|
+
const allErrors = results.flatMap((r) => r.errors);
|
|
47
|
+
const errorCount = allErrors.filter((e) => e.severity === 'error').length;
|
|
48
|
+
const warnCount = allErrors.filter((e) => e.severity === 'warning').length;
|
|
49
|
+
return {
|
|
50
|
+
overallPassed: failed === 0,
|
|
51
|
+
totalValidators: results.length,
|
|
52
|
+
passedValidators: passed,
|
|
53
|
+
failedValidators: failed,
|
|
54
|
+
skippedValidators: skipped,
|
|
55
|
+
allErrors,
|
|
56
|
+
summary: {
|
|
57
|
+
totalErrors: errorCount,
|
|
58
|
+
totalWarnings: warnCount,
|
|
59
|
+
errorsByLayer: { L2: errorCount, L3: 0, L4: 0 },
|
|
60
|
+
},
|
|
61
|
+
results,
|
|
62
|
+
};
|
|
101
63
|
}
|
|
102
64
|
|
|
103
|
-
// ─── Main ───
|
|
104
|
-
|
|
105
65
|
async function main(): Promise<void> {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
// Check if L2 (pre-commit) layer is enabled
|
|
109
|
-
if (!config.layers.L2_precommit.enabled) {
|
|
110
|
-
console.log(`${DIM}[harness] L2 Pre-commit layer is disabled. Skipping.${RESET}`);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Get staged .ts files, filtering excluded paths
|
|
115
|
-
const allStaged = getStagedFiles();
|
|
116
|
-
const tsFiles = allStaged
|
|
117
|
-
.filter((f) => f.endsWith(".ts"))
|
|
118
|
-
.filter((f) => !isExcludedPath(f, config));
|
|
66
|
+
const stagedFiles = getStagedFiles();
|
|
67
|
+
const tsFiles = stagedFiles.filter((f) => f.endsWith('.ts'));
|
|
119
68
|
|
|
120
69
|
if (tsFiles.length === 0) {
|
|
121
|
-
|
|
122
|
-
|
|
70
|
+
process.stdout.write(`${DIM}[phasegate] No staged TypeScript files. Skipping.${RESET}\n`);
|
|
71
|
+
process.exit(0);
|
|
123
72
|
}
|
|
124
73
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const allErrors: HarnessError[] = [];
|
|
74
|
+
process.stdout.write(`${BOLD}[phasegate]${RESET} Pre-commit check (${tsFiles.length} file(s))\n`);
|
|
128
75
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
76
|
+
const mod = createValidatorSystemModule();
|
|
77
|
+
const results = await mod.runL2ValidatorsUseCase.execute({
|
|
78
|
+
targetPaths: tsFiles,
|
|
79
|
+
unitName: '',
|
|
80
|
+
currentPhase: '',
|
|
81
|
+
});
|
|
134
82
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
83
|
+
const report = buildReport(results);
|
|
84
|
+
process.stdout.write(`${new HumanValidationResultFormatter().format(report)}\n`);
|
|
139
85
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
allErrors.push(...toHarnessErrors(errors, entry));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Report results
|
|
146
|
-
const errorCount = allErrors.filter((e) => e.severity === "error").length;
|
|
147
|
-
|
|
148
|
-
if (allErrors.length > 0) {
|
|
149
|
-
console.log();
|
|
150
|
-
console.log(formatForHuman(allErrors, config));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (errorCount > 0) {
|
|
154
|
-
console.log(`\n${RED}${BOLD}[harness] Commit blocked.${RESET}`);
|
|
86
|
+
if (!report.overallPassed) {
|
|
87
|
+
process.stdout.write(`\n${RED}${BOLD}[phasegate] Commit blocked.${RESET}\n`);
|
|
155
88
|
process.exit(1);
|
|
156
89
|
}
|
|
157
|
-
|
|
158
|
-
|
|
90
|
+
process.stdout.write(`\n${GREEN}[phasegate]${RESET} All checks passed.\n`);
|
|
91
|
+
process.exit(0);
|
|
159
92
|
}
|
|
160
93
|
|
|
161
94
|
main().catch((err) => {
|
|
162
|
-
|
|
163
|
-
process.
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
process.stderr.write(`${RED}[phasegate] Unexpected error:${RESET} ${msg}\n`);
|
|
97
|
+
process.exit(2);
|
|
164
98
|
});
|
package/scripts/harness/main.ts
CHANGED
|
@@ -29,10 +29,11 @@ import { buildCiGovernance } from './ci-governance/composition-root.js';
|
|
|
29
29
|
import { createSkillQualityHandlers } from './skill-quality/composition-root.js';
|
|
30
30
|
import { buildRegressionSuite } from './regression-suite/composition-root.js';
|
|
31
31
|
import { buildPhase2Extensions } from './phase2-extensions/composition-root.js';
|
|
32
|
-
import { deploySkills, deployHookScripts, getDeployedVersion, getHarnessVersion, initHarnessConfig, SKILL_CATEGORIES, getCategoryForSkill } from './setup/skill-deployer.js';
|
|
32
|
+
import { deploySkills, deployHookScripts, getDeployedVersion, getHarnessVersion, initHarnessConfig, deployDesignDocs, deployHuskyHook, SKILL_CATEGORIES, getCategoryForSkill } from './setup/skill-deployer.js';
|
|
33
33
|
import type { SkillSet } from './setup/skill-deployer.js';
|
|
34
34
|
import type { HarnessConfigV2 } from './config-foundation/domain/harness-config.js';
|
|
35
35
|
import { ConfigValidationError } from './config-foundation/domain/errors/config-validation-error.js';
|
|
36
|
+
import { ConfigNotFoundError, ConfigPersistenceError } from './config-foundation/infrastructure/repositories/file-system-config-repository.js';
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* main.ts (scripts/harness/main.ts) から2階層上がパッケージルート。
|
|
@@ -51,8 +52,8 @@ function printUsage(): void {
|
|
|
51
52
|
Usage: phasegate <command> [options]
|
|
52
53
|
|
|
53
54
|
Setup:
|
|
54
|
-
init Initialize project: deploy skills +
|
|
55
|
-
(--name <project-name>, --preset <full|standard|minimal|custom
|
|
55
|
+
init Initialize project: deploy skills + design docs + phasegate.config.json
|
|
56
|
+
(--name <project-name>, --preset <full|standard|minimal|custom>, --with-husky)
|
|
56
57
|
update-skills Re-deploy skills from current harness version
|
|
57
58
|
|
|
58
59
|
Commands:
|
|
@@ -84,7 +85,7 @@ Commands:
|
|
|
84
85
|
phasegate:complete-check Complete L2-L4 check (--json)
|
|
85
86
|
phasegate:impact-analysis Impact analysis for story (<storyId>, --json)
|
|
86
87
|
|
|
87
|
-
ci:generate-template Generate CI template (--preset <id>, --type <
|
|
88
|
+
ci:generate-template Generate CI template (--preset <id>, --type <aidlc-gate|consistency-check|pre-commit>, --render, --json)
|
|
88
89
|
ci:migrate-agents-md Migrate AGENTS.md (--dry-run, --validate-only, --json)
|
|
89
90
|
ci:check-repetition Check error repetition (--code <errorCode>, --reset, --json)
|
|
90
91
|
|
|
@@ -105,6 +106,9 @@ Commands:
|
|
|
105
106
|
p2:check-freshness Check doc freshness (--pattern <glob>, --dry-run, --format text|json)
|
|
106
107
|
p2:validate-pointers Validate doc pointers (--include-urls, --format text|json)
|
|
107
108
|
p2:generate-e2e-template Generate E2E test template (--phase <phase>, --output <path>)
|
|
109
|
+
hook <pre-tool-use|post-tool-use|stop> Run Claude Code hook (reads JSON from stdin)
|
|
110
|
+
pre-commit Run L2 pre-commit validators on staged files
|
|
111
|
+
delegate-sonnet [...args] Delegate task to Sonnet 4.6 (forwards args to scripts/delegate-sonnet.sh)
|
|
108
112
|
|
|
109
113
|
Skills:
|
|
110
114
|
skills list List all available skills
|
|
@@ -265,10 +269,22 @@ async function loadStoryReflectionProvider(
|
|
|
265
269
|
};
|
|
266
270
|
reporting?: { outputDir?: string };
|
|
267
271
|
};
|
|
272
|
+
let content: string;
|
|
273
|
+
try {
|
|
274
|
+
content = await fsReadFile(configPath, 'utf8');
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
280
|
+
process.stderr.write(`Warning: failed to read phasegate.config.json: ${message}\n`);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
268
283
|
try {
|
|
269
|
-
const content = await fsReadFile(configPath, 'utf8');
|
|
270
284
|
raw = JSON.parse(content);
|
|
271
|
-
} catch {
|
|
285
|
+
} catch (error) {
|
|
286
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
287
|
+
process.stderr.write(`Warning: phasegate.config.json is not valid JSON: ${message}\n`);
|
|
272
288
|
return null;
|
|
273
289
|
}
|
|
274
290
|
const section: PhaseDepConfigSection = {
|
|
@@ -325,7 +341,15 @@ async function loadResolvedConfig(): Promise<HarnessConfigV2 | undefined> {
|
|
|
325
341
|
process.stderr.write(`Invalid phasegate.config.json: ${error.message}\n`);
|
|
326
342
|
process.exit(2);
|
|
327
343
|
}
|
|
328
|
-
|
|
344
|
+
if (error instanceof ConfigNotFoundError) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
348
|
+
if (error instanceof ConfigPersistenceError) {
|
|
349
|
+
process.stderr.write(`Warning: phasegate.config.json is not valid JSON: ${message}\n`);
|
|
350
|
+
} else {
|
|
351
|
+
process.stderr.write(`Warning: failed to load phasegate.config.json: ${message}\n`);
|
|
352
|
+
}
|
|
329
353
|
return undefined;
|
|
330
354
|
}
|
|
331
355
|
}
|
|
@@ -379,6 +403,11 @@ async function main(): Promise<void> {
|
|
|
379
403
|
const result = await deploySkills(harnessRoot, rootDir, skillSet);
|
|
380
404
|
const configResult = await initHarnessConfig(rootDir, projectName, phasePreset);
|
|
381
405
|
const hooksResult = await deployHookScripts(harnessRoot, rootDir);
|
|
406
|
+
const designDocsResult = await deployDesignDocs(harnessRoot, rootDir);
|
|
407
|
+
const withHusky = hasFlag(args, '--with-husky');
|
|
408
|
+
const huskyResult = withHusky
|
|
409
|
+
? await deployHuskyHook(harnessRoot, rootDir)
|
|
410
|
+
: null;
|
|
382
411
|
console.log(`✓ Skills deployed to ${result.targetDir} (${result.deployedSkills.length} skills, set: ${skillSet})`);
|
|
383
412
|
if (configResult.created) {
|
|
384
413
|
console.log(`✓ phasegate.config.json created`);
|
|
@@ -393,6 +422,19 @@ async function main(): Promise<void> {
|
|
|
393
422
|
} else if (hooksResult.scriptsDeployed > 0) {
|
|
394
423
|
console.log(` .claude/settings.json already exists, skipped`);
|
|
395
424
|
}
|
|
425
|
+
if (designDocsResult.copiedFiles.length > 0) {
|
|
426
|
+
console.log(`✓ Design docs deployed (${designDocsResult.copiedFiles.length} files)`);
|
|
427
|
+
}
|
|
428
|
+
for (const skipped of designDocsResult.skippedFiles) {
|
|
429
|
+
console.log(` ${skipped} already exists, skipped`);
|
|
430
|
+
}
|
|
431
|
+
if (huskyResult !== null) {
|
|
432
|
+
if (huskyResult.created) {
|
|
433
|
+
console.log(`✓ .husky/pre-commit deployed`);
|
|
434
|
+
} else {
|
|
435
|
+
console.log(` .husky/pre-commit already exists, skipped`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
396
438
|
console.log(`✓ Harness v${result.version} initialized`);
|
|
397
439
|
console.log('');
|
|
398
440
|
console.log('Next steps:');
|
|
@@ -656,8 +698,29 @@ async function main(): Promise<void> {
|
|
|
656
698
|
}
|
|
657
699
|
|
|
658
700
|
case 'phasegate:check-phase': {
|
|
701
|
+
// ISSUE-005 P2-6: --help / --json を positional として食わないようにする
|
|
702
|
+
if (hasFlag(args, '--help')) {
|
|
703
|
+
process.stdout.write([
|
|
704
|
+
'Usage: phasegate phasegate:check-phase [options]',
|
|
705
|
+
'',
|
|
706
|
+
'Check phase gate for a specific unit.',
|
|
707
|
+
'',
|
|
708
|
+
'Options:',
|
|
709
|
+
' --unit <unitId> Target unit ID (e.g., harness-api). If omitted,',
|
|
710
|
+
' the first positional argument is used.',
|
|
711
|
+
' --json Output result as JSON.',
|
|
712
|
+
' --help Show this help.',
|
|
713
|
+
'',
|
|
714
|
+
'Examples:',
|
|
715
|
+
' phasegate phasegate:check-phase --unit harness-api',
|
|
716
|
+
' phasegate phasegate:check-phase harness-api --json',
|
|
717
|
+
'',
|
|
718
|
+
].join('\n'));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
659
721
|
const mod = createHarnessApiModule();
|
|
660
|
-
const
|
|
722
|
+
const positional = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
|
|
723
|
+
const unit = parseFlag(args, '--unit') ?? positional ?? '';
|
|
661
724
|
const flags: Record<string, boolean | string> = {};
|
|
662
725
|
if (json) flags.json = true;
|
|
663
726
|
await mod.handlers.checkPhase.handle({ unit }, flags);
|
|
@@ -685,7 +748,7 @@ async function main(): Promise<void> {
|
|
|
685
748
|
const flags: Record<string, boolean | string> = {};
|
|
686
749
|
if (json) flags.json = true;
|
|
687
750
|
await mod.handlers.status.handle({}, flags);
|
|
688
|
-
await printStoryReflectionStatusLine(rootDir);
|
|
751
|
+
if (!json) await printStoryReflectionStatusLine(rootDir);
|
|
689
752
|
break;
|
|
690
753
|
}
|
|
691
754
|
|
|
@@ -718,6 +781,25 @@ async function main(): Promise<void> {
|
|
|
718
781
|
|
|
719
782
|
// ── ci-governance ──
|
|
720
783
|
case 'ci:generate-template': {
|
|
784
|
+
if (hasFlag(args, '--help')) {
|
|
785
|
+
console.log(`Usage: phasegate ci:generate-template [options]
|
|
786
|
+
|
|
787
|
+
Generates a CI template configuration.
|
|
788
|
+
|
|
789
|
+
Options:
|
|
790
|
+
--preset <id> Preset name (e.g. standard, strict). Required.
|
|
791
|
+
--type <type> Template purpose (NOT CI platform name). One of:
|
|
792
|
+
aidlc-gate — AIDLC phase gate checks
|
|
793
|
+
consistency-check — Doc/code consistency checks
|
|
794
|
+
pre-commit — Pre-commit hook template
|
|
795
|
+
--render Render the template to stdout
|
|
796
|
+
--json Output in JSON format
|
|
797
|
+
|
|
798
|
+
Examples:
|
|
799
|
+
phasegate ci:generate-template --preset standard --type aidlc-gate
|
|
800
|
+
phasegate ci:generate-template --preset strict --type pre-commit --render`);
|
|
801
|
+
process.exit(0);
|
|
802
|
+
}
|
|
721
803
|
const mod = buildCiGovernance(rootDir);
|
|
722
804
|
const presetId = parseFlag(args, '--preset') ?? 'default';
|
|
723
805
|
const templateType = parseFlag(args, '--type') ?? 'aidlc-gate';
|
|
@@ -914,6 +996,49 @@ async function main(): Promise<void> {
|
|
|
914
996
|
break;
|
|
915
997
|
}
|
|
916
998
|
|
|
999
|
+
// ── agent integration / hooks ──
|
|
1000
|
+
case 'hook': {
|
|
1001
|
+
const subCommand = args[1];
|
|
1002
|
+
if (!subCommand) {
|
|
1003
|
+
console.error('Usage: phasegate hook <pre-tool-use|post-tool-use|stop>');
|
|
1004
|
+
process.exit(2);
|
|
1005
|
+
}
|
|
1006
|
+
const hookFileName: Record<string, string> = {
|
|
1007
|
+
'pre-tool-use': 'pre-tool-use-hook.js',
|
|
1008
|
+
'post-tool-use': 'post-tool-use-hook.js',
|
|
1009
|
+
'stop': 'stop-hook.js',
|
|
1010
|
+
};
|
|
1011
|
+
const fileName = hookFileName[subCommand];
|
|
1012
|
+
if (!fileName) {
|
|
1013
|
+
console.error(`Unknown hook subcommand: ${subCommand}`);
|
|
1014
|
+
console.error('Usage: phasegate hook <pre-tool-use|post-tool-use|stop>');
|
|
1015
|
+
process.exit(2);
|
|
1016
|
+
}
|
|
1017
|
+
const hookPath = join(harnessRoot, 'scripts/harness/agent-integration/presentation', fileName);
|
|
1018
|
+
await import(hookPath);
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
case 'pre-commit': {
|
|
1023
|
+
const preCommitPath = join(harnessRoot, 'scripts/harness/integrations/pre-commit.js');
|
|
1024
|
+
await import(preCommitPath);
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
case 'delegate-sonnet': {
|
|
1029
|
+
const { spawn } = await import('node:child_process');
|
|
1030
|
+
const scriptPath = join(harnessRoot, 'scripts/delegate-sonnet.sh');
|
|
1031
|
+
const forwardArgs = args.slice(1);
|
|
1032
|
+
const child = spawn('bash', [scriptPath, ...forwardArgs], { stdio: 'inherit' });
|
|
1033
|
+
await new Promise<void>((_, reject) => {
|
|
1034
|
+
child.on('exit', (code) => {
|
|
1035
|
+
process.exit(code ?? 1);
|
|
1036
|
+
});
|
|
1037
|
+
child.on('error', reject);
|
|
1038
|
+
});
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
917
1042
|
// ── skills ──
|
|
918
1043
|
case 'skills': {
|
|
919
1044
|
const subCommand = args[1];
|
package/scripts/harness/phase2-extensions/infrastructure/adapters/git-log-document-age-adapter.ts
CHANGED
|
@@ -17,13 +17,19 @@ export class GitLogDocumentAgeAdapter implements DocumentAgePort {
|
|
|
17
17
|
constructor(
|
|
18
18
|
private readonly projectRoot: string,
|
|
19
19
|
private readonly nowProvider: () => Date = () => new Date(),
|
|
20
|
-
private readonly gitLogExecutor: (
|
|
20
|
+
private readonly gitLogExecutor: (
|
|
21
|
+
command: string,
|
|
22
|
+
options: { cwd: string; stdio?: readonly ['pipe', 'pipe', 'pipe'] },
|
|
23
|
+
) => Buffer = execSync,
|
|
21
24
|
) {}
|
|
22
25
|
|
|
23
26
|
async getAge(documentPath: string): Promise<DocumentAge> {
|
|
24
27
|
try {
|
|
25
28
|
const output = this.gitLogExecutor(`git log --format=%ai -1 -- "${documentPath}"`, {
|
|
26
29
|
cwd: this.projectRoot,
|
|
30
|
+
// ISSUE-005 P1-3: fresh repo では "fatal: your current branch ... does not have
|
|
31
|
+
// any commits yet" が 34回 stderr に漏れる。pipe に束ねて静音化する。
|
|
32
|
+
stdio: ['pipe', 'pipe', 'pipe'] as const,
|
|
27
33
|
})
|
|
28
34
|
.toString()
|
|
29
35
|
.trim();
|
package/scripts/harness/quick-mode/infrastructure/adapters/git-diff-changed-files-adapter.ts
CHANGED
|
@@ -42,9 +42,17 @@ export class GitDiffChangedFilesAdapter {
|
|
|
42
42
|
getChangedFiles(): readonly ChangedFile[] {
|
|
43
43
|
let output: string;
|
|
44
44
|
|
|
45
|
+
// ISSUE-005 P1-3: fresh repo (HEAD 未作成) では `git diff ... HEAD` が fatal
|
|
46
|
+
// になるため、HEAD の存在を事前確認して命令を切り替える
|
|
47
|
+
const hasHead = this.hasHead();
|
|
48
|
+
const command = hasHead
|
|
49
|
+
? 'git diff --name-status --cached HEAD'
|
|
50
|
+
: 'git diff --name-status --cached';
|
|
51
|
+
|
|
45
52
|
try {
|
|
46
|
-
output = childProcess.execSync(
|
|
53
|
+
output = childProcess.execSync(command, {
|
|
47
54
|
encoding: 'utf8',
|
|
55
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
56
|
}) as string;
|
|
49
57
|
} catch (err) {
|
|
50
58
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -89,4 +97,15 @@ export class GitDiffChangedFilesAdapter {
|
|
|
89
97
|
|
|
90
98
|
return Object.freeze(files);
|
|
91
99
|
}
|
|
100
|
+
|
|
101
|
+
private hasHead(): boolean {
|
|
102
|
+
try {
|
|
103
|
+
childProcess.execSync('git rev-parse --verify HEAD', {
|
|
104
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
105
|
+
});
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
92
111
|
}
|
|
@@ -40,7 +40,10 @@ export function buildRegressionSuite(baseDir: string): RegressionSuiteCompositio
|
|
|
40
40
|
const suiteRegistryPort = new StaticSuiteRegistryAdapter();
|
|
41
41
|
const testRunnerPort = new VitestTestRunnerAdapter();
|
|
42
42
|
const configQueryPort = new HarnessConfigQueryAdapter();
|
|
43
|
-
|
|
43
|
+
// ISSUE-005 P2-7: リポジトリ直下ではなく reports/regression/ 配下に出力する
|
|
44
|
+
const ciGateResultWriterPort = new JsonCiGateResultWriterAdapter(
|
|
45
|
+
path.join(baseDir, 'reports', 'regression'),
|
|
46
|
+
);
|
|
44
47
|
const importAnalyzerPort = new BiomeAstImportAnalyzerAdapter();
|
|
45
48
|
const v0SpecReaderPort = new FileSystemV0SpecReaderAdapter(baseDir);
|
|
46
49
|
const migrationMappingRepositoryPort = new MarkdownMigrationMappingRepositoryAdapter(
|
|
@@ -292,3 +292,102 @@ export async function initHarnessConfig(
|
|
|
292
292
|
await fs.writeFile(configPath, JSON.stringify(template, null, 2) + '\n', 'utf-8');
|
|
293
293
|
return { created: true, path: configPath };
|
|
294
294
|
}
|
|
295
|
+
|
|
296
|
+
export interface DeployDesignDocsResult {
|
|
297
|
+
copiedFiles: string[];
|
|
298
|
+
skippedFiles: string[];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 設計原則ドキュメントを対象プロジェクトの docs/ にデプロイする。
|
|
303
|
+
* - docs/folder_management_rules.md → <projectRoot>/docs/folder_management_rules.md
|
|
304
|
+
* - docs/principles/*.md → <projectRoot>/docs/principles/*.md
|
|
305
|
+
* 既存ファイルは上書きせずスキップする。
|
|
306
|
+
*/
|
|
307
|
+
export async function deployDesignDocs(
|
|
308
|
+
harnessRoot: string,
|
|
309
|
+
projectRoot: string,
|
|
310
|
+
): Promise<DeployDesignDocsResult> {
|
|
311
|
+
const copiedFiles: string[] = [];
|
|
312
|
+
const skippedFiles: string[] = [];
|
|
313
|
+
const docsTargetDir = join(projectRoot, 'docs');
|
|
314
|
+
const principlesTargetDir = join(docsTargetDir, 'principles');
|
|
315
|
+
|
|
316
|
+
await fs.mkdir(docsTargetDir, { recursive: true });
|
|
317
|
+
await fs.mkdir(principlesTargetDir, { recursive: true });
|
|
318
|
+
|
|
319
|
+
const folderRulesRelativePath = join('docs', 'folder_management_rules.md');
|
|
320
|
+
const folderRulesSource = join(harnessRoot, folderRulesRelativePath);
|
|
321
|
+
const folderRulesTarget = join(projectRoot, folderRulesRelativePath);
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
await fs.access(folderRulesTarget);
|
|
325
|
+
skippedFiles.push(folderRulesRelativePath);
|
|
326
|
+
} catch {
|
|
327
|
+
try {
|
|
328
|
+
await fs.access(folderRulesSource);
|
|
329
|
+
await fs.copyFile(folderRulesSource, folderRulesTarget);
|
|
330
|
+
copiedFiles.push(folderRulesRelativePath);
|
|
331
|
+
} catch {
|
|
332
|
+
// 配置元が存在しない場合はスキップ
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const principlesSourceDir = join(harnessRoot, 'docs', 'principles');
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const principleEntries = await fs.readdir(principlesSourceDir, { withFileTypes: true });
|
|
340
|
+
const principleFiles = principleEntries
|
|
341
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
342
|
+
.map((entry) => entry.name)
|
|
343
|
+
.sort();
|
|
344
|
+
|
|
345
|
+
for (const principleFile of principleFiles) {
|
|
346
|
+
const relativePath = join('docs', 'principles', principleFile);
|
|
347
|
+
const sourcePath = join(principlesSourceDir, principleFile);
|
|
348
|
+
const targetPath = join(projectRoot, relativePath);
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
await fs.access(targetPath);
|
|
352
|
+
skippedFiles.push(relativePath);
|
|
353
|
+
} catch {
|
|
354
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
355
|
+
copiedFiles.push(relativePath);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
// principles ディレクトリが存在しない場合はスキップ
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { copiedFiles, skippedFiles };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export interface DeployHuskyHookResult {
|
|
366
|
+
created: boolean;
|
|
367
|
+
path: string;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* husky pre-commit フックを対象プロジェクトの .husky/ にデプロイする。
|
|
372
|
+
* 既存があればスキップする。実行権限 0o755 を付与する。
|
|
373
|
+
*/
|
|
374
|
+
export async function deployHuskyHook(
|
|
375
|
+
harnessRoot: string,
|
|
376
|
+
projectRoot: string,
|
|
377
|
+
): Promise<DeployHuskyHookResult> {
|
|
378
|
+
const targetPath = join(projectRoot, '.husky', 'pre-commit');
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
await fs.access(targetPath);
|
|
382
|
+
return { created: false, path: targetPath };
|
|
383
|
+
} catch {
|
|
384
|
+
// 配置先が存在しない場合は新規作成
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const sourcePath = join(harnessRoot, 'templates', '.husky', 'pre-commit');
|
|
388
|
+
await fs.mkdir(join(projectRoot, '.husky'), { recursive: true });
|
|
389
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
390
|
+
await fs.chmod(targetPath, 0o755);
|
|
391
|
+
|
|
392
|
+
return { created: true, path: targetPath };
|
|
393
|
+
}
|
|
@@ -13,4 +13,9 @@ export interface RunFullValidationInput {
|
|
|
13
13
|
readonly failOnWarning?: boolean;
|
|
14
14
|
readonly coverageReportPath?: string;
|
|
15
15
|
readonly requirementMatrixPath?: string;
|
|
16
|
+
/**
|
|
17
|
+
* ISSUE-005 P1-4: 実行レイヤー絞り込み。未指定は全レイヤー実行。
|
|
18
|
+
* 指定された場合、`includeL4` より優先される。
|
|
19
|
+
*/
|
|
20
|
+
readonly targetLayers?: readonly ('L2' | 'L3' | 'L4')[];
|
|
16
21
|
}
|