pi-bro 0.23.1 → 0.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bro",
3
- "version": "0.23.1",
3
+ "version": "0.25.0",
4
4
  "description": "A sidekick for you and your Pi agent: plain-language explanations, side questions, diagrams, PR review and second opinions, without cluttering the agent's context.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,6 +46,8 @@
46
46
  "src/show.ts",
47
47
  "src/advisor.ts",
48
48
  "src/btw.ts",
49
+ "src/elevate.ts",
50
+ "src/elevate-engine.ts",
49
51
  "src/explain-modal.ts",
50
52
  "src/review.ts",
51
53
  "src/review-source.ts",
@@ -56,6 +58,7 @@
56
58
  "src/sources.ts",
57
59
  "src/config-ui.ts",
58
60
  "src/ui-capabilities.ts",
61
+ "src/modal-kit.ts",
59
62
  "src/backend.ts",
60
63
  "src/prompt.ts",
61
64
  "src/vendor/mermaid.min.js",
@@ -63,9 +66,15 @@
63
66
  "CHANGELOG.md",
64
67
  "LICENSE",
65
68
  "THIRD_PARTY_NOTICES.md",
69
+ "docs/README.md",
70
+ "docs/getting-started.md",
66
71
  "docs/user-guide.md",
67
72
  "docs/pig-compatibility.md",
68
- "docs/guided-review.md"
73
+ "docs/guided-review.md",
74
+ "docs/elevate.md",
75
+ "docs/examples.md",
76
+ "docs/features",
77
+ "docs/reference"
69
78
  ],
70
79
  "publishConfig": {
71
80
  "access": "public"
@@ -74,8 +83,9 @@
74
83
  "node": ">=22.19.0"
75
84
  },
76
85
  "scripts": {
86
+ "docs:check": "node scripts/check-docs.mjs && node --test test/docs-quality.test.mjs",
77
87
  "typecheck": "tsc --noEmit",
78
- "test": "npm run typecheck && node .github/scripts/release-utils.mjs selftest && node --test test/review-ux.test.mjs test/review-doctor.test.mjs test/review-versions.test.mjs test/review-regeneration.test.mjs test/review-modal.test.mjs test/review.test.ts test/review-ui.test.mjs test/review-session.test.mjs test/helpers.test.mjs test/prompt.test.ts test/backend.test.ts test/claude.test.ts test/grok.test.ts test/codex.test.ts test/muse.test.ts test/settings.test.ts test/sources.test.ts test/release.test.ts test/show-html.test.ts test/smoke-rpc.test.ts test/ui-capabilities.test.ts benchmark/*.test.ts && sh ./test/smoke-test.sh",
88
+ "test": "npm run typecheck && npm run docs:check && node .github/scripts/release-utils.mjs selftest && node --test test/elevate-docs.test.mjs test/elevate.test.ts test/review-ux.test.mjs test/review-doctor.test.mjs test/review-versions.test.mjs test/review-regeneration.test.mjs test/review-modal.test.mjs test/review.test.ts test/review-ui.test.mjs test/review-session.test.mjs test/helpers.test.mjs test/prompt.test.ts test/backend.test.ts test/claude.test.ts test/grok.test.ts test/codex.test.ts test/muse.test.ts test/settings.test.ts test/sources.test.ts test/release.test.ts test/show-html.test.ts test/smoke-rpc.test.ts test/ui-capabilities.test.ts test/modal-contract.test.mjs benchmark/*.test.ts && sh ./test/smoke-test.sh",
79
89
  "benchmark:dry-run": "node benchmark/run.ts dry-run",
80
90
  "benchmark:run": "node benchmark/run.ts run",
81
91
  "benchmark:report": "node benchmark/run.ts report",
package/src/advisor.ts CHANGED
@@ -460,11 +460,11 @@ export function registerAdvisorTool(pi: ExtensionAPI): void {
460
460
  name: ADVISOR_TOOL_NAME,
461
461
  label: "Bro advisor",
462
462
  description:
463
- "Consult a fresh, independent process of the configured advisor backend for a second opinion mid-task. It has real, unsandboxed tool access in the current workspace (read files, search, run commands) with permissions auto-approved, and is instructed to investigate before advising and to leave edits to you — that is a behavioral instruction to the advisor, not an enforced restriction, so treat its findings as advice rather than a delegated implementation. You never need to prepare a summary or evidence first: Bro automatically captures your system instructions, active tools, and the conversation so far, plus any human-set steering priorities, and sends them to the advisor.",
463
+ "Consult a fresh, independent process of the configured advisor backend for a second opinion mid-task. It has real, unsandboxed tool access in the current workspace (read files, search, run commands) with permissions auto-approved, and is instructed to investigate before advising and to leave edits to you — that is a behavioral instruction to the advisor, not an enforced restriction, so treat its findings as advice rather than a delegated implementation. No need to prepare a summary or evidence first: Bro automatically captures your system instructions, active tools, and the conversation so far, plus any human-set steering priorities, and sends them to the advisor.",
464
464
  promptSnippet: "Consult a fresh configured-backend process for a second opinion mid-task; it investigates the workspace itself and returns advice",
465
465
  promptGuidelines: [
466
- "Call bro_advisor before or after a non-trivial design or scope decision, or when genuinely uncertain, for a second opinion from a fresh, independent process of the configured advisor backend.",
467
- "bro_advisor's question parameter is optional — never delay a call to first prepare a summary or evidence; Bro captures context automatically.",
466
+ "Use bro_advisor when the user asks for an independent opinion, or when one would likely settle real uncertainty or catch a costly mistake. Routine decisions do not require consultation.",
467
+ "bro_advisor's question parameter is optional — Bro captures context automatically, so you do not need to prepare a summary or evidence before calling.",
468
468
  "Any human-set steering priorities are applied automatically by bro_advisor; you don't need to relay or repeat them.",
469
469
  "bro_advisor is instructed to only return advice and leave edits to you — that instruction is not enforced, so verify its findings yourself rather than treating them as a completed implementation.",
470
470
  ],
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
- 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.";
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 ?? (full ? 610_000 : isBtw ? 130_000 : 125_000);
237
- const printTimeout = full ? "10m" : "2m";
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.";
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 ?? 610_000;
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 ?? (full || inspect ? 610_000 : isBtw ? 130_000 : 125_000);
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 every feature with all capabilities on (--sandbox off, bypassPermissions, no tool
755
- // blacklist). explain/show run in a fresh mkdtemp scratch cwd removed afterwards (the Agy pattern);
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 runs fresh in the workspace. Restricted
758
- // requests get GROK_RESTRICTED_PREFIX. The prompt goes through a private 0600 file in its own
759
- // mkdtemp directory, removed once the attempt ends.
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 blocks only when
764
- // no delta streamed for it. Thinking is never reported. btw requires init/result session ids to
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 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.";
780
- const prompt = request.access === "restricted" ? `${GROK_RESTRICTED_PREFIX}\n\n${request.prompt}` : request.prompt;
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
- "--sandbox",
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
- if (event.permissionMode !== "bypassPermissions") {
840
- throw new Error("Grok did not apply the required bypassPermissions mode; check Grok policy/configuration.");
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 ?? (full ? 610_000 : isBtw ? 130_000 : 125_000);
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.";
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 ?? (full || inspect ? 610_000 : isBtw ? 130_000 : 125_000);
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\`.` };
package/src/bro.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  resolveBtwThread,
9
9
  } from "./btw.ts";
10
10
  import { doctorReport, showBroConfigModal } from "./doctor.ts";
11
+ import { openElevateModal } from "./elevate.ts";
11
12
  import {
12
13
  type BroResult,
13
14
  type ModalResult,
@@ -53,6 +54,8 @@ export * from "./show.ts";
53
54
  export * from "./advisor.ts";
54
55
  export * from "./doctor.ts";
55
56
  export * from "./btw.ts";
57
+ export * from "./elevate.ts";
58
+ export * from "./elevate-engine.ts";
56
59
  export * from "./explain-modal.ts";
57
60
 
58
61
  const COMMANDS = [
@@ -67,6 +70,7 @@ const COMMANDS = [
67
70
  { value: "preferences", label: "preferences", description: "View or edit what Bro knows about you and how you like answers" },
68
71
  { value: "guided-review", label: "guided-review", description: "Understand and assess a PR with captured code and optional private questions" },
69
72
  { value: "btw", label: "btw", description: "Open a side conversation (starts conversation-only; /mode toggles full permission)" },
73
+ { value: "elevate", label: "elevate", description: "Discover missing expert considerations, then improve a request" },
70
74
  { value: "config", label: "config", description: "Configure shared defaults and per-capability model/effort overrides" },
71
75
  { value: "advisor", label: "advisor", description: "Check whether the executor's advisor tool is available right now" },
72
76
  { value: "advisor-steer", label: "advisor-steer", description: "View, edit, save, or clear the advisor's persistent steering brief" },
@@ -281,6 +285,13 @@ export default async function bro(pi: ExtensionAPI) {
281
285
  return;
282
286
  }
283
287
 
288
+ if (action === "elevate") {
289
+ if (!hasBroCustomUi(ctx)) { ctx.ui.notify("Use /bro elevate in Pi interactive UI.", "warning"); return; }
290
+ try { await openElevateModal(ctx, value); }
291
+ catch (error) { ctx.ui.notify(withDoctor(error), "error"); }
292
+ return;
293
+ }
294
+
284
295
  if (action === "config") {
285
296
  if (parts.length !== 1) {
286
297
  ctx.ui.notify("Use /bro config.", "warning");
@@ -419,6 +430,20 @@ export default async function bro(pi: ExtensionAPI) {
419
430
  },
420
431
  });
421
432
 
433
+ // Registered after /bro and isolated: a shortcut that cannot register must never remove the
434
+ // working commands. The shortcut can read an unsent draft and uses only context and UI methods
435
+ // shared by ExtensionContext and ExtensionCommandContext.
436
+ try {
437
+ if (typeof pi.registerShortcut === "function") pi.registerShortcut("ctrl+shift+e", {
438
+ description: "Bro Elevate: improve the current editor draft",
439
+ handler: async (ctx) => {
440
+ if (!hasBroCustomUi(ctx)) { ctx.ui.notify("Elevate requires Pi interactive UI.", "warning"); return; }
441
+ try { await openElevateModal(ctx as import("@earendil-works/pi-coding-agent").ExtensionCommandContext); }
442
+ catch (error) { ctx.ui.notify(withDoctor(error), "error"); }
443
+ },
444
+ });
445
+ } catch { /* /bro still works; the command is the supported path to Elevate */ }
446
+
422
447
  // Keep the command available even when Bro cannot create its settings file; Doctor can then explain the problem.
423
448
  await ensureSettingsFile().catch(() => undefined);
424
449
  }