phasegate 0.144.1 → 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.
Files changed (38) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/package.json +1 -1
  3. package/scripts/harness/installation/application/checks/check-utils.ts +65 -0
  4. package/scripts/harness/installation/application/checks/ci-workflow-missing-check.ts +29 -0
  5. package/scripts/harness/installation/application/checks/claude-hook-missing-check.ts +45 -0
  6. package/scripts/harness/installation/application/checks/claude-skills-symlink-check.ts +25 -0
  7. package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +44 -0
  8. package/scripts/harness/installation/application/checks/codex-skills-symlink-check.ts +25 -0
  9. package/scripts/harness/installation/application/checks/husky-commit-msg-missing-check.ts +25 -0
  10. package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +37 -0
  11. package/scripts/harness/installation/application/checks/husky-pre-push-missing-check.ts +25 -0
  12. package/scripts/harness/installation/application/checks/package-json-devdep-missing-check.ts +37 -0
  13. package/scripts/harness/installation/application/ports/file-inspector-port.ts +11 -0
  14. package/scripts/harness/installation/application/ports/hash-calculator-port.ts +9 -0
  15. package/scripts/harness/installation/application/ports/manifest-repository-port.ts +12 -0
  16. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +44 -0
  17. package/scripts/harness/installation/application/wrappers/skill-deployer-manifest-builder.ts +38 -0
  18. package/scripts/harness/installation/composition-root.ts +39 -0
  19. package/scripts/harness/installation/domain/check-id.ts +21 -0
  20. package/scripts/harness/installation/domain/deployment-entry.ts +86 -0
  21. package/scripts/harness/installation/domain/deployment-manifest.ts +90 -0
  22. package/scripts/harness/installation/domain/diagnostic-finding.ts +71 -0
  23. package/scripts/harness/installation/domain/diagnostic-report.ts +52 -0
  24. package/scripts/harness/installation/domain/hash.ts +30 -0
  25. package/scripts/harness/installation/domain/managed-block.ts +37 -0
  26. package/scripts/harness/installation/domain/ports/heuristic-check.ts +12 -0
  27. package/scripts/harness/installation/domain/ports/merge-strategy.ts +13 -0
  28. package/scripts/harness/installation/domain/ports/reconcile-strategy.ts +15 -0
  29. package/scripts/harness/installation/domain/ports/uninstall-reverse-strategy.ts +10 -0
  30. package/scripts/harness/installation/domain/repair-mode.ts +11 -0
  31. package/scripts/harness/installation/domain/repair-table.ts +41 -0
  32. package/scripts/harness/installation/domain/suggested-skill.ts +37 -0
  33. package/scripts/harness/installation/infrastructure/adapters/file-system-manifest-repository-adapter.ts +62 -0
  34. package/scripts/harness/installation/infrastructure/adapters/node-crypto-hash-adapter.ts +13 -0
  35. package/scripts/harness/installation/infrastructure/adapters/node-fs-file-inspector-adapter.ts +53 -0
  36. package/scripts/harness/installation/presentation/cli/doctor-handler.ts +51 -0
  37. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +53 -0
  38. package/scripts/harness/main.ts +106 -1
@@ -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
+ }
@@ -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
@@ -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
@@ -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();