Review Panel
The Review panel loads a GitHub pull request into a local git worktree, runs an agent review pass against the diff, and lets the user push the resulting inline comments back to the PR — either one at a time or all at once.
Enabling
The panel is gated behind review.enabled (default false). Set it in
~/.loop/config.json (or a project override) to opt in:
"review": {
"enabled": true
}When disabled, the FE hides the Review tab from the panel picker and the
backend returns 403 for /review/* requests. The flag is layered
per-global / per-project / per-worktree the same way as github.gh_user.
Lifecycle
Load — the FE fetches open PRs (
GET /review/prs→gh pr list) and renders them as a clickable list; the user picks one. The backend:- Looks the PR up via
gh pr view <number>for metadata and base/head refs. - Resolves the head SHA via
gh pr view --json headRefOid. - Creates a worktree off the PR head branch under the channel’s
dir_path. - Fetches the base ref into the parent repo (
git fetch origin <base>) soorigin/<base>resolves locally for the Run step.
- Looks the PR up via
Run — the user clicks the primary Run button. The button is a split button with a caret dropdown that picks one of two modes:
- Run review — a single review pass (the original one-shot behavior).
- Run review + fix loop — review → fix → re-review, capped at
max_iterations(set via the small numeric input next to the button, 1–10, default1). Each iteration runs the same review prompt as the one-shot mode; comments stream live into the panel as they arrive. The loop stops early when an iteration returns zero comments or the same comment-id set as the previous iteration (SameAsPrevgate — guards against the agent ignoring “do not re-emit” instructions). The mode and max-iter value persist inlocalStorageso they survive reloads.
Both modes are backed by seeded workflows (
review-loop,review-fix-loop) shipped viafsmigrate; the fix-loop body runsreview → fix → verifyper iteration, whereverifystages and commits any leftover changes viagit add -u(tracked-only). Because these are real workflow runs, each node’s input/output (and thefixprompt node’s Claude session id) is inspectable in the Workflows panel — see Per-Node Run View . A chip in the panel header mirrors the workflow events (workflow.node_started,workflow.run_completed, …) so the operator can seereview iter 2/3 — running,fixing — iter 2/3,paused at gate — …,done — 0 comments remaining, orstopped — no progress (same findings)without leaving the Review panel. The agent emits<review-comment path="..." line="N" side="RIGHT|LEFT">...body...</review-comment>blocks, which are parsed and streamed to the FE as they arrive.Push — each comment ships with Push (single) and a Push all (N) affordance in the header (when at least one comment is unpushed). The backend uses
gh api ... /pulls/N/commentsagainst the captured head SHA so comments anchor to the right commit even if the PR is force-pushed later.Close — closing the session deletes the in-memory session record and removes the worktree on disk. Pushed comments remain on GitHub.
Concurrency
A second POST /review/run while the first is still in flight returns
202 {"status":"in_progress"} without restarting the agent. Comments
keep streaming through the existing run.
While a fix loop is active, the primary Run button is disabled on the FE: the workflow controls the channel’s worktree (review session + auto-commits), and starting a second concurrent run mid-loop would race those operations.
Gate approvals during a fix loop
When the fix step trips a security-gate rule with decision: approve
(gates.agentgate / gates.docker_proxy, see gates.md
),
the loop pauses and the workflow run enters paused status. The
ApprovalCard renders inline inside the Review panel when no Chat
panel is mounted in the current layout, so operators working in a
Review-only layout can resolve the gate without switching layouts.
When a Chat panel is mounted, the chip flips to
paused at gate — see chat and the card renders in chat as it
already does for other agent runs.
CLI
The host-side loop review run subcommand drives the same async
endpoint from a shell or a workflow bash node. The agent container
exports both CHANNEL_ID and API_URL, and the CLI falls back to them,
so the seeded review workflows’ bash body is simply:
loop review run --pr {{.Inputs.pr}} --wait--pr is optional (blank via the seeded pr input) — leave it blank to
review the channel’s already-loaded review, or pass a PR number/URL to load
and review that PR. The load is idempotent: loop review run first GETs
the channel’s review session and skips the (destructive, worktree-rebuilding)
load when it’s already on that PR. So the Review panel can pre-load a PR and
pass its number as the pr input for traceability without triggering a second
load. Any session-lookup failure falls back to loading.
| Flag | Default | Description |
|---|---|---|
--channel-id | $CHANNEL_ID | Channel whose review session to drive. Falls back to the container-injected $CHANNEL_ID; required only if neither is set. |
--api-url | $API_URL then http://localhost:8222 | Daemon URL. The agent container already exports $API_URL. |
--pr | (none) | PR number (567) or URL (.../pull/567) to load into the channel’s review session (fetch PR + create its worktree) before running. Omitted → review whatever the channel already has loaded. |
--wait | false | Block until the session reaches a terminal status (ready or error) and emit the JSON envelope to stdout. Without --wait, the command exits 0 immediately after the 202. |
--timeout | 60m | Bound on the total --wait time. Enforced inside the HTTP client, not just between polls, so a hung response can’t outlive the deadline. Transient transport errors (TCP reset, momentary daemon restart, proxy 502) back off and retry instead of failing the whole loop. Sits above the daemon-side review ceiling (50m) so the daemon flips first with a meaningful error rather than the CLI’s generic timeout. |
The emitted JSON shape is {"status":"ready","no_comments":bool,"comments":[...]} — the same payload used by the workflow body parser to populate {{.Review.*}} templates inside the seeded loops.
Status transitions
Status is broadcast over the WebSocket as review.status events so
multiple panes / browser tabs stay in sync without polling.
idle ──load──▶ loading ──ok──▶ ready ──run──▶ reviewing ──ok──▶ ready
│ │
└──err──▶ error └──err──▶ errorConfiguration
{
"review": {
"prompt": "Review the diff between <diff> tags and emit one <review-comment> block per actionable issue. Skip style nits."
}
}Or, to keep the prompt out of the JSON file:
{ "review": { "prompt_path": "review.md" } }The latter is read from ~/.loop/review/review.md. Setting both is an
error; setting neither uses the daemon’s built-in default prompt.
Required output format
The parser anchors comments by the tag attributes, so an override prompt must instruct the agent to emit blocks shaped exactly like:
<review-comment path="path/to/file" line="N" side="RIGHT">
One paragraph describing the issue.
</review-comment>pathis repo-relative.lineis the line on the indicated side of the diff.sideis"RIGHT"for added/modified lines (the common case) or"LEFT"for lines removed from the base. This matches GitHub’spulls/{N}/commentsAPI, so the value is forwarded as-is on push.
Blocks that fail to parse are silently dropped, and the parser deduplicates by content hash so re-emitting the same block during streaming is safe.
See also
- api.md — full HTTP and event surface.
- layouts.md — how to add a Review pane.