phasegate 0.144.0 → 0.145.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/docs/guide/cli-reference.md +1 -1
- package/package.json +1 -1
- package/scripts/harness/installation/application/checks/check-utils.ts +65 -0
- package/scripts/harness/installation/application/checks/ci-workflow-missing-check.ts +29 -0
- package/scripts/harness/installation/application/checks/claude-hook-missing-check.ts +45 -0
- package/scripts/harness/installation/application/checks/claude-skills-symlink-check.ts +25 -0
- package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +44 -0
- package/scripts/harness/installation/application/checks/codex-skills-symlink-check.ts +25 -0
- package/scripts/harness/installation/application/checks/husky-commit-msg-missing-check.ts +25 -0
- package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +37 -0
- package/scripts/harness/installation/application/checks/husky-pre-push-missing-check.ts +25 -0
- package/scripts/harness/installation/application/checks/package-json-devdep-missing-check.ts +37 -0
- package/scripts/harness/installation/application/ports/file-inspector-port.ts +11 -0
- package/scripts/harness/installation/application/ports/hash-calculator-port.ts +9 -0
- package/scripts/harness/installation/application/ports/manifest-repository-port.ts +12 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +44 -0
- package/scripts/harness/installation/application/wrappers/skill-deployer-manifest-builder.ts +38 -0
- package/scripts/harness/installation/composition-root.ts +39 -0
- package/scripts/harness/installation/domain/check-id.ts +21 -0
- package/scripts/harness/installation/domain/deployment-entry.ts +86 -0
- package/scripts/harness/installation/domain/deployment-manifest.ts +90 -0
- package/scripts/harness/installation/domain/diagnostic-finding.ts +71 -0
- package/scripts/harness/installation/domain/diagnostic-report.ts +52 -0
- package/scripts/harness/installation/domain/hash.ts +30 -0
- package/scripts/harness/installation/domain/managed-block.ts +37 -0
- package/scripts/harness/installation/domain/ports/heuristic-check.ts +12 -0
- package/scripts/harness/installation/domain/ports/merge-strategy.ts +13 -0
- package/scripts/harness/installation/domain/ports/reconcile-strategy.ts +15 -0
- package/scripts/harness/installation/domain/ports/uninstall-reverse-strategy.ts +10 -0
- package/scripts/harness/installation/domain/repair-mode.ts +11 -0
- package/scripts/harness/installation/domain/repair-table.ts +41 -0
- package/scripts/harness/installation/domain/suggested-skill.ts +37 -0
- package/scripts/harness/installation/infrastructure/adapters/file-system-manifest-repository-adapter.ts +62 -0
- package/scripts/harness/installation/infrastructure/adapters/node-crypto-hash-adapter.ts +13 -0
- package/scripts/harness/installation/infrastructure/adapters/node-fs-file-inspector-adapter.ts +53 -0
- package/scripts/harness/installation/presentation/cli/doctor-handler.ts +51 -0
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +53 -0
- package/scripts/harness/main.ts +113 -7
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @unit installation
|
|
2
|
+
// @layer presentation
|
|
3
|
+
// @work-item-id WI-145
|
|
4
|
+
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
7
|
+
import type { RunDoctorDiagnosticsUseCase } from "../../application/usecases/run-doctor-diagnostics.js";
|
|
8
|
+
import { DiagnosticReportFormatter } from "../formatters/diagnostic-report-formatter.js";
|
|
9
|
+
|
|
10
|
+
export interface DoctorHandlerInput {
|
|
11
|
+
readonly projectRoot: string;
|
|
12
|
+
readonly strict: boolean;
|
|
13
|
+
readonly json: boolean;
|
|
14
|
+
readonly reportOut: string | null;
|
|
15
|
+
readonly phasegateVersion: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DoctorHandlerOutput {
|
|
19
|
+
readonly stdout: string;
|
|
20
|
+
readonly exitCode: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class DoctorHandler {
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly useCase: RunDoctorDiagnosticsUseCase,
|
|
26
|
+
private readonly formatter = new DiagnosticReportFormatter(),
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
async execute(input: DoctorHandlerInput): Promise<DoctorHandlerOutput> {
|
|
30
|
+
const result = await this.useCase.execute({
|
|
31
|
+
projectRoot: input.projectRoot,
|
|
32
|
+
strict: input.strict,
|
|
33
|
+
});
|
|
34
|
+
const formatInput = {
|
|
35
|
+
report: result.report,
|
|
36
|
+
phasegateVersion: input.phasegateVersion,
|
|
37
|
+
projectRoot: input.projectRoot,
|
|
38
|
+
exitCode: result.exitCode,
|
|
39
|
+
};
|
|
40
|
+
const jsonOutput = this.formatter.formatJson(formatInput);
|
|
41
|
+
if (input.reportOut !== null) {
|
|
42
|
+
const reportPath = isAbsolute(input.reportOut) ? input.reportOut : join(input.projectRoot, input.reportOut);
|
|
43
|
+
await mkdir(dirname(reportPath), { recursive: true });
|
|
44
|
+
await writeFile(reportPath, `${jsonOutput}\n`, "utf8");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
stdout: input.json ? jsonOutput : this.formatter.formatHuman(formatInput),
|
|
48
|
+
exitCode: result.exitCode,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @unit installation
|
|
2
|
+
// @layer presentation
|
|
3
|
+
// @work-item-id WI-145
|
|
4
|
+
|
|
5
|
+
import type { DiagnosticReport } from "../../domain/diagnostic-report.js";
|
|
6
|
+
|
|
7
|
+
export interface DiagnosticReportFormatterInput {
|
|
8
|
+
readonly report: DiagnosticReport;
|
|
9
|
+
readonly phasegateVersion: string;
|
|
10
|
+
readonly projectRoot: string;
|
|
11
|
+
readonly exitCode: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class DiagnosticReportFormatter {
|
|
15
|
+
formatJson(input: DiagnosticReportFormatterInput): string {
|
|
16
|
+
return JSON.stringify(
|
|
17
|
+
{
|
|
18
|
+
schemaVersion: "1.0",
|
|
19
|
+
phasegateVersion: input.phasegateVersion,
|
|
20
|
+
projectRoot: input.projectRoot,
|
|
21
|
+
overallStatus: input.report.overallStatus,
|
|
22
|
+
findings: input.report.findings.map((finding) => finding.toJSON()),
|
|
23
|
+
exitCode: input.exitCode,
|
|
24
|
+
},
|
|
25
|
+
null,
|
|
26
|
+
2,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
formatHuman(input: DiagnosticReportFormatterInput): string {
|
|
31
|
+
const lines = [
|
|
32
|
+
`phasegate doctor v${input.phasegateVersion}`,
|
|
33
|
+
`Project: ${input.projectRoot}`,
|
|
34
|
+
"",
|
|
35
|
+
];
|
|
36
|
+
for (const finding of input.report.findings) {
|
|
37
|
+
lines.push(`[${finding.severity}] ${finding.checkId}: ${finding.message}`);
|
|
38
|
+
lines.push(` target: ${finding.target}`);
|
|
39
|
+
lines.push(` repairMode: ${finding.repairMode}`);
|
|
40
|
+
if (finding.repairHint !== null) lines.push(` fix: ${finding.repairHint}`);
|
|
41
|
+
if (finding.suggestedSkill !== null) {
|
|
42
|
+
lines.push(` suggested: ${finding.suggestedSkill.skillName} (${finding.suggestedSkill.invokeCommand})`);
|
|
43
|
+
lines.push(` rationale: ${finding.suggestedSkill.rationale}`);
|
|
44
|
+
}
|
|
45
|
+
lines.push("");
|
|
46
|
+
}
|
|
47
|
+
const redCount = input.report.findings.filter((finding) => finding.severity === "red").length;
|
|
48
|
+
const warnCount = input.report.findings.filter((finding) => finding.severity === "warn").length;
|
|
49
|
+
lines.push(`Status: ${input.report.overallStatus.toUpperCase()} (${input.report.findings.length} findings: ${redCount} red, ${warnCount} warn)`);
|
|
50
|
+
lines.push(`Exit: ${input.exitCode}`);
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
}
|
package/scripts/harness/main.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 起動時に config-foundation で設定を解決し、他Unit に注入する(Cross-unit wiring)。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { access, readFile as fsReadFile } from "node:fs/promises";
|
|
11
|
+
import { access, readFile as fsReadFile, readlink as fsReadlink } from "node:fs/promises";
|
|
12
12
|
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
import { createAdrFoundationModule } from "./adr-foundation/composition-root.js";
|
|
14
14
|
import { createBiomeAstEngineModule } from "./biome-ast-engine/composition-root.js";
|
|
@@ -24,6 +24,9 @@ import {
|
|
|
24
24
|
} from "./config-foundation/infrastructure/repositories/file-system-config-repository.js";
|
|
25
25
|
import { createHarnessApiModule } from "./harness-api/composition-root.js";
|
|
26
26
|
import { createHarnessErrorModule } from "./harness-error/composition-root.js";
|
|
27
|
+
import { createInstallationModule } from "./installation/composition-root.js";
|
|
28
|
+
import { SkillDeployerManifestBuilder, type DeployManifestRecord } from "./installation/application/wrappers/skill-deployer-manifest-builder.js";
|
|
29
|
+
import { NodeCryptoHashAdapter } from "./installation/infrastructure/adapters/node-crypto-hash-adapter.js";
|
|
27
30
|
import { CheckStoryReflectionUseCase } from "./phase-dependency-model/application/usecases/check-story-reflection-usecase.js";
|
|
28
31
|
import { createPhaseDependencyModelModule } from "./phase-dependency-model/composition-root.js";
|
|
29
32
|
import { StoryReflectionChecker } from "./phase-dependency-model/domain/services/story-reflection-checker.js";
|
|
@@ -93,6 +96,10 @@ Setup:
|
|
|
93
96
|
(--name <project-name>, --preset <full|standard|minimal|custom>,
|
|
94
97
|
--skills <core|all>, --agent <claude|codex|both>, --with-husky, --with-ci, --yes)
|
|
95
98
|
update-skills Re-deploy skills from current harness version
|
|
99
|
+
doctor Diagnose silent installation failures (--json, --strict, --report-out <path>)
|
|
100
|
+
install Install phasegate managed files (stub until WI-146)
|
|
101
|
+
uninstall Uninstall phasegate managed files (stub until WI-147)
|
|
102
|
+
reconcile Reconcile phasegate managed files (stub until WI-148)
|
|
96
103
|
|
|
97
104
|
Commands:
|
|
98
105
|
enable-feature <name> Enable a harness feature
|
|
@@ -125,7 +132,7 @@ Commands:
|
|
|
125
132
|
phasegate:complete-check Complete L2-L4 check (--json)
|
|
126
133
|
phasegate:impact-analysis Impact analysis for story (<storyId>, --json)
|
|
127
134
|
|
|
128
|
-
ci:generate-template Generate CI template (--preset <id>, --type <aidlc-gate|consistency-check|pre-commit|agent-context-refresh>, --render, --json)
|
|
135
|
+
ci:generate-template Generate CI template (--preset <id>, default: standard; --type <aidlc-gate|consistency-check|pre-commit|agent-context-refresh>, --render, --json)
|
|
129
136
|
ci:migrate-agents-md Migrate AGENTS.md (--dry-run, --validate-only, --json)
|
|
130
137
|
ci:auto-refresh-agent-context Refresh AGENTS.md / CLAUDE.md (--dry-run, --apply, --json)
|
|
131
138
|
refresh-claude-md Refresh CLAUDE.md standard sections (--dry-run, --apply, --json)
|
|
@@ -181,6 +188,40 @@ function hasFlag(args: readonly string[], flag: string): boolean {
|
|
|
181
188
|
return args.includes(flag);
|
|
182
189
|
}
|
|
183
190
|
|
|
191
|
+
async function createFileManifestRecord(
|
|
192
|
+
rootDir: string,
|
|
193
|
+
relativePath: string,
|
|
194
|
+
mode: "created" | "merged" = "created",
|
|
195
|
+
): Promise<DeployManifestRecord | null> {
|
|
196
|
+
try {
|
|
197
|
+
const contentForHash = await fsReadFile(join(rootDir, relativePath), "utf8");
|
|
198
|
+
return { path: relativePath, mode, contentForHash };
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function createSymlinkManifestRecord(rootDir: string, relativePath: string): Promise<DeployManifestRecord | null> {
|
|
205
|
+
try {
|
|
206
|
+
const target = await fsReadlink(join(rootDir, relativePath));
|
|
207
|
+
return { path: relativePath, mode: "symlink", contentForHash: target };
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function saveInstallationManifest(
|
|
214
|
+
rootDir: string,
|
|
215
|
+
version: string,
|
|
216
|
+
records: readonly (DeployManifestRecord | null | Promise<DeployManifestRecord | null>)[],
|
|
217
|
+
): Promise<void> {
|
|
218
|
+
const filteredRecords = (await Promise.all(records)).filter((record) => record !== null);
|
|
219
|
+
if (filteredRecords.length === 0) return;
|
|
220
|
+
const mod = createInstallationModule();
|
|
221
|
+
const builder = new SkillDeployerManifestBuilder(new NodeCryptoHashAdapter());
|
|
222
|
+
await mod.manifestRepository.save(rootDir, builder.build(version, filteredRecords));
|
|
223
|
+
}
|
|
224
|
+
|
|
184
225
|
/**
|
|
185
226
|
* WI-094 / ADR-017: CLI で boolean フラグを tri-state に解釈する。
|
|
186
227
|
* - `--<flag>` 指定 → true
|
|
@@ -367,7 +408,7 @@ Examples:
|
|
|
367
408
|
Generates a CI template configuration.
|
|
368
409
|
|
|
369
410
|
Options:
|
|
370
|
-
--preset <id> Preset name (e.g. standard, strict).
|
|
411
|
+
--preset <id> Preset name (e.g. standard, strict). Default: standard.
|
|
371
412
|
--type <type> Template purpose (NOT CI platform name). One of:
|
|
372
413
|
aidlc-gate — AIDLC phase gate checks
|
|
373
414
|
consistency-check — Doc/code consistency checks
|
|
@@ -377,7 +418,7 @@ Options:
|
|
|
377
418
|
--json Output in JSON format
|
|
378
419
|
|
|
379
420
|
Examples:
|
|
380
|
-
phasegate ci:generate-template --
|
|
421
|
+
phasegate ci:generate-template --type aidlc-gate
|
|
381
422
|
phasegate ci:generate-template --preset strict --type pre-commit --render`,
|
|
382
423
|
};
|
|
383
424
|
|
|
@@ -722,6 +763,27 @@ async function main(): Promise<void> {
|
|
|
722
763
|
const huskyCommitMsgResult = withHusky ? await deployHuskyCommitMsgHook(harnessRoot, rootDir) : null;
|
|
723
764
|
const huskyPrePushResult = withHusky ? await deployHuskyPrePushHook(harnessRoot, rootDir) : null;
|
|
724
765
|
const ciWorkflowResult = withCi ? await deployCiWorkflows(harnessRoot, rootDir) : null;
|
|
766
|
+
await saveInstallationManifest(rootDir, result.version, [
|
|
767
|
+
...result.deployedSkills.map((skill) => ({
|
|
768
|
+
path: join("skills", skill),
|
|
769
|
+
mode: "created" as const,
|
|
770
|
+
contentForHash: `${result.version}:${skill}`,
|
|
771
|
+
})),
|
|
772
|
+
await createFileManifestRecord(rootDir, join("skills", ".harness-version")),
|
|
773
|
+
configResult.created ? await createFileManifestRecord(rootDir, "phasegate.config.json") : null,
|
|
774
|
+
hooksResult.settingsCreated ? await createFileManifestRecord(rootDir, join(".claude", "settings.json")) : null,
|
|
775
|
+
hooksResult.hookConfigGenerated
|
|
776
|
+
? await createFileManifestRecord(rootDir, join(".claude", "scripts", "hook-config.json"))
|
|
777
|
+
: null,
|
|
778
|
+
skillLinkResult.claude !== null ? await createSymlinkManifestRecord(rootDir, join(".claude", "skills")) : null,
|
|
779
|
+
codexResult?.created ? await createFileManifestRecord(rootDir, join(".codex", "hooks.json")) : null,
|
|
780
|
+
skillLinkResult.codex !== null ? await createSymlinkManifestRecord(rootDir, join(".codex", "skills")) : null,
|
|
781
|
+
...designDocsResult.copiedFiles.map((path) => createFileManifestRecord(rootDir, path)),
|
|
782
|
+
huskyResult?.created ? await createFileManifestRecord(rootDir, join(".husky", "pre-commit")) : null,
|
|
783
|
+
huskyCommitMsgResult?.created ? await createFileManifestRecord(rootDir, join(".husky", "commit-msg")) : null,
|
|
784
|
+
huskyPrePushResult?.created ? await createFileManifestRecord(rootDir, join(".husky", "pre-push")) : null,
|
|
785
|
+
...(ciWorkflowResult?.copiedFiles.map((path) => createFileManifestRecord(rootDir, path)) ?? []),
|
|
786
|
+
]);
|
|
725
787
|
console.log(
|
|
726
788
|
`✓ Skills deployed to ${result.targetDir} (${result.deployedSkills.length} skills, set: ${skillSet})`,
|
|
727
789
|
);
|
|
@@ -854,11 +916,54 @@ async function main(): Promise<void> {
|
|
|
854
916
|
claude: shouldLinkClaude,
|
|
855
917
|
codex: shouldLinkCodex,
|
|
856
918
|
});
|
|
919
|
+
await saveInstallationManifest(rootDir, current, [
|
|
920
|
+
...result.deployedSkills.map((skill) => ({
|
|
921
|
+
path: join("skills", skill),
|
|
922
|
+
mode: "created" as const,
|
|
923
|
+
contentForHash: `${current}:${skill}`,
|
|
924
|
+
})),
|
|
925
|
+
await createFileManifestRecord(rootDir, join("skills", ".harness-version")),
|
|
926
|
+
shouldLinkClaude ? await createSymlinkManifestRecord(rootDir, join(".claude", "skills")) : null,
|
|
927
|
+
shouldLinkCodex ? await createSymlinkManifestRecord(rootDir, join(".codex", "skills")) : null,
|
|
928
|
+
]);
|
|
857
929
|
console.log(`✓ Skills updated (${result.deployedSkills.length} skills redeployed, set: ${updateSkillSet})`);
|
|
858
930
|
process.exit(0);
|
|
859
931
|
break;
|
|
860
932
|
}
|
|
861
933
|
|
|
934
|
+
case "doctor": {
|
|
935
|
+
const mod = createInstallationModule();
|
|
936
|
+
const phasegateVersion = await getHarnessVersion(harnessRoot);
|
|
937
|
+
const result = await mod.doctorHandler.execute({
|
|
938
|
+
projectRoot: rootDir,
|
|
939
|
+
strict: hasFlag(args, "--strict"),
|
|
940
|
+
json,
|
|
941
|
+
reportOut: parseFlag(args, "--report-out") ?? null,
|
|
942
|
+
phasegateVersion,
|
|
943
|
+
});
|
|
944
|
+
console.log(result.stdout);
|
|
945
|
+
process.exit(result.exitCode);
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
case "install": {
|
|
950
|
+
console.error("Not yet implemented: phasegate install is owned by WI-146");
|
|
951
|
+
process.exit(2);
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
case "uninstall": {
|
|
956
|
+
console.error("Not yet implemented: phasegate uninstall is owned by WI-147");
|
|
957
|
+
process.exit(2);
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
case "reconcile": {
|
|
962
|
+
console.error("Not yet implemented: phasegate reconcile is owned by WI-148");
|
|
963
|
+
process.exit(2);
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
|
|
862
967
|
// ── config-foundation ──
|
|
863
968
|
case "enable-feature": {
|
|
864
969
|
const mod = createConfigFoundationModule();
|
|
@@ -1253,21 +1358,22 @@ async function main(): Promise<void> {
|
|
|
1253
1358
|
Generates a CI template configuration.
|
|
1254
1359
|
|
|
1255
1360
|
Options:
|
|
1256
|
-
--preset <id> Preset name (e.g. standard, strict).
|
|
1361
|
+
--preset <id> Preset name (e.g. standard, strict). Default: standard.
|
|
1257
1362
|
--type <type> Template purpose (NOT CI platform name). One of:
|
|
1258
1363
|
aidlc-gate — AIDLC phase gate checks
|
|
1259
1364
|
consistency-check — Doc/code consistency checks
|
|
1260
1365
|
pre-commit — Pre-commit hook template
|
|
1366
|
+
agent-context-refresh — AGENTS.md / CLAUDE.md refresh workflow
|
|
1261
1367
|
--render Render the template to stdout
|
|
1262
1368
|
--json Output in JSON format
|
|
1263
1369
|
|
|
1264
1370
|
Examples:
|
|
1265
|
-
phasegate ci:generate-template --
|
|
1371
|
+
phasegate ci:generate-template --type aidlc-gate
|
|
1266
1372
|
phasegate ci:generate-template --preset strict --type pre-commit --render`);
|
|
1267
1373
|
process.exit(0);
|
|
1268
1374
|
}
|
|
1269
1375
|
const mod = buildCiGovernance(rootDir, harnessRoot);
|
|
1270
|
-
const presetId = parseFlag(args, "--preset") ?? "
|
|
1376
|
+
const presetId = parseFlag(args, "--preset") ?? "standard";
|
|
1271
1377
|
const templateType = parseFlag(args, "--type") ?? "aidlc-gate";
|
|
1272
1378
|
const render = hasFlag(args, "--render");
|
|
1273
1379
|
const format = json ? "json" : "human";
|