pi-bro 0.16.0 → 0.18.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.
Files changed (6) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +206 -222
  3. package/backend.ts +314 -23
  4. package/bro.ts +371 -396
  5. package/package.json +2 -2
  6. package/prompt.ts +17 -5
package/backend.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { type ChildProcess, spawn } from "node:child_process";
2
- import { mkdtemp, rm } from "node:fs/promises";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  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 (explain/show/advisor; no btw yet): it owns CLI selection, process invocation, progress/outcome normalization, continuation, and
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
 
@@ -14,9 +14,11 @@ export type BackendFeature = "explain" | "show" | "btw" | "advisor";
14
14
  export type BackendAccess = "restricted" | "workspace-full";
15
15
  export type AgySelection = { model: string; effort?: "low" | "medium" | "high" };
16
16
  export const CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
17
+ export const GROK_EFFORTS = ["low", "medium", "high", "xhigh"] as const;
17
18
  export type BackendSelection =
18
19
  | ({ backend?: "agy" } & AgySelection)
19
- | { backend: "claude"; model: string; effort?: (typeof CLAUDE_EFFORTS)[number] };
20
+ | { backend: "claude"; model: string; effort?: (typeof CLAUDE_EFFORTS)[number] }
21
+ | { backend: "grok"; model: string; effort?: (typeof GROK_EFFORTS)[number] };
20
22
  export type BackendContinuation = { id: string };
21
23
  export type BackendProgress = { kind: "text"; text: string } | { kind: "activity"; label: string; timestamp: number };
22
24
  export type BackendOnProgress = (progress: BackendProgress) => void;
@@ -69,7 +71,9 @@ function processStartMessage(processError: NodeJS.ErrnoException, cli = "Agy"):
69
71
  if (processError.code === "ENOENT") {
70
72
  return cli === "Agy"
71
73
  ? "Agy could not start. Make sure Agy is installed and on PATH, then run `/bro doctor`."
72
- : "Claude Code could not start. Make sure `claude` is installed and on PATH, then run `/bro doctor`.";
74
+ : cli === "Grok"
75
+ ? "Grok could not start. Make sure `grok` is installed and on PATH, then run `/bro doctor`."
76
+ : "Claude Code could not start. Make sure `claude` is installed and on PATH, then run `/bro doctor`.";
73
77
  }
74
78
  return `${cli} could not start: ${processError.message}\n\nRun \`/bro doctor\` for setup help.`;
75
79
  }
@@ -489,14 +493,16 @@ async function executeAdvisorStdin(
489
493
  }
490
494
 
491
495
 
492
- // Claude Code supports every feature except btw (it has no continuation wiring yet).
493
- export function backendSupports(backend: "agy" | "claude", feature: BackendFeature): boolean {
494
- return backend === "agy" || 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;
495
500
  }
496
501
 
497
502
  type ClaudeEvent = {
498
503
  type?: unknown;
499
504
  subtype?: unknown;
505
+ session_id?: unknown;
500
506
  is_error?: unknown;
501
507
  result?: unknown;
502
508
  parent_tool_use_id?: unknown;
@@ -506,11 +512,14 @@ type ClaudeEvent = {
506
512
  message?: { content?: unknown };
507
513
  };
508
514
 
509
- // Every Claude run is fresh (never resumed, nothing persisted) under --safe-mode, which disables
510
- // CLAUDE.md, skills, plugins, hooks and MCP servers but keeps the user's auth (--bare would break
511
- // OAuth). The prompt goes over stdin. explain/show additionally run tool-less in a scratch cwd;
512
- // advisor runs in the caller's workspace with tools auto-approved. Only the terminal `result`
513
- // event is authoritative; streamed text_delta events are progress only, and thinking is dropped.
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).
514
523
  async function executeClaude(
515
524
  request: BackendRequest,
516
525
  selection: { model: string; effort?: string },
@@ -520,11 +529,13 @@ async function executeClaude(
520
529
  deadlineMsOverride: number | undefined,
521
530
  ): Promise<BackendOutcome> {
522
531
  const isAdvisor = request.feature === "advisor";
523
- const deadlineMs = deadlineMsOverride ?? (isAdvisor ? 610_000 : 125_000);
524
- const action = isAdvisor ? "complete the advisor consultation" : "simplify the response";
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";
525
536
 
526
537
  if (signal.aborted) return { status: "cancelled", message: "Canceled." };
527
- const runDirectory = isAdvisor ? undefined : await mkdtemp(join(tmpdir(), "pi-bro-"));
538
+ const runDirectory = isAdvisor || isBtw ? undefined : await mkdtemp(join(tmpdir(), "pi-bro-"));
528
539
  if (signal.aborted) {
529
540
  if (runDirectory) await rm(runDirectory, { recursive: true, force: true });
530
541
  return { status: "cancelled", message: "Canceled." };
@@ -536,21 +547,22 @@ async function executeClaude(
536
547
  [
537
548
  "-p",
538
549
  "--safe-mode",
539
- "--no-session-persistence",
550
+ ...(isBtw ? [] : ["--no-session-persistence"]),
540
551
  "--disable-slash-commands",
541
552
  "--output-format",
542
553
  "stream-json",
543
554
  "--verbose",
544
555
  "--include-partial-messages",
545
- ...(isAdvisor
556
+ ...(full
546
557
  ? ["--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--dangerously-skip-permissions"]
547
558
  : ["--tools", "", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--permission-mode", "dontAsk"]),
559
+ ...(isBtw && request.continuation ? ["--resume", request.continuation.id] : []),
548
560
  "--model",
549
561
  selection.model,
550
562
  ...(selection.effort ? ["--effort", selection.effort] : []),
551
563
  ],
552
564
  {
553
- cwd: isAdvisor ? request.cwd : runDirectory,
565
+ cwd: runDirectory ?? request.cwd,
554
566
  stdio: ["pipe", "pipe", "pipe"],
555
567
  windowsHide: true,
556
568
  detached: process.platform !== "win32",
@@ -565,6 +577,8 @@ async function executeClaude(
565
577
  let terminalError: string | undefined;
566
578
  let protocolError: string | undefined;
567
579
  let stdoutBuffer = "";
580
+ let initSessionId: string | undefined;
581
+ let resultSessionId: string | undefined;
568
582
 
569
583
  child.stderr?.setEncoding("utf8");
570
584
  child.stderr?.on("data", (chunk: string) => {
@@ -587,6 +601,7 @@ async function executeClaude(
587
601
  throw new Error("Claude emitted invalid stream-json output.");
588
602
  }
589
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;
590
605
  if (!isAdvisor && topLevel && event.type === "stream_event" && event.event?.type === "content_block_delta") {
591
606
  const delta = event.event.delta;
592
607
  if (delta?.type === "text_delta" && typeof delta.text === "string" && delta.text) {
@@ -606,6 +621,7 @@ async function executeClaude(
606
621
  }
607
622
  }
608
623
  if (event.type !== "result" || !topLevel) return;
624
+ if (typeof event.session_id === "string") resultSessionId = event.session_id;
609
625
  if (event.subtype === "success" && event.is_error === false && (event.stop_reason !== "end_turn" || (event.terminal_reason !== undefined && event.terminal_reason !== "completed"))) {
610
626
  terminalError ??= `Claude did not complete the answer (stop reason: ${String(event.stop_reason)}, terminal reason: ${String(event.terminal_reason)}).`;
611
627
  return;
@@ -661,7 +677,7 @@ async function executeClaude(
661
677
  const cause = attempt.causeOf();
662
678
  if (cause === "cancelled") return { status: "cancelled", message: "Canceled.", partialText };
663
679
  if (cause === "timeout") {
664
- 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";
665
681
  return { status: "timeout", message: `Claude timed out ${during}. Run \`/bro doctor\` for setup help.`, partialText };
666
682
  }
667
683
  if (protocolError) return { status: "failure", message: withDoctor(protocolError), partialText };
@@ -682,13 +698,272 @@ async function executeClaude(
682
698
  return { status: "failure", message: withDoctor(`Claude exited without a result event${stderr.trim() ? `: ${stderr.trim()}` : "."}`), partialText };
683
699
  }
684
700
  const text = final.trim();
685
- if (!text) return { status: "failure", message: withDoctor("Claude returned no final answer."), partialText };
686
- 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 } };
687
713
  } finally {
688
714
  if (runDirectory) await rm(runDirectory, { recursive: true, force: true });
689
715
  }
690
716
  }
691
717
 
718
+ type GrokEvent = {
719
+ permissionMode?: unknown;
720
+ type?: unknown;
721
+ subtype?: unknown;
722
+ is_error?: unknown;
723
+ result?: unknown;
724
+ errors?: unknown;
725
+ message?: unknown;
726
+ parent_tool_use_id?: unknown;
727
+ stop_reason?: unknown;
728
+ session_id?: unknown;
729
+ event?: { type?: unknown; delta?: { type?: unknown; text?: unknown } };
730
+ };
731
+
732
+ // Grok's "restricted" access is a request in the prompt, NOT technical enforcement: every Grok run
733
+ // has all tools on (--sandbox off, bypassPermissions, subagents/web/scheduler available), so the
734
+ // model can still read, write or reach out if it ignores this. The parent UI owns telling the user
735
+ // that truthfully.
736
+ export const GROK_RESTRICTED_PREFIX =
737
+ "Bro restricted mode (a request, not a technical restriction): answer only from the context supplied in this prompt. " +
738
+ "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.";
739
+
740
+ // Grok runs every feature with all capabilities on (--sandbox off, bypassPermissions, no tool
741
+ // blacklist). explain/show run in a fresh mkdtemp scratch cwd removed afterwards (the Agy pattern);
742
+ // btw (both accesses) runs in the caller's workspace cwd and continues natively with --resume, whose
743
+ // session keeps the cwd it was created in; the advisor runs fresh in the workspace. Restricted
744
+ // requests get GROK_RESTRICTED_PREFIX. The prompt goes through a private 0600 file in its own
745
+ // mkdtemp directory, removed once the attempt ends.
746
+ // Only a top-level terminal `result` with success/is_error false/end_turn is authoritative; the
747
+ // first failure (a failed result or a top-level `error` event, even after a success) is latched.
748
+ // Nested (subagent) frames are ignored. The advisor reports activity labels only; explain/show/btw
749
+ // report top-level text_delta progress, falling back to an assistant message's text blocks only when
750
+ // no delta streamed for it. Thinking is never reported. btw requires init/result session ids to
751
+ // agree (and to equal the resumed id) and returns that id as the continuation.
752
+ async function executeGrok(
753
+ request: BackendRequest,
754
+ selection: { model: string; effort?: string },
755
+ signal: AbortSignal,
756
+ onProgress: BackendOnProgress | undefined,
757
+ killEscalationMs: number,
758
+ deadlineMsOverride: number | undefined,
759
+ ): Promise<BackendOutcome> {
760
+ const isAdvisor = request.feature === "advisor";
761
+ const isBtw = request.feature === "btw";
762
+ const deadlineMs = deadlineMsOverride ?? (isAdvisor || (isBtw && request.access === "workspace-full") ? 610_000 : isBtw ? 130_000 : 125_000);
763
+ const action = isAdvisor ? "complete the advisor consultation" : isBtw ? "answer the side question" : "simplify the response";
764
+ const timeoutVerb = isAdvisor ? "during the advisor consultation" : isBtw ? "during the side conversation" : "while simplifying the response";
765
+ const emptyTextMessage = isAdvisor ? "Grok returned no advice." : isBtw ? "Grok returned no answer for the side question." : "Grok returned no final explanation.";
766
+ const prompt = request.access === "restricted" ? `${GROK_RESTRICTED_PREFIX}\n\n${request.prompt}` : request.prompt;
767
+
768
+ if (signal.aborted) return { status: "cancelled", message: "Canceled." };
769
+ const promptDirectory = await mkdtemp(join(tmpdir(), "pi-bro-grok-"));
770
+ let runDirectory: string | undefined;
771
+ try {
772
+ if (signal.aborted) return { status: "cancelled", message: "Canceled." };
773
+ const promptFile = join(promptDirectory, "prompt.txt");
774
+ await writeFile(promptFile, prompt, { encoding: "utf8", mode: 0o600 });
775
+ if (!isAdvisor && !isBtw) runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
776
+ if (signal.aborted) return { status: "cancelled", message: "Canceled." };
777
+
778
+ const child = spawn(
779
+ "grok",
780
+ [
781
+ "--sandbox",
782
+ "off",
783
+ "--permission-mode",
784
+ "bypassPermissions",
785
+ "--output-format",
786
+ "streaming-messages-json",
787
+ "--include-partial-messages",
788
+ "--model", selection.model,
789
+ ...(selection.effort ? ["--reasoning-effort", selection.effort] : []),
790
+ ...(isBtw && request.continuation ? ["--resume", request.continuation.id] : []),
791
+ "--prompt-file",
792
+ promptFile,
793
+ ],
794
+ { cwd: runDirectory ?? request.cwd, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, detached: process.platform !== "win32" },
795
+ );
796
+
797
+ const attempt = beginAttempt(child, signal, deadlineMs, killEscalationMs);
798
+ let processError: Error | undefined;
799
+ let stderr = "";
800
+ let partial = "";
801
+ let streamedSinceAssistant = false;
802
+ let initSessionId: string | undefined;
803
+ let resultSessionId: string | undefined;
804
+ let final: string | undefined;
805
+ let terminalError: string | undefined;
806
+ let protocolError: string | undefined;
807
+ let stdoutBuffer = "";
808
+
809
+ child.stderr?.setEncoding("utf8");
810
+ child.stderr?.on("data", (chunk: string) => {
811
+ stderr += chunk;
812
+ });
813
+ child.once("error", (error) => {
814
+ processError = error;
815
+ });
816
+
817
+ const handleLine = (line: string) => {
818
+ if (!line.trim() || attempt.causeOf()) return;
819
+ let event: GrokEvent;
820
+ try {
821
+ event = JSON.parse(line) as GrokEvent;
822
+ } catch {
823
+ throw new Error("Grok emitted invalid streaming-messages-json output.");
824
+ }
825
+ if (event.parent_tool_use_id !== undefined && event.parent_tool_use_id !== null) return;
826
+ if (event.type === "system" && event.subtype === "init") {
827
+ if (event.permissionMode !== "bypassPermissions") {
828
+ throw new Error("Grok did not apply the required bypassPermissions mode; check Grok policy/configuration.");
829
+ }
830
+ if (typeof event.session_id === "string") initSessionId = event.session_id;
831
+ }
832
+ if (!isAdvisor && event.type === "stream_event" && event.event?.type === "content_block_delta") {
833
+ const delta = event.event.delta;
834
+ if (delta?.type === "text_delta" && typeof delta.text === "string" && delta.text) {
835
+ partial += delta.text;
836
+ streamedSinceAssistant = true;
837
+ onProgress?.({ kind: "text", text: partial });
838
+ }
839
+ }
840
+ if (event.type === "error") {
841
+ terminalError ??= `Grok error: ${typeof event.message === "string" && event.message.trim() ? event.message.trim() : "unknown error"}`;
842
+ return;
843
+ }
844
+ const message = event.message as { content?: unknown } | undefined;
845
+ if (!isAdvisor && event.type === "assistant" && Array.isArray(message?.content)) {
846
+ // Fallback only: an assistant message whose text already streamed as deltas is not re-appended.
847
+ if (!streamedSinceAssistant) {
848
+ const text = (message.content as Array<{ type?: unknown; text?: unknown }>)
849
+ .map((block) => (block?.type === "text" && typeof block.text === "string" ? block.text : ""))
850
+ .join("");
851
+ if (text) {
852
+ partial += text;
853
+ onProgress?.({ kind: "text", text: partial });
854
+ }
855
+ }
856
+ streamedSinceAssistant = false;
857
+ }
858
+ if (isAdvisor && event.type === "assistant" && Array.isArray(message?.content)) {
859
+ for (const block of message.content as Array<{ type?: unknown; name?: unknown; text?: unknown }>) {
860
+ const label =
861
+ block?.type === "tool_use" && typeof block.name === "string"
862
+ ? block.name.trim()
863
+ : block?.type === "text" && typeof block.text === "string"
864
+ ? block.text.split("\n").find((text) => text.trim())?.trim()
865
+ : undefined;
866
+ if (label) onProgress?.({ kind: "activity", label, timestamp: Date.now() });
867
+ }
868
+ }
869
+ if (event.type !== "result") return;
870
+ if (typeof event.session_id === "string") resultSessionId = event.session_id;
871
+ if (event.subtype === "success" && event.is_error === false) {
872
+ if (event.stop_reason !== "end_turn") {
873
+ terminalError ??= `Grok did not complete the ${isAdvisor ? "advice" : "answer"} (stop reason: ${String(event.stop_reason)}).`;
874
+ } else if (typeof event.result === "string") {
875
+ final = event.result;
876
+ } else {
877
+ terminalError ??= "Grok reported success without a final result.";
878
+ }
879
+ return;
880
+ }
881
+ const firstError = Array.isArray(event.errors)
882
+ ? event.errors.map((item) => (typeof item === "string" ? item : (item as { message?: unknown })?.message)).find((item) => typeof item === "string" && item.trim())
883
+ : undefined;
884
+ const detail =
885
+ typeof firstError === "string"
886
+ ? firstError.trim()
887
+ : typeof event.subtype === "string" && event.subtype !== "success"
888
+ ? event.subtype
889
+ : typeof event.result === "string" && event.result.trim()
890
+ ? event.result.trim()
891
+ : "turn failed";
892
+ terminalError ??= `Grok failed: ${detail}`;
893
+ };
894
+
895
+ child.stdout?.setEncoding("utf8");
896
+ child.stdout?.on("data", (chunk: string) => {
897
+ stdoutBuffer += chunk;
898
+ const parts = stdoutBuffer.split(/\r?\n/);
899
+ stdoutBuffer = parts.pop() ?? "";
900
+ if (stdoutBuffer.length > ADVISOR_MAX_STDOUT_LINE_CHARS || parts.some((line) => line.length > ADVISOR_MAX_STDOUT_LINE_CHARS)) {
901
+ protocolError ??= `Grok emitted a stdout line over ${ADVISOR_MAX_STDOUT_LINE_CHARS} characters; the stream is unparseable.`;
902
+ stdoutBuffer = "";
903
+ attempt.stop("protocol");
904
+ return;
905
+ }
906
+ for (const line of parts) {
907
+ try {
908
+ handleLine(line);
909
+ } catch (error) {
910
+ protocolError ??= errorMessage(error);
911
+ attempt.stop("protocol");
912
+ return;
913
+ }
914
+ }
915
+ });
916
+
917
+ const { code, exitSignal } = await attempt.closed;
918
+ attempt.dispose();
919
+ if (stdoutBuffer.trim()) {
920
+ try {
921
+ handleLine(stdoutBuffer);
922
+ } catch (error) {
923
+ protocolError ??= errorMessage(error);
924
+ }
925
+ }
926
+
927
+ const partialText = partial ? { partialText: partial } : {};
928
+ const cause = attempt.causeOf();
929
+ if (cause === "cancelled") return { status: "cancelled", message: "Canceled.", ...partialText };
930
+ if (cause === "timeout") return { status: "timeout", message: `Grok timed out ${timeoutVerb}. Run \`/bro doctor\` for setup help.`, ...partialText };
931
+ if (protocolError) return { status: "failure", message: withDoctor(protocolError), ...partialText };
932
+ if (processError) return { status: "failure", message: processStartMessage(processError as NodeJS.ErrnoException, "Grok") };
933
+ if (exitSignal || code === null) return { status: "failure", message: unexpectedSignalMessage(exitSignal, "Grok"), ...partialText };
934
+ if (terminalError) return { status: "failure", message: withDoctor(terminalError), ...partialText };
935
+ if (code !== 0) {
936
+ const detail = stderr.trim();
937
+ return {
938
+ status: "failure",
939
+ message: detail
940
+ ? `Grok could not ${action}: ${detail}\n\nRun \`/bro doctor\` for setup help.`
941
+ : `Grok could not ${action}. Make sure \`grok\` is installed and signed in, then run \`/bro doctor\`.`,
942
+ ...partialText,
943
+ };
944
+ }
945
+ if (final === undefined) {
946
+ return { status: "failure", message: withDoctor(`Grok exited without a terminal result event${stderr.trim() ? `: ${stderr.trim()}` : "."}`), ...partialText };
947
+ }
948
+ const text = final.trim();
949
+ if (!text) return { status: "failure", message: withDoctor(emptyTextMessage), ...partialText };
950
+ if (!isBtw) return { status: "success", text };
951
+ // btw continuation: the session id must be reported, consistent, and (on resume) unchanged.
952
+ const sessionId = resultSessionId ?? initSessionId;
953
+ if (!sessionId || (initSessionId !== undefined && initSessionId !== sessionId) || (request.continuation && sessionId !== request.continuation.id)) {
954
+ return {
955
+ status: "failure",
956
+ message: withDoctor(`Grok reported an inconsistent session id (expected ${request.continuation?.id ?? "one id"}, init ${initSessionId ?? "none"}, result ${resultSessionId ?? "none"}).`),
957
+ ...partialText,
958
+ };
959
+ }
960
+ return { status: "success", text, continuation: { id: sessionId } };
961
+ } finally {
962
+ if (runDirectory) await rm(runDirectory, { recursive: true, force: true });
963
+ await rm(promptDirectory, { recursive: true, force: true });
964
+ }
965
+ }
966
+
692
967
  // Single-attempt executor shared by all four features. Never retries (retries are feature-owned,
693
968
  // e.g. advisor's 3-attempt backoff in bro.ts); never spawns a pre-aborted request; on cancellation,
694
969
  // host deadline, or a protocol failure, stops the whole POSIX process group (SIGTERM, then SIGKILL
@@ -707,10 +982,26 @@ export async function execute(
707
982
  ((request.feature === "explain" || request.feature === "show") && request.access !== "restricted") ||
708
983
  (request.feature !== "btw" && request.continuation)
709
984
  ) return { status: "failure", message: "Unsupported execution request: check feature access, workspace cwd and continuation." };
985
+ const backend = (selection as { backend?: unknown }).backend;
986
+ // An explicit tag guard: a stale or corrupt runtime tag must fail, never fall through to Agy.
987
+ if (backend !== undefined && backend !== "agy" && backend !== "claude" && backend !== "grok") {
988
+ return { status: "failure", message: `Unknown backend ${JSON.stringify(backend)}: pick Agy, Claude or Grok in \`/bro config\`.` };
989
+ }
710
990
  const killEscalationMs = options?.killEscalationMs ?? DEFAULT_KILL_ESCALATION_MS;
991
+ if (selection.backend === "grok") {
992
+ // Grok btw always runs (and resumes) in the caller's workspace, even restricted.
993
+ if (request.feature === "btw" && !request.cwd?.trim()) {
994
+ return { status: "failure", message: "Unsupported execution request: check feature access, workspace cwd and continuation." };
995
+ }
996
+ if (typeof selection.model !== "string" || !selection.model.trim() || (selection.effort !== undefined && !GROK_EFFORTS.includes(selection.effort))) {
997
+ return { status: "failure", message: "Unsupported Grok selection: check the model and effort (low, medium, high or xhigh)." };
998
+ }
999
+ return executeGrok(request, selection, signal, onProgress, killEscalationMs, options?.deadlineMs);
1000
+ }
711
1001
  if (selection.backend === "claude") {
712
- if (!backendSupports("claude", request.feature)) {
713
- return { status: "failure", message: "Claude does not support /bro btw yet; switch btw back to Agy in `/bro config`." };
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." };
714
1005
  }
715
1006
  if (!selection.model.trim() || (selection.effort !== undefined && !CLAUDE_EFFORTS.includes(selection.effort))) {
716
1007
  return { status: "failure", message: "Unsupported Claude selection: check the model and effort (low, medium, high, xhigh or max)." };