gsd-pi 2.36.0-dev.f887f4e → 2.37.0-dev.3186675
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/cmux/package.json +7 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
- package/dist/resources/extensions/gsd/auto-loop.js +29 -4
- package/dist/resources/extensions/gsd/auto.js +35 -5
- 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/git-service.js +9 -1
- package/dist/resources/extensions/gsd/history.js +2 -1
- package/dist/resources/extensions/gsd/index.js +5 -0
- package/dist/resources/extensions/gsd/metrics.js +4 -2
- 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/session-lock.js +26 -6
- 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/format-utils.js +5 -41
- package/dist/resources/extensions/shared/layout-utils.js +46 -0
- package/dist/resources/extensions/shared/mod.js +2 -1
- 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/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -4
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
- 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/cmux/package.json +7 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
- package/src/resources/extensions/gsd/auto-loop.ts +66 -6
- package/src/resources/extensions/gsd/auto.ts +45 -5
- 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/git-service.ts +12 -1
- package/src/resources/extensions/gsd/history.ts +2 -1
- package/src/resources/extensions/gsd/index.ts +8 -0
- package/src/resources/extensions/gsd/metrics.ts +4 -2
- 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/session-lock.ts +41 -6
- package/src/resources/extensions/gsd/templates/preferences.md +6 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +39 -1
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/cmux.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +23 -0
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
- package/src/resources/extensions/search-the-web/native-search.ts +50 -4
- package/src/resources/extensions/shared/format-utils.ts +5 -44
- package/src/resources/extensions/shared/layout-utils.ts +49 -0
- package/src/resources/extensions/shared/mod.ts +7 -4
- package/src/resources/extensions/shared/terminal.ts +5 -0
- package/src/resources/extensions/shared/tests/format-utils.test.ts +5 -3
- package/src/resources/extensions/subagent/index.ts +236 -79
|
@@ -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
|
|
|
@@ -320,7 +320,7 @@ export function updateSessionLock(basePath, unitType, unitId, completedUnits, se
|
|
|
320
320
|
*
|
|
321
321
|
* This is called periodically during the dispatch loop.
|
|
322
322
|
*/
|
|
323
|
-
export function
|
|
323
|
+
export function getSessionLockStatus(basePath) {
|
|
324
324
|
// Lock was compromised by proper-lockfile (mtime drift from sleep, stall, etc.)
|
|
325
325
|
if (_lockCompromised) {
|
|
326
326
|
// Recovery gate (#1512): Before declaring the lock lost, check if the lock
|
|
@@ -335,27 +335,47 @@ export function validateSessionLock(basePath) {
|
|
|
335
335
|
const result = acquireSessionLock(basePath);
|
|
336
336
|
if (result.acquired) {
|
|
337
337
|
process.stderr.write(`[gsd] Lock recovered after onCompromised — lock file PID matched, re-acquired.\n`);
|
|
338
|
-
return true;
|
|
338
|
+
return { valid: true, recovered: true };
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
catch {
|
|
342
342
|
// Re-acquisition failed — fall through to return false
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
|
-
return
|
|
345
|
+
return {
|
|
346
|
+
valid: false,
|
|
347
|
+
failureReason: "compromised",
|
|
348
|
+
existingPid: existing?.pid,
|
|
349
|
+
expectedPid: process.pid,
|
|
350
|
+
};
|
|
346
351
|
}
|
|
347
352
|
// If we have an OS-level lock, we're still the owner
|
|
348
353
|
if (_releaseFunction && _lockedPath === basePath) {
|
|
349
|
-
return true;
|
|
354
|
+
return { valid: true };
|
|
350
355
|
}
|
|
351
356
|
// Fallback: check the lock file PID
|
|
352
357
|
const lp = lockPath(basePath);
|
|
353
358
|
const existing = readExistingLockData(lp);
|
|
354
359
|
if (!existing) {
|
|
355
360
|
// Lock file was deleted — we lost ownership
|
|
356
|
-
return
|
|
361
|
+
return {
|
|
362
|
+
valid: false,
|
|
363
|
+
failureReason: "missing-metadata",
|
|
364
|
+
expectedPid: process.pid,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
if (existing.pid !== process.pid) {
|
|
368
|
+
return {
|
|
369
|
+
valid: false,
|
|
370
|
+
failureReason: "pid-mismatch",
|
|
371
|
+
existingPid: existing.pid,
|
|
372
|
+
expectedPid: process.pid,
|
|
373
|
+
};
|
|
357
374
|
}
|
|
358
|
-
return
|
|
375
|
+
return { valid: true };
|
|
376
|
+
}
|
|
377
|
+
export function validateSessionLock(basePath) {
|
|
378
|
+
return getSessionLockStatus(basePath).valid;
|
|
359
379
|
}
|
|
360
380
|
/**
|
|
361
381
|
* Release the session lock. Called on clean stop/pause.
|
|
@@ -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;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared formatting
|
|
2
|
+
* Shared pure formatting utilities — no @gsd/pi-tui dependency.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* ANSI-aware layout helpers (padRight, joinColumns, centerLine, fitColumns)
|
|
5
|
+
* live in layout-utils.ts to avoid pulling @gsd/pi-tui into modules that
|
|
6
|
+
* run outside jiti's alias resolution (e.g. HTML report generation via
|
|
7
|
+
* dynamic import in auto-loop).
|
|
6
8
|
*/
|
|
7
|
-
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
8
9
|
// ─── Duration Formatting ──────────────────────────────────────────────────────
|
|
9
10
|
/** Format a millisecond duration as a compact human-readable string. */
|
|
10
11
|
export function formatDuration(ms) {
|
|
@@ -30,43 +31,6 @@ export function formatTokenCount(count) {
|
|
|
30
31
|
return `${(count / 1000).toFixed(1)}k`;
|
|
31
32
|
return `${(count / 1_000_000).toFixed(2)}M`;
|
|
32
33
|
}
|
|
33
|
-
// ─── Layout Helpers ───────────────────────────────────────────────────────────
|
|
34
|
-
/** Pad a string with trailing spaces to fill `width` (ANSI-aware). */
|
|
35
|
-
export function padRight(content, width) {
|
|
36
|
-
const vis = visibleWidth(content);
|
|
37
|
-
return content + " ".repeat(Math.max(0, width - vis));
|
|
38
|
-
}
|
|
39
|
-
/** Build a line with left-aligned and right-aligned content. */
|
|
40
|
-
export function joinColumns(left, right, width) {
|
|
41
|
-
const leftW = visibleWidth(left);
|
|
42
|
-
const rightW = visibleWidth(right);
|
|
43
|
-
if (leftW + rightW + 2 > width) {
|
|
44
|
-
return truncateToWidth(`${left} ${right}`, width);
|
|
45
|
-
}
|
|
46
|
-
return left + " ".repeat(width - leftW - rightW) + right;
|
|
47
|
-
}
|
|
48
|
-
/** Center content within `width` (ANSI-aware). */
|
|
49
|
-
export function centerLine(content, width) {
|
|
50
|
-
const vis = visibleWidth(content);
|
|
51
|
-
if (vis >= width)
|
|
52
|
-
return truncateToWidth(content, width);
|
|
53
|
-
const leftPad = Math.floor((width - vis) / 2);
|
|
54
|
-
return " ".repeat(leftPad) + content;
|
|
55
|
-
}
|
|
56
|
-
/** Join as many parts as fit within `width`, separated by `separator`. */
|
|
57
|
-
export function fitColumns(parts, width, separator = " ") {
|
|
58
|
-
const filtered = parts.filter(Boolean);
|
|
59
|
-
if (filtered.length === 0)
|
|
60
|
-
return "";
|
|
61
|
-
let result = filtered[0];
|
|
62
|
-
for (let i = 1; i < filtered.length; i++) {
|
|
63
|
-
const candidate = `${result}${separator}${filtered[i]}`;
|
|
64
|
-
if (visibleWidth(candidate) > width)
|
|
65
|
-
break;
|
|
66
|
-
result = candidate;
|
|
67
|
-
}
|
|
68
|
-
return truncateToWidth(result, width);
|
|
69
|
-
}
|
|
70
34
|
// ─── Text Truncation ─────────────────────────────────────────────────────────
|
|
71
35
|
/** Truncate a string to `maxLength` characters, replacing the last character with an ellipsis if needed. */
|
|
72
36
|
export function truncateWithEllipsis(text, maxLength) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI-aware TUI layout utilities that depend on @gsd/pi-tui.
|
|
3
|
+
*
|
|
4
|
+
* Separated from format-utils.ts so that modules needing only pure
|
|
5
|
+
* formatting (e.g. HTML report generation) can import format-utils
|
|
6
|
+
* without pulling in the @gsd/pi-tui dependency — which fails when
|
|
7
|
+
* loaded outside jiti's alias resolution context.
|
|
8
|
+
*/
|
|
9
|
+
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
10
|
+
// ─── Layout Helpers ───────────────────────────────────────────────────────────
|
|
11
|
+
/** Pad a string with trailing spaces to fill `width` (ANSI-aware). */
|
|
12
|
+
export function padRight(content, width) {
|
|
13
|
+
const vis = visibleWidth(content);
|
|
14
|
+
return content + " ".repeat(Math.max(0, width - vis));
|
|
15
|
+
}
|
|
16
|
+
/** Build a line with left-aligned and right-aligned content. */
|
|
17
|
+
export function joinColumns(left, right, width) {
|
|
18
|
+
const leftW = visibleWidth(left);
|
|
19
|
+
const rightW = visibleWidth(right);
|
|
20
|
+
if (leftW + rightW + 2 > width) {
|
|
21
|
+
return truncateToWidth(`${left} ${right}`, width);
|
|
22
|
+
}
|
|
23
|
+
return left + " ".repeat(width - leftW - rightW) + right;
|
|
24
|
+
}
|
|
25
|
+
/** Center content within `width` (ANSI-aware). */
|
|
26
|
+
export function centerLine(content, width) {
|
|
27
|
+
const vis = visibleWidth(content);
|
|
28
|
+
if (vis >= width)
|
|
29
|
+
return truncateToWidth(content, width);
|
|
30
|
+
const leftPad = Math.floor((width - vis) / 2);
|
|
31
|
+
return " ".repeat(leftPad) + content;
|
|
32
|
+
}
|
|
33
|
+
/** Join as many parts as fit within `width`, separated by `separator`. */
|
|
34
|
+
export function fitColumns(parts, width, separator = " ") {
|
|
35
|
+
const filtered = parts.filter(Boolean);
|
|
36
|
+
if (filtered.length === 0)
|
|
37
|
+
return "";
|
|
38
|
+
let result = filtered[0];
|
|
39
|
+
for (let i = 1; i < filtered.length; i++) {
|
|
40
|
+
const candidate = `${result}${separator}${filtered[i]}`;
|
|
41
|
+
if (visibleWidth(candidate) > width)
|
|
42
|
+
break;
|
|
43
|
+
result = candidate;
|
|
44
|
+
}
|
|
45
|
+
return truncateToWidth(result, width);
|
|
46
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Barrel file — re-exports consumed by external modules
|
|
2
2
|
export { makeUI, GLYPH, INDENT, STATUS_GLYPH, STATUS_COLOR, } from "./ui.js";
|
|
3
|
-
export { stripAnsi, formatTokenCount, formatDuration,
|
|
3
|
+
export { stripAnsi, formatTokenCount, formatDuration, sparkline, normalizeStringArray, fileLink, } from "./format-utils.js";
|
|
4
|
+
export { padRight, joinColumns, centerLine, fitColumns, } from "./layout-utils.js";
|
|
4
5
|
export { shortcutDesc } from "./terminal.js";
|
|
5
6
|
export { toPosixPath } from "./path-display.js";
|
|
6
7
|
export { showInterviewRound } from "./interview-ui.js";
|
|
@@ -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
|
/**
|
|
@@ -24,6 +24,8 @@ import { formatTokenCount } from "../shared/mod.js";
|
|
|
24
24
|
import { discoverAgents } from "./agents.js";
|
|
25
25
|
import { createIsolation, mergeDeltaPatches, readIsolationMode, } from "./isolation.js";
|
|
26
26
|
import { registerWorker, updateWorker } from "./worker-registry.js";
|
|
27
|
+
import { loadEffectiveGSDPreferences } from "../gsd/preferences.js";
|
|
28
|
+
import { CmuxClient, shellEscape } from "../cmux/index.js";
|
|
27
29
|
const MAX_PARALLEL_TASKS = 8;
|
|
28
30
|
const MAX_CONCURRENCY = 4;
|
|
29
31
|
const COLLAPSED_ITEM_COUNT = 10;
|
|
@@ -191,6 +193,66 @@ function writePromptToTempFile(agentName, prompt) {
|
|
|
191
193
|
fs.writeFileSync(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
|
|
192
194
|
return { dir: tmpDir, filePath };
|
|
193
195
|
}
|
|
196
|
+
function buildSubagentProcessArgs(agent, task, tmpPromptPath) {
|
|
197
|
+
const args = ["--mode", "json", "-p", "--no-session"];
|
|
198
|
+
if (agent.model)
|
|
199
|
+
args.push("--model", agent.model);
|
|
200
|
+
if (agent.tools && agent.tools.length > 0)
|
|
201
|
+
args.push("--tools", agent.tools.join(","));
|
|
202
|
+
if (tmpPromptPath)
|
|
203
|
+
args.push("--append-system-prompt", tmpPromptPath);
|
|
204
|
+
args.push(`Task: ${task}`);
|
|
205
|
+
return args;
|
|
206
|
+
}
|
|
207
|
+
function processSubagentEventLine(line, currentResult, emitUpdate) {
|
|
208
|
+
if (!line.trim())
|
|
209
|
+
return;
|
|
210
|
+
let event;
|
|
211
|
+
try {
|
|
212
|
+
event = JSON.parse(line);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (event.type === "message_end" && event.message) {
|
|
218
|
+
const msg = event.message;
|
|
219
|
+
currentResult.messages.push(msg);
|
|
220
|
+
if (msg.role === "assistant") {
|
|
221
|
+
currentResult.usage.turns++;
|
|
222
|
+
const usage = msg.usage;
|
|
223
|
+
if (usage) {
|
|
224
|
+
currentResult.usage.input += usage.input || 0;
|
|
225
|
+
currentResult.usage.output += usage.output || 0;
|
|
226
|
+
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
|
227
|
+
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
|
228
|
+
currentResult.usage.cost += usage.cost?.total || 0;
|
|
229
|
+
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
230
|
+
}
|
|
231
|
+
if (!currentResult.model && msg.model)
|
|
232
|
+
currentResult.model = msg.model;
|
|
233
|
+
if (msg.stopReason)
|
|
234
|
+
currentResult.stopReason = msg.stopReason;
|
|
235
|
+
if (msg.errorMessage)
|
|
236
|
+
currentResult.errorMessage = msg.errorMessage;
|
|
237
|
+
}
|
|
238
|
+
emitUpdate();
|
|
239
|
+
}
|
|
240
|
+
if (event.type === "tool_result_end" && event.message) {
|
|
241
|
+
currentResult.messages.push(event.message);
|
|
242
|
+
emitUpdate();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function waitForFile(filePath, signal, timeoutMs = 30 * 60 * 1000) {
|
|
246
|
+
const started = Date.now();
|
|
247
|
+
while (Date.now() - started < timeoutMs) {
|
|
248
|
+
if (signal?.aborted)
|
|
249
|
+
return false;
|
|
250
|
+
if (fs.existsSync(filePath))
|
|
251
|
+
return true;
|
|
252
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
194
256
|
async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails) {
|
|
195
257
|
const agent = agents.find((a) => a.name === agentName);
|
|
196
258
|
if (!agent) {
|
|
@@ -206,11 +268,6 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
206
268
|
step,
|
|
207
269
|
};
|
|
208
270
|
}
|
|
209
|
-
const args = ["--mode", "json", "-p", "--no-session"];
|
|
210
|
-
if (agent.model)
|
|
211
|
-
args.push("--model", agent.model);
|
|
212
|
-
if (agent.tools && agent.tools.length > 0)
|
|
213
|
-
args.push("--tools", agent.tools.join(","));
|
|
214
271
|
let tmpPromptDir = null;
|
|
215
272
|
let tmpPromptPath = null;
|
|
216
273
|
const currentResult = {
|
|
@@ -237,9 +294,8 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
237
294
|
const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
238
295
|
tmpPromptDir = tmp.dir;
|
|
239
296
|
tmpPromptPath = tmp.filePath;
|
|
240
|
-
args.push("--append-system-prompt", tmpPromptPath);
|
|
241
297
|
}
|
|
242
|
-
args
|
|
298
|
+
const args = buildSubagentProcessArgs(agent, task, tmpPromptPath);
|
|
243
299
|
let wasAborted = false;
|
|
244
300
|
const exitCode = await new Promise((resolve) => {
|
|
245
301
|
const bundledPaths = (process.env.GSD_BUNDLED_EXTENSION_PATHS ?? "").split(path.delimiter).map(s => s.trim()).filter(Boolean);
|
|
@@ -247,50 +303,12 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
247
303
|
const proc = spawn(process.execPath, [process.env.GSD_BIN_PATH, ...extensionArgs, ...args], { cwd: cwd ?? defaultCwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
248
304
|
liveSubagentProcesses.add(proc);
|
|
249
305
|
let buffer = "";
|
|
250
|
-
const processLine = (line) => {
|
|
251
|
-
if (!line.trim())
|
|
252
|
-
return;
|
|
253
|
-
let event;
|
|
254
|
-
try {
|
|
255
|
-
event = JSON.parse(line);
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
if (event.type === "message_end" && event.message) {
|
|
261
|
-
const msg = event.message;
|
|
262
|
-
currentResult.messages.push(msg);
|
|
263
|
-
if (msg.role === "assistant") {
|
|
264
|
-
currentResult.usage.turns++;
|
|
265
|
-
const usage = msg.usage;
|
|
266
|
-
if (usage) {
|
|
267
|
-
currentResult.usage.input += usage.input || 0;
|
|
268
|
-
currentResult.usage.output += usage.output || 0;
|
|
269
|
-
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
|
270
|
-
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
|
271
|
-
currentResult.usage.cost += usage.cost?.total || 0;
|
|
272
|
-
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
273
|
-
}
|
|
274
|
-
if (!currentResult.model && msg.model)
|
|
275
|
-
currentResult.model = msg.model;
|
|
276
|
-
if (msg.stopReason)
|
|
277
|
-
currentResult.stopReason = msg.stopReason;
|
|
278
|
-
if (msg.errorMessage)
|
|
279
|
-
currentResult.errorMessage = msg.errorMessage;
|
|
280
|
-
}
|
|
281
|
-
emitUpdate();
|
|
282
|
-
}
|
|
283
|
-
if (event.type === "tool_result_end" && event.message) {
|
|
284
|
-
currentResult.messages.push(event.message);
|
|
285
|
-
emitUpdate();
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
306
|
proc.stdout.on("data", (data) => {
|
|
289
307
|
buffer += data.toString();
|
|
290
308
|
const lines = buffer.split("\n");
|
|
291
309
|
buffer = lines.pop() || "";
|
|
292
310
|
for (const line of lines)
|
|
293
|
-
|
|
311
|
+
processSubagentEventLine(line, currentResult, emitUpdate);
|
|
294
312
|
});
|
|
295
313
|
proc.stderr.on("data", (data) => {
|
|
296
314
|
currentResult.stderr += data.toString();
|
|
@@ -298,7 +316,7 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
298
316
|
proc.on("close", (code) => {
|
|
299
317
|
liveSubagentProcesses.delete(proc);
|
|
300
318
|
if (buffer.trim())
|
|
301
|
-
|
|
319
|
+
processSubagentEventLine(buffer, currentResult, emitUpdate);
|
|
302
320
|
resolve(code ?? 0);
|
|
303
321
|
});
|
|
304
322
|
proc.on("error", () => {
|
|
@@ -342,6 +360,103 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
342
360
|
}
|
|
343
361
|
}
|
|
344
362
|
}
|
|
363
|
+
async function runSingleAgentInCmuxSplit(cmuxClient, direction, defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails) {
|
|
364
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
365
|
+
if (!agent) {
|
|
366
|
+
return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
|
|
367
|
+
}
|
|
368
|
+
let tmpPromptDir = null;
|
|
369
|
+
let tmpPromptPath = null;
|
|
370
|
+
let tmpOutputDir = null;
|
|
371
|
+
const currentResult = {
|
|
372
|
+
agent: agentName,
|
|
373
|
+
agentSource: agent.source,
|
|
374
|
+
task,
|
|
375
|
+
exitCode: 0,
|
|
376
|
+
messages: [],
|
|
377
|
+
stderr: "",
|
|
378
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
379
|
+
model: agent.model,
|
|
380
|
+
step,
|
|
381
|
+
};
|
|
382
|
+
const emitUpdate = () => {
|
|
383
|
+
if (onUpdate) {
|
|
384
|
+
onUpdate({
|
|
385
|
+
content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
|
|
386
|
+
details: makeDetails([currentResult]),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
try {
|
|
391
|
+
if (agent.systemPrompt.trim()) {
|
|
392
|
+
const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
393
|
+
tmpPromptDir = tmp.dir;
|
|
394
|
+
tmpPromptPath = tmp.filePath;
|
|
395
|
+
}
|
|
396
|
+
tmpOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-cmux-"));
|
|
397
|
+
const stdoutPath = path.join(tmpOutputDir, "stdout.jsonl");
|
|
398
|
+
const stderrPath = path.join(tmpOutputDir, "stderr.log");
|
|
399
|
+
const exitPath = path.join(tmpOutputDir, "exit.code");
|
|
400
|
+
const cmuxSurfaceId = await cmuxClient.createSplit(direction);
|
|
401
|
+
if (!cmuxSurfaceId) {
|
|
402
|
+
return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
|
|
403
|
+
}
|
|
404
|
+
const bundledPaths = (process.env.GSD_BUNDLED_EXTENSION_PATHS ?? "").split(path.delimiter).map((s) => s.trim()).filter(Boolean);
|
|
405
|
+
const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
|
|
406
|
+
const processArgs = [process.env.GSD_BIN_PATH, ...extensionArgs, ...buildSubagentProcessArgs(agent, task, tmpPromptPath)];
|
|
407
|
+
const innerScript = [
|
|
408
|
+
`cd ${shellEscape(cwd ?? defaultCwd)}`,
|
|
409
|
+
"set -o pipefail",
|
|
410
|
+
`${shellEscape(process.execPath)} ${processArgs.map(shellEscape).join(" ")} 2> >(tee ${shellEscape(stderrPath)} >&2) | tee ${shellEscape(stdoutPath)}`,
|
|
411
|
+
"status=${PIPESTATUS[0]}",
|
|
412
|
+
`printf '%s' "$status" > ${shellEscape(exitPath)}`,
|
|
413
|
+
].join("; ");
|
|
414
|
+
const sent = await cmuxClient.sendSurface(cmuxSurfaceId, `bash -lc ${shellEscape(innerScript)}`);
|
|
415
|
+
if (!sent) {
|
|
416
|
+
return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
|
|
417
|
+
}
|
|
418
|
+
const finished = await waitForFile(exitPath, signal);
|
|
419
|
+
if (!finished) {
|
|
420
|
+
currentResult.exitCode = 1;
|
|
421
|
+
currentResult.stderr = "cmux split execution timed out or was aborted";
|
|
422
|
+
return currentResult;
|
|
423
|
+
}
|
|
424
|
+
if (fs.existsSync(stdoutPath)) {
|
|
425
|
+
const stdout = fs.readFileSync(stdoutPath, "utf-8");
|
|
426
|
+
for (const line of stdout.split("\n")) {
|
|
427
|
+
processSubagentEventLine(line, currentResult, emitUpdate);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (fs.existsSync(stderrPath)) {
|
|
431
|
+
currentResult.stderr = fs.readFileSync(stderrPath, "utf-8");
|
|
432
|
+
}
|
|
433
|
+
currentResult.exitCode = Number.parseInt(fs.readFileSync(exitPath, "utf-8").trim() || "1", 10) || 0;
|
|
434
|
+
return currentResult;
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
if (tmpPromptPath)
|
|
438
|
+
try {
|
|
439
|
+
fs.unlinkSync(tmpPromptPath);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
/* ignore */
|
|
443
|
+
}
|
|
444
|
+
if (tmpPromptDir)
|
|
445
|
+
try {
|
|
446
|
+
fs.rmdirSync(tmpPromptDir);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
/* ignore */
|
|
450
|
+
}
|
|
451
|
+
if (tmpOutputDir)
|
|
452
|
+
try {
|
|
453
|
+
fs.rmSync(tmpOutputDir, { recursive: true, force: true });
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
/* ignore */
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
345
460
|
const TaskItem = Type.Object({
|
|
346
461
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
347
462
|
task: Type.String({ description: "Task to delegate to the agent" }),
|
|
@@ -412,6 +527,8 @@ export default function (pi) {
|
|
|
412
527
|
const discovery = discoverAgents(ctx.cwd, agentScope);
|
|
413
528
|
const agents = discovery.agents;
|
|
414
529
|
const confirmProjectAgents = params.confirmProjectAgents ?? false;
|
|
530
|
+
const cmuxClient = CmuxClient.fromPreferences(loadEffectiveGSDPreferences()?.preferences);
|
|
531
|
+
const cmuxSplitsEnabled = cmuxClient.getConfig().splits;
|
|
415
532
|
// Resolve isolation mode
|
|
416
533
|
const isolationMode = readIsolationMode();
|
|
417
534
|
const useIsolation = Boolean(params.isolated) && isolationMode !== "none";
|
|
@@ -541,23 +658,24 @@ export default function (pi) {
|
|
|
541
658
|
const batchSize = params.tasks.length;
|
|
542
659
|
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
543
660
|
const workerId = registerWorker(t.agent, t.task, index, batchSize, batchId);
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Auto-retry failed tasks (likely API rate limit or transient error)
|
|
553
|
-
const isFailed = result.exitCode !== 0 || (result.messages.length === 0 && !signal?.aborted);
|
|
554
|
-
if (isFailed && MAX_RETRIES > 0 && !signal?.aborted) {
|
|
555
|
-
result = await runSingleAgent(ctx.cwd, agents, t.agent, t.task, t.cwd, undefined, signal, (partial) => {
|
|
661
|
+
const runTask = () => cmuxSplitsEnabled
|
|
662
|
+
? runSingleAgentInCmuxSplit(cmuxClient, index % 2 === 0 ? "right" : "down", ctx.cwd, agents, t.agent, t.task, t.cwd, undefined, signal, (partial) => {
|
|
663
|
+
if (partial.details?.results[0]) {
|
|
664
|
+
allResults[index] = partial.details.results[0];
|
|
665
|
+
emitParallelUpdate();
|
|
666
|
+
}
|
|
667
|
+
}, makeDetails("parallel"))
|
|
668
|
+
: runSingleAgent(ctx.cwd, agents, t.agent, t.task, t.cwd, undefined, signal, (partial) => {
|
|
556
669
|
if (partial.details?.results[0]) {
|
|
557
670
|
allResults[index] = partial.details.results[0];
|
|
558
671
|
emitParallelUpdate();
|
|
559
672
|
}
|
|
560
673
|
}, makeDetails("parallel"));
|
|
674
|
+
let result = await runTask();
|
|
675
|
+
// Auto-retry failed tasks (likely API rate limit or transient error)
|
|
676
|
+
const isFailed = result.exitCode !== 0 || (result.messages.length === 0 && !signal?.aborted);
|
|
677
|
+
if (isFailed && MAX_RETRIES > 0 && !signal?.aborted) {
|
|
678
|
+
result = await runTask();
|
|
561
679
|
}
|
|
562
680
|
updateWorker(workerId, result.exitCode === 0 ? "completed" : "failed");
|
|
563
681
|
allResults[index] = result;
|
|
@@ -591,7 +709,9 @@ export default function (pi) {
|
|
|
591
709
|
const taskId = crypto.randomUUID();
|
|
592
710
|
isolation = await createIsolation(effectiveCwd, taskId, isolationMode);
|
|
593
711
|
}
|
|
594
|
-
const result =
|
|
712
|
+
const result = cmuxSplitsEnabled
|
|
713
|
+
? await runSingleAgentInCmuxSplit(cmuxClient, "right", ctx.cwd, agents, params.agent, params.task, isolation ? isolation.workDir : params.cwd, undefined, signal, onUpdate, makeDetails("single"))
|
|
714
|
+
: await runSingleAgent(ctx.cwd, agents, params.agent, params.task, isolation ? isolation.workDir : params.cwd, undefined, signal, onUpdate, makeDetails("single"));
|
|
595
715
|
// Capture and merge delta if isolated
|
|
596
716
|
if (isolation) {
|
|
597
717
|
const patches = await isolation.captureDelta();
|