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
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { existsSync, mkdirSync
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
|
|
5
|
-
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile
|
|
5
|
+
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile } from "./paths.js";
|
|
6
6
|
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
7
7
|
import { invalidateAllCaches } from "./cache.js";
|
|
8
8
|
import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
|
|
9
9
|
|
|
10
|
-
import type { DoctorIssue, DoctorIssueCode
|
|
10
|
+
import type { DoctorIssue, DoctorIssueCode } from "./doctor-types.js";
|
|
11
11
|
import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
|
|
12
|
-
import type { RoadmapSliceEntry } from "./types.js";
|
|
13
12
|
import { checkGitHealth, checkRuntimeHealth } from "./doctor-checks.js";
|
|
14
13
|
import { checkEnvironmentHealth } from "./doctor-environment.js";
|
|
15
14
|
import { runProviderChecks } from "./doctor-providers.js";
|
|
@@ -18,7 +17,7 @@ import { runProviderChecks } from "./doctor-providers.js";
|
|
|
18
17
|
// All public types and functions from extracted modules are re-exported here
|
|
19
18
|
// so that existing imports from "./doctor.js" continue to work unchanged.
|
|
20
19
|
export type { DoctorSeverity, DoctorIssueCode, DoctorIssue, DoctorReport, DoctorSummary } from "./doctor-types.js";
|
|
21
|
-
export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt
|
|
20
|
+
export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt } from "./doctor-format.js";
|
|
22
21
|
export { runEnvironmentChecks, runFullEnvironmentChecks, formatEnvironmentReport, type EnvironmentCheckResult } from "./doctor-environment.js";
|
|
23
22
|
export { computeProgressScore, computeProgressScoreWithContext, formatProgressLine, formatProgressReport, type ProgressScore, type ProgressLevel } from "./progress-score.js";
|
|
24
23
|
|
|
@@ -351,60 +350,10 @@ export async function selectDoctorScope(basePath: string, requestedScope?: strin
|
|
|
351
350
|
return state.registry[0]?.id;
|
|
352
351
|
}
|
|
353
352
|
|
|
354
|
-
|
|
355
|
-
function detectCircularDependencies(slices: RoadmapSliceEntry[]): string[][] {
|
|
356
|
-
const known = new Set(slices.map(s => s.id));
|
|
357
|
-
const adj = new Map<string, string[]>();
|
|
358
|
-
for (const s of slices) adj.set(s.id, s.depends.filter(d => known.has(d)));
|
|
359
|
-
const state = new Map<string, "unvisited" | "visiting" | "done">();
|
|
360
|
-
for (const s of slices) state.set(s.id, "unvisited");
|
|
361
|
-
const cycles: string[][] = [];
|
|
362
|
-
function dfs(id: string, path: string[]): void {
|
|
363
|
-
const st = state.get(id);
|
|
364
|
-
if (st === "done") return;
|
|
365
|
-
if (st === "visiting") { cycles.push([...path.slice(path.indexOf(id)), id]); return; }
|
|
366
|
-
state.set(id, "visiting");
|
|
367
|
-
for (const dep of adj.get(id) ?? []) dfs(dep, [...path, id]);
|
|
368
|
-
state.set(id, "done");
|
|
369
|
-
}
|
|
370
|
-
for (const s of slices) if (state.get(s.id) === "unvisited") dfs(s.id, []);
|
|
371
|
-
return cycles;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// ── Helper: doctor run history ──────────────────────────────────────────────
|
|
375
|
-
interface DoctorHistoryEntry { ts: string; ok: boolean; errors: number; warnings: number; fixes: number; codes: string[] }
|
|
376
|
-
|
|
377
|
-
async function appendDoctorHistory(basePath: string, report: DoctorReport): Promise<void> {
|
|
378
|
-
try {
|
|
379
|
-
const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
|
|
380
|
-
const entry = JSON.stringify({
|
|
381
|
-
ts: new Date().toISOString(),
|
|
382
|
-
ok: report.ok,
|
|
383
|
-
errors: report.issues.filter(i => i.severity === "error").length,
|
|
384
|
-
warnings: report.issues.filter(i => i.severity === "warning").length,
|
|
385
|
-
fixes: report.fixesApplied.length,
|
|
386
|
-
codes: [...new Set(report.issues.map(i => i.code))],
|
|
387
|
-
} satisfies DoctorHistoryEntry);
|
|
388
|
-
const existing = existsSync(historyPath) ? readFileSync(historyPath, "utf-8") : "";
|
|
389
|
-
await saveFile(historyPath, existing + entry + "\n");
|
|
390
|
-
} catch { /* non-fatal */ }
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/** Read the last N doctor history entries. Returns most-recent-first. */
|
|
394
|
-
export async function readDoctorHistory(basePath: string, lastN = 50): Promise<DoctorHistoryEntry[]> {
|
|
395
|
-
try {
|
|
396
|
-
const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
|
|
397
|
-
if (!existsSync(historyPath)) return [];
|
|
398
|
-
const lines = readFileSync(historyPath, "utf-8").split("\n").filter(l => l.trim());
|
|
399
|
-
return lines.slice(-lastN).reverse().map(l => JSON.parse(l) as DoctorHistoryEntry);
|
|
400
|
-
} catch { return []; }
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
export async function runGSDDoctor(basePath: string, options?: { fix?: boolean; dryRun?: boolean; scope?: string; fixLevel?: "task" | "all"; isolationMode?: "none" | "worktree" | "branch"; includeBuild?: boolean; includeTests?: boolean }): Promise<DoctorReport> {
|
|
353
|
+
export async function runGSDDoctor(basePath: string, options?: { fix?: boolean; scope?: string; fixLevel?: "task" | "all"; isolationMode?: "none" | "worktree" | "branch" }): Promise<import("./doctor-types.js").DoctorReport> {
|
|
404
354
|
const issues: DoctorIssue[] = [];
|
|
405
355
|
const fixesApplied: string[] = [];
|
|
406
356
|
const fix = options?.fix === true;
|
|
407
|
-
const dryRun = options?.dryRun === true;
|
|
408
357
|
const fixLevel = options?.fixLevel ?? "all";
|
|
409
358
|
|
|
410
359
|
// Issue codes that represent completion state transitions — creating summary
|
|
@@ -415,18 +364,11 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
415
364
|
|
|
416
365
|
/** Whether a given issue code should be auto-fixed at the current fixLevel. */
|
|
417
366
|
const shouldFix = (code: DoctorIssueCode): boolean => {
|
|
418
|
-
if (!fix
|
|
367
|
+
if (!fix) return false;
|
|
419
368
|
if (fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code)) return false;
|
|
420
369
|
return true;
|
|
421
370
|
};
|
|
422
371
|
|
|
423
|
-
/** Log a dry-run "would fix" entry when fix=true but dryRun=true. */
|
|
424
|
-
const dryRunCanFix = (code: DoctorIssueCode, message: string): void => {
|
|
425
|
-
if (dryRun && fix && !(fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code))) {
|
|
426
|
-
fixesApplied.push(`[dry-run] would fix: ${message}`);
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
|
|
430
372
|
const prefs = loadEffectiveGSDPreferences();
|
|
431
373
|
if (prefs) {
|
|
432
374
|
const prefIssues = validatePreferenceShape(prefs.preferences);
|
|
@@ -443,33 +385,21 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
443
385
|
}
|
|
444
386
|
}
|
|
445
387
|
|
|
446
|
-
// Git health checks
|
|
447
|
-
const t0git = Date.now();
|
|
388
|
+
// Git health checks (orphaned worktrees, stale branches, corrupt merge state, tracked runtime files)
|
|
448
389
|
const isolationMode: "none" | "worktree" | "branch" = options?.isolationMode ??
|
|
449
390
|
(prefs?.preferences?.git?.isolation === "none" ? "none" :
|
|
450
391
|
prefs?.preferences?.git?.isolation === "branch" ? "branch" : "worktree");
|
|
451
392
|
await checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode);
|
|
452
|
-
const gitMs = Date.now() - t0git;
|
|
453
393
|
|
|
454
|
-
// Runtime health checks
|
|
455
|
-
const t0runtime = Date.now();
|
|
394
|
+
// Runtime health checks (crash locks, completed-units, hook state, activity logs, STATE.md, gitignore)
|
|
456
395
|
await checkRuntimeHealth(basePath, issues, fixesApplied, shouldFix);
|
|
457
|
-
const runtimeMs = Date.now() - t0runtime;
|
|
458
396
|
|
|
459
|
-
// Environment health checks
|
|
460
|
-
|
|
461
|
-
await checkEnvironmentHealth(basePath, issues, {
|
|
462
|
-
includeRemote: !options?.scope,
|
|
463
|
-
includeBuild: options?.includeBuild,
|
|
464
|
-
includeTests: options?.includeTests,
|
|
465
|
-
});
|
|
466
|
-
const envMs = Date.now() - t0env;
|
|
397
|
+
// Environment health checks (#1221: missing tools, port conflicts, stale deps, disk space)
|
|
398
|
+
await checkEnvironmentHealth(basePath, issues, { includeRemote: !options?.scope });
|
|
467
399
|
|
|
468
400
|
const milestonesPath = milestonesDir(basePath);
|
|
469
401
|
if (!existsSync(milestonesPath)) {
|
|
470
|
-
|
|
471
|
-
await appendDoctorHistory(basePath, report);
|
|
472
|
-
return report;
|
|
402
|
+
return { ok: issues.every(issue => issue.severity !== "error"), basePath, issues, fixesApplied };
|
|
473
403
|
}
|
|
474
404
|
|
|
475
405
|
const requirementsPath = resolveGsdRootFile(basePath, "REQUIREMENTS");
|
|
@@ -535,43 +465,6 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
535
465
|
if (!roadmapContent) continue;
|
|
536
466
|
const roadmap = parseRoadmap(roadmapContent);
|
|
537
467
|
|
|
538
|
-
// ── Circular dependency detection ──────────────────────────────────────
|
|
539
|
-
for (const cycle of detectCircularDependencies(roadmap.slices)) {
|
|
540
|
-
issues.push({
|
|
541
|
-
severity: "error",
|
|
542
|
-
code: "circular_slice_dependency",
|
|
543
|
-
scope: "milestone",
|
|
544
|
-
unitId: milestoneId,
|
|
545
|
-
message: `Circular dependency detected: ${cycle.join(" → ")}`,
|
|
546
|
-
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
547
|
-
fixable: false,
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// ── Orphaned slice directories ─────────────────────────────────────────
|
|
552
|
-
try {
|
|
553
|
-
const slicesDir = join(milestonePath, "slices");
|
|
554
|
-
if (existsSync(slicesDir)) {
|
|
555
|
-
const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
|
|
556
|
-
for (const entry of readdirSync(slicesDir)) {
|
|
557
|
-
try {
|
|
558
|
-
if (!lstatSync(join(slicesDir, entry)).isDirectory()) continue;
|
|
559
|
-
} catch { continue; }
|
|
560
|
-
if (!knownSliceIds.has(entry)) {
|
|
561
|
-
issues.push({
|
|
562
|
-
severity: "warning",
|
|
563
|
-
code: "orphaned_slice_directory",
|
|
564
|
-
scope: "milestone",
|
|
565
|
-
unitId: milestoneId,
|
|
566
|
-
message: `Directory "${entry}" exists in ${milestoneId}/slices/ but is not referenced in the roadmap`,
|
|
567
|
-
file: `${relMilestonePath(basePath, milestoneId)}/slices/${entry}`,
|
|
568
|
-
fixable: false,
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
} catch { /* non-fatal */ }
|
|
574
|
-
|
|
575
468
|
for (const slice of roadmap.slices) {
|
|
576
469
|
const unitId = `${milestoneId}/${slice.id}`;
|
|
577
470
|
if (options?.scope && !matchesScope(unitId, options.scope) && options.scope !== milestoneId) continue;
|
|
@@ -646,33 +539,6 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
646
539
|
continue;
|
|
647
540
|
}
|
|
648
541
|
|
|
649
|
-
// ── Duplicate task IDs ───────────────────────────────────────────────
|
|
650
|
-
const taskIdCounts = new Map<string, number>();
|
|
651
|
-
for (const task of plan.tasks) taskIdCounts.set(task.id, (taskIdCounts.get(task.id) ?? 0) + 1);
|
|
652
|
-
for (const [taskId, count] of taskIdCounts) {
|
|
653
|
-
if (count > 1) {
|
|
654
|
-
issues.push({ severity: "error", code: "duplicate_task_id", scope: "slice", unitId,
|
|
655
|
-
message: `Task ID "${taskId}" appears ${count} times in ${slice.id}-PLAN.md — duplicate IDs cause dispatch failures`,
|
|
656
|
-
file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"), fixable: false });
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// ── Task files on disk not in plan ────────────────────────────────────
|
|
661
|
-
try {
|
|
662
|
-
if (tasksDir) {
|
|
663
|
-
const planTaskIds = new Set(plan.tasks.map(t => t.id));
|
|
664
|
-
for (const f of readdirSync(tasksDir)) {
|
|
665
|
-
if (!f.endsWith("-SUMMARY.md")) continue;
|
|
666
|
-
const diskTaskId = f.replace(/-SUMMARY\.md$/, "");
|
|
667
|
-
if (!planTaskIds.has(diskTaskId)) {
|
|
668
|
-
issues.push({ severity: "info", code: "task_file_not_in_plan", scope: "slice", unitId,
|
|
669
|
-
message: `Task summary "${f}" exists on disk but "${diskTaskId}" is not in ${slice.id}-PLAN.md`,
|
|
670
|
-
file: relTaskFile(basePath, milestoneId, slice.id, diskTaskId, "SUMMARY"), fixable: false });
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
} catch { /* non-fatal */ }
|
|
675
|
-
|
|
676
542
|
let allTasksDone = plan.tasks.length > 0;
|
|
677
543
|
for (const task of plan.tasks) {
|
|
678
544
|
const taskUnitId = `${unitId}/${task.id}`;
|
|
@@ -689,7 +555,6 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
689
555
|
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
|
|
690
556
|
fixable: true,
|
|
691
557
|
});
|
|
692
|
-
dryRunCanFix("task_done_missing_summary", `create stub summary for ${taskUnitId}`);
|
|
693
558
|
if (shouldFix("task_done_missing_summary")) {
|
|
694
559
|
const stubPath = join(
|
|
695
560
|
basePath, ".gsd", "milestones", milestoneId, "slices", slice.id, "tasks",
|
|
@@ -753,22 +618,6 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
753
618
|
}
|
|
754
619
|
}
|
|
755
620
|
|
|
756
|
-
// ── Future timestamp check ─────────────────────────────────────
|
|
757
|
-
if (task.done && hasSummary && summaryPath) {
|
|
758
|
-
try {
|
|
759
|
-
const rawSummary = await loadFile(summaryPath);
|
|
760
|
-
const m = rawSummary?.match(/^completed_at:\s*(.+)$/m);
|
|
761
|
-
if (m) {
|
|
762
|
-
const ts = new Date(m[1].trim());
|
|
763
|
-
if (!isNaN(ts.getTime()) && ts.getTime() > Date.now() + 24 * 60 * 60 * 1000) {
|
|
764
|
-
issues.push({ severity: "warning", code: "future_timestamp", scope: "task", unitId: taskUnitId,
|
|
765
|
-
message: `Task ${task.id} has completed_at "${m[1].trim()}" which is more than 24h in the future`,
|
|
766
|
-
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"), fixable: false });
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
} catch { /* non-fatal */ }
|
|
770
|
-
}
|
|
771
|
-
|
|
772
621
|
allTasksDone = allTasksDone && task.done;
|
|
773
622
|
}
|
|
774
623
|
|
|
@@ -797,13 +646,6 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
797
646
|
}
|
|
798
647
|
}
|
|
799
648
|
|
|
800
|
-
// ── Stale REPLAN: exists but all tasks done ────────────────────────
|
|
801
|
-
if (replanPath && allTasksDone) {
|
|
802
|
-
issues.push({ severity: "info", code: "stale_replan_file", scope: "slice", unitId,
|
|
803
|
-
message: `${slice.id} has a REPLAN.md but all tasks are done — REPLAN.md may be stale`,
|
|
804
|
-
file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"), fixable: false });
|
|
805
|
-
}
|
|
806
|
-
|
|
807
649
|
const sliceSummaryPath = resolveSliceFile(basePath, milestoneId, slice.id, "SUMMARY");
|
|
808
650
|
const sliceUatPath = join(slicePath, `${slice.id}-UAT.md`);
|
|
809
651
|
const hasSliceSummary = !!(sliceSummaryPath && await loadFile(sliceSummaryPath));
|
|
@@ -819,7 +661,6 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
819
661
|
file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
|
|
820
662
|
fixable: true,
|
|
821
663
|
});
|
|
822
|
-
dryRunCanFix("all_tasks_done_missing_slice_summary", `create placeholder summary for ${unitId}`);
|
|
823
664
|
if (shouldFix("all_tasks_done_missing_slice_summary")) await ensureSliceSummaryStub(basePath, milestoneId, slice.id, fixesApplied);
|
|
824
665
|
}
|
|
825
666
|
|
|
@@ -833,7 +674,6 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
833
674
|
file: `${relSlicePath(basePath, milestoneId, slice.id)}/${slice.id}-UAT.md`,
|
|
834
675
|
fixable: true,
|
|
835
676
|
});
|
|
836
|
-
dryRunCanFix("all_tasks_done_missing_slice_uat", `create placeholder UAT for ${unitId}`);
|
|
837
677
|
if (shouldFix("all_tasks_done_missing_slice_uat")) await ensureSliceUatStub(basePath, milestoneId, slice.id, fixesApplied);
|
|
838
678
|
}
|
|
839
679
|
|
|
@@ -847,7 +687,6 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
847
687
|
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
848
688
|
fixable: true,
|
|
849
689
|
});
|
|
850
|
-
dryRunCanFix("all_tasks_done_roadmap_not_checked", `mark ${slice.id} done in roadmap`);
|
|
851
690
|
if (shouldFix("all_tasks_done_roadmap_not_checked") && (hasSliceSummary || issues.some(issue => issue.code === "all_tasks_done_missing_slice_summary" && issue.unitId === unitId))) {
|
|
852
691
|
await markSliceDoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
|
|
853
692
|
}
|
|
@@ -905,17 +744,14 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
905
744
|
}
|
|
906
745
|
}
|
|
907
746
|
|
|
908
|
-
if (fix &&
|
|
747
|
+
if (fix && fixesApplied.length > 0) {
|
|
909
748
|
await updateStateFile(basePath, fixesApplied);
|
|
910
749
|
}
|
|
911
750
|
|
|
912
|
-
|
|
751
|
+
return {
|
|
913
752
|
ok: issues.every(issue => issue.severity !== "error"),
|
|
914
753
|
basePath,
|
|
915
754
|
issues,
|
|
916
755
|
fixesApplied,
|
|
917
|
-
timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: Math.max(0, Date.now() - t0env - envMs) },
|
|
918
756
|
};
|
|
919
|
-
await appendDoctorHistory(basePath, report);
|
|
920
|
-
return report;
|
|
921
757
|
}
|
|
@@ -100,99 +100,6 @@ test("buildCmuxStatusLabel and progress prefer deepest active unit", () => {
|
|
|
100
100
|
assert.deepEqual(buildCmuxProgress(state), { value: 0.4, label: "2/5 tasks" });
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
describe("createGridLayout", () => {
|
|
104
|
-
// Create a mock CmuxClient that tracks createSplitFrom calls
|
|
105
|
-
function makeMockClient() {
|
|
106
|
-
let nextId = 1;
|
|
107
|
-
const calls: Array<{ source: string | undefined; direction: string }> = [];
|
|
108
|
-
|
|
109
|
-
const client = {
|
|
110
|
-
calls,
|
|
111
|
-
async createGridLayout(count: number) {
|
|
112
|
-
// Simulate the grid layout logic with a fake client
|
|
113
|
-
if (count <= 0) return [];
|
|
114
|
-
const surfaces: string[] = [];
|
|
115
|
-
|
|
116
|
-
const createSplitFrom = async (source: string | undefined, direction: string) => {
|
|
117
|
-
calls.push({ source, direction });
|
|
118
|
-
return `surface-${nextId++}`;
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const rightCol = await createSplitFrom("gsd-surface", "right");
|
|
122
|
-
surfaces.push(rightCol);
|
|
123
|
-
if (count === 1) return surfaces;
|
|
124
|
-
|
|
125
|
-
const bottomRight = await createSplitFrom(rightCol, "down");
|
|
126
|
-
surfaces.push(bottomRight);
|
|
127
|
-
if (count === 2) return surfaces;
|
|
128
|
-
|
|
129
|
-
const bottomLeft = await createSplitFrom("gsd-surface", "down");
|
|
130
|
-
surfaces.push(bottomLeft);
|
|
131
|
-
if (count === 3) return surfaces;
|
|
132
|
-
|
|
133
|
-
let lastSurface = bottomRight;
|
|
134
|
-
for (let i = 3; i < count; i++) {
|
|
135
|
-
const next = await createSplitFrom(lastSurface, "down");
|
|
136
|
-
surfaces.push(next);
|
|
137
|
-
lastSurface = next;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return surfaces;
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
return client;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
test("1 agent creates single right split", async () => {
|
|
147
|
-
const mock = makeMockClient();
|
|
148
|
-
const surfaces = await mock.createGridLayout(1);
|
|
149
|
-
assert.equal(surfaces.length, 1);
|
|
150
|
-
assert.deepEqual(mock.calls, [
|
|
151
|
-
{ source: "gsd-surface", direction: "right" },
|
|
152
|
-
]);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("2 agents creates right column then splits it down", async () => {
|
|
156
|
-
const mock = makeMockClient();
|
|
157
|
-
const surfaces = await mock.createGridLayout(2);
|
|
158
|
-
assert.equal(surfaces.length, 2);
|
|
159
|
-
assert.deepEqual(mock.calls, [
|
|
160
|
-
{ source: "gsd-surface", direction: "right" },
|
|
161
|
-
{ source: "surface-1", direction: "down" },
|
|
162
|
-
]);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test("3 agents creates 2x2 grid (gsd + 3 agent surfaces)", async () => {
|
|
166
|
-
const mock = makeMockClient();
|
|
167
|
-
const surfaces = await mock.createGridLayout(3);
|
|
168
|
-
assert.equal(surfaces.length, 3);
|
|
169
|
-
assert.deepEqual(mock.calls, [
|
|
170
|
-
{ source: "gsd-surface", direction: "right" },
|
|
171
|
-
{ source: "surface-1", direction: "down" },
|
|
172
|
-
{ source: "gsd-surface", direction: "down" },
|
|
173
|
-
]);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("4 agents creates 2x2 grid with extra split", async () => {
|
|
177
|
-
const mock = makeMockClient();
|
|
178
|
-
const surfaces = await mock.createGridLayout(4);
|
|
179
|
-
assert.equal(surfaces.length, 4);
|
|
180
|
-
assert.deepEqual(mock.calls, [
|
|
181
|
-
{ source: "gsd-surface", direction: "right" },
|
|
182
|
-
{ source: "surface-1", direction: "down" },
|
|
183
|
-
{ source: "gsd-surface", direction: "down" },
|
|
184
|
-
{ source: "surface-2", direction: "down" },
|
|
185
|
-
]);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("0 agents returns empty", async () => {
|
|
189
|
-
const mock = makeMockClient();
|
|
190
|
-
const surfaces = await mock.createGridLayout(0);
|
|
191
|
-
assert.equal(surfaces.length, 0);
|
|
192
|
-
assert.equal(mock.calls.length, 0);
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
103
|
describe("cmux extension discovery opt-out", () => {
|
|
197
104
|
test("cmux directory has package.json with pi manifest to prevent auto-discovery as extension", () => {
|
|
198
105
|
const cmuxDir = path.resolve(
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
getMainBranch,
|
|
12
12
|
getSliceBranchName,
|
|
13
13
|
parseSliceBranch,
|
|
14
|
-
resolveProjectRoot,
|
|
15
14
|
setActiveMilestoneId,
|
|
16
15
|
SLICE_BRANCH_RE,
|
|
17
16
|
} from "../worktree.ts";
|
|
@@ -166,52 +165,6 @@ async function main(): Promise<void> {
|
|
|
166
165
|
rmSync(repo, { recursive: true, force: true });
|
|
167
166
|
}
|
|
168
167
|
|
|
169
|
-
// ── detectWorktreeName: symlink-resolved paths ───────────────────────────
|
|
170
|
-
console.log("\n=== detectWorktreeName (symlink-resolved paths) ===");
|
|
171
|
-
assertEq(
|
|
172
|
-
detectWorktreeName("/Users/fran/.gsd/projects/89e1c9ad49bf/worktrees/M001"),
|
|
173
|
-
"M001",
|
|
174
|
-
"detects milestone in symlink-resolved path",
|
|
175
|
-
);
|
|
176
|
-
assertEq(
|
|
177
|
-
detectWorktreeName("/Users/fran/.gsd/projects/abc123/worktrees/M002/subdir"),
|
|
178
|
-
"M002",
|
|
179
|
-
"detects milestone with trailing subdir in symlink-resolved path",
|
|
180
|
-
);
|
|
181
|
-
assertEq(
|
|
182
|
-
detectWorktreeName("/Users/fran/.gsd/projects/abc123"),
|
|
183
|
-
null,
|
|
184
|
-
"returns null for project root without worktrees segment",
|
|
185
|
-
);
|
|
186
|
-
assertEq(
|
|
187
|
-
detectWorktreeName("/foo/.gsd/worktrees/M001"),
|
|
188
|
-
"M001",
|
|
189
|
-
"still detects direct layout path",
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
// ── resolveProjectRoot: symlink-resolved paths ──────────────────────────
|
|
193
|
-
console.log("\n=== resolveProjectRoot (symlink-resolved paths) ===");
|
|
194
|
-
assertEq(
|
|
195
|
-
resolveProjectRoot("/Users/fran/.gsd/projects/89e1c9ad49bf/worktrees/M001"),
|
|
196
|
-
"/Users/fran",
|
|
197
|
-
"resolves to user home for symlink-resolved path",
|
|
198
|
-
);
|
|
199
|
-
assertEq(
|
|
200
|
-
resolveProjectRoot("/foo/.gsd/worktrees/M001"),
|
|
201
|
-
"/foo",
|
|
202
|
-
"still resolves direct layout path",
|
|
203
|
-
);
|
|
204
|
-
assertEq(
|
|
205
|
-
resolveProjectRoot("/some/repo"),
|
|
206
|
-
"/some/repo",
|
|
207
|
-
"returns unchanged for non-worktree path",
|
|
208
|
-
);
|
|
209
|
-
assertEq(
|
|
210
|
-
resolveProjectRoot("/data/.gsd/projects/deadbeef/worktrees/M003/nested"),
|
|
211
|
-
"/data",
|
|
212
|
-
"resolves correctly with nested subdirs after worktree name",
|
|
213
|
-
);
|
|
214
|
-
|
|
215
168
|
rmSync(base, { recursive: true, force: true });
|
|
216
169
|
report();
|
|
217
170
|
}
|
|
@@ -67,60 +67,40 @@ export function captureIntegrationBranch(basePath: string, milestoneId: string,
|
|
|
67
67
|
|
|
68
68
|
// ─── Pure Utility Functions (unchanged) ────────────────────────────────────
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
* Find the worktrees segment in a path, supporting both direct
|
|
72
|
-
* (`/.gsd/worktrees/`) and symlink-resolved (`/.gsd/projects/<hash>/worktrees/`)
|
|
73
|
-
* layouts. When `.gsd` is a symlink to `~/.gsd/projects/<hash>`, resolved
|
|
74
|
-
* paths contain the intermediate `projects/<hash>/` segment that the old
|
|
75
|
-
* single-marker check missed.
|
|
76
|
-
*/
|
|
77
|
-
function findWorktreeSegment(normalizedPath: string): { gsdIdx: number; afterWorktrees: number } | null {
|
|
78
|
-
// Direct layout: /.gsd/worktrees/<name>
|
|
79
|
-
const directMarker = "/.gsd/worktrees/";
|
|
80
|
-
const idx = normalizedPath.indexOf(directMarker);
|
|
81
|
-
if (idx !== -1) {
|
|
82
|
-
return { gsdIdx: idx, afterWorktrees: idx + directMarker.length };
|
|
83
|
-
}
|
|
84
|
-
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/<name>
|
|
85
|
-
const symlinkRe = /\/\.gsd\/projects\/[a-f0-9]+\/worktrees\//;
|
|
86
|
-
const match = normalizedPath.match(symlinkRe);
|
|
87
|
-
if (match && match.index !== undefined) {
|
|
88
|
-
return { gsdIdx: match.index, afterWorktrees: match.index + match[0].length };
|
|
89
|
-
}
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
70
|
/**
|
|
94
71
|
* Detect the active worktree name from the current working directory.
|
|
95
72
|
* Returns null if not inside a GSD worktree (.gsd/worktrees/<name>/).
|
|
96
73
|
*/
|
|
97
74
|
export function detectWorktreeName(basePath: string): string | null {
|
|
98
75
|
const normalizedPath = basePath.replaceAll("\\", "/");
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
76
|
+
const marker = "/.gsd/worktrees/";
|
|
77
|
+
const idx = normalizedPath.indexOf(marker);
|
|
78
|
+
if (idx === -1) return null;
|
|
79
|
+
const afterMarker = normalizedPath.slice(idx + marker.length);
|
|
102
80
|
const name = afterMarker.split("/")[0];
|
|
103
81
|
return name || null;
|
|
104
82
|
}
|
|
105
83
|
|
|
106
84
|
/**
|
|
107
85
|
* Resolve the project root from a path that may be inside a worktree.
|
|
108
|
-
* If the path contains
|
|
109
|
-
* `/.gsd/`. Otherwise returns the input unchanged.
|
|
86
|
+
* If the path contains `/.gsd/worktrees/<name>/`, returns the portion
|
|
87
|
+
* before `/.gsd/`. Otherwise returns the input unchanged.
|
|
110
88
|
*
|
|
111
89
|
* Use this in commands that call `process.cwd()` to ensure they always
|
|
112
90
|
* operate against the real project root, not a worktree subdirectory.
|
|
113
91
|
*/
|
|
114
92
|
export function resolveProjectRoot(basePath: string): string {
|
|
115
93
|
const normalizedPath = basePath.replaceAll("\\", "/");
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
94
|
+
const marker = "/.gsd/worktrees/";
|
|
95
|
+
const idx = normalizedPath.indexOf(marker);
|
|
96
|
+
if (idx === -1) return basePath;
|
|
97
|
+
// Return the original path up to the .gsd/ marker (un-normalized)
|
|
98
|
+
// Account for potential OS-specific separators
|
|
119
99
|
const sep = basePath.includes("\\") ? "\\" : "/";
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
return basePath.slice(0,
|
|
100
|
+
const markerOs = `${sep}.gsd${sep}worktrees${sep}`;
|
|
101
|
+
const idxOs = basePath.indexOf(markerOs);
|
|
102
|
+
if (idxOs !== -1) return basePath.slice(0, idxOs);
|
|
103
|
+
return basePath.slice(0, idx);
|
|
124
104
|
}
|
|
125
105
|
|
|
126
106
|
/**
|
|
@@ -452,7 +452,7 @@ async function runSingleAgent(
|
|
|
452
452
|
|
|
453
453
|
async function runSingleAgentInCmuxSplit(
|
|
454
454
|
cmuxClient: CmuxClient,
|
|
455
|
-
|
|
455
|
+
direction: "right" | "down",
|
|
456
456
|
defaultCwd: string,
|
|
457
457
|
agents: AgentConfig[],
|
|
458
458
|
agentName: string,
|
|
@@ -503,12 +503,7 @@ async function runSingleAgentInCmuxSplit(
|
|
|
503
503
|
const stdoutPath = path.join(tmpOutputDir, "stdout.jsonl");
|
|
504
504
|
const stderrPath = path.join(tmpOutputDir, "stderr.log");
|
|
505
505
|
const exitPath = path.join(tmpOutputDir, "exit.code");
|
|
506
|
-
|
|
507
|
-
const isDirection = directionOrSurfaceId === "right" || directionOrSurfaceId === "down"
|
|
508
|
-
|| directionOrSurfaceId === "left" || directionOrSurfaceId === "up";
|
|
509
|
-
const cmuxSurfaceId = isDirection
|
|
510
|
-
? await cmuxClient.createSplit(directionOrSurfaceId as "right" | "down" | "left" | "up")
|
|
511
|
-
: directionOrSurfaceId;
|
|
506
|
+
const cmuxSurfaceId = await cmuxClient.createSplit(direction);
|
|
512
507
|
if (!cmuxSurfaceId) {
|
|
513
508
|
return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
|
|
514
509
|
}
|
|
@@ -811,16 +806,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
811
806
|
const MAX_RETRIES = 1; // Retry failed tasks once
|
|
812
807
|
const batchId = crypto.randomUUID();
|
|
813
808
|
const batchSize = params.tasks.length;
|
|
814
|
-
// Pre-create a grid layout for cmux splits so agents get a clean tiled arrangement
|
|
815
|
-
const gridSurfaces = cmuxSplitsEnabled
|
|
816
|
-
? await cmuxClient.createGridLayout(Math.min(batchSize, MAX_CONCURRENCY))
|
|
817
|
-
: [];
|
|
818
809
|
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
819
810
|
const workerId = registerWorker(t.agent, t.task, index, batchSize, batchId);
|
|
820
811
|
const runTask = () => cmuxSplitsEnabled
|
|
821
812
|
? runSingleAgentInCmuxSplit(
|
|
822
813
|
cmuxClient,
|
|
823
|
-
|
|
814
|
+
index % 2 === 0 ? "right" : "down",
|
|
824
815
|
ctx.cwd,
|
|
825
816
|
agents,
|
|
826
817
|
t.agent,
|
package/dist/welcome-screen.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GSD Welcome Screen
|
|
3
|
-
*
|
|
4
|
-
* Rendered to stderr before the TUI takes over.
|
|
5
|
-
* No box, no panels — logo with metadata alongside, dim hint below.
|
|
6
|
-
*/
|
|
7
|
-
export interface WelcomeScreenOptions {
|
|
8
|
-
version: string;
|
|
9
|
-
modelName?: string;
|
|
10
|
-
provider?: string;
|
|
11
|
-
}
|
|
12
|
-
export declare function printWelcomeScreen(opts: WelcomeScreenOptions): void;
|
package/dist/welcome-screen.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GSD Welcome Screen
|
|
3
|
-
*
|
|
4
|
-
* Rendered to stderr before the TUI takes over.
|
|
5
|
-
* No box, no panels — logo with metadata alongside, dim hint below.
|
|
6
|
-
*/
|
|
7
|
-
import os from 'node:os';
|
|
8
|
-
import chalk from 'chalk';
|
|
9
|
-
import { GSD_LOGO } from './logo.js';
|
|
10
|
-
function getShortCwd() {
|
|
11
|
-
const cwd = process.cwd();
|
|
12
|
-
const home = os.homedir();
|
|
13
|
-
return cwd.startsWith(home) ? '~' + cwd.slice(home.length) : cwd;
|
|
14
|
-
}
|
|
15
|
-
export function printWelcomeScreen(opts) {
|
|
16
|
-
if (!process.stderr.isTTY)
|
|
17
|
-
return;
|
|
18
|
-
const { version, modelName, provider } = opts;
|
|
19
|
-
const shortCwd = getShortCwd();
|
|
20
|
-
// Info lines to sit alongside the logo (one per logo row)
|
|
21
|
-
const modelLine = [modelName, provider].filter(Boolean).join(' · ');
|
|
22
|
-
const INFO = [
|
|
23
|
-
` ${chalk.bold('Get Shit Done')} ${chalk.dim('v' + version)}`,
|
|
24
|
-
undefined,
|
|
25
|
-
modelLine ? ` ${chalk.dim(modelLine)}` : undefined,
|
|
26
|
-
` ${chalk.dim(shortCwd)}`,
|
|
27
|
-
undefined,
|
|
28
|
-
undefined,
|
|
29
|
-
];
|
|
30
|
-
const lines = [''];
|
|
31
|
-
for (let i = 0; i < GSD_LOGO.length; i++) {
|
|
32
|
-
lines.push(chalk.cyan(GSD_LOGO[i]) + (INFO[i] ?? ''));
|
|
33
|
-
}
|
|
34
|
-
// Tool status + hint — dim, aligned under the info text
|
|
35
|
-
const pad = ' '.repeat(28) + ' '; // aligns with the info text column
|
|
36
|
-
const toolParts = [];
|
|
37
|
-
if (process.env.BRAVE_API_KEY)
|
|
38
|
-
toolParts.push('Brave ✓');
|
|
39
|
-
if (process.env.BRAVE_ANSWERS_KEY)
|
|
40
|
-
toolParts.push('Answers ✓');
|
|
41
|
-
if (process.env.JINA_API_KEY)
|
|
42
|
-
toolParts.push('Jina ✓');
|
|
43
|
-
if (process.env.TAVILY_API_KEY)
|
|
44
|
-
toolParts.push('Tavily ✓');
|
|
45
|
-
if (process.env.CONTEXT7_API_KEY)
|
|
46
|
-
toolParts.push('Context7 ✓');
|
|
47
|
-
if (toolParts.length > 0) {
|
|
48
|
-
lines.push(chalk.dim(pad + ['Web search loaded', ...toolParts].join(' · ')));
|
|
49
|
-
}
|
|
50
|
-
lines.push(chalk.dim(pad + '/gsd to begin · /gsd help for all commands'));
|
|
51
|
-
lines.push('');
|
|
52
|
-
process.stderr.write(lines.join('\n') + '\n');
|
|
53
|
-
}
|