gsd-pi 2.38.0-dev.96dc7fb → 2.38.0-dev.eeb3520
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/dist/cli.js +0 -9
- package/dist/extension-discovery.d.ts +3 -5
- package/dist/extension-discovery.js +9 -14
- package/dist/resources/extensions/browser-tools/package.json +1 -3
- package/dist/resources/extensions/cmux/index.js +1 -55
- package/dist/resources/extensions/context7/package.json +1 -1
- package/dist/resources/extensions/google-search/package.json +1 -3
- package/dist/resources/extensions/gsd/auto-loop.js +1 -7
- package/dist/resources/extensions/gsd/auto-start.js +1 -6
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +4 -11
- package/dist/resources/extensions/gsd/captures.js +1 -9
- package/dist/resources/extensions/gsd/commands-handlers.js +3 -16
- package/dist/resources/extensions/gsd/commands.js +1 -20
- package/dist/resources/extensions/gsd/doctor-checks.js +0 -82
- package/dist/resources/extensions/gsd/doctor-environment.js +0 -78
- package/dist/resources/extensions/gsd/doctor-format.js +0 -15
- package/dist/resources/extensions/gsd/doctor.js +11 -184
- package/dist/resources/extensions/gsd/package.json +1 -1
- package/dist/resources/extensions/gsd/worktree.js +16 -35
- package/dist/resources/extensions/subagent/index.js +3 -12
- package/dist/resources/extensions/universal-config/package.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +4 -8
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/src/core/package-manager.ts +4 -8
- package/src/resources/extensions/cmux/index.ts +1 -57
- package/src/resources/extensions/gsd/auto-loop.ts +1 -13
- package/src/resources/extensions/gsd/auto-start.ts +1 -7
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -12
- package/src/resources/extensions/gsd/captures.ts +1 -10
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -17
- package/src/resources/extensions/gsd/commands.ts +1 -21
- package/src/resources/extensions/gsd/doctor-checks.ts +0 -75
- package/src/resources/extensions/gsd/doctor-environment.ts +1 -82
- package/src/resources/extensions/gsd/doctor-format.ts +0 -20
- package/src/resources/extensions/gsd/doctor-types.ts +1 -16
- package/src/resources/extensions/gsd/doctor.ts +13 -177
- package/src/resources/extensions/gsd/tests/cmux.test.ts +0 -93
- package/src/resources/extensions/gsd/tests/worktree.test.ts +0 -47
- package/src/resources/extensions/gsd/worktree.ts +15 -35
- package/src/resources/extensions/subagent/index.ts +3 -12
- package/dist/welcome-screen.d.ts +0 -12
- package/dist/welcome-screen.js +0 -53
- package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +0 -266
|
@@ -289,17 +289,10 @@ export class CmuxClient {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
async createSplit(direction: "right" | "down" | "left" | "up"): Promise<string | null> {
|
|
292
|
-
return this.createSplitFrom(this.config.surfaceId, direction);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async createSplitFrom(
|
|
296
|
-
sourceSurfaceId: string | undefined,
|
|
297
|
-
direction: "right" | "down" | "left" | "up",
|
|
298
|
-
): Promise<string | null> {
|
|
299
292
|
if (!this.config.splits) return null;
|
|
300
293
|
const before = new Set(await this.listSurfaceIds());
|
|
301
294
|
const args = ["new-split", direction];
|
|
302
|
-
const scopedArgs = this.appendSurface(this.appendWorkspace(args),
|
|
295
|
+
const scopedArgs = this.appendSurface(this.appendWorkspace(args), this.config.surfaceId);
|
|
303
296
|
await this.runAsync(scopedArgs);
|
|
304
297
|
const after = await this.listSurfaceIds();
|
|
305
298
|
for (const id of after) {
|
|
@@ -308,55 +301,6 @@ export class CmuxClient {
|
|
|
308
301
|
return null;
|
|
309
302
|
}
|
|
310
303
|
|
|
311
|
-
/**
|
|
312
|
-
* Create a grid of surfaces for parallel agent execution.
|
|
313
|
-
*
|
|
314
|
-
* Layout strategy (gsd stays in the original surface):
|
|
315
|
-
* 1 agent: [gsd | A]
|
|
316
|
-
* 2 agents: [gsd | A]
|
|
317
|
-
* [ | B]
|
|
318
|
-
* 3 agents: [gsd | A]
|
|
319
|
-
* [ C | B]
|
|
320
|
-
* 4 agents: [gsd | A]
|
|
321
|
-
* [ C | B] (D splits from B downward)
|
|
322
|
-
* [ | D]
|
|
323
|
-
*
|
|
324
|
-
* Returns surface IDs in order, or empty array on failure.
|
|
325
|
-
*/
|
|
326
|
-
async createGridLayout(count: number): Promise<string[]> {
|
|
327
|
-
if (!this.config.splits || count <= 0) return [];
|
|
328
|
-
const surfaces: string[] = [];
|
|
329
|
-
|
|
330
|
-
// First split: create right column from the gsd surface
|
|
331
|
-
const rightCol = await this.createSplitFrom(this.config.surfaceId, "right");
|
|
332
|
-
if (!rightCol) return [];
|
|
333
|
-
surfaces.push(rightCol);
|
|
334
|
-
if (count === 1) return surfaces;
|
|
335
|
-
|
|
336
|
-
// Second split: split right column down → bottom-right
|
|
337
|
-
const bottomRight = await this.createSplitFrom(rightCol, "down");
|
|
338
|
-
if (!bottomRight) return surfaces;
|
|
339
|
-
surfaces.push(bottomRight);
|
|
340
|
-
if (count === 2) return surfaces;
|
|
341
|
-
|
|
342
|
-
// Third split: split gsd surface down → bottom-left
|
|
343
|
-
const bottomLeft = await this.createSplitFrom(this.config.surfaceId, "down");
|
|
344
|
-
if (!bottomLeft) return surfaces;
|
|
345
|
-
surfaces.push(bottomLeft);
|
|
346
|
-
if (count === 3) return surfaces;
|
|
347
|
-
|
|
348
|
-
// Fourth+: split subsequent surfaces down from the last created
|
|
349
|
-
let lastSurface = bottomRight;
|
|
350
|
-
for (let i = 3; i < count; i++) {
|
|
351
|
-
const next = await this.createSplitFrom(lastSurface, "down");
|
|
352
|
-
if (!next) break;
|
|
353
|
-
surfaces.push(next);
|
|
354
|
-
lastSurface = next;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return surfaces;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
304
|
async sendSurface(surfaceId: string, text: string): Promise<boolean> {
|
|
361
305
|
const payload = text.endsWith("\n") ? text : `${text}\n`;
|
|
362
306
|
const stdout = await this.runAsync(["send-surface", "--surface", surfaceId, payload]);
|
|
@@ -787,7 +787,7 @@ export async function autoLoop(
|
|
|
787
787
|
(m: { status: string }) =>
|
|
788
788
|
m.status !== "complete" && m.status !== "parked",
|
|
789
789
|
);
|
|
790
|
-
if (incomplete.length === 0
|
|
790
|
+
if (incomplete.length === 0) {
|
|
791
791
|
// All milestones complete — merge milestone branch before stopping
|
|
792
792
|
if (s.currentMilestoneId) {
|
|
793
793
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
@@ -804,18 +804,6 @@ export async function autoLoop(
|
|
|
804
804
|
"success",
|
|
805
805
|
);
|
|
806
806
|
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
807
|
-
} else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
808
|
-
// Empty registry — no milestones visible, likely a path resolution bug
|
|
809
|
-
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
810
|
-
ctx.ui.notify(
|
|
811
|
-
`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`,
|
|
812
|
-
"error",
|
|
813
|
-
);
|
|
814
|
-
await deps.stopAuto(
|
|
815
|
-
ctx,
|
|
816
|
-
pi,
|
|
817
|
-
`No milestones found — check basePath resolution`,
|
|
818
|
-
);
|
|
819
807
|
} else if (state.phase === "blocked") {
|
|
820
808
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
821
809
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
@@ -429,16 +429,10 @@ export async function bootstrapAutoSession(
|
|
|
429
429
|
s.originalBasePath = base;
|
|
430
430
|
|
|
431
431
|
const isUnderGsdWorktrees = (p: string): boolean => {
|
|
432
|
-
// Direct layout: /.gsd/worktrees/
|
|
433
432
|
const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
|
|
434
433
|
if (p.includes(marker)) return true;
|
|
435
434
|
const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`;
|
|
436
|
-
|
|
437
|
-
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
|
|
438
|
-
const symlinkRe = new RegExp(
|
|
439
|
-
`\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees(?:\\${pathSep}|$)`,
|
|
440
|
-
);
|
|
441
|
-
return symlinkRe.test(p);
|
|
435
|
+
return p.endsWith(worktreesSuffix);
|
|
442
436
|
};
|
|
443
437
|
|
|
444
438
|
if (
|
|
@@ -153,18 +153,9 @@ export function checkResourcesStale(
|
|
|
153
153
|
* Returns the corrected base path.
|
|
154
154
|
*/
|
|
155
155
|
export function escapeStaleWorktree(base: string): string {
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
if (idx === -1) {
|
|
160
|
-
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
|
|
161
|
-
const symlinkRe = new RegExp(
|
|
162
|
-
`\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees\\${pathSep}`,
|
|
163
|
-
);
|
|
164
|
-
const match = base.match(symlinkRe);
|
|
165
|
-
if (!match || match.index === undefined) return base;
|
|
166
|
-
idx = match.index;
|
|
167
|
-
}
|
|
156
|
+
const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
|
|
157
|
+
const idx = base.indexOf(marker);
|
|
158
|
+
if (idx === -1) return base;
|
|
168
159
|
|
|
169
160
|
// base is inside .gsd/worktrees/<something> — extract the project root
|
|
170
161
|
const projectRoot = base.slice(0, idx);
|
|
@@ -59,17 +59,8 @@ const VALID_CLASSIFICATIONS: readonly string[] = [
|
|
|
59
59
|
*/
|
|
60
60
|
export function resolveCapturesPath(basePath: string): string {
|
|
61
61
|
const resolved = resolve(basePath);
|
|
62
|
-
// Direct layout: /.gsd/worktrees/
|
|
63
62
|
const worktreeMarker = `${sep}.gsd${sep}worktrees${sep}`;
|
|
64
|
-
|
|
65
|
-
if (idx === -1) {
|
|
66
|
-
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
|
|
67
|
-
const symlinkRe = new RegExp(
|
|
68
|
-
`\\${sep}\\.gsd\\${sep}projects\\${sep}[a-f0-9]+\\${sep}worktrees\\${sep}`,
|
|
69
|
-
);
|
|
70
|
-
const match = resolved.match(symlinkRe);
|
|
71
|
-
if (match && match.index !== undefined) idx = match.index;
|
|
72
|
-
}
|
|
63
|
+
const idx = resolved.indexOf(worktreeMarker);
|
|
73
64
|
if (idx !== -1) {
|
|
74
65
|
// basePath is inside a worktree — resolve to project root
|
|
75
66
|
const projectRoot = resolved.slice(0, idx);
|
|
@@ -15,7 +15,6 @@ import { appendOverride, appendKnowledge } from "./files.js";
|
|
|
15
15
|
import {
|
|
16
16
|
formatDoctorIssuesForPrompt,
|
|
17
17
|
formatDoctorReport,
|
|
18
|
-
formatDoctorReportJson,
|
|
19
18
|
runGSDDoctor,
|
|
20
19
|
selectDoctorScope,
|
|
21
20
|
filterDoctorIssues,
|
|
@@ -44,30 +43,16 @@ export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined,
|
|
|
44
43
|
|
|
45
44
|
export async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
46
45
|
const trimmed = args.trim();
|
|
47
|
-
|
|
48
|
-
const jsonMode = trimmed.includes("--json");
|
|
49
|
-
const dryRun = trimmed.includes("--dry-run");
|
|
50
|
-
const includeBuild = trimmed.includes("--build");
|
|
51
|
-
const includeTests = trimmed.includes("--test");
|
|
52
|
-
const stripped = trimmed.replace(/--json|--dry-run|--build|--test/g, "").trim();
|
|
53
|
-
const parts = stripped ? stripped.split(/\s+/) : [];
|
|
46
|
+
const parts = trimmed ? trimmed.split(/\s+/) : [];
|
|
54
47
|
const mode = parts[0] === "fix" || parts[0] === "heal" || parts[0] === "audit" ? parts[0] : "doctor";
|
|
55
48
|
const requestedScope = mode === "doctor" ? parts[0] : parts[1];
|
|
56
49
|
const scope = await selectDoctorScope(projectRoot(), requestedScope);
|
|
57
50
|
const effectiveScope = mode === "audit" ? requestedScope : scope;
|
|
58
51
|
const report = await runGSDDoctor(projectRoot(), {
|
|
59
|
-
fix: mode === "fix" || mode === "heal"
|
|
60
|
-
dryRun,
|
|
52
|
+
fix: mode === "fix" || mode === "heal",
|
|
61
53
|
scope: effectiveScope,
|
|
62
|
-
includeBuild,
|
|
63
|
-
includeTests,
|
|
64
54
|
});
|
|
65
55
|
|
|
66
|
-
if (jsonMode) {
|
|
67
|
-
ctx.ui.notify(formatDoctorReportJson(report), "info");
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
56
|
const reportText = formatDoctorReport(report, {
|
|
72
57
|
scope: effectiveScope,
|
|
73
58
|
includeWarnings: mode === "audit",
|
|
@@ -159,7 +159,7 @@ async function guardRemoteSession(
|
|
|
159
159
|
|
|
160
160
|
export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
161
161
|
pi.registerCommand("gsd", {
|
|
162
|
-
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|
|
|
162
|
+
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|update",
|
|
163
163
|
getArgumentCompletions: (prefix: string) => {
|
|
164
164
|
const subcommands = [
|
|
165
165
|
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
|
@@ -210,11 +210,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
210
210
|
{ cmd: "templates", desc: "List available workflow templates" },
|
|
211
211
|
{ cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" },
|
|
212
212
|
];
|
|
213
|
-
const hasTrailingSpace = prefix.endsWith(" ");
|
|
214
213
|
const parts = prefix.trim().split(/\s+/);
|
|
215
|
-
if (hasTrailingSpace && parts.length >= 1) {
|
|
216
|
-
parts.push("");
|
|
217
|
-
}
|
|
218
214
|
|
|
219
215
|
if (parts.length <= 1) {
|
|
220
216
|
return subcommands
|
|
@@ -513,10 +509,6 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
513
509
|
{ cmd: "fix", desc: "Auto-fix detected issues" },
|
|
514
510
|
{ cmd: "heal", desc: "AI-driven deep healing" },
|
|
515
511
|
{ cmd: "audit", desc: "Run health audit without fixing" },
|
|
516
|
-
{ cmd: "--dry-run", desc: "Show what --fix would change without applying" },
|
|
517
|
-
{ cmd: "--json", desc: "Output report as JSON (CI/tooling friendly)" },
|
|
518
|
-
{ cmd: "--build", desc: "Include slow build health check (npm run build)" },
|
|
519
|
-
{ cmd: "--test", desc: "Include slow test health check (npm test)" },
|
|
520
512
|
];
|
|
521
513
|
|
|
522
514
|
if (parts.length <= 2) {
|
|
@@ -544,18 +536,6 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
544
536
|
.map((p) => ({ value: `dispatch ${p.cmd}`, label: p.cmd, description: p.desc }));
|
|
545
537
|
}
|
|
546
538
|
|
|
547
|
-
if (parts[0] === "rate" && parts.length <= 2) {
|
|
548
|
-
const tierPrefix = parts[1] ?? "";
|
|
549
|
-
const tiers = [
|
|
550
|
-
{ cmd: "over", desc: "Model was overqualified for this task" },
|
|
551
|
-
{ cmd: "ok", desc: "Model was appropriate for this task" },
|
|
552
|
-
{ cmd: "under", desc: "Model was underqualified for this task" },
|
|
553
|
-
];
|
|
554
|
-
return tiers
|
|
555
|
-
.filter((t) => t.cmd.startsWith(tierPrefix))
|
|
556
|
-
.map((t) => ({ value: `rate ${t.cmd}`, label: t.cmd, description: t.desc }));
|
|
557
|
-
}
|
|
558
|
-
|
|
559
539
|
return [];
|
|
560
540
|
},
|
|
561
541
|
|
|
@@ -657,81 +657,6 @@ export async function checkRuntimeHealth(
|
|
|
657
657
|
} catch {
|
|
658
658
|
// Non-fatal — external state check failed
|
|
659
659
|
}
|
|
660
|
-
|
|
661
|
-
// ── Metrics ledger integrity ───────────────────────────────────────────
|
|
662
|
-
try {
|
|
663
|
-
const metricsPath = join(root, "metrics.json");
|
|
664
|
-
if (existsSync(metricsPath)) {
|
|
665
|
-
try {
|
|
666
|
-
const raw = readFileSync(metricsPath, "utf-8");
|
|
667
|
-
const ledger = JSON.parse(raw);
|
|
668
|
-
if (ledger.version !== 1 || !Array.isArray(ledger.units)) {
|
|
669
|
-
issues.push({
|
|
670
|
-
severity: "warning",
|
|
671
|
-
code: "metrics_ledger_corrupt",
|
|
672
|
-
scope: "project",
|
|
673
|
-
unitId: "project",
|
|
674
|
-
message: "metrics.json has an unexpected structure (version !== 1 or units is not an array) — metrics data may be unreliable",
|
|
675
|
-
file: ".gsd/metrics.json",
|
|
676
|
-
fixable: false,
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
} catch {
|
|
680
|
-
issues.push({
|
|
681
|
-
severity: "warning",
|
|
682
|
-
code: "metrics_ledger_corrupt",
|
|
683
|
-
scope: "project",
|
|
684
|
-
unitId: "project",
|
|
685
|
-
message: "metrics.json is not valid JSON — metrics data may be corrupt",
|
|
686
|
-
file: ".gsd/metrics.json",
|
|
687
|
-
fixable: false,
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
} catch {
|
|
692
|
-
// Non-fatal — metrics check failed
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// ── Large planning file detection ──────────────────────────────────────
|
|
696
|
-
// Files over 100KB can cause LLM context pressure. Report the worst offenders.
|
|
697
|
-
try {
|
|
698
|
-
const MAX_FILE_BYTES = 100 * 1024; // 100KB
|
|
699
|
-
const milestonesPath = milestonesDir(basePath);
|
|
700
|
-
if (existsSync(milestonesPath)) {
|
|
701
|
-
const largeFiles: Array<{ path: string; sizeKB: number }> = [];
|
|
702
|
-
function scanForLargeFiles(dir: string, depth = 0): void {
|
|
703
|
-
if (depth > 6) return;
|
|
704
|
-
try {
|
|
705
|
-
for (const entry of readdirSync(dir)) {
|
|
706
|
-
const full = join(dir, entry);
|
|
707
|
-
try {
|
|
708
|
-
const s = statSync(full);
|
|
709
|
-
if (s.isDirectory()) { scanForLargeFiles(full, depth + 1); continue; }
|
|
710
|
-
if (entry.endsWith(".md") && s.size > MAX_FILE_BYTES) {
|
|
711
|
-
largeFiles.push({ path: full.replace(basePath + "/", ""), sizeKB: Math.round(s.size / 1024) });
|
|
712
|
-
}
|
|
713
|
-
} catch { /* skip entry */ }
|
|
714
|
-
}
|
|
715
|
-
} catch { /* skip dir */ }
|
|
716
|
-
}
|
|
717
|
-
scanForLargeFiles(milestonesPath);
|
|
718
|
-
if (largeFiles.length > 0) {
|
|
719
|
-
largeFiles.sort((a, b) => b.sizeKB - a.sizeKB);
|
|
720
|
-
const worst = largeFiles[0]!;
|
|
721
|
-
issues.push({
|
|
722
|
-
severity: "warning",
|
|
723
|
-
code: "large_planning_file",
|
|
724
|
-
scope: "project",
|
|
725
|
-
unitId: "project",
|
|
726
|
-
message: `${largeFiles.length} planning file(s) exceed 100KB — largest: ${worst.path} (${worst.sizeKB}KB). Large files cause LLM context pressure.`,
|
|
727
|
-
file: worst.path,
|
|
728
|
-
fixable: false,
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
} catch {
|
|
733
|
-
// Non-fatal — large file scan failed
|
|
734
|
-
}
|
|
735
660
|
}
|
|
736
661
|
|
|
737
662
|
/**
|
|
@@ -407,63 +407,6 @@ function checkGitRemote(basePath: string): EnvironmentCheckResult | null {
|
|
|
407
407
|
return { name: "git_remote", status: "ok", message: "Git remote reachable" };
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
/**
|
|
411
|
-
* Check if the project build passes (opt-in slow check, use --build flag).
|
|
412
|
-
* Runs npm run build and reports failure as env_build.
|
|
413
|
-
*/
|
|
414
|
-
function checkBuildHealth(basePath: string): EnvironmentCheckResult | null {
|
|
415
|
-
const pkgPath = join(basePath, "package.json");
|
|
416
|
-
if (!existsSync(pkgPath)) return null;
|
|
417
|
-
|
|
418
|
-
try {
|
|
419
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
420
|
-
const buildScript = pkg.scripts?.build;
|
|
421
|
-
if (!buildScript) return null;
|
|
422
|
-
|
|
423
|
-
const result = tryExec("npm run build 2>&1", basePath);
|
|
424
|
-
if (result === null) {
|
|
425
|
-
return {
|
|
426
|
-
name: "build",
|
|
427
|
-
status: "error",
|
|
428
|
-
message: "Build failed — npm run build exited non-zero",
|
|
429
|
-
detail: "Fix build errors before dispatching work",
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
return { name: "build", status: "ok", message: "Build passes" };
|
|
433
|
-
} catch {
|
|
434
|
-
return null;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Check if tests pass (opt-in slow check, use --test flag).
|
|
440
|
-
* Runs npm test and reports failures as env_test.
|
|
441
|
-
*/
|
|
442
|
-
function checkTestHealth(basePath: string): EnvironmentCheckResult | null {
|
|
443
|
-
const pkgPath = join(basePath, "package.json");
|
|
444
|
-
if (!existsSync(pkgPath)) return null;
|
|
445
|
-
|
|
446
|
-
try {
|
|
447
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
448
|
-
const testScript = pkg.scripts?.test;
|
|
449
|
-
// Skip if no test script or the default placeholder
|
|
450
|
-
if (!testScript || testScript.includes("no test specified")) return null;
|
|
451
|
-
|
|
452
|
-
const result = tryExec("npm test 2>&1", basePath);
|
|
453
|
-
if (result === null) {
|
|
454
|
-
return {
|
|
455
|
-
name: "test",
|
|
456
|
-
status: "warning",
|
|
457
|
-
message: "Tests failing — npm test exited non-zero",
|
|
458
|
-
detail: "Fix failing tests before shipping",
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
return { name: "test", status: "ok", message: "Tests pass" };
|
|
462
|
-
} catch {
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
410
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
468
411
|
|
|
469
412
|
/**
|
|
@@ -511,26 +454,6 @@ export function runFullEnvironmentChecks(basePath: string): EnvironmentCheckResu
|
|
|
511
454
|
return results;
|
|
512
455
|
}
|
|
513
456
|
|
|
514
|
-
/**
|
|
515
|
-
* Run slow opt-in checks (build and/or test).
|
|
516
|
-
* These are never run on the pre-dispatch gate — only on explicit /gsd doctor --build/--test.
|
|
517
|
-
*/
|
|
518
|
-
export function runSlowEnvironmentChecks(
|
|
519
|
-
basePath: string,
|
|
520
|
-
options?: { includeBuild?: boolean; includeTests?: boolean },
|
|
521
|
-
): EnvironmentCheckResult[] {
|
|
522
|
-
const results: EnvironmentCheckResult[] = [];
|
|
523
|
-
if (options?.includeBuild) {
|
|
524
|
-
const buildCheck = checkBuildHealth(basePath);
|
|
525
|
-
if (buildCheck) results.push(buildCheck);
|
|
526
|
-
}
|
|
527
|
-
if (options?.includeTests) {
|
|
528
|
-
const testCheck = checkTestHealth(basePath);
|
|
529
|
-
if (testCheck) results.push(testCheck);
|
|
530
|
-
}
|
|
531
|
-
return results;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
457
|
/**
|
|
535
458
|
* Convert environment check results to DoctorIssue format for the doctor pipeline.
|
|
536
459
|
*/
|
|
@@ -554,16 +477,12 @@ export function environmentResultsToDoctorIssues(results: EnvironmentCheckResult
|
|
|
554
477
|
export async function checkEnvironmentHealth(
|
|
555
478
|
basePath: string,
|
|
556
479
|
issues: DoctorIssue[],
|
|
557
|
-
options?: { includeRemote?: boolean
|
|
480
|
+
options?: { includeRemote?: boolean },
|
|
558
481
|
): Promise<void> {
|
|
559
482
|
const results = options?.includeRemote
|
|
560
483
|
? runFullEnvironmentChecks(basePath)
|
|
561
484
|
: runEnvironmentChecks(basePath);
|
|
562
485
|
|
|
563
|
-
if (options?.includeBuild || options?.includeTests) {
|
|
564
|
-
results.push(...runSlowEnvironmentChecks(basePath, options));
|
|
565
|
-
}
|
|
566
|
-
|
|
567
486
|
issues.push(...environmentResultsToDoctorIssues(results));
|
|
568
487
|
}
|
|
569
488
|
|
|
@@ -76,23 +76,3 @@ export function formatDoctorIssuesForPrompt(issues: DoctorIssue[]): string {
|
|
|
76
76
|
return `- [${prefix}] ${issue.unitId} | ${issue.code} | ${issue.message}${issue.file ? ` | file: ${issue.file}` : ""} | fixable: ${issue.fixable ? "yes" : "no"}`;
|
|
77
77
|
}).join("\n");
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Serialize a doctor report to JSON — suitable for CI/tooling integration.
|
|
82
|
-
* Usage: /gsd doctor --json
|
|
83
|
-
*/
|
|
84
|
-
export function formatDoctorReportJson(report: DoctorReport): string {
|
|
85
|
-
return JSON.stringify(
|
|
86
|
-
{
|
|
87
|
-
ok: report.ok,
|
|
88
|
-
basePath: report.basePath,
|
|
89
|
-
generatedAt: new Date().toISOString(),
|
|
90
|
-
summary: summarizeDoctorIssues(report.issues),
|
|
91
|
-
issues: report.issues,
|
|
92
|
-
fixesApplied: report.fixesApplied,
|
|
93
|
-
...(report.timing ? { timing: report.timing } : {}),
|
|
94
|
-
},
|
|
95
|
-
null,
|
|
96
|
-
2,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
@@ -53,20 +53,7 @@ export type DoctorIssueCode =
|
|
|
53
53
|
| "stranded_lock_directory"
|
|
54
54
|
// Git / worktree integrity checks
|
|
55
55
|
| "integration_branch_missing"
|
|
56
|
-
| "worktree_directory_orphaned"
|
|
57
|
-
// GSD state structural checks
|
|
58
|
-
| "circular_slice_dependency"
|
|
59
|
-
| "orphaned_slice_directory"
|
|
60
|
-
| "duplicate_task_id"
|
|
61
|
-
| "task_file_not_in_plan"
|
|
62
|
-
| "stale_replan_file"
|
|
63
|
-
| "future_timestamp"
|
|
64
|
-
// Runtime data integrity
|
|
65
|
-
| "metrics_ledger_corrupt"
|
|
66
|
-
| "large_planning_file"
|
|
67
|
-
// Slow environment checks (opt-in via --build / --test flags)
|
|
68
|
-
| "env_build"
|
|
69
|
-
| "env_test";
|
|
56
|
+
| "worktree_directory_orphaned";
|
|
70
57
|
|
|
71
58
|
/**
|
|
72
59
|
* Issue codes that represent expected completion-transition states.
|
|
@@ -96,8 +83,6 @@ export interface DoctorReport {
|
|
|
96
83
|
basePath: string;
|
|
97
84
|
issues: DoctorIssue[];
|
|
98
85
|
fixesApplied: string[];
|
|
99
|
-
/** Per-domain check durations in milliseconds. Present on explicit /gsd doctor runs. */
|
|
100
|
-
timing?: { git: number; runtime: number; environment: number; gsdState: number };
|
|
101
86
|
}
|
|
102
87
|
|
|
103
88
|
export interface DoctorSummary {
|