gsd-pi 2.36.0 → 2.37.0-dev.68605cd

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 (71) hide show
  1. package/dist/resources/extensions/cmux/index.js +321 -0
  2. package/dist/resources/extensions/cmux/package.json +7 -0
  3. package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
  4. package/dist/resources/extensions/gsd/auto-loop.js +29 -4
  5. package/dist/resources/extensions/gsd/auto.js +58 -5
  6. package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
  7. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  8. package/dist/resources/extensions/gsd/commands.js +131 -34
  9. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  10. package/dist/resources/extensions/gsd/git-service.js +9 -1
  11. package/dist/resources/extensions/gsd/history.js +2 -1
  12. package/dist/resources/extensions/gsd/index.js +5 -0
  13. package/dist/resources/extensions/gsd/metrics.js +4 -2
  14. package/dist/resources/extensions/gsd/notifications.js +10 -1
  15. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  16. package/dist/resources/extensions/gsd/preferences-validation.js +29 -0
  17. package/dist/resources/extensions/gsd/preferences.js +3 -0
  18. package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  19. package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
  20. package/dist/resources/extensions/gsd/session-lock.js +26 -6
  21. package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
  22. package/dist/resources/extensions/search-the-web/native-search.js +45 -4
  23. package/dist/resources/extensions/shared/format-utils.js +5 -41
  24. package/dist/resources/extensions/shared/layout-utils.js +46 -0
  25. package/dist/resources/extensions/shared/mod.js +2 -1
  26. package/dist/resources/extensions/shared/terminal.js +5 -0
  27. package/dist/resources/extensions/subagent/index.js +180 -60
  28. package/package.json +1 -1
  29. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  30. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -4
  31. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  32. package/packages/pi-coding-agent/package.json +1 -1
  33. package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
  34. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  35. package/packages/pi-tui/dist/terminal-image.js +4 -0
  36. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  37. package/packages/pi-tui/src/terminal-image.ts +5 -0
  38. package/pkg/package.json +1 -1
  39. package/src/resources/extensions/cmux/index.ts +384 -0
  40. package/src/resources/extensions/cmux/package.json +7 -0
  41. package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
  42. package/src/resources/extensions/gsd/auto-loop.ts +66 -6
  43. package/src/resources/extensions/gsd/auto.ts +77 -5
  44. package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
  45. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  46. package/src/resources/extensions/gsd/commands.ts +139 -32
  47. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  48. package/src/resources/extensions/gsd/git-service.ts +12 -1
  49. package/src/resources/extensions/gsd/history.ts +2 -1
  50. package/src/resources/extensions/gsd/index.ts +8 -0
  51. package/src/resources/extensions/gsd/metrics.ts +4 -2
  52. package/src/resources/extensions/gsd/notifications.ts +10 -1
  53. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  54. package/src/resources/extensions/gsd/preferences-validation.ts +26 -0
  55. package/src/resources/extensions/gsd/preferences.ts +4 -0
  56. package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  57. package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
  58. package/src/resources/extensions/gsd/session-lock.ts +41 -6
  59. package/src/resources/extensions/gsd/templates/preferences.md +6 -0
  60. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +39 -1
  61. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
  62. package/src/resources/extensions/gsd/tests/cmux.test.ts +122 -0
  63. package/src/resources/extensions/gsd/tests/preferences.test.ts +23 -0
  64. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
  65. package/src/resources/extensions/search-the-web/native-search.ts +50 -4
  66. package/src/resources/extensions/shared/format-utils.ts +5 -44
  67. package/src/resources/extensions/shared/layout-utils.ts +49 -0
  68. package/src/resources/extensions/shared/mod.ts +7 -4
  69. package/src/resources/extensions/shared/terminal.ts +5 -0
  70. package/src/resources/extensions/shared/tests/format-utils.test.ts +5 -3
  71. package/src/resources/extensions/subagent/index.ts +236 -79
@@ -218,10 +218,24 @@ export async function autoLoop(ctx, pi, s, deps) {
218
218
  }
219
219
  try {
220
220
  // ── Blanket try/catch: one bad iteration must not kill the session
221
- if (deps.lockBase() && !deps.validateSessionLock(deps.lockBase())) {
222
- deps.handleLostSessionLock(ctx);
223
- debugLog("autoLoop", { phase: "exit", reason: "session-lock-lost" });
224
- break;
221
+ const sessionLockBase = deps.lockBase();
222
+ if (sessionLockBase) {
223
+ const lockStatus = deps.validateSessionLock(sessionLockBase);
224
+ if (!lockStatus.valid) {
225
+ debugLog("autoLoop", {
226
+ phase: "session-lock-invalid",
227
+ reason: lockStatus.failureReason ?? "unknown",
228
+ existingPid: lockStatus.existingPid,
229
+ expectedPid: lockStatus.expectedPid,
230
+ });
231
+ deps.handleLostSessionLock(ctx, lockStatus);
232
+ debugLog("autoLoop", {
233
+ phase: "exit",
234
+ reason: "session-lock-lost",
235
+ detail: lockStatus.failureReason ?? "unknown",
236
+ });
237
+ break;
238
+ }
225
239
  }
226
240
  // ── Phase 1: Pre-dispatch ───────────────────────────────────────────
227
241
  // Resource version guard
@@ -258,6 +272,7 @@ export async function autoLoop(ctx, pi, s, deps) {
258
272
  }
259
273
  // Derive state
260
274
  let state = await deps.deriveState(s.basePath);
275
+ deps.syncCmuxSidebar(deps.loadEffectiveGSDPreferences()?.preferences, state);
261
276
  let mid = state.activeMilestone?.id;
262
277
  let midTitle = state.activeMilestone?.title;
263
278
  debugLog("autoLoop", {
@@ -270,6 +285,7 @@ export async function autoLoop(ctx, pi, s, deps) {
270
285
  if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
271
286
  ctx.ui.notify(`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`, "info");
272
287
  deps.sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone");
288
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`, "success");
273
289
  const vizPrefs = deps.loadEffectiveGSDPreferences()?.preferences;
274
290
  if (vizPrefs?.auto_visualize) {
275
291
  ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
@@ -363,6 +379,7 @@ export async function autoLoop(ctx, pi, s, deps) {
363
379
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
364
380
  }
365
381
  deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
382
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, "All milestones complete.", "success");
366
383
  await deps.stopAuto(ctx, pi, "All milestones complete");
367
384
  }
368
385
  else if (state.phase === "blocked") {
@@ -370,6 +387,7 @@ export async function autoLoop(ctx, pi, s, deps) {
370
387
  await deps.stopAuto(ctx, pi, blockerMsg);
371
388
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
372
389
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
390
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
373
391
  }
374
392
  else {
375
393
  const ids = incomplete.map((m) => m.id).join(", ");
@@ -415,6 +433,7 @@ export async function autoLoop(ctx, pi, s, deps) {
415
433
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
416
434
  }
417
435
  deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
436
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${mid} complete.`, "success");
418
437
  await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
419
438
  debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
420
439
  break;
@@ -428,6 +447,7 @@ export async function autoLoop(ctx, pi, s, deps) {
428
447
  await deps.stopAuto(ctx, pi, blockerMsg);
429
448
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
430
449
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
450
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
431
451
  debugLog("autoLoop", { phase: "exit", reason: "blocked" });
432
452
  break;
433
453
  }
@@ -458,30 +478,35 @@ export async function autoLoop(ctx, pi, s, deps) {
458
478
  if (budgetEnforcementAction === "pause") {
459
479
  ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
460
480
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
481
+ deps.logCmuxEvent(prefs, msg, "warning");
461
482
  await deps.pauseAuto(ctx, pi);
462
483
  debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
463
484
  break;
464
485
  }
465
486
  ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
466
487
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
488
+ deps.logCmuxEvent(prefs, msg, "warning");
467
489
  }
468
490
  else if (newBudgetAlertLevel === 90) {
469
491
  s.lastBudgetAlertLevel =
470
492
  newBudgetAlertLevel;
471
493
  ctx.ui.notify(`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
472
494
  deps.sendDesktopNotification("GSD", `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning", "budget");
495
+ deps.logCmuxEvent(prefs, `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
473
496
  }
474
497
  else if (newBudgetAlertLevel === 80) {
475
498
  s.lastBudgetAlertLevel =
476
499
  newBudgetAlertLevel;
477
500
  ctx.ui.notify(`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
478
501
  deps.sendDesktopNotification("GSD", `Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning", "budget");
502
+ deps.logCmuxEvent(prefs, `Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
479
503
  }
480
504
  else if (newBudgetAlertLevel === 75) {
481
505
  s.lastBudgetAlertLevel =
482
506
  newBudgetAlertLevel;
483
507
  ctx.ui.notify(`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info");
484
508
  deps.sendDesktopNotification("GSD", `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info", "budget");
509
+ deps.logCmuxEvent(prefs, `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "progress");
485
510
  }
486
511
  else if (budgetAlertLevel === 0) {
487
512
  s.lastBudgetAlertLevel = 0;
@@ -18,7 +18,7 @@ import { invalidateAllCaches } from "./cache.js";
18
18
  import { clearActivityLogState } from "./activity-log.js";
19
19
  import { synthesizeCrashRecovery, getDeepDiagnostic, } from "./session-forensics.js";
20
20
  import { writeLock, clearLock, readCrashLock, isLockProcessAlive, } from "./crash-recovery.js";
21
- import { acquireSessionLock, validateSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
21
+ import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
22
22
  import { clearUnitRuntimeRecord, readUnitRuntimeRecord, writeUnitRuntimeRecord, } from "./unit-runtime.js";
23
23
  import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, getIsolationMode, } from "./preferences.js";
24
24
  import { sendDesktopNotification } from "./notifications.js";
@@ -50,6 +50,7 @@ import { updateProgressWidget as _updateProgressWidget, updateSliceProgressCache
50
50
  import { registerSigtermHandler as _registerSigtermHandler, deregisterSigtermHandler as _deregisterSigtermHandler, } from "./auto-supervisor.js";
51
51
  import { isDbAvailable } from "./gsd-db.js";
52
52
  import { countPendingCaptures } from "./captures.js";
53
+ import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
53
54
  // ── Extracted modules ──────────────────────────────────────────────────────
54
55
  import { startUnitSupervision } from "./auto-timers.js";
55
56
  import { runPostUnitVerification } from "./auto-verification.js";
@@ -208,6 +209,29 @@ export function stopAutoRemote(projectRoot) {
208
209
  return { found: false, error: err.message };
209
210
  }
210
211
  }
212
+ /**
213
+ * Check if a remote auto-mode session is running (from a different process).
214
+ * Reads the crash lock, checks PID liveness, and returns session details.
215
+ * Used by the guard in commands.ts to prevent bare /gsd, /gsd next, and
216
+ * /gsd auto from stealing the session lock.
217
+ */
218
+ export function checkRemoteAutoSession(projectRoot) {
219
+ const lock = readCrashLock(projectRoot);
220
+ if (!lock)
221
+ return { running: false };
222
+ if (!isLockProcessAlive(lock)) {
223
+ // Stale lock from a dead process — not a live remote session
224
+ return { running: false };
225
+ }
226
+ return {
227
+ running: true,
228
+ pid: lock.pid,
229
+ unitType: lock.unitType,
230
+ unitId: lock.unitId,
231
+ startedAt: lock.startedAt,
232
+ completedUnits: lock.completedUnits,
233
+ };
234
+ }
211
235
  export function isStepMode() {
212
236
  return s.stepMode;
213
237
  }
@@ -242,13 +266,28 @@ function buildSnapshotOpts(unitType, unitId) {
242
266
  ...(runtime?.continueHereFired ? { continueHereFired: true } : {}),
243
267
  };
244
268
  }
245
- function handleLostSessionLock(ctx) {
246
- debugLog("session-lock-lost", { lockBase: lockBase() });
269
+ function handleLostSessionLock(ctx, lockStatus) {
270
+ debugLog("session-lock-lost", {
271
+ lockBase: lockBase(),
272
+ reason: lockStatus?.failureReason,
273
+ existingPid: lockStatus?.existingPid,
274
+ expectedPid: lockStatus?.expectedPid,
275
+ });
247
276
  s.active = false;
248
277
  s.paused = false;
249
278
  clearUnitTimeout();
250
279
  deregisterSigtermHandler();
251
- ctx?.ui.notify("Session lock lost — another GSD process appears to have taken over. Stopping gracefully.", "error");
280
+ clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
281
+ const message = lockStatus?.failureReason === "pid-mismatch"
282
+ ? lockStatus.existingPid
283
+ ? `Session lock moved to PID ${lockStatus.existingPid} — another GSD process appears to have taken over. Stopping gracefully.`
284
+ : "Session lock moved to a different process — another GSD process appears to have taken over. Stopping gracefully."
285
+ : lockStatus?.failureReason === "missing-metadata"
286
+ ? "Session lock metadata disappeared, so ownership could not be confirmed. Stopping gracefully."
287
+ : lockStatus?.failureReason === "compromised"
288
+ ? "Session lock was compromised or invalidated during heartbeat checks; takeover was not confirmed. Stopping gracefully."
289
+ : "Session lock lost. Stopping gracefully.";
290
+ ctx?.ui.notify(message, "error");
252
291
  ctx?.ui.setStatus("gsd-auto", undefined);
253
292
  ctx?.ui.setWidget("gsd-progress", undefined);
254
293
  ctx?.ui.setFooter(undefined);
@@ -256,6 +295,7 @@ function handleLostSessionLock(ctx) {
256
295
  export async function stopAuto(ctx, pi, reason) {
257
296
  if (!s.active && !s.paused)
258
297
  return;
298
+ const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
259
299
  const reasonSuffix = reason ? ` — ${reason}` : "";
260
300
  clearUnitTimeout();
261
301
  if (lockBase())
@@ -314,6 +354,8 @@ export async function stopAuto(ctx, pi, reason) {
314
354
  });
315
355
  }
316
356
  }
357
+ clearCmuxSidebar(loadedPreferences);
358
+ logCmuxEvent(loadedPreferences, `Auto-mode stopped${reasonSuffix || ""}.`, reason?.startsWith("Blocked:") ? "warning" : "info");
317
359
  if (isDebugEnabled()) {
318
360
  const logPath = writeDebugSummary();
319
361
  if (logPath) {
@@ -455,6 +497,8 @@ function buildLoopDeps() {
455
497
  pauseAuto,
456
498
  clearUnitTimeout,
457
499
  updateProgressWidget,
500
+ syncCmuxSidebar,
501
+ logCmuxEvent,
458
502
  // State and cache
459
503
  invalidateAllCaches,
460
504
  deriveState,
@@ -466,7 +510,7 @@ function buildLoopDeps() {
466
510
  // Resource version guard
467
511
  checkResourcesStale,
468
512
  // Session lock
469
- validateSessionLock,
513
+ validateSessionLock: getSessionLockStatus,
470
514
  updateSessionLock,
471
515
  handleLostSessionLock,
472
516
  // Milestone transition
@@ -605,6 +649,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
605
649
  restoreHookState(s.basePath);
606
650
  try {
607
651
  await rebuildState(s.basePath);
652
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
608
653
  }
609
654
  catch (e) {
610
655
  debugLog("resume-rebuild-state-failed", {
@@ -634,6 +679,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
634
679
  }
635
680
  updateSessionLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
636
681
  writeLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
682
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
637
683
  await autoLoop(ctx, pi, s, buildLoopDeps());
638
684
  return;
639
685
  }
@@ -647,6 +693,13 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
647
693
  const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps);
648
694
  if (!ready)
649
695
  return;
696
+ try {
697
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
698
+ }
699
+ catch {
700
+ // Best-effort only — sidebar sync must never block auto-mode startup
701
+ }
702
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
650
703
  // Dispatch the first unit
651
704
  await autoLoop(ctx, pi, s, buildLoopDeps());
652
705
  }
@@ -0,0 +1,120 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { clearCmuxSidebar, CmuxClient, detectCmuxEnvironment, resolveCmuxConfig } from "../cmux/index.js";
3
+ import { saveFile } from "./files.js";
4
+ import { getProjectGSDPreferencesPath, loadEffectiveGSDPreferences, loadProjectGSDPreferences, } from "./preferences.js";
5
+ import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
6
+ function extractBodyAfterFrontmatter(content) {
7
+ const start = content.startsWith("---\n") ? 4 : content.startsWith("---\r\n") ? 5 : -1;
8
+ if (start === -1)
9
+ return null;
10
+ const closingIdx = content.indexOf("\n---", start);
11
+ if (closingIdx === -1)
12
+ return null;
13
+ const after = content.slice(closingIdx + 4);
14
+ return after.trim() ? after : null;
15
+ }
16
+ async function writeProjectCmuxPreferences(ctx, updater) {
17
+ const path = getProjectGSDPreferencesPath();
18
+ await ensurePreferencesFile(path, ctx, "project");
19
+ const existing = loadProjectGSDPreferences();
20
+ const prefs = existing?.preferences ? { ...existing.preferences } : { version: 1 };
21
+ updater(prefs);
22
+ prefs.version = prefs.version || 1;
23
+ const frontmatter = serializePreferencesToFrontmatter(prefs);
24
+ let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
25
+ if (existsSync(path)) {
26
+ const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
27
+ if (preserved)
28
+ body = preserved;
29
+ }
30
+ await saveFile(path, `---\n${frontmatter}---${body}`);
31
+ await ctx.waitForIdle();
32
+ await ctx.reload();
33
+ }
34
+ function formatCmuxStatus() {
35
+ const loaded = loadEffectiveGSDPreferences();
36
+ const detected = detectCmuxEnvironment();
37
+ const resolved = resolveCmuxConfig(loaded?.preferences);
38
+ const capabilities = new CmuxClient(resolved).getCapabilities();
39
+ const accessMode = typeof capabilities?.mode === "string"
40
+ ? capabilities.mode
41
+ : typeof capabilities?.access_mode === "string"
42
+ ? capabilities.access_mode
43
+ : "unknown";
44
+ const methods = Array.isArray(capabilities?.methods) ? capabilities.methods.length : 0;
45
+ return [
46
+ "cmux status",
47
+ "",
48
+ `Detected: ${detected.available ? "yes" : "no"}`,
49
+ `Enabled: ${resolved.enabled ? "yes" : "no"}`,
50
+ `CLI available: ${detected.cliAvailable ? "yes" : "no"}`,
51
+ `Socket: ${detected.socketPath}`,
52
+ `Workspace: ${detected.workspaceId ?? "(none)"}`,
53
+ `Surface: ${detected.surfaceId ?? "(none)"}`,
54
+ `Features: notifications=${resolved.notifications ? "on" : "off"}, sidebar=${resolved.sidebar ? "on" : "off"}, splits=${resolved.splits ? "on" : "off"}, browser=${resolved.browser ? "on" : "off"}`,
55
+ `Capabilities: access=${accessMode}, methods=${methods}`,
56
+ ].join("\n");
57
+ }
58
+ function ensureCmuxAvailableForEnable(ctx) {
59
+ const detected = detectCmuxEnvironment();
60
+ if (detected.available)
61
+ return true;
62
+ ctx.ui.notify("cmux not detected. Install it from https://cmux.com and run gsd inside a cmux terminal.", "warning");
63
+ return false;
64
+ }
65
+ export async function handleCmux(args, ctx) {
66
+ const trimmed = args.trim();
67
+ if (!trimmed || trimmed === "status") {
68
+ ctx.ui.notify(formatCmuxStatus(), "info");
69
+ return;
70
+ }
71
+ if (trimmed === "on") {
72
+ if (!ensureCmuxAvailableForEnable(ctx))
73
+ return;
74
+ await writeProjectCmuxPreferences(ctx, (prefs) => {
75
+ prefs.cmux = {
76
+ enabled: true,
77
+ notifications: true,
78
+ sidebar: true,
79
+ splits: false,
80
+ browser: false,
81
+ ...(prefs.cmux ?? {}),
82
+ };
83
+ prefs.cmux.enabled = true;
84
+ });
85
+ ctx.ui.notify("cmux integration enabled in project preferences.", "info");
86
+ return;
87
+ }
88
+ if (trimmed === "off") {
89
+ const effective = loadEffectiveGSDPreferences()?.preferences;
90
+ await writeProjectCmuxPreferences(ctx, (prefs) => {
91
+ prefs.cmux = { ...(prefs.cmux ?? {}), enabled: false };
92
+ });
93
+ clearCmuxSidebar(effective);
94
+ ctx.ui.notify("cmux integration disabled in project preferences.", "info");
95
+ return;
96
+ }
97
+ const parts = trimmed.split(/\s+/);
98
+ if (parts.length === 2 && ["notifications", "sidebar", "splits", "browser"].includes(parts[0]) && ["on", "off"].includes(parts[1])) {
99
+ const feature = parts[0];
100
+ const enabled = parts[1] === "on";
101
+ if (enabled && !ensureCmuxAvailableForEnable(ctx))
102
+ return;
103
+ await writeProjectCmuxPreferences(ctx, (prefs) => {
104
+ const next = { ...(prefs.cmux ?? {}) };
105
+ next[feature] = enabled;
106
+ if (enabled)
107
+ next.enabled = true;
108
+ prefs.cmux = next;
109
+ });
110
+ if (!enabled && feature === "sidebar") {
111
+ clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
112
+ }
113
+ const note = feature === "browser" && enabled
114
+ ? " Browser surfaces are still a follow-up path."
115
+ : "";
116
+ ctx.ui.notify(`cmux ${feature} ${enabled ? "enabled" : "disabled"}.${note}`, "info");
117
+ return;
118
+ }
119
+ ctx.ui.notify("Usage: /gsd cmux <status|on|off|notifications on|notifications off|sidebar on|sidebar off|splits on|splits off|browser on|browser off>", "info");
120
+ }
@@ -626,7 +626,7 @@ export function serializePreferencesToFrontmatter(prefs) {
626
626
  "skill_rules", "custom_instructions", "models", "skill_discovery",
627
627
  "skill_staleness_days", "auto_supervisor", "uat_dispatch", "unique_milestone_ids",
628
628
  "budget_ceiling", "budget_enforcement", "context_pause_threshold",
629
- "notifications", "remote_questions", "git",
629
+ "notifications", "cmux", "remote_questions", "git",
630
630
  "post_unit_hooks", "pre_dispatch_hooks",
631
631
  "dynamic_routing", "token_profile", "phases", "parallel",
632
632
  "auto_visualize", "auto_report",
@@ -12,7 +12,7 @@ import { deriveState } from "./state.js";
12
12
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
13
13
  import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
14
14
  import { showQueue, showDiscuss, showHeadlessMilestoneCreation } from "./guided-flow.js";
15
- import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, stopAutoRemote } from "./auto.js";
15
+ import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, stopAutoRemote, checkRemoteAutoSession } from "./auto.js";
16
16
  import { dispatchDirectPhase } from "./auto-direct-dispatch.js";
17
17
  import { resolveProjectRoot } from "./worktree.js";
18
18
  import { assertSafeDirectory } from "./validate-directory.js";
@@ -36,7 +36,8 @@ import { computeProgressScore, formatProgressLine } from "./progress-score.js";
36
36
  import { runEnvironmentChecks } from "./doctor-environment.js";
37
37
  import { handleLogs } from "./commands-logs.js";
38
38
  import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
39
- import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
39
+ import { handleCmux } from "./commands-cmux.js";
40
+ import { showNextAction } from "../shared/mod.js";
40
41
  /** Resolve the effective project root, accounting for worktree paths. */
41
42
  export function projectRoot() {
42
43
  const cwd = process.cwd();
@@ -56,40 +57,85 @@ export function projectRoot() {
56
57
  return root;
57
58
  }
58
59
  /**
59
- * Check if another process holds the auto-mode session lock.
60
- * Returns the lock data if a remote session is alive, null otherwise.
60
+ * Guard against starting auto-mode when a remote session is already running.
61
+ * Returns true if the caller should proceed with startAuto, false if handled.
61
62
  */
62
- function getRemoteAutoSession(basePath) {
63
- const lockData = readSessionLockData(basePath);
64
- if (!lockData)
65
- return null;
66
- if (lockData.pid === process.pid)
67
- return null;
68
- if (!isSessionLockProcessAlive(lockData))
69
- return null;
70
- return { pid: lockData.pid };
71
- }
72
- /**
73
- * Show a steering menu when auto-mode is running in another process.
74
- * Returns true if a remote session was detected (caller should return early).
75
- */
76
- function notifyRemoteAutoActive(ctx, basePath) {
77
- const remote = getRemoteAutoSession(basePath);
78
- if (!remote)
63
+ async function guardRemoteSession(ctx, pi) {
64
+ // Local session already active — proceed (startAuto handles re-entrant calls)
65
+ if (isAutoActive() || isAutoPaused())
66
+ return true;
67
+ const remote = checkRemoteAutoSession(projectRoot());
68
+ if (!remote.running || !remote.pid)
69
+ return true;
70
+ const unitLabel = remote.unitType && remote.unitId
71
+ ? `${remote.unitType} (${remote.unitId})`
72
+ : "unknown unit";
73
+ const unitsMsg = remote.completedUnits != null
74
+ ? `${remote.completedUnits} units completed`
75
+ : "";
76
+ const choice = await showNextAction(ctx, {
77
+ title: `Auto-mode is running in another terminal (PID ${remote.pid})`,
78
+ summary: [
79
+ `Currently executing: ${unitLabel}`,
80
+ ...(unitsMsg ? [unitsMsg] : []),
81
+ ...(remote.startedAt ? [`Started: ${remote.startedAt}`] : []),
82
+ ],
83
+ actions: [
84
+ {
85
+ id: "status",
86
+ label: "View status",
87
+ description: "Show the current GSD progress dashboard.",
88
+ recommended: true,
89
+ },
90
+ {
91
+ id: "steer",
92
+ label: "Steer the session",
93
+ description: "Use /gsd steer <instruction> to redirect the running session.",
94
+ },
95
+ {
96
+ id: "stop",
97
+ label: "Stop remote session",
98
+ description: `Send SIGTERM to PID ${remote.pid} to stop it gracefully.`,
99
+ },
100
+ {
101
+ id: "force",
102
+ label: "Force start (steal lock)",
103
+ description: "Start a new session, terminating the existing one.",
104
+ },
105
+ ],
106
+ notYetMessage: "Run /gsd when ready.",
107
+ });
108
+ if (choice === "status") {
109
+ await handleStatus(ctx);
79
110
  return false;
80
- ctx.ui.notify(`Auto-mode is running in another process (PID ${remote.pid}).\n` +
81
- `Use these commands to interact with it:\n` +
82
- ` /gsd status — check progress\n` +
83
- ` /gsd discuss — discuss architecture decisions\n` +
84
- ` /gsd queue — queue the next milestone\n` +
85
- ` /gsd steer — apply an override to active work\n` +
86
- ` /gsd capture — fire-and-forget thought\n` +
87
- ` /gsd stop — stop auto-mode`, "warning");
88
- return true;
111
+ }
112
+ if (choice === "steer") {
113
+ ctx.ui.notify("Use /gsd steer <instruction> to redirect the running auto-mode session.\n" +
114
+ "Example: /gsd steer Use Postgres instead of SQLite", "info");
115
+ return false;
116
+ }
117
+ if (choice === "stop") {
118
+ const result = stopAutoRemote(projectRoot());
119
+ if (result.found) {
120
+ ctx.ui.notify(`Sent stop signal to auto-mode session (PID ${result.pid}). It will shut down gracefully.`, "info");
121
+ }
122
+ else if (result.error) {
123
+ ctx.ui.notify(`Failed to stop remote auto-mode: ${result.error}`, "error");
124
+ }
125
+ else {
126
+ ctx.ui.notify("Remote session is no longer running.", "info");
127
+ }
128
+ return false;
129
+ }
130
+ if (choice === "force") {
131
+ return true; // Proceed — startAuto will steal the lock
132
+ }
133
+ // "not_yet" or escape
134
+ return false;
89
135
  }
90
136
  export function registerGSDCommand(pi) {
91
137
  pi.registerCommand("gsd", {
92
- 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|update",
138
+ 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",
93
139
  getArgumentCompletions: (prefix) => {
94
140
  const subcommands = [
95
141
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -98,6 +144,7 @@ export function registerGSDCommand(pi) {
98
144
  { cmd: "stop", desc: "Stop auto mode gracefully" },
99
145
  { cmd: "pause", desc: "Pause auto-mode (preserves state, /gsd auto to resume)" },
100
146
  { cmd: "status", desc: "Progress dashboard" },
147
+ { cmd: "widget", desc: "Cycle widget: full → small → min → off" },
101
148
  { cmd: "visualize", desc: "Open 10-tab workflow visualizer (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)" },
102
149
  { cmd: "queue", desc: "Queue and reorder future milestones" },
103
150
  { cmd: "quick", desc: "Execute a quick task without full planning overhead" },
@@ -131,6 +178,7 @@ export function registerGSDCommand(pi) {
131
178
  { cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
132
179
  { cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
133
180
  { cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
181
+ { cmd: "cmux", desc: "Manage cmux integration (status, sidebar, notifications, splits)" },
134
182
  { cmd: "park", desc: "Park a milestone — skip without deleting" },
135
183
  { cmd: "unpark", desc: "Reactivate a parked milestone" },
136
184
  { cmd: "update", desc: "Update GSD to the latest version" },
@@ -182,6 +230,36 @@ export function registerGSDCommand(pi) {
182
230
  .filter((s) => s.cmd.startsWith(subPrefix))
183
231
  .map((s) => ({ value: `parallel ${s.cmd}`, label: s.cmd, description: s.desc }));
184
232
  }
233
+ if (parts[0] === "cmux") {
234
+ if (parts.length <= 2) {
235
+ const subPrefix = parts[1] ?? "";
236
+ const subs = [
237
+ { cmd: "status", desc: "Show cmux detection, prefs, and capabilities" },
238
+ { cmd: "on", desc: "Enable cmux integration" },
239
+ { cmd: "off", desc: "Disable cmux integration" },
240
+ { cmd: "notifications", desc: "Toggle cmux desktop notifications" },
241
+ { cmd: "sidebar", desc: "Toggle cmux sidebar metadata" },
242
+ { cmd: "splits", desc: "Toggle cmux visual subagent splits" },
243
+ { cmd: "browser", desc: "Toggle future browser integration flag" },
244
+ ];
245
+ return subs
246
+ .filter((s) => s.cmd.startsWith(subPrefix))
247
+ .map((s) => ({ value: `cmux ${s.cmd}`, label: s.cmd, description: s.desc }));
248
+ }
249
+ if (parts.length <= 3 && ["notifications", "sidebar", "splits", "browser"].includes(parts[1])) {
250
+ const togglePrefix = parts[2] ?? "";
251
+ return [
252
+ { cmd: "on", desc: "Enable this cmux area" },
253
+ { cmd: "off", desc: "Disable this cmux area" },
254
+ ]
255
+ .filter((item) => item.cmd.startsWith(togglePrefix))
256
+ .map((item) => ({
257
+ value: `cmux ${parts[1]} ${item.cmd}`,
258
+ label: item.cmd,
259
+ description: item.desc,
260
+ }));
261
+ }
262
+ }
185
263
  if (parts[0] === "setup" && parts.length <= 2) {
186
264
  const subPrefix = parts[1] ?? "";
187
265
  const subs = [
@@ -430,6 +508,18 @@ export async function handleGSDCommand(args, ctx, pi) {
430
508
  await handleStatus(ctx);
431
509
  return;
432
510
  }
511
+ if (trimmed === "widget" || trimmed.startsWith("widget ")) {
512
+ const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
513
+ const arg = trimmed.replace(/^widget\s*/, "").trim();
514
+ if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
515
+ setWidgetMode(arg);
516
+ }
517
+ else {
518
+ cycleWidgetMode();
519
+ }
520
+ ctx.ui.notify(`Widget: ${getWidgetMode()}`, "info");
521
+ return;
522
+ }
433
523
  if (trimmed === "visualize") {
434
524
  await handleVisualize(ctx);
435
525
  return;
@@ -446,6 +536,10 @@ export async function handleGSDCommand(args, ctx, pi) {
446
536
  await handlePrefs(trimmed.replace(/^prefs\s*/, "").trim(), ctx);
447
537
  return;
448
538
  }
539
+ if (trimmed === "cmux" || trimmed.startsWith("cmux ")) {
540
+ await handleCmux(trimmed.replace(/^cmux\s*/, "").trim(), ctx);
541
+ return;
542
+ }
449
543
  if (trimmed === "init") {
450
544
  const { detectProjectState } = await import("./detection.js");
451
545
  const { showProjectInit, handleReinit } = await import("./init-wizard.js");
@@ -493,12 +587,12 @@ export async function handleGSDCommand(args, ctx, pi) {
493
587
  await handleDryRun(ctx, projectRoot());
494
588
  return;
495
589
  }
496
- if (notifyRemoteAutoActive(ctx, projectRoot()))
497
- return;
498
590
  const verboseMode = trimmed.includes("--verbose");
499
591
  const debugMode = trimmed.includes("--debug");
500
592
  if (debugMode)
501
593
  enableDebug(projectRoot());
594
+ if (!(await guardRemoteSession(ctx, pi)))
595
+ return;
502
596
  await startAuto(ctx, pi, projectRoot(), verboseMode, { step: true });
503
597
  return;
504
598
  }
@@ -507,6 +601,8 @@ export async function handleGSDCommand(args, ctx, pi) {
507
601
  const debugMode = trimmed.includes("--debug");
508
602
  if (debugMode)
509
603
  enableDebug(projectRoot());
604
+ if (!(await guardRemoteSession(ctx, pi)))
605
+ return;
510
606
  await startAuto(ctx, pi, projectRoot(), verboseMode);
511
607
  return;
512
608
  }
@@ -850,7 +946,7 @@ Examples:
850
946
  return;
851
947
  }
852
948
  if (trimmed === "") {
853
- if (notifyRemoteAutoActive(ctx, projectRoot()))
949
+ if (!(await guardRemoteSession(ctx, pi)))
854
950
  return;
855
951
  await startAuto(ctx, pi, projectRoot(), false, { step: true });
856
952
  return;
@@ -900,6 +996,7 @@ function showHelp(ctx) {
900
996
  " /gsd setup Global setup status [llm|search|remote|keys|prefs]",
901
997
  " /gsd mode Set workflow mode (solo/team) [global|project]",
902
998
  " /gsd prefs Manage preferences [global|project|status|wizard|setup|import-claude]",
999
+ " /gsd cmux Manage cmux integration [status|on|off|notifications|sidebar|splits|browser]",
903
1000
  " /gsd config Set API keys for external tools",
904
1001
  " /gsd keys API key manager [list|add|remove|test|rotate|doctor]",
905
1002
  " /gsd hooks Show post-unit hook configuration",