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.
Files changed (45) hide show
  1. package/dist/cli.js +0 -9
  2. package/dist/extension-discovery.d.ts +3 -5
  3. package/dist/extension-discovery.js +9 -14
  4. package/dist/resources/extensions/browser-tools/package.json +1 -3
  5. package/dist/resources/extensions/cmux/index.js +1 -55
  6. package/dist/resources/extensions/context7/package.json +1 -1
  7. package/dist/resources/extensions/google-search/package.json +1 -3
  8. package/dist/resources/extensions/gsd/auto-loop.js +1 -7
  9. package/dist/resources/extensions/gsd/auto-start.js +1 -6
  10. package/dist/resources/extensions/gsd/auto-worktree-sync.js +4 -11
  11. package/dist/resources/extensions/gsd/captures.js +1 -9
  12. package/dist/resources/extensions/gsd/commands-handlers.js +3 -16
  13. package/dist/resources/extensions/gsd/commands.js +1 -20
  14. package/dist/resources/extensions/gsd/doctor-checks.js +0 -82
  15. package/dist/resources/extensions/gsd/doctor-environment.js +0 -78
  16. package/dist/resources/extensions/gsd/doctor-format.js +0 -15
  17. package/dist/resources/extensions/gsd/doctor.js +11 -184
  18. package/dist/resources/extensions/gsd/package.json +1 -1
  19. package/dist/resources/extensions/gsd/worktree.js +16 -35
  20. package/dist/resources/extensions/subagent/index.js +3 -12
  21. package/dist/resources/extensions/universal-config/package.json +1 -1
  22. package/package.json +1 -1
  23. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  24. package/packages/pi-coding-agent/dist/core/package-manager.js +4 -8
  25. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  26. package/packages/pi-coding-agent/src/core/package-manager.ts +4 -8
  27. package/src/resources/extensions/cmux/index.ts +1 -57
  28. package/src/resources/extensions/gsd/auto-loop.ts +1 -13
  29. package/src/resources/extensions/gsd/auto-start.ts +1 -7
  30. package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -12
  31. package/src/resources/extensions/gsd/captures.ts +1 -10
  32. package/src/resources/extensions/gsd/commands-handlers.ts +2 -17
  33. package/src/resources/extensions/gsd/commands.ts +1 -21
  34. package/src/resources/extensions/gsd/doctor-checks.ts +0 -75
  35. package/src/resources/extensions/gsd/doctor-environment.ts +1 -82
  36. package/src/resources/extensions/gsd/doctor-format.ts +0 -20
  37. package/src/resources/extensions/gsd/doctor-types.ts +1 -16
  38. package/src/resources/extensions/gsd/doctor.ts +13 -177
  39. package/src/resources/extensions/gsd/tests/cmux.test.ts +0 -93
  40. package/src/resources/extensions/gsd/tests/worktree.test.ts +0 -47
  41. package/src/resources/extensions/gsd/worktree.ts +15 -35
  42. package/src/resources/extensions/subagent/index.ts +3 -12
  43. package/dist/welcome-screen.d.ts +0 -12
  44. package/dist/welcome-screen.js +0 -53
  45. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +0 -266
@@ -1,15 +1,14 @@
1
- import { existsSync, mkdirSync, lstatSync, readdirSync, readFileSync } from "node:fs";
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, relMilestonePath } from "./paths.js";
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, DoctorReport } from "./doctor-types.js";
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, formatDoctorReportJson } from "./doctor-format.js";
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
- // ── Helper: circular dependency detection ──────────────────────────────────
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 || dryRun) return false;
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 timed
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 timed
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 timed
460
- const t0env = Date.now();
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
- const report: DoctorReport = { ok: issues.every(i => i.severity !== "error"), basePath, issues, fixesApplied, timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: 0 } };
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 && !dryRun && fixesApplied.length > 0) {
747
+ if (fix && fixesApplied.length > 0) {
909
748
  await updateStateFile(basePath, fixesApplied);
910
749
  }
911
750
 
912
- const report: DoctorReport = {
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 seg = findWorktreeSegment(normalizedPath);
100
- if (!seg) return null;
101
- const afterMarker = normalizedPath.slice(seg.afterWorktrees);
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 a worktrees segment, returns the portion before
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 seg = findWorktreeSegment(normalizedPath);
117
- if (!seg) return basePath;
118
- // Return the original path up to the /.gsd/ boundary
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 gsdMarker = `${sep}.gsd${sep}`;
121
- const gsdIdx = basePath.indexOf(gsdMarker);
122
- if (gsdIdx !== -1) return basePath.slice(0, gsdIdx);
123
- return basePath.slice(0, seg.gsdIdx);
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
- directionOrSurfaceId: "right" | "down" | string,
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
- // Accept either a pre-created surface ID or a direction to create a new split
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
- gridSurfaces[index] ?? (index % 2 === 0 ? "right" : "down"),
814
+ index % 2 === 0 ? "right" : "down",
824
815
  ctx.cwd,
825
816
  agents,
826
817
  t.agent,
@@ -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;
@@ -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
- }