phasegate 0.39.0 → 0.62.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 (56) hide show
  1. package/README.ja.md +32 -0
  2. package/README.md +33 -0
  3. package/docs/guide/cli-reference.md +15 -0
  4. package/docs/guide/codex-integration.md +162 -0
  5. package/docs/guide/quick-vs-full-mode.md +141 -0
  6. package/package.json +1 -1
  7. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +60 -0
  8. package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +299 -0
  9. package/scripts/harness/agent-integration/presentation/session-start-hook.ts +54 -0
  10. package/scripts/harness/agent-integration/presentation/user-prompt-submit-hook.ts +70 -0
  11. package/scripts/harness/ci-governance/application/usecases/generate-ci-template-usecase.ts +6 -2
  12. package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +11 -2
  13. package/scripts/harness/integrations/pre-commit.ts +161 -127
  14. package/scripts/harness/main.ts +94 -11
  15. package/scripts/harness/phase2-extensions/application/dto/check-initial-creation-expiration-input.ts +9 -0
  16. package/scripts/harness/phase2-extensions/application/dto/check-initial-creation-expiration-output.ts +17 -0
  17. package/scripts/harness/phase2-extensions/application/usecases/check-initial-creation-expiration-usecase.ts +103 -0
  18. package/scripts/harness/phase2-extensions/composition-root.ts +22 -0
  19. package/scripts/harness/phase2-extensions/domain/aggregates/initial-creation-expiration-rule.ts +103 -0
  20. package/scripts/harness/phase2-extensions/domain/ports/frontmatter-reader-port.ts +17 -0
  21. package/scripts/harness/phase2-extensions/domain/ports/initial-creation-age-port.ts +9 -0
  22. package/scripts/harness/phase2-extensions/domain/ports/initial-creation-expiration-config-port.ts +9 -0
  23. package/scripts/harness/phase2-extensions/domain/services/initial-creation-expiration-check-service.ts +54 -0
  24. package/scripts/harness/phase2-extensions/domain/value-objects/initial-creation-age.ts +54 -0
  25. package/scripts/harness/phase2-extensions/infrastructure/adapters/git-log-document-age-adapter.ts +7 -1
  26. package/scripts/harness/phase2-extensions/infrastructure/adapters/git-log-initial-creation-age-adapter.ts +77 -0
  27. package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-initial-creation-expiration-adapter.ts +57 -0
  28. package/scripts/harness/phase2-extensions/infrastructure/adapters/markdown-frontmatter-reader-adapter.ts +56 -0
  29. package/scripts/harness/phase2-extensions/presentation/formatters/initial-creation-expiration-result-formatter.ts +23 -0
  30. package/scripts/harness/phase2-extensions/presentation/handlers/check-initial-creation-expiration-handler.ts +38 -0
  31. package/scripts/harness/quick-mode/infrastructure/adapters/git-diff-changed-files-adapter.ts +20 -1
  32. package/scripts/harness/regression-suite/composition-root.ts +4 -1
  33. package/scripts/harness/setup/skill-deployer.ts +30 -0
  34. package/scripts/harness/traceability-model/composition-root.ts +14 -0
  35. package/scripts/harness/traceability-model/domain/value-objects/project-relative-path.ts +3 -0
  36. package/scripts/harness/traceability-model/infrastructure/parsers/markdown-story-annotation-parser.ts +39 -4
  37. package/scripts/harness/traceability-model/presentation/cli/validate-metadata-command-handler.ts +103 -9
  38. package/scripts/harness/validator-system/application/dto/run-full-validation-input.ts +5 -0
  39. package/scripts/harness/validator-system/application/use-cases/run-full-validation-usecase.ts +40 -12
  40. package/scripts/harness/validator-system/composition-root.ts +2 -0
  41. package/scripts/harness/validator-system/domain/services/l4/drift-detection-service.ts +29 -2
  42. package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +17 -0
  43. package/scripts/harness/validator-system/infrastructure/adapters/markdown-design-document-adapter.ts +65 -5
  44. package/scripts/harness/validator-system/presentation/handlers/run-validators-handler.ts +9 -0
  45. package/skills/domain-designer/SKILL.md +34 -0
  46. package/skills/it-test-logic-designer/SKILL.md +16 -0
  47. package/skills/logical-designer/SKILL.md +34 -0
  48. package/skills/quick-implementor/SKILL.md +10 -1
  49. package/skills/scenario-test-logic-designer/SKILL.md +16 -0
  50. package/skills/story-implementor/SKILL.md +58 -0
  51. package/skills/unit-designer/SKILL.md +41 -0
  52. package/skills/unit-test-logic-designer/SKILL.md +18 -0
  53. package/templates/.codex/hooks.json +63 -0
  54. package/templates/logical_design.template.md +79 -0
  55. package/templates/source.template.ts +18 -0
  56. package/templates/test.template.ts +37 -0
@@ -1,37 +1,82 @@
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 against staged TypeScript files AND design-document
7
+ * metadata checks against staged Markdown files. Invoked from `.husky/pre-commit`
8
+ * or `npx phasegate pre-commit`.
7
9
  *
8
- * Called from .husky/pre-commit.
9
- * Exit code 0 = pass, non-zero = block commit.
10
+ * Exit codes:
11
+ * 0 = pass (or nothing to check)
12
+ * 1 = validation failure (commit blocked)
13
+ * 2 = runtime error
10
14
  */
11
15
 
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";
16
+ import { execSync } from 'node:child_process';
17
+ import { createValidatorSystemModule } from '../validator-system/composition-root.js';
18
+ import { HumanValidationResultFormatter } from '../validator-system/presentation/formatters/human-validation-result-formatter.js';
19
+ import type { AggregatedValidationReport } from '../validator-system/application/dto/aggregated-validation-report.js';
20
+ import type { ValidationResultContract } from '../validator-system/application/dto/validation-result-contract.js';
21
+ import { createTraceabilityModelModule } from '../traceability-model/composition-root.js';
22
+ import type { ValidateMetadataCommandOutput } from '../traceability-model/presentation/cli/validate-metadata-command-handler.js';
23
+
24
+ const GREEN = '\x1b[32m';
25
+ const RED = '\x1b[31m';
26
+ const BOLD = '\x1b[1m';
27
+ const DIM = '\x1b[2m';
28
+ const RESET = '\x1b[0m';
29
+
30
+ const TS_EXTENSION = '.ts';
31
+ const MD_EXTENSION = '.md';
32
+ const TEST_FILE_SUFFIXES = Object.freeze([
33
+ '.test.ts',
34
+ '.test.tsx',
35
+ '.spec.ts',
36
+ '.spec.tsx',
37
+ ]);
38
+
39
+ function isTestFile(path: string): boolean {
40
+ return TEST_FILE_SUFFIXES.some((suffix) => path.endsWith(suffix));
41
+ }
17
42
 
18
- // ─── ANSI Helpers ───
43
+ interface RunL2Input {
44
+ readonly targetPaths: readonly string[];
45
+ readonly unitName: string;
46
+ readonly currentPhase: string;
47
+ }
19
48
 
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";
49
+ interface RunL2UseCaseLike {
50
+ execute(input: RunL2Input): Promise<readonly ValidationResultContract[]>;
51
+ }
25
52
 
26
- // ─── Staged File Detection ───
53
+ interface ValidateMetadataInput {
54
+ readonly filePaths: readonly string[];
55
+ readonly json?: boolean;
56
+ }
57
+
58
+ interface ValidateMetadataHandlerLike {
59
+ execute(input: ValidateMetadataInput): Promise<ValidateMetadataCommandOutput>;
60
+ }
61
+
62
+ export interface PreCommitDeps {
63
+ readonly runL2ValidatorsUseCase: RunL2UseCaseLike;
64
+ readonly validateMetadataCommandHandler: ValidateMetadataHandlerLike;
65
+ }
66
+
67
+ export interface PreCommitResult {
68
+ readonly exitCode: 0 | 1 | 2;
69
+ readonly stdout: string;
70
+ }
27
71
 
28
72
  function getStagedFiles(): string[] {
29
73
  try {
30
- const output = execSync("git diff --cached --name-only --diff-filter=ACM", {
31
- encoding: "utf-8",
74
+ const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
75
+ encoding: 'utf-8',
76
+ stdio: ['ignore', 'pipe', 'ignore'],
32
77
  });
33
78
  return output
34
- .split("\n")
79
+ .split('\n')
35
80
  .map((f) => f.trim())
36
81
  .filter((f) => f.length > 0);
37
82
  } catch {
@@ -39,126 +84,115 @@ function getStagedFiles(): string[] {
39
84
  }
40
85
  }
41
86
 
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
- }
87
+ function buildReport(
88
+ results: readonly ValidationResultContract[],
89
+ ): AggregatedValidationReport {
90
+ const passed = results.filter((r) => r.passed && !r.skipped).length;
91
+ const failed = results.filter((r) => !r.passed && !r.skipped).length;
92
+ const skipped = results.filter((r) => r.skipped).length;
93
+ const allErrors = results.flatMap((r) => r.errors);
94
+ const errorCount = allErrors.filter((e) => e.severity === 'error').length;
95
+ const warnCount = allErrors.filter((e) => e.severity === 'warning').length;
96
+ return {
97
+ overallPassed: failed === 0,
98
+ totalValidators: results.length,
99
+ passedValidators: passed,
100
+ failedValidators: failed,
101
+ skippedValidators: skipped,
102
+ allErrors,
103
+ summary: {
104
+ totalErrors: errorCount,
105
+ totalWarnings: warnCount,
106
+ errorsByLayer: { L2: errorCount, L3: 0, L4: 0 },
107
+ },
108
+ results,
109
+ };
79
110
  }
80
111
 
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
- );
112
+ function maxExitCode(a: 0 | 1 | 2, b: 0 | 1 | 2): 0 | 1 | 2 {
113
+ return (Math.max(a, b) as 0 | 1 | 2);
101
114
  }
102
115
 
103
- // ─── Main ───
104
-
105
- 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;
116
+ export async function runPreCommit(
117
+ stagedFiles: readonly string[],
118
+ deps: PreCommitDeps,
119
+ ): Promise<PreCommitResult> {
120
+ const tsFiles = stagedFiles.filter((f) => f.endsWith(TS_EXTENSION));
121
+ const mdFiles = stagedFiles.filter((f) => f.endsWith(MD_EXTENSION));
122
+ const testFiles = tsFiles.filter((f) => isTestFile(f));
123
+ const metadataFiles = [...mdFiles, ...testFiles];
124
+
125
+ if (tsFiles.length === 0 && mdFiles.length === 0) {
126
+ return {
127
+ exitCode: 0,
128
+ stdout: `${DIM}[phasegate] No staged files to check. Skipping.${RESET}`,
129
+ };
112
130
  }
113
131
 
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));
119
-
120
- if (tsFiles.length === 0) {
121
- console.log(`${DIM}[harness] No relevant TypeScript files staged. Skipping.${RESET}`);
122
- return;
123
- }
124
-
125
- console.log(`${BOLD}[harness]${RESET} Pre-commit check (${tsFiles.length} file(s))`);
126
-
127
- const allErrors: HarnessError[] = [];
132
+ const sections: string[] = [];
133
+ sections.push(
134
+ `${BOLD}[phasegate]${RESET} Pre-commit check ` +
135
+ `(${tsFiles.length} .ts file(s), ${mdFiles.length} .md file(s))`,
136
+ );
128
137
 
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
- }
138
+ let exitCode: 0 | 1 | 2 = 0;
134
139
 
135
- const runValidator = await loadValidator(entry);
136
- if (!runValidator) {
137
- continue;
140
+ if (tsFiles.length > 0) {
141
+ const results = await deps.runL2ValidatorsUseCase.execute({
142
+ targetPaths: tsFiles,
143
+ unitName: '',
144
+ currentPhase: '',
145
+ });
146
+ const report = buildReport(results);
147
+ sections.push('');
148
+ sections.push(`${BOLD}== TypeScript 実装 (${tsFiles.length} file(s)) ==${RESET}`);
149
+ sections.push(new HumanValidationResultFormatter().format(report));
150
+ if (!report.overallPassed) {
151
+ exitCode = maxExitCode(exitCode, 1);
138
152
  }
139
-
140
- console.log(` ${DIM}Running ${entry.name}...${RESET}`);
141
- const errors = runValidator(tsFiles, config);
142
- allErrors.push(...toHarnessErrors(errors, entry));
143
153
  }
144
154
 
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));
155
+ if (metadataFiles.length > 0) {
156
+ const metadataResult = await deps.validateMetadataCommandHandler.execute({
157
+ filePaths: metadataFiles,
158
+ });
159
+ sections.push('');
160
+ sections.push(
161
+ `${BOLD}== 設計 / テスト メタデータ注釈 (${metadataFiles.length} file(s)) ==${RESET}`,
162
+ );
163
+ sections.push(metadataResult.text);
164
+ exitCode = maxExitCode(exitCode, metadataResult.exitCode);
151
165
  }
152
166
 
153
- if (errorCount > 0) {
154
- console.log(`\n${RED}${BOLD}[harness] Commit blocked.${RESET}`);
155
- process.exit(1);
167
+ sections.push('');
168
+ if (exitCode === 0) {
169
+ sections.push(`${GREEN}[phasegate]${RESET} All checks passed.`);
170
+ } else {
171
+ sections.push(`${RED}${BOLD}[phasegate] Commit blocked.${RESET}`);
156
172
  }
157
173
 
158
- console.log(`${GREEN}[harness]${RESET} All checks passed.`);
174
+ return {
175
+ exitCode,
176
+ stdout: sections.join('\n'),
177
+ };
159
178
  }
160
179
 
161
- main().catch((err) => {
162
- console.error(`${RED}[harness] Unexpected error:${RESET}`, err);
163
- process.exit(1);
164
- });
180
+ export async function runPreCommitCli(): Promise<void> {
181
+ try {
182
+ const stagedFiles = getStagedFiles();
183
+ const validatorMod = createValidatorSystemModule();
184
+ const traceabilityMod = createTraceabilityModelModule(process.cwd());
185
+
186
+ const result = await runPreCommit(stagedFiles, {
187
+ runL2ValidatorsUseCase: validatorMod.runL2ValidatorsUseCase,
188
+ validateMetadataCommandHandler: traceabilityMod.validateMetadataCommandHandler,
189
+ });
190
+
191
+ process.stdout.write(`${result.stdout}\n`);
192
+ process.exit(result.exitCode);
193
+ } catch (err) {
194
+ const msg = err instanceof Error ? err.message : String(err);
195
+ process.stderr.write(`${RED}[phasegate] Unexpected error:${RESET} ${msg}\n`);
196
+ process.exit(2);
197
+ }
198
+ }
@@ -29,7 +29,7 @@ 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, deployDesignDocs, deployHuskyHook, SKILL_CATEGORIES, getCategoryForSkill } from './setup/skill-deployer.js';
32
+ import { deploySkills, deployHookScripts, getDeployedVersion, getHarnessVersion, initHarnessConfig, deployDesignDocs, deployHuskyHook, deployCodexHooks, 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';
@@ -53,7 +53,7 @@ Usage: phasegate <command> [options]
53
53
 
54
54
  Setup:
55
55
  init Initialize project: deploy skills + design docs + phasegate.config.json
56
- (--name <project-name>, --preset <full|standard|minimal|custom>, --with-husky)
56
+ (--name <project-name>, --preset <full|standard|minimal|custom>, --agent <claude|codex|both>, --with-husky)
57
57
  update-skills Re-deploy skills from current harness version
58
58
 
59
59
  Commands:
@@ -85,7 +85,7 @@ Commands:
85
85
  phasegate:complete-check Complete L2-L4 check (--json)
86
86
  phasegate:impact-analysis Impact analysis for story (<storyId>, --json)
87
87
 
88
- 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)
89
89
  ci:migrate-agents-md Migrate AGENTS.md (--dry-run, --validate-only, --json)
90
90
  ci:check-repetition Check error repetition (--code <errorCode>, --reset, --json)
91
91
 
@@ -106,7 +106,8 @@ Commands:
106
106
  p2:check-freshness Check doc freshness (--pattern <glob>, --dry-run, --format text|json)
107
107
  p2:validate-pointers Validate doc pointers (--include-urls, --format text|json)
108
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)
109
+ p2:check-initial-creation Detect long-lived initial_creation:true docs (--pattern <glob>, --format text|json)
110
+ hook <pre-tool-use|post-tool-use|stop|session-start|user-prompt-submit> Run agent hook (reads JSON from stdin; writes JSON to stdout for session-start/user-prompt-submit)
110
111
  pre-commit Run L2 pre-commit validators on staged files
111
112
  delegate-sonnet [...args] Delegate task to Sonnet 4.6 (forwards args to scripts/delegate-sonnet.sh)
112
113
 
@@ -400,9 +401,22 @@ async function main(): Promise<void> {
400
401
  process.exit(2);
401
402
  }
402
403
  const skillSet: SkillSet = skillSetRaw;
404
+ const agentRaw = parseFlag(args, '--agent') ?? 'claude';
405
+ if (agentRaw !== 'claude' && agentRaw !== 'codex' && agentRaw !== 'both') {
406
+ console.error(`Invalid --agent value: "${agentRaw}". Use "claude", "codex", or "both".`);
407
+ process.exit(2);
408
+ }
409
+ const agent = agentRaw;
410
+ const deployClaude = agent === 'claude' || agent === 'both';
411
+ const deployCodex = agent === 'codex' || agent === 'both';
403
412
  const result = await deploySkills(harnessRoot, rootDir, skillSet);
404
413
  const configResult = await initHarnessConfig(rootDir, projectName, phasePreset);
405
- const hooksResult = await deployHookScripts(harnessRoot, rootDir);
414
+ const hooksResult = deployClaude
415
+ ? await deployHookScripts(harnessRoot, rootDir)
416
+ : { scriptsDeployed: 0, settingsCreated: false };
417
+ const codexResult = deployCodex
418
+ ? await deployCodexHooks(harnessRoot, rootDir)
419
+ : null;
406
420
  const designDocsResult = await deployDesignDocs(harnessRoot, rootDir);
407
421
  const withHusky = hasFlag(args, '--with-husky');
408
422
  const huskyResult = withHusky
@@ -422,6 +436,13 @@ async function main(): Promise<void> {
422
436
  } else if (hooksResult.scriptsDeployed > 0) {
423
437
  console.log(` .claude/settings.json already exists, skipped`);
424
438
  }
439
+ if (codexResult !== null) {
440
+ if (codexResult.created) {
441
+ console.log(`✓ .codex/hooks.json deployed`);
442
+ } else {
443
+ console.log(` .codex/hooks.json already exists, skipped`);
444
+ }
445
+ }
425
446
  if (designDocsResult.copiedFiles.length > 0) {
426
447
  console.log(`✓ Design docs deployed (${designDocsResult.copiedFiles.length} files)`);
427
448
  }
@@ -435,7 +456,7 @@ async function main(): Promise<void> {
435
456
  console.log(` .husky/pre-commit already exists, skipped`);
436
457
  }
437
458
  }
438
- console.log(`✓ Harness v${result.version} initialized`);
459
+ console.log(`✓ Harness v${result.version} initialized (agent: ${agent})`);
439
460
  console.log('');
440
461
  console.log('Next steps:');
441
462
  if (skillSet === 'core') {
@@ -444,7 +465,14 @@ async function main(): Promise<void> {
444
465
  console.log(' 1. Run the product-architect skill to start AIDLC');
445
466
  }
446
467
  console.log(' 2. Customize phasegate.config.json if needed');
447
- console.log(' 3. Edit .claude/scripts/hook-config.json to set target directories');
468
+ if (deployClaude) {
469
+ console.log(' 3. Edit .claude/scripts/hook-config.json to set target directories');
470
+ }
471
+ if (deployCodex) {
472
+ console.log(` ${deployClaude ? '4' : '3'}. Enable Codex hooks: codex features enable codex_hooks`);
473
+ console.log(` ${deployClaude ? '5' : '4'}. (Recommended) Install pre-commit backstop: rerun with --with-husky or set up husky manually`);
474
+ console.log(` See docs/guide/codex-integration.md for the native apply_patch limitation.`);
475
+ }
448
476
  process.exit(0);
449
477
  break;
450
478
  }
@@ -698,8 +726,29 @@ async function main(): Promise<void> {
698
726
  }
699
727
 
700
728
  case 'phasegate:check-phase': {
729
+ // ISSUE-005 P2-6: --help / --json を positional として食わないようにする
730
+ if (hasFlag(args, '--help')) {
731
+ process.stdout.write([
732
+ 'Usage: phasegate phasegate:check-phase [options]',
733
+ '',
734
+ 'Check phase gate for a specific unit.',
735
+ '',
736
+ 'Options:',
737
+ ' --unit <unitId> Target unit ID (e.g., harness-api). If omitted,',
738
+ ' the first positional argument is used.',
739
+ ' --json Output result as JSON.',
740
+ ' --help Show this help.',
741
+ '',
742
+ 'Examples:',
743
+ ' phasegate phasegate:check-phase --unit harness-api',
744
+ ' phasegate phasegate:check-phase harness-api --json',
745
+ '',
746
+ ].join('\n'));
747
+ return;
748
+ }
701
749
  const mod = createHarnessApiModule();
702
- const unit = parseFlag(args, '--unit') ?? args[1] ?? '';
750
+ const positional = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
751
+ const unit = parseFlag(args, '--unit') ?? positional ?? '';
703
752
  const flags: Record<string, boolean | string> = {};
704
753
  if (json) flags.json = true;
705
754
  await mod.handlers.checkPhase.handle({ unit }, flags);
@@ -760,6 +809,25 @@ async function main(): Promise<void> {
760
809
 
761
810
  // ── ci-governance ──
762
811
  case 'ci:generate-template': {
812
+ if (hasFlag(args, '--help')) {
813
+ console.log(`Usage: phasegate ci:generate-template [options]
814
+
815
+ Generates a CI template configuration.
816
+
817
+ Options:
818
+ --preset <id> Preset name (e.g. standard, strict). Required.
819
+ --type <type> Template purpose (NOT CI platform name). One of:
820
+ aidlc-gate — AIDLC phase gate checks
821
+ consistency-check — Doc/code consistency checks
822
+ pre-commit — Pre-commit hook template
823
+ --render Render the template to stdout
824
+ --json Output in JSON format
825
+
826
+ Examples:
827
+ phasegate ci:generate-template --preset standard --type aidlc-gate
828
+ phasegate ci:generate-template --preset strict --type pre-commit --render`);
829
+ process.exit(0);
830
+ }
763
831
  const mod = buildCiGovernance(rootDir);
764
832
  const presetId = parseFlag(args, '--preset') ?? 'default';
765
833
  const templateType = parseFlag(args, '--type') ?? 'aidlc-gate';
@@ -956,22 +1024,34 @@ async function main(): Promise<void> {
956
1024
  break;
957
1025
  }
958
1026
 
1027
+ case 'p2:check-initial-creation': {
1028
+ const mod = buildPhase2Extensions(rootDir, resolvedConfig ?? undefined);
1029
+ const p2args = args.slice(1);
1030
+ const result = await mod.checkInitialCreationExpirationHandler.handle(p2args);
1031
+ console.log(result.stdout);
1032
+ process.exit(result.exitCode);
1033
+ break;
1034
+ }
1035
+
959
1036
  // ── agent integration / hooks ──
960
1037
  case 'hook': {
961
1038
  const subCommand = args[1];
1039
+ const usage = 'Usage: phasegate hook <pre-tool-use|post-tool-use|stop|session-start|user-prompt-submit>';
962
1040
  if (!subCommand) {
963
- console.error('Usage: phasegate hook <pre-tool-use|post-tool-use|stop>');
1041
+ console.error(usage);
964
1042
  process.exit(2);
965
1043
  }
966
1044
  const hookFileName: Record<string, string> = {
967
1045
  'pre-tool-use': 'pre-tool-use-hook.js',
968
1046
  'post-tool-use': 'post-tool-use-hook.js',
969
1047
  'stop': 'stop-hook.js',
1048
+ 'session-start': 'session-start-hook.js',
1049
+ 'user-prompt-submit': 'user-prompt-submit-hook.js',
970
1050
  };
971
1051
  const fileName = hookFileName[subCommand];
972
1052
  if (!fileName) {
973
1053
  console.error(`Unknown hook subcommand: ${subCommand}`);
974
- console.error('Usage: phasegate hook <pre-tool-use|post-tool-use|stop>');
1054
+ console.error(usage);
975
1055
  process.exit(2);
976
1056
  }
977
1057
  const hookPath = join(harnessRoot, 'scripts/harness/agent-integration/presentation', fileName);
@@ -981,7 +1061,10 @@ async function main(): Promise<void> {
981
1061
 
982
1062
  case 'pre-commit': {
983
1063
  const preCommitPath = join(harnessRoot, 'scripts/harness/integrations/pre-commit.js');
984
- await import(preCommitPath);
1064
+ const preCommitMod = (await import(preCommitPath)) as {
1065
+ runPreCommitCli: () => Promise<void>;
1066
+ };
1067
+ await preCommitMod.runPreCommitCli();
985
1068
  break;
986
1069
  }
987
1070
 
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @layer application
3
+ * @unit phase2-extensions
4
+ */
5
+ export interface CheckInitialCreationExpirationInput {
6
+ targetPattern?: string;
7
+ format?: 'text' | 'json';
8
+ dryRun?: boolean;
9
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @layer application
3
+ * @unit phase2-extensions
4
+ */
5
+ import type { HarnessErrorContract } from '../../../harness-error/application/dto/harness-error-contract.js';
6
+ import type { InitialCreationExpirationResult } from '../../domain/services/initial-creation-expiration-check-service.js';
7
+
8
+ export interface CheckInitialCreationExpirationOutput {
9
+ results: InitialCreationExpirationResult[];
10
+ summary: {
11
+ total: number;
12
+ ok: number;
13
+ warn: number;
14
+ };
15
+ warnings: HarnessErrorContract[];
16
+ errors: HarnessErrorContract[];
17
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @layer application
3
+ * @unit phase2-extensions
4
+ */
5
+ import type { CheckInitialCreationExpirationInput } from '../dto/check-initial-creation-expiration-input.js';
6
+ import type { CheckInitialCreationExpirationOutput } from '../dto/check-initial-creation-expiration-output.js';
7
+ import type { InitialCreationExpirationConfigPort } from '../../domain/ports/initial-creation-expiration-config-port.js';
8
+ import type { DocumentScannerPort } from '../../domain/ports/document-scanner-port.js';
9
+ import type { FrontmatterReaderPort } from '../../domain/ports/frontmatter-reader-port.js';
10
+ import type { InitialCreationAgePort } from '../../domain/ports/initial-creation-age-port.js';
11
+ import type { InitialCreationExpirationCheckService } from '../../domain/services/initial-creation-expiration-check-service.js';
12
+ import type { HarnessErrorContract } from '../../../harness-error/application/dto/harness-error-contract.js';
13
+ import type { InitialCreationExpirationResult } from '../../domain/services/initial-creation-expiration-check-service.js';
14
+
15
+ export class CheckInitialCreationExpirationUseCase {
16
+ constructor(
17
+ private readonly configPort: InitialCreationExpirationConfigPort,
18
+ private readonly scannerPort: DocumentScannerPort,
19
+ private readonly frontmatterReaderPort: FrontmatterReaderPort,
20
+ private readonly agePort: InitialCreationAgePort,
21
+ private readonly checkService: InitialCreationExpirationCheckService,
22
+ ) {}
23
+
24
+ async execute(input: CheckInitialCreationExpirationInput): Promise<CheckInitialCreationExpirationOutput> {
25
+ try {
26
+ const allRules = await this.configPort.loadRules();
27
+ const filteredRules = input.targetPattern
28
+ ? allRules.filter((rule) => rule.documentPattern === input.targetPattern)
29
+ : allRules;
30
+
31
+ const results: InitialCreationExpirationResult[] = [];
32
+ const warnings: HarnessErrorContract[] = [];
33
+
34
+ for (const rule of filteredRules) {
35
+ if (!rule.isEnabled()) {
36
+ continue;
37
+ }
38
+
39
+ const documentPaths = await this.scannerPort.scan(rule.documentPattern);
40
+
41
+ for (const documentPath of documentPaths) {
42
+ const fmResult = await this.frontmatterReaderPort.read(documentPath);
43
+
44
+ if (fmResult.parseError !== null) {
45
+ warnings.push({
46
+ code: 'L4-232',
47
+ severity: 'warning',
48
+ message: `frontmatter parse failed for ${documentPath}: ${fmResult.parseError}`,
49
+ suggestion: 'YAML 構文を確認してください',
50
+ });
51
+ continue;
52
+ }
53
+
54
+ if (fmResult.flags === null || fmResult.flags.initialCreation !== true) {
55
+ continue;
56
+ }
57
+
58
+ const age = await this.agePort.getAge(documentPath);
59
+ const checkResult = this.checkService.check(rule, age, documentPath);
60
+ results.push(checkResult);
61
+
62
+ if (checkResult.level === 'warn') {
63
+ warnings.push({
64
+ code: 'L4-231',
65
+ severity: 'warning',
66
+ message: checkResult.message,
67
+ suggestion: 'frontmatter を削除し @story-id 注釈を付与してください',
68
+ });
69
+ }
70
+ }
71
+ }
72
+
73
+ const summary = {
74
+ total: results.length,
75
+ ok: results.filter((result) => result.level === 'ok').length,
76
+ warn: results.filter((result) => result.level === 'warn').length,
77
+ };
78
+
79
+ return {
80
+ results,
81
+ summary,
82
+ warnings,
83
+ errors: [],
84
+ };
85
+ } catch (error) {
86
+ const message = error instanceof Error ? error.message : 'unknown error';
87
+
88
+ return {
89
+ results: [],
90
+ summary: { total: 0, ok: 0, warn: 0 },
91
+ warnings: [],
92
+ errors: [
93
+ {
94
+ code: 'L4-299',
95
+ severity: 'error',
96
+ message,
97
+ suggestion: 'phasegate.config.json を確認してください',
98
+ },
99
+ ],
100
+ };
101
+ }
102
+ }
103
+ }