gsd-pi 2.36.0-dev.f887f4e → 2.37.0

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 (48) hide show
  1. package/dist/resources/extensions/cmux/index.js +321 -0
  2. package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
  3. package/dist/resources/extensions/gsd/auto-loop.js +11 -0
  4. package/dist/resources/extensions/gsd/auto.js +16 -0
  5. package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
  6. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  7. package/dist/resources/extensions/gsd/commands.js +51 -1
  8. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  9. package/dist/resources/extensions/gsd/index.js +5 -0
  10. package/dist/resources/extensions/gsd/notifications.js +10 -1
  11. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  12. package/dist/resources/extensions/gsd/preferences-validation.js +29 -0
  13. package/dist/resources/extensions/gsd/preferences.js +3 -0
  14. package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  15. package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
  16. package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
  17. package/dist/resources/extensions/search-the-web/native-search.js +45 -4
  18. package/dist/resources/extensions/shared/terminal.js +5 -0
  19. package/dist/resources/extensions/subagent/index.js +180 -60
  20. package/package.json +1 -1
  21. package/packages/pi-coding-agent/package.json +1 -1
  22. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  23. package/packages/pi-tui/dist/terminal-image.js +4 -0
  24. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  25. package/packages/pi-tui/src/terminal-image.ts +5 -0
  26. package/pkg/package.json +1 -1
  27. package/src/resources/extensions/cmux/index.ts +384 -0
  28. package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
  29. package/src/resources/extensions/gsd/auto-loop.ts +42 -0
  30. package/src/resources/extensions/gsd/auto.ts +21 -0
  31. package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
  32. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  33. package/src/resources/extensions/gsd/commands.ts +54 -1
  34. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  35. package/src/resources/extensions/gsd/index.ts +8 -0
  36. package/src/resources/extensions/gsd/notifications.ts +10 -1
  37. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  38. package/src/resources/extensions/gsd/preferences-validation.ts +26 -0
  39. package/src/resources/extensions/gsd/preferences.ts +4 -0
  40. package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  41. package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
  42. package/src/resources/extensions/gsd/templates/preferences.md +6 -0
  43. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  44. package/src/resources/extensions/gsd/tests/cmux.test.ts +98 -0
  45. package/src/resources/extensions/gsd/tests/preferences.test.ts +23 -0
  46. package/src/resources/extensions/search-the-web/native-search.ts +50 -4
  47. package/src/resources/extensions/shared/terminal.ts +5 -0
  48. package/src/resources/extensions/subagent/index.ts +236 -79
@@ -25,6 +25,7 @@ import type {
25
25
  import type { DispatchAction } from "./auto-dispatch.js";
26
26
  import type { WorktreeResolver } from "./worktree-resolver.js";
27
27
  import { debugLog } from "./debug-logger.js";
28
+ import type { CmuxLogLevel } from "../cmux/index.js";
28
29
 
29
30
  /**
30
31
  * Maximum total loop iterations before forced stop. Prevents runaway loops
@@ -276,6 +277,12 @@ export interface LoopDeps {
276
277
  unitId: string,
277
278
  state: GSDState,
278
279
  ) => void;
280
+ syncCmuxSidebar: (preferences: GSDPreferences | undefined, state: GSDState) => void;
281
+ logCmuxEvent: (
282
+ preferences: GSDPreferences | undefined,
283
+ message: string,
284
+ level?: CmuxLogLevel,
285
+ ) => void;
279
286
 
280
287
  // State and cache functions
281
288
  invalidateAllCaches: () => void;
@@ -609,6 +616,7 @@ export async function autoLoop(
609
616
 
610
617
  // Derive state
611
618
  let state = await deps.deriveState(s.basePath);
619
+ deps.syncCmuxSidebar(deps.loadEffectiveGSDPreferences()?.preferences, state);
612
620
  let mid = state.activeMilestone?.id;
613
621
  let midTitle = state.activeMilestone?.title;
614
622
  debugLog("autoLoop", {
@@ -630,6 +638,11 @@ export async function autoLoop(
630
638
  "success",
631
639
  "milestone",
632
640
  );
641
+ deps.logCmuxEvent(
642
+ deps.loadEffectiveGSDPreferences()?.preferences,
643
+ `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
644
+ "success",
645
+ );
633
646
 
634
647
  const vizPrefs = deps.loadEffectiveGSDPreferences()?.preferences;
635
648
  if (vizPrefs?.auto_visualize) {
@@ -767,12 +780,18 @@ export async function autoLoop(
767
780
  "success",
768
781
  "milestone",
769
782
  );
783
+ deps.logCmuxEvent(
784
+ deps.loadEffectiveGSDPreferences()?.preferences,
785
+ "All milestones complete.",
786
+ "success",
787
+ );
770
788
  await deps.stopAuto(ctx, pi, "All milestones complete");
771
789
  } else if (state.phase === "blocked") {
772
790
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
773
791
  await deps.stopAuto(ctx, pi, blockerMsg);
774
792
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
775
793
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
794
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
776
795
  } else {
777
796
  const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
778
797
  const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
@@ -850,6 +869,11 @@ export async function autoLoop(
850
869
  "success",
851
870
  "milestone",
852
871
  );
872
+ deps.logCmuxEvent(
873
+ deps.loadEffectiveGSDPreferences()?.preferences,
874
+ `Milestone ${mid} complete.`,
875
+ "success",
876
+ );
853
877
  await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
854
878
  debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
855
879
  break;
@@ -871,6 +895,7 @@ export async function autoLoop(
871
895
  await deps.stopAuto(ctx, pi, blockerMsg);
872
896
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
873
897
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
898
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
874
899
  debugLog("autoLoop", { phase: "exit", reason: "blocked" });
875
900
  break;
876
901
  }
@@ -914,12 +939,14 @@ export async function autoLoop(
914
939
  "warning",
915
940
  );
916
941
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
942
+ deps.logCmuxEvent(prefs, msg, "warning");
917
943
  await deps.pauseAuto(ctx, pi);
918
944
  debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
919
945
  break;
920
946
  }
921
947
  ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
922
948
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
949
+ deps.logCmuxEvent(prefs, msg, "warning");
923
950
  } else if (newBudgetAlertLevel === 90) {
924
951
  s.lastBudgetAlertLevel =
925
952
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
@@ -933,6 +960,11 @@ export async function autoLoop(
933
960
  "warning",
934
961
  "budget",
935
962
  );
963
+ deps.logCmuxEvent(
964
+ prefs,
965
+ `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
966
+ "warning",
967
+ );
936
968
  } else if (newBudgetAlertLevel === 80) {
937
969
  s.lastBudgetAlertLevel =
938
970
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
@@ -946,6 +978,11 @@ export async function autoLoop(
946
978
  "warning",
947
979
  "budget",
948
980
  );
981
+ deps.logCmuxEvent(
982
+ prefs,
983
+ `Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
984
+ "warning",
985
+ );
949
986
  } else if (newBudgetAlertLevel === 75) {
950
987
  s.lastBudgetAlertLevel =
951
988
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
@@ -959,6 +996,11 @@ export async function autoLoop(
959
996
  "info",
960
997
  "budget",
961
998
  );
999
+ deps.logCmuxEvent(
1000
+ prefs,
1001
+ `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
1002
+ "progress",
1003
+ );
962
1004
  } else if (budgetAlertLevel === 0) {
963
1005
  s.lastBudgetAlertLevel = 0;
964
1006
  }
@@ -184,6 +184,7 @@ import {
184
184
  } from "./auto-supervisor.js";
185
185
  import { isDbAvailable } from "./gsd-db.js";
186
186
  import { countPendingCaptures } from "./captures.js";
187
+ import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
187
188
 
188
189
  // ── Extracted modules ──────────────────────────────────────────────────────
189
190
  import { startUnitSupervision } from "./auto-timers.js";
@@ -466,6 +467,7 @@ function handleLostSessionLock(ctx?: ExtensionContext): void {
466
467
  s.paused = false;
467
468
  clearUnitTimeout();
468
469
  deregisterSigtermHandler();
470
+ clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
469
471
  ctx?.ui.notify(
470
472
  "Session lock lost — another GSD process appears to have taken over. Stopping gracefully.",
471
473
  "error",
@@ -481,6 +483,7 @@ export async function stopAuto(
481
483
  reason?: string,
482
484
  ): Promise<void> {
483
485
  if (!s.active && !s.paused) return;
486
+ const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
484
487
  const reasonSuffix = reason ? ` — ${reason}` : "";
485
488
  clearUnitTimeout();
486
489
  if (lockBase()) clearLock(lockBase());
@@ -543,6 +546,13 @@ export async function stopAuto(
543
546
  }
544
547
  }
545
548
 
549
+ clearCmuxSidebar(loadedPreferences);
550
+ logCmuxEvent(
551
+ loadedPreferences,
552
+ `Auto-mode stopped${reasonSuffix || ""}.`,
553
+ reason?.startsWith("Blocked:") ? "warning" : "info",
554
+ );
555
+
546
556
  if (isDebugEnabled()) {
547
557
  const logPath = writeDebugSummary();
548
558
  if (logPath) {
@@ -708,6 +718,8 @@ function buildLoopDeps(): LoopDeps {
708
718
  pauseAuto,
709
719
  clearUnitTimeout,
710
720
  updateProgressWidget,
721
+ syncCmuxSidebar,
722
+ logCmuxEvent,
711
723
 
712
724
  // State and cache
713
725
  invalidateAllCaches,
@@ -890,6 +902,7 @@ export async function startAuto(
890
902
  restoreHookState(s.basePath);
891
903
  try {
892
904
  await rebuildState(s.basePath);
905
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
893
906
  } catch (e) {
894
907
  debugLog("resume-rebuild-state-failed", {
895
908
  error: e instanceof Error ? e.message : String(e),
@@ -941,6 +954,7 @@ export async function startAuto(
941
954
  s.currentMilestoneId ?? "unknown",
942
955
  s.completedUnits.length,
943
956
  );
957
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
944
958
 
945
959
  await autoLoop(ctx, pi, s, buildLoopDeps());
946
960
  return;
@@ -965,6 +979,13 @@ export async function startAuto(
965
979
  );
966
980
  if (!ready) return;
967
981
 
982
+ try {
983
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
984
+ } catch {
985
+ // Best-effort only — sidebar sync must never block auto-mode startup
986
+ }
987
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
988
+
968
989
  // Dispatch the first unit
969
990
  await autoLoop(ctx, pi, s, buildLoopDeps());
970
991
  }
@@ -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
@@ -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();
@@ -4,6 +4,7 @@
4
4
  import { execFileSync } from "node:child_process";
5
5
  import type { NotificationPreferences } from "./types.js";
6
6
  import { loadEffectiveGSDPreferences } from "./preferences.js";
7
+ import { CmuxClient, emitOsc777Notification, resolveCmuxConfig } from "../cmux/index.js";
7
8
 
8
9
  export type NotifyLevel = "info" | "success" | "warning" | "error";
9
10
  export type NotificationKind = "complete" | "error" | "budget" | "milestone" | "attention";
@@ -23,7 +24,15 @@ export function sendDesktopNotification(
23
24
  level: NotifyLevel = "info",
24
25
  kind: NotificationKind = "complete",
25
26
  ): void {
26
- if (!shouldSendDesktopNotification(kind)) return;
27
+ const loaded = loadEffectiveGSDPreferences()?.preferences;
28
+ if (!shouldSendDesktopNotification(kind, loaded?.notifications)) return;
29
+
30
+ const cmux = resolveCmuxConfig(loaded);
31
+ if (cmux.notifications) {
32
+ const delivered = CmuxClient.fromPreferences(loaded).notify(title, message);
33
+ if (delivered) return;
34
+ emitOsc777Notification(title, message);
35
+ }
27
36
 
28
37
  try {
29
38
  const command = buildDesktopNotificationCommand(process.platform, title, message, level);
@@ -68,6 +68,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
68
68
  "budget_enforcement",
69
69
  "context_pause_threshold",
70
70
  "notifications",
71
+ "cmux",
71
72
  "remote_questions",
72
73
  "git",
73
74
  "post_unit_hooks",
@@ -84,6 +85,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
84
85
  "search_provider",
85
86
  "compression_strategy",
86
87
  "context_selection",
88
+ "widget_mode",
87
89
  ]);
88
90
 
89
91
  /** Canonical list of all dispatch unit types. */
@@ -164,6 +166,14 @@ export interface RemoteQuestionsConfig {
164
166
  poll_interval_seconds?: number; // clamped to 2-30
165
167
  }
166
168
 
169
+ export interface CmuxPreferences {
170
+ enabled?: boolean;
171
+ notifications?: boolean;
172
+ sidebar?: boolean;
173
+ splits?: boolean;
174
+ browser?: boolean;
175
+ }
176
+
167
177
  export interface GSDPreferences {
168
178
  version?: number;
169
179
  mode?: WorkflowMode;
@@ -182,6 +192,7 @@ export interface GSDPreferences {
182
192
  budget_enforcement?: BudgetEnforcementMode;
183
193
  context_pause_threshold?: number;
184
194
  notifications?: NotificationPreferences;
195
+ cmux?: CmuxPreferences;
185
196
  remote_questions?: RemoteQuestionsConfig;
186
197
  git?: GitPreferences;
187
198
  post_unit_hooks?: PostUnitHookConfig[];
@@ -202,6 +213,8 @@ export interface GSDPreferences {
202
213
  compression_strategy?: CompressionStrategy;
203
214
  /** Context selection mode for file inlining. "full" inlines entire files, "smart" uses semantic chunking. Default derived from token profile. */
204
215
  context_selection?: ContextSelectionMode;
216
+ /** Default widget display mode for auto-mode dashboard. "full" | "small" | "min" | "off". Default: "full". */
217
+ widget_mode?: "full" | "small" | "min" | "off";
205
218
  }
206
219
 
207
220
  export interface LoadedGSDPreferences {
@@ -242,6 +242,32 @@ export function validatePreferences(preferences: GSDPreferences): {
242
242
  }
243
243
  }
244
244
 
245
+ // ─── Cmux ───────────────────────────────────────────────────────────────
246
+ if (preferences.cmux !== undefined) {
247
+ if (preferences.cmux && typeof preferences.cmux === "object") {
248
+ const cmux = preferences.cmux as Record<string, unknown>;
249
+ const validatedCmux: NonNullable<GSDPreferences["cmux"]> = {};
250
+ if (cmux.enabled !== undefined) validatedCmux.enabled = !!cmux.enabled;
251
+ if (cmux.notifications !== undefined) validatedCmux.notifications = !!cmux.notifications;
252
+ if (cmux.sidebar !== undefined) validatedCmux.sidebar = !!cmux.sidebar;
253
+ if (cmux.splits !== undefined) validatedCmux.splits = !!cmux.splits;
254
+ if (cmux.browser !== undefined) validatedCmux.browser = !!cmux.browser;
255
+
256
+ const knownCmuxKeys = new Set(["enabled", "notifications", "sidebar", "splits", "browser"]);
257
+ for (const key of Object.keys(cmux)) {
258
+ if (!knownCmuxKeys.has(key)) {
259
+ warnings.push(`unknown cmux key "${key}" — ignored`);
260
+ }
261
+ }
262
+
263
+ if (Object.keys(validatedCmux).length > 0) {
264
+ validated.cmux = validatedCmux;
265
+ }
266
+ } else {
267
+ errors.push("cmux must be an object");
268
+ }
269
+ }
270
+
245
271
  // ─── Remote Questions ───────────────────────────────────────────────
246
272
  if (preferences.remote_questions !== undefined) {
247
273
  if (preferences.remote_questions && typeof preferences.remote_questions === "object") {
@@ -45,6 +45,7 @@ export type {
45
45
  SkillDiscoveryMode,
46
46
  AutoSupervisorConfig,
47
47
  RemoteQuestionsConfig,
48
+ CmuxPreferences,
48
49
  GSDPreferences,
49
50
  LoadedGSDPreferences,
50
51
  SkillResolution,
@@ -241,6 +242,9 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
241
242
  notifications: (base.notifications || override.notifications)
242
243
  ? { ...(base.notifications ?? {}), ...(override.notifications ?? {}) }
243
244
  : undefined,
245
+ cmux: (base.cmux || override.cmux)
246
+ ? { ...(base.cmux ?? {}), ...(override.cmux ?? {}) }
247
+ : undefined,
244
248
  remote_questions: override.remote_questions
245
249
  ? { ...(base.remote_questions ?? {}), ...override.remote_questions }
246
250
  : base.remote_questions,
@@ -25,9 +25,10 @@ Then research the codebase and relevant technologies. Narrate key findings and s
25
25
  2. **Skill Discovery ({{skillDiscoveryMode}}):**{{skillDiscoveryInstructions}}
26
26
  3. Explore relevant code. For small/familiar codebases, use `rg`, `find`, and targeted reads. For large or unfamiliar codebases, use `scout` to build a broad map efficiently before diving in.
27
27
  4. Use `resolve_library` / `get_library_docs` for unfamiliar libraries — skip this for libraries already used in the codebase
28
- 5. Use the **Research** output template from the inlined context above include only sections that have real content
29
- 6. If `.gsd/REQUIREMENTS.md` exists, research against it. Identify which Active requirements are table stakes, likely omissions, overbuilt risks, or domain-standard behaviors the user may or may not want.
30
- 7. Write `{{outputPath}}`
28
+ 5. **Web search budget:** You have a limited budget of web searches (max ~15 per session). Use them strategically prefer `resolve_library` / `get_library_docs` for library documentation. Do NOT repeat the same or similar queries. If a search didn't find what you need, rephrase once or move on. Target 3-5 total web searches for a typical research unit.
29
+ 6. Use the **Research** output template from the inlined context above include only sections that have real content
30
+ 7. If `.gsd/REQUIREMENTS.md` exists, research against it. Identify which Active requirements are table stakes, likely omissions, overbuilt risks, or domain-standard behaviors the user may or may not want.
31
+ 8. Write `{{outputPath}}`
31
32
 
32
33
  ## Strategic Questions to Answer
33
34