Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a87a8b0
Add review configuration plugin
ravarora2 Nov 11, 2026
fe42ff8
Save reviewer agents with each review lane
ravarora2 Nov 11, 2026
b0bde7e
Submit PRs to saved review lanes
ravarora2 Nov 12, 2026
b8531d8
Route PR reviews to matching lanes
ravarora2 Nov 12, 2026
fcbb2d1
Separate repository settings and route pasted PR URLs
ravarora2 Nov 12, 2026
fd7ebc6
Edit and save review lanes individually
ravarora2 Nov 12, 2026
99b22e4
feat(review): separate one-step PR submission and notify existing rev…
ravarora2 Nov 12, 2026
94846c0
test(review): cover separate submission page registration
ravarora2 Nov 12, 2026
53c76b8
feat(review): separate PR table and submission actions
ravarora2 Nov 12, 2026
4be5f1f
fix(review): group page navigation in a menu
ravarora2 Nov 12, 2026
df1913c
Put review actions and PR table on the main page
ravarora2 Nov 12, 2026
e3b4dee
Open pull requests from a top action
ravarora2 Nov 12, 2026
4cd3073
Restart deleted reviews and add reviewer reminders
ravarora2 Nov 12, 2026
b19ed8f
Show PR status and stop reminders for closed reviews
ravarora2 Nov 12, 2026
747edf3
Record PR review status from host-bound threads
ravarora2 Nov 12, 2026
ec72417
Merge main into review configuration
ravarora2 Nov 12, 2026
6f3b0ab
Document ACP review context handoff
ravarora2 Nov 12, 2026
921fb9b
Register review plugin in native catalog
ravarora2 Nov 13, 2026
094f9f4
Gate PR reviews with admission checks and triage
ravarora2 Nov 13, 2026
8a6bbd7
Clarify review outcomes, approval policy, and lane reminders
ravarora2 Nov 13, 2026
ce052f8
Record review acceptance and clarify reviewer instructions
ravarora2 Nov 13, 2026
1e6a164
Add direct Pull requests dock navigation
ravarora2 Nov 13, 2026
2b6abc2
Use Testing Library options in PR navigation test
ravarora2 Nov 13, 2026
89d1fc6
Give Review Configuration its own dock entry
ravarora2 Nov 13, 2026
e5a547a
Require review acceptance before outcomes
ravarora2 Nov 13, 2026
1ab3cf3
Show PR review updates and dates
ravarora2 Nov 13, 2026
7d5bde9
Warm the diff fixture before browser navigation
ravarora2 Nov 13, 2026
18e16c4
Acknowledge PR updates on opening the list
ravarora2 Nov 13, 2026
a96446a
Type the lane reminder test fixture explicitly
ravarora2 Nov 13, 2026
035c6c8
Show compact PR rows with full review details
ravarora2 Nov 13, 2026
ce05290
Scope review configuration to private channels
ravarora2 Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/plugin-manager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,10 @@ pub fn bundled_manifests() -> Vec<Manifest> {
"../../../src/bundled/changesets/manifest.json"
))
.expect("changesets manifest"),
serde_json::from_str(include_str!(
"../../../src/bundled/review-configuration/manifest.json"
))
.expect("review configuration manifest"),
]
}
/// New bundles must opt in to the default-on policy.
Expand Down
1 change: 1 addition & 0 deletions crates/plugin-manager/tests/management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1384,6 +1384,7 @@ fn bundled_defaults_preserve_saved_choices_and_only_channels_is_required() {
| "buzz.drafts-and-sent"
| "buzz.changesets"
| "buzz.charts"
| "buzz.review-configuration"
);
assert_eq!(plugin.enabled, default, "{id}");
if id == "buzz.channels" {
Expand Down
48 changes: 47 additions & 1 deletion docs/buzz-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,53 @@ tab, asks **Discard changes?** first; **Keep editing** keeps them.
local installs. Upgrades preserve their previous state, including Bestie off
when no choice was saved. Enable or disable them in **Manage plugins**; saved
choices persist. A deliberate plugin-settings reset uses the current defaults.
- **Review Configuration** is optional and starts off. Enable it in **Manage
plugins** to add separate **Review Configuration** and **Pull requests** dock icons. Register a repository in a private review channel you belong to,
then save named lanes with reviewer agents and
instructions. Each lane supports multiple reviewers. Saved
repositories appear in a list; open one to manage its lanes. **Reload**
reads the latest saved versions. Settings live on the community relay and are
visible to channel members; members who can post can edit them. A conflicting
edit preserves your draft: copy your changes before cancelling and reloading.
**Review Configuration** has matching **Register repository**, **Submit PR**, and **Pull requests** actions at the top, followed by **Repositories**, grouped with their channel configurations. Set a personal default channel per repository; Submit PR uses it and allows an override. Defaults are saved on this device per identity/community. If access to a default is lost, choose another configuration. Only current members can use a private review channel; existing open-channel configurations are excluded and left unchanged. **Edit lane**
opens one lane; **Save lane** persists it before **Add lane** starts another.
**Back to lanes** cancels the editor; **Back to repositories** returns to the list.
Every new or edited lane requires at least one all/any routing rule matching
title text, labels, changed-file counts, or paths such as `src/**/*.tsx`.
Earlier rule-free lanes still match every PR until edited.
Each repository also has **Admission checks**, all off by default: require a
non-draft PR, green CI, no merge conflicts, and/or agent triage. Save selected
checkboxes before leaving. CI requires all reported statuses/checks to pass
(neutral/skipped count as passing); missing/pending results and unknown merge
conflicts block submission with a reason. Agent triage requires an assigned
owned agent and instructions; **Refresh agents** updates choices. Basic checks
run first. Triage gets its own thread and **Awaiting triage** status. Its agent
calls `pr_triage_decision` with `admit` or `reject` and a message; Buzz supplies
the target. Admit rechecks the PR and starts lane routing automatically;
reject shows the reason without closing the GitHub PR. Head/base changes during
triage require deleting and resubmitting. Keep the submitting device connected
with this plugin enabled; continuation works with the page closed and resumes
after reconnect. Submit again on that device to retry a failed continuation.
Decisions are final; delete and resubmit to request fresh triage. Saved admission
settings apply to new submissions. Pending/rejected triage cannot use Remind.
The **Submit PR** page has a full GitHub PR URL field and a
Submit button. It finds the repository in the selected review channel, reads
current PR details, and assigns the first configured reviewer in each matching
lane. Missing configuration or zero matching lanes sends nothing. Results show
assigned agents and their thread buttons. Choose the **Pull requests** dock icon or the matching action on the configuration page to see one compact row per PR with its overall status, lane-status summary, and update marker. Click a row or activate its PR button to open full details; **Back to Pull requests** returns to the list. Back/Forward and reload restore the selected PR after confirming access. The Pull requests page also has a **Submit PR** button. The table shows GitHub status (Open, Draft, Closed, or Merged); Reload refreshes it. Remind is disabled for closed, merged, loading, or unavailable PRs and rechecks GitHub before sending. The PR detail table separates **Lane threads**, **Lane status**, and **Lane last message**, with one row per lane. **Full message** is an underlined link in **Lane last message** and opens a popup; Back/Forward preserves it, while Close dismisses it. The original submitter can click **Remind** on each review lane to notify its reviewer in the existing thread, with an optional message included in the reminder; Admission Triage has no reminder. Only the reminded lane resets to Needs review. Finish an interrupted reminder in that lane before starting another. The submitter can also confirm **Delete** to remove a submission from the shared list; its conversations and GitHub PR remain. Deleting does not stop active reviews. Resubmitting a deleted PR applies current routing and creates new reviewer threads.
Submitting an existing PR reuses its saved reviewers and threads and mentions
them again in those threads. Incomplete initial delivery resumes from the original
browser profile. An interrupted notification retries its saved messages.
Internal **Review status** is separate from GitHub status: Needs review, Reviewing, Changes requested, Looks good · not approved, or Approved. Reviewers must first call `pr_review_status_update` with `reviewing` to acknowledge acceptance, even for acknowledgment-only requests; the status tool refuses outcomes until Reviewing is saved for that request and posts a tagged correction for a skipped step. A reminder requires a new Reviewing acknowledgment. Requests also ask reviewers to add their configured GitHub account to the PR’s requested reviewers when permitted; shared GitHub accounts do not create distinct reviewer identities. After reviewing, they report `request_changes`, `lgtm_not_approved`, or `approved` through `pr_review_status_update`; Buzz supplies the review thread. Each lane has **Allow automatic approval**, unchecked by default. When off, reviewers must not approve on GitHub and the tool refuses `approved`. When on, reviewers may approve after review and report `approved` only after GitHub succeeds. The tool records the reviewer’s report; it does not submit or independently verify GitHub approval. Lane settings apply to new submissions. Older Done reports appear as Completed · outcome unspecified; they do not imply approval. Requests put **Lane instructions** first; reminders highlight **Your reminder message** above them. Compact steps come from the status tool. Remind resets its selected lane to Needs review; resubmitting an existing PR resets all review lanes. Reviews of a PR run sequentially in the same lane threads. Incoming reports update the table; Reload catches missed updates. Reporting requires native Codex thread sessions, a compatible shared harness, or a standalone MCP process bound to that thread by its host. An unsupported host refuses the update; a chat reply alone does not record a result.
Completed outcomes are green, pending review is amber, and changes requested are red; labels still distinguish approval from completion. **Submitted** shows the original submission date (older records say **Not recorded**). **Last reviewed** shows the latest result, including previous reminder rounds; accepting a review does not count. Dates use your local timezone.
The **Pull requests** dock badge counts PRs with unseen status updates in the selected review channel. Opening Pull requests clears the badge for the loaded updates. Affected PRs and lanes keep an **Updated** marker during that visit, including list → PR details, so you can see what changed. Live changes while the page is open are highlighted too. Each lane shows **Last updated** from its latest status report or thread message, including reminders. Seen state persists on this device for your identity, community, and channel; later changes mark the PR again.
The separate Submit PR form has a **Back to Review Configuration** button. All pages remember the selected
review channel. Reviewers must have a verified owner who belongs to the selected channel, and already
belong to the channel. A connected agent runtime is needed for replies;
delivery alone does not mean a review started. Desktop uses signed-in GitHub
access; browser metadata reads support public PRs and may hit rate limits.
PRs above 3,000 changed files or 100 labels are unsupported. This does not post
GitHub comments, track verdicts, pin reviewer checkouts, or schedule reminders.
- A plugin's details in Manage plugins link to the Settings sections that hold
its settings: sections it adds, such as GitHub comment authors, and Decisions
API for a decision provider, such as TypeSafe. Only sections the Settings
Expand Down Expand Up @@ -1499,4 +1546,3 @@ occurs. It contains operation metadata, with no draft text or secrets. See
## Plugin account actions

Plugins can add actions to your avatar menu. Profile-related actions may appear just above Settings; other plugin actions appear below it.

9 changes: 9 additions & 0 deletions docs/plugin-authoring-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ export function apply(ctx) {
| `subjectLift` (optional) | Lift a message image's subject | `const lift = ctx.reflect.get("subjectLift")`. `lift?.supports(source)`, then `await lift.lift(source, signal?)` resolves a transparent PNG `File` ready for `session.emoji.upload`, or rejects with a reason. `source` is `session.media(attachment.url)`. macOS 14+ only; see the plugin architecture's Subject lift contract. |
| `pluginInstaller` (optional) | Ask the owner to install another plugin, check installation status, or access retained update reviews | `requestInstall(...)` resolves `"installed" \| "updated" \| "cancelled" \| "unavailable"` and never installs silently. Status methods are described below. `updates` is an optional, host-retained capability; see [Plugin architecture](plugin-architecture.md#plugin-installer). |

Plugins can read the current documentation for a built-in or active plugin tool
with `ctx.agents.describeTool(name)`. It returns a detached
`{ name, description, inputSchema }`, or `undefined` when unavailable, and never
returns the handler. Use this to compose agent instructions from the actual tool
definition instead of maintaining a second copy. Read it when composing each
message; already sent messages keep their original text. Feature-check the method
on older hosts. Describing a tool does not grant access to call it or install it
on another agent's runtime.

The installation-status methods may be missing on older hosts even when `pluginInstaller` is available, so feature-check each method before calling it. `getInstallationStatus` returns `"installed"` when the manifest ID is in the ready catalog, `"absent"` when it is not, and `"unavailable"` while the catalog is not ready. Installed status is independent of whether the plugin is enabled, contributes UI, or came from a particular install source. A subscription reports later status changes; subscribe first, then read the current status to avoid missing a change between the read and subscription. Own the subscription with `ctx.effect`:

```js
Expand Down
59 changes: 59 additions & 0 deletions src/app/pages.integration.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ test("the app runtime exposes ready bundled pages and removes them on disable",
"buzz.focus",
"buzz.charts",
"buzz.drafts-and-sent",
"buzz.review-configuration",
"buzz.changesets",
].includes(plugin.manifest.id),
plugin.manifest.id,
Expand All @@ -103,12 +104,70 @@ test("the app runtime exposes ready bundled pages and removes them on disable",
"buzz.reminders",
"buzz.workflows",
"buzz.drafts-and-sent",
"buzz.review-configuration",
]) {
assert.equal(
services.pages.snapshot().some((page) => page.pluginId === id),
false,
);
}
await services.plugins.change("enable", "buzz.review-configuration");
await vi.waitFor(() =>
assert.ok(
services.pages
.snapshot()
.some(
(page) => page.key === "buzz.review-configuration/configuration",
),
),
);
const reviewConfiguration = services.pages
.snapshot()
.find((page) => page.pluginId === "buzz.review-configuration");
assert.equal(reviewConfiguration.title, "Review Configuration");
assert.equal(reviewConfiguration.primary, true);
assert.equal(reviewConfiguration.placement, "dock");
assert.match(
renderToStaticMarkup(createElement(reviewConfiguration.component)),
/Connect to a community to configure reviews/,
);
const submitPr = services.pages
.snapshot()
.find((page) => page.key === "buzz.review-configuration/submit");
assert.ok(submitPr);
assert.equal(submitPr.title, "Submit PR");
assert.equal(submitPr.primary, false);
assert.equal(submitPr.placement, "sidebar");
assert.match(
renderToStaticMarkup(createElement(submitPr.component)),
/Connect to a community to submit PRs/,
);
const allPrs = services.pages
.snapshot()
.find((page) => page.key === "buzz.review-configuration/all");
assert.ok(allPrs);
assert.equal(allPrs.title, "Pull requests");
assert.equal(allPrs.primary, true);
assert.equal(allPrs.placement, "dock");
const detail = { channel: "reviews", submission: "submission" };
assert.equal(allPrs.route.validate(detail), true);
assert.equal(
allPrs.route.validate({ ...detail, message: "a".repeat(64) }),
true,
);
assert.equal(
allPrs.route.validate({ ...detail, submission: "../invalid" }),
false,
);
assert.equal(submitPr.route.validate(detail), false);
assert.equal(reviewConfiguration.route.validate(detail), false);
await services.plugins.change("disable", "buzz.review-configuration");
assert.equal(
services.pages
.snapshot()
.some((page) => page.pluginId === "buzz.review-configuration"),
false,
);
await services.plugins.change("enable", "buzz.reminders");
await vi.waitFor(() =>
assert.ok(
Expand Down
4 changes: 4 additions & 0 deletions src/app/shell/presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
NoteIcon,
GearIcon,
LightningIcon,
ListChecksIcon,
GitPullRequestIcon,
RobotIcon,
SendIcon,
} from "../../shared/design-system/icons/index";
Expand Down Expand Up @@ -58,6 +60,8 @@ const bundledIcons = new Map<string, typeof BrowserIcon>([
["buzz.reminders/reminders", AlarmIcon],
["buzz.agents/agents", RobotIcon],
["buzz.workflows/workflows", LightningIcon],
["buzz.review-configuration/configuration", ListChecksIcon],
["buzz.review-configuration/all", GitPullRequestIcon],
["buzz.relay-notes/notes", NoteIcon],
]);
/** Resolve host navigation policy once, independently of icons and registrations. */
Expand Down
1 change: 1 addition & 0 deletions src/bundled/agents/AgentsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ function setup(
session: vi.fn(async () => {}),
register: vi.fn(),
addTool: vi.fn(),
describeTool: vi.fn(),
createToolOperation: vi.fn(() => {
throw new Error("No operation provider in this fixture");
}),
Expand Down
43 changes: 42 additions & 1 deletion src/bundled/codex/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4880,7 +4880,7 @@ it("offers the tools plugins add, runs them as the agent here, and starts a new
expect(JSON.stringify(reply?.result)).toContain("created");
expect(call).toHaveBeenCalledWith(
expect.objectContaining({ pubkey }),
{ channel: "channel", root },
{ channel: "channel", root, review: { channel: "channel", root } },
{ title: "Ship it" },
);
await f.complete();
Expand Down Expand Up @@ -7377,6 +7377,47 @@ it("neither lists nor runs the retired owner-read tools, while its own reads wor
await f.complete();
});

it("supplies an immutable review thread outside tool arguments and omits it for channel-wide sessions", async () => {
const f = fixture();
const contexts: unknown[] = [];
f.added.push({
name: "inspect_review_context",
description: "Inspect host context.",
inputSchema: { type: "object", properties: {} },
call: async (_client, context) => {
contexts.push(context.review);
return "inspected";
},
});
await f.runtime.run(f.delivery("review"));
await vi.waitFor(() => expect(f.starts()).toHaveLength(1));
await f.tool("inspect_review_context", {
review: { channel: "wrong", root: "wrong" },
});
await f.runtime.run(f.delivery("follow-up"));
await vi.waitFor(() =>
expect(f.sent.some((wire) => wire.method === "turn/steer")).toBe(true),
);
await f.tool("inspect_review_context", {});
expect(contexts).toEqual([
{ channel: "channel", root },
{ channel: "channel", root },
]);
await f.complete();
await vi.waitFor(() =>
expect(f.runtime.sessions(pubkey)[0]?.status).toBe("Idle"),
);
const next = f.delivery("channel scope", "d".repeat(64));
await f.runtime.run({
...next,
config: { ...(next.config as object), scope: "channel" },
});
await vi.waitFor(() => expect(f.starts()).toHaveLength(2));
await f.tool("inspect_review_context", {}, 1);
expect(contexts[2]).toBeUndefined();
await f.complete(1);
});

it("supplies an operation proof only while the native Codex call is active", async () => {
const f = fixture();
const provider = createToolOperation("example/proposal", () => ({
Expand Down
10 changes: 10 additions & 0 deletions src/bundled/codex/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ type Saved = {
protection?: string;
};
type Active = {
readonly review?: Context["review"];
/** The server this turn runs on; steering must reach the same process. */
rpc: AppServer;
threadId?: string;
Expand Down Expand Up @@ -2324,6 +2325,7 @@ export class CodexRuntime {
// A watch or timer turn has no default destination.
const context: Context = request
? {
...(active?.review ? { review: active.review } : {}),
...(request.session ? { session: request.session } : {}),
channel: request.conversation.channelId,
...(request.toolRoot ? { root: request.toolRoot } : {}),
Expand Down Expand Up @@ -2774,6 +2776,14 @@ export class CodexRuntime {
...(adopted && lane.voice ? [lane.voice.signal] : []),
]);
const active: Active = {
...(request.conversation.root && !request.wake
? {
review: {
channel: request.conversation.channelId,
root: request.conversation.root,
},
}
: {}),
rpc: server.rpc,
...(adopted && lane.voice
? { threadId: lane.voice.loaded.threadId, turnId: adopted }
Expand Down
7 changes: 7 additions & 0 deletions src/bundled/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ import changesetsManifest from "./changesets/manifest.json";
import * as changesets from "./changesets";
import workflowsManifest from "./workflows/manifest.json";
import * as workflows from "./workflows";
import reviewConfigurationManifest from "./review-configuration/manifest.json";
import * as reviewConfiguration from "./review-configuration";
import type { BundledPlugin } from "../plugins/manager";
import linksManifest from "./links/manifest.json";
import * as links from "./links";
Expand Down Expand Up @@ -258,6 +260,11 @@ export const bundledPlugins: readonly BundledPlugin[] = [
module: workflows,
enabledByDefault: false,
},
{
manifest: { ...reviewConfigurationManifest, apiVersion: 1 },
module: reviewConfiguration,
enabledByDefault: false,
},
{
manifest: { ...moderationManifest, apiVersion: 1 },
module: moderation,
Expand Down
Loading
Loading