Rig review
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'.From its SKILL.md
npx -y skills add agent-rig/rig --skill rig-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
14.4 KB, ~3.9k tokens by cl100k_base, as published. Nobody here has run it
rig-review — find and fix, in one skill
Local code review with two verbs:
find(default) — walk the project'sREVIEWER.mdcatalog and the applicable per-scope invariants against a diff with a fresh-contextrevieweragent (two passes), and return a triaged P0–P3 finding list. Read-only — it reports, it doesn't edit.fix— take review feedback and loop acoderagent 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 localfindresults.
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, ornone(defaultnone).noneforces the local-only loop forfix.review.botRetrigger— comment that re-triggers the bot (e.g.@codex review). Required whenbotis notnone.review.maxRounds— max fix↔recheck rounds before handing to a human (default5);--rounds Noverrides.vcs.baseRef— diff base when no<base>is given (defaultorigin/main).project.repo—owner/namefor everygh apicall (bot source). Never hardcode; derive from the git remote if unset.agents.reviewer/agents.coder— the project's names for those roles (defaultrig-<role>).
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>orvcs.baseRef. For a stacked/epic child, pass the integration branch.fix [<PR>] [--source bot|local] [--rounds N] [--base <ref>]<PR>implies--source botunlessreview.botisnoneor--source localis set.--sourcedefault:botif a PR is given andreview.bot != none, elselocal.--roundsdefaultreview.maxRounds;--base(local only) defaultvcs.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}...HEADIf 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 worksCapture as
{scope-invariants}. Informational, never fails; if it prints "No scope REVIEWER.md apply", drop the scope-invariant paragraph below. -
Spawn the
revieweragent (viaagents.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
findafterfix, 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
reviewercall — 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 taggedscope-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].
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
findoutput if passed; else run thefindverb (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
findverb (fresh context). If still P0/P1, incrementround.
- Spawn coder (via
- 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.
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→ regexchatgpt-codex-connector.bot: claude→ regexclaude.bot: bugbot→ regexcursor(Cursor Bugbot posts ascursor[bot]; re-trigger comment isbugbot 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 offCHECK, 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 codexCOMMENTEDcase): keep polling. If inlines never populate within budget, treatneutralas clean (body-only, no actionable threads) andfailure/action_requiredas actionable to stay safe.
- anything not
completed:(none:/queued:/in_progress:) → keep polling.
REACTION > 0orREVIEW == "APPROVED"→ clean, exit.REVIEW == "COMMENTED"ANDINLINE > 0→ actionable, go to 3.REVIEW == "COMMENTED"ANDINLINE == 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 —
COMMENTEDwith 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:
Bucket each by severity yourself (blocking = correctness/security/data-loss; advisory = style/nit). (If the project ships a P1-gate classifier script, run it withgh api "repos/$REPO/pulls/$PR/comments" \ -q ".[] | select(.user.login | test(\"$BOT\"; \"i\")) | {id, path, line, body}"REVIEW_BOT_LOGINset 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 withid) 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; updatePUSH_TIMEto the new commit; loop to 1.
- Read and classify the bot's findings. Pull inline threads (same regex),
keep each comment's
-
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 withREVIEW_BOT_LOGINset to the bot's login regex (the same one used above — e.g.REVIEW_BOT_LOGIN=cursorfor 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 →actionablewith 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 blockersactionable — 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
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.