phasegate 0.160.13 → 0.160.15
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.md +3 -3
- package/docs/guide/getting-started.md +1 -1
- package/docs/guide/installation.md +3 -3
- package/docs/guide/setup-artifacts.md +12 -3
- package/docs/templates/personal/phasegate-local-config.json +73 -3
- package/docs/templates/project/phasegate.config.json +29 -0
- package/package.json +1 -1
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +19 -9
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +19 -9
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +23 -12
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +19 -9
- package/scripts/harness/agent-integration/presentation/user-prompt-submit-hook.ts +3 -2
- package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +10 -4
- package/scripts/harness/installation/application/checks/check-utils.ts +5 -0
- package/scripts/harness/installation/application/checks/claude-skills-symlink-check.ts +5 -2
- package/scripts/harness/installation/application/checks/codex-skills-symlink-check.ts +5 -2
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +36 -2
- package/scripts/harness/installation/application/usecases/run-install.ts +200 -61
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +39 -4
- package/scripts/harness/installation/presentation/cli/doctor-handler.ts +2 -0
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +11 -3
- package/scripts/harness/main.ts +3 -2
- package/skills/README.md +1 -1
- package/skills/phasegate-config-doctor/SKILL.md +2 -2
- package/skills/phasegate-toolkit-guide/SKILL.md +1 -1
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
// @work-item-id WI-182
|
|
9
9
|
// @work-item-id WI-183
|
|
10
10
|
// @work-item-id WI-207
|
|
11
|
+
// @work-item-id WI-208
|
|
12
|
+
// @work-item-id WI-209
|
|
11
13
|
|
|
12
|
-
import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink } from "node:fs/promises";
|
|
14
|
+
import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink, readdir } from "node:fs/promises";
|
|
13
15
|
import { dirname, join } from "node:path";
|
|
14
16
|
import { DeploymentEntry } from "../../domain/deployment-entry.js";
|
|
15
17
|
import { DeploymentManifest } from "../../domain/deployment-manifest.js";
|
|
@@ -19,7 +21,7 @@ import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.j
|
|
|
19
21
|
import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
20
22
|
|
|
21
23
|
type InstallAction = "missing" | "will-merge" | "will-skip" | "will-overwrite";
|
|
22
|
-
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "text-managed" | "copy";
|
|
24
|
+
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "text-managed" | "copy" | "copy-dir" | "symlink";
|
|
23
25
|
|
|
24
26
|
export interface InstallPlanItem {
|
|
25
27
|
readonly path: string;
|
|
@@ -83,6 +85,7 @@ const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
|
83
85
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
84
86
|
const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
|
|
85
87
|
const TEXT_END = "# phasegate personal install exclude (END)";
|
|
88
|
+
const PERSONAL_AGENT_RUNTIME_FILES = new Set([".claude/settings.json", ".codex/hooks.json"]);
|
|
86
89
|
|
|
87
90
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
88
91
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -105,6 +108,20 @@ async function readTextOrNull(path: string): Promise<string | null> {
|
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
async function copyDirectory(src: string, dest: string): Promise<void> {
|
|
112
|
+
await mkdir(dest, { recursive: true });
|
|
113
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const srcPath = join(src, entry.name);
|
|
116
|
+
const destPath = join(dest, entry.name);
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
await copyDirectory(srcPath, destPath);
|
|
119
|
+
} else if (entry.isFile()) {
|
|
120
|
+
await copyFile(srcPath, destPath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
function normalizeJsonEntry(value: unknown): string {
|
|
109
126
|
return JSON.stringify(value);
|
|
110
127
|
}
|
|
@@ -295,7 +312,7 @@ export class RunInstallUseCase {
|
|
|
295
312
|
const workflow = input.workflow ?? "standard";
|
|
296
313
|
const agent = input.agent ?? (includeClaude && includeCodex ? "both" : includeCodex ? "codex" : "claude");
|
|
297
314
|
const targets = input.personal
|
|
298
|
-
? this.createPersonalTargets()
|
|
315
|
+
? this.createPersonalTargets({ includeClaude, includeCodex })
|
|
299
316
|
: this.createTargets({ includeClaude, includeCodex, includeHusky, includeCi });
|
|
300
317
|
const existingManifest = await this.manifestRepository.load(input.projectRoot);
|
|
301
318
|
const baseManifest = existingManifest ?? DeploymentManifest.create(input.phasegateVersion);
|
|
@@ -313,8 +330,14 @@ export class RunInstallUseCase {
|
|
|
313
330
|
const template = target.strategy === "markdown-managed"
|
|
314
331
|
? renderAgentContextTemplate(rawTemplate, { agent, skillSet, workflow, includeHusky, includeCi })
|
|
315
332
|
: rawTemplate;
|
|
316
|
-
const
|
|
317
|
-
const
|
|
333
|
+
const existingEntry = baseManifest.findEntry(target.path);
|
|
334
|
+
const beforeHash = before === null ? null : this.hashCalculator.compute(before);
|
|
335
|
+
const unmanagedPersonalRuntimeFile = input.personal
|
|
336
|
+
&& PERSONAL_AGENT_RUNTIME_FILES.has(target.path)
|
|
337
|
+
&& before !== null
|
|
338
|
+
&& (existingEntry === null || beforeHash === null || !beforeHash.equals(existingEntry.hash));
|
|
339
|
+
const repairMode = unmanagedPersonalRuntimeFile ? "manual" : this.repairMode(target, before);
|
|
340
|
+
const next = unmanagedPersonalRuntimeFile ? before : this.merge(target, before, template, input.phasegateVersion);
|
|
318
341
|
const didChange = before !== next;
|
|
319
342
|
const action = this.actionFor(before, didChange, input.force);
|
|
320
343
|
const item: InstallPlanItem = {
|
|
@@ -323,8 +346,10 @@ export class RunInstallUseCase {
|
|
|
323
346
|
repairMode,
|
|
324
347
|
strategy: target.strategy,
|
|
325
348
|
changed: didChange,
|
|
326
|
-
summary:
|
|
327
|
-
|
|
349
|
+
summary: unmanagedPersonalRuntimeFile
|
|
350
|
+
? `${target.path}: existing non-phasegate path requires manual review`
|
|
351
|
+
: didChange ? `${target.path}: ${action}` : `${target.path}: already up to date`,
|
|
352
|
+
diff: unmanagedPersonalRuntimeFile ? "manual review required" : this.diffSummary(before, next),
|
|
328
353
|
skillHint: repairMode === "ai-assisted" ? SKILL_HINT : null,
|
|
329
354
|
};
|
|
330
355
|
plan.push(item);
|
|
@@ -340,30 +365,30 @@ export class RunInstallUseCase {
|
|
|
340
365
|
await this.backup(input.projectRoot, target.path, backupDir);
|
|
341
366
|
}
|
|
342
367
|
|
|
368
|
+
try {
|
|
369
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
370
|
+
} catch (error) {
|
|
371
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "mkdir", error);
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
await writeFile(absolutePath, next, "utf8");
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "writeFile", error);
|
|
377
|
+
}
|
|
378
|
+
if (target.executable) {
|
|
343
379
|
try {
|
|
344
|
-
await
|
|
345
|
-
} catch (error) {
|
|
346
|
-
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "mkdir", error);
|
|
347
|
-
}
|
|
348
|
-
try {
|
|
349
|
-
await writeFile(absolutePath, next, "utf8");
|
|
380
|
+
await chmod(absolutePath, 0o755);
|
|
350
381
|
} catch (error) {
|
|
351
|
-
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "
|
|
352
|
-
}
|
|
353
|
-
if (target.executable) {
|
|
354
|
-
try {
|
|
355
|
-
await chmod(absolutePath, 0o755);
|
|
356
|
-
} catch (error) {
|
|
357
|
-
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "chmod", error);
|
|
358
|
-
}
|
|
382
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "chmod", error);
|
|
359
383
|
}
|
|
384
|
+
}
|
|
360
385
|
changed.push(item);
|
|
361
386
|
|
|
362
387
|
const mode = before === null ? "created" : "merged";
|
|
363
388
|
const hash = this.hashCalculator.compute(next);
|
|
364
|
-
const
|
|
365
|
-
if (
|
|
366
|
-
manifest = manifest.addEntry(
|
|
389
|
+
const manifestEntry = baseManifest.findEntry(target.path);
|
|
390
|
+
if (manifestEntry !== null && manifestEntry.hash.equals(hash)) {
|
|
391
|
+
manifest = manifest.addEntry(manifestEntry);
|
|
367
392
|
} else {
|
|
368
393
|
manifest = manifest.addEntry(
|
|
369
394
|
DeploymentEntry.create({
|
|
@@ -377,52 +402,73 @@ export class RunInstallUseCase {
|
|
|
377
402
|
}
|
|
378
403
|
}
|
|
379
404
|
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
405
|
+
const personalSkillTargets = input.personal
|
|
406
|
+
? [
|
|
407
|
+
...(includeClaude ? [".claude/skills"] : []),
|
|
408
|
+
...(includeCodex ? [".codex/skills"] : []),
|
|
409
|
+
]
|
|
410
|
+
: [];
|
|
411
|
+
for (const skillPath of personalSkillTargets) {
|
|
412
|
+
const item = await this.planPersonalSkillDirectory(input, skillPath, baseManifest);
|
|
413
|
+
plan.push(item);
|
|
414
|
+
if (input.apply && item.changed && item.repairMode === "mechanical") {
|
|
415
|
+
try {
|
|
416
|
+
await copyDirectory(join(input.harnessRoot, "skills"), join(input.projectRoot, skillPath));
|
|
417
|
+
await writeFile(
|
|
418
|
+
join(input.projectRoot, skillPath, ".harness-version"),
|
|
419
|
+
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
420
|
+
"utf8",
|
|
421
|
+
);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, item.path, "copyDirectory", error);
|
|
424
|
+
}
|
|
425
|
+
changed.push(item);
|
|
426
|
+
manifest = this.addManifestEntry(baseManifest, manifest, {
|
|
427
|
+
path: item.path,
|
|
428
|
+
mode: "created",
|
|
429
|
+
contentForHash: this.personalSkillsHashInput(item.path, input.phasegateVersion, skillSet),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const linkSpecs = input.personal
|
|
435
|
+
? []
|
|
436
|
+
: [
|
|
437
|
+
...(includeClaude ? [{ path: ".claude/skills", target: "../skills" }] : []),
|
|
438
|
+
...(includeCodex ? [{ path: ".codex/skills", target: "../skills" }] : []),
|
|
439
|
+
];
|
|
440
|
+
for (const linkSpec of linkSpecs) {
|
|
441
|
+
const item = await this.planSkillLink(input.projectRoot, linkSpec.path, linkSpec.target);
|
|
386
442
|
plan.push(item);
|
|
387
443
|
if (!input.apply || !item.changed) continue;
|
|
388
444
|
try {
|
|
389
|
-
await mkdir(join(input.projectRoot, "skills"), { recursive: true });
|
|
390
|
-
await mkdir(dirname(join(input.projectRoot,
|
|
445
|
+
if (!input.personal) await mkdir(join(input.projectRoot, "skills"), { recursive: true });
|
|
446
|
+
await mkdir(dirname(join(input.projectRoot, linkSpec.path)), { recursive: true });
|
|
391
447
|
} catch (error) {
|
|
392
|
-
return this.withApplyError({ plan, refused, changed, backupDir },
|
|
448
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, linkSpec.path, "mkdir", error);
|
|
393
449
|
}
|
|
394
450
|
try {
|
|
395
|
-
await symlink(
|
|
451
|
+
await symlink(linkSpec.target, join(input.projectRoot, linkSpec.path), process.platform === "win32" ? "junction" : "dir");
|
|
396
452
|
} catch (error) {
|
|
397
|
-
return this.withApplyError({ plan, refused, changed, backupDir },
|
|
453
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, linkSpec.path, "symlink", error);
|
|
398
454
|
}
|
|
399
455
|
changed.push(item);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
manifest = manifest.addEntry(
|
|
406
|
-
DeploymentEntry.create({
|
|
407
|
-
path: linkPath,
|
|
408
|
-
mode: "symlink",
|
|
409
|
-
block: null,
|
|
410
|
-
hash,
|
|
411
|
-
deployedAt: new Date().toISOString(),
|
|
412
|
-
}),
|
|
413
|
-
);
|
|
414
|
-
}
|
|
456
|
+
manifest = this.addManifestEntry(baseManifest, manifest, {
|
|
457
|
+
path: linkSpec.path,
|
|
458
|
+
mode: "symlink",
|
|
459
|
+
contentForHash: linkSpec.target,
|
|
460
|
+
});
|
|
415
461
|
}
|
|
416
462
|
|
|
417
463
|
if (input.personal && includeCodex) {
|
|
418
464
|
plan.push({
|
|
419
|
-
path: "~/.codex/
|
|
465
|
+
path: "~/.codex/config.toml",
|
|
420
466
|
action: "will-skip",
|
|
421
467
|
repairMode: "manual",
|
|
422
468
|
strategy: "json",
|
|
423
469
|
changed: false,
|
|
424
|
-
summary: "~/.codex/
|
|
425
|
-
diff: "manual
|
|
470
|
+
summary: "~/.codex/config.toml: personal mode does not write user-level Codex feature flags; enable hooks manually when needed",
|
|
471
|
+
diff: "manual Codex hooks feature enablement may be required",
|
|
426
472
|
skillHint: null,
|
|
427
473
|
});
|
|
428
474
|
}
|
|
@@ -465,6 +511,11 @@ export class RunInstallUseCase {
|
|
|
465
511
|
readonly includeCi: boolean;
|
|
466
512
|
}): readonly InstallTarget[] {
|
|
467
513
|
return [
|
|
514
|
+
{
|
|
515
|
+
path: "phasegate.config.json",
|
|
516
|
+
strategy: "copy" as const,
|
|
517
|
+
templatePath: "docs/templates/project/phasegate.config.json",
|
|
518
|
+
},
|
|
468
519
|
...(options.includeClaude
|
|
469
520
|
? [
|
|
470
521
|
{ path: ".claude/settings.json", strategy: "json" as const, templatePath: "templates/.claude/settings.json" },
|
|
@@ -525,13 +576,31 @@ export class RunInstallUseCase {
|
|
|
525
576
|
];
|
|
526
577
|
}
|
|
527
578
|
|
|
528
|
-
private createPersonalTargets(): readonly InstallTarget[] {
|
|
579
|
+
private createPersonalTargets(options: { readonly includeClaude: boolean; readonly includeCodex: boolean }): readonly InstallTarget[] {
|
|
529
580
|
return [
|
|
530
581
|
{
|
|
531
|
-
path: ".phasegate-local/config.json",
|
|
582
|
+
path: ".phasegate-local/phasegate.config.json",
|
|
532
583
|
strategy: "copy" as const,
|
|
533
584
|
templatePath: "docs/templates/personal/phasegate-local-config.json",
|
|
534
585
|
},
|
|
586
|
+
...(options.includeClaude
|
|
587
|
+
? [
|
|
588
|
+
{
|
|
589
|
+
path: ".claude/settings.json",
|
|
590
|
+
strategy: "copy" as const,
|
|
591
|
+
templatePath: "templates/.claude/settings.json",
|
|
592
|
+
},
|
|
593
|
+
]
|
|
594
|
+
: []),
|
|
595
|
+
...(options.includeCodex
|
|
596
|
+
? [
|
|
597
|
+
{
|
|
598
|
+
path: ".codex/hooks.json",
|
|
599
|
+
strategy: "copy" as const,
|
|
600
|
+
templatePath: "templates/.codex/hooks.json",
|
|
601
|
+
},
|
|
602
|
+
]
|
|
603
|
+
: []),
|
|
535
604
|
{
|
|
536
605
|
path: ".git/info/exclude",
|
|
537
606
|
strategy: "text-managed" as const,
|
|
@@ -545,6 +614,8 @@ export class RunInstallUseCase {
|
|
|
545
614
|
if (target.strategy === "shell") return shellRepairMode(before);
|
|
546
615
|
if (target.strategy === "text-managed") return "mechanical";
|
|
547
616
|
if (target.strategy === "copy") return "mechanical";
|
|
617
|
+
if (target.strategy === "copy-dir") return "mechanical";
|
|
618
|
+
if (target.strategy === "symlink") return "mechanical";
|
|
548
619
|
if (target.strategy === "json") return jsonRepairMode(before);
|
|
549
620
|
if (target.strategy === "markdown-managed") return "mechanical";
|
|
550
621
|
return "mechanical";
|
|
@@ -589,16 +660,69 @@ export class RunInstallUseCase {
|
|
|
589
660
|
};
|
|
590
661
|
}
|
|
591
662
|
|
|
592
|
-
private async
|
|
663
|
+
private async planPersonalSkillDirectory(
|
|
664
|
+
input: RunInstallInput,
|
|
665
|
+
relativePath: string,
|
|
666
|
+
baseManifest: DeploymentManifest,
|
|
667
|
+
): Promise<InstallPlanItem> {
|
|
668
|
+
const absolutePath = join(input.projectRoot, relativePath);
|
|
669
|
+
const versionPath = join(absolutePath, ".harness-version");
|
|
670
|
+
const current = await readTextOrNull(versionPath);
|
|
671
|
+
const expectedNeedle = `"version": "${input.phasegateVersion}"`;
|
|
672
|
+
const manifestEntry = baseManifest.findEntry(relativePath);
|
|
673
|
+
const pathExists = await exists(absolutePath);
|
|
674
|
+
const unmanagedExisting = pathExists && (current === null || manifestEntry === null);
|
|
675
|
+
const changed = !unmanagedExisting && (current === null || !current.includes(expectedNeedle));
|
|
676
|
+
return {
|
|
677
|
+
path: relativePath,
|
|
678
|
+
action: changed ? "missing" : unmanagedExisting ? "will-merge" : "will-skip",
|
|
679
|
+
repairMode: unmanagedExisting ? "manual" : "mechanical",
|
|
680
|
+
strategy: "copy-dir",
|
|
681
|
+
changed,
|
|
682
|
+
summary: unmanagedExisting
|
|
683
|
+
? `${relativePath}: existing non-phasegate directory requires manual review`
|
|
684
|
+
: changed ? `${relativePath}: deploy bundled skills` : `${relativePath}: already up to date`,
|
|
685
|
+
diff: unmanagedExisting ? "manual review required" : changed ? "+ bundled skills" : "no changes",
|
|
686
|
+
skillHint: null,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private personalSkillsHashInput(path: string, version: string, skillSet: "core" | "all"): string {
|
|
691
|
+
return `personal-skills:${path}:${version}:${skillSet}`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private addManifestEntry(
|
|
695
|
+
baseManifest: DeploymentManifest,
|
|
696
|
+
manifest: DeploymentManifest,
|
|
697
|
+
input: { readonly path: string; readonly mode: "created" | "symlink"; readonly contentForHash: string },
|
|
698
|
+
): DeploymentManifest {
|
|
699
|
+
const hash = this.hashCalculator.compute(input.contentForHash);
|
|
700
|
+
const existingEntry = baseManifest.findEntry(input.path);
|
|
701
|
+
if (existingEntry !== null && existingEntry.hash.equals(hash)) {
|
|
702
|
+
return manifest.addEntry(existingEntry);
|
|
703
|
+
}
|
|
704
|
+
return manifest.addEntry(
|
|
705
|
+
DeploymentEntry.create({
|
|
706
|
+
path: input.path,
|
|
707
|
+
mode: input.mode,
|
|
708
|
+
block: null,
|
|
709
|
+
hash,
|
|
710
|
+
deployedAt: new Date().toISOString(),
|
|
711
|
+
}),
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private async planSkillLink(projectRoot: string, relativePath: string, target: string): Promise<InstallPlanItem> {
|
|
593
716
|
const absolutePath = join(projectRoot, relativePath);
|
|
717
|
+
const parentPath = dirname(absolutePath);
|
|
594
718
|
try {
|
|
595
719
|
const stat = await lstat(absolutePath);
|
|
596
|
-
if (stat.isSymbolicLink() && (await readlink(absolutePath)) ===
|
|
720
|
+
if (stat.isSymbolicLink() && (await readlink(absolutePath)) === target) {
|
|
597
721
|
return {
|
|
598
722
|
path: relativePath,
|
|
599
723
|
action: "will-skip",
|
|
600
724
|
repairMode: "mechanical",
|
|
601
|
-
strategy: "
|
|
725
|
+
strategy: "symlink",
|
|
602
726
|
changed: false,
|
|
603
727
|
summary: `${relativePath}: already linked`,
|
|
604
728
|
diff: "no changes",
|
|
@@ -609,21 +733,36 @@ export class RunInstallUseCase {
|
|
|
609
733
|
path: relativePath,
|
|
610
734
|
action: "will-merge",
|
|
611
735
|
repairMode: "manual",
|
|
612
|
-
strategy: "
|
|
736
|
+
strategy: "symlink",
|
|
613
737
|
changed: false,
|
|
614
738
|
summary: `${relativePath}: existing non-phasegate path requires manual review`,
|
|
615
739
|
diff: "manual review required",
|
|
616
740
|
skillHint: null,
|
|
617
741
|
};
|
|
618
742
|
} catch {
|
|
743
|
+
try {
|
|
744
|
+
const parentStat = await lstat(parentPath);
|
|
745
|
+
if (!parentStat.isDirectory()) {
|
|
746
|
+
return {
|
|
747
|
+
path: relativePath,
|
|
748
|
+
action: "will-merge",
|
|
749
|
+
repairMode: "manual",
|
|
750
|
+
strategy: "symlink",
|
|
751
|
+
changed: false,
|
|
752
|
+
summary: `${relativePath}: parent path exists and requires manual review`,
|
|
753
|
+
diff: "manual review required",
|
|
754
|
+
skillHint: null,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
} catch {}
|
|
619
758
|
return {
|
|
620
759
|
path: relativePath,
|
|
621
760
|
action: "missing",
|
|
622
761
|
repairMode: "mechanical",
|
|
623
|
-
strategy: "
|
|
762
|
+
strategy: "symlink",
|
|
624
763
|
changed: true,
|
|
625
764
|
summary: `${relativePath}: create symlink`,
|
|
626
|
-
diff:
|
|
765
|
+
diff: `+ symlink ${target}`,
|
|
627
766
|
skillHint: null,
|
|
628
767
|
};
|
|
629
768
|
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// @work-item-id WI-174
|
|
5
5
|
// @work-item-id WI-199
|
|
6
6
|
// @work-item-id WI-207
|
|
7
|
+
// @work-item-id WI-208
|
|
8
|
+
// @work-item-id WI-209
|
|
7
9
|
|
|
8
10
|
import { access, copyFile, lstat, mkdir, readFile, readlink, rm, rmdir, writeFile } from "node:fs/promises";
|
|
9
11
|
import { dirname, join, relative, resolve } from "node:path";
|
|
@@ -163,6 +165,7 @@ export class RunUninstallUseCase {
|
|
|
163
165
|
repairMode: "manual",
|
|
164
166
|
strategy: "unknown",
|
|
165
167
|
changed: false,
|
|
168
|
+
protected: false,
|
|
166
169
|
summary: "manifest missing; run phasegate doctor and clean up manually",
|
|
167
170
|
diff: "manual cleanup required",
|
|
168
171
|
skillHint: SKILL_HINT,
|
|
@@ -216,8 +219,12 @@ export class RunUninstallUseCase {
|
|
|
216
219
|
readonly apply: () => Promise<void>;
|
|
217
220
|
}> {
|
|
218
221
|
const absolutePath = this.resolveProjectPath(input.projectRoot, entry.path);
|
|
219
|
-
const currentContent = await readTextOrNull(absolutePath);
|
|
220
222
|
const strategy = this.strategyFor(entry.path, entry.mode);
|
|
223
|
+
if (entry.mode === "symlink") return this.planSymlink(input.projectRoot, entry);
|
|
224
|
+
if (entry.mode === "created" && await this.isDirectory(absolutePath)) {
|
|
225
|
+
return this.planCreatedDirectory(input.projectRoot, entry);
|
|
226
|
+
}
|
|
227
|
+
const currentContent = await readTextOrNull(absolutePath);
|
|
221
228
|
if (currentContent === null && strategy !== "symlink") {
|
|
222
229
|
return {
|
|
223
230
|
item: this.item(entry.path, "skip", "mechanical", strategy, false, `${entry.path}: already absent`, "no changes", null),
|
|
@@ -226,7 +233,6 @@ export class RunUninstallUseCase {
|
|
|
226
233
|
};
|
|
227
234
|
}
|
|
228
235
|
|
|
229
|
-
if (entry.mode === "symlink") return this.planSymlink(input.projectRoot, entry);
|
|
230
236
|
if (entry.mode === "created") return this.planCreated(input.projectRoot, entry, currentContent ?? "");
|
|
231
237
|
return this.planMerged(input, entry, currentContent ?? "", strategy);
|
|
232
238
|
}
|
|
@@ -240,9 +246,9 @@ export class RunUninstallUseCase {
|
|
|
240
246
|
} catch {
|
|
241
247
|
target = null;
|
|
242
248
|
}
|
|
243
|
-
if (target
|
|
249
|
+
if (target !== null && this.hashCalculator.compute(target).equals(entry.hash)) {
|
|
244
250
|
return {
|
|
245
|
-
item: this.item(entry.path, "unlink", "mechanical", "symlink", true, `${entry.path}: remove symlink`,
|
|
251
|
+
item: this.item(entry.path, "unlink", "mechanical", "symlink", true, `${entry.path}: remove symlink`, `- symlink ${target}`, null),
|
|
246
252
|
needsBackup: false,
|
|
247
253
|
apply: async () => {
|
|
248
254
|
await rm(absolutePath, { force: true });
|
|
@@ -256,6 +262,35 @@ export class RunUninstallUseCase {
|
|
|
256
262
|
};
|
|
257
263
|
}
|
|
258
264
|
|
|
265
|
+
private async isDirectory(path: string): Promise<boolean> {
|
|
266
|
+
try {
|
|
267
|
+
const stat = await lstat(path);
|
|
268
|
+
return stat.isDirectory();
|
|
269
|
+
} catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private async planCreatedDirectory(projectRoot: string, entry: DeploymentEntry) {
|
|
275
|
+
const absolutePath = this.resolveProjectPath(projectRoot, entry.path);
|
|
276
|
+
return {
|
|
277
|
+
item: this.item(
|
|
278
|
+
entry.path,
|
|
279
|
+
"delete",
|
|
280
|
+
"mechanical",
|
|
281
|
+
"created",
|
|
282
|
+
true,
|
|
283
|
+
`${entry.path}: delete created directory`,
|
|
284
|
+
"- directory",
|
|
285
|
+
null,
|
|
286
|
+
),
|
|
287
|
+
needsBackup: false,
|
|
288
|
+
apply: async () => {
|
|
289
|
+
await rm(absolutePath, { recursive: true, force: true });
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
259
294
|
private async planCreated(projectRoot: string, entry: DeploymentEntry, currentContent: string) {
|
|
260
295
|
const absolutePath = this.resolveProjectPath(projectRoot, entry.path);
|
|
261
296
|
const currentHash = this.hashCalculator.compute(currentContent);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// @layer presentation
|
|
3
3
|
// @work-item-id WI-145
|
|
4
4
|
// @work-item-id WI-178
|
|
5
|
+
// @work-item-id WI-208
|
|
5
6
|
|
|
6
7
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
8
|
import { dirname, isAbsolute, join } from "node:path";
|
|
@@ -37,6 +38,7 @@ export class DoctorHandler {
|
|
|
37
38
|
const formatInput = {
|
|
38
39
|
report: result.report,
|
|
39
40
|
agent: result.agent,
|
|
41
|
+
installationMode: result.installationMode,
|
|
40
42
|
scopedOutFindings: result.scopedOutFindings,
|
|
41
43
|
phasegateVersion: input.phasegateVersion,
|
|
42
44
|
projectRoot: input.projectRoot,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// @work-item-id WI-178
|
|
5
5
|
// @work-item-id WI-179
|
|
6
6
|
// @work-item-id WI-180
|
|
7
|
+
// @work-item-id WI-208
|
|
7
8
|
|
|
8
9
|
import type { DoctorAgentScope, ScopedOutDiagnosticFinding } from "../../application/usecases/run-doctor-diagnostics.js";
|
|
9
10
|
import type { DiagnosticReport } from "../../domain/diagnostic-report.js";
|
|
@@ -11,6 +12,7 @@ import type { DiagnosticReport } from "../../domain/diagnostic-report.js";
|
|
|
11
12
|
export interface DiagnosticReportFormatterInput {
|
|
12
13
|
readonly report: DiagnosticReport;
|
|
13
14
|
readonly agent: DoctorAgentScope;
|
|
15
|
+
readonly installationMode: "project" | "personal";
|
|
14
16
|
readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
|
|
15
17
|
readonly phasegateVersion: string;
|
|
16
18
|
readonly projectRoot: string;
|
|
@@ -26,7 +28,8 @@ export class DiagnosticReportFormatter {
|
|
|
26
28
|
projectRoot: input.projectRoot,
|
|
27
29
|
scope: {
|
|
28
30
|
agent: input.agent,
|
|
29
|
-
|
|
31
|
+
installationMode: input.installationMode,
|
|
32
|
+
description: scopeDescription(input.agent, input.installationMode),
|
|
30
33
|
},
|
|
31
34
|
overallStatus: input.report.overallStatus,
|
|
32
35
|
findings: input.report.findings.map((finding) => ({
|
|
@@ -60,7 +63,7 @@ export class DiagnosticReportFormatter {
|
|
|
60
63
|
const lines = [
|
|
61
64
|
`phasegate doctor v${input.phasegateVersion}`,
|
|
62
65
|
`Project: ${input.projectRoot}`,
|
|
63
|
-
`Scope: ${input.agent} (${scopeDescription(input.agent)})`,
|
|
66
|
+
`Scope: ${input.agent} / ${input.installationMode} (${scopeDescription(input.agent, input.installationMode)})`,
|
|
64
67
|
"",
|
|
65
68
|
];
|
|
66
69
|
for (const finding of input.report.findings) {
|
|
@@ -86,7 +89,12 @@ export class DiagnosticReportFormatter {
|
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
function scopeDescription(agent: DoctorAgentScope): string {
|
|
92
|
+
function scopeDescription(agent: DoctorAgentScope, installationMode: "project" | "personal"): string {
|
|
93
|
+
if (installationMode === "personal") {
|
|
94
|
+
if (agent === "claude") return "Personal Claude Code sandbox; team/project Husky, CI, package, and Codex-only findings are not repair targets.";
|
|
95
|
+
if (agent === "codex") return "Personal Codex sandbox; team/project Husky, CI, package, and Claude-only findings are not repair targets.";
|
|
96
|
+
return "Personal sandbox diagnostics; team/project Husky, CI, and package findings are not repair targets.";
|
|
97
|
+
}
|
|
90
98
|
if (agent === "claude") return "Claude Code and shared setup targets; Codex-only findings are not applicable.";
|
|
91
99
|
if (agent === "codex") return "Codex and shared setup targets; Claude-only findings are not applicable.";
|
|
92
100
|
return "Full setup diagnostics for Claude, Codex, and shared targets.";
|
package/scripts/harness/main.ts
CHANGED
|
@@ -171,7 +171,7 @@ Setup:
|
|
|
171
171
|
scaffold-wi <unit|_cross> <story|issue|chore>
|
|
172
172
|
Create docs/inception/{unit}/WI-XXX/description.md
|
|
173
173
|
emit-agent-rules Print AGENTS.md / CLAUDE.md WI workflow rules block
|
|
174
|
-
install Install phasegate managed files (--dry-run|--apply, --force, --personal)
|
|
174
|
+
install Install phasegate managed files (--dry-run|--apply, --force, --personal, --agent <claude|codex|both>)
|
|
175
175
|
uninstall Uninstall phasegate managed files (--dry-run|--apply, --force)
|
|
176
176
|
reconcile Reconcile phasegate managed files (--dry-run|--apply, --force)
|
|
177
177
|
setup:agent Diagnose repo setup and produce/apply an agent-readable setup plan
|
|
@@ -625,7 +625,8 @@ Options:
|
|
|
625
625
|
--workflow <standard|strict> Rendered agent context workflow mode (default: standard)
|
|
626
626
|
--with-husky Include Husky hook targets
|
|
627
627
|
--with-ci Include GitHub Actions target
|
|
628
|
-
--personal Use local-only install: no package.json, agent docs, Husky, CI,
|
|
628
|
+
--personal Use local-only install: no package.json, agent docs, Husky, CI, .gitignore, GitHub CLI, secrets, or CI setting writes.
|
|
629
|
+
With --agent claude, initializes .phasegate-local config/settings/skills and ignored .claude shims.
|
|
629
630
|
--json Output machine-readable JSON
|
|
630
631
|
--help, -h Show this help`,
|
|
631
632
|
"setup:agent": `Usage: phasegate setup:agent [options]
|
package/skills/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
`.agent/skills` は旧 setup 由来の互換パスです。新規導入では管理対象にしません。<!-- @work-item-id WI-157 -->
|
|
18
18
|
|
|
19
|
-
`phasegate install --personal` はチーム所有リポジトリでの個人評価用です。このモードでは
|
|
19
|
+
`phasegate install --personal` はチーム所有リポジトリでの個人評価用です。このモードでは team-owned files を変更せず、`.phasegate-local/phasegate.config.json` と `.git/info/exclude` の local-only managed block、選択 agent 用の real `.claude/*` / `.codex/*` runtime artifacts だけを扱います。通常の共有導入が必要になった時点で `phasegate install --dry-run` / `phasegate install --apply` を使います。<!-- @work-item-id WI-207 --> <!-- @work-item-id WI-209 -->
|
|
20
20
|
|
|
21
21
|
## 新しいスキルの追加
|
|
22
22
|
|
|
@@ -35,7 +35,7 @@ phasegate を導入した直後の config は単純な default で、実プロ
|
|
|
35
35
|
| hook config | `.claude/scripts/hook-config.json` | 既存 hook 設定確認 |
|
|
36
36
|
| doctor report | 明示された report path、またはユーザーが指定した `.phasegate/last-doctor-report.json` | `repairMode` / `repairHint` / `suggestedSkill` の確認 |
|
|
37
37
|
| manifest | `.phasegate/manifest.json` | install / reconcile / uninstall の managed target と hash 状態確認 |
|
|
38
|
-
| personal config | `.phasegate-local/config.json` | `install --personal` の local-only 設定確認 |
|
|
38
|
+
| personal config | `.phasegate-local/phasegate.config.json` | `install --personal` の local-only 設定確認 |
|
|
39
39
|
| git local exclude | `.git/info/exclude` | `install --personal` の repository-local ignore block 確認 |
|
|
40
40
|
| Claude hooks | `.claude/settings.json` | managed hook JSON と user customization の確認 |
|
|
41
41
|
| Codex hooks | `.codex/hooks.json` | managed hook JSON と Codex hook 配線確認 |
|
|
@@ -129,7 +129,7 @@ product-architect で Unit を作り、いくつかの logical_design を書い
|
|
|
129
129
|
#### 観点 9: setup lifecycle と doctor finding
|
|
130
130
|
|
|
131
131
|
- `phasegate doctor --json` の finding に `repairMode: "ai-assisted"` と `suggestedSkill.skillName = "phasegate-config-doctor"` がある → 本 skill が merge 方針、保持する user content、実行すべき `install --apply` / `--force` / `reconcile --apply` を提案する
|
|
132
|
-
- チーム所有リポジトリで個人評価だけを行う相談では、通常の `install --apply` ではなく `phasegate install --personal --dry-run` を先に提案する。personal install は `package.json`, `AGENTS.md`, `CLAUDE.md`, `.husky/*`, `.github/workflows/*`, `.gitignore`,
|
|
132
|
+
- チーム所有リポジトリで個人評価だけを行う相談では、通常の `install --apply` ではなく `phasegate install --personal --agent claude --dry-run` を先に提案する。personal install は `package.json`, `AGENTS.md`, `CLAUDE.md`, `.husky/*`, `.github/workflows/*`, `.gitignore`, GitHub CLI config, repo secrets, CI settings を変更対象にしない。選択 agent 向けには `.phasegate-local/phasegate.config.json`, real `.claude/settings.json` / `.claude/skills/`, real `.codex/hooks.json` / `.codex/skills/`, `.phasegate/manifest.json`, `.git/info/exclude` の managed block を自動作成する。既存 `.claude/*` / `.codex/*` がある場合は上書きせず manual review として扱う。Codex user-level hook feature enablement は manual action として扱う。<!-- @work-item-id WI-207 --> <!-- @work-item-id WI-208 --> <!-- @work-item-id WI-209 -->
|
|
133
133
|
- Claude-only / Codex-only 導入後は `phasegate doctor --agent claude --json` または `phasegate doctor --agent codex --json` を使って selected agent の readiness を読む。`scopedOutFindings` は未選択 agent の `not-applicable` 情報なので、ユーザーがその agent を導入したいと言っていない限り repair 提案にしない。`repairHint: null` / `suggestedSkill: null` は意図的な抑制で、`currentScopeRepairTarget: false` と `repairModeApplicability: "only-if-agent-selected"` は raw `repairMode` が current scope の修復指示ではないという印である。<!-- @work-item-id WI-178, WI-179, WI-180 -->
|
|
134
134
|
- `repairHint` がある mechanical finding → 原則として hint のコマンドを優先し、実行前に対象ファイルと manifest の差分を確認
|
|
135
135
|
- manifest parse error → `.phasegate/manifest.json` を手で修復する前に backup / uninstall / reinstall の選択肢を提示
|
|
@@ -130,7 +130,7 @@ docs/guide/ # phasegate リポジトリ自体 (dogfood)
|
|
|
130
130
|
- 既存プロジェクト導入: `docs/guide/retrofit-adoption.md`
|
|
131
131
|
- setup artifact / doctor finding / legacy artifact: `docs/guide/setup-artifacts.md`
|
|
132
132
|
|
|
133
|
-
チーム所有リポジトリで個人評価だけを行いたい場合は、`docs/guide/installation.md` の personal install セクションを読む。`phasegate install --personal
|
|
133
|
+
チーム所有リポジトリで個人評価だけを行いたい場合は、`docs/guide/installation.md` の personal install セクションを読む。`phasegate install --personal --agent <agent>` は `package.json`、`AGENTS.md`、`CLAUDE.md`、`.husky/*`、`.github/workflows/*`、`.gitignore`、GitHub CLI config、repo secrets、CI settings を変更せず、`.phasegate-local/phasegate.config.json`、real `.claude/settings.json` / `.claude/skills/`、real `.codex/hooks.json` / `.codex/skills/`、`.git/info/exclude` の managed block を使う。Codex user-level hook feature enablement は manual action として説明する。<!-- @work-item-id WI-207 --> <!-- @work-item-id WI-208 --> <!-- @work-item-id WI-209 -->
|
|
134
134
|
|
|
135
135
|
`setup-artifacts.md` は managed target / generated artifact / runtime state / legacy artifact / user-level setting の分類を持つ。`doctor --report-out` は明示 path への出力で、`.phasegate/last-doctor-report.json` は固定生成物ではない点もここを参照する。<!-- @work-item-id WI-153 -->
|
|
136
136
|
|