rig-review — find and fix, in one skill
Local code review with two verbs:
find (default) — walk the project's REVIEWER.md catalog and the
applicable per-scope invariants against a diff with a fresh-context reviewer
agent (two passes), and return a triaged P0–P3 finding list. Read-only —
it reports, it doesn't edit.
fix — take review feedback and loop a coder agent over it until the
review comes back clean (or a round budget is hit). Two feedback sources: a PR
review bot on an open PR, or local find results.
The point is to pre-empt the PR review bot: every finding caught and fixed
locally is one you don't pay PR round-trip latency on. Implement/epic-style
flows call rig-review find then rig-review fix --source local so the loop
lives in one place.
Configuration
Reads .rig/config.json:
review.patternsFile — the P0–P3 catalog the reviewer walks (default
.claude/REVIEWER.md). If absent, fall back to the kit's generic categories
(correctness, security/trust-boundary, error-handling, concurrency,
API/contract, tests, style) and say so.
review.bot — PR review bot to poll/re-trigger: codex, claude, or none
(default none). none forces the local-only loop for fix.
review.botRetrigger — comment that re-triggers the bot (e.g. @codex review). Required when bot is not none.
review.maxRounds — max fix↔recheck rounds before handing to a human
(default 5); --rounds N overrides.
vcs.baseRef — diff base when no <base> is given (default origin/main).
project.repo — owner/name for every gh api call (bot source). Never
hardcode; derive from the git remote if unset.
agents.reviewer / agents.coder — the project's names for those roles
(default rig-<role>).
style.guideFile — the writing style for findings (default
.claude/STYLE.md).
Scope invariants (per-subsystem REVIEWER.md). Beyond the root catalog, a
subsystem may carry its own REVIEWER.md colocated with its code — concrete
correctness invariants it "learned the hard way," each ideally citing the PR
where it was found. scripts/scope-reviewer.ts resolves which apply to a diff
(ancestor-walk + any governs: globs a scope declares). find collects them
and has the reviewer assert each as a P1. No-op if the project ships none.
Verbs & arguments
$ARGUMENTS begins with an optional verb, then args:
find [<base>] (default when no verb) — diff against <base> or
vcs.baseRef. For a stacked/epic child, pass the integration branch.
fix [<PR>] [--source bot|local] [--rounds N] [--base <ref>]
<PR> implies --source bot unless review.bot is none or --source local is set.
--source default: bot if a PR is given and review.bot != none, else
local.
--rounds default review.maxRounds; --base (local only) default
vcs.baseRef.
If invoked from another skill, the caller may thread {base}, {ticket-id},
{worktree-path} — use them.
find — the read-only gate
Resolve the base and confirm there's a diff:
git fetch origin
git diff --stat {base}...HEAD
If empty, report clean — no changes vs {base} and stop.
Then collect the scope invariants for the changed files:
bun scripts/scope-reviewer.ts {base} # or your TS runner; --files <paths> also works
Capture as {scope-invariants}. Informational, never fails; if it prints "No
scope REVIEWER.md apply", drop the scope-invariant paragraph below.
Spawn the reviewer agent (via agents.reviewer) against the diff,
working in {worktree-path} if a caller passed one:
"Review the diff git diff {base}...HEAD{ticket-suffix}. Read
{patternsFile} first and walk every pattern against the diff. (If it
doesn't exist, walk the generic categories: correctness,
security/trust-boundary, error-handling, concurrency, API/contract, tests,
style — and say so.) Worktree path: {worktree-path}. Be specific — file and
line refs. Tag each finding with its pattern number and severity
(P0/P1/P2/P3). If clean against every pattern, say so explicitly.
This diff touches scopes with recorded invariants — assert the diff against
every one; a violation is a P1 tagged scope-invariant.
Invariants:\n{scope-invariants}"
({ticket-suffix} = for {ticket-id} when passed, else empty. Omit the
scope-invariant paragraph when step 1 found none.)
If pass 1 has any P0/P1, stop here and report (step 5) — no value hunting
for more before known blockers are fixed. The caller re-invokes find after
fix, and the adversarial pass runs on that next iteration.
Adversarial second pass (only when pass 1 is clean of P0/P1). Spawn a
second, independent reviewer call — fresh context, framed to default to
skepticism:
"A prior reviewer found this diff clean of blocking issues: git diff {base}...HEAD{ticket-suffix}. Assume that verdict is wrong and find what it
missed. Read {patternsFile} and walk every pattern independently — don't
anchor on 'a reviewer already passed this.' Worktree path: {worktree-path}.
File/line refs, tagged with pattern number and severity. Also assert the
diff against the recorded scope invariants below independently; a violation
is a P1 tagged scope-invariant. Invariants:\n{scope-invariants} If you
also find it clean, say so; don't invent findings to justify a second
opinion."
(Omit the scope-invariant lines when step 1 found none.) Merge any P0/P1 the
second pass surfaces into the list.
Classify and report (this verb finds, it does not edit):
clean — 0 findings vs {base} (2 passes)
findings — N P0/P1, M P2/P3 — each with file:line, pattern number,
severity, and [pass 1]/[pass 2].
Each finding is one entry in one order — severity, location, the defect, the
consequence, the fix — per style.guideFile (default .claude/STYLE.md):
P1 — api/invoices.ts:88: the lookup uses the raw request ID, so any tenant
can read another tenant's invoice. Scope it to session.tenantId and add a
negative test.
Address the code, not the author. Don't hedge a P0 into a suggestion, and
don't pad a clean report to look thorough — if the diff is clean, say so in
one line and stop.
P0/P1 block (don't push). P2/P3 ship-with-a-note. To fix them, hand off to
fix --source local.
fix — drive feedback to convergence
Source: local (findings from find)
- Get findings. Use the caller's
find output if passed; else run the
find verb (rig-review {base}) first.
- Classify: no P0/P1 → return
clean (surface P2/P3); any P0/P1 → fix round.
- Fix round (while
round <= N):
- Spawn coder (via
agents.coder) in {worktree-path}:
"Address these review findings on {ticket-id}. Worktree: {worktree-path}.
Findings:\n{find_output}\n\nFor each, either apply the fix or explain in
one line why the finding is wrong. Don't silently skip."
- Re-run the
find verb (fresh context). If still P0/P1, increment round.
- Return the outcome.
Outcomes (local):
clean — 0 P0/P1 vs {base} (P2/P3, if any, listed)
unresolved — N P0/P1 after {N} rounds, leaving for your review
Source: bot (review bot on a PR)
Drives whatever bot review.bot names — mechanics are identical; only the
author login and re-trigger phrase differ. Set REPO="$(project.repo)" and
RETRIGGER="$(review.botRetrigger)" up front.
Headless in CI. This same fix loop can run in GitHub Actions on each
posted review (out of any local session) via ci/workflows/auto-review-fix.yml
— one round per invocation, re-fired by the bot's re-review. Setup:
ci/README.md#review-bot-bundle + docs/auto-fix-app.md.
Identifying the bot. Match the author with a case-insensitive regex on the
login, not an exact string — most bots post under more than one identity (a
...-connector for reviews, a ...[bot] for inline threads); exact match
misses half the surface and stalls polling.
bot: codex → regex chatgpt-codex-connector.
bot: claude → regex claude.
bot: bugbot → regex cursor (Cursor Bugbot posts as cursor[bot]; re-trigger
comment is bugbot run).
Clean signal differs by bot. codex/claude announce a clean run with an
APPROVED review or a +1 reaction. Bugbot does not — on a clean PR it posts
no review and no reaction; it completes a GitHub check-run (Cursor Bugbot,
app cursor) with conclusion: success. So for bot: bugbot the poll must read
that check-run — keying on review/reaction alone makes every clean PR time out and
the merge gates never go green.
Loop, round starting at 1:
Wait for the bot. Poll ~5 min (15 × 20s). Watch review state AND inline
comments AND reactions in one snapshot:
PR=<N>
PUSH_TIME=$(gh pr view $PR --repo "$REPO" --json commits -q '.commits[-1].committedDate')
BOT='<login-regex for review.bot>'
REVIEW=$(gh pr view $PR --repo "$REPO" --json reviews -q "
[.reviews[] | select((.author.login | test(\"$BOT\"; \"i\")) and .submittedAt > \"$PUSH_TIME\")] | last | .state // \"none\"")
REACTION=$(gh api "repos/$REPO/issues/$PR/reactions" -q "
[.[] | select((.user.login | test(\"$BOT\"; \"i\")) and .content == \"+1\" and .created_at > \"$PUSH_TIME\")] | length")
INLINE=$(gh api "repos/$REPO/pulls/$PR/comments" -q "
[.[] | select((.user.login | test(\"$BOT\"; \"i\")) and .created_at > \"$PUSH_TIME\")] | length")
# bugbot only: its verdict is a check-run, not a review/reaction. Filter
# server-side by name (so it can't fall off page 1 of an unfiltered list),
# take the NEWEST run by started_at (a push auto-starts one and `bugbot run`
# starts another on the same SHA — an older success must not win over a newer
# in-progress run), and guard the no-run-yet case (`// "none"`) so the jq
# concat doesn't error on early polls before Bugbot has created its run.
SHA=$(gh pr view $PR --repo "$REPO" --json headRefOid -q .headRefOid)
CHECK=$(gh api "repos/$REPO/commits/$SHA/check-runs?check_name=Cursor%20Bugbot" -q "
[.check_runs[] | select(.name | test(\"bugbot\"; \"i\"))] | sort_by(.started_at) | last
| ((.status // \"none\") + \":\" + (.conclusion // \"\"))")
[ -z "$CHECK" ] && CHECK="none:" # gh/network error → treat as not-yet-run
Classify (codex/claude use the review/reaction signals; bugbot uses CHECK):
bot: bugbot (classify off CHECK, the newest run for the head SHA):
completed:success → clean, exit.
completed: with any other conclusion (neutral/action_required/failure):
INLINE > 0 → actionable, go to 3.
INLINE == 0 → race window (inline threads lag the check conclusion,
like the codex COMMENTED case): keep polling. If inlines never populate
within budget, treat neutral as clean (body-only, no actionable
threads) and failure/action_required as actionable to stay safe.
- anything not
completed: (none:/queued:/in_progress:) → keep polling.
REACTION > 0 or REVIEW == "APPROVED" → clean, exit.
REVIEW == "COMMENTED" AND INLINE > 0 → actionable, go to 3.
REVIEW == "COMMENTED" AND INLINE == 0 → race window: review state can
flip before inline threads are API-visible. Keep polling. If inlines never
populate within budget AND the review body is boilerplate-only, treat as
clean (a header-only review is a soft +1).
- Timeout (5 min, nothing) → timeout, exit.
Never classify on review-state alone — COMMENTED with empty inlines is a
transient race, not "no findings."
Fix round (only if round <= N):
- Read and classify the bot's findings. Pull inline threads (same regex),
keep each comment's
id:gh api "repos/$REPO/pulls/$PR/comments" \
-q ".[] | select(.user.login | test(\"$BOT\"; \"i\")) | {id, path, line, body}"
Bucket each by severity yourself (blocking = correctness/security/data-loss;
advisory = style/nit). (If the project ships a P1-gate classifier script,
run it with REVIEW_BOT_LOGIN set to the bot's login regex — see step 4 —
and use its output as authoritative.)
- Spawn coder (via
agents.coder) with the review body + inline comments
(each with id) and the instruction to apply fixes in {worktree-path}.
Tell coder: "Address each actionable suggestion. If one is wrong, push
back in that comment's own inline thread — reply to it, don't open a
top-level comment — with the technical reason; don't silently skip":gh api --method POST "repos/$REPO/pulls/$PR/comments/<comment_id>/replies" -f body="<reason>"
- Stage, commit, push (
fix({ticket-id}): address review).
- Resolve the threads you addressed. If merge gates on unresolved blocking
threads, resolve each the coder actually fixed; for a blocker coder pushed
back on, reply with rationale, resolve as explicit dismissal, and surface to
the user — never leave an applicable blocker unresolved. Use the project's
thread-resolution tooling if it ships one. A resolve without a push
doesn't re-run a gate check — re-run the required check or let the next push
re-trigger it.
- Re-trigger:
gh pr comment $PR --repo "$REPO" --body "$RETRIGGER".
- Increment
round; update PUSH_TIME to the new commit; loop to 1.
After N rounds (or clean/timeout): the merge-queue gate is the
authority — converge to it. If the project ships a P1-gate script, run it
with REVIEW_BOT_LOGIN set to the bot's login regex (the same one used
above — e.g. REVIEW_BOT_LOGIN=cursor for bugbot) so it matches the right
author; left unset it defaults to a placeholder that matches nothing, so the
gate falsely passes and can wave through an unresolved blocker. (exit 0 →
clean; non-zero → actionable with blocking titles + URLs.) Else use the
final snapshot + tracked unresolved blockers.
Outcomes (bot) — stacked/epic flows read this to decide auto-merge:
clean — PR <URL> reviewed by <bot>, no unresolved blockers
actionable — PR <URL> still has N unresolved blocker(s) after {N} rounds. Findings: <titles + URLs>
timeout — PR <URL> open, <bot> did not respond within 5 min, leaving for your review
1---2name: rig-review3description: Local code review, both halves. `find` (default): walk the REVIEWER.md catalog + scope invariants against a diff, two adversarial passes, return triaged P0–P3 findings — read-only. `fix`: drive review feedback to convergence, from a PR review bot OR local `find` results (spawn coder → recheck until clean). Triggers on: 'review my changes', 'review the diff', 'pre-PR review', 'review gate', 'check the diff before pushing', 'review fix', 'address the review', 'apply review feedback', 'fix the bot', 'address codex review', 'fix the findings', 'review loop'.4---56# rig-review — find and fix, in one skill78Local code review with two verbs:910- **`find`** (default) — walk the project's `REVIEWER.md` catalog **and** the11 applicable per-scope invariants against a diff with a fresh-context `reviewer`12 agent (two passes), and return a triaged P0–P3 finding list. **Read-only** —13 it reports, it doesn't edit.14- **`fix`** — take review feedback and loop a `coder` agent over it until the15 review comes back clean (or a round budget is hit). Two feedback sources: a PR16 **review bot** on an open PR, or **local** `find` results.1718The point is to **pre-empt the PR review bot**: every finding caught and fixed19locally is one you don't pay PR round-trip latency on. Implement/epic-style20flows call `rig-review find` then `rig-review fix --source local` so the loop21lives in one place.2223## Configuration2425Reads `.rig/config.json`:2627- `review.patternsFile` — the P0–P3 catalog the reviewer walks (default28 `.claude/REVIEWER.md`). If absent, fall back to the kit's generic categories29 (correctness, security/trust-boundary, error-handling, concurrency,30 API/contract, tests, style) and say so.31- `review.bot` — PR review bot to poll/re-trigger: `codex`, `claude`, or `none`32 (default `none`). **`none` forces the local-only loop** for `fix`.33- `review.botRetrigger` — comment that re-triggers the bot (e.g. `@codex34 review`). Required when `bot` is not `none`.35- `review.maxRounds` — max fix↔recheck rounds before handing to a human36 (default `5`); `--rounds N` overrides.37- `vcs.baseRef` — diff base when no `<base>` is given (default `origin/main`).38- `project.repo` — `owner/name` for every `gh api` call (bot source). Never39 hardcode; derive from the git remote if unset.40- `agents.reviewer` / `agents.coder` — the project's names for those roles41 (default `rig-<role>`).42- `style.guideFile` — the writing style for findings (default43 `.claude/STYLE.md`).4445**Scope invariants (per-subsystem `REVIEWER.md`).** Beyond the root catalog, a46subsystem may carry its own `REVIEWER.md` colocated with its code — concrete47correctness invariants it "learned the hard way," each ideally citing the PR48where it was found. `scripts/scope-reviewer.ts` resolves which apply to a diff49(ancestor-walk + any `governs:` globs a scope declares). `find` collects them50and has the reviewer assert each as a **P1**. No-op if the project ships none.5152## Verbs & arguments5354`$ARGUMENTS` begins with an optional verb, then args:5556- **`find [<base>]`** (default when no verb) — diff against `<base>` or57 `vcs.baseRef`. For a stacked/epic child, pass the integration branch.58- **`fix [<PR>] [--source bot|local] [--rounds N] [--base <ref>]`**59 - `<PR>` implies `--source bot` unless `review.bot` is `none` or `--source60 local` is set.61 - `--source` default: `bot` if a PR is given and `review.bot != none`, else62 `local`.63 - `--rounds` default `review.maxRounds`; `--base` (local only) default64 `vcs.baseRef`.6566If invoked from another skill, the caller may thread `{base}`, `{ticket-id}`,67`{worktree-path}` — use them.6869---70# `find` — the read-only gate71721. **Resolve the base** and confirm there's a diff:73 ```bash74 git fetch origin75 git diff --stat {base}...HEAD76 ```77 If empty, report `clean — no changes vs {base}` and stop.7879 Then **collect the scope invariants** for the changed files:80 ```bash81 bun scripts/scope-reviewer.ts {base} # or your TS runner; --files <paths> also works82 ```83 Capture as `{scope-invariants}`. Informational, never fails; if it prints "No84 scope REVIEWER.md apply", **drop the scope-invariant paragraph** below.85862. **Spawn the `reviewer` agent** (via `agents.reviewer`) against the diff,87 working in `{worktree-path}` if a caller passed one:8889 > "Review the diff `git diff {base}...HEAD`{ticket-suffix}. Read90 > `{patternsFile}` first and walk **every** pattern against the diff. (If it91 > doesn't exist, walk the generic categories: correctness,92 > security/trust-boundary, error-handling, concurrency, API/contract, tests,93 > style — and say so.) Worktree path: {worktree-path}. Be specific — file and94 > line refs. Tag each finding with its pattern number and severity95 > (P0/P1/P2/P3). If clean against every pattern, say so explicitly.96 >97 > This diff touches scopes with recorded invariants — assert the diff against98 > **every** one; a violation is a **P1** tagged `scope-invariant`.99 > Invariants:\n{scope-invariants}"100101 (`{ticket-suffix}` = ` for {ticket-id}` when passed, else empty. **Omit the102 scope-invariant paragraph** when step 1 found none.)1031043. **If pass 1 has any P0/P1, stop here** and report (step 5) — no value hunting105 for more before known blockers are fixed. The caller re-invokes `find` after106 `fix`, and the adversarial pass runs on that next iteration.1071084. **Adversarial second pass (only when pass 1 is clean of P0/P1).** Spawn a109 **second, independent** `reviewer` call — fresh context, framed to default to110 skepticism:111112 > "A prior reviewer found this diff clean of blocking issues: `git diff113 > {base}...HEAD`{ticket-suffix}. Assume that verdict is wrong and find what it114 > missed. Read `{patternsFile}` and walk every pattern independently — don't115 > anchor on 'a reviewer already passed this.' Worktree path: {worktree-path}.116 > File/line refs, tagged with pattern number and severity. Also assert the117 > diff against the recorded scope invariants below independently; a violation118 > is a **P1** tagged `scope-invariant`. Invariants:\n{scope-invariants} If you119 > also find it clean, say so; don't invent findings to justify a second120 > opinion."121122 (Omit the scope-invariant lines when step 1 found none.) Merge any P0/P1 the123 second pass surfaces into the list.1241255. **Classify and report** (this verb finds, it does not edit):126 - `clean — 0 findings vs {base} (2 passes)`127 - `findings — N P0/P1, M P2/P3` — each with file:line, pattern number,128 severity, and `[pass 1]`/`[pass 2]`.129130 Each finding is one entry in one order — severity, location, the defect, the131 consequence, the fix — per `style.guideFile` (default `.claude/STYLE.md`):132133 > P1 — `api/invoices.ts:88`: the lookup uses the raw request ID, so any tenant134 > can read another tenant's invoice. Scope it to `session.tenantId` and add a135 > negative test.136137 Address the code, not the author. Don't hedge a P0 into a suggestion, and138 don't pad a clean report to look thorough — if the diff is clean, say so in139 one line and stop.140141 P0/P1 block (don't push). P2/P3 ship-with-a-note. To fix them, hand off to142 `fix --source local`.143144---145# `fix` — drive feedback to convergence146147## Source: `local` (findings from `find`)1481491. **Get findings.** Use the caller's `find` output if passed; else run the150 `find` verb (`rig-review {base}`) first.1512. **Classify:** no P0/P1 → return `clean` (surface P2/P3); any P0/P1 → fix round.1523. **Fix round** (while `round <= N`):153 - Spawn **coder** (via `agents.coder`) in `{worktree-path}`:154 > "Address these review findings on {ticket-id}. Worktree: {worktree-path}.155 > Findings:\n{find_output}\n\nFor each, either apply the fix or explain in156 > one line why the finding is wrong. Don't silently skip."157 - Re-run the `find` verb (fresh context). If still P0/P1, increment `round`.1584. Return the outcome.159160**Outcomes (local):**161- `clean — 0 P0/P1 vs {base}` (P2/P3, if any, listed)162- `unresolved — N P0/P1 after {N} rounds, leaving for your review`163164## Source: `bot` (review bot on a PR)165166Drives whatever bot `review.bot` names — mechanics are identical; only the167author login and re-trigger phrase differ. Set `REPO="$(project.repo)"` and168`RETRIGGER="$(review.botRetrigger)"` up front.169170> **Headless in CI.** This same `fix` loop can run in GitHub Actions on each171> posted review (out of any local session) via `ci/workflows/auto-review-fix.yml`172> — one round per invocation, re-fired by the bot's re-review. Setup:173> `ci/README.md#review-bot-bundle` + `docs/auto-fix-app.md`.174175**Identifying the bot.** Match the author with a case-insensitive regex on the176login, not an exact string — most bots post under more than one identity (a177`...-connector` for reviews, a `...[bot]` for inline threads); exact match178misses half the surface and stalls polling.179- `bot: codex` → regex `chatgpt-codex-connector`.180- `bot: claude` → regex `claude`.181- `bot: bugbot` → regex `cursor` (Cursor Bugbot posts as `cursor[bot]`; re-trigger182 comment is `bugbot run`).183184**Clean signal differs by bot.** codex/claude announce a clean run with an185`APPROVED` review or a `+1` reaction. **Bugbot does not** — on a clean PR it posts186no review and no reaction; it completes a GitHub **check-run** (`Cursor Bugbot`,187app `cursor`) with `conclusion: success`. So for `bot: bugbot` the poll must read188that check-run — keying on review/reaction alone makes every clean PR time out and189the merge gates never go green.190191Loop, `round` starting at 1:1921931. **Wait for the bot.** Poll ~5 min (15 × 20s). Watch review state AND inline194 comments AND reactions in one snapshot:195 ```bash196 PR=<N>197 PUSH_TIME=$(gh pr view $PR --repo "$REPO" --json commits -q '.commits[-1].committedDate')198 BOT='<login-regex for review.bot>'199 REVIEW=$(gh pr view $PR --repo "$REPO" --json reviews -q "200 [.reviews[] | select((.author.login | test(\"$BOT\"; \"i\")) and .submittedAt > \"$PUSH_TIME\")] | last | .state // \"none\"")201 REACTION=$(gh api "repos/$REPO/issues/$PR/reactions" -q "202 [.[] | select((.user.login | test(\"$BOT\"; \"i\")) and .content == \"+1\" and .created_at > \"$PUSH_TIME\")] | length")203 INLINE=$(gh api "repos/$REPO/pulls/$PR/comments" -q "204 [.[] | select((.user.login | test(\"$BOT\"; \"i\")) and .created_at > \"$PUSH_TIME\")] | length")205 # bugbot only: its verdict is a check-run, not a review/reaction. Filter206 # server-side by name (so it can't fall off page 1 of an unfiltered list),207 # take the NEWEST run by started_at (a push auto-starts one and `bugbot run`208 # starts another on the same SHA — an older success must not win over a newer209 # in-progress run), and guard the no-run-yet case (`// "none"`) so the jq210 # concat doesn't error on early polls before Bugbot has created its run.211 SHA=$(gh pr view $PR --repo "$REPO" --json headRefOid -q .headRefOid)212 CHECK=$(gh api "repos/$REPO/commits/$SHA/check-runs?check_name=Cursor%20Bugbot" -q "213 [.check_runs[] | select(.name | test(\"bugbot\"; \"i\"))] | sort_by(.started_at) | last214 | ((.status // \"none\") + \":\" + (.conclusion // \"\"))")215 [ -z "$CHECK" ] && CHECK="none:" # gh/network error → treat as not-yet-run216 ```2172. **Classify** (codex/claude use the review/reaction signals; bugbot uses `CHECK`):218 - **`bot: bugbot`** (classify off `CHECK`, the newest run for the head SHA):219 - `completed:success` → **clean**, exit.220 - `completed:` with any other conclusion (`neutral`/`action_required`/`failure`):221 - `INLINE > 0` → **actionable**, go to 3.222 - `INLINE == 0` → **race window** (inline threads lag the check conclusion,223 like the codex `COMMENTED` case): keep polling. If inlines never populate224 within budget, treat `neutral` as **clean** (body-only, no actionable225 threads) and `failure`/`action_required` as **actionable** to stay safe.226 - anything not `completed:` (`none:`/`queued:`/`in_progress:`) → keep polling.227 - `REACTION > 0` or `REVIEW == "APPROVED"` → **clean**, exit.228 - `REVIEW == "COMMENTED"` AND `INLINE > 0` → **actionable**, go to 3.229 - `REVIEW == "COMMENTED"` AND `INLINE == 0` → **race window**: review state can230 flip before inline threads are API-visible. Keep polling. If inlines never231 populate within budget AND the review body is boilerplate-only, treat as232 **clean** (a header-only review is a soft +1).233 - Timeout (5 min, nothing) → **timeout**, exit.234235 **Never classify on review-state alone** — `COMMENTED` with empty inlines is a236 transient race, not "no findings."2373. **Fix round** (only if `round <= N`):238 - **Read and classify the bot's findings.** Pull inline threads (same regex),239 keep each comment's `id`:240 ```bash241 gh api "repos/$REPO/pulls/$PR/comments" \242 -q ".[] | select(.user.login | test(\"$BOT\"; \"i\")) | {id, path, line, body}"243 ```244 Bucket each by severity yourself (blocking = correctness/security/data-loss;245 advisory = style/nit). *(If the project ships a P1-gate classifier script,246 run it with `REVIEW_BOT_LOGIN` set to the bot's login regex — see step 4 —247 and use its output as authoritative.)*248 - Spawn **coder** (via `agents.coder`) with the review body + inline comments249 (each with `id`) and the instruction to apply fixes in `{worktree-path}`.250 Tell coder: "Address each actionable suggestion. If one is wrong, **push251 back in that comment's own inline thread — reply to it, don't open a252 top-level comment** — with the technical reason; don't silently skip":253 ```bash254 gh api --method POST "repos/$REPO/pulls/$PR/comments/<comment_id>/replies" -f body="<reason>"255 ```256 - Stage, commit, push (`fix({ticket-id}): address review`).257 - **Resolve the threads you addressed.** If merge gates on unresolved blocking258 threads, resolve each the coder actually fixed; for a blocker coder pushed259 back on, reply with rationale, resolve as explicit dismissal, and surface to260 the user — never leave an applicable blocker unresolved. Use the project's261 thread-resolution tooling if it ships one. A resolve *without* a push262 doesn't re-run a gate check — re-run the required check or let the next push263 re-trigger it.264 - Re-trigger: `gh pr comment $PR --repo "$REPO" --body "$RETRIGGER"`.265 - Increment `round`; update `PUSH_TIME` to the new commit; loop to 1.2664. **After N rounds** (or `clean`/`timeout`): the merge-queue gate is the267 authority — converge to it. If the project ships a P1-gate script, run it268 **with `REVIEW_BOT_LOGIN` set to the bot's login regex** (the same one used269 above — e.g. `REVIEW_BOT_LOGIN=cursor` for bugbot) so it matches the right270 author; left unset it defaults to a placeholder that matches nothing, so the271 gate falsely passes and can wave through an unresolved blocker. (exit 0 →272 `clean`; non-zero → `actionable` with blocking titles + URLs.) Else use the273 final snapshot + tracked unresolved blockers.274275**Outcomes (bot)** — stacked/epic flows read this to decide auto-merge:276- `clean — PR <URL> reviewed by <bot>, no unresolved blockers`277- `actionable — PR <URL> still has N unresolved blocker(s) after {N} rounds. Findings: <titles + URLs>`278- `timeout — PR <URL> open, <bot> did not respond within 5 min, leaving for your review`