gsd-pi 2.36.0-dev.f887f4e → 2.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/cmux/index.js +321 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
- package/dist/resources/extensions/gsd/auto-loop.js +11 -0
- package/dist/resources/extensions/gsd/auto.js +16 -0
- package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +51 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
- package/dist/resources/extensions/gsd/index.js +5 -0
- package/dist/resources/extensions/gsd/notifications.js +10 -1
- package/dist/resources/extensions/gsd/preferences-types.js +2 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +29 -0
- package/dist/resources/extensions/gsd/preferences.js +3 -0
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
- package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
- package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
- package/dist/resources/extensions/search-the-web/native-search.js +45 -4
- package/dist/resources/extensions/shared/terminal.js +5 -0
- package/dist/resources/extensions/subagent/index.js +180 -60
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal-image.js +4 -0
- package/packages/pi-tui/dist/terminal-image.js.map +1 -1
- package/packages/pi-tui/src/terminal-image.ts +5 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/index.ts +384 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
- package/src/resources/extensions/gsd/auto-loop.ts +42 -0
- package/src/resources/extensions/gsd/auto.ts +21 -0
- package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +54 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
- package/src/resources/extensions/gsd/index.ts +8 -0
- package/src/resources/extensions/gsd/notifications.ts +10 -1
- package/src/resources/extensions/gsd/preferences-types.ts +13 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +26 -0
- package/src/resources/extensions/gsd/preferences.ts +4 -0
- package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
- package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
- package/src/resources/extensions/gsd/templates/preferences.md +6 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/cmux.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +23 -0
- package/src/resources/extensions/search-the-web/native-search.ts +50 -4
- package/src/resources/extensions/shared/terminal.ts +5 -0
- package/src/resources/extensions/subagent/index.ts +236 -79
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
import type { DispatchAction } from "./auto-dispatch.js";
|
|
26
26
|
import type { WorktreeResolver } from "./worktree-resolver.js";
|
|
27
27
|
import { debugLog } from "./debug-logger.js";
|
|
28
|
+
import type { CmuxLogLevel } from "../cmux/index.js";
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Maximum total loop iterations before forced stop. Prevents runaway loops
|
|
@@ -276,6 +277,12 @@ export interface LoopDeps {
|
|
|
276
277
|
unitId: string,
|
|
277
278
|
state: GSDState,
|
|
278
279
|
) => void;
|
|
280
|
+
syncCmuxSidebar: (preferences: GSDPreferences | undefined, state: GSDState) => void;
|
|
281
|
+
logCmuxEvent: (
|
|
282
|
+
preferences: GSDPreferences | undefined,
|
|
283
|
+
message: string,
|
|
284
|
+
level?: CmuxLogLevel,
|
|
285
|
+
) => void;
|
|
279
286
|
|
|
280
287
|
// State and cache functions
|
|
281
288
|
invalidateAllCaches: () => void;
|
|
@@ -609,6 +616,7 @@ export async function autoLoop(
|
|
|
609
616
|
|
|
610
617
|
// Derive state
|
|
611
618
|
let state = await deps.deriveState(s.basePath);
|
|
619
|
+
deps.syncCmuxSidebar(deps.loadEffectiveGSDPreferences()?.preferences, state);
|
|
612
620
|
let mid = state.activeMilestone?.id;
|
|
613
621
|
let midTitle = state.activeMilestone?.title;
|
|
614
622
|
debugLog("autoLoop", {
|
|
@@ -630,6 +638,11 @@ export async function autoLoop(
|
|
|
630
638
|
"success",
|
|
631
639
|
"milestone",
|
|
632
640
|
);
|
|
641
|
+
deps.logCmuxEvent(
|
|
642
|
+
deps.loadEffectiveGSDPreferences()?.preferences,
|
|
643
|
+
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
|
|
644
|
+
"success",
|
|
645
|
+
);
|
|
633
646
|
|
|
634
647
|
const vizPrefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
635
648
|
if (vizPrefs?.auto_visualize) {
|
|
@@ -767,12 +780,18 @@ export async function autoLoop(
|
|
|
767
780
|
"success",
|
|
768
781
|
"milestone",
|
|
769
782
|
);
|
|
783
|
+
deps.logCmuxEvent(
|
|
784
|
+
deps.loadEffectiveGSDPreferences()?.preferences,
|
|
785
|
+
"All milestones complete.",
|
|
786
|
+
"success",
|
|
787
|
+
);
|
|
770
788
|
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
771
789
|
} else if (state.phase === "blocked") {
|
|
772
790
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
773
791
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
774
792
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
775
793
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
794
|
+
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
|
|
776
795
|
} else {
|
|
777
796
|
const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
|
|
778
797
|
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
@@ -850,6 +869,11 @@ export async function autoLoop(
|
|
|
850
869
|
"success",
|
|
851
870
|
"milestone",
|
|
852
871
|
);
|
|
872
|
+
deps.logCmuxEvent(
|
|
873
|
+
deps.loadEffectiveGSDPreferences()?.preferences,
|
|
874
|
+
`Milestone ${mid} complete.`,
|
|
875
|
+
"success",
|
|
876
|
+
);
|
|
853
877
|
await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
|
|
854
878
|
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
855
879
|
break;
|
|
@@ -871,6 +895,7 @@ export async function autoLoop(
|
|
|
871
895
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
872
896
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
873
897
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
898
|
+
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
|
|
874
899
|
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
875
900
|
break;
|
|
876
901
|
}
|
|
@@ -914,12 +939,14 @@ export async function autoLoop(
|
|
|
914
939
|
"warning",
|
|
915
940
|
);
|
|
916
941
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
942
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
917
943
|
await deps.pauseAuto(ctx, pi);
|
|
918
944
|
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
919
945
|
break;
|
|
920
946
|
}
|
|
921
947
|
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
922
948
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
949
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
923
950
|
} else if (newBudgetAlertLevel === 90) {
|
|
924
951
|
s.lastBudgetAlertLevel =
|
|
925
952
|
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
@@ -933,6 +960,11 @@ export async function autoLoop(
|
|
|
933
960
|
"warning",
|
|
934
961
|
"budget",
|
|
935
962
|
);
|
|
963
|
+
deps.logCmuxEvent(
|
|
964
|
+
prefs,
|
|
965
|
+
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
966
|
+
"warning",
|
|
967
|
+
);
|
|
936
968
|
} else if (newBudgetAlertLevel === 80) {
|
|
937
969
|
s.lastBudgetAlertLevel =
|
|
938
970
|
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
@@ -946,6 +978,11 @@ export async function autoLoop(
|
|
|
946
978
|
"warning",
|
|
947
979
|
"budget",
|
|
948
980
|
);
|
|
981
|
+
deps.logCmuxEvent(
|
|
982
|
+
prefs,
|
|
983
|
+
`Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
984
|
+
"warning",
|
|
985
|
+
);
|
|
949
986
|
} else if (newBudgetAlertLevel === 75) {
|
|
950
987
|
s.lastBudgetAlertLevel =
|
|
951
988
|
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
@@ -959,6 +996,11 @@ export async function autoLoop(
|
|
|
959
996
|
"info",
|
|
960
997
|
"budget",
|
|
961
998
|
);
|
|
999
|
+
deps.logCmuxEvent(
|
|
1000
|
+
prefs,
|
|
1001
|
+
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1002
|
+
"progress",
|
|
1003
|
+
);
|
|
962
1004
|
} else if (budgetAlertLevel === 0) {
|
|
963
1005
|
s.lastBudgetAlertLevel = 0;
|
|
964
1006
|
}
|
|
@@ -184,6 +184,7 @@ import {
|
|
|
184
184
|
} from "./auto-supervisor.js";
|
|
185
185
|
import { isDbAvailable } from "./gsd-db.js";
|
|
186
186
|
import { countPendingCaptures } from "./captures.js";
|
|
187
|
+
import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
|
|
187
188
|
|
|
188
189
|
// ── Extracted modules ──────────────────────────────────────────────────────
|
|
189
190
|
import { startUnitSupervision } from "./auto-timers.js";
|
|
@@ -466,6 +467,7 @@ function handleLostSessionLock(ctx?: ExtensionContext): void {
|
|
|
466
467
|
s.paused = false;
|
|
467
468
|
clearUnitTimeout();
|
|
468
469
|
deregisterSigtermHandler();
|
|
470
|
+
clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
|
|
469
471
|
ctx?.ui.notify(
|
|
470
472
|
"Session lock lost — another GSD process appears to have taken over. Stopping gracefully.",
|
|
471
473
|
"error",
|
|
@@ -481,6 +483,7 @@ export async function stopAuto(
|
|
|
481
483
|
reason?: string,
|
|
482
484
|
): Promise<void> {
|
|
483
485
|
if (!s.active && !s.paused) return;
|
|
486
|
+
const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
|
|
484
487
|
const reasonSuffix = reason ? ` — ${reason}` : "";
|
|
485
488
|
clearUnitTimeout();
|
|
486
489
|
if (lockBase()) clearLock(lockBase());
|
|
@@ -543,6 +546,13 @@ export async function stopAuto(
|
|
|
543
546
|
}
|
|
544
547
|
}
|
|
545
548
|
|
|
549
|
+
clearCmuxSidebar(loadedPreferences);
|
|
550
|
+
logCmuxEvent(
|
|
551
|
+
loadedPreferences,
|
|
552
|
+
`Auto-mode stopped${reasonSuffix || ""}.`,
|
|
553
|
+
reason?.startsWith("Blocked:") ? "warning" : "info",
|
|
554
|
+
);
|
|
555
|
+
|
|
546
556
|
if (isDebugEnabled()) {
|
|
547
557
|
const logPath = writeDebugSummary();
|
|
548
558
|
if (logPath) {
|
|
@@ -708,6 +718,8 @@ function buildLoopDeps(): LoopDeps {
|
|
|
708
718
|
pauseAuto,
|
|
709
719
|
clearUnitTimeout,
|
|
710
720
|
updateProgressWidget,
|
|
721
|
+
syncCmuxSidebar,
|
|
722
|
+
logCmuxEvent,
|
|
711
723
|
|
|
712
724
|
// State and cache
|
|
713
725
|
invalidateAllCaches,
|
|
@@ -890,6 +902,7 @@ export async function startAuto(
|
|
|
890
902
|
restoreHookState(s.basePath);
|
|
891
903
|
try {
|
|
892
904
|
await rebuildState(s.basePath);
|
|
905
|
+
syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
|
|
893
906
|
} catch (e) {
|
|
894
907
|
debugLog("resume-rebuild-state-failed", {
|
|
895
908
|
error: e instanceof Error ? e.message : String(e),
|
|
@@ -941,6 +954,7 @@ export async function startAuto(
|
|
|
941
954
|
s.currentMilestoneId ?? "unknown",
|
|
942
955
|
s.completedUnits.length,
|
|
943
956
|
);
|
|
957
|
+
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
|
|
944
958
|
|
|
945
959
|
await autoLoop(ctx, pi, s, buildLoopDeps());
|
|
946
960
|
return;
|
|
@@ -965,6 +979,13 @@ export async function startAuto(
|
|
|
965
979
|
);
|
|
966
980
|
if (!ready) return;
|
|
967
981
|
|
|
982
|
+
try {
|
|
983
|
+
syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
|
|
984
|
+
} catch {
|
|
985
|
+
// Best-effort only — sidebar sync must never block auto-mode startup
|
|
986
|
+
}
|
|
987
|
+
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
|
|
988
|
+
|
|
968
989
|
// Dispatch the first unit
|
|
969
990
|
await autoLoop(ctx, pi, s, buildLoopDeps());
|
|
970
991
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { clearCmuxSidebar, CmuxClient, detectCmuxEnvironment, resolveCmuxConfig } from "../cmux/index.js";
|
|
4
|
+
import { saveFile } from "./files.js";
|
|
5
|
+
import {
|
|
6
|
+
getProjectGSDPreferencesPath,
|
|
7
|
+
loadEffectiveGSDPreferences,
|
|
8
|
+
loadProjectGSDPreferences,
|
|
9
|
+
} from "./preferences.js";
|
|
10
|
+
import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
|
|
11
|
+
|
|
12
|
+
function extractBodyAfterFrontmatter(content: string): string | null {
|
|
13
|
+
const start = content.startsWith("---\n") ? 4 : content.startsWith("---\r\n") ? 5 : -1;
|
|
14
|
+
if (start === -1) return null;
|
|
15
|
+
const closingIdx = content.indexOf("\n---", start);
|
|
16
|
+
if (closingIdx === -1) return null;
|
|
17
|
+
const after = content.slice(closingIdx + 4);
|
|
18
|
+
return after.trim() ? after : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function writeProjectCmuxPreferences(
|
|
22
|
+
ctx: ExtensionCommandContext,
|
|
23
|
+
updater: (prefs: Record<string, unknown>) => void,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const path = getProjectGSDPreferencesPath();
|
|
26
|
+
await ensurePreferencesFile(path, ctx, "project");
|
|
27
|
+
|
|
28
|
+
const existing = loadProjectGSDPreferences();
|
|
29
|
+
const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : { version: 1 };
|
|
30
|
+
updater(prefs);
|
|
31
|
+
prefs.version = prefs.version || 1;
|
|
32
|
+
|
|
33
|
+
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
34
|
+
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
35
|
+
if (existsSync(path)) {
|
|
36
|
+
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
|
|
37
|
+
if (preserved) body = preserved;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await saveFile(path, `---\n${frontmatter}---${body}`);
|
|
41
|
+
await ctx.waitForIdle();
|
|
42
|
+
await ctx.reload();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatCmuxStatus(): string {
|
|
46
|
+
const loaded = loadEffectiveGSDPreferences();
|
|
47
|
+
const detected = detectCmuxEnvironment();
|
|
48
|
+
const resolved = resolveCmuxConfig(loaded?.preferences);
|
|
49
|
+
const capabilities = new CmuxClient(resolved).getCapabilities() as Record<string, unknown> | null;
|
|
50
|
+
const accessMode = typeof capabilities?.mode === "string"
|
|
51
|
+
? capabilities.mode
|
|
52
|
+
: typeof capabilities?.access_mode === "string"
|
|
53
|
+
? capabilities.access_mode
|
|
54
|
+
: "unknown";
|
|
55
|
+
const methods = Array.isArray(capabilities?.methods) ? capabilities.methods.length : 0;
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
"cmux status",
|
|
59
|
+
"",
|
|
60
|
+
`Detected: ${detected.available ? "yes" : "no"}`,
|
|
61
|
+
`Enabled: ${resolved.enabled ? "yes" : "no"}`,
|
|
62
|
+
`CLI available: ${detected.cliAvailable ? "yes" : "no"}`,
|
|
63
|
+
`Socket: ${detected.socketPath}`,
|
|
64
|
+
`Workspace: ${detected.workspaceId ?? "(none)"}`,
|
|
65
|
+
`Surface: ${detected.surfaceId ?? "(none)"}`,
|
|
66
|
+
`Features: notifications=${resolved.notifications ? "on" : "off"}, sidebar=${resolved.sidebar ? "on" : "off"}, splits=${resolved.splits ? "on" : "off"}, browser=${resolved.browser ? "on" : "off"}`,
|
|
67
|
+
`Capabilities: access=${accessMode}, methods=${methods}`,
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ensureCmuxAvailableForEnable(ctx: ExtensionCommandContext): boolean {
|
|
72
|
+
const detected = detectCmuxEnvironment();
|
|
73
|
+
if (detected.available) return true;
|
|
74
|
+
ctx.ui.notify(
|
|
75
|
+
"cmux not detected. Install it from https://cmux.com and run gsd inside a cmux terminal.",
|
|
76
|
+
"warning",
|
|
77
|
+
);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function handleCmux(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
82
|
+
const trimmed = args.trim();
|
|
83
|
+
if (!trimmed || trimmed === "status") {
|
|
84
|
+
ctx.ui.notify(formatCmuxStatus(), "info");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (trimmed === "on") {
|
|
89
|
+
if (!ensureCmuxAvailableForEnable(ctx)) return;
|
|
90
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
91
|
+
prefs.cmux = {
|
|
92
|
+
enabled: true,
|
|
93
|
+
notifications: true,
|
|
94
|
+
sidebar: true,
|
|
95
|
+
splits: false,
|
|
96
|
+
browser: false,
|
|
97
|
+
...((prefs.cmux as Record<string, unknown> | undefined) ?? {}),
|
|
98
|
+
};
|
|
99
|
+
(prefs.cmux as Record<string, unknown>).enabled = true;
|
|
100
|
+
});
|
|
101
|
+
ctx.ui.notify("cmux integration enabled in project preferences.", "info");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (trimmed === "off") {
|
|
106
|
+
const effective = loadEffectiveGSDPreferences()?.preferences;
|
|
107
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
108
|
+
prefs.cmux = { ...((prefs.cmux as Record<string, unknown> | undefined) ?? {}), enabled: false };
|
|
109
|
+
});
|
|
110
|
+
clearCmuxSidebar(effective);
|
|
111
|
+
ctx.ui.notify("cmux integration disabled in project preferences.", "info");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const parts = trimmed.split(/\s+/);
|
|
116
|
+
if (parts.length === 2 && ["notifications", "sidebar", "splits", "browser"].includes(parts[0]) && ["on", "off"].includes(parts[1])) {
|
|
117
|
+
const feature = parts[0] as "notifications" | "sidebar" | "splits" | "browser";
|
|
118
|
+
const enabled = parts[1] === "on";
|
|
119
|
+
if (enabled && !ensureCmuxAvailableForEnable(ctx)) return;
|
|
120
|
+
|
|
121
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
122
|
+
const next = { ...((prefs.cmux as Record<string, unknown> | undefined) ?? {}) };
|
|
123
|
+
next[feature] = enabled;
|
|
124
|
+
if (enabled) next.enabled = true;
|
|
125
|
+
prefs.cmux = next;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!enabled && feature === "sidebar") {
|
|
129
|
+
clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const note = feature === "browser" && enabled
|
|
133
|
+
? " Browser surfaces are still a follow-up path."
|
|
134
|
+
: "";
|
|
135
|
+
ctx.ui.notify(`cmux ${feature} ${enabled ? "enabled" : "disabled"}.${note}`, "info");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ctx.ui.notify(
|
|
140
|
+
"Usage: /gsd cmux <status|on|off|notifications on|notifications off|sidebar on|sidebar off|splits on|splits off|browser on|browser off>",
|
|
141
|
+
"info",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -740,7 +740,7 @@ export function serializePreferencesToFrontmatter(prefs: Record<string, unknown>
|
|
|
740
740
|
"skill_rules", "custom_instructions", "models", "skill_discovery",
|
|
741
741
|
"skill_staleness_days", "auto_supervisor", "uat_dispatch", "unique_milestone_ids",
|
|
742
742
|
"budget_ceiling", "budget_enforcement", "context_pause_threshold",
|
|
743
|
-
"notifications", "remote_questions", "git",
|
|
743
|
+
"notifications", "cmux", "remote_questions", "git",
|
|
744
744
|
"post_unit_hooks", "pre_dispatch_hooks",
|
|
745
745
|
"dynamic_routing", "token_profile", "phases", "parallel",
|
|
746
746
|
"auto_visualize", "auto_report",
|
|
@@ -49,6 +49,7 @@ import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
|
49
49
|
import { handleLogs } from "./commands-logs.js";
|
|
50
50
|
import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
|
|
51
51
|
import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
|
|
52
|
+
import { handleCmux } from "./commands-cmux.js";
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
/** Resolve the effective project root, accounting for worktree paths. */
|
|
@@ -105,7 +106,7 @@ function notifyRemoteAutoActive(ctx: ExtensionCommandContext, basePath: string):
|
|
|
105
106
|
|
|
106
107
|
export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
107
108
|
pi.registerCommand("gsd", {
|
|
108
|
-
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|update",
|
|
109
|
+
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|update",
|
|
109
110
|
getArgumentCompletions: (prefix: string) => {
|
|
110
111
|
const subcommands = [
|
|
111
112
|
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
|
@@ -114,6 +115,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
114
115
|
{ cmd: "stop", desc: "Stop auto mode gracefully" },
|
|
115
116
|
{ cmd: "pause", desc: "Pause auto-mode (preserves state, /gsd auto to resume)" },
|
|
116
117
|
{ cmd: "status", desc: "Progress dashboard" },
|
|
118
|
+
{ cmd: "widget", desc: "Cycle widget: full → small → min → off" },
|
|
117
119
|
{ cmd: "visualize", desc: "Open 10-tab workflow visualizer (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)" },
|
|
118
120
|
{ cmd: "queue", desc: "Queue and reorder future milestones" },
|
|
119
121
|
{ cmd: "quick", desc: "Execute a quick task without full planning overhead" },
|
|
@@ -147,6 +149,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
147
149
|
{ cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
|
|
148
150
|
{ cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
|
|
149
151
|
{ cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
|
|
152
|
+
{ cmd: "cmux", desc: "Manage cmux integration (status, sidebar, notifications, splits)" },
|
|
150
153
|
{ cmd: "park", desc: "Park a milestone — skip without deleting" },
|
|
151
154
|
{ cmd: "unpark", desc: "Reactivate a parked milestone" },
|
|
152
155
|
{ cmd: "update", desc: "Update GSD to the latest version" },
|
|
@@ -203,6 +206,38 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
203
206
|
.map((s) => ({ value: `parallel ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
204
207
|
}
|
|
205
208
|
|
|
209
|
+
if (parts[0] === "cmux") {
|
|
210
|
+
if (parts.length <= 2) {
|
|
211
|
+
const subPrefix = parts[1] ?? "";
|
|
212
|
+
const subs = [
|
|
213
|
+
{ cmd: "status", desc: "Show cmux detection, prefs, and capabilities" },
|
|
214
|
+
{ cmd: "on", desc: "Enable cmux integration" },
|
|
215
|
+
{ cmd: "off", desc: "Disable cmux integration" },
|
|
216
|
+
{ cmd: "notifications", desc: "Toggle cmux desktop notifications" },
|
|
217
|
+
{ cmd: "sidebar", desc: "Toggle cmux sidebar metadata" },
|
|
218
|
+
{ cmd: "splits", desc: "Toggle cmux visual subagent splits" },
|
|
219
|
+
{ cmd: "browser", desc: "Toggle future browser integration flag" },
|
|
220
|
+
];
|
|
221
|
+
return subs
|
|
222
|
+
.filter((s) => s.cmd.startsWith(subPrefix))
|
|
223
|
+
.map((s) => ({ value: `cmux ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (parts.length <= 3 && ["notifications", "sidebar", "splits", "browser"].includes(parts[1])) {
|
|
227
|
+
const togglePrefix = parts[2] ?? "";
|
|
228
|
+
return [
|
|
229
|
+
{ cmd: "on", desc: "Enable this cmux area" },
|
|
230
|
+
{ cmd: "off", desc: "Disable this cmux area" },
|
|
231
|
+
]
|
|
232
|
+
.filter((item) => item.cmd.startsWith(togglePrefix))
|
|
233
|
+
.map((item) => ({
|
|
234
|
+
value: `cmux ${parts[1]} ${item.cmd}`,
|
|
235
|
+
label: item.cmd,
|
|
236
|
+
description: item.desc,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
206
241
|
if (parts[0] === "setup" && parts.length <= 2) {
|
|
207
242
|
const subPrefix = parts[1] ?? "";
|
|
208
243
|
const subs = [
|
|
@@ -474,6 +509,18 @@ export async function handleGSDCommand(
|
|
|
474
509
|
return;
|
|
475
510
|
}
|
|
476
511
|
|
|
512
|
+
if (trimmed === "widget" || trimmed.startsWith("widget ")) {
|
|
513
|
+
const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
|
|
514
|
+
const arg = trimmed.replace(/^widget\s*/, "").trim();
|
|
515
|
+
if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
|
|
516
|
+
setWidgetMode(arg);
|
|
517
|
+
} else {
|
|
518
|
+
cycleWidgetMode();
|
|
519
|
+
}
|
|
520
|
+
ctx.ui.notify(`Widget: ${getWidgetMode()}`, "info");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
477
524
|
if (trimmed === "visualize") {
|
|
478
525
|
await handleVisualize(ctx);
|
|
479
526
|
return;
|
|
@@ -493,6 +540,11 @@ export async function handleGSDCommand(
|
|
|
493
540
|
return;
|
|
494
541
|
}
|
|
495
542
|
|
|
543
|
+
if (trimmed === "cmux" || trimmed.startsWith("cmux ")) {
|
|
544
|
+
await handleCmux(trimmed.replace(/^cmux\s*/, "").trim(), ctx);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
496
548
|
if (trimmed === "init") {
|
|
497
549
|
const { detectProjectState } = await import("./detection.js");
|
|
498
550
|
const { showProjectInit, handleReinit } = await import("./init-wizard.js");
|
|
@@ -996,6 +1048,7 @@ function showHelp(ctx: ExtensionCommandContext): void {
|
|
|
996
1048
|
" /gsd setup Global setup status [llm|search|remote|keys|prefs]",
|
|
997
1049
|
" /gsd mode Set workflow mode (solo/team) [global|project]",
|
|
998
1050
|
" /gsd prefs Manage preferences [global|project|status|wizard|setup|import-claude]",
|
|
1051
|
+
" /gsd cmux Manage cmux integration [status|on|off|notifications|sidebar|splits|browser]",
|
|
999
1052
|
" /gsd config Set API keys for external tools",
|
|
1000
1053
|
" /gsd keys API key manager [list|add|remove|test|rotate|doctor]",
|
|
1001
1054
|
" /gsd hooks Show post-unit hook configuration",
|
|
@@ -173,6 +173,13 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|
|
173
173
|
- `on_milestone`: boolean — notify when a milestone finishes. Default: `true`.
|
|
174
174
|
- `on_attention`: boolean — notify when manual attention is needed. Default: `true`.
|
|
175
175
|
|
|
176
|
+
- `cmux`: configures cmux terminal integration when GSD is running inside a cmux workspace. Keys:
|
|
177
|
+
- `enabled`: boolean — master toggle for cmux integration. Default: `false`.
|
|
178
|
+
- `notifications`: boolean — route desktop notifications through cmux. Default: `true` when enabled.
|
|
179
|
+
- `sidebar`: boolean — publish status, progress, and log metadata to the cmux sidebar. Default: `true` when enabled.
|
|
180
|
+
- `splits`: boolean — run supported subagent work in visible cmux splits. Default: `false`.
|
|
181
|
+
- `browser`: boolean — reserve the future browser integration flag. Default: `false`.
|
|
182
|
+
|
|
176
183
|
- `dynamic_routing`: configures the dynamic model router that adjusts model selection based on task complexity. Keys:
|
|
177
184
|
- `enabled`: boolean — enable dynamic routing. Default: `false`.
|
|
178
185
|
- `tier_models`: object — model overrides per complexity tier. Keys: `light`, `standard`, `heavy`. Values are model ID strings.
|
|
@@ -477,6 +484,24 @@ Disables per-unit completion notifications (noisy in long runs) while keeping er
|
|
|
477
484
|
|
|
478
485
|
---
|
|
479
486
|
|
|
487
|
+
## cmux Example
|
|
488
|
+
|
|
489
|
+
```yaml
|
|
490
|
+
---
|
|
491
|
+
version: 1
|
|
492
|
+
cmux:
|
|
493
|
+
enabled: true
|
|
494
|
+
notifications: true
|
|
495
|
+
sidebar: true
|
|
496
|
+
splits: true
|
|
497
|
+
browser: false
|
|
498
|
+
---
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Enables cmux-aware notifications, sidebar metadata, and visible subagent splits when GSD is running inside a cmux terminal.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
480
505
|
## Post-Unit Hooks Example
|
|
481
506
|
|
|
482
507
|
```yaml
|
|
@@ -65,6 +65,7 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
|
|
|
65
65
|
import { toPosixPath } from "../shared/mod.js";
|
|
66
66
|
import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
|
|
67
67
|
import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
|
|
68
|
+
import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js";
|
|
68
69
|
|
|
69
70
|
// ── Agent Instructions (DEPRECATED) ──────────────────────────────────────
|
|
70
71
|
// agent-instructions.md is deprecated. Use AGENTS.md or CLAUDE.md instead.
|
|
@@ -623,6 +624,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
623
624
|
const stopContextTimer = debugTime("context-inject");
|
|
624
625
|
const systemContent = loadPrompt("system");
|
|
625
626
|
const loadedPreferences = loadEffectiveGSDPreferences();
|
|
627
|
+
if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) {
|
|
628
|
+
markCmuxPromptShown();
|
|
629
|
+
ctx.ui.notify(
|
|
630
|
+
"cmux detected. Run /gsd cmux on to enable sidebar metadata, notifications, and visual subagent splits for this project.",
|
|
631
|
+
"info",
|
|
632
|
+
);
|
|
633
|
+
}
|
|
626
634
|
let preferenceBlock = "";
|
|
627
635
|
if (loadedPreferences) {
|
|
628
636
|
const cwd = process.cwd();
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import type { NotificationPreferences } from "./types.js";
|
|
6
6
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
7
|
+
import { CmuxClient, emitOsc777Notification, resolveCmuxConfig } from "../cmux/index.js";
|
|
7
8
|
|
|
8
9
|
export type NotifyLevel = "info" | "success" | "warning" | "error";
|
|
9
10
|
export type NotificationKind = "complete" | "error" | "budget" | "milestone" | "attention";
|
|
@@ -23,7 +24,15 @@ export function sendDesktopNotification(
|
|
|
23
24
|
level: NotifyLevel = "info",
|
|
24
25
|
kind: NotificationKind = "complete",
|
|
25
26
|
): void {
|
|
26
|
-
|
|
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
|
|
29
|
-
6.
|
|
30
|
-
7.
|
|
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
|
|