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.
Files changed (37) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.ja.md +498 -660
  3. package/README.md +8 -11
  4. package/docs/folder_management_rules.md +251 -0
  5. package/docs/guide/cli-reference.md +15 -0
  6. package/package.json +3 -1
  7. package/scripts/delegate-sonnet.sh +105 -0
  8. package/scripts/harness/ci-governance/application/usecases/generate-ci-template-usecase.ts +6 -2
  9. package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +11 -2
  10. package/scripts/harness/integrations/pre-commit.ts +64 -130
  11. package/scripts/harness/main.ts +134 -9
  12. package/scripts/harness/phase2-extensions/infrastructure/adapters/git-log-document-age-adapter.ts +7 -1
  13. package/scripts/harness/quick-mode/infrastructure/adapters/git-diff-changed-files-adapter.ts +20 -1
  14. package/scripts/harness/regression-suite/composition-root.ts +4 -1
  15. package/scripts/harness/setup/skill-deployer.ts +99 -0
  16. package/scripts/harness/validator-system/application/dto/run-full-validation-input.ts +5 -0
  17. package/scripts/harness/validator-system/application/use-cases/run-full-validation-usecase.ts +40 -12
  18. package/scripts/harness/validator-system/composition-root.ts +2 -0
  19. package/scripts/harness/validator-system/domain/services/l4/drift-detection-service.ts +29 -2
  20. package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +17 -0
  21. package/scripts/harness/validator-system/infrastructure/adapters/markdown-design-document-adapter.ts +65 -5
  22. package/scripts/harness/validator-system/presentation/handlers/run-validators-handler.ts +9 -0
  23. package/skills/environment-designer/SKILL.md +1 -1
  24. package/skills/implementation-planner/SKILL.md +1 -1
  25. package/skills/it-test-designer/SKILL.md +3 -3
  26. package/skills/it-test-logic-designer/SKILL.md +2 -2
  27. package/skills/mock-designer/SKILL.md +1 -1
  28. package/skills/scenario-test-designer/SKILL.md +3 -3
  29. package/skills/scenario-test-logic-designer/SKILL.md +2 -2
  30. package/skills/story-mapper/SKILL.md +1 -1
  31. package/skills/story-writer/SKILL.md +1 -1
  32. package/skills/unit-designer/SKILL.md +1 -1
  33. package/skills/unit-test-designer/SKILL.md +3 -3
  34. package/skills/unit-test-logic-designer/SKILL.md +2 -2
  35. package/templates/.claude/settings.json +3 -3
  36. package/templates/.husky/pre-commit +1 -1
  37. package/templates/phasegate.config.json +0 -30
@@ -1,37 +1,37 @@
1
- // @layer infrastructure
2
1
  /**
3
- * Harness Engineering - Pre-commit Integration
2
+ * @unit harness-api
3
+ * @layer presentation
4
4
  *
5
- * Reads harness config, determines which validators to run,
6
- * and executes enabled checks against staged files.
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
- * Called from .husky/pre-commit.
9
- * Exit code 0 = pass, non-zero = block commit.
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 "node:child_process";
13
- import { loadConfig, isHarnessEnabled } from "../core/config-loader.js";
14
- import { isExcludedPath } from "../core/metadata-parser.js";
15
- import type { HarnessError } from "../core/error-reporter.js";
16
- import { createError, formatForHuman } from "../core/error-reporter.js";
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
- // ─── ANSI Helpers ───
19
-
20
- const GREEN = "\x1b[32m";
21
- const BOLD = "\x1b[1m";
22
- const DIM = "\x1b[2m";
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("git diff --cached --name-only --diff-filter=ACM", {
31
- encoding: "utf-8",
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("\n")
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
- // ─── Dynamic Validator Loading ───
43
-
44
- type ValidatorFn = (files: string[], config: import("../core/config-schema.js").HarnessConfig) => Array<{
45
- rule: string;
46
- severity: "error" | "warning";
47
- file: string;
48
- message: string;
49
- suggestion?: string;
50
- }>;
51
-
52
- interface ValidatorEntry {
53
- name: string;
54
- harnessKey: keyof import("../core/config-schema.js").HarnessesConfig;
55
- category: import("../core/error-reporter.js").HarnessError["category"];
56
- importPath: string;
57
- exportName: string;
58
- }
59
-
60
- const VALIDATOR_REGISTRY: ValidatorEntry[] = [
61
- { name: "phase-gate", harnessKey: "phaseGate", category: "phase_gate", importPath: "../validators/phase-gate.js", exportName: "runPhaseGateCheck" },
62
- { name: "architecture", harnessKey: "architecture", category: "architecture", importPath: "../validators/architecture.js", exportName: "runArchitectureCheck" },
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 config = loadConfig();
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
- console.log(`${DIM}[harness] No relevant TypeScript files staged. Skipping.${RESET}`);
122
- return;
70
+ process.stdout.write(`${DIM}[phasegate] No staged TypeScript files. Skipping.${RESET}\n`);
71
+ process.exit(0);
123
72
  }
124
73
 
125
- console.log(`${BOLD}[harness]${RESET} Pre-commit check (${tsFiles.length} file(s))`);
126
-
127
- const allErrors: HarnessError[] = [];
74
+ process.stdout.write(`${BOLD}[phasegate]${RESET} Pre-commit check (${tsFiles.length} file(s))\n`);
128
75
 
129
- // Load and run enabled validators in order
130
- for (const entry of VALIDATOR_REGISTRY) {
131
- if (!isHarnessEnabled(config, entry.harnessKey, "L2_precommit")) {
132
- continue;
133
- }
76
+ const mod = createValidatorSystemModule();
77
+ const results = await mod.runL2ValidatorsUseCase.execute({
78
+ targetPaths: tsFiles,
79
+ unitName: '',
80
+ currentPhase: '',
81
+ });
134
82
 
135
- const runValidator = await loadValidator(entry);
136
- if (!runValidator) {
137
- continue;
138
- }
83
+ const report = buildReport(results);
84
+ process.stdout.write(`${new HumanValidationResultFormatter().format(report)}\n`);
139
85
 
140
- console.log(` ${DIM}Running ${entry.name}...${RESET}`);
141
- const errors = runValidator(tsFiles, config);
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
- console.log(`${GREEN}[harness]${RESET} All checks passed.`);
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
- console.error(`${RED}[harness] Unexpected error:${RESET}`, err);
163
- process.exit(1);
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
  });
@@ -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 + create phasegate.config.json
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 <type>, --render, --json)
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 unit = parseFlag(args, '--unit') ?? args[1] ?? '';
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];
@@ -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: (command: string, options: { cwd: string }) => Buffer = execSync,
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();
@@ -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('git diff --name-status --cached HEAD', {
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
- const ciGateResultWriterPort = new JsonCiGateResultWriterAdapter(baseDir);
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
  }