gsd-pi 2.36.0 → 2.37.0-dev.68605cd

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/resources/extensions/cmux/index.js +321 -0
  2. package/dist/resources/extensions/cmux/package.json +7 -0
  3. package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
  4. package/dist/resources/extensions/gsd/auto-loop.js +29 -4
  5. package/dist/resources/extensions/gsd/auto.js +58 -5
  6. package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
  7. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  8. package/dist/resources/extensions/gsd/commands.js +131 -34
  9. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  10. package/dist/resources/extensions/gsd/git-service.js +9 -1
  11. package/dist/resources/extensions/gsd/history.js +2 -1
  12. package/dist/resources/extensions/gsd/index.js +5 -0
  13. package/dist/resources/extensions/gsd/metrics.js +4 -2
  14. package/dist/resources/extensions/gsd/notifications.js +10 -1
  15. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  16. package/dist/resources/extensions/gsd/preferences-validation.js +29 -0
  17. package/dist/resources/extensions/gsd/preferences.js +3 -0
  18. package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  19. package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
  20. package/dist/resources/extensions/gsd/session-lock.js +26 -6
  21. package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
  22. package/dist/resources/extensions/search-the-web/native-search.js +45 -4
  23. package/dist/resources/extensions/shared/format-utils.js +5 -41
  24. package/dist/resources/extensions/shared/layout-utils.js +46 -0
  25. package/dist/resources/extensions/shared/mod.js +2 -1
  26. package/dist/resources/extensions/shared/terminal.js +5 -0
  27. package/dist/resources/extensions/subagent/index.js +180 -60
  28. package/package.json +1 -1
  29. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  30. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -4
  31. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  32. package/packages/pi-coding-agent/package.json +1 -1
  33. package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
  34. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  35. package/packages/pi-tui/dist/terminal-image.js +4 -0
  36. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  37. package/packages/pi-tui/src/terminal-image.ts +5 -0
  38. package/pkg/package.json +1 -1
  39. package/src/resources/extensions/cmux/index.ts +384 -0
  40. package/src/resources/extensions/cmux/package.json +7 -0
  41. package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
  42. package/src/resources/extensions/gsd/auto-loop.ts +66 -6
  43. package/src/resources/extensions/gsd/auto.ts +77 -5
  44. package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
  45. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  46. package/src/resources/extensions/gsd/commands.ts +139 -32
  47. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  48. package/src/resources/extensions/gsd/git-service.ts +12 -1
  49. package/src/resources/extensions/gsd/history.ts +2 -1
  50. package/src/resources/extensions/gsd/index.ts +8 -0
  51. package/src/resources/extensions/gsd/metrics.ts +4 -2
  52. package/src/resources/extensions/gsd/notifications.ts +10 -1
  53. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  54. package/src/resources/extensions/gsd/preferences-validation.ts +26 -0
  55. package/src/resources/extensions/gsd/preferences.ts +4 -0
  56. package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  57. package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
  58. package/src/resources/extensions/gsd/session-lock.ts +41 -6
  59. package/src/resources/extensions/gsd/templates/preferences.md +6 -0
  60. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +39 -1
  61. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
  62. package/src/resources/extensions/gsd/tests/cmux.test.ts +122 -0
  63. package/src/resources/extensions/gsd/tests/preferences.test.ts +23 -0
  64. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
  65. package/src/resources/extensions/search-the-web/native-search.ts +50 -4
  66. package/src/resources/extensions/shared/format-utils.ts +5 -44
  67. package/src/resources/extensions/shared/layout-utils.ts +49 -0
  68. package/src/resources/extensions/shared/mod.ts +7 -4
  69. package/src/resources/extensions/shared/terminal.ts +5 -0
  70. package/src/resources/extensions/shared/tests/format-utils.test.ts +5 -3
  71. package/src/resources/extensions/subagent/index.ts +236 -79
@@ -15,7 +15,7 @@ import { deriveState } from "./state.js";
15
15
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
16
16
  import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
17
17
  import { showQueue, showDiscuss, showHeadlessMilestoneCreation } from "./guided-flow.js";
18
- import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote } from "./auto.js";
18
+ import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote, checkRemoteAutoSession } from "./auto.js";
19
19
  import { dispatchDirectPhase } from "./auto-direct-dispatch.js";
20
20
  import { resolveProjectRoot } from "./worktree.js";
21
21
  import { assertSafeDirectory } from "./validate-directory.js";
@@ -49,6 +49,8 @@ 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";
53
+ import { showNextAction } from "../shared/mod.js";
52
54
 
53
55
 
54
56
  /** Resolve the effective project root, accounting for worktree paths. */
@@ -71,41 +73,93 @@ export function projectRoot(): string {
71
73
  }
72
74
 
73
75
  /**
74
- * Check if another process holds the auto-mode session lock.
75
- * Returns the lock data if a remote session is alive, null otherwise.
76
+ * Guard against starting auto-mode when a remote session is already running.
77
+ * Returns true if the caller should proceed with startAuto, false if handled.
76
78
  */
77
- function getRemoteAutoSession(basePath: string): { pid: number } | null {
78
- const lockData = readSessionLockData(basePath);
79
- if (!lockData) return null;
80
- if (lockData.pid === process.pid) return null;
81
- if (!isSessionLockProcessAlive(lockData)) return null;
82
- return { pid: lockData.pid };
83
- }
79
+ async function guardRemoteSession(
80
+ ctx: ExtensionCommandContext,
81
+ pi: ExtensionAPI,
82
+ ): Promise<boolean> {
83
+ // Local session already active — proceed (startAuto handles re-entrant calls)
84
+ if (isAutoActive() || isAutoPaused()) return true;
85
+
86
+ const remote = checkRemoteAutoSession(projectRoot());
87
+ if (!remote.running || !remote.pid) return true;
88
+
89
+ const unitLabel = remote.unitType && remote.unitId
90
+ ? `${remote.unitType} (${remote.unitId})`
91
+ : "unknown unit";
92
+ const unitsMsg = remote.completedUnits != null
93
+ ? `${remote.completedUnits} units completed`
94
+ : "";
95
+
96
+ const choice = await showNextAction(ctx, {
97
+ title: `Auto-mode is running in another terminal (PID ${remote.pid})`,
98
+ summary: [
99
+ `Currently executing: ${unitLabel}`,
100
+ ...(unitsMsg ? [unitsMsg] : []),
101
+ ...(remote.startedAt ? [`Started: ${remote.startedAt}`] : []),
102
+ ],
103
+ actions: [
104
+ {
105
+ id: "status",
106
+ label: "View status",
107
+ description: "Show the current GSD progress dashboard.",
108
+ recommended: true,
109
+ },
110
+ {
111
+ id: "steer",
112
+ label: "Steer the session",
113
+ description: "Use /gsd steer <instruction> to redirect the running session.",
114
+ },
115
+ {
116
+ id: "stop",
117
+ label: "Stop remote session",
118
+ description: `Send SIGTERM to PID ${remote.pid} to stop it gracefully.`,
119
+ },
120
+ {
121
+ id: "force",
122
+ label: "Force start (steal lock)",
123
+ description: "Start a new session, terminating the existing one.",
124
+ },
125
+ ],
126
+ notYetMessage: "Run /gsd when ready.",
127
+ });
84
128
 
85
- /**
86
- * Show a steering menu when auto-mode is running in another process.
87
- * Returns true if a remote session was detected (caller should return early).
88
- */
89
- function notifyRemoteAutoActive(ctx: ExtensionCommandContext, basePath: string): boolean {
90
- const remote = getRemoteAutoSession(basePath);
91
- if (!remote) return false;
92
- ctx.ui.notify(
93
- `Auto-mode is running in another process (PID ${remote.pid}).\n` +
94
- `Use these commands to interact with it:\n` +
95
- ` /gsd status — check progress\n` +
96
- ` /gsd discuss — discuss architecture decisions\n` +
97
- ` /gsd queue — queue the next milestone\n` +
98
- ` /gsd steer — apply an override to active work\n` +
99
- ` /gsd capture — fire-and-forget thought\n` +
100
- ` /gsd stop stop auto-mode`,
101
- "warning",
102
- );
103
- return true;
129
+ if (choice === "status") {
130
+ await handleStatus(ctx);
131
+ return false;
132
+ }
133
+ if (choice === "steer") {
134
+ ctx.ui.notify(
135
+ "Use /gsd steer <instruction> to redirect the running auto-mode session.\n" +
136
+ "Example: /gsd steer Use Postgres instead of SQLite",
137
+ "info",
138
+ );
139
+ return false;
140
+ }
141
+ if (choice === "stop") {
142
+ const result = stopAutoRemote(projectRoot());
143
+ if (result.found) {
144
+ ctx.ui.notify(`Sent stop signal to auto-mode session (PID ${result.pid}). It will shut down gracefully.`, "info");
145
+ } else if (result.error) {
146
+ ctx.ui.notify(`Failed to stop remote auto-mode: ${result.error}`, "error");
147
+ } else {
148
+ ctx.ui.notify("Remote session is no longer running.", "info");
149
+ }
150
+ return false;
151
+ }
152
+ if (choice === "force") {
153
+ return true; // Proceed — startAuto will steal the lock
154
+ }
155
+
156
+ // "not_yet" or escape
157
+ return false;
104
158
  }
105
159
 
106
160
  export function registerGSDCommand(pi: ExtensionAPI): void {
107
161
  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",
162
+ 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
163
  getArgumentCompletions: (prefix: string) => {
110
164
  const subcommands = [
111
165
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -114,6 +168,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
114
168
  { cmd: "stop", desc: "Stop auto mode gracefully" },
115
169
  { cmd: "pause", desc: "Pause auto-mode (preserves state, /gsd auto to resume)" },
116
170
  { cmd: "status", desc: "Progress dashboard" },
171
+ { cmd: "widget", desc: "Cycle widget: full → small → min → off" },
117
172
  { cmd: "visualize", desc: "Open 10-tab workflow visualizer (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)" },
118
173
  { cmd: "queue", desc: "Queue and reorder future milestones" },
119
174
  { cmd: "quick", desc: "Execute a quick task without full planning overhead" },
@@ -147,6 +202,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
147
202
  { cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
148
203
  { cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
149
204
  { cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
205
+ { cmd: "cmux", desc: "Manage cmux integration (status, sidebar, notifications, splits)" },
150
206
  { cmd: "park", desc: "Park a milestone — skip without deleting" },
151
207
  { cmd: "unpark", desc: "Reactivate a parked milestone" },
152
208
  { cmd: "update", desc: "Update GSD to the latest version" },
@@ -203,6 +259,38 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
203
259
  .map((s) => ({ value: `parallel ${s.cmd}`, label: s.cmd, description: s.desc }));
204
260
  }
205
261
 
262
+ if (parts[0] === "cmux") {
263
+ if (parts.length <= 2) {
264
+ const subPrefix = parts[1] ?? "";
265
+ const subs = [
266
+ { cmd: "status", desc: "Show cmux detection, prefs, and capabilities" },
267
+ { cmd: "on", desc: "Enable cmux integration" },
268
+ { cmd: "off", desc: "Disable cmux integration" },
269
+ { cmd: "notifications", desc: "Toggle cmux desktop notifications" },
270
+ { cmd: "sidebar", desc: "Toggle cmux sidebar metadata" },
271
+ { cmd: "splits", desc: "Toggle cmux visual subagent splits" },
272
+ { cmd: "browser", desc: "Toggle future browser integration flag" },
273
+ ];
274
+ return subs
275
+ .filter((s) => s.cmd.startsWith(subPrefix))
276
+ .map((s) => ({ value: `cmux ${s.cmd}`, label: s.cmd, description: s.desc }));
277
+ }
278
+
279
+ if (parts.length <= 3 && ["notifications", "sidebar", "splits", "browser"].includes(parts[1])) {
280
+ const togglePrefix = parts[2] ?? "";
281
+ return [
282
+ { cmd: "on", desc: "Enable this cmux area" },
283
+ { cmd: "off", desc: "Disable this cmux area" },
284
+ ]
285
+ .filter((item) => item.cmd.startsWith(togglePrefix))
286
+ .map((item) => ({
287
+ value: `cmux ${parts[1]} ${item.cmd}`,
288
+ label: item.cmd,
289
+ description: item.desc,
290
+ }));
291
+ }
292
+ }
293
+
206
294
  if (parts[0] === "setup" && parts.length <= 2) {
207
295
  const subPrefix = parts[1] ?? "";
208
296
  const subs = [
@@ -474,6 +562,18 @@ export async function handleGSDCommand(
474
562
  return;
475
563
  }
476
564
 
565
+ if (trimmed === "widget" || trimmed.startsWith("widget ")) {
566
+ const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
567
+ const arg = trimmed.replace(/^widget\s*/, "").trim();
568
+ if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
569
+ setWidgetMode(arg);
570
+ } else {
571
+ cycleWidgetMode();
572
+ }
573
+ ctx.ui.notify(`Widget: ${getWidgetMode()}`, "info");
574
+ return;
575
+ }
576
+
477
577
  if (trimmed === "visualize") {
478
578
  await handleVisualize(ctx);
479
579
  return;
@@ -493,6 +593,11 @@ export async function handleGSDCommand(
493
593
  return;
494
594
  }
495
595
 
596
+ if (trimmed === "cmux" || trimmed.startsWith("cmux ")) {
597
+ await handleCmux(trimmed.replace(/^cmux\s*/, "").trim(), ctx);
598
+ return;
599
+ }
600
+
496
601
  if (trimmed === "init") {
497
602
  const { detectProjectState } = await import("./detection.js");
498
603
  const { showProjectInit, handleReinit } = await import("./init-wizard.js");
@@ -546,10 +651,10 @@ export async function handleGSDCommand(
546
651
  await handleDryRun(ctx, projectRoot());
547
652
  return;
548
653
  }
549
- if (notifyRemoteAutoActive(ctx, projectRoot())) return;
550
654
  const verboseMode = trimmed.includes("--verbose");
551
655
  const debugMode = trimmed.includes("--debug");
552
656
  if (debugMode) enableDebug(projectRoot());
657
+ if (!(await guardRemoteSession(ctx, pi))) return;
553
658
  await startAuto(ctx, pi, projectRoot(), verboseMode, { step: true });
554
659
  return;
555
660
  }
@@ -558,6 +663,7 @@ export async function handleGSDCommand(
558
663
  const verboseMode = trimmed.includes("--verbose");
559
664
  const debugMode = trimmed.includes("--debug");
560
665
  if (debugMode) enableDebug(projectRoot());
666
+ if (!(await guardRemoteSession(ctx, pi))) return;
561
667
  await startAuto(ctx, pi, projectRoot(), verboseMode);
562
668
  return;
563
669
  }
@@ -941,7 +1047,7 @@ Examples:
941
1047
  }
942
1048
 
943
1049
  if (trimmed === "") {
944
- if (notifyRemoteAutoActive(ctx, projectRoot())) return;
1050
+ if (!(await guardRemoteSession(ctx, pi))) return;
945
1051
  await startAuto(ctx, pi, projectRoot(), false, { step: true });
946
1052
  return;
947
1053
  }
@@ -996,6 +1102,7 @@ function showHelp(ctx: ExtensionCommandContext): void {
996
1102
  " /gsd setup Global setup status [llm|search|remote|keys|prefs]",
997
1103
  " /gsd mode Set workflow mode (solo/team) [global|project]",
998
1104
  " /gsd prefs Manage preferences [global|project|status|wizard|setup|import-claude]",
1105
+ " /gsd cmux Manage cmux integration [status|on|off|notifications|sidebar|splits|browser]",
999
1106
  " /gsd config Set API keys for external tools",
1000
1107
  " /gsd keys API key manager [list|add|remove|test|rotate|doctor]",
1001
1108
  " /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();
@@ -20,8 +20,10 @@ import { getAndClearSkills } from "./skill-telemetry.js";
20
20
  import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
21
21
  import { parseUnitId } from "./unit-id.js";
22
22
 
23
- // Re-export from shared — canonical implementation lives in format-utils.
24
- export { formatTokenCount } from "../shared/mod.js";
23
+ // Re-export from shared — import directly from format-utils to avoid pulling
24
+ // in the full barrel (mod.js → ui.js → @gsd/pi-tui) which breaks when loaded
25
+ // outside jiti's alias resolution (e.g. dynamic import in auto-loop reports).
26
+ export { formatTokenCount } from "../shared/format-utils.js";
25
27
 
26
28
  // ─── Types ────────────────────────────────────────────────────────────────────
27
29
 
@@ -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
 
@@ -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
 
@@ -40,6 +40,19 @@ export type SessionLockResult =
40
40
  | { acquired: true }
41
41
  | { acquired: false; reason: string; existingPid?: number };
42
42
 
43
+ export type SessionLockFailureReason =
44
+ | "compromised"
45
+ | "missing-metadata"
46
+ | "pid-mismatch";
47
+
48
+ export interface SessionLockStatus {
49
+ valid: boolean;
50
+ failureReason?: SessionLockFailureReason;
51
+ existingPid?: number;
52
+ expectedPid?: number;
53
+ recovered?: boolean;
54
+ }
55
+
43
56
  // ─── Module State ───────────────────────────────────────────────────────────
44
57
 
45
58
  /** Release function from proper-lockfile — calling it releases the OS lock. */
@@ -368,7 +381,7 @@ export function updateSessionLock(
368
381
  *
369
382
  * This is called periodically during the dispatch loop.
370
383
  */
371
- export function validateSessionLock(basePath: string): boolean {
384
+ export function getSessionLockStatus(basePath: string): SessionLockStatus {
372
385
  // Lock was compromised by proper-lockfile (mtime drift from sleep, stall, etc.)
373
386
  if (_lockCompromised) {
374
387
  // Recovery gate (#1512): Before declaring the lock lost, check if the lock
@@ -385,18 +398,23 @@ export function validateSessionLock(basePath: string): boolean {
385
398
  process.stderr.write(
386
399
  `[gsd] Lock recovered after onCompromised — lock file PID matched, re-acquired.\n`,
387
400
  );
388
- return true;
401
+ return { valid: true, recovered: true };
389
402
  }
390
403
  } catch {
391
404
  // Re-acquisition failed — fall through to return false
392
405
  }
393
406
  }
394
- return false;
407
+ return {
408
+ valid: false,
409
+ failureReason: "compromised",
410
+ existingPid: existing?.pid,
411
+ expectedPid: process.pid,
412
+ };
395
413
  }
396
414
 
397
415
  // If we have an OS-level lock, we're still the owner
398
416
  if (_releaseFunction && _lockedPath === basePath) {
399
- return true;
417
+ return { valid: true };
400
418
  }
401
419
 
402
420
  // Fallback: check the lock file PID
@@ -404,10 +422,27 @@ export function validateSessionLock(basePath: string): boolean {
404
422
  const existing = readExistingLockData(lp);
405
423
  if (!existing) {
406
424
  // Lock file was deleted — we lost ownership
407
- return false;
425
+ return {
426
+ valid: false,
427
+ failureReason: "missing-metadata",
428
+ expectedPid: process.pid,
429
+ };
430
+ }
431
+
432
+ if (existing.pid !== process.pid) {
433
+ return {
434
+ valid: false,
435
+ failureReason: "pid-mismatch",
436
+ existingPid: existing.pid,
437
+ expectedPid: process.pid,
438
+ };
408
439
  }
409
440
 
410
- return existing.pid === process.pid;
441
+ return { valid: true };
442
+ }
443
+
444
+ export function validateSessionLock(basePath: string): boolean {
445
+ return getSessionLockStatus(basePath).valid;
411
446
  }
412
447
 
413
448
  /**
@@ -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: