gsd-pi 2.36.0-dev.f887f4e → 2.37.0-dev.8acfc31

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
@@ -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";
@@ -248,6 +249,7 @@ function handleLostSessionLock(ctx) {
248
249
  s.paused = false;
249
250
  clearUnitTimeout();
250
251
  deregisterSigtermHandler();
252
+ clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
251
253
  ctx?.ui.notify("Session lock lost — another GSD process appears to have taken over. Stopping gracefully.", "error");
252
254
  ctx?.ui.setStatus("gsd-auto", undefined);
253
255
  ctx?.ui.setWidget("gsd-progress", undefined);
@@ -256,6 +258,7 @@ function handleLostSessionLock(ctx) {
256
258
  export async function stopAuto(ctx, pi, reason) {
257
259
  if (!s.active && !s.paused)
258
260
  return;
261
+ const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
259
262
  const reasonSuffix = reason ? ` — ${reason}` : "";
260
263
  clearUnitTimeout();
261
264
  if (lockBase())
@@ -314,6 +317,8 @@ export async function stopAuto(ctx, pi, reason) {
314
317
  });
315
318
  }
316
319
  }
320
+ clearCmuxSidebar(loadedPreferences);
321
+ logCmuxEvent(loadedPreferences, `Auto-mode stopped${reasonSuffix || ""}.`, reason?.startsWith("Blocked:") ? "warning" : "info");
317
322
  if (isDebugEnabled()) {
318
323
  const logPath = writeDebugSummary();
319
324
  if (logPath) {
@@ -455,6 +460,8 @@ function buildLoopDeps() {
455
460
  pauseAuto,
456
461
  clearUnitTimeout,
457
462
  updateProgressWidget,
463
+ syncCmuxSidebar,
464
+ logCmuxEvent,
458
465
  // State and cache
459
466
  invalidateAllCaches,
460
467
  deriveState,
@@ -605,6 +612,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
605
612
  restoreHookState(s.basePath);
606
613
  try {
607
614
  await rebuildState(s.basePath);
615
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
608
616
  }
609
617
  catch (e) {
610
618
  debugLog("resume-rebuild-state-failed", {
@@ -634,6 +642,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
634
642
  }
635
643
  updateSessionLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
636
644
  writeLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
645
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
637
646
  await autoLoop(ctx, pi, s, buildLoopDeps());
638
647
  return;
639
648
  }
@@ -647,6 +656,13 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
647
656
  const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps);
648
657
  if (!ready)
649
658
  return;
659
+ try {
660
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
661
+ }
662
+ catch {
663
+ // Best-effort only — sidebar sync must never block auto-mode startup
664
+ }
665
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
650
666
  // Dispatch the first unit
651
667
  await autoLoop(ctx, pi, s, buildLoopDeps());
652
668
  }
@@ -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
@@ -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();
@@ -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") {
@@ -174,6 +174,9 @@ function mergePreferences(base, override) {
174
174
  notifications: (base.notifications || override.notifications)
175
175
  ? { ...(base.notifications ?? {}), ...(override.notifications ?? {}) }
176
176
  : undefined,
177
+ cmux: (base.cmux || override.cmux)
178
+ ? { ...(base.cmux ?? {}), ...(override.cmux ?? {}) }
179
+ : undefined,
177
180
  remote_questions: override.remote_questions
178
181
  ? { ...(base.remote_questions ?? {}), ...override.remote_questions }
179
182
  : 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
 
@@ -46,8 +46,9 @@ Research what this slice needs. Narrate key findings and surprises as you go —
46
46
  2. **Skill Discovery ({{skillDiscoveryMode}}):**{{skillDiscoveryInstructions}}
47
47
  3. Explore relevant code for this slice's scope. For targeted exploration, use `rg`, `find`, and reads. For broad or unfamiliar subsystems, use `scout` to map the relevant area first.
48
48
  4. Use `resolve_library` / `get_library_docs` for unfamiliar libraries — skip this for libraries already used in the codebase
49
- 5. Use the **Research** output template from the inlined context aboveinclude only sections that have real content. The template is already inlined above; do NOT attempt to read any template file from disk (there is no `templates/SLICE-RESEARCH.md` the correct template is already present in this prompt).
50
- 6. Write `{{outputPath}}`
49
+ 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.
50
+ 6. Use the **Research** output template from the inlined context above — include only sections that have real content. The template is already inlined above; do NOT attempt to read any template file from disk (there is no `templates/SLICE-RESEARCH.md` — the correct template is already present in this prompt).
51
+ 7. Write `{{outputPath}}`
51
52
 
52
53
  The slice directory already exists at `{{slicePath}}/`. Do NOT mkdir — just write the file.
53
54
 
@@ -57,6 +57,12 @@ notifications:
57
57
  on_budget:
58
58
  on_milestone:
59
59
  on_attention:
60
+ cmux:
61
+ enabled:
62
+ notifications:
63
+ sidebar:
64
+ splits:
65
+ browser:
60
66
  remote_questions:
61
67
  channel:
62
68
  channel_id:
@@ -11,6 +11,15 @@ export const BRAVE_TOOL_NAMES = ["search-the-web", "search_and_read"];
11
11
  export const CUSTOM_SEARCH_TOOL_NAMES = ["search-the-web", "search_and_read", "google_search"];
12
12
  /** Thinking block types that require signature validation by the API */
13
13
  const THINKING_TYPES = new Set(["thinking", "redacted_thinking"]);
14
+ /**
15
+ * Maximum number of native web searches allowed per session (agent unit).
16
+ * The Anthropic API's `max_uses` is per-request — it resets on each API call.
17
+ * When `pause_turn` triggers a resubmit, the model gets a fresh budget.
18
+ * This session-level cap prevents unbounded search accumulation (#1309).
19
+ *
20
+ * 15 = 3 full turns of 5 searches each — generous for research, but bounded.
21
+ */
22
+ export const MAX_NATIVE_SEARCHES_PER_SESSION = 15;
14
23
  /** When true, skip native web search injection and keep Brave/custom tools active on Anthropic. */
15
24
  export function preferBraveSearch() {
16
25
  // preferences.md takes priority over env var
@@ -57,6 +66,10 @@ export function stripThinkingFromHistory(messages) {
57
66
  export function registerNativeSearchHooks(pi) {
58
67
  let isAnthropicProvider = false;
59
68
  let modelSelectFired = false;
69
+ // Session-level native search counter (#1309).
70
+ // Tracks cumulative web_search_tool_result blocks across all turns in a session.
71
+ // Reset on session_start. Used to compute remaining budget for max_uses.
72
+ let sessionSearchCount = 0;
60
73
  // Track provider changes via model selection — also handles diagnostics
61
74
  // since model_select fires AFTER session_start and knows the provider.
62
75
  pi.on("model_select", async (event, ctx) => {
@@ -135,18 +148,46 @@ export function registerNativeSearchHooks(pi) {
135
148
  // the model and causes it to pick custom tools which can fail with network errors.
136
149
  tools = tools.filter((t) => !CUSTOM_SEARCH_TOOL_NAMES.includes(t.name));
137
150
  payload.tools = tools;
151
+ // ── Session-level search budget (#1309) ──────────────────────────────
152
+ // Count web_search_tool_result blocks in the conversation history to
153
+ // determine how many native searches have already been used this session.
154
+ // The Anthropic API's max_uses resets per request, so without this guard,
155
+ // pause_turn → resubmit cycles allow unlimited total searches.
156
+ if (Array.isArray(messages)) {
157
+ let historySearchCount = 0;
158
+ for (const msg of messages) {
159
+ const content = msg.content;
160
+ if (!Array.isArray(content))
161
+ continue;
162
+ for (const block of content) {
163
+ if (block?.type === "web_search_tool_result") {
164
+ historySearchCount++;
165
+ }
166
+ }
167
+ }
168
+ // Sync counter from history (handles session restore / context replay)
169
+ sessionSearchCount = historySearchCount;
170
+ }
171
+ const remaining = Math.max(0, MAX_NATIVE_SEARCHES_PER_SESSION - sessionSearchCount);
172
+ if (remaining <= 0) {
173
+ // Budget exhausted — don't inject the search tool at all.
174
+ // The model will proceed without web search capability.
175
+ return payload;
176
+ }
138
177
  tools.push({
139
178
  type: "web_search_20250305",
140
179
  name: "web_search",
141
- // Cap server-side searches per response to prevent the model from
142
- // looping on web_search without synthesizing results (#817).
143
- // 5 searches is generous most queries need 1-2.
144
- max_uses: 5,
180
+ // Cap per-request searches to the lesser of 5 (per-turn cap) or the
181
+ // remaining session budget (#1309). This prevents the model from
182
+ // consuming unlimited searches via pause_turn resubmit cycles.
183
+ max_uses: Math.min(5, remaining),
145
184
  });
146
185
  return payload;
147
186
  });
148
187
  // Basic startup diagnostics — provider-specific info comes from model_select
149
188
  pi.on("session_start", async (_event, ctx) => {
189
+ // Reset session-level search budget (#1309)
190
+ sessionSearchCount = 0;
150
191
  const hasBrave = !!process.env.BRAVE_API_KEY;
151
192
  const hasJina = !!process.env.JINA_API_KEY;
152
193
  const hasAnswers = !!process.env.BRAVE_ANSWERS_KEY;
@@ -5,9 +5,14 @@
5
5
  * Terminals that lack this support silently swallow the key combos.
6
6
  */
7
7
  const UNSUPPORTED_TERMS = ["apple_terminal", "warpterm"];
8
+ export function isCmuxTerminal(env = process.env) {
9
+ return Boolean(env.CMUX_WORKSPACE_ID && env.CMUX_SURFACE_ID);
10
+ }
8
11
  export function supportsCtrlAltShortcuts() {
9
12
  const term = (process.env.TERM_PROGRAM || "").toLowerCase();
10
13
  const jetbrains = (process.env.TERMINAL_EMULATOR || "").toLowerCase().includes("jetbrains");
14
+ if (isCmuxTerminal())
15
+ return true;
11
16
  return !UNSUPPORTED_TERMS.some((t) => term.includes(t)) && !jetbrains;
12
17
  }
13
18
  /**