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
@@ -15,6 +15,7 @@ import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
15
15
  import type { AutoSession } from "./auto/session.js";
16
16
  import { NEW_SESSION_TIMEOUT_MS } from "./auto/session.js";
17
17
  import type { GSDPreferences } from "./preferences.js";
18
+ import type { SessionLockStatus } from "./session-lock.js";
18
19
  import type { GSDState } from "./types.js";
19
20
  import type { CloseoutOptions } from "./auto-unit-closeout.js";
20
21
  import type { PostUnitContext } from "./auto-post-unit.js";
@@ -25,6 +26,7 @@ import type {
25
26
  import type { DispatchAction } from "./auto-dispatch.js";
26
27
  import type { WorktreeResolver } from "./worktree-resolver.js";
27
28
  import { debugLog } from "./debug-logger.js";
29
+ import type { CmuxLogLevel } from "../cmux/index.js";
28
30
 
29
31
  /**
30
32
  * Maximum total loop iterations before forced stop. Prevents runaway loops
@@ -276,6 +278,12 @@ export interface LoopDeps {
276
278
  unitId: string,
277
279
  state: GSDState,
278
280
  ) => void;
281
+ syncCmuxSidebar: (preferences: GSDPreferences | undefined, state: GSDState) => void;
282
+ logCmuxEvent: (
283
+ preferences: GSDPreferences | undefined,
284
+ message: string,
285
+ level?: CmuxLogLevel,
286
+ ) => void;
279
287
 
280
288
  // State and cache functions
281
289
  invalidateAllCaches: () => void;
@@ -300,7 +308,7 @@ export interface LoopDeps {
300
308
  checkResourcesStale: (version: string | null) => string | null;
301
309
 
302
310
  // Session lock
303
- validateSessionLock: (basePath: string) => boolean;
311
+ validateSessionLock: (basePath: string) => SessionLockStatus;
304
312
  updateSessionLock: (
305
313
  basePath: string,
306
314
  unitType: string,
@@ -308,7 +316,10 @@ export interface LoopDeps {
308
316
  completedUnits: number,
309
317
  sessionFile?: string,
310
318
  ) => void;
311
- handleLostSessionLock: (ctx?: ExtensionContext) => void;
319
+ handleLostSessionLock: (
320
+ ctx?: ExtensionContext,
321
+ lockStatus?: SessionLockStatus,
322
+ ) => void;
312
323
 
313
324
  // Milestone transition functions
314
325
  sendDesktopNotification: (
@@ -552,10 +563,24 @@ export async function autoLoop(
552
563
  try {
553
564
  // ── Blanket try/catch: one bad iteration must not kill the session
554
565
 
555
- if (deps.lockBase() && !deps.validateSessionLock(deps.lockBase())) {
556
- deps.handleLostSessionLock(ctx);
557
- debugLog("autoLoop", { phase: "exit", reason: "session-lock-lost" });
558
- break;
566
+ const sessionLockBase = deps.lockBase();
567
+ if (sessionLockBase) {
568
+ const lockStatus = deps.validateSessionLock(sessionLockBase);
569
+ if (!lockStatus.valid) {
570
+ debugLog("autoLoop", {
571
+ phase: "session-lock-invalid",
572
+ reason: lockStatus.failureReason ?? "unknown",
573
+ existingPid: lockStatus.existingPid,
574
+ expectedPid: lockStatus.expectedPid,
575
+ });
576
+ deps.handleLostSessionLock(ctx, lockStatus);
577
+ debugLog("autoLoop", {
578
+ phase: "exit",
579
+ reason: "session-lock-lost",
580
+ detail: lockStatus.failureReason ?? "unknown",
581
+ });
582
+ break;
583
+ }
559
584
  }
560
585
 
561
586
  // ── Phase 1: Pre-dispatch ───────────────────────────────────────────
@@ -609,6 +634,7 @@ export async function autoLoop(
609
634
 
610
635
  // Derive state
611
636
  let state = await deps.deriveState(s.basePath);
637
+ deps.syncCmuxSidebar(deps.loadEffectiveGSDPreferences()?.preferences, state);
612
638
  let mid = state.activeMilestone?.id;
613
639
  let midTitle = state.activeMilestone?.title;
614
640
  debugLog("autoLoop", {
@@ -630,6 +656,11 @@ export async function autoLoop(
630
656
  "success",
631
657
  "milestone",
632
658
  );
659
+ deps.logCmuxEvent(
660
+ deps.loadEffectiveGSDPreferences()?.preferences,
661
+ `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
662
+ "success",
663
+ );
633
664
 
634
665
  const vizPrefs = deps.loadEffectiveGSDPreferences()?.preferences;
635
666
  if (vizPrefs?.auto_visualize) {
@@ -767,12 +798,18 @@ export async function autoLoop(
767
798
  "success",
768
799
  "milestone",
769
800
  );
801
+ deps.logCmuxEvent(
802
+ deps.loadEffectiveGSDPreferences()?.preferences,
803
+ "All milestones complete.",
804
+ "success",
805
+ );
770
806
  await deps.stopAuto(ctx, pi, "All milestones complete");
771
807
  } else if (state.phase === "blocked") {
772
808
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
773
809
  await deps.stopAuto(ctx, pi, blockerMsg);
774
810
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
775
811
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
812
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
776
813
  } else {
777
814
  const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
778
815
  const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
@@ -850,6 +887,11 @@ export async function autoLoop(
850
887
  "success",
851
888
  "milestone",
852
889
  );
890
+ deps.logCmuxEvent(
891
+ deps.loadEffectiveGSDPreferences()?.preferences,
892
+ `Milestone ${mid} complete.`,
893
+ "success",
894
+ );
853
895
  await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
854
896
  debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
855
897
  break;
@@ -871,6 +913,7 @@ export async function autoLoop(
871
913
  await deps.stopAuto(ctx, pi, blockerMsg);
872
914
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
873
915
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
916
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
874
917
  debugLog("autoLoop", { phase: "exit", reason: "blocked" });
875
918
  break;
876
919
  }
@@ -914,12 +957,14 @@ export async function autoLoop(
914
957
  "warning",
915
958
  );
916
959
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
960
+ deps.logCmuxEvent(prefs, msg, "warning");
917
961
  await deps.pauseAuto(ctx, pi);
918
962
  debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
919
963
  break;
920
964
  }
921
965
  ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
922
966
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
967
+ deps.logCmuxEvent(prefs, msg, "warning");
923
968
  } else if (newBudgetAlertLevel === 90) {
924
969
  s.lastBudgetAlertLevel =
925
970
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
@@ -933,6 +978,11 @@ export async function autoLoop(
933
978
  "warning",
934
979
  "budget",
935
980
  );
981
+ deps.logCmuxEvent(
982
+ prefs,
983
+ `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
984
+ "warning",
985
+ );
936
986
  } else if (newBudgetAlertLevel === 80) {
937
987
  s.lastBudgetAlertLevel =
938
988
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
@@ -946,6 +996,11 @@ export async function autoLoop(
946
996
  "warning",
947
997
  "budget",
948
998
  );
999
+ deps.logCmuxEvent(
1000
+ prefs,
1001
+ `Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
1002
+ "warning",
1003
+ );
949
1004
  } else if (newBudgetAlertLevel === 75) {
950
1005
  s.lastBudgetAlertLevel =
951
1006
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
@@ -959,6 +1014,11 @@ export async function autoLoop(
959
1014
  "info",
960
1015
  "budget",
961
1016
  );
1017
+ deps.logCmuxEvent(
1018
+ prefs,
1019
+ `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
1020
+ "progress",
1021
+ );
962
1022
  } else if (budgetAlertLevel === 0) {
963
1023
  s.lastBudgetAlertLevel = 0;
964
1024
  }
@@ -47,10 +47,11 @@ import {
47
47
  } from "./crash-recovery.js";
48
48
  import {
49
49
  acquireSessionLock,
50
- validateSessionLock,
50
+ getSessionLockStatus,
51
51
  releaseSessionLock,
52
52
  updateSessionLock,
53
53
  } from "./session-lock.js";
54
+ import type { SessionLockStatus } from "./session-lock.js";
54
55
  import {
55
56
  clearUnitRuntimeRecord,
56
57
  inspectExecuteTaskDurability,
@@ -184,6 +185,7 @@ import {
184
185
  } from "./auto-supervisor.js";
185
186
  import { isDbAvailable } from "./gsd-db.js";
186
187
  import { countPendingCaptures } from "./captures.js";
188
+ import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
187
189
 
188
190
  // ── Extracted modules ──────────────────────────────────────────────────────
189
191
  import { startUnitSupervision } from "./auto-timers.js";
@@ -460,14 +462,33 @@ function buildSnapshotOpts(
460
462
  };
461
463
  }
462
464
 
463
- function handleLostSessionLock(ctx?: ExtensionContext): void {
464
- debugLog("session-lock-lost", { lockBase: lockBase() });
465
+ function handleLostSessionLock(
466
+ ctx?: ExtensionContext,
467
+ lockStatus?: SessionLockStatus,
468
+ ): void {
469
+ debugLog("session-lock-lost", {
470
+ lockBase: lockBase(),
471
+ reason: lockStatus?.failureReason,
472
+ existingPid: lockStatus?.existingPid,
473
+ expectedPid: lockStatus?.expectedPid,
474
+ });
465
475
  s.active = false;
466
476
  s.paused = false;
467
477
  clearUnitTimeout();
468
478
  deregisterSigtermHandler();
479
+ clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
480
+ const message =
481
+ lockStatus?.failureReason === "pid-mismatch"
482
+ ? lockStatus.existingPid
483
+ ? `Session lock moved to PID ${lockStatus.existingPid} — another GSD process appears to have taken over. Stopping gracefully.`
484
+ : "Session lock moved to a different process — another GSD process appears to have taken over. Stopping gracefully."
485
+ : lockStatus?.failureReason === "missing-metadata"
486
+ ? "Session lock metadata disappeared, so ownership could not be confirmed. Stopping gracefully."
487
+ : lockStatus?.failureReason === "compromised"
488
+ ? "Session lock was compromised or invalidated during heartbeat checks; takeover was not confirmed. Stopping gracefully."
489
+ : "Session lock lost. Stopping gracefully.";
469
490
  ctx?.ui.notify(
470
- "Session lock lost — another GSD process appears to have taken over. Stopping gracefully.",
491
+ message,
471
492
  "error",
472
493
  );
473
494
  ctx?.ui.setStatus("gsd-auto", undefined);
@@ -481,6 +502,7 @@ export async function stopAuto(
481
502
  reason?: string,
482
503
  ): Promise<void> {
483
504
  if (!s.active && !s.paused) return;
505
+ const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
484
506
  const reasonSuffix = reason ? ` — ${reason}` : "";
485
507
  clearUnitTimeout();
486
508
  if (lockBase()) clearLock(lockBase());
@@ -543,6 +565,13 @@ export async function stopAuto(
543
565
  }
544
566
  }
545
567
 
568
+ clearCmuxSidebar(loadedPreferences);
569
+ logCmuxEvent(
570
+ loadedPreferences,
571
+ `Auto-mode stopped${reasonSuffix || ""}.`,
572
+ reason?.startsWith("Blocked:") ? "warning" : "info",
573
+ );
574
+
546
575
  if (isDebugEnabled()) {
547
576
  const logPath = writeDebugSummary();
548
577
  if (logPath) {
@@ -708,6 +737,8 @@ function buildLoopDeps(): LoopDeps {
708
737
  pauseAuto,
709
738
  clearUnitTimeout,
710
739
  updateProgressWidget,
740
+ syncCmuxSidebar,
741
+ logCmuxEvent,
711
742
 
712
743
  // State and cache
713
744
  invalidateAllCaches,
@@ -724,7 +755,7 @@ function buildLoopDeps(): LoopDeps {
724
755
  checkResourcesStale,
725
756
 
726
757
  // Session lock
727
- validateSessionLock,
758
+ validateSessionLock: getSessionLockStatus,
728
759
  updateSessionLock,
729
760
  handleLostSessionLock,
730
761
 
@@ -890,6 +921,7 @@ export async function startAuto(
890
921
  restoreHookState(s.basePath);
891
922
  try {
892
923
  await rebuildState(s.basePath);
924
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
893
925
  } catch (e) {
894
926
  debugLog("resume-rebuild-state-failed", {
895
927
  error: e instanceof Error ? e.message : String(e),
@@ -941,6 +973,7 @@ export async function startAuto(
941
973
  s.currentMilestoneId ?? "unknown",
942
974
  s.completedUnits.length,
943
975
  );
976
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
944
977
 
945
978
  await autoLoop(ctx, pi, s, buildLoopDeps());
946
979
  return;
@@ -965,6 +998,13 @@ export async function startAuto(
965
998
  );
966
999
  if (!ready) return;
967
1000
 
1001
+ try {
1002
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
1003
+ } catch {
1004
+ // Best-effort only — sidebar sync must never block auto-mode startup
1005
+ }
1006
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
1007
+
968
1008
  // Dispatch the first unit
969
1009
  await autoLoop(ctx, pi, s, buildLoopDeps());
970
1010
  }
@@ -0,0 +1,143 @@
1
+ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { clearCmuxSidebar, CmuxClient, detectCmuxEnvironment, resolveCmuxConfig } from "../cmux/index.js";
4
+ import { saveFile } from "./files.js";
5
+ import {
6
+ getProjectGSDPreferencesPath,
7
+ loadEffectiveGSDPreferences,
8
+ loadProjectGSDPreferences,
9
+ } from "./preferences.js";
10
+ import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
11
+
12
+ function extractBodyAfterFrontmatter(content: string): string | null {
13
+ const start = content.startsWith("---\n") ? 4 : content.startsWith("---\r\n") ? 5 : -1;
14
+ if (start === -1) return null;
15
+ const closingIdx = content.indexOf("\n---", start);
16
+ if (closingIdx === -1) return null;
17
+ const after = content.slice(closingIdx + 4);
18
+ return after.trim() ? after : null;
19
+ }
20
+
21
+ async function writeProjectCmuxPreferences(
22
+ ctx: ExtensionCommandContext,
23
+ updater: (prefs: Record<string, unknown>) => void,
24
+ ): Promise<void> {
25
+ const path = getProjectGSDPreferencesPath();
26
+ await ensurePreferencesFile(path, ctx, "project");
27
+
28
+ const existing = loadProjectGSDPreferences();
29
+ const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : { version: 1 };
30
+ updater(prefs);
31
+ prefs.version = prefs.version || 1;
32
+
33
+ const frontmatter = serializePreferencesToFrontmatter(prefs);
34
+ let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
35
+ if (existsSync(path)) {
36
+ const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
37
+ if (preserved) body = preserved;
38
+ }
39
+
40
+ await saveFile(path, `---\n${frontmatter}---${body}`);
41
+ await ctx.waitForIdle();
42
+ await ctx.reload();
43
+ }
44
+
45
+ function formatCmuxStatus(): string {
46
+ const loaded = loadEffectiveGSDPreferences();
47
+ const detected = detectCmuxEnvironment();
48
+ const resolved = resolveCmuxConfig(loaded?.preferences);
49
+ const capabilities = new CmuxClient(resolved).getCapabilities() as Record<string, unknown> | null;
50
+ const accessMode = typeof capabilities?.mode === "string"
51
+ ? capabilities.mode
52
+ : typeof capabilities?.access_mode === "string"
53
+ ? capabilities.access_mode
54
+ : "unknown";
55
+ const methods = Array.isArray(capabilities?.methods) ? capabilities.methods.length : 0;
56
+
57
+ return [
58
+ "cmux status",
59
+ "",
60
+ `Detected: ${detected.available ? "yes" : "no"}`,
61
+ `Enabled: ${resolved.enabled ? "yes" : "no"}`,
62
+ `CLI available: ${detected.cliAvailable ? "yes" : "no"}`,
63
+ `Socket: ${detected.socketPath}`,
64
+ `Workspace: ${detected.workspaceId ?? "(none)"}`,
65
+ `Surface: ${detected.surfaceId ?? "(none)"}`,
66
+ `Features: notifications=${resolved.notifications ? "on" : "off"}, sidebar=${resolved.sidebar ? "on" : "off"}, splits=${resolved.splits ? "on" : "off"}, browser=${resolved.browser ? "on" : "off"}`,
67
+ `Capabilities: access=${accessMode}, methods=${methods}`,
68
+ ].join("\n");
69
+ }
70
+
71
+ function ensureCmuxAvailableForEnable(ctx: ExtensionCommandContext): boolean {
72
+ const detected = detectCmuxEnvironment();
73
+ if (detected.available) return true;
74
+ ctx.ui.notify(
75
+ "cmux not detected. Install it from https://cmux.com and run gsd inside a cmux terminal.",
76
+ "warning",
77
+ );
78
+ return false;
79
+ }
80
+
81
+ export async function handleCmux(args: string, ctx: ExtensionCommandContext): Promise<void> {
82
+ const trimmed = args.trim();
83
+ if (!trimmed || trimmed === "status") {
84
+ ctx.ui.notify(formatCmuxStatus(), "info");
85
+ return;
86
+ }
87
+
88
+ if (trimmed === "on") {
89
+ if (!ensureCmuxAvailableForEnable(ctx)) return;
90
+ await writeProjectCmuxPreferences(ctx, (prefs) => {
91
+ prefs.cmux = {
92
+ enabled: true,
93
+ notifications: true,
94
+ sidebar: true,
95
+ splits: false,
96
+ browser: false,
97
+ ...((prefs.cmux as Record<string, unknown> | undefined) ?? {}),
98
+ };
99
+ (prefs.cmux as Record<string, unknown>).enabled = true;
100
+ });
101
+ ctx.ui.notify("cmux integration enabled in project preferences.", "info");
102
+ return;
103
+ }
104
+
105
+ if (trimmed === "off") {
106
+ const effective = loadEffectiveGSDPreferences()?.preferences;
107
+ await writeProjectCmuxPreferences(ctx, (prefs) => {
108
+ prefs.cmux = { ...((prefs.cmux as Record<string, unknown> | undefined) ?? {}), enabled: false };
109
+ });
110
+ clearCmuxSidebar(effective);
111
+ ctx.ui.notify("cmux integration disabled in project preferences.", "info");
112
+ return;
113
+ }
114
+
115
+ const parts = trimmed.split(/\s+/);
116
+ if (parts.length === 2 && ["notifications", "sidebar", "splits", "browser"].includes(parts[0]) && ["on", "off"].includes(parts[1])) {
117
+ const feature = parts[0] as "notifications" | "sidebar" | "splits" | "browser";
118
+ const enabled = parts[1] === "on";
119
+ if (enabled && !ensureCmuxAvailableForEnable(ctx)) return;
120
+
121
+ await writeProjectCmuxPreferences(ctx, (prefs) => {
122
+ const next = { ...((prefs.cmux as Record<string, unknown> | undefined) ?? {}) };
123
+ next[feature] = enabled;
124
+ if (enabled) next.enabled = true;
125
+ prefs.cmux = next;
126
+ });
127
+
128
+ if (!enabled && feature === "sidebar") {
129
+ clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
130
+ }
131
+
132
+ const note = feature === "browser" && enabled
133
+ ? " Browser surfaces are still a follow-up path."
134
+ : "";
135
+ ctx.ui.notify(`cmux ${feature} ${enabled ? "enabled" : "disabled"}.${note}`, "info");
136
+ return;
137
+ }
138
+
139
+ ctx.ui.notify(
140
+ "Usage: /gsd cmux <status|on|off|notifications on|notifications off|sidebar on|sidebar off|splits on|splits off|browser on|browser off>",
141
+ "info",
142
+ );
143
+ }
@@ -740,7 +740,7 @@ export function serializePreferencesToFrontmatter(prefs: Record<string, unknown>
740
740
  "skill_rules", "custom_instructions", "models", "skill_discovery",
741
741
  "skill_staleness_days", "auto_supervisor", "uat_dispatch", "unique_milestone_ids",
742
742
  "budget_ceiling", "budget_enforcement", "context_pause_threshold",
743
- "notifications", "remote_questions", "git",
743
+ "notifications", "cmux", "remote_questions", "git",
744
744
  "post_unit_hooks", "pre_dispatch_hooks",
745
745
  "dynamic_routing", "token_profile", "phases", "parallel",
746
746
  "auto_visualize", "auto_report",
@@ -49,6 +49,7 @@ import { runEnvironmentChecks } from "./doctor-environment.js";
49
49
  import { handleLogs } from "./commands-logs.js";
50
50
  import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
51
51
  import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
52
+ import { handleCmux } from "./commands-cmux.js";
52
53
 
53
54
 
54
55
  /** Resolve the effective project root, accounting for worktree paths. */
@@ -105,7 +106,7 @@ function notifyRemoteAutoActive(ctx: ExtensionCommandContext, basePath: string):
105
106
 
106
107
  export function registerGSDCommand(pi: ExtensionAPI): void {
107
108
  pi.registerCommand("gsd", {
108
- 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",
109
+ 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",
109
110
  getArgumentCompletions: (prefix: string) => {
110
111
  const subcommands = [
111
112
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -114,6 +115,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
114
115
  { cmd: "stop", desc: "Stop auto mode gracefully" },
115
116
  { cmd: "pause", desc: "Pause auto-mode (preserves state, /gsd auto to resume)" },
116
117
  { cmd: "status", desc: "Progress dashboard" },
118
+ { cmd: "widget", desc: "Cycle widget: full → small → min → off" },
117
119
  { cmd: "visualize", desc: "Open 10-tab workflow visualizer (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)" },
118
120
  { cmd: "queue", desc: "Queue and reorder future milestones" },
119
121
  { cmd: "quick", desc: "Execute a quick task without full planning overhead" },
@@ -147,6 +149,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
147
149
  { cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
148
150
  { cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
149
151
  { cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
152
+ { cmd: "cmux", desc: "Manage cmux integration (status, sidebar, notifications, splits)" },
150
153
  { cmd: "park", desc: "Park a milestone — skip without deleting" },
151
154
  { cmd: "unpark", desc: "Reactivate a parked milestone" },
152
155
  { cmd: "update", desc: "Update GSD to the latest version" },
@@ -203,6 +206,38 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
203
206
  .map((s) => ({ value: `parallel ${s.cmd}`, label: s.cmd, description: s.desc }));
204
207
  }
205
208
 
209
+ if (parts[0] === "cmux") {
210
+ if (parts.length <= 2) {
211
+ const subPrefix = parts[1] ?? "";
212
+ const subs = [
213
+ { cmd: "status", desc: "Show cmux detection, prefs, and capabilities" },
214
+ { cmd: "on", desc: "Enable cmux integration" },
215
+ { cmd: "off", desc: "Disable cmux integration" },
216
+ { cmd: "notifications", desc: "Toggle cmux desktop notifications" },
217
+ { cmd: "sidebar", desc: "Toggle cmux sidebar metadata" },
218
+ { cmd: "splits", desc: "Toggle cmux visual subagent splits" },
219
+ { cmd: "browser", desc: "Toggle future browser integration flag" },
220
+ ];
221
+ return subs
222
+ .filter((s) => s.cmd.startsWith(subPrefix))
223
+ .map((s) => ({ value: `cmux ${s.cmd}`, label: s.cmd, description: s.desc }));
224
+ }
225
+
226
+ if (parts.length <= 3 && ["notifications", "sidebar", "splits", "browser"].includes(parts[1])) {
227
+ const togglePrefix = parts[2] ?? "";
228
+ return [
229
+ { cmd: "on", desc: "Enable this cmux area" },
230
+ { cmd: "off", desc: "Disable this cmux area" },
231
+ ]
232
+ .filter((item) => item.cmd.startsWith(togglePrefix))
233
+ .map((item) => ({
234
+ value: `cmux ${parts[1]} ${item.cmd}`,
235
+ label: item.cmd,
236
+ description: item.desc,
237
+ }));
238
+ }
239
+ }
240
+
206
241
  if (parts[0] === "setup" && parts.length <= 2) {
207
242
  const subPrefix = parts[1] ?? "";
208
243
  const subs = [
@@ -474,6 +509,18 @@ export async function handleGSDCommand(
474
509
  return;
475
510
  }
476
511
 
512
+ if (trimmed === "widget" || trimmed.startsWith("widget ")) {
513
+ const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
514
+ const arg = trimmed.replace(/^widget\s*/, "").trim();
515
+ if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
516
+ setWidgetMode(arg);
517
+ } else {
518
+ cycleWidgetMode();
519
+ }
520
+ ctx.ui.notify(`Widget: ${getWidgetMode()}`, "info");
521
+ return;
522
+ }
523
+
477
524
  if (trimmed === "visualize") {
478
525
  await handleVisualize(ctx);
479
526
  return;
@@ -493,6 +540,11 @@ export async function handleGSDCommand(
493
540
  return;
494
541
  }
495
542
 
543
+ if (trimmed === "cmux" || trimmed.startsWith("cmux ")) {
544
+ await handleCmux(trimmed.replace(/^cmux\s*/, "").trim(), ctx);
545
+ return;
546
+ }
547
+
496
548
  if (trimmed === "init") {
497
549
  const { detectProjectState } = await import("./detection.js");
498
550
  const { showProjectInit, handleReinit } = await import("./init-wizard.js");
@@ -996,6 +1048,7 @@ function showHelp(ctx: ExtensionCommandContext): void {
996
1048
  " /gsd setup Global setup status [llm|search|remote|keys|prefs]",
997
1049
  " /gsd mode Set workflow mode (solo/team) [global|project]",
998
1050
  " /gsd prefs Manage preferences [global|project|status|wizard|setup|import-claude]",
1051
+ " /gsd cmux Manage cmux integration [status|on|off|notifications|sidebar|splits|browser]",
999
1052
  " /gsd config Set API keys for external tools",
1000
1053
  " /gsd keys API key manager [list|add|remove|test|rotate|doctor]",
1001
1054
  " /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
@@ -479,9 +479,20 @@ export class GitServiceImpl {
479
479
 
480
480
  const wtName = detectWorktreeName(this.basePath);
481
481
  if (wtName) {
482
+ // Auto-mode worktrees use milestone/<MID> branches (wtName = milestone ID)
483
+ const milestoneBranch = `milestone/${wtName}`;
484
+ const currentBranch = nativeGetCurrentBranch(this.basePath);
485
+
486
+ // If we're on a milestone/<MID> branch, use it (auto-mode case)
487
+ if (currentBranch.startsWith("milestone/")) {
488
+ return currentBranch;
489
+ }
490
+
491
+ // Otherwise check for manual worktree branch (worktree/<name>)
482
492
  const wtBranch = `worktree/${wtName}`;
483
493
  if (nativeBranchExists(this.basePath, wtBranch)) return wtBranch;
484
- return nativeGetCurrentBranch(this.basePath);
494
+
495
+ return currentBranch;
485
496
  }
486
497
 
487
498
  // Repo-level default detection: origin/HEAD → main → master → current branch.
@@ -2,7 +2,8 @@
2
2
  // Human-readable display of past auto-mode unit executions.
3
3
 
4
4
  import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
5
- import { formatDuration, padRight, truncateWithEllipsis } from "../shared/format-utils.js";
5
+ import { formatDuration, truncateWithEllipsis } from "../shared/format-utils.js";
6
+ import { padRight } from "../shared/layout-utils.js";
6
7
  import {
7
8
  getLedger, getProjectTotals, formatCost, formatTokenCount,
8
9
  aggregateBySlice, aggregateByPhase, aggregateByModel, loadLedgerFromDisk,
@@ -65,6 +65,7 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
65
65
  import { toPosixPath } from "../shared/mod.js";
66
66
  import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
67
67
  import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
68
+ import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js";
68
69
 
69
70
  // ── Agent Instructions (DEPRECATED) ──────────────────────────────────────
70
71
  // agent-instructions.md is deprecated. Use AGENTS.md or CLAUDE.md instead.
@@ -623,6 +624,13 @@ export default function (pi: ExtensionAPI) {
623
624
  const stopContextTimer = debugTime("context-inject");
624
625
  const systemContent = loadPrompt("system");
625
626
  const loadedPreferences = loadEffectiveGSDPreferences();
627
+ if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) {
628
+ markCmuxPromptShown();
629
+ ctx.ui.notify(
630
+ "cmux detected. Run /gsd cmux on to enable sidebar metadata, notifications, and visual subagent splits for this project.",
631
+ "info",
632
+ );
633
+ }
626
634
  let preferenceBlock = "";
627
635
  if (loadedPreferences) {
628
636
  const cwd = process.cwd();