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,6 +15,7 @@ import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
15
15
  import type { AutoSession } from "./auto/session.js";
16
16
  import { NEW_SESSION_TIMEOUT_MS } from "./auto/session.js";
17
17
  import type { GSDPreferences } from "./preferences.js";
18
+ import type { SessionLockStatus } from "./session-lock.js";
18
19
  import type { GSDState } from "./types.js";
19
20
  import type { CloseoutOptions } from "./auto-unit-closeout.js";
20
21
  import type { PostUnitContext } from "./auto-post-unit.js";
@@ -25,6 +26,7 @@ import type {
25
26
  import type { DispatchAction } from "./auto-dispatch.js";
26
27
  import type { WorktreeResolver } from "./worktree-resolver.js";
27
28
  import { debugLog } from "./debug-logger.js";
29
+ import type { CmuxLogLevel } from "../cmux/index.js";
28
30
 
29
31
  /**
30
32
  * Maximum total loop iterations before forced stop. Prevents runaway loops
@@ -276,6 +278,12 @@ export interface LoopDeps {
276
278
  unitId: string,
277
279
  state: GSDState,
278
280
  ) => void;
281
+ syncCmuxSidebar: (preferences: GSDPreferences | undefined, state: GSDState) => void;
282
+ logCmuxEvent: (
283
+ preferences: GSDPreferences | undefined,
284
+ message: string,
285
+ level?: CmuxLogLevel,
286
+ ) => void;
279
287
 
280
288
  // State and cache functions
281
289
  invalidateAllCaches: () => void;
@@ -300,7 +308,7 @@ export interface LoopDeps {
300
308
  checkResourcesStale: (version: string | null) => string | null;
301
309
 
302
310
  // Session lock
303
- validateSessionLock: (basePath: string) => boolean;
311
+ validateSessionLock: (basePath: string) => SessionLockStatus;
304
312
  updateSessionLock: (
305
313
  basePath: string,
306
314
  unitType: string,
@@ -308,7 +316,10 @@ export interface LoopDeps {
308
316
  completedUnits: number,
309
317
  sessionFile?: string,
310
318
  ) => void;
311
- handleLostSessionLock: (ctx?: ExtensionContext) => void;
319
+ handleLostSessionLock: (
320
+ ctx?: ExtensionContext,
321
+ lockStatus?: SessionLockStatus,
322
+ ) => void;
312
323
 
313
324
  // Milestone transition functions
314
325
  sendDesktopNotification: (
@@ -552,10 +563,24 @@ export async function autoLoop(
552
563
  try {
553
564
  // ── Blanket try/catch: one bad iteration must not kill the session
554
565
 
555
- if (deps.lockBase() && !deps.validateSessionLock(deps.lockBase())) {
556
- deps.handleLostSessionLock(ctx);
557
- debugLog("autoLoop", { phase: "exit", reason: "session-lock-lost" });
558
- break;
566
+ const sessionLockBase = deps.lockBase();
567
+ if (sessionLockBase) {
568
+ const lockStatus = deps.validateSessionLock(sessionLockBase);
569
+ if (!lockStatus.valid) {
570
+ debugLog("autoLoop", {
571
+ phase: "session-lock-invalid",
572
+ reason: lockStatus.failureReason ?? "unknown",
573
+ existingPid: lockStatus.existingPid,
574
+ expectedPid: lockStatus.expectedPid,
575
+ });
576
+ deps.handleLostSessionLock(ctx, lockStatus);
577
+ debugLog("autoLoop", {
578
+ phase: "exit",
579
+ reason: "session-lock-lost",
580
+ detail: lockStatus.failureReason ?? "unknown",
581
+ });
582
+ break;
583
+ }
559
584
  }
560
585
 
561
586
  // ── Phase 1: Pre-dispatch ───────────────────────────────────────────
@@ -609,6 +634,7 @@ export async function autoLoop(
609
634
 
610
635
  // Derive state
611
636
  let state = await deps.deriveState(s.basePath);
637
+ deps.syncCmuxSidebar(deps.loadEffectiveGSDPreferences()?.preferences, state);
612
638
  let mid = state.activeMilestone?.id;
613
639
  let midTitle = state.activeMilestone?.title;
614
640
  debugLog("autoLoop", {
@@ -630,6 +656,11 @@ export async function autoLoop(
630
656
  "success",
631
657
  "milestone",
632
658
  );
659
+ deps.logCmuxEvent(
660
+ deps.loadEffectiveGSDPreferences()?.preferences,
661
+ `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
662
+ "success",
663
+ );
633
664
 
634
665
  const vizPrefs = deps.loadEffectiveGSDPreferences()?.preferences;
635
666
  if (vizPrefs?.auto_visualize) {
@@ -767,12 +798,18 @@ export async function autoLoop(
767
798
  "success",
768
799
  "milestone",
769
800
  );
801
+ deps.logCmuxEvent(
802
+ deps.loadEffectiveGSDPreferences()?.preferences,
803
+ "All milestones complete.",
804
+ "success",
805
+ );
770
806
  await deps.stopAuto(ctx, pi, "All milestones complete");
771
807
  } else if (state.phase === "blocked") {
772
808
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
773
809
  await deps.stopAuto(ctx, pi, blockerMsg);
774
810
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
775
811
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
812
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
776
813
  } else {
777
814
  const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
778
815
  const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
@@ -850,6 +887,11 @@ export async function autoLoop(
850
887
  "success",
851
888
  "milestone",
852
889
  );
890
+ deps.logCmuxEvent(
891
+ deps.loadEffectiveGSDPreferences()?.preferences,
892
+ `Milestone ${mid} complete.`,
893
+ "success",
894
+ );
853
895
  await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
854
896
  debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
855
897
  break;
@@ -871,6 +913,7 @@ export async function autoLoop(
871
913
  await deps.stopAuto(ctx, pi, blockerMsg);
872
914
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
873
915
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
916
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
874
917
  debugLog("autoLoop", { phase: "exit", reason: "blocked" });
875
918
  break;
876
919
  }
@@ -914,12 +957,14 @@ export async function autoLoop(
914
957
  "warning",
915
958
  );
916
959
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
960
+ deps.logCmuxEvent(prefs, msg, "warning");
917
961
  await deps.pauseAuto(ctx, pi);
918
962
  debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
919
963
  break;
920
964
  }
921
965
  ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
922
966
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
967
+ deps.logCmuxEvent(prefs, msg, "warning");
923
968
  } else if (newBudgetAlertLevel === 90) {
924
969
  s.lastBudgetAlertLevel =
925
970
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
@@ -933,6 +978,11 @@ export async function autoLoop(
933
978
  "warning",
934
979
  "budget",
935
980
  );
981
+ deps.logCmuxEvent(
982
+ prefs,
983
+ `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
984
+ "warning",
985
+ );
936
986
  } else if (newBudgetAlertLevel === 80) {
937
987
  s.lastBudgetAlertLevel =
938
988
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
@@ -946,6 +996,11 @@ export async function autoLoop(
946
996
  "warning",
947
997
  "budget",
948
998
  );
999
+ deps.logCmuxEvent(
1000
+ prefs,
1001
+ `Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
1002
+ "warning",
1003
+ );
949
1004
  } else if (newBudgetAlertLevel === 75) {
950
1005
  s.lastBudgetAlertLevel =
951
1006
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
@@ -959,6 +1014,11 @@ export async function autoLoop(
959
1014
  "info",
960
1015
  "budget",
961
1016
  );
1017
+ deps.logCmuxEvent(
1018
+ prefs,
1019
+ `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
1020
+ "progress",
1021
+ );
962
1022
  } else if (budgetAlertLevel === 0) {
963
1023
  s.lastBudgetAlertLevel = 0;
964
1024
  }
@@ -47,10 +47,11 @@ import {
47
47
  } from "./crash-recovery.js";
48
48
  import {
49
49
  acquireSessionLock,
50
- validateSessionLock,
50
+ getSessionLockStatus,
51
51
  releaseSessionLock,
52
52
  updateSessionLock,
53
53
  } from "./session-lock.js";
54
+ import type { SessionLockStatus } from "./session-lock.js";
54
55
  import {
55
56
  clearUnitRuntimeRecord,
56
57
  inspectExecuteTaskDurability,
@@ -184,6 +185,7 @@ import {
184
185
  } from "./auto-supervisor.js";
185
186
  import { isDbAvailable } from "./gsd-db.js";
186
187
  import { countPendingCaptures } from "./captures.js";
188
+ import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
187
189
 
188
190
  // ── Extracted modules ──────────────────────────────────────────────────────
189
191
  import { startUnitSupervision } from "./auto-timers.js";
@@ -416,6 +418,38 @@ export function stopAutoRemote(projectRoot: string): {
416
418
  }
417
419
  }
418
420
 
421
+ /**
422
+ * Check if a remote auto-mode session is running (from a different process).
423
+ * Reads the crash lock, checks PID liveness, and returns session details.
424
+ * Used by the guard in commands.ts to prevent bare /gsd, /gsd next, and
425
+ * /gsd auto from stealing the session lock.
426
+ */
427
+ export function checkRemoteAutoSession(projectRoot: string): {
428
+ running: boolean;
429
+ pid?: number;
430
+ unitType?: string;
431
+ unitId?: string;
432
+ startedAt?: string;
433
+ completedUnits?: number;
434
+ } {
435
+ const lock = readCrashLock(projectRoot);
436
+ if (!lock) return { running: false };
437
+
438
+ if (!isLockProcessAlive(lock)) {
439
+ // Stale lock from a dead process — not a live remote session
440
+ return { running: false };
441
+ }
442
+
443
+ return {
444
+ running: true,
445
+ pid: lock.pid,
446
+ unitType: lock.unitType,
447
+ unitId: lock.unitId,
448
+ startedAt: lock.startedAt,
449
+ completedUnits: lock.completedUnits,
450
+ };
451
+ }
452
+
419
453
  export function isStepMode(): boolean {
420
454
  return s.stepMode;
421
455
  }
@@ -460,14 +494,33 @@ function buildSnapshotOpts(
460
494
  };
461
495
  }
462
496
 
463
- function handleLostSessionLock(ctx?: ExtensionContext): void {
464
- debugLog("session-lock-lost", { lockBase: lockBase() });
497
+ function handleLostSessionLock(
498
+ ctx?: ExtensionContext,
499
+ lockStatus?: SessionLockStatus,
500
+ ): void {
501
+ debugLog("session-lock-lost", {
502
+ lockBase: lockBase(),
503
+ reason: lockStatus?.failureReason,
504
+ existingPid: lockStatus?.existingPid,
505
+ expectedPid: lockStatus?.expectedPid,
506
+ });
465
507
  s.active = false;
466
508
  s.paused = false;
467
509
  clearUnitTimeout();
468
510
  deregisterSigtermHandler();
511
+ clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
512
+ const message =
513
+ lockStatus?.failureReason === "pid-mismatch"
514
+ ? lockStatus.existingPid
515
+ ? `Session lock moved to PID ${lockStatus.existingPid} — another GSD process appears to have taken over. Stopping gracefully.`
516
+ : "Session lock moved to a different process — another GSD process appears to have taken over. Stopping gracefully."
517
+ : lockStatus?.failureReason === "missing-metadata"
518
+ ? "Session lock metadata disappeared, so ownership could not be confirmed. Stopping gracefully."
519
+ : lockStatus?.failureReason === "compromised"
520
+ ? "Session lock was compromised or invalidated during heartbeat checks; takeover was not confirmed. Stopping gracefully."
521
+ : "Session lock lost. Stopping gracefully.";
469
522
  ctx?.ui.notify(
470
- "Session lock lost — another GSD process appears to have taken over. Stopping gracefully.",
523
+ message,
471
524
  "error",
472
525
  );
473
526
  ctx?.ui.setStatus("gsd-auto", undefined);
@@ -481,6 +534,7 @@ export async function stopAuto(
481
534
  reason?: string,
482
535
  ): Promise<void> {
483
536
  if (!s.active && !s.paused) return;
537
+ const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
484
538
  const reasonSuffix = reason ? ` — ${reason}` : "";
485
539
  clearUnitTimeout();
486
540
  if (lockBase()) clearLock(lockBase());
@@ -543,6 +597,13 @@ export async function stopAuto(
543
597
  }
544
598
  }
545
599
 
600
+ clearCmuxSidebar(loadedPreferences);
601
+ logCmuxEvent(
602
+ loadedPreferences,
603
+ `Auto-mode stopped${reasonSuffix || ""}.`,
604
+ reason?.startsWith("Blocked:") ? "warning" : "info",
605
+ );
606
+
546
607
  if (isDebugEnabled()) {
547
608
  const logPath = writeDebugSummary();
548
609
  if (logPath) {
@@ -708,6 +769,8 @@ function buildLoopDeps(): LoopDeps {
708
769
  pauseAuto,
709
770
  clearUnitTimeout,
710
771
  updateProgressWidget,
772
+ syncCmuxSidebar,
773
+ logCmuxEvent,
711
774
 
712
775
  // State and cache
713
776
  invalidateAllCaches,
@@ -724,7 +787,7 @@ function buildLoopDeps(): LoopDeps {
724
787
  checkResourcesStale,
725
788
 
726
789
  // Session lock
727
- validateSessionLock,
790
+ validateSessionLock: getSessionLockStatus,
728
791
  updateSessionLock,
729
792
  handleLostSessionLock,
730
793
 
@@ -890,6 +953,7 @@ export async function startAuto(
890
953
  restoreHookState(s.basePath);
891
954
  try {
892
955
  await rebuildState(s.basePath);
956
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
893
957
  } catch (e) {
894
958
  debugLog("resume-rebuild-state-failed", {
895
959
  error: e instanceof Error ? e.message : String(e),
@@ -941,6 +1005,7 @@ export async function startAuto(
941
1005
  s.currentMilestoneId ?? "unknown",
942
1006
  s.completedUnits.length,
943
1007
  );
1008
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
944
1009
 
945
1010
  await autoLoop(ctx, pi, s, buildLoopDeps());
946
1011
  return;
@@ -965,6 +1030,13 @@ export async function startAuto(
965
1030
  );
966
1031
  if (!ready) return;
967
1032
 
1033
+ try {
1034
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
1035
+ } catch {
1036
+ // Best-effort only — sidebar sync must never block auto-mode startup
1037
+ }
1038
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
1039
+
968
1040
  // Dispatch the first unit
969
1041
  await autoLoop(ctx, pi, s, buildLoopDeps());
970
1042
  }
@@ -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",