| name | gh-review-loop |
| description | Use after a GitHub PR is opened or to handle AI reviewer feedback (Codex, CodeRabbit, Copilot, or any reviewer bot) - run the review loop, fix reviewer comments, sweep sibling instances, verify, push, request re-review. |
AI Reviewer PR Review Loop
Run the full GitHub PR loop: wait for the configured AI reviewer, fetch unresolved actionable review threads, acknowledge, fix, verify against the repo's own checks, push, request re-review — capped. Prefer thread-aware review data over flat PR comments (threads preserve isResolved, isOutdated, paths, line anchors, diff hunks).
Reference files (load on demand)
This file holds the every-cycle happy path. Read the referenced file at the point of need, not up front:
| File | Load when |
|---|
references/reviewer-selection.md | No persisted reviewer; discovery returns none_configured/partial/confirmation_required; reviewer switch/reset |
references/verification-profile.md | First run for a repo (edit gate blocks until a profile decision); customize/un-skip/re-detect |
references/sweep-internals.md | Receipt shows a multi-site pattern (count >= 2), a multi-line ranged finding, or a Clustering: advisory |
references/write-safety.md | Resolving beyond the auto-resolve set; write constraints; is a push safe; what the bundled hooks enforce |
references/judge-eval.md | judge_mode ≠ off, the user mentions judge/eval, or the one-time tip is due |
references/receipts-and-metrics.md | --post-receipt/--sticky-receipt, --semantic-risk, --stats, the --record-run flag catalog |
references/terminal-report.md | Terminal [loop] Summary has remaining_actionable > 0 |
references/resume-and-recovery.md | Re-invocation at the cap; new pushes after the loop stopped; resumed/compacted session |
references/variations.md | Phrasing isn't the plain default loop (severity filters, audit-only, cap changes, judge modes, stats) |
references/script-usage.md | Any invocation not shown inline (wait catalog + outcome handling, discovery, read-only, dry-run, JSON, history) |
Reviewer Selection
One configured reviewer bot per PR; returning runs reuse the persisted selection silently. Anything else — no persisted reviewer, none_configured, partial: true, confirmation_required: true, or a switch/reset — load references/reviewer-selection.md and run its prompt flow before any edit or re-review request. Never wait on a reviewer the user did not choose; never hand-write a trigger mention for an unknown bot.
Thread States
RESOLVED / OUTDATED — skip (outdated is auto-resolved by the script).
ADDRESSED_BY_REPLY — a maintainer's substantive reply deferred it: do not fix again; auto-resolved next pass (details: references/write-safety.md).
UNRESOLVED — actionable; drives the next fix attempt. Ordered critical → high → medium → low → unknown, parsed from the reviewer's severity badges.
Cycle Counting
A cycle is one agent-posted re-review request after the reviewer's first review (cycle 0, free). Cap = max_rereview_requests (prefs file, default 3); after cycle N, hard stop. Replies and pushes without a re-review request do not count — only the agent's own requests do (login auto-detected; --agent-login/--no-agent-filter to override). The cap blocks new fix cycles and re-review requests, not stale-thread cleanup, metrics, terminal classification, or recording terminal state.
Pattern → Sweep → Converge
Bot reviewers are LLMs: fixing only the flagged sites of a pattern teaches the bot to flag more next cycle. Collapse that into one cycle. The receipt's Patterns (N): section clusters findings — reason about patterns, not a flat list. For each multi-site pattern (count >= 2) and each multi-line ranged finding: load references/sweep-internals.md, run sweep_siblings.py over the PR's changed files only, print the sweep report first (report-then-go — never edit unflagged code silently), then fix the cluster plus reported siblings this cycle. Honor the script's status (too_few_sites/pattern_too_thin/no_source → do not sweep; truncated: true → show the report, ask first). Mark swept patterns with --swept-pattern <sig> alongside --fixed-finding.
Verification Profile
Each repo can have a code-derived verification profile — the checks the verify step runs. First run for a repo (a PreToolUse hook blocks edits until a profile decision is saved): load references/verification-profile.md and run its detect → menu → save flow before any edit. Subsequent runs: no prompt — the fetch output ends with the profile intro and planned-verification blocks; relay the intro from there, then for confirmed/customized run run_profile.py <owner/repo> <repo_root> — never call the test runner directly — feeding its verification field into --verification and its JSON into --verification-details. Verify fails iff any required check fails or times out. On skipped/unknown, relay the fallback intro and use ad-hoc narrowest-meaningful checks.
Receipts: per-cycle and terminal
--cycle-summary — read-only mid-loop receipt from accumulated run state. Safe every cycle.
--record-run — terminal: appends one record to local runs.jsonl and clears the run accumulator. Call exactly once, at loop end (flag catalog: references/receipts-and-metrics.md).
Emit a receipt at the end of every cycle. Non-terminal → --cycle-summary right after verify, REQUIRED even when fixes were small. Terminal → --record-run only (never both on one cycle; never twice).
Single-channel delivery. Both deliver the FULL receipt to the sticky PR comment (one per PR, edited in place) and print a one-line [loop] pointer to stdout. Relay the pointer verbatim; do NOT reprint the full receipt in chat. On comment-write failure (or --dry-run) the script prints the full receipt to stdout — relay that fallback verbatim instead.
Script-owned human blocks — relay verbatim, never paraphrase unless the script fails: profile intro, planned verification, judge table/skip line, receipt pointer (or fallback receipt), semantic-risk note, Next options:, wait heartbeat. Before the reviewer confirms a re-review use the script's fixed-pending wording, never Remaining valid actionable. --json/--format json stdout is machine JSON — parse, don't relay.
Progress Narration
Before `git push` on a non-terminal cycle: run `--cycle-summary` and relay its printed `[loop]` pointer line. For a terminal cycle **that follows a final push**: request re-review, capture `REREVIEW_AT`, wait with `--wait --after "$REREVIEW_AT"`, and set the terminal reviewer-confirmation flag from that wait result before `--record-run`. Terminal paths that publish nothing (already clean, cap reached, human decision, regression, no progress) record directly — no push, re-review, or wait. Enforced mechanically on hook runtimes: `loop_summary_gate.py` blocks `git push` while the summary is stale (exit 2 names the exact fix); `--record-run` is exempt. Violating the letter violates the spirit.
Emit one-line status updates at each phase transition. N = session cycle: copy it from the script ([loop] session cycle N, loopStatus.sessionCycle, or the receipt header) — never count it yourself. It exists only after a fetch, so the pre-fetch line omits it. M/K = cap consumed.
| Phase | Narration line |
|---|
| Before fetch | [loop] fetching threads from PR #<num>... (no ordinal yet — the fetch is what reports it) |
| After fetch | [loop] session cycle N — re-review cap: M/K consumed. <J> actionable thread(s) (severity: <breakdown>). Fixing. + judge tip/block if due |
| After fixes | [loop] session cycle N — fixes applied. Verifying via profile runner. |
| After verify | [loop] session cycle N — verified (<test summary>). |
| Before push | --cycle-summary + relay pointer (HARD GATE), then [loop] session cycle N — committing and pushing <sha>... |
| After push | [loop] session cycle N — pushed. Requesting reviewer re-review. Cap now M/K. |
| Reviewer wait | Background task (primary) or chunked heartbeats (fallback) — Workflow step 8 |
| Stop | [loop] STOP — <stop-condition>: <one-line explanation>. |
| Done | [loop] DONE — 0 actionable threads remaining. Cycles used: M/K. (cap consumption, not N — a clean PR ends at 0/K) + relay the --record-run pointer; remaining_actionable > 0 → references/terminal-report.md |
Skip narration only in pure non-interactive batch mode. User stepping away → pair with --sticky-receipt.
Optional Judge Eval
An opt-in, read-only OpenAI judge can classify findings. Off by default; nothing is sent to OpenAI unless the user opts in. When the saved mode ≠ off, the user mentions judge eval, or the one-time tip is due, load references/judge-eval.md.
Stopping Conditions
Stop and report instead of pushing or re-asking when any is true: 1. Cap reached. 2. All clean — no UNRESOLVED actionable threads after cleanup. 3. Human decision required — remaining threads are informational, duplicate, contradictory, or need a human call (includes ADDRESSED_BY_REPLY). 4. Test regression — failure not clearly caused by the finding-addressing change. 5. No progress — the script prints [loop] no_progress: … (unchanged actionable fingerprint): stop immediately with --record-run --outcome no_progress; do not push or re-request.
At the cap still run cleanup, terminal classification, metrics recording, and the final summary. Re-invocation at the cap is usually a resume signal — load references/resume-and-recovery.md before declaring a hard stop.
Workflow
- Trigger by default after PR creation. "Create the PR", "ship this", "run the review loop" all authorize the full loop.
- Resolve the PR — given URL/number, else
gh pr view --json number,url,headRefName,baseRefName. No PR → report the blocker.
- Select the reviewer (persisted → continue silently; else see Reviewer Selection).
- Wait for the first review.
reviewerSelection.auto_reviews: false (Codex) and no review activity → post the trigger via request_rereview.py first, then wait with --after; waiting without pinging burns the whole timeout. Activity already present → skip the wait and fetch. Timeout → say so; never invent feedback.
- Check loop status — count prior agent re-review requests; at or above the cap → Stopping Conditions.
- Fetch, acknowledge, classify. Default fetch (below). Summarize actionable findings grouped by file/behavior; none → report clean and stop. First run for the repo → profile decision NOW, before any edit (see Verification Profile), then relay the profile intro. Explanation requests get a reply draft, not a forced edit; conflicts or regression risk → stop and surface the tradeoff.
- Fix + verify. Scoped to feedback; read before editing; each change traceable to a feedback cluster; sweep per Pattern → Sweep → Converge. Verify via the profile runner; checks can't run → report why.
- Commit, push, re-review, wait, record. Commit;
--cycle-summary + relay pointer; push. Within the cap, post the re-review via request_rereview.py --repo OWNER/REPO --pr N --json (never hand-write the trigger — it also stamps the cycle boundary, so a hand-written ping leaves the ordinal stuck; status: no_safe_trigger → stop and relay exactly), capturing created_at as REREVIEW_AT. Then wait — primary (runtimes with background-task completion notifications, e.g. Claude Code): a background Bash task, one turn per wait:
python3 "$GGRL_PLUGIN_ROOT/skills/gh-review-loop/scripts/fetch_gemini_threads.py" \
--wait --after "$REREVIEW_AT" --timeout 1800
Do not poll it; never invent its result; relay its final output once. Fallback (no completion notifications, e.g. Codex): chunked waits — commands, statuses, refused/timed-out handling in . After the final wait, exactly once and relay its pointer line.
Script Usage
Resolve the runtime-neutral plugin root once, as its own Bash call:
GGRL_PLUGIN_ROOT="${GGRL_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
if [ -z "$GGRL_PLUGIN_ROOT" ]; then
GGRL_PLUGIN_ROOT=$(
find ~/.codex/plugins ~/.codex/plugins/cache ~/.claude/plugins/cache \
-type d -path "*/skills/gh-review-loop" 2>/dev/null \
| sort -rV | head -1 | sed 's|/skills/gh-review-loop$||'
)
fi
if [ -z "$GGRL_PLUGIN_ROOT" ] && [ -d "$(git rev-parse --show-toplevel 2>/dev/null)/plugins/gh-review-loop" ]; then
GGRL_PLUGIN_ROOT="$(git rev-parse --show-toplevel)/plugins/gh-review-loop"
fi
export GGRL_PLUGIN_ROOT
Default fetch (resolves stale threads, prints current feedback, ends with the profile intro + planned-verification blocks):
python3 "$GGRL_PLUGIN_ROOT/skills/gh-review-loop/scripts/fetch_gemini_threads.py" [--pr <URL>]
Delta mode. Threads unchanged since the previous cycle collapse to one line (anchor, severity, URL) — not missing data; any change renders full automatically. On a resumed session or after context compaction, run one fetch with --full to re-establish the baseline. Full option catalog: references/script-usage.md. The script warns on stderr when a GraphQL page limit is hit.
GitHub Write Safety
Invariants: never resolve an UNRESOLVED thread without an explicit user request; never submit approve/request-changes reviews unless explicitly asked. Stale threads (outdated, addressed-by-reply) auto-resolve by default; uncertain run → --dry-run first. Full policy, publish-stop conditions, and the bundled-hooks table: references/write-safety.md.