pi-bro 0.21.0 → 0.22.1
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/CHANGELOG.md +85 -0
- package/README.md +117 -945
- package/backend.ts +55 -42
- package/bro.ts +167 -1360
- package/config-ui.ts +325 -0
- package/docs/README.md +8 -2
- package/docs/guided-review.md +131 -0
- package/docs/pig-compatibility.md +2 -1
- package/docs/user-guide.md +986 -0
- package/package.json +20 -5
- package/prompt.ts +19 -4
- package/review-modal.ts +1272 -0
- package/review-source.ts +296 -0
- package/review-ui.ts +519 -0
- package/review.ts +586 -0
- package/settings.ts +505 -0
- package/sources.ts +421 -0
- package/util.ts +26 -0
package/backend.ts
CHANGED
|
@@ -3,14 +3,17 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
-
// Shared internal execution boundary for
|
|
7
|
-
// This implements docs/plans/2026-09-22-shared-backend-design.md
|
|
8
|
-
// Claude Code
|
|
9
|
-
// single-attempt cleanup. Feature code
|
|
10
|
-
// settings.
|
|
6
|
+
// Shared internal execution boundary for explain, show, btw, advisor, and review.
|
|
7
|
+
// This implements docs/plans/2026-09-22-shared-backend-design.md across all supported backends
|
|
8
|
+
// (Agy, Claude Code, Grok, Codex, Muse): it owns CLI selection, process invocation,
|
|
9
|
+
// progress/outcome normalization, continuation, and single-attempt cleanup. Feature code
|
|
10
|
+
// keeps retries, UI, source/session capture and settings.
|
|
11
11
|
|
|
12
|
-
export type BackendFeature = "explain" | "show" | "btw" | "advisor";
|
|
12
|
+
export type BackendFeature = "explain" | "show" | "btw" | "advisor" | "review";
|
|
13
13
|
export type BackendAccess = "restricted" | "workspace-full";
|
|
14
|
+
export function reviewBackendError(backend: string): string | undefined {
|
|
15
|
+
if (backend !== "claude" && backend !== "muse") return "Guided Review requires Claude or Muse for file-only inspection. Set the review backend in /bro config; saved reviews remain readable.";
|
|
16
|
+
}
|
|
14
17
|
export type AgySelection = { model: string; effort?: "low" | "medium" | "high" };
|
|
15
18
|
export const CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
16
19
|
export const GROK_EFFORTS = ["low", "medium", "high", "xhigh"] as const;
|
|
@@ -90,8 +93,8 @@ function unexpectedSignalMessage(exitSignal: NodeJS.Signals | null, cli = "Agy")
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
// Sends to the whole POSIX process group when possible so a misbehaving grandchild dies too, not
|
|
93
|
-
// just the immediate
|
|
94
|
-
function
|
|
96
|
+
// just the immediate child process -- child.kill() alone only ever reaches the immediate child.
|
|
97
|
+
function killProcessGroup(child: ChildProcess, signalName: NodeJS.Signals): void {
|
|
95
98
|
if (process.platform !== "win32" && typeof child.pid === "number") {
|
|
96
99
|
try {
|
|
97
100
|
process.kill(-child.pid, signalName);
|
|
@@ -104,7 +107,7 @@ function killAgyGroup(child: ChildProcess, signalName: NodeJS.Signals): void {
|
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
// The three causes that stop an in-flight attempt: user cancellation, the host-imposed deadline,
|
|
107
|
-
// and a protocol failure (malformed/inconsistent
|
|
110
|
+
// and a protocol failure (malformed/inconsistent backend output). Exactly one is latched -- the first
|
|
108
111
|
// to occur -- and it is never relabeled by a later signal (e.g. a cancel arriving after a deadline
|
|
109
112
|
// already fired stays a timeout, not a cancellation).
|
|
110
113
|
type StopCause = "cancelled" | "timeout" | "protocol";
|
|
@@ -139,9 +142,9 @@ export function beginAttempt(child: ChildProcess, signal: AbortSignal, deadlineM
|
|
|
139
142
|
if (cause) return; // latched: the first stop cause wins
|
|
140
143
|
cause = next;
|
|
141
144
|
if (isClosed) return;
|
|
142
|
-
|
|
145
|
+
killProcessGroup(child, "SIGTERM");
|
|
143
146
|
killTimer = setTimeout(() => {
|
|
144
|
-
|
|
147
|
+
killProcessGroup(child, "SIGKILL");
|
|
145
148
|
// Detached descendants (or Windows grandchildren) may retain inherited pipes.
|
|
146
149
|
// Stop waiting on those pipes after escalation; never promote this stop to success.
|
|
147
150
|
child.stdin?.destroy();
|
|
@@ -229,12 +232,12 @@ async function executeArgvPrint(
|
|
|
229
232
|
// stdin protocol rather than risking Linux's per-argument byte limit.
|
|
230
233
|
const stdinPrompt = Buffer.byteLength(request.prompt, "utf8") >= 120_000;
|
|
231
234
|
const isBtw = request.feature === "btw";
|
|
232
|
-
const full =
|
|
233
|
-
const deadlineMs = deadlineMsOverride ?? (
|
|
235
|
+
const full = request.access === "workspace-full";
|
|
236
|
+
const deadlineMs = deadlineMsOverride ?? (full ? 610_000 : isBtw ? 130_000 : 125_000);
|
|
234
237
|
const printTimeout = full ? "10m" : "2m";
|
|
235
|
-
const action = isBtw ? "answer the side question" : "simplify the response";
|
|
236
|
-
const timeoutVerb = isBtw ? "during the side conversation" : "while simplifying the response";
|
|
237
|
-
const emptyTextMessage = isBtw ? "Agy returned no answer for the side question." : "Agy returned no final explanation.";
|
|
238
|
+
const action = request.feature === "review" ? "complete the guided review request" : isBtw ? "answer the side question" : "simplify the response";
|
|
239
|
+
const timeoutVerb = request.feature === "review" ? "during the guided review request" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
240
|
+
const emptyTextMessage = request.feature === "review" ? "Agy returned no response for Guided Review." : isBtw ? "Agy returned no answer for the side question." : "Agy returned no final explanation.";
|
|
238
241
|
|
|
239
242
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
240
243
|
|
|
@@ -257,7 +260,7 @@ async function executeArgvPrint(
|
|
|
257
260
|
...(selection.effort ? ["--effort", selection.effort] : []),
|
|
258
261
|
"--print-timeout",
|
|
259
262
|
printTimeout,
|
|
260
|
-
...(request.continuation ? ["--conversation", request.continuation.id] : []),
|
|
263
|
+
...(isBtw && request.continuation ? ["--conversation", request.continuation.id] : []),
|
|
261
264
|
...(stdinPrompt ? ["--input-format", "stream-json"] : ["--print", request.prompt]),
|
|
262
265
|
],
|
|
263
266
|
{
|
|
@@ -565,12 +568,13 @@ async function executeClaude(
|
|
|
565
568
|
): Promise<BackendOutcome> {
|
|
566
569
|
const isAdvisor = request.feature === "advisor";
|
|
567
570
|
const isBtw = request.feature === "btw";
|
|
571
|
+
const inspect = request.feature === "review";
|
|
568
572
|
const full = request.access === "workspace-full";
|
|
569
|
-
const deadlineMs = deadlineMsOverride ?? (full ? 610_000 : isBtw ? 130_000 : 125_000);
|
|
570
|
-
const action = isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
573
|
+
const deadlineMs = deadlineMsOverride ?? (full || inspect ? 610_000 : isBtw ? 130_000 : 125_000);
|
|
574
|
+
const action = request.feature === "review" ? "complete the guided review request" : isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
571
575
|
|
|
572
576
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
573
|
-
const runDirectory =
|
|
577
|
+
const runDirectory = full || isBtw || inspect ? undefined : await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
574
578
|
if (signal.aborted) {
|
|
575
579
|
if (runDirectory) await rm(runDirectory, { recursive: true, force: true });
|
|
576
580
|
return { status: "cancelled", message: "Canceled." };
|
|
@@ -588,7 +592,9 @@ async function executeClaude(
|
|
|
588
592
|
"stream-json",
|
|
589
593
|
"--verbose",
|
|
590
594
|
"--include-partial-messages",
|
|
591
|
-
...(
|
|
595
|
+
...(inspect
|
|
596
|
+
? ["--restricted", "--tools", "Read,Grep,Glob", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--permission-mode", "dontAsk"]
|
|
597
|
+
: full
|
|
592
598
|
? ["--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--dangerously-skip-permissions"]
|
|
593
599
|
: ["--tools", "", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--permission-mode", "dontAsk"]),
|
|
594
600
|
...(isBtw && request.continuation ? ["--resume", request.continuation.id] : []),
|
|
@@ -685,7 +691,7 @@ async function executeClaude(
|
|
|
685
691
|
const cause = attempt.causeOf();
|
|
686
692
|
if (cause === "cancelled") return { status: "cancelled", message: "Canceled.", partialText };
|
|
687
693
|
if (cause === "timeout") {
|
|
688
|
-
const during = isAdvisor ? "during the advisor consultation" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
694
|
+
const during = request.feature === "review" ? "during the guided review request" : isAdvisor ? "during the advisor consultation" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
689
695
|
return { status: "timeout", message: `Claude timed out ${during}. Run \`/bro doctor\` for setup help.`, partialText };
|
|
690
696
|
}
|
|
691
697
|
if (protocolError) return { status: "failure", message: withDoctor(protocolError), partialText };
|
|
@@ -706,7 +712,7 @@ async function executeClaude(
|
|
|
706
712
|
return { status: "failure", message: withDoctor(`Claude exited without a result event${stderr.trim() ? `: ${stderr.trim()}` : "."}`), partialText };
|
|
707
713
|
}
|
|
708
714
|
const text = final.trim();
|
|
709
|
-
if (!text) return { status: "failure", message: withDoctor(isBtw ? "Claude returned no answer for the side question." : "Claude returned no final answer."), partialText };
|
|
715
|
+
if (!text) return { status: "failure", message: withDoctor(request.feature === "review" ? "Claude returned no response for Guided Review." : isBtw ? "Claude returned no answer for the side question." : "Claude returned no final answer."), partialText };
|
|
710
716
|
if (!isBtw) return { status: "success", text };
|
|
711
717
|
// btw continuation: the session id must be reported, consistent, and (on resume) unchanged.
|
|
712
718
|
const sessionId = resultSessionId ?? initSessionId;
|
|
@@ -767,10 +773,10 @@ async function executeGrok(
|
|
|
767
773
|
): Promise<BackendOutcome> {
|
|
768
774
|
const isAdvisor = request.feature === "advisor";
|
|
769
775
|
const isBtw = request.feature === "btw";
|
|
770
|
-
const deadlineMs = deadlineMsOverride ?? (
|
|
771
|
-
const action = isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
772
|
-
const timeoutVerb = isAdvisor ? "during the advisor consultation" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
773
|
-
const emptyTextMessage = isAdvisor ? "Grok returned no advice." : isBtw ? "Grok returned no answer for the side question." : "Grok returned no final explanation.";
|
|
776
|
+
const deadlineMs = deadlineMsOverride ?? (request.access === "workspace-full" ? 610_000 : isBtw ? 130_000 : 125_000);
|
|
777
|
+
const action = request.feature === "review" ? "complete the guided review request" : isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
778
|
+
const timeoutVerb = request.feature === "review" ? "during the guided review request" : isAdvisor ? "during the advisor consultation" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
779
|
+
const emptyTextMessage = request.feature === "review" ? "Grok returned no response for Guided Review." : isAdvisor ? "Grok returned no advice." : isBtw ? "Grok returned no answer for the side question." : "Grok returned no final explanation.";
|
|
774
780
|
const prompt = request.access === "restricted" ? `${GROK_RESTRICTED_PREFIX}\n\n${request.prompt}` : request.prompt;
|
|
775
781
|
|
|
776
782
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
@@ -780,7 +786,7 @@ async function executeGrok(
|
|
|
780
786
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
781
787
|
const promptFile = join(promptDirectory, "prompt.txt");
|
|
782
788
|
await writeFile(promptFile, prompt, { encoding: "utf8", mode: 0o600 });
|
|
783
|
-
if (
|
|
789
|
+
if (request.access !== "workspace-full" && !isBtw) runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
784
790
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
785
791
|
|
|
786
792
|
const child = spawn(
|
|
@@ -1003,12 +1009,12 @@ async function executeCodex(
|
|
|
1003
1009
|
const isBtw = request.feature === "btw";
|
|
1004
1010
|
const full = request.access === "workspace-full";
|
|
1005
1011
|
const deadlineMs = deadlineMsOverride ?? (full ? 610_000 : isBtw ? 130_000 : 125_000);
|
|
1006
|
-
const action = isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
1007
|
-
const timeoutVerb = isAdvisor ? "during the advisor consultation" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
1008
|
-
const emptyTextMessage = isAdvisor ? "Codex returned no advice." : isBtw ? "Codex returned no answer for the side question." : "Codex returned no final explanation.";
|
|
1012
|
+
const action = request.feature === "review" ? "complete the guided review request" : isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
1013
|
+
const timeoutVerb = request.feature === "review" ? "during the guided review request" : isAdvisor ? "during the advisor consultation" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
1014
|
+
const emptyTextMessage = request.feature === "review" ? "Codex returned no response for Guided Review." : isAdvisor ? "Codex returned no advice." : isBtw ? "Codex returned no answer for the side question." : "Codex returned no final explanation.";
|
|
1009
1015
|
|
|
1010
1016
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
1011
|
-
const runDirectory =
|
|
1017
|
+
const runDirectory = full || isBtw ? undefined : await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
1012
1018
|
if (signal.aborted) {
|
|
1013
1019
|
if (runDirectory) await rm(runDirectory, { recursive: true, force: true });
|
|
1014
1020
|
return { status: "cancelled", message: "Canceled." };
|
|
@@ -1172,11 +1178,12 @@ async function executeMuse(
|
|
|
1172
1178
|
): Promise<BackendOutcome> {
|
|
1173
1179
|
const isAdvisor = request.feature === "advisor";
|
|
1174
1180
|
const isBtw = request.feature === "btw";
|
|
1181
|
+
const inspect = request.feature === "review";
|
|
1175
1182
|
const full = request.access === "workspace-full";
|
|
1176
|
-
const deadlineMs = deadlineMsOverride ?? (full ? 610_000 : isBtw ? 130_000 : 125_000);
|
|
1177
|
-
const action = isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
1178
|
-
const timeoutVerb = isAdvisor ? "during the advisor consultation" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
1179
|
-
const emptyTextMessage = isAdvisor ? "Muse returned no advice." : isBtw ? "Muse returned no answer for the side question." : "Muse returned no final explanation.";
|
|
1183
|
+
const deadlineMs = deadlineMsOverride ?? (full || inspect ? 610_000 : isBtw ? 130_000 : 125_000);
|
|
1184
|
+
const action = request.feature === "review" ? "complete the guided review request" : isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
1185
|
+
const timeoutVerb = request.feature === "review" ? "during the guided review request" : isAdvisor ? "during the advisor consultation" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
1186
|
+
const emptyTextMessage = request.feature === "review" ? "Muse returned no response for Guided Review." : isAdvisor ? "Muse returned no advice." : isBtw ? "Muse returned no answer for the side question." : "Muse returned no final explanation.";
|
|
1180
1187
|
|
|
1181
1188
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
1182
1189
|
const promptDirectory = await mkdtemp(join(tmpdir(), "pi-bro-muse-"));
|
|
@@ -1186,7 +1193,7 @@ async function executeMuse(
|
|
|
1186
1193
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
1187
1194
|
const promptFile = join(promptDirectory, "prompt.txt");
|
|
1188
1195
|
await writeFile(promptFile, request.prompt, { encoding: "utf8", mode: 0o600 });
|
|
1189
|
-
if (!
|
|
1196
|
+
if (!full && !isBtw && !inspect) runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
1190
1197
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
1191
1198
|
const workspace = runDirectory ?? request.cwd!;
|
|
1192
1199
|
|
|
@@ -1197,6 +1204,7 @@ async function executeMuse(
|
|
|
1197
1204
|
"--workspace", workspace,
|
|
1198
1205
|
...(!isBtw ? ["--no-session-log"] : []),
|
|
1199
1206
|
...(full ? ["--yolo"] : ["--disable-approval", "--disable-write", "--disable-shell"]),
|
|
1207
|
+
...(inspect ? ["--disable-web-tools", "--no-foreign-personal-context"] : []),
|
|
1200
1208
|
...(isBtw && request.continuation ? ["--session-id", request.continuation.id] : []),
|
|
1201
1209
|
"--model", selection.model,
|
|
1202
1210
|
...(selection.effort ? ["--reasoning-effort", selection.effort] : []),
|
|
@@ -1341,7 +1349,7 @@ async function executeMuse(
|
|
|
1341
1349
|
}
|
|
1342
1350
|
}
|
|
1343
1351
|
|
|
1344
|
-
// Single-attempt executor shared by all
|
|
1352
|
+
// Single-attempt executor shared by all features. Never retries (retries are feature-owned,
|
|
1345
1353
|
// e.g. advisor's 3-attempt backoff in bro.ts); never spawns a pre-aborted request; on cancellation,
|
|
1346
1354
|
// host deadline, or a protocol failure, stops the whole POSIX process group (SIGTERM, then SIGKILL
|
|
1347
1355
|
// after a bounded grace period) before resolving. `options` is for offline tests only -- production
|
|
@@ -1356,10 +1364,15 @@ export async function execute(
|
|
|
1356
1364
|
if (
|
|
1357
1365
|
(request.access === "workspace-full" && !request.cwd?.trim()) ||
|
|
1358
1366
|
(request.feature === "advisor" && request.access !== "workspace-full") ||
|
|
1367
|
+
(request.feature === "review" && (request.access !== "restricted" || !request.cwd?.trim())) ||
|
|
1359
1368
|
((request.feature === "explain" || request.feature === "show") && request.access !== "restricted") ||
|
|
1360
1369
|
(request.feature !== "btw" && request.continuation)
|
|
1361
1370
|
) return { status: "failure", message: "Unsupported execution request: check feature access, workspace cwd and continuation." };
|
|
1362
1371
|
const backend = (selection as { backend?: unknown }).backend;
|
|
1372
|
+
if (request.feature === "review") {
|
|
1373
|
+
const error = reviewBackendError(String(backend ?? "agy"));
|
|
1374
|
+
if (error) return { status: "failure", message: error };
|
|
1375
|
+
}
|
|
1363
1376
|
// An explicit tag guard: a stale or corrupt runtime tag must fail, never fall through to Agy.
|
|
1364
1377
|
if (backend !== undefined && backend !== "agy" && backend !== "claude" && backend !== "grok" && backend !== "codex" && backend !== "muse") {
|
|
1365
1378
|
return { status: "failure", message: `Unknown backend ${JSON.stringify(backend)}: pick Agy, Claude, Grok, Codex or Muse in \`/bro config\`.` };
|
|
@@ -1367,7 +1380,7 @@ export async function execute(
|
|
|
1367
1380
|
const killEscalationMs = options?.killEscalationMs ?? DEFAULT_KILL_ESCALATION_MS;
|
|
1368
1381
|
if (selection.backend === "codex") {
|
|
1369
1382
|
// Codex btw always runs (and resumes) in the caller's workspace, even restricted.
|
|
1370
|
-
if (request.feature === "btw" && !request.cwd?.trim()) {
|
|
1383
|
+
if ((request.feature === "btw" || request.feature === "review") && !request.cwd?.trim()) {
|
|
1371
1384
|
return { status: "failure", message: "Unsupported execution request: check feature access, workspace cwd and continuation." };
|
|
1372
1385
|
}
|
|
1373
1386
|
if (typeof selection.model !== "string" || !selection.model.trim() || (selection.effort !== undefined && !CODEX_EFFORTS.includes(selection.effort))) {
|
|
@@ -1377,7 +1390,7 @@ export async function execute(
|
|
|
1377
1390
|
}
|
|
1378
1391
|
if (selection.backend === "muse") {
|
|
1379
1392
|
// Muse btw always runs (and resumes) in the caller's workspace, even restricted.
|
|
1380
|
-
if (request.feature === "btw" && !request.cwd?.trim()) {
|
|
1393
|
+
if ((request.feature === "btw" || request.feature === "review") && !request.cwd?.trim()) {
|
|
1381
1394
|
return { status: "failure", message: "Unsupported execution request: check feature access, workspace cwd and continuation." };
|
|
1382
1395
|
}
|
|
1383
1396
|
if (typeof selection.model !== "string" || !selection.model.trim() || (selection.effort !== undefined && !MUSE_EFFORTS.includes(selection.effort))) {
|
|
@@ -1387,7 +1400,7 @@ export async function execute(
|
|
|
1387
1400
|
}
|
|
1388
1401
|
if (selection.backend === "grok") {
|
|
1389
1402
|
// Grok btw always runs (and resumes) in the caller's workspace, even restricted.
|
|
1390
|
-
if (request.feature === "btw" && !request.cwd?.trim()) {
|
|
1403
|
+
if ((request.feature === "btw" || request.feature === "review") && !request.cwd?.trim()) {
|
|
1391
1404
|
return { status: "failure", message: "Unsupported execution request: check feature access, workspace cwd and continuation." };
|
|
1392
1405
|
}
|
|
1393
1406
|
if (typeof selection.model !== "string" || !selection.model.trim() || (selection.effort !== undefined && !GROK_EFFORTS.includes(selection.effort))) {
|
|
@@ -1397,7 +1410,7 @@ export async function execute(
|
|
|
1397
1410
|
}
|
|
1398
1411
|
if (selection.backend === "claude") {
|
|
1399
1412
|
// Claude btw always runs (and resumes) in the caller's workspace, even restricted.
|
|
1400
|
-
if (request.feature === "btw" && !request.cwd?.trim()) {
|
|
1413
|
+
if ((request.feature === "btw" || request.feature === "review") && !request.cwd?.trim()) {
|
|
1401
1414
|
return { status: "failure", message: "Unsupported execution request: check feature access, workspace cwd and continuation." };
|
|
1402
1415
|
}
|
|
1403
1416
|
if (!selection.model.trim() || (selection.effort !== undefined && !CLAUDE_EFFORTS.includes(selection.effort))) {
|