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.
- package/README.ja.md +32 -0
- package/README.md +33 -0
- package/docs/guide/cli-reference.md +15 -0
- package/docs/guide/codex-integration.md +162 -0
- package/docs/guide/quick-vs-full-mode.md +141 -0
- package/package.json +1 -1
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +60 -0
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +299 -0
- package/scripts/harness/agent-integration/presentation/session-start-hook.ts +54 -0
- package/scripts/harness/agent-integration/presentation/user-prompt-submit-hook.ts +70 -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 +161 -127
- package/scripts/harness/main.ts +94 -11
- package/scripts/harness/phase2-extensions/application/dto/check-initial-creation-expiration-input.ts +9 -0
- package/scripts/harness/phase2-extensions/application/dto/check-initial-creation-expiration-output.ts +17 -0
- package/scripts/harness/phase2-extensions/application/usecases/check-initial-creation-expiration-usecase.ts +103 -0
- package/scripts/harness/phase2-extensions/composition-root.ts +22 -0
- package/scripts/harness/phase2-extensions/domain/aggregates/initial-creation-expiration-rule.ts +103 -0
- package/scripts/harness/phase2-extensions/domain/ports/frontmatter-reader-port.ts +17 -0
- package/scripts/harness/phase2-extensions/domain/ports/initial-creation-age-port.ts +9 -0
- package/scripts/harness/phase2-extensions/domain/ports/initial-creation-expiration-config-port.ts +9 -0
- package/scripts/harness/phase2-extensions/domain/services/initial-creation-expiration-check-service.ts +54 -0
- package/scripts/harness/phase2-extensions/domain/value-objects/initial-creation-age.ts +54 -0
- package/scripts/harness/phase2-extensions/infrastructure/adapters/git-log-document-age-adapter.ts +7 -1
- package/scripts/harness/phase2-extensions/infrastructure/adapters/git-log-initial-creation-age-adapter.ts +77 -0
- package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-initial-creation-expiration-adapter.ts +57 -0
- package/scripts/harness/phase2-extensions/infrastructure/adapters/markdown-frontmatter-reader-adapter.ts +56 -0
- package/scripts/harness/phase2-extensions/presentation/formatters/initial-creation-expiration-result-formatter.ts +23 -0
- package/scripts/harness/phase2-extensions/presentation/handlers/check-initial-creation-expiration-handler.ts +38 -0
- 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 +30 -0
- package/scripts/harness/traceability-model/composition-root.ts +14 -0
- package/scripts/harness/traceability-model/domain/value-objects/project-relative-path.ts +3 -0
- package/scripts/harness/traceability-model/infrastructure/parsers/markdown-story-annotation-parser.ts +39 -4
- package/scripts/harness/traceability-model/presentation/cli/validate-metadata-command-handler.ts +103 -9
- 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/domain-designer/SKILL.md +34 -0
- package/skills/it-test-logic-designer/SKILL.md +16 -0
- package/skills/logical-designer/SKILL.md +34 -0
- package/skills/quick-implementor/SKILL.md +10 -1
- package/skills/scenario-test-logic-designer/SKILL.md +16 -0
- package/skills/story-implementor/SKILL.md +58 -0
- package/skills/unit-designer/SKILL.md +41 -0
- package/skills/unit-test-logic-designer/SKILL.md +18 -0
- package/templates/.codex/hooks.json +63 -0
- package/templates/logical_design.template.md +79 -0
- package/templates/source.template.ts +18 -0
- package/templates/test.template.ts +37 -0
|
@@ -1,37 +1,82 @@
|
|
|
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 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
|
-
*
|
|
9
|
-
*
|
|
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
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import type {
|
|
16
|
-
import {
|
|
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
|
-
|
|
43
|
+
interface RunL2Input {
|
|
44
|
+
readonly targetPaths: readonly string[];
|
|
45
|
+
readonly unitName: string;
|
|
46
|
+
readonly currentPhase: string;
|
|
47
|
+
}
|
|
19
48
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const RED = "\x1b[31m";
|
|
24
|
-
const RESET = "\x1b[0m";
|
|
49
|
+
interface RunL2UseCaseLike {
|
|
50
|
+
execute(input: RunL2Input): Promise<readonly ValidationResultContract[]>;
|
|
51
|
+
}
|
|
25
52
|
|
|
26
|
-
|
|
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(
|
|
31
|
-
encoding:
|
|
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(
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
174
|
+
return {
|
|
175
|
+
exitCode,
|
|
176
|
+
stdout: sections.join('\n'),
|
|
177
|
+
};
|
|
159
178
|
}
|
|
160
179
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
}
|
package/scripts/harness/main.ts
CHANGED
|
@@ -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 <
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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,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
|
+
}
|