Skip to content

Commit 69ba7c4

Browse files
bgagentclaude
andcommitted
feat(tooling): add /review_prs command to batch-review a filtered set of PRs (#620)
Adds .abca/commands/review_prs.md, a thin orchestrator over the existing single-PR review_pr workflow. It derives a filtered work-list (default: non-fork PRs requesting your review, unapproved, ready), then fans out one full review_pr per PR. The Stage-2 fan-out runs as a Workflow (deterministic multi-agent orchestration), not prose subagent dispatch: each PR's diff, nested review_pr agents, and review body stay in a Workflow-spawned subagent's context, and only a compact schema-validated result per PR (number, verdict, rationale, review URL) returns to the caller — so a large queue can't blow the main context. Git pre-staging (worktrees/branches) stays inline before the Workflow since it mutates shared .git state. Execution context: /review_prs is an interactive Claude Code / Cursor command. It cannot run in ABCA's headless runtime, which resolves its SDK tool surface from an allowlist (runner.py _resolve_allowed_tools / _FULL_TOOL_SURFACE) that excludes Workflow/Task/Agent — the incident-driven hardening against a repo-less task spawning a detached Workflow. The command documents this, notes .claude/ config is discoverable to a cloned-repo task (so the allowlist, not obscurity, is the guarantee), and that coding/pr-review-v1 (bound to the pr_review prompt) can't be steered into it. Pre-staging is idempotent (forced +pull/<n>/head refspec + run-scoped scratch dir) and Stage-3 cleanup runs unconditionally (finally-style, worktrees/branches removed on throw/interrupt too). Stage-1 --json now fetches headRepositoryOwner so a dropped fork PR can be named. The command delegates to review_pr.md via a relative link rather than duplicating the review process, so the review bar stays single-sourced. Addresses review feedback from theagenticguy on #621. Refs #620 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 22705e8 commit 69ba7c4

5 files changed

Lines changed: 200 additions & 2 deletions

File tree

.abca/commands/review_prs.md

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Review Multiple Pull Requests in ABCA
2+
3+
## Persona
4+
5+
Hold the same bar as [`review_pr`](./review_pr.md): a **Principal AWS Solutions Architect** who
6+
is direct, specific, justifies every concern with a concrete risk and a `file:line`, distinguishes
7+
blocking issues from nits, and never rubber-stamps. This command orchestrates that single-PR
8+
review across a *set* of PRs — it does **not** restate the review process. Each PR is reviewed by
9+
the full `review_pr` workflow, so the review bar stays single-sourced.
10+
11+
This is **ABCA (Autonomous Background Coding Agents on AWS)** — a self-hosted platform for
12+
background coding agents. Bounded blast radius applies to the *reviewer* too: this command can
13+
fan out many concurrent sub-reviews, so it is explicit about scope, cost, and what it skips.
14+
15+
**How the fan-out runs — and why it keeps the caller's context fresh.** Stage 1 (scouting the
16+
work-list) runs inline in the main context; it is small and cheap. Stage 2 (the per-PR reviews)
17+
runs as a **`Workflow`** — a deterministic multi-agent orchestration — *not* as prose-directed
18+
subagent dispatch. Each PR's heavy work (a large diff, the three nested `review_pr` review agents,
19+
the full review body) is confined to a Workflow-spawned subagent's context; only a **small
20+
structured result per PR** (number, verdict, one-line rationale, review URL) returns to the caller.
21+
Making the fan-out a `Workflow` means that isolation is a property of the control flow, not
22+
something an executor must remember to do — so a large queue can't blow the main window on diff
23+
bodies and agent transcripts. (Caveat: per-PR *summaries* still accrete linearly in the caller, so
24+
"fresh" ≠ "constant" — a 200-PR run still returns 200 rows.)
25+
26+
## Execution context — interactive operator only
27+
28+
**This is an interactive Claude Code / Cursor command, run by a human operator. It is NOT — and
29+
cannot be — executed by ABCA's headless agent runtime.** The distinction matters because Stage 2's
30+
core tool, `Workflow`, is unavailable to headless tasks by construction:
31+
32+
- The runtime resolves its SDK tool surface from an **allowlist**, not the on-disk config:
33+
`agent/src/runner.py` `_resolve_allowed_tools()` returns the workflow's `agent_config.allowed_tools`
34+
or falls back to `_FULL_TOOL_SURFACE = ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "WebFetch"]`
35+
(`read_only` lanes drop `Write`/`Edit` on top). `Workflow`, `Task`, and `Agent` are **not in that
36+
allowlist**, so a headless task physically cannot spawn a `Workflow` — the incident-driven
37+
hardening against a repo-less task launching a detached background `Workflow` and finalizing on a
38+
placeholder. Interactive Claude Code / Cursor sessions are not tool-gated this way, so `Workflow`
39+
is available there — which is where this command runs.
40+
- The runtime does load project `.claude/` config (`ClaudeAgentOptions(setting_sources=["project"])`
41+
in `runner.py`), so this command's stub **is** discoverable to a task that clones this repo. That
42+
is precisely why the allowlist — not mere obscurity — is the guarantee: even though the file is on
43+
disk, the tool it calls is off-surface.
44+
- The read-only review workflow (`coding/pr-review-v1`) cannot be steered into `/review_prs`: it is
45+
bound to the `pr_review` system prompt (`agent/src/workflow/loader.py`) and drives the agent
46+
through that fixed prompt over the allowlisted tool surface — there is no "invoke a slash command"
47+
path, and `Workflow` is off-surface regardless.
48+
49+
In short: operators run `/review_prs`; the ABCA runtime never does, and structurally cannot.
50+
51+
## Arguments
52+
53+
Optional.
54+
55+
- **No arguments** — review every PR that currently requests your review (the default query in
56+
Stage 1).
57+
- **Explicit list** — e.g. `#616 #612 590` — review exactly those PRs, skipping Stage-1 derivation.
58+
- **Natural-language filter** — e.g. "all open non-fork PRs with green CI", "everything P1 and
59+
above requesting my review" — translate to the appropriate `gh` search and gates in Stage 1.
60+
61+
## Stage 1: Build the work-list
62+
63+
Derive the candidate PRs. The default set is *ready-to-review, not mine, not yet approved by me,
64+
requesting my review, excluding forks*:
65+
66+
```sh
67+
gh pr list --repo <owner>/<repo> \
68+
--search "is:open is:pr draft:false -author:@me -review:approved review-requested:@me" \
69+
--json number,isCrossRepository,headRepositoryOwner \
70+
--jq '.[] | select(.isCrossRepository == false) | .number'
71+
```
72+
73+
- **Fork exclusion is client-side.** GitHub's issue/PR search has **no fork qualifier**`is:fork`
74+
is a *repository*-search qualifier and is silently ignored in a PR search (it returns a superset,
75+
not an error). Filter on `isCrossRepository == false` (a PR whose head branch lives in a fork is
76+
cross-repository); `headRepositoryOwner.login` (fetched above) tells you *which* fork — surface it
77+
when announcing a dropped cross-repo PR (e.g. `#42 — dropped: fork PR from acme-labs`).
78+
- **Optional CI-green gate (only when the user asks for "green" / "passing" / "CI clean").** Keep a
79+
PR iff its checks are all terminal **and** all passed — i.e. `gh pr checks <n>` exits `0`. Do
80+
**not** pass `--json` to `gh pr checks` in the gate: with `--json` it always exits `0` and the
81+
exit code stops meaning anything (use `--json state,bucket` only for *reporting* the state). The
82+
gate is opt-in, not the default: a red or still-running PR is often exactly the one that needs a
83+
"request changes", so gating it out by default would hide the work.
84+
- **Announce what you drop.** For every PR removed by the CI gate (or any other filter), say so and
85+
why. Silent truncation reads as "reviewed everything" when it wasn't.
86+
- **Empty list** — if nothing matches, say so and stop. Never invent PR numbers.
87+
- **Transient by design** — submitting a review clears `review-requested:@me`, so re-running the
88+
default query later returns a *different* set. That is correct for a worklist; do not treat it as
89+
a bug or try to re-review already-reviewed PRs.
90+
91+
Present the resolved list (with the reason any candidate was dropped) before fanning out.
92+
93+
## Stage 2: Fan out via `Workflow` — one full `review_pr` per PR
94+
95+
Run the per-PR reviews as a **`Workflow`**, not as inline subagent dispatch. The `Workflow` tool
96+
requires explicit opt-in — reaching for it here is warranted because the user invoked a batch
97+
review, and it is what keeps the diffs and nested-agent transcripts out of the caller's context
98+
(see the Persona note). For a **single** PR, skip the Workflow and just run `review_pr` directly —
99+
orchestration overhead isn't worth it for `N == 1`.
100+
101+
**Pre-stage shared git state ONCE, inline, before invoking the Workflow** — worktree/branch
102+
creation mutates shared `.git` state and must not race across concurrent agents:
103+
104+
1. `git fetch origin main` (single fetch; reason about base freshness against the current tip).
105+
2. Per PR: fetch its head with a **forced** refspec — `git fetch origin +pull/<n>/head:pr<n>head`
106+
(the leading `+` overwrites an existing `pr<n>head`, so a re-run after an interrupted or
107+
uncleaned run doesn't fail on the stale local branch). Add a worktree on that ref, dump
108+
`gh pr diff <n>` to a scratch file, and capture the head SHA (`gh pr view <n> --json
109+
headRefOid`) and base branch (`--json baseRefName`). Use a run-scoped scratch dir (e.g. a
110+
per-invocation temp path) so concurrent or repeated runs don't collide on worktree paths.
111+
112+
Assemble one descriptor per PR — `{ number, worktreePath, diffPath, headSha, baseRef }` — and pass
113+
the **array** as the Workflow's `args`. Then invoke `Workflow` with a self-contained script that:
114+
115+
- reads the descriptors from `args` and fans out **one agent per PR concurrently** (`parallel()`
116+
over the descriptors, or `pipeline()` if you add a downstream stage); the tool caps real
117+
concurrency, so pass all PRs — they queue and drain automatically.
118+
- has each agent execute the full [`review_pr`](./review_pr.md) workflow for its PR and **submit**
119+
the review (inline suggestions + an Approve / Comment / Request-changes decision) via the Stage-3
120+
mechanism. The agent prompt must include that PR's `worktreePath`, `diffPath`, `headSha` (the
121+
review `commit_id`), and `baseRef` — and **flag stacked PRs** whose `baseRef != main` (the diff
122+
is against the base branch, not `main`, and merge order matters).
123+
- carries the standing `review_pr` governance rule into each agent prompt: the gate is an
124+
**approved backing issue** (see
125+
[ADR-003](../../docs/decisions/ADR-003-contribution-governance.md)); a `pr/*` branch name is a
126+
de-facto-waived nit, **not** a blocker. Each agent must **read existing review threads first**
127+
and independently verify any prior blocking claim against current code before finalizing.
128+
- returns a **compact `schema`-validated result per PR** — `{ number, verdict, rationale,
129+
reviewUrl }` — and nothing larger. The `schema` is what guarantees only the summary crosses back
130+
to the caller; without it an agent's free-text return could drag a full review body into the main
131+
context, defeating the freshness goal. The Workflow's return value is the array of these results.
132+
133+
Sketch (illustrative — adapt names/paths):
134+
135+
```js
136+
export const meta = {
137+
name: 'review-prs',
138+
description: 'Review a set of PRs, one review_pr per PR, return compact verdicts',
139+
phases: [{ title: 'Review' }],
140+
}
141+
const RESULT = { type: 'object', required: ['number', 'verdict', 'reviewUrl'], properties: {
142+
number: { type: 'number' },
143+
verdict: { enum: ['APPROVE', 'COMMENT', 'REQUEST_CHANGES'] },
144+
rationale: { type: 'string' },
145+
reviewUrl: { type: 'string' },
146+
} }
147+
const results = await parallel(args.map((pr) => () =>
148+
agent(
149+
`Run the /review_pr workflow for PR #${pr.number} and SUBMIT the review with inline ` +
150+
`suggestions. Worktree: ${pr.worktreePath}. Diff: ${pr.diffPath}. commit_id: ${pr.headSha}. ` +
151+
`Base: ${pr.baseRef}${pr.baseRef !== 'main' ? ' (STACKED — diff is vs the base branch)' : ''}. ` +
152+
`Governance: gate is an approved backing issue; a pr/* branch is a waived nit. Read existing ` +
153+
`review threads first and verify prior blocking claims. Submit via gh api (see review_pr).`,
154+
{ label: `review:#${pr.number}`, phase: 'Review', schema: RESULT },
155+
)))
156+
return results.filter(Boolean)
157+
```
158+
159+
Scale the depth to the request inside each agent: a routine queue is one agent per PR running
160+
`review_pr` once; "audit these thoroughly" lets each agent invoke the deeper Stage-3 agent fan-out
161+
that `review_pr` itself prescribes.
162+
163+
## Stage 3: Submit & report
164+
165+
- **Submission mechanism** — the GitHub MCP `pull_request_review_write` tool may lack the required
166+
token scope (`Resource not accessible by personal access token`). When it does, submit via the
167+
`gh` CLI instead:
168+
169+
```sh
170+
gh api --method POST repos/<owner>/<repo>/pulls/<n>/reviews --input <review>.json
171+
```
172+
173+
where `<review>.json` has `commit_id`, `event` (`APPROVE` | `COMMENT` | `REQUEST_CHANGES`),
174+
`body`, and `comments[]`. Inline comments must anchor to a line present on the **RIGHT** side of
175+
the diff, or the call returns `422` — move any such finding into the review body instead.
176+
Each Workflow agent submits its own review this way from inside its context — the review bodies
177+
never return to the caller (only the compact `schema` result does).
178+
- **Clean up — unconditionally.** Remove every per-PR worktree (`git worktree remove --force`) and
179+
`pr<n>head` branch created in Stage 2, **whether the Workflow succeeded, threw, or the run was
180+
interrupted** — not only on the happy path. A partial or failed run still leaves worktrees and
181+
branches in the shared `.git`, so treat teardown as a `finally`, not a trailing step: bounded
182+
blast radius applies to the reviewer too. (The forced refspec in Stage 2 also lets a later run
183+
self-heal if a crash skipped teardown entirely.)
184+
- **Report** — assemble a summary table from the Workflow's returned results:
185+
**PR · verdict · one-line rationale · review URL**. Include the count reviewed and the count
186+
(and reasons) dropped in Stage 1.
187+
188+
## Notes
189+
190+
- This command is a thin orchestrator over `review_pr`. It intentionally does not duplicate the
191+
review stages, agent-invocation requirements, or output structure — refinements to the review bar
192+
land in [`review_pr`](./review_pr.md) and are inherited here automatically.
193+
- The Stage-2 fan-out uses the **`Workflow`** tool so context-freshness is enforced by the control
194+
flow rather than left to an executor following prose. This command *uses* `Workflow`; it does not
195+
modify orchestration infrastructure. Git pre-staging (worktrees/branches) stays inline before the
196+
Workflow because it mutates shared `.git` state and must not race across concurrent agents.

.claude/commands/review_prs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.abca/commands/review_prs.md

.cursor/commands/review_prs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.abca/commands/review_prs.md

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Rules:
5454
- The PR title and description become the squash commit message, so keep them accurate throughout the review.
5555
- The CI workflow runs `mise run install` then `mise run build` (compile + lint + test + synth + security scans for all packages).
5656
- Iterate on review feedback by pushing new commits to the same branch. Maintainers squash-merge when approved.
57-
- For structured reviews (human or agent), use the [`review_pr` command](https://github.com/aws-samples/sample-autonomous-cloud-coding-agents/blob/main/.abca/commands/review_pr.md) — including the **human review heuristics** (Proportionality, Coherence, Clarity, Appropriateness) for smell dimensions automation cannot catch.
57+
- For structured reviews (human or agent), use the [`review_pr` command](https://github.com/aws-samples/sample-autonomous-cloud-coding-agents/blob/main/.abca/commands/review_pr.md) — including the **human review heuristics** (Proportionality, Coherence, Clarity, Appropriateness) for smell dimensions automation cannot catch. To review a queue of PRs in one pass, use the [`review_prs` command](https://github.com/aws-samples/sample-autonomous-cloud-coding-agents/blob/main/.abca/commands/review_prs.md), which derives a filtered work-list and runs `review_pr` across each.
5858

5959
### PR checklist
6060

docs/src/content/docs/developer-guide/Contributing.md

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)