pi-bro 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +10 -9
- package/docs/guided-review.md +8 -12
- package/docs/user-guide.md +38 -19
- package/package.json +8 -2
- package/src/advisor.ts +573 -0
- package/src/backend.ts +100 -42
- package/src/bro.ts +37 -2623
- package/src/btw.ts +556 -0
- package/src/config-ui.ts +76 -39
- package/src/doctor.ts +538 -0
- package/src/explain-modal.ts +777 -0
- package/src/modal-kit.ts +258 -0
- package/src/review-modal.ts +95 -48
- package/src/review-ui.ts +57 -47
- package/src/settings.ts +45 -3
- package/src/show.ts +243 -0
- package/src/ui-capabilities.ts +32 -0
package/src/backend.ts
CHANGED
|
@@ -8,12 +8,28 @@ import { join } from "node:path";
|
|
|
8
8
|
// (Agy, Claude Code, Grok, Codex, Muse): it owns CLI selection, process invocation,
|
|
9
9
|
// progress/outcome normalization, continuation, and single-attempt cleanup. Feature code
|
|
10
10
|
// keeps retries, UI, source/session capture and settings.
|
|
11
|
+
// For Guided Review (`feature: "review"`), Claude and Muse enforce strict file-only inspection;
|
|
12
|
+
// Agy, Grok, and Codex run inside their own sandbox with shell available, and the UI owns telling
|
|
13
|
+
// the user via `reviewBackendNotice`.
|
|
11
14
|
|
|
12
15
|
export type BackendFeature = "explain" | "show" | "btw" | "advisor" | "review";
|
|
13
16
|
export type BackendAccess = "restricted" | "workspace-full";
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
// Per-backend truth, not one shared sentence: Codex and Grok have a read-only sandbox, Agy's
|
|
18
|
+
// sandbox only restricts terminal commands and still lets it write inside the project. Kept short
|
|
19
|
+
// enough that the whole notice fits the review window's advisory rows on a narrow terminal.
|
|
20
|
+
export function reviewBackendNotice(backend: "agy" | "claude" | "grok" | "codex" | "muse"): string | undefined {
|
|
21
|
+
if (backend === "claude" || backend === "muse") return undefined;
|
|
22
|
+
const sandbox = backend === "agy" ? "sandboxed shell (project writes allowed)" : "sandboxed shell in a read-only sandbox";
|
|
23
|
+
return `Guided Review on ${backend} uses ${sandbox}, not strict file-only inspection. Claude or Muse is recommended for untrusted PRs.`;
|
|
16
24
|
}
|
|
25
|
+
// One default deadline table for every adapter: workspace-full, Guided Review and the advisor get
|
|
26
|
+
// the long bound; btw the middle one; explain/show the short one.
|
|
27
|
+
export const REVIEW_DEADLINE_MS = 610_000;
|
|
28
|
+
export function backendDeadlineMs(feature: BackendFeature, access: BackendAccess): number {
|
|
29
|
+
if (access === "workspace-full" || feature === "review") return REVIEW_DEADLINE_MS;
|
|
30
|
+
return feature === "btw" ? 130_000 : 125_000;
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
export type AgySelection = { model: string; effort?: "low" | "medium" | "high" };
|
|
18
34
|
export const CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
19
35
|
export const GROK_EFFORTS = ["low", "medium", "high", "xhigh"] as const;
|
|
@@ -232,16 +248,17 @@ async function executeArgvPrint(
|
|
|
232
248
|
// stdin protocol rather than risking Linux's per-argument byte limit.
|
|
233
249
|
const stdinPrompt = Buffer.byteLength(request.prompt, "utf8") >= 120_000;
|
|
234
250
|
const isBtw = request.feature === "btw";
|
|
251
|
+
const inspect = request.feature === "review";
|
|
235
252
|
const full = request.access === "workspace-full";
|
|
236
|
-
const deadlineMs = deadlineMsOverride ?? (
|
|
237
|
-
const printTimeout = full ? "10m" : "2m";
|
|
238
|
-
const action =
|
|
239
|
-
const timeoutVerb =
|
|
240
|
-
const emptyTextMessage =
|
|
253
|
+
const deadlineMs = deadlineMsOverride ?? backendDeadlineMs(request.feature, request.access);
|
|
254
|
+
const printTimeout = full || inspect ? "10m" : "2m";
|
|
255
|
+
const action = inspect ? "complete the guided review request" : isBtw ? "answer the side question" : "simplify the response";
|
|
256
|
+
const timeoutVerb = inspect ? "during the guided review request" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
257
|
+
const emptyTextMessage = inspect ? "Agy returned no response for Guided Review." : isBtw ? "Agy returned no answer for the side question." : "Agy returned no final explanation.";
|
|
241
258
|
|
|
242
259
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
243
260
|
|
|
244
|
-
const runDirectory = full ? undefined : await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
261
|
+
const runDirectory = full || inspect ? undefined : await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
245
262
|
if (signal.aborted) {
|
|
246
263
|
if (runDirectory) await rm(runDirectory, { recursive: true, force: true });
|
|
247
264
|
return { status: "cancelled", message: "Canceled." };
|
|
@@ -264,7 +281,7 @@ async function executeArgvPrint(
|
|
|
264
281
|
...(stdinPrompt ? ["--input-format", "stream-json"] : ["--print", request.prompt]),
|
|
265
282
|
],
|
|
266
283
|
{
|
|
267
|
-
cwd: full ? request.cwd : runDirectory,
|
|
284
|
+
cwd: full || inspect ? request.cwd : runDirectory,
|
|
268
285
|
stdio: [stdinPrompt ? "pipe" : "ignore", "pipe", "pipe"],
|
|
269
286
|
windowsHide: true,
|
|
270
287
|
detached: process.platform !== "win32",
|
|
@@ -446,7 +463,7 @@ async function executeAdvisorStdin(
|
|
|
446
463
|
): Promise<BackendOutcome> {
|
|
447
464
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
448
465
|
|
|
449
|
-
const deadlineMs = deadlineMsOverride ??
|
|
466
|
+
const deadlineMs = deadlineMsOverride ?? backendDeadlineMs(request.feature, request.access);
|
|
450
467
|
const child = spawn(
|
|
451
468
|
"agy",
|
|
452
469
|
[
|
|
@@ -570,7 +587,7 @@ async function executeClaude(
|
|
|
570
587
|
const isBtw = request.feature === "btw";
|
|
571
588
|
const inspect = request.feature === "review";
|
|
572
589
|
const full = request.access === "workspace-full";
|
|
573
|
-
const deadlineMs = deadlineMsOverride ?? (
|
|
590
|
+
const deadlineMs = deadlineMsOverride ?? backendDeadlineMs(request.feature, request.access);
|
|
574
591
|
const action = request.feature === "review" ? "complete the guided review request" : isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
575
592
|
|
|
576
593
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
@@ -751,18 +768,21 @@ export const GROK_RESTRICTED_PREFIX =
|
|
|
751
768
|
"Bro restricted mode (a request, not a technical restriction): answer only from the context supplied in this prompt. " +
|
|
752
769
|
"Do not inspect, read, or write workspace files, run commands, or use web or other external tools unless the user explicitly asks you to in this prompt.";
|
|
753
770
|
|
|
754
|
-
// Grok runs
|
|
755
|
-
//
|
|
771
|
+
// Grok runs explain, show, btw, and advisor with all capabilities on (--sandbox off,
|
|
772
|
+
// bypassPermissions, no tool blacklist); Guided Review (`review`) runs in the caller's workspace
|
|
773
|
+
// under Grok's read-only sandbox (--sandbox read-only, --permission-mode dontAsk).
|
|
774
|
+
// explain/show run in a fresh mkdtemp scratch cwd removed afterwards (the Agy pattern);
|
|
756
775
|
// btw (both accesses) runs in the caller's workspace cwd and continues natively with --resume, whose
|
|
757
|
-
// session keeps the cwd it was created in; the advisor
|
|
758
|
-
// requests
|
|
759
|
-
//
|
|
776
|
+
// session keeps the cwd it was created in; the advisor and review run fresh in the workspace.
|
|
777
|
+
// Restricted requests other than review get GROK_RESTRICTED_PREFIX (review must be allowed to
|
|
778
|
+
// inspect the captured checkout). The prompt goes through a private 0600 file in its own mkdtemp
|
|
779
|
+
// directory, removed once the attempt ends.
|
|
760
780
|
// Only a top-level terminal `result` with success/is_error false/end_turn is authoritative; the
|
|
761
781
|
// first failure (a failed result or a top-level `error` event, even after a success) is latched.
|
|
762
|
-
// Nested (subagent) frames are ignored. The advisor reports activity labels only; explain/show/btw
|
|
763
|
-
// report top-level text_delta progress, falling back to an assistant message's text
|
|
764
|
-
// no delta streamed for it. Thinking is never reported. btw requires init/result
|
|
765
|
-
// agree (and to equal the resumed id) and returns that id as the continuation.
|
|
782
|
+
// Nested (subagent) frames are ignored. The advisor reports activity labels only; explain/show/btw/review
|
|
783
|
+
// report top-level text_delta progress, falling back to an assistant message's text
|
|
784
|
+
// blocks only when no delta streamed for it. Thinking is never reported. btw requires init/result
|
|
785
|
+
// session ids to agree (and to equal the resumed id) and returns that id as the continuation.
|
|
766
786
|
async function executeGrok(
|
|
767
787
|
request: BackendRequest,
|
|
768
788
|
selection: { model: string; effort?: string },
|
|
@@ -773,11 +793,13 @@ async function executeGrok(
|
|
|
773
793
|
): Promise<BackendOutcome> {
|
|
774
794
|
const isAdvisor = request.feature === "advisor";
|
|
775
795
|
const isBtw = request.feature === "btw";
|
|
776
|
-
const
|
|
777
|
-
const
|
|
778
|
-
const
|
|
779
|
-
const
|
|
780
|
-
const
|
|
796
|
+
const inspect = request.feature === "review";
|
|
797
|
+
const deadlineMs = deadlineMsOverride ?? backendDeadlineMs(request.feature, request.access);
|
|
798
|
+
const action = inspect ? "complete the guided review request" : isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
799
|
+
const timeoutVerb = inspect ? "during the guided review request" : isAdvisor ? "during the advisor consultation" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
800
|
+
const emptyTextMessage = inspect ? "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.";
|
|
801
|
+
// Review inspects the captured checkout, so it must not carry the "do not inspect" restriction.
|
|
802
|
+
const prompt = request.access === "restricted" && !inspect ? `${GROK_RESTRICTED_PREFIX}\n\n${request.prompt}` : request.prompt;
|
|
781
803
|
|
|
782
804
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
783
805
|
const promptDirectory = await mkdtemp(join(tmpdir(), "pi-bro-grok-"));
|
|
@@ -786,16 +808,14 @@ async function executeGrok(
|
|
|
786
808
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
787
809
|
const promptFile = join(promptDirectory, "prompt.txt");
|
|
788
810
|
await writeFile(promptFile, prompt, { encoding: "utf8", mode: 0o600 });
|
|
789
|
-
if (request.access !== "workspace-full" && !isBtw) runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
811
|
+
if (request.access !== "workspace-full" && !isBtw && !inspect) runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
790
812
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
791
813
|
|
|
792
814
|
const child = spawn(
|
|
793
815
|
"grok",
|
|
794
816
|
[
|
|
795
|
-
|
|
796
|
-
"off",
|
|
797
|
-
"--permission-mode",
|
|
798
|
-
"bypassPermissions",
|
|
817
|
+
// Review never auto-approves: dontAsk denies unpromptable asks instead of bypassing them. Unverified against a local grok CLI; an unsupported flag fails visibly rather than falling back to --sandbox off.
|
|
818
|
+
...(inspect ? ["--sandbox", "read-only", "--permission-mode", "dontAsk"] : ["--sandbox", "off", "--permission-mode", "bypassPermissions"]),
|
|
799
819
|
"--output-format",
|
|
800
820
|
"streaming-messages-json",
|
|
801
821
|
"--include-partial-messages",
|
|
@@ -836,8 +856,14 @@ async function executeGrok(
|
|
|
836
856
|
}
|
|
837
857
|
if (event.parent_tool_use_id !== undefined && event.parent_tool_use_id !== null) return;
|
|
838
858
|
if (event.type === "system" && event.subtype === "init") {
|
|
839
|
-
|
|
840
|
-
|
|
859
|
+
// Review asks for dontAsk, but `--sandbox read-only` in argv is the real boundary: another
|
|
860
|
+
// restricting mode is not a downgrade, and only auto-approval means the request was
|
|
861
|
+
// ignored. Both paths fail closed when the mode is missing, exactly like explain/btw.
|
|
862
|
+
const escalated = event.permissionMode === "bypassPermissions" || event.permissionMode === "always-approve";
|
|
863
|
+
if (inspect ? typeof event.permissionMode !== "string" || escalated : event.permissionMode !== "bypassPermissions") {
|
|
864
|
+
throw new Error(
|
|
865
|
+
`Grok did not apply the required ${inspect ? "restricted" : "bypassPermissions"} mode; check Grok policy/configuration.`,
|
|
866
|
+
);
|
|
841
867
|
}
|
|
842
868
|
if (typeof event.session_id === "string") initSessionId = event.session_id;
|
|
843
869
|
}
|
|
@@ -1007,14 +1033,15 @@ async function executeCodex(
|
|
|
1007
1033
|
): Promise<BackendOutcome> {
|
|
1008
1034
|
const isAdvisor = request.feature === "advisor";
|
|
1009
1035
|
const isBtw = request.feature === "btw";
|
|
1036
|
+
const inspect = request.feature === "review";
|
|
1010
1037
|
const full = request.access === "workspace-full";
|
|
1011
|
-
const deadlineMs = deadlineMsOverride ?? (
|
|
1012
|
-
const action =
|
|
1013
|
-
const timeoutVerb =
|
|
1014
|
-
const emptyTextMessage =
|
|
1038
|
+
const deadlineMs = deadlineMsOverride ?? backendDeadlineMs(request.feature, request.access);
|
|
1039
|
+
const action = inspect ? "complete the guided review request" : isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
1040
|
+
const timeoutVerb = inspect ? "during the guided review request" : isAdvisor ? "during the advisor consultation" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
1041
|
+
const emptyTextMessage = inspect ? "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.";
|
|
1015
1042
|
|
|
1016
1043
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
1017
|
-
const runDirectory = full || isBtw ? undefined : await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
1044
|
+
const runDirectory = full || isBtw || inspect ? undefined : await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
1018
1045
|
if (signal.aborted) {
|
|
1019
1046
|
if (runDirectory) await rm(runDirectory, { recursive: true, force: true });
|
|
1020
1047
|
return { status: "cancelled", message: "Canceled." };
|
|
@@ -1180,7 +1207,7 @@ async function executeMuse(
|
|
|
1180
1207
|
const isBtw = request.feature === "btw";
|
|
1181
1208
|
const inspect = request.feature === "review";
|
|
1182
1209
|
const full = request.access === "workspace-full";
|
|
1183
|
-
const deadlineMs = deadlineMsOverride ?? (
|
|
1210
|
+
const deadlineMs = deadlineMsOverride ?? backendDeadlineMs(request.feature, request.access);
|
|
1184
1211
|
const action = request.feature === "review" ? "complete the guided review request" : isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
1185
1212
|
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
1213
|
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.";
|
|
@@ -1369,10 +1396,6 @@ export async function execute(
|
|
|
1369
1396
|
(request.feature !== "btw" && request.continuation)
|
|
1370
1397
|
) return { status: "failure", message: "Unsupported execution request: check feature access, workspace cwd and continuation." };
|
|
1371
1398
|
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
|
-
}
|
|
1376
1399
|
// An explicit tag guard: a stale or corrupt runtime tag must fail, never fall through to Agy.
|
|
1377
1400
|
if (backend !== undefined && backend !== "agy" && backend !== "claude" && backend !== "grok" && backend !== "codex" && backend !== "muse") {
|
|
1378
1401
|
return { status: "failure", message: `Unknown backend ${JSON.stringify(backend)}: pick Agy, Claude, Grok, Codex or Muse in \`/bro config\`.` };
|
|
@@ -1423,3 +1446,38 @@ export async function execute(
|
|
|
1423
1446
|
}
|
|
1424
1447
|
return executeArgvPrint(request, selection, signal, onProgress, killEscalationMs, options?.deadlineMs);
|
|
1425
1448
|
}
|
|
1449
|
+
|
|
1450
|
+
// Thin presentation-boundary wrapper around the shared backend: coalesces raw text progress to the
|
|
1451
|
+
// existing 75ms cadence and translates the backend's tagged outcome back into this function's
|
|
1452
|
+
// existing throw-on-failure contract.
|
|
1453
|
+
export async function runBackendText(
|
|
1454
|
+
prompt: string,
|
|
1455
|
+
selection: BackendSelection,
|
|
1456
|
+
signal: AbortSignal,
|
|
1457
|
+
onProgress?: (text: string) => void,
|
|
1458
|
+
feature: "explain" | "show" = "explain",
|
|
1459
|
+
): Promise<string> {
|
|
1460
|
+
let updateTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1461
|
+
let latest: string | undefined;
|
|
1462
|
+
const throttledProgress = onProgress
|
|
1463
|
+
? (progress: BackendProgress) => {
|
|
1464
|
+
if (progress.kind !== "text") return;
|
|
1465
|
+
latest = progress.text;
|
|
1466
|
+
if (!updateTimer) {
|
|
1467
|
+
updateTimer = setTimeout(() => {
|
|
1468
|
+
updateTimer = undefined;
|
|
1469
|
+
if (!signal.aborted && latest !== undefined) onProgress(latest);
|
|
1470
|
+
}, 75);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
: undefined;
|
|
1474
|
+
|
|
1475
|
+
try {
|
|
1476
|
+
const outcome = await execute({ feature, prompt, access: "restricted" }, selection, signal, throttledProgress);
|
|
1477
|
+
if (outcome.status === "success") return outcome.text;
|
|
1478
|
+
throw new Error(outcome.message);
|
|
1479
|
+
} finally {
|
|
1480
|
+
if (updateTimer) clearTimeout(updateTimer);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|