gsd-pi 2.36.0-dev.f887f4e → 2.37.0-dev.3186675

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 +35 -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 +51 -1
  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 +45 -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 +54 -1
  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";
@@ -242,13 +243,28 @@ function buildSnapshotOpts(unitType, unitId) {
242
243
  ...(runtime?.continueHereFired ? { continueHereFired: true } : {}),
243
244
  };
244
245
  }
245
- function handleLostSessionLock(ctx) {
246
- debugLog("session-lock-lost", { lockBase: lockBase() });
246
+ function handleLostSessionLock(ctx, lockStatus) {
247
+ debugLog("session-lock-lost", {
248
+ lockBase: lockBase(),
249
+ reason: lockStatus?.failureReason,
250
+ existingPid: lockStatus?.existingPid,
251
+ expectedPid: lockStatus?.expectedPid,
252
+ });
247
253
  s.active = false;
248
254
  s.paused = false;
249
255
  clearUnitTimeout();
250
256
  deregisterSigtermHandler();
251
- ctx?.ui.notify("Session lock lost — another GSD process appears to have taken over. Stopping gracefully.", "error");
257
+ clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
258
+ const message = lockStatus?.failureReason === "pid-mismatch"
259
+ ? lockStatus.existingPid
260
+ ? `Session lock moved to PID ${lockStatus.existingPid} — another GSD process appears to have taken over. Stopping gracefully.`
261
+ : "Session lock moved to a different process — another GSD process appears to have taken over. Stopping gracefully."
262
+ : lockStatus?.failureReason === "missing-metadata"
263
+ ? "Session lock metadata disappeared, so ownership could not be confirmed. Stopping gracefully."
264
+ : lockStatus?.failureReason === "compromised"
265
+ ? "Session lock was compromised or invalidated during heartbeat checks; takeover was not confirmed. Stopping gracefully."
266
+ : "Session lock lost. Stopping gracefully.";
267
+ ctx?.ui.notify(message, "error");
252
268
  ctx?.ui.setStatus("gsd-auto", undefined);
253
269
  ctx?.ui.setWidget("gsd-progress", undefined);
254
270
  ctx?.ui.setFooter(undefined);
@@ -256,6 +272,7 @@ function handleLostSessionLock(ctx) {
256
272
  export async function stopAuto(ctx, pi, reason) {
257
273
  if (!s.active && !s.paused)
258
274
  return;
275
+ const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
259
276
  const reasonSuffix = reason ? ` — ${reason}` : "";
260
277
  clearUnitTimeout();
261
278
  if (lockBase())
@@ -314,6 +331,8 @@ export async function stopAuto(ctx, pi, reason) {
314
331
  });
315
332
  }
316
333
  }
334
+ clearCmuxSidebar(loadedPreferences);
335
+ logCmuxEvent(loadedPreferences, `Auto-mode stopped${reasonSuffix || ""}.`, reason?.startsWith("Blocked:") ? "warning" : "info");
317
336
  if (isDebugEnabled()) {
318
337
  const logPath = writeDebugSummary();
319
338
  if (logPath) {
@@ -455,6 +474,8 @@ function buildLoopDeps() {
455
474
  pauseAuto,
456
475
  clearUnitTimeout,
457
476
  updateProgressWidget,
477
+ syncCmuxSidebar,
478
+ logCmuxEvent,
458
479
  // State and cache
459
480
  invalidateAllCaches,
460
481
  deriveState,
@@ -466,7 +487,7 @@ function buildLoopDeps() {
466
487
  // Resource version guard
467
488
  checkResourcesStale,
468
489
  // Session lock
469
- validateSessionLock,
490
+ validateSessionLock: getSessionLockStatus,
470
491
  updateSessionLock,
471
492
  handleLostSessionLock,
472
493
  // Milestone transition
@@ -605,6 +626,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
605
626
  restoreHookState(s.basePath);
606
627
  try {
607
628
  await rebuildState(s.basePath);
629
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
608
630
  }
609
631
  catch (e) {
610
632
  debugLog("resume-rebuild-state-failed", {
@@ -634,6 +656,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
634
656
  }
635
657
  updateSessionLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
636
658
  writeLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
659
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
637
660
  await autoLoop(ctx, pi, s, buildLoopDeps());
638
661
  return;
639
662
  }
@@ -647,6 +670,13 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
647
670
  const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps);
648
671
  if (!ready)
649
672
  return;
673
+ try {
674
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
675
+ }
676
+ catch {
677
+ // Best-effort only — sidebar sync must never block auto-mode startup
678
+ }
679
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
650
680
  // Dispatch the first unit
651
681
  await autoLoop(ctx, pi, s, buildLoopDeps());
652
682
  }
@@ -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",
@@ -37,6 +37,7 @@ 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
39
  import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
40
+ import { handleCmux } from "./commands-cmux.js";
40
41
  /** Resolve the effective project root, accounting for worktree paths. */
41
42
  export function projectRoot() {
42
43
  const cwd = process.cwd();
@@ -89,7 +90,7 @@ function notifyRemoteAutoActive(ctx, basePath) {
89
90
  }
90
91
  export function registerGSDCommand(pi) {
91
92
  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",
93
+ 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
94
  getArgumentCompletions: (prefix) => {
94
95
  const subcommands = [
95
96
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -98,6 +99,7 @@ export function registerGSDCommand(pi) {
98
99
  { cmd: "stop", desc: "Stop auto mode gracefully" },
99
100
  { cmd: "pause", desc: "Pause auto-mode (preserves state, /gsd auto to resume)" },
100
101
  { cmd: "status", desc: "Progress dashboard" },
102
+ { cmd: "widget", desc: "Cycle widget: full → small → min → off" },
101
103
  { cmd: "visualize", desc: "Open 10-tab workflow visualizer (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)" },
102
104
  { cmd: "queue", desc: "Queue and reorder future milestones" },
103
105
  { cmd: "quick", desc: "Execute a quick task without full planning overhead" },
@@ -131,6 +133,7 @@ export function registerGSDCommand(pi) {
131
133
  { cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
132
134
  { cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
133
135
  { cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
136
+ { cmd: "cmux", desc: "Manage cmux integration (status, sidebar, notifications, splits)" },
134
137
  { cmd: "park", desc: "Park a milestone — skip without deleting" },
135
138
  { cmd: "unpark", desc: "Reactivate a parked milestone" },
136
139
  { cmd: "update", desc: "Update GSD to the latest version" },
@@ -182,6 +185,36 @@ export function registerGSDCommand(pi) {
182
185
  .filter((s) => s.cmd.startsWith(subPrefix))
183
186
  .map((s) => ({ value: `parallel ${s.cmd}`, label: s.cmd, description: s.desc }));
184
187
  }
188
+ if (parts[0] === "cmux") {
189
+ if (parts.length <= 2) {
190
+ const subPrefix = parts[1] ?? "";
191
+ const subs = [
192
+ { cmd: "status", desc: "Show cmux detection, prefs, and capabilities" },
193
+ { cmd: "on", desc: "Enable cmux integration" },
194
+ { cmd: "off", desc: "Disable cmux integration" },
195
+ { cmd: "notifications", desc: "Toggle cmux desktop notifications" },
196
+ { cmd: "sidebar", desc: "Toggle cmux sidebar metadata" },
197
+ { cmd: "splits", desc: "Toggle cmux visual subagent splits" },
198
+ { cmd: "browser", desc: "Toggle future browser integration flag" },
199
+ ];
200
+ return subs
201
+ .filter((s) => s.cmd.startsWith(subPrefix))
202
+ .map((s) => ({ value: `cmux ${s.cmd}`, label: s.cmd, description: s.desc }));
203
+ }
204
+ if (parts.length <= 3 && ["notifications", "sidebar", "splits", "browser"].includes(parts[1])) {
205
+ const togglePrefix = parts[2] ?? "";
206
+ return [
207
+ { cmd: "on", desc: "Enable this cmux area" },
208
+ { cmd: "off", desc: "Disable this cmux area" },
209
+ ]
210
+ .filter((item) => item.cmd.startsWith(togglePrefix))
211
+ .map((item) => ({
212
+ value: `cmux ${parts[1]} ${item.cmd}`,
213
+ label: item.cmd,
214
+ description: item.desc,
215
+ }));
216
+ }
217
+ }
185
218
  if (parts[0] === "setup" && parts.length <= 2) {
186
219
  const subPrefix = parts[1] ?? "";
187
220
  const subs = [
@@ -430,6 +463,18 @@ export async function handleGSDCommand(args, ctx, pi) {
430
463
  await handleStatus(ctx);
431
464
  return;
432
465
  }
466
+ if (trimmed === "widget" || trimmed.startsWith("widget ")) {
467
+ const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
468
+ const arg = trimmed.replace(/^widget\s*/, "").trim();
469
+ if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
470
+ setWidgetMode(arg);
471
+ }
472
+ else {
473
+ cycleWidgetMode();
474
+ }
475
+ ctx.ui.notify(`Widget: ${getWidgetMode()}`, "info");
476
+ return;
477
+ }
433
478
  if (trimmed === "visualize") {
434
479
  await handleVisualize(ctx);
435
480
  return;
@@ -446,6 +491,10 @@ export async function handleGSDCommand(args, ctx, pi) {
446
491
  await handlePrefs(trimmed.replace(/^prefs\s*/, "").trim(), ctx);
447
492
  return;
448
493
  }
494
+ if (trimmed === "cmux" || trimmed.startsWith("cmux ")) {
495
+ await handleCmux(trimmed.replace(/^cmux\s*/, "").trim(), ctx);
496
+ return;
497
+ }
449
498
  if (trimmed === "init") {
450
499
  const { detectProjectState } = await import("./detection.js");
451
500
  const { showProjectInit, handleReinit } = await import("./init-wizard.js");
@@ -900,6 +949,7 @@ function showHelp(ctx) {
900
949
  " /gsd setup Global setup status [llm|search|remote|keys|prefs]",
901
950
  " /gsd mode Set workflow mode (solo/team) [global|project]",
902
951
  " /gsd prefs Manage preferences [global|project|status|wizard|setup|import-claude]",
952
+ " /gsd cmux Manage cmux integration [status|on|off|notifications|sidebar|splits|browser]",
903
953
  " /gsd config Set API keys for external tools",
904
954
  " /gsd keys API key manager [list|add|remove|test|rotate|doctor]",
905
955
  " /gsd hooks Show post-unit hook configuration",
@@ -173,6 +173,13 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
173
173
  - `on_milestone`: boolean — notify when a milestone finishes. Default: `true`.
174
174
  - `on_attention`: boolean — notify when manual attention is needed. Default: `true`.
175
175
 
176
+ - `cmux`: configures cmux terminal integration when GSD is running inside a cmux workspace. Keys:
177
+ - `enabled`: boolean — master toggle for cmux integration. Default: `false`.
178
+ - `notifications`: boolean — route desktop notifications through cmux. Default: `true` when enabled.
179
+ - `sidebar`: boolean — publish status, progress, and log metadata to the cmux sidebar. Default: `true` when enabled.
180
+ - `splits`: boolean — run supported subagent work in visible cmux splits. Default: `false`.
181
+ - `browser`: boolean — reserve the future browser integration flag. Default: `false`.
182
+
176
183
  - `dynamic_routing`: configures the dynamic model router that adjusts model selection based on task complexity. Keys:
177
184
  - `enabled`: boolean — enable dynamic routing. Default: `false`.
178
185
  - `tier_models`: object — model overrides per complexity tier. Keys: `light`, `standard`, `heavy`. Values are model ID strings.
@@ -477,6 +484,24 @@ Disables per-unit completion notifications (noisy in long runs) while keeping er
477
484
 
478
485
  ---
479
486
 
487
+ ## cmux Example
488
+
489
+ ```yaml
490
+ ---
491
+ version: 1
492
+ cmux:
493
+ enabled: true
494
+ notifications: true
495
+ sidebar: true
496
+ splits: true
497
+ browser: false
498
+ ---
499
+ ```
500
+
501
+ Enables cmux-aware notifications, sidebar metadata, and visible subagent splits when GSD is running inside a cmux terminal.
502
+
503
+ ---
504
+
480
505
  ## Post-Unit Hooks Example
481
506
 
482
507
  ```yaml
@@ -349,10 +349,18 @@ export class GitServiceImpl {
349
349
  }
350
350
  const wtName = detectWorktreeName(this.basePath);
351
351
  if (wtName) {
352
+ // Auto-mode worktrees use milestone/<MID> branches (wtName = milestone ID)
353
+ const milestoneBranch = `milestone/${wtName}`;
354
+ const currentBranch = nativeGetCurrentBranch(this.basePath);
355
+ // If we're on a milestone/<MID> branch, use it (auto-mode case)
356
+ if (currentBranch.startsWith("milestone/")) {
357
+ return currentBranch;
358
+ }
359
+ // Otherwise check for manual worktree branch (worktree/<name>)
352
360
  const wtBranch = `worktree/${wtName}`;
353
361
  if (nativeBranchExists(this.basePath, wtBranch))
354
362
  return wtBranch;
355
- return nativeGetCurrentBranch(this.basePath);
363
+ return currentBranch;
356
364
  }
357
365
  // Repo-level default detection: origin/HEAD → main → master → current branch.
358
366
  // Native path uses libgit2 (single call), fallback spawns multiple git processes.
@@ -1,6 +1,7 @@
1
1
  // GSD Extension — Session History View
2
2
  // Human-readable display of past auto-mode unit executions.
3
- import { formatDuration, padRight, truncateWithEllipsis } from "../shared/format-utils.js";
3
+ import { formatDuration, truncateWithEllipsis } from "../shared/format-utils.js";
4
+ import { padRight } from "../shared/layout-utils.js";
4
5
  import { getLedger, getProjectTotals, formatCost, formatTokenCount, aggregateBySlice, aggregateByPhase, aggregateByModel, loadLedgerFromDisk, } from "./metrics.js";
5
6
  /**
6
7
  * Show recent unit execution history with cost, tokens, and duration.
@@ -46,6 +46,7 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
46
46
  import { toPosixPath } from "../shared/mod.js";
47
47
  import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
48
48
  import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
49
+ import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js";
49
50
  // ── Agent Instructions (DEPRECATED) ──────────────────────────────────────
50
51
  // agent-instructions.md is deprecated. Use AGENTS.md or CLAUDE.md instead.
51
52
  // Pi core natively supports AGENTS.md (with CLAUDE.md fallback) per directory.
@@ -532,6 +533,10 @@ export default function (pi) {
532
533
  const stopContextTimer = debugTime("context-inject");
533
534
  const systemContent = loadPrompt("system");
534
535
  const loadedPreferences = loadEffectiveGSDPreferences();
536
+ if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) {
537
+ markCmuxPromptShown();
538
+ ctx.ui.notify("cmux detected. Run /gsd cmux on to enable sidebar metadata, notifications, and visual subagent splits for this project.", "info");
539
+ }
535
540
  let preferenceBlock = "";
536
541
  if (loadedPreferences) {
537
542
  const cwd = process.cwd();
@@ -17,8 +17,10 @@ import { gsdRoot } from "./paths.js";
17
17
  import { getAndClearSkills } from "./skill-telemetry.js";
18
18
  import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
19
19
  import { parseUnitId } from "./unit-id.js";
20
- // Re-export from shared — canonical implementation lives in format-utils.
21
- export { formatTokenCount } from "../shared/mod.js";
20
+ // Re-export from shared — import directly from format-utils to avoid pulling
21
+ // in the full barrel (mod.js → ui.js → @gsd/pi-tui) which breaks when loaded
22
+ // outside jiti's alias resolution (e.g. dynamic import in auto-loop reports).
23
+ export { formatTokenCount } from "../shared/format-utils.js";
22
24
  export function classifyUnitPhase(unitType) {
23
25
  switch (unitType) {
24
26
  case "research-milestone":
@@ -2,13 +2,22 @@
2
2
  // Cross-platform desktop notifications for auto-mode events.
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { loadEffectiveGSDPreferences } from "./preferences.js";
5
+ import { CmuxClient, emitOsc777Notification, resolveCmuxConfig } from "../cmux/index.js";
5
6
  /**
6
7
  * Send a native desktop notification. Non-blocking, non-fatal.
7
8
  * macOS: osascript, Linux: notify-send, Windows: skipped.
8
9
  */
9
10
  export function sendDesktopNotification(title, message, level = "info", kind = "complete") {
10
- if (!shouldSendDesktopNotification(kind))
11
+ const loaded = loadEffectiveGSDPreferences()?.preferences;
12
+ if (!shouldSendDesktopNotification(kind, loaded?.notifications))
11
13
  return;
14
+ const cmux = resolveCmuxConfig(loaded);
15
+ if (cmux.notifications) {
16
+ const delivered = CmuxClient.fromPreferences(loaded).notify(title, message);
17
+ if (delivered)
18
+ return;
19
+ emitOsc777Notification(title, message);
20
+ }
12
21
  try {
13
22
  const command = buildDesktopNotificationCommand(process.platform, title, message, level);
14
23
  if (!command)
@@ -47,6 +47,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
47
47
  "budget_enforcement",
48
48
  "context_pause_threshold",
49
49
  "notifications",
50
+ "cmux",
50
51
  "remote_questions",
51
52
  "git",
52
53
  "post_unit_hooks",
@@ -63,6 +64,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
63
64
  "search_provider",
64
65
  "compression_strategy",
65
66
  "context_selection",
67
+ "widget_mode",
66
68
  ]);
67
69
  /** Canonical list of all dispatch unit types. */
68
70
  export const KNOWN_UNIT_TYPES = [
@@ -225,6 +225,35 @@ export function validatePreferences(preferences) {
225
225
  errors.push("notifications must be an object");
226
226
  }
227
227
  }
228
+ // ─── Cmux ───────────────────────────────────────────────────────────────
229
+ if (preferences.cmux !== undefined) {
230
+ if (preferences.cmux && typeof preferences.cmux === "object") {
231
+ const cmux = preferences.cmux;
232
+ const validatedCmux = {};
233
+ if (cmux.enabled !== undefined)
234
+ validatedCmux.enabled = !!cmux.enabled;
235
+ if (cmux.notifications !== undefined)
236
+ validatedCmux.notifications = !!cmux.notifications;
237
+ if (cmux.sidebar !== undefined)
238
+ validatedCmux.sidebar = !!cmux.sidebar;
239
+ if (cmux.splits !== undefined)
240
+ validatedCmux.splits = !!cmux.splits;
241
+ if (cmux.browser !== undefined)
242
+ validatedCmux.browser = !!cmux.browser;
243
+ const knownCmuxKeys = new Set(["enabled", "notifications", "sidebar", "splits", "browser"]);
244
+ for (const key of Object.keys(cmux)) {
245
+ if (!knownCmuxKeys.has(key)) {
246
+ warnings.push(`unknown cmux key "${key}" — ignored`);
247
+ }
248
+ }
249
+ if (Object.keys(validatedCmux).length > 0) {
250
+ validated.cmux = validatedCmux;
251
+ }
252
+ }
253
+ else {
254
+ errors.push("cmux must be an object");
255
+ }
256
+ }
228
257
  // ─── Remote Questions ───────────────────────────────────────────────
229
258
  if (preferences.remote_questions !== undefined) {
230
259
  if (preferences.remote_questions && typeof preferences.remote_questions === "object") {