pi-bro 0.23.1 → 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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to pi-bro are documented here.
4
4
 
5
+ ## [0.24.0] - 2026-10-19
6
+
7
+ ### Highlights
8
+
9
+ - Guided Review now works with Codex, Grok and Agy, not just Claude and Muse. Those three run shell commands inside their own sandbox on the captured checkout.
10
+ - Bro tells you plainly when a review backend can run shell commands (and that Agy's sandbox still allows writes), and recommends Claude or Muse for PRs you do not trust.
11
+ - `/bro doctor` no longer treats a Codex/Grok/Agy review choice as a setup problem; it shows the same advisory as information.
12
+ - Nothing changes for Claude and Muse, which still use strict file-only inspection.
13
+
14
+ ### Changed
15
+
16
+ - Review generation and questions no longer fail before starting on Codex, Grok and Agy. Codex runs `--sandbox read-only --ephemeral`, Grok runs `--sandbox read-only --permission-mode dontAsk`, and Agy keeps `--sandbox` (which limits terminal commands, not file writes); all three use the captured checkout and the 10-minute review deadline. No bypass flag is ever added to a review, and Grok review no longer gets the "do not inspect" prompt restriction.
17
+ - A new advisory appears while a review is being prepared, in the review window, and in `/bro doctor`: the backend runs commands inside its sandbox rather than doing strict file-only inspection.
18
+ - Saved reviews still open with any backend.
19
+
5
20
  ## [0.23.1] - 2026-10-15
6
21
 
7
22
  ### Highlights
package/README.md CHANGED
@@ -51,7 +51,7 @@ GitHub, use `pi install git:github.com/tranhoangnguyen03/pi-bro`.
51
51
  | `/bro`, `/bro text`, `/bro file`, `/bro url` | **[Explain](docs/user-guide.md#what-bro-can-explain).** A plain-language version of the agent's last reply, pasted text, a PDF/DOCX/Markdown file, or a public webpage. Three modes: `brief`, `balanced`, `faithful`. |
52
52
  | `/bro show` | **[Show](docs/user-guide.md#bro-show).** The last few turns drawn as a shape (call tree, file tree, diff, pseudocode, a sequence diagram, or an HTML layout) instead of prose. |
53
53
  | `/bro btw` | **[Side questions](docs/user-guide.md#bro-btw-side-conversation).** A separate conversation seeded with recent context, so you can ask without interrupting the agent. ⚠️ [Full-permission mode](#what-leaves-your-machine) can edit files. |
54
- | `/bro guided-review <PR>` | **[Guided Review](docs/guided-review.md).** An explanation and quality assessment of a GitHub PR, with ranked findings, source evidence and private questions. Needs the Claude or Muse backend. |
54
+ | `/bro guided-review <PR>` | **[Guided Review](docs/guided-review.md).** An explanation and quality assessment of a GitHub PR, with ranked findings, source evidence and private questions. Claude or Muse is recommended for untrusted PRs. |
55
55
  | `bro_advisor` tool | **[Second opinion](docs/user-guide.md#bro-advisor).** A tool your agent can call mid-task; a fresh backend process investigates and returns advice to the agent. ⚠️ [Runs with permissions bypassed](#what-leaves-your-machine). |
56
56
  | `/bro config`, `/bro preferences` | **[Settings](docs/user-guide.md#settings) and [preferences](docs/user-guide.md#preferences).** Pick the backend, model and effort per feature, and tell Bro how you like answers. |
57
57
 
@@ -68,14 +68,14 @@ Guided Review has its own [guide](docs/guided-review.md).
68
68
 
69
69
  | Backend | CLI | Notes |
70
70
  | --- | --- | --- |
71
- | [Google Antigravity](https://antigravity.google/docs/cli-install) | `agy` | Default for new settings. Needs `>=1.1.15`. Not for Guided Review. |
72
- | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | `claude` | Works for every feature, including Guided Review. |
73
- | [Grok Build](https://docs.x.ai/build/overview) | `grok` | Not for Guided Review. |
74
- | [Codex CLI](https://github.com/openai/codex) | `codex` | Not for Guided Review. |
75
- | [Muse Code](https://dev.meta.ai/docs/muse-code) | `muse` | Works for every feature, including Guided Review. |
71
+ | [Google Antigravity](https://antigravity.google/docs/cli-install) | `agy` | Default for new settings. Needs `>=1.1.15`. Guided Review uses `--sandbox`, which restricts shell commands but still allows writes inside the project. |
72
+ | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | `claude` | Works for every feature; strict file-only inspection for Guided Review. |
73
+ | [Grok Build](https://docs.x.ai/build/overview) | `grok` | Guided Review uses its read-only sandbox (shell-enabled). |
74
+ | [Codex CLI](https://github.com/openai/codex) | `codex` | Guided Review uses its read-only sandbox (shell-enabled). |
75
+ | [Muse Code](https://dev.meta.ai/docs/muse-code) | `muse` | Works for every feature; strict file-only inspection for Guided Review. |
76
76
 
77
77
  - Guided Review also needs Git and an authenticated [`gh`](https://cli.github.com/).
78
- Choose Claude or Muse for it in `/bro config`.
78
+ Claude or Muse is recommended for untrusted PRs (strict file-only inspection); Codex, Grok and Agy run with shell available under their sandbox, with an advisory.
79
79
  - [PiG](docs/pig-compatibility.md) support is experimental.
80
80
 
81
81
  ## What leaves your machine
@@ -105,8 +105,9 @@ tool results.
105
105
  > only an instruction.
106
106
  > - **BTW full-permission mode** (turned on with `/mode` inside BTW): it is
107
107
  > meant to edit files and run commands.
108
- > - **Grok, wherever you use it:** its sandbox is always off. "Answer only from
109
- > the supplied context" is only an instruction.
108
+ > - **Grok:** its sandbox is off except during Guided Review, which uses the
109
+ > read-only sandbox. "Answer only from the supplied context" is only an
110
+ > instruction.
110
111
 
111
112
  Details per backend:
112
113
  [Backends: access and retention](docs/user-guide.md#backends-access-and-retention)
@@ -6,7 +6,7 @@ The normal journey is: **open a PR → read and investigate → leave when satis
6
6
 
7
7
  ## Open a review
8
8
 
9
- Requirements: local Pi interactive mode, Git, authenticated GitHub CLI (`gh`), and Claude or Muse configured for review. Run `gh auth status` to check GitHub authentication; `/bro doctor` checks Bro's selected backend setup but does not prove GitHub access or model connectivity.
9
+ Requirements: local Pi interactive mode, Git, authenticated GitHub CLI (`gh`), and a configured review backend (Claude or Muse is recommended for untrusted PRs). Run `gh auth status` to check GitHub authentication; `/bro doctor` checks Bro's selected backend setup and shows an advisory when the review backend uses a shell-capable sandbox, without proving GitHub access or model connectivity.
10
10
 
11
11
  ```text
12
12
  /bro guided-review https://github.com/OWNER/REPO/pull/123
@@ -20,12 +20,6 @@ A URL works even when your current workspace belongs to a different repository.
20
20
 
21
21
  A **new** review captures the PR's base/head commits outside your working tree and automatically makes one model request. Subsequent questions each make another request. Calls use your selected backend CLI's account and billing—not Pi's model account. Bro does not automatically retry a failed generation or switch backends.
22
22
 
23
- For fullscreen mouse scrolling, start Pi with:
24
-
25
- ```sh
26
- pi --tui-mode fullscreen
27
- ```
28
-
29
23
  Opening the **same PR again** restores its saved review without another model call. Bare `/bro guided-review` lists saved reviews; `/bro guided-review resume` remains an alias for that list.
30
24
 
31
25
  **Reopening keeps the captured code.** It does not fetch a new comparison when the author pushes commits. Opening by URL/number checks current metadata and reports newer source, but continues the saved capture. The saved-review picker does not check freshness. Reviewing newer revisions is not implemented.
@@ -63,7 +57,7 @@ The generation header shows observable application stages and elapsed time—not
63
57
  | Close | Save automatically and leave |
64
58
  | Stop and close | Stop active work, await cancellation, save partial work, then leave |
65
59
 
66
- In fullscreen mode, the wheel scrolls the pane under the pointer without moving its list selection or keyboard focus. Reading an active answer pauses following; deliberately returning to the bottom resumes it. In regular terminal mode, the wheel belongs to terminal scrollback.
60
+ The wheel scrolls the pane under the pointer without moving its list selection or keyboard focus, in regular and fullscreen mode. Reading an active answer pauses following; deliberately returning to the bottom resumes it. In regular terminal mode, Bro captures the mouse while the review is open (as its other windows do), so terminal scrollback and mouse text selection return when you close it; use the copy actions meanwhile.
67
61
 
68
62
  ## Ask privately, if needed
69
63
 
@@ -89,13 +83,15 @@ Use Overview → Regenerate guide only when you want a fresh explanation/assessm
89
83
 
90
84
  ## Access, privacy and storage
91
85
 
92
- Review uses **file-only inspection**, not full-access execution. Claude runs with `--restricted`, Read/Grep/Glob only, `dontAsk`, safe mode and no MCP; Muse disables write, shell and web tools, foreign personal context and native session logs, without trusting repository rules. No model-run project tests, commands, Git or network tools are available. Unsupported/older CLI flags fail visibly; there is no full-access fallback.
86
+ Review never runs with full-access or bypass flags. **Claude and Muse provide strict file-only inspection** and are recommended for untrusted PRs: Claude runs with `--restricted`, Read/Grep/Glob only, `dontAsk`, safe mode and no MCP; Muse disables write, shell and web tools, foreign personal context and native session logs, without trusting repository rules. No model-run project tests, commands, Git or network tools are available on Claude or Muse.
87
+
88
+ **Codex, Grok and Agy run with shell available inside their own sandbox**, and each sandbox is different: Codex and Grok apply a read-only sandbox (`codex exec --sandbox read-only --ephemeral`, `grok --sandbox read-only --permission-mode dontAsk`), while Agy's `--sandbox` only restricts terminal commands and still lets them write inside the project. All three inspect the captured checkout with a 10-minute review deadline, and **none is shell-free**. When one of these backends is selected for review, Bro shows an advisory during acquisition, in the live review workspace, and in `/bro doctor` (as a non-fatal informational note). Set the **review** override to Claude or Muse in `/bro config` for untrusted PRs; saved reviews remain readable with any backend. Unsupported/older CLI flags fail visibly; there is no bypass fallback.
93
89
 
94
- Agy, Grok and Codex are not available for review generation/questions: supported controls do not establish equivalent shell-free inspection (Codex read-only sandbox still executes commands). Set the **review** override to Claude or Muse in `/bro config`; shared defaults and other features are unchanged. Saved reviews remain readable without a supported model backend. Doctor shows the review override requirement as information; it does not make an otherwise healthy installation fail.
90
+ Captured evidence is read from pinned Git blobs, so a shell-capable backend that writes inside the checkout cannot alter the code shown as evidence — but it can still change what it later reads, so treat very large changes with suspicion when using Agy.
95
91
 
96
92
  These controls reduce malicious-code execution/write risk, but repository content can still manipulate the model's conclusions. Captured content, questions and answers go to the provider; do not use review as a secrets boundary or approval certificate. CLI enforcement and managed host policies remain the backend's responsibility.
97
93
 
98
- PR identity/description, diff, relevant saved discussion and your Bro preferences are sent to the selected backend. It can read the captured source. Main-session conversation is not automatically attached. Each request reconstructs context; review does not resume BTW/native sessions. Claude/Muse disable native session persistence where their CLIs support it; backend/provider retention policies still apply.
94
+ PR identity/description, diff, relevant saved discussion and your Bro preferences are sent to the selected backend. It can inspect the captured source. Main-session conversation is not automatically attached. Each request reconstructs context; review does not resume BTW/native sessions. Claude, Muse and Codex disable native session persistence where their CLIs support it; backend/provider retention policies still apply.
99
95
 
100
96
  Records, private text and captured source remain under:
101
97
 
@@ -112,7 +108,7 @@ One open writer per review is supported within a Pi process. Concurrent editing
112
108
  | Problem | What to do |
113
109
  | --- | --- |
114
110
  | GitHub unavailable when reopening | Use bare `/bro guided-review` to choose a saved review; the picker does not fetch |
115
- | Unsupported review backend | Choose Claude or Muse for review in `/bro config`; no automatic switching |
111
+ | Sandboxed shell review backend (Codex, Grok, Agy) | Advisory shown; choose Claude or Muse in `/bro config` for strict file-only inspection on untrusted PRs |
116
112
  | Git/gh authentication or fetch error | Read the acquisition error; check Git/`gh auth status`; retry explicitly |
117
113
  | Invalid/failed preparation | Changed files remain available; use Prepare guide to retry when ready for another call |
118
114
  | Failed regeneration | Current guide is kept; retry only if you want another call |
@@ -88,6 +88,7 @@ next mode without saving it:
88
88
 
89
89
  - **Mouse wheel / trackpad**: Scroll in regular or fullscreen mode
90
90
  - **↑ / ↓**: Scroll in any mode
91
+ - **PgUp / PgDn**: Scroll a page at a time (also in btw)
91
92
  - **C**: Copy the complete explanation
92
93
  - **R**: Simplify the captured source again in the mode shown, or run the
93
94
  current Doctor check again
@@ -119,8 +120,9 @@ Nothing is automatically applied or posted to GitHub.
119
120
  /bro guided-review https://github.com/OWNER/REPO/pull/123
120
121
  ```
121
122
 
122
- Local Pi interactive mode, Git, authenticated `gh`, and Claude or Muse configured for review
123
- are required. A new review makes a model call using that backend's account.
123
+ Local Pi interactive mode, Git, authenticated `gh`, and a configured review backend
124
+ (Claude or Muse is recommended for untrusted PRs). A new review makes a model call
125
+ using that backend's account.
124
126
  Opening the same PR restores its saved review without another model call.
125
127
  Bare `/bro guided-review` lists saved reviews (`resume` is an alias).
126
128
 
@@ -138,8 +140,15 @@ Optional conveniences:
138
140
 
139
141
  Reopening also keeps the captured revision, even after the author pushes.
140
142
  Findings are model suggestions, not approval; citations show real locations,
141
- not proof of the conclusion. Review uses Claude or Muse file-only inspection: shell, writes and web tools are disabled. Agy/Grok/Codex review requests fail clearly; choose a review override in `/bro config`. Saved reviews remain readable with any selection. Repository text can still mislead the model; tool restrictions are not proof of its conclusions.
142
- Use `pi --tui-mode fullscreen` for pane-local wheel scrolling.
143
+ not proof of the conclusion. Claude and Muse use strict file-only inspection
144
+ (shell, writes and web tools disabled). Codex and Grok use a read-only sandbox;
145
+ Agy's sandbox only restricts terminal commands and still allows writes inside
146
+ the project. All three run shell commands, and an advisory says so.
147
+ Claude or Muse is recommended for untrusted PRs; choose a review override in
148
+ `/bro config`. Saved reviews remain readable with any selection. Repository
149
+ text can still mislead the model; sandboxing and tool restrictions are not
150
+ proof of its conclusions.
151
+ The wheel scrolls the pane under the pointer in regular and fullscreen mode.
143
152
 
144
153
  See the [Guided Review guide](guided-review.md) for controls, calls and
145
154
  costs, source freshness, access/privacy, local storage, recovery and current
@@ -207,8 +216,8 @@ activates `bro_advisor`, its resolved backend/model/effort, steering
207
216
  presence, and backend compatibility (for Agy, a minimum CLI version with an
208
217
  `agy update` action when it is too old).
209
218
 
210
- - **Automatic context, no prep needed**: the executor never assembles a
211
- summary. Bro captures a harness-neutral snapshot — the executor's system
219
+ - **Automatic context, no prep needed**: the executor does not need to assemble
220
+ a summary. Bro captures a harness-neutral snapshot — the executor's system
212
221
  instructions, its active tools, and the conversation so far including tool
213
222
  calls and results — and sends it, along with an optional `question` the
214
223
  executor may pass, to a **fresh, standalone process of the selected
@@ -748,8 +757,10 @@ Use `/bro config` to review or change the shared default backend/model/effort
748
757
  and any per-capability (explain/show/btw/advisor/review) overrides, the explanation
749
758
  mode, and the default show turn count. Changes save immediately. Esc inside a
750
759
  picker cancels that pick; Esc on the settings screen closes it, keeping
751
- whatever was already saved. A failed save (for example, a read-only file) is
752
- shown inline. `/bro mode` changes the mode directly (**M** in an explanation
760
+ whatever was already saved. If the terminal becomes too small, config blocks
761
+ hidden actions; Esc closes config even if a picker was open, after pending saves
762
+ finish. A failed save cancels that close and shows its error inline; if the error
763
+ alone makes config too small, any other key clears it so you can try again. `/bro mode` changes the mode directly (**M** in an explanation
753
764
  tries another mode without saving it), and `/bro help` shows the active settings
754
765
  and file path.
755
766
 
@@ -800,18 +811,25 @@ data under their own settings and policies.
800
811
 
801
812
  | | Explain / show | BTW conversation-only | BTW full permission | Advisor | Guided Review |
802
813
  | --- | --- | --- | --- | --- | --- |
803
- | **Agy** | Temporary directory, Agy sandbox | Temporary directory, Agy sandbox | Workspace, permissions bypassed | Fresh workspace process, permissions bypassed | Unavailable; choose Claude/Muse |
814
+ | **Agy** | Temporary directory, Agy sandbox | Temporary directory, Agy sandbox | Workspace, permissions bypassed | Fresh workspace process, permissions bypassed | Captured checkout, Agy sandbox (shell and in-project writes allowed; advisory) |
804
815
  | **Claude Code** | Scratch directory, tools/MCP/skills disabled, no session persistence | Workspace, tools disabled | Workspace, permissions bypassed | Fresh workspace process, permissions bypassed | Restricted Read/Grep/Glob only; confined checkout, no persistence |
805
- | **Grok** | Temporary directory, **prompt instruction only** | Workspace, **prompt instruction only** | Workspace, permissions bypassed | Fresh workspace process, permissions bypassed | Unavailable; choose Claude/Muse |
806
- | **Codex** | Scratch directory, read-only sandbox, ephemeral session | Workspace, read-only sandbox | Workspace, approvals and sandbox bypassed | Fresh workspace process, approvals and sandbox bypassed | Unavailable; command execution is not allowed for review |
816
+ | **Grok** | Temporary directory, **prompt instruction only** | Workspace, **prompt instruction only** | Workspace, permissions bypassed | Fresh workspace process, permissions bypassed | Captured checkout, read-only sandbox (shell-enabled; advisory) |
817
+ | **Codex** | Scratch directory, read-only sandbox, ephemeral session | Workspace, read-only sandbox | Workspace, approvals and sandbox bypassed | Fresh workspace process, approvals and sandbox bypassed | Captured checkout, read-only sandbox, ephemeral (shell-enabled; advisory) |
807
818
  | **Muse** | Scratch directory, approval/write/shell disabled, no session log | Workspace, approval/write/shell disabled | Workspace, permissions bypassed (`--yolo`) | Fresh workspace process, permissions bypassed (`--yolo`) | Captured checkout; writes/shell/web disabled, no session log |
808
819
 
809
- - Grok always runs with its sandbox off and permissions bypassed; its tools,
810
- hooks, skills, plugins, and MCP may remain available. "Answer only from the
811
- supplied context" is a request, not an enforced restriction.
820
+ - Grok always runs explain, show, btw and the advisor with its sandbox off and
821
+ permissions bypassed; its tools, hooks, skills, plugins, and MCP may remain
822
+ available. "Answer only from the supplied context" is a request, not an
823
+ enforced restriction. Guided Review is the exception: it runs with
824
+ `--sandbox read-only --permission-mode dontAsk` but still executes shell
825
+ commands inside that sandbox.
812
826
  - Codex's read-only sandbox is not a tool-free mode: workspace reads and
813
- sandboxed commands remain available. Bro does not disable configured MCP
814
- servers or web search; their access is governed by Codex configuration.
827
+ sandboxed commands remain available, including for Guided Review. Bro does
828
+ not disable configured MCP servers or web search; their access is governed
829
+ by Codex configuration.
830
+ - Agy's Guided Review runs under `--sandbox`, which restricts terminal
831
+ commands but still permits writes inside the project; it has no file-only
832
+ tool filter.
815
833
  - Muse's restricted mode disables write and shell tools and approval prompts,
816
834
  not all tools: workspace reads and web tools remain available without approval.
817
835
  It is not a tool-free or network-isolated mode.
@@ -914,7 +932,7 @@ read. Move what you want to keep into `/bro preferences`, and choose
914
932
  seeded main-session conversation text (plus earlier turns when a native
915
933
  session is reseeded) to the selected backend. In full permission mode the
916
934
  side agent can additionally read and edit the workspace.
917
- - **Guided Review requests**: PR identity/description, captured diff, relevant private discussion and preferences go to the selected backend, which can inspect captured files using Claude/Muse read-only tools. Main-session context is not attached. Review shell/write/web tools are disabled; Claude file tools are confined with `--restricted`, Muse uses workspace-scoped inspection. Prompt injection can still corrupt conclusions or reveal captured content to the provider; this is not a general confidentiality guarantee. See [Guided Review access and storage](guided-review.md#access-privacy-and-storage).
935
+ - **Guided Review requests**: PR identity/description, captured diff, relevant private discussion and preferences go to the selected backend, which can inspect captured files. Main-session context is not attached. Claude (`--restricted`, file tools only) and Muse (write/shell/web disabled) use strict file-only inspection and are recommended for untrusted PRs. Codex and Grok inspect the captured checkout under a read-only sandbox; Agy's `--sandbox` still allows in-project writes. All three run shell commands and show an advisory. Prompt injection can still corrupt conclusions or reveal captured content to the provider; this is not a general confidentiality guarantee. See [Guided Review access and storage](guided-review.md#access-privacy-and-storage).
918
936
  - **Preferences**: `bro-preferences.md` is sent with every explain, Show,
919
937
  BTW, and Guided Review request to the selected backend. It is never sent to the advisor or
920
938
  to Pi's main model.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bro",
3
- "version": "0.23.1",
3
+ "version": "0.24.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",
@@ -56,6 +56,7 @@
56
56
  "src/sources.ts",
57
57
  "src/config-ui.ts",
58
58
  "src/ui-capabilities.ts",
59
+ "src/modal-kit.ts",
59
60
  "src/backend.ts",
60
61
  "src/prompt.ts",
61
62
  "src/vendor/mermaid.min.js",
@@ -75,7 +76,7 @@
75
76
  },
76
77
  "scripts": {
77
78
  "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",
79
+ "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 test/modal-contract.test.mjs benchmark/*.test.ts && sh ./test/smoke-test.sh",
79
80
  "benchmark:dry-run": "node benchmark/run.ts dry-run",
80
81
  "benchmark:run": "node benchmark/run.ts run",
81
82
  "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\`.` };