gsd-pi 2.36.0-dev.f887f4e → 2.37.0-dev.8acfc31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -50,6 +50,7 @@ import { updateProgressWidget as _updateProgressWidget, updateSliceProgressCache
|
|
|
50
50
|
import { registerSigtermHandler as _registerSigtermHandler, deregisterSigtermHandler as _deregisterSigtermHandler, } from "./auto-supervisor.js";
|
|
51
51
|
import { isDbAvailable } from "./gsd-db.js";
|
|
52
52
|
import { countPendingCaptures } from "./captures.js";
|
|
53
|
+
import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
|
|
53
54
|
// ── Extracted modules ──────────────────────────────────────────────────────
|
|
54
55
|
import { startUnitSupervision } from "./auto-timers.js";
|
|
55
56
|
import { runPostUnitVerification } from "./auto-verification.js";
|
|
@@ -248,6 +249,7 @@ function handleLostSessionLock(ctx) {
|
|
|
248
249
|
s.paused = false;
|
|
249
250
|
clearUnitTimeout();
|
|
250
251
|
deregisterSigtermHandler();
|
|
252
|
+
clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
|
|
251
253
|
ctx?.ui.notify("Session lock lost — another GSD process appears to have taken over. Stopping gracefully.", "error");
|
|
252
254
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
253
255
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
@@ -256,6 +258,7 @@ function handleLostSessionLock(ctx) {
|
|
|
256
258
|
export async function stopAuto(ctx, pi, reason) {
|
|
257
259
|
if (!s.active && !s.paused)
|
|
258
260
|
return;
|
|
261
|
+
const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
|
|
259
262
|
const reasonSuffix = reason ? ` — ${reason}` : "";
|
|
260
263
|
clearUnitTimeout();
|
|
261
264
|
if (lockBase())
|
|
@@ -314,6 +317,8 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
314
317
|
});
|
|
315
318
|
}
|
|
316
319
|
}
|
|
320
|
+
clearCmuxSidebar(loadedPreferences);
|
|
321
|
+
logCmuxEvent(loadedPreferences, `Auto-mode stopped${reasonSuffix || ""}.`, reason?.startsWith("Blocked:") ? "warning" : "info");
|
|
317
322
|
if (isDebugEnabled()) {
|
|
318
323
|
const logPath = writeDebugSummary();
|
|
319
324
|
if (logPath) {
|
|
@@ -455,6 +460,8 @@ function buildLoopDeps() {
|
|
|
455
460
|
pauseAuto,
|
|
456
461
|
clearUnitTimeout,
|
|
457
462
|
updateProgressWidget,
|
|
463
|
+
syncCmuxSidebar,
|
|
464
|
+
logCmuxEvent,
|
|
458
465
|
// State and cache
|
|
459
466
|
invalidateAllCaches,
|
|
460
467
|
deriveState,
|
|
@@ -605,6 +612,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
605
612
|
restoreHookState(s.basePath);
|
|
606
613
|
try {
|
|
607
614
|
await rebuildState(s.basePath);
|
|
615
|
+
syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
|
|
608
616
|
}
|
|
609
617
|
catch (e) {
|
|
610
618
|
debugLog("resume-rebuild-state-failed", {
|
|
@@ -634,6 +642,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
634
642
|
}
|
|
635
643
|
updateSessionLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
|
|
636
644
|
writeLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
|
|
645
|
+
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
|
|
637
646
|
await autoLoop(ctx, pi, s, buildLoopDeps());
|
|
638
647
|
return;
|
|
639
648
|
}
|
|
@@ -647,6 +656,13 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
647
656
|
const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps);
|
|
648
657
|
if (!ready)
|
|
649
658
|
return;
|
|
659
|
+
try {
|
|
660
|
+
syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
// Best-effort only — sidebar sync must never block auto-mode startup
|
|
664
|
+
}
|
|
665
|
+
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
|
|
650
666
|
// Dispatch the first unit
|
|
651
667
|
await autoLoop(ctx, pi, s, buildLoopDeps());
|
|
652
668
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { clearCmuxSidebar, CmuxClient, detectCmuxEnvironment, resolveCmuxConfig } from "../cmux/index.js";
|
|
3
|
+
import { saveFile } from "./files.js";
|
|
4
|
+
import { getProjectGSDPreferencesPath, loadEffectiveGSDPreferences, loadProjectGSDPreferences, } from "./preferences.js";
|
|
5
|
+
import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
|
|
6
|
+
function extractBodyAfterFrontmatter(content) {
|
|
7
|
+
const start = content.startsWith("---\n") ? 4 : content.startsWith("---\r\n") ? 5 : -1;
|
|
8
|
+
if (start === -1)
|
|
9
|
+
return null;
|
|
10
|
+
const closingIdx = content.indexOf("\n---", start);
|
|
11
|
+
if (closingIdx === -1)
|
|
12
|
+
return null;
|
|
13
|
+
const after = content.slice(closingIdx + 4);
|
|
14
|
+
return after.trim() ? after : null;
|
|
15
|
+
}
|
|
16
|
+
async function writeProjectCmuxPreferences(ctx, updater) {
|
|
17
|
+
const path = getProjectGSDPreferencesPath();
|
|
18
|
+
await ensurePreferencesFile(path, ctx, "project");
|
|
19
|
+
const existing = loadProjectGSDPreferences();
|
|
20
|
+
const prefs = existing?.preferences ? { ...existing.preferences } : { version: 1 };
|
|
21
|
+
updater(prefs);
|
|
22
|
+
prefs.version = prefs.version || 1;
|
|
23
|
+
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
24
|
+
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
25
|
+
if (existsSync(path)) {
|
|
26
|
+
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
|
|
27
|
+
if (preserved)
|
|
28
|
+
body = preserved;
|
|
29
|
+
}
|
|
30
|
+
await saveFile(path, `---\n${frontmatter}---${body}`);
|
|
31
|
+
await ctx.waitForIdle();
|
|
32
|
+
await ctx.reload();
|
|
33
|
+
}
|
|
34
|
+
function formatCmuxStatus() {
|
|
35
|
+
const loaded = loadEffectiveGSDPreferences();
|
|
36
|
+
const detected = detectCmuxEnvironment();
|
|
37
|
+
const resolved = resolveCmuxConfig(loaded?.preferences);
|
|
38
|
+
const capabilities = new CmuxClient(resolved).getCapabilities();
|
|
39
|
+
const accessMode = typeof capabilities?.mode === "string"
|
|
40
|
+
? capabilities.mode
|
|
41
|
+
: typeof capabilities?.access_mode === "string"
|
|
42
|
+
? capabilities.access_mode
|
|
43
|
+
: "unknown";
|
|
44
|
+
const methods = Array.isArray(capabilities?.methods) ? capabilities.methods.length : 0;
|
|
45
|
+
return [
|
|
46
|
+
"cmux status",
|
|
47
|
+
"",
|
|
48
|
+
`Detected: ${detected.available ? "yes" : "no"}`,
|
|
49
|
+
`Enabled: ${resolved.enabled ? "yes" : "no"}`,
|
|
50
|
+
`CLI available: ${detected.cliAvailable ? "yes" : "no"}`,
|
|
51
|
+
`Socket: ${detected.socketPath}`,
|
|
52
|
+
`Workspace: ${detected.workspaceId ?? "(none)"}`,
|
|
53
|
+
`Surface: ${detected.surfaceId ?? "(none)"}`,
|
|
54
|
+
`Features: notifications=${resolved.notifications ? "on" : "off"}, sidebar=${resolved.sidebar ? "on" : "off"}, splits=${resolved.splits ? "on" : "off"}, browser=${resolved.browser ? "on" : "off"}`,
|
|
55
|
+
`Capabilities: access=${accessMode}, methods=${methods}`,
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
|
58
|
+
function ensureCmuxAvailableForEnable(ctx) {
|
|
59
|
+
const detected = detectCmuxEnvironment();
|
|
60
|
+
if (detected.available)
|
|
61
|
+
return true;
|
|
62
|
+
ctx.ui.notify("cmux not detected. Install it from https://cmux.com and run gsd inside a cmux terminal.", "warning");
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
export async function handleCmux(args, ctx) {
|
|
66
|
+
const trimmed = args.trim();
|
|
67
|
+
if (!trimmed || trimmed === "status") {
|
|
68
|
+
ctx.ui.notify(formatCmuxStatus(), "info");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (trimmed === "on") {
|
|
72
|
+
if (!ensureCmuxAvailableForEnable(ctx))
|
|
73
|
+
return;
|
|
74
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
75
|
+
prefs.cmux = {
|
|
76
|
+
enabled: true,
|
|
77
|
+
notifications: true,
|
|
78
|
+
sidebar: true,
|
|
79
|
+
splits: false,
|
|
80
|
+
browser: false,
|
|
81
|
+
...(prefs.cmux ?? {}),
|
|
82
|
+
};
|
|
83
|
+
prefs.cmux.enabled = true;
|
|
84
|
+
});
|
|
85
|
+
ctx.ui.notify("cmux integration enabled in project preferences.", "info");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (trimmed === "off") {
|
|
89
|
+
const effective = loadEffectiveGSDPreferences()?.preferences;
|
|
90
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
91
|
+
prefs.cmux = { ...(prefs.cmux ?? {}), enabled: false };
|
|
92
|
+
});
|
|
93
|
+
clearCmuxSidebar(effective);
|
|
94
|
+
ctx.ui.notify("cmux integration disabled in project preferences.", "info");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const parts = trimmed.split(/\s+/);
|
|
98
|
+
if (parts.length === 2 && ["notifications", "sidebar", "splits", "browser"].includes(parts[0]) && ["on", "off"].includes(parts[1])) {
|
|
99
|
+
const feature = parts[0];
|
|
100
|
+
const enabled = parts[1] === "on";
|
|
101
|
+
if (enabled && !ensureCmuxAvailableForEnable(ctx))
|
|
102
|
+
return;
|
|
103
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
104
|
+
const next = { ...(prefs.cmux ?? {}) };
|
|
105
|
+
next[feature] = enabled;
|
|
106
|
+
if (enabled)
|
|
107
|
+
next.enabled = true;
|
|
108
|
+
prefs.cmux = next;
|
|
109
|
+
});
|
|
110
|
+
if (!enabled && feature === "sidebar") {
|
|
111
|
+
clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
|
|
112
|
+
}
|
|
113
|
+
const note = feature === "browser" && enabled
|
|
114
|
+
? " Browser surfaces are still a follow-up path."
|
|
115
|
+
: "";
|
|
116
|
+
ctx.ui.notify(`cmux ${feature} ${enabled ? "enabled" : "disabled"}.${note}`, "info");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
ctx.ui.notify("Usage: /gsd cmux <status|on|off|notifications on|notifications off|sidebar on|sidebar off|splits on|splits off|browser on|browser off>", "info");
|
|
120
|
+
}
|
|
@@ -626,7 +626,7 @@ export function serializePreferencesToFrontmatter(prefs) {
|
|
|
626
626
|
"skill_rules", "custom_instructions", "models", "skill_discovery",
|
|
627
627
|
"skill_staleness_days", "auto_supervisor", "uat_dispatch", "unique_milestone_ids",
|
|
628
628
|
"budget_ceiling", "budget_enforcement", "context_pause_threshold",
|
|
629
|
-
"notifications", "remote_questions", "git",
|
|
629
|
+
"notifications", "cmux", "remote_questions", "git",
|
|
630
630
|
"post_unit_hooks", "pre_dispatch_hooks",
|
|
631
631
|
"dynamic_routing", "token_profile", "phases", "parallel",
|
|
632
632
|
"auto_visualize", "auto_report",
|
|
@@ -37,6 +37,7 @@ import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
|
37
37
|
import { handleLogs } from "./commands-logs.js";
|
|
38
38
|
import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
|
|
39
39
|
import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
|
|
40
|
+
import { handleCmux } from "./commands-cmux.js";
|
|
40
41
|
/** Resolve the effective project root, accounting for worktree paths. */
|
|
41
42
|
export function projectRoot() {
|
|
42
43
|
const cwd = process.cwd();
|
|
@@ -89,7 +90,7 @@ function notifyRemoteAutoActive(ctx, basePath) {
|
|
|
89
90
|
}
|
|
90
91
|
export function registerGSDCommand(pi) {
|
|
91
92
|
pi.registerCommand("gsd", {
|
|
92
|
-
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|update",
|
|
93
|
+
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|update",
|
|
93
94
|
getArgumentCompletions: (prefix) => {
|
|
94
95
|
const subcommands = [
|
|
95
96
|
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
|
@@ -98,6 +99,7 @@ export function registerGSDCommand(pi) {
|
|
|
98
99
|
{ cmd: "stop", desc: "Stop auto mode gracefully" },
|
|
99
100
|
{ cmd: "pause", desc: "Pause auto-mode (preserves state, /gsd auto to resume)" },
|
|
100
101
|
{ cmd: "status", desc: "Progress dashboard" },
|
|
102
|
+
{ cmd: "widget", desc: "Cycle widget: full → small → min → off" },
|
|
101
103
|
{ cmd: "visualize", desc: "Open 10-tab workflow visualizer (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)" },
|
|
102
104
|
{ cmd: "queue", desc: "Queue and reorder future milestones" },
|
|
103
105
|
{ cmd: "quick", desc: "Execute a quick task without full planning overhead" },
|
|
@@ -131,6 +133,7 @@ export function registerGSDCommand(pi) {
|
|
|
131
133
|
{ cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
|
|
132
134
|
{ cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
|
|
133
135
|
{ cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
|
|
136
|
+
{ cmd: "cmux", desc: "Manage cmux integration (status, sidebar, notifications, splits)" },
|
|
134
137
|
{ cmd: "park", desc: "Park a milestone — skip without deleting" },
|
|
135
138
|
{ cmd: "unpark", desc: "Reactivate a parked milestone" },
|
|
136
139
|
{ cmd: "update", desc: "Update GSD to the latest version" },
|
|
@@ -182,6 +185,36 @@ export function registerGSDCommand(pi) {
|
|
|
182
185
|
.filter((s) => s.cmd.startsWith(subPrefix))
|
|
183
186
|
.map((s) => ({ value: `parallel ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
184
187
|
}
|
|
188
|
+
if (parts[0] === "cmux") {
|
|
189
|
+
if (parts.length <= 2) {
|
|
190
|
+
const subPrefix = parts[1] ?? "";
|
|
191
|
+
const subs = [
|
|
192
|
+
{ cmd: "status", desc: "Show cmux detection, prefs, and capabilities" },
|
|
193
|
+
{ cmd: "on", desc: "Enable cmux integration" },
|
|
194
|
+
{ cmd: "off", desc: "Disable cmux integration" },
|
|
195
|
+
{ cmd: "notifications", desc: "Toggle cmux desktop notifications" },
|
|
196
|
+
{ cmd: "sidebar", desc: "Toggle cmux sidebar metadata" },
|
|
197
|
+
{ cmd: "splits", desc: "Toggle cmux visual subagent splits" },
|
|
198
|
+
{ cmd: "browser", desc: "Toggle future browser integration flag" },
|
|
199
|
+
];
|
|
200
|
+
return subs
|
|
201
|
+
.filter((s) => s.cmd.startsWith(subPrefix))
|
|
202
|
+
.map((s) => ({ value: `cmux ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
203
|
+
}
|
|
204
|
+
if (parts.length <= 3 && ["notifications", "sidebar", "splits", "browser"].includes(parts[1])) {
|
|
205
|
+
const togglePrefix = parts[2] ?? "";
|
|
206
|
+
return [
|
|
207
|
+
{ cmd: "on", desc: "Enable this cmux area" },
|
|
208
|
+
{ cmd: "off", desc: "Disable this cmux area" },
|
|
209
|
+
]
|
|
210
|
+
.filter((item) => item.cmd.startsWith(togglePrefix))
|
|
211
|
+
.map((item) => ({
|
|
212
|
+
value: `cmux ${parts[1]} ${item.cmd}`,
|
|
213
|
+
label: item.cmd,
|
|
214
|
+
description: item.desc,
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
185
218
|
if (parts[0] === "setup" && parts.length <= 2) {
|
|
186
219
|
const subPrefix = parts[1] ?? "";
|
|
187
220
|
const subs = [
|
|
@@ -430,6 +463,18 @@ export async function handleGSDCommand(args, ctx, pi) {
|
|
|
430
463
|
await handleStatus(ctx);
|
|
431
464
|
return;
|
|
432
465
|
}
|
|
466
|
+
if (trimmed === "widget" || trimmed.startsWith("widget ")) {
|
|
467
|
+
const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
|
|
468
|
+
const arg = trimmed.replace(/^widget\s*/, "").trim();
|
|
469
|
+
if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
|
|
470
|
+
setWidgetMode(arg);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
cycleWidgetMode();
|
|
474
|
+
}
|
|
475
|
+
ctx.ui.notify(`Widget: ${getWidgetMode()}`, "info");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
433
478
|
if (trimmed === "visualize") {
|
|
434
479
|
await handleVisualize(ctx);
|
|
435
480
|
return;
|
|
@@ -446,6 +491,10 @@ export async function handleGSDCommand(args, ctx, pi) {
|
|
|
446
491
|
await handlePrefs(trimmed.replace(/^prefs\s*/, "").trim(), ctx);
|
|
447
492
|
return;
|
|
448
493
|
}
|
|
494
|
+
if (trimmed === "cmux" || trimmed.startsWith("cmux ")) {
|
|
495
|
+
await handleCmux(trimmed.replace(/^cmux\s*/, "").trim(), ctx);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
449
498
|
if (trimmed === "init") {
|
|
450
499
|
const { detectProjectState } = await import("./detection.js");
|
|
451
500
|
const { showProjectInit, handleReinit } = await import("./init-wizard.js");
|
|
@@ -900,6 +949,7 @@ function showHelp(ctx) {
|
|
|
900
949
|
" /gsd setup Global setup status [llm|search|remote|keys|prefs]",
|
|
901
950
|
" /gsd mode Set workflow mode (solo/team) [global|project]",
|
|
902
951
|
" /gsd prefs Manage preferences [global|project|status|wizard|setup|import-claude]",
|
|
952
|
+
" /gsd cmux Manage cmux integration [status|on|off|notifications|sidebar|splits|browser]",
|
|
903
953
|
" /gsd config Set API keys for external tools",
|
|
904
954
|
" /gsd keys API key manager [list|add|remove|test|rotate|doctor]",
|
|
905
955
|
" /gsd hooks Show post-unit hook configuration",
|
|
@@ -173,6 +173,13 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|
|
173
173
|
- `on_milestone`: boolean — notify when a milestone finishes. Default: `true`.
|
|
174
174
|
- `on_attention`: boolean — notify when manual attention is needed. Default: `true`.
|
|
175
175
|
|
|
176
|
+
- `cmux`: configures cmux terminal integration when GSD is running inside a cmux workspace. Keys:
|
|
177
|
+
- `enabled`: boolean — master toggle for cmux integration. Default: `false`.
|
|
178
|
+
- `notifications`: boolean — route desktop notifications through cmux. Default: `true` when enabled.
|
|
179
|
+
- `sidebar`: boolean — publish status, progress, and log metadata to the cmux sidebar. Default: `true` when enabled.
|
|
180
|
+
- `splits`: boolean — run supported subagent work in visible cmux splits. Default: `false`.
|
|
181
|
+
- `browser`: boolean — reserve the future browser integration flag. Default: `false`.
|
|
182
|
+
|
|
176
183
|
- `dynamic_routing`: configures the dynamic model router that adjusts model selection based on task complexity. Keys:
|
|
177
184
|
- `enabled`: boolean — enable dynamic routing. Default: `false`.
|
|
178
185
|
- `tier_models`: object — model overrides per complexity tier. Keys: `light`, `standard`, `heavy`. Values are model ID strings.
|
|
@@ -477,6 +484,24 @@ Disables per-unit completion notifications (noisy in long runs) while keeping er
|
|
|
477
484
|
|
|
478
485
|
---
|
|
479
486
|
|
|
487
|
+
## cmux Example
|
|
488
|
+
|
|
489
|
+
```yaml
|
|
490
|
+
---
|
|
491
|
+
version: 1
|
|
492
|
+
cmux:
|
|
493
|
+
enabled: true
|
|
494
|
+
notifications: true
|
|
495
|
+
sidebar: true
|
|
496
|
+
splits: true
|
|
497
|
+
browser: false
|
|
498
|
+
---
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Enables cmux-aware notifications, sidebar metadata, and visible subagent splits when GSD is running inside a cmux terminal.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
480
505
|
## Post-Unit Hooks Example
|
|
481
506
|
|
|
482
507
|
```yaml
|
|
@@ -46,6 +46,7 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
|
|
|
46
46
|
import { toPosixPath } from "../shared/mod.js";
|
|
47
47
|
import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
|
|
48
48
|
import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
|
|
49
|
+
import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js";
|
|
49
50
|
// ── Agent Instructions (DEPRECATED) ──────────────────────────────────────
|
|
50
51
|
// agent-instructions.md is deprecated. Use AGENTS.md or CLAUDE.md instead.
|
|
51
52
|
// Pi core natively supports AGENTS.md (with CLAUDE.md fallback) per directory.
|
|
@@ -532,6 +533,10 @@ export default function (pi) {
|
|
|
532
533
|
const stopContextTimer = debugTime("context-inject");
|
|
533
534
|
const systemContent = loadPrompt("system");
|
|
534
535
|
const loadedPreferences = loadEffectiveGSDPreferences();
|
|
536
|
+
if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) {
|
|
537
|
+
markCmuxPromptShown();
|
|
538
|
+
ctx.ui.notify("cmux detected. Run /gsd cmux on to enable sidebar metadata, notifications, and visual subagent splits for this project.", "info");
|
|
539
|
+
}
|
|
535
540
|
let preferenceBlock = "";
|
|
536
541
|
if (loadedPreferences) {
|
|
537
542
|
const cwd = process.cwd();
|
|
@@ -2,13 +2,22 @@
|
|
|
2
2
|
// Cross-platform desktop notifications for auto-mode events.
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
5
|
+
import { CmuxClient, emitOsc777Notification, resolveCmuxConfig } from "../cmux/index.js";
|
|
5
6
|
/**
|
|
6
7
|
* Send a native desktop notification. Non-blocking, non-fatal.
|
|
7
8
|
* macOS: osascript, Linux: notify-send, Windows: skipped.
|
|
8
9
|
*/
|
|
9
10
|
export function sendDesktopNotification(title, message, level = "info", kind = "complete") {
|
|
10
|
-
|
|
11
|
+
const loaded = loadEffectiveGSDPreferences()?.preferences;
|
|
12
|
+
if (!shouldSendDesktopNotification(kind, loaded?.notifications))
|
|
11
13
|
return;
|
|
14
|
+
const cmux = resolveCmuxConfig(loaded);
|
|
15
|
+
if (cmux.notifications) {
|
|
16
|
+
const delivered = CmuxClient.fromPreferences(loaded).notify(title, message);
|
|
17
|
+
if (delivered)
|
|
18
|
+
return;
|
|
19
|
+
emitOsc777Notification(title, message);
|
|
20
|
+
}
|
|
12
21
|
try {
|
|
13
22
|
const command = buildDesktopNotificationCommand(process.platform, title, message, level);
|
|
14
23
|
if (!command)
|
|
@@ -47,6 +47,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
|
|
|
47
47
|
"budget_enforcement",
|
|
48
48
|
"context_pause_threshold",
|
|
49
49
|
"notifications",
|
|
50
|
+
"cmux",
|
|
50
51
|
"remote_questions",
|
|
51
52
|
"git",
|
|
52
53
|
"post_unit_hooks",
|
|
@@ -63,6 +64,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
|
|
|
63
64
|
"search_provider",
|
|
64
65
|
"compression_strategy",
|
|
65
66
|
"context_selection",
|
|
67
|
+
"widget_mode",
|
|
66
68
|
]);
|
|
67
69
|
/** Canonical list of all dispatch unit types. */
|
|
68
70
|
export const KNOWN_UNIT_TYPES = [
|
|
@@ -225,6 +225,35 @@ export function validatePreferences(preferences) {
|
|
|
225
225
|
errors.push("notifications must be an object");
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
+
// ─── Cmux ───────────────────────────────────────────────────────────────
|
|
229
|
+
if (preferences.cmux !== undefined) {
|
|
230
|
+
if (preferences.cmux && typeof preferences.cmux === "object") {
|
|
231
|
+
const cmux = preferences.cmux;
|
|
232
|
+
const validatedCmux = {};
|
|
233
|
+
if (cmux.enabled !== undefined)
|
|
234
|
+
validatedCmux.enabled = !!cmux.enabled;
|
|
235
|
+
if (cmux.notifications !== undefined)
|
|
236
|
+
validatedCmux.notifications = !!cmux.notifications;
|
|
237
|
+
if (cmux.sidebar !== undefined)
|
|
238
|
+
validatedCmux.sidebar = !!cmux.sidebar;
|
|
239
|
+
if (cmux.splits !== undefined)
|
|
240
|
+
validatedCmux.splits = !!cmux.splits;
|
|
241
|
+
if (cmux.browser !== undefined)
|
|
242
|
+
validatedCmux.browser = !!cmux.browser;
|
|
243
|
+
const knownCmuxKeys = new Set(["enabled", "notifications", "sidebar", "splits", "browser"]);
|
|
244
|
+
for (const key of Object.keys(cmux)) {
|
|
245
|
+
if (!knownCmuxKeys.has(key)) {
|
|
246
|
+
warnings.push(`unknown cmux key "${key}" — ignored`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (Object.keys(validatedCmux).length > 0) {
|
|
250
|
+
validated.cmux = validatedCmux;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
errors.push("cmux must be an object");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
228
257
|
// ─── Remote Questions ───────────────────────────────────────────────
|
|
229
258
|
if (preferences.remote_questions !== undefined) {
|
|
230
259
|
if (preferences.remote_questions && typeof preferences.remote_questions === "object") {
|
|
@@ -174,6 +174,9 @@ function mergePreferences(base, override) {
|
|
|
174
174
|
notifications: (base.notifications || override.notifications)
|
|
175
175
|
? { ...(base.notifications ?? {}), ...(override.notifications ?? {}) }
|
|
176
176
|
: undefined,
|
|
177
|
+
cmux: (base.cmux || override.cmux)
|
|
178
|
+
? { ...(base.cmux ?? {}), ...(override.cmux ?? {}) }
|
|
179
|
+
: undefined,
|
|
177
180
|
remote_questions: override.remote_questions
|
|
178
181
|
? { ...(base.remote_questions ?? {}), ...override.remote_questions }
|
|
179
182
|
: base.remote_questions,
|
|
@@ -25,9 +25,10 @@ Then research the codebase and relevant technologies. Narrate key findings and s
|
|
|
25
25
|
2. **Skill Discovery ({{skillDiscoveryMode}}):**{{skillDiscoveryInstructions}}
|
|
26
26
|
3. Explore relevant code. For small/familiar codebases, use `rg`, `find`, and targeted reads. For large or unfamiliar codebases, use `scout` to build a broad map efficiently before diving in.
|
|
27
27
|
4. Use `resolve_library` / `get_library_docs` for unfamiliar libraries — skip this for libraries already used in the codebase
|
|
28
|
-
5. Use
|
|
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
|
|
|
@@ -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.
|
|
50
|
-
6.
|
|
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
|
|
|
@@ -11,6 +11,15 @@ export const BRAVE_TOOL_NAMES = ["search-the-web", "search_and_read"];
|
|
|
11
11
|
export const CUSTOM_SEARCH_TOOL_NAMES = ["search-the-web", "search_and_read", "google_search"];
|
|
12
12
|
/** Thinking block types that require signature validation by the API */
|
|
13
13
|
const THINKING_TYPES = new Set(["thinking", "redacted_thinking"]);
|
|
14
|
+
/**
|
|
15
|
+
* Maximum number of native web searches allowed per session (agent unit).
|
|
16
|
+
* The Anthropic API's `max_uses` is per-request — it resets on each API call.
|
|
17
|
+
* When `pause_turn` triggers a resubmit, the model gets a fresh budget.
|
|
18
|
+
* This session-level cap prevents unbounded search accumulation (#1309).
|
|
19
|
+
*
|
|
20
|
+
* 15 = 3 full turns of 5 searches each — generous for research, but bounded.
|
|
21
|
+
*/
|
|
22
|
+
export const MAX_NATIVE_SEARCHES_PER_SESSION = 15;
|
|
14
23
|
/** When true, skip native web search injection and keep Brave/custom tools active on Anthropic. */
|
|
15
24
|
export function preferBraveSearch() {
|
|
16
25
|
// preferences.md takes priority over env var
|
|
@@ -57,6 +66,10 @@ export function stripThinkingFromHistory(messages) {
|
|
|
57
66
|
export function registerNativeSearchHooks(pi) {
|
|
58
67
|
let isAnthropicProvider = false;
|
|
59
68
|
let modelSelectFired = false;
|
|
69
|
+
// Session-level native search counter (#1309).
|
|
70
|
+
// Tracks cumulative web_search_tool_result blocks across all turns in a session.
|
|
71
|
+
// Reset on session_start. Used to compute remaining budget for max_uses.
|
|
72
|
+
let sessionSearchCount = 0;
|
|
60
73
|
// Track provider changes via model selection — also handles diagnostics
|
|
61
74
|
// since model_select fires AFTER session_start and knows the provider.
|
|
62
75
|
pi.on("model_select", async (event, ctx) => {
|
|
@@ -135,18 +148,46 @@ export function registerNativeSearchHooks(pi) {
|
|
|
135
148
|
// the model and causes it to pick custom tools which can fail with network errors.
|
|
136
149
|
tools = tools.filter((t) => !CUSTOM_SEARCH_TOOL_NAMES.includes(t.name));
|
|
137
150
|
payload.tools = tools;
|
|
151
|
+
// ── Session-level search budget (#1309) ──────────────────────────────
|
|
152
|
+
// Count web_search_tool_result blocks in the conversation history to
|
|
153
|
+
// determine how many native searches have already been used this session.
|
|
154
|
+
// The Anthropic API's max_uses resets per request, so without this guard,
|
|
155
|
+
// pause_turn → resubmit cycles allow unlimited total searches.
|
|
156
|
+
if (Array.isArray(messages)) {
|
|
157
|
+
let historySearchCount = 0;
|
|
158
|
+
for (const msg of messages) {
|
|
159
|
+
const content = msg.content;
|
|
160
|
+
if (!Array.isArray(content))
|
|
161
|
+
continue;
|
|
162
|
+
for (const block of content) {
|
|
163
|
+
if (block?.type === "web_search_tool_result") {
|
|
164
|
+
historySearchCount++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Sync counter from history (handles session restore / context replay)
|
|
169
|
+
sessionSearchCount = historySearchCount;
|
|
170
|
+
}
|
|
171
|
+
const remaining = Math.max(0, MAX_NATIVE_SEARCHES_PER_SESSION - sessionSearchCount);
|
|
172
|
+
if (remaining <= 0) {
|
|
173
|
+
// Budget exhausted — don't inject the search tool at all.
|
|
174
|
+
// The model will proceed without web search capability.
|
|
175
|
+
return payload;
|
|
176
|
+
}
|
|
138
177
|
tools.push({
|
|
139
178
|
type: "web_search_20250305",
|
|
140
179
|
name: "web_search",
|
|
141
|
-
// Cap
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
max_uses: 5,
|
|
180
|
+
// Cap per-request searches to the lesser of 5 (per-turn cap) or the
|
|
181
|
+
// remaining session budget (#1309). This prevents the model from
|
|
182
|
+
// consuming unlimited searches via pause_turn → resubmit cycles.
|
|
183
|
+
max_uses: Math.min(5, remaining),
|
|
145
184
|
});
|
|
146
185
|
return payload;
|
|
147
186
|
});
|
|
148
187
|
// Basic startup diagnostics — provider-specific info comes from model_select
|
|
149
188
|
pi.on("session_start", async (_event, ctx) => {
|
|
189
|
+
// Reset session-level search budget (#1309)
|
|
190
|
+
sessionSearchCount = 0;
|
|
150
191
|
const hasBrave = !!process.env.BRAVE_API_KEY;
|
|
151
192
|
const hasJina = !!process.env.JINA_API_KEY;
|
|
152
193
|
const hasAnswers = !!process.env.BRAVE_ANSWERS_KEY;
|
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
* Terminals that lack this support silently swallow the key combos.
|
|
6
6
|
*/
|
|
7
7
|
const UNSUPPORTED_TERMS = ["apple_terminal", "warpterm"];
|
|
8
|
+
export function isCmuxTerminal(env = process.env) {
|
|
9
|
+
return Boolean(env.CMUX_WORKSPACE_ID && env.CMUX_SURFACE_ID);
|
|
10
|
+
}
|
|
8
11
|
export function supportsCtrlAltShortcuts() {
|
|
9
12
|
const term = (process.env.TERM_PROGRAM || "").toLowerCase();
|
|
10
13
|
const jetbrains = (process.env.TERMINAL_EMULATOR || "").toLowerCase().includes("jetbrains");
|
|
14
|
+
if (isCmuxTerminal())
|
|
15
|
+
return true;
|
|
11
16
|
return !UNSUPPORTED_TERMS.some((t) => term.includes(t)) && !jetbrains;
|
|
12
17
|
}
|
|
13
18
|
/**
|