pi-bro 0.17.0 → 0.18.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 +25 -0
- package/README.md +199 -243
- package/backend.ts +43 -22
- package/bro.ts +121 -373
- package/package.json +5 -4
- package/prompt.ts +17 -5
package/backend.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { createInterface } from "node:readline";
|
|
|
6
6
|
|
|
7
7
|
// Shared internal execution boundary for all four Bro features (explain, show, btw, advisor).
|
|
8
8
|
// This implements docs/plans/2026-09-22-shared-backend-design.md for Agy (all features) and the
|
|
9
|
-
// Claude Code CLI (
|
|
9
|
+
// Claude Code CLI (all features) and the Grok CLI (all features): it owns CLI selection, process invocation, progress/outcome normalization, continuation, and
|
|
10
10
|
// single-attempt cleanup. Feature code (bro.ts) keeps retries, UI, source/session capture and
|
|
11
11
|
// settings.
|
|
12
12
|
|
|
@@ -493,16 +493,16 @@ async function executeAdvisorStdin(
|
|
|
493
493
|
}
|
|
494
494
|
|
|
495
495
|
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
return backend === "agy" || backend === "grok" || feature !== "btw";
|
|
496
|
+
// Every backend supports every feature. Grok's "restricted" access is a prompt instruction, not
|
|
497
|
+
// enforcement (see GROK_RESTRICTED_PREFIX); Claude's restricted btw runs tool-less.
|
|
498
|
+
export function backendSupports(_backend: "agy" | "claude" | "grok", _feature: BackendFeature): boolean {
|
|
499
|
+
return true;
|
|
501
500
|
}
|
|
502
501
|
|
|
503
502
|
type ClaudeEvent = {
|
|
504
503
|
type?: unknown;
|
|
505
504
|
subtype?: unknown;
|
|
505
|
+
session_id?: unknown;
|
|
506
506
|
is_error?: unknown;
|
|
507
507
|
result?: unknown;
|
|
508
508
|
parent_tool_use_id?: unknown;
|
|
@@ -512,11 +512,14 @@ type ClaudeEvent = {
|
|
|
512
512
|
message?: { content?: unknown };
|
|
513
513
|
};
|
|
514
514
|
|
|
515
|
-
// Every Claude run
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
515
|
+
// Every Claude run uses --safe-mode, which disables CLAUDE.md, skills, plugins, hooks and MCP
|
|
516
|
+
// servers but keeps the user's auth (--bare would break OAuth). The prompt goes over stdin.
|
|
517
|
+
// explain/show run fresh and tool-less in a scratch cwd; advisor runs fresh in the caller's
|
|
518
|
+
// workspace with tools auto-approved. btw persists its session (so `--resume` can continue it) and
|
|
519
|
+
// always runs in the caller's workspace, since Claude stores sessions per cwd: restricted turns are
|
|
520
|
+
// tool-less, full turns auto-approve tools, and one session resumes across both. Only the terminal
|
|
521
|
+
// `result` event is authoritative; streamed text_delta events are progress only, and thinking is
|
|
522
|
+
// dropped. btw requires init/result session ids to agree (and to equal the resumed id).
|
|
520
523
|
async function executeClaude(
|
|
521
524
|
request: BackendRequest,
|
|
522
525
|
selection: { model: string; effort?: string },
|
|
@@ -526,11 +529,13 @@ async function executeClaude(
|
|
|
526
529
|
deadlineMsOverride: number | undefined,
|
|
527
530
|
): Promise<BackendOutcome> {
|
|
528
531
|
const isAdvisor = request.feature === "advisor";
|
|
529
|
-
const
|
|
530
|
-
const
|
|
532
|
+
const isBtw = request.feature === "btw";
|
|
533
|
+
const full = request.access === "workspace-full";
|
|
534
|
+
const deadlineMs = deadlineMsOverride ?? (full ? 610_000 : isBtw ? 130_000 : 125_000);
|
|
535
|
+
const action = isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
|
|
531
536
|
|
|
532
537
|
if (signal.aborted) return { status: "cancelled", message: "Canceled." };
|
|
533
|
-
const runDirectory = isAdvisor ? undefined : await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
538
|
+
const runDirectory = isAdvisor || isBtw ? undefined : await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
534
539
|
if (signal.aborted) {
|
|
535
540
|
if (runDirectory) await rm(runDirectory, { recursive: true, force: true });
|
|
536
541
|
return { status: "cancelled", message: "Canceled." };
|
|
@@ -542,21 +547,22 @@ async function executeClaude(
|
|
|
542
547
|
[
|
|
543
548
|
"-p",
|
|
544
549
|
"--safe-mode",
|
|
545
|
-
"--no-session-persistence",
|
|
550
|
+
...(isBtw ? [] : ["--no-session-persistence"]),
|
|
546
551
|
"--disable-slash-commands",
|
|
547
552
|
"--output-format",
|
|
548
553
|
"stream-json",
|
|
549
554
|
"--verbose",
|
|
550
555
|
"--include-partial-messages",
|
|
551
|
-
...(
|
|
556
|
+
...(full
|
|
552
557
|
? ["--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--dangerously-skip-permissions"]
|
|
553
558
|
: ["--tools", "", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--permission-mode", "dontAsk"]),
|
|
559
|
+
...(isBtw && request.continuation ? ["--resume", request.continuation.id] : []),
|
|
554
560
|
"--model",
|
|
555
561
|
selection.model,
|
|
556
562
|
...(selection.effort ? ["--effort", selection.effort] : []),
|
|
557
563
|
],
|
|
558
564
|
{
|
|
559
|
-
cwd:
|
|
565
|
+
cwd: runDirectory ?? request.cwd,
|
|
560
566
|
stdio: ["pipe", "pipe", "pipe"],
|
|
561
567
|
windowsHide: true,
|
|
562
568
|
detached: process.platform !== "win32",
|
|
@@ -571,6 +577,8 @@ async function executeClaude(
|
|
|
571
577
|
let terminalError: string | undefined;
|
|
572
578
|
let protocolError: string | undefined;
|
|
573
579
|
let stdoutBuffer = "";
|
|
580
|
+
let initSessionId: string | undefined;
|
|
581
|
+
let resultSessionId: string | undefined;
|
|
574
582
|
|
|
575
583
|
child.stderr?.setEncoding("utf8");
|
|
576
584
|
child.stderr?.on("data", (chunk: string) => {
|
|
@@ -593,6 +601,7 @@ async function executeClaude(
|
|
|
593
601
|
throw new Error("Claude emitted invalid stream-json output.");
|
|
594
602
|
}
|
|
595
603
|
const topLevel = event.parent_tool_use_id === undefined || event.parent_tool_use_id === null;
|
|
604
|
+
if (topLevel && event.type === "system" && event.subtype === "init" && typeof event.session_id === "string") initSessionId = event.session_id;
|
|
596
605
|
if (!isAdvisor && topLevel && event.type === "stream_event" && event.event?.type === "content_block_delta") {
|
|
597
606
|
const delta = event.event.delta;
|
|
598
607
|
if (delta?.type === "text_delta" && typeof delta.text === "string" && delta.text) {
|
|
@@ -612,6 +621,7 @@ async function executeClaude(
|
|
|
612
621
|
}
|
|
613
622
|
}
|
|
614
623
|
if (event.type !== "result" || !topLevel) return;
|
|
624
|
+
if (typeof event.session_id === "string") resultSessionId = event.session_id;
|
|
615
625
|
if (event.subtype === "success" && event.is_error === false && (event.stop_reason !== "end_turn" || (event.terminal_reason !== undefined && event.terminal_reason !== "completed"))) {
|
|
616
626
|
terminalError ??= `Claude did not complete the answer (stop reason: ${String(event.stop_reason)}, terminal reason: ${String(event.terminal_reason)}).`;
|
|
617
627
|
return;
|
|
@@ -667,7 +677,7 @@ async function executeClaude(
|
|
|
667
677
|
const cause = attempt.causeOf();
|
|
668
678
|
if (cause === "cancelled") return { status: "cancelled", message: "Canceled.", partialText };
|
|
669
679
|
if (cause === "timeout") {
|
|
670
|
-
const during = isAdvisor ? "during the advisor consultation" : "while simplifying the response";
|
|
680
|
+
const during = isAdvisor ? "during the advisor consultation" : isBtw ? "during the side conversation" : "while simplifying the response";
|
|
671
681
|
return { status: "timeout", message: `Claude timed out ${during}. Run \`/bro doctor\` for setup help.`, partialText };
|
|
672
682
|
}
|
|
673
683
|
if (protocolError) return { status: "failure", message: withDoctor(protocolError), partialText };
|
|
@@ -688,8 +698,18 @@ async function executeClaude(
|
|
|
688
698
|
return { status: "failure", message: withDoctor(`Claude exited without a result event${stderr.trim() ? `: ${stderr.trim()}` : "."}`), partialText };
|
|
689
699
|
}
|
|
690
700
|
const text = final.trim();
|
|
691
|
-
if (!text) return { status: "failure", message: withDoctor("Claude returned no final answer."), partialText };
|
|
692
|
-
return { status: "success", text };
|
|
701
|
+
if (!text) return { status: "failure", message: withDoctor(isBtw ? "Claude returned no answer for the side question." : "Claude returned no final answer."), partialText };
|
|
702
|
+
if (!isBtw) return { status: "success", text };
|
|
703
|
+
// btw continuation: the session id must be reported, consistent, and (on resume) unchanged.
|
|
704
|
+
const sessionId = resultSessionId ?? initSessionId;
|
|
705
|
+
if (!sessionId || (initSessionId !== undefined && initSessionId !== sessionId) || (request.continuation && sessionId !== request.continuation.id)) {
|
|
706
|
+
return {
|
|
707
|
+
status: "failure",
|
|
708
|
+
message: withDoctor(`Claude reported an inconsistent session id (expected ${request.continuation?.id ?? "one id"}, init ${initSessionId ?? "none"}, result ${resultSessionId ?? "none"}).`),
|
|
709
|
+
partialText,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
return { status: "success", text, continuation: { id: sessionId } };
|
|
693
713
|
} finally {
|
|
694
714
|
if (runDirectory) await rm(runDirectory, { recursive: true, force: true });
|
|
695
715
|
}
|
|
@@ -979,8 +999,9 @@ export async function execute(
|
|
|
979
999
|
return executeGrok(request, selection, signal, onProgress, killEscalationMs, options?.deadlineMs);
|
|
980
1000
|
}
|
|
981
1001
|
if (selection.backend === "claude") {
|
|
982
|
-
|
|
983
|
-
|
|
1002
|
+
// Claude btw always runs (and resumes) in the caller's workspace, even restricted.
|
|
1003
|
+
if (request.feature === "btw" && !request.cwd?.trim()) {
|
|
1004
|
+
return { status: "failure", message: "Unsupported execution request: check feature access, workspace cwd and continuation." };
|
|
984
1005
|
}
|
|
985
1006
|
if (!selection.model.trim() || (selection.effort !== undefined && !CLAUDE_EFFORTS.includes(selection.effort))) {
|
|
986
1007
|
return { status: "failure", message: "Unsupported Claude selection: check the model and effort (low, medium, high, xhigh or max)." };
|