Drive one PR/MR to a clean review verdict by looping: read review → ARD every
finding → push → post summary → re-request review → repeat until clean.
-
Identify and claim the PR/MR. Use the current branch's open MR, or
the one the user specified. Post a brief claim comment (COMMENT_PR) so a
parallel @claude CI run or another person doesn't start a colliding
session:
gh pr comment <N> --body "Driving this PR to clean — back off until done."
Skip if your most recent comment already says so. (COMMENT_PR and the
other bracketed tokens below are abstract operation tokens — resolve to
your model's tool via tool-mappings.md.)
-
Read the latest review. Pull the most recent reviewer comment — the
@claude bot's, or a human's. Don't trust earlier cached verdicts — actively
poll until a review appears that references the commit you just pushed, then
read that one.
gh pr checks (PR_CHECKS) / glab ci list going green is about CI
state, not the review verdict — always parse the latest review body
for findings.
When the user provides a specific review link/ID (e.g. #pullrequestreview-4761444085):
Fetch that review directly via the GitHub API using its ID. Many bot reviews have a
generic overview body but the actual findings live in inline comments on specific lines —
don't rely on the top-level review body alone. Fetch both the review overview and its inline
comments:
gh api "repos/<owner>/<repo>/pulls/<N>/reviews/<review-id>" --jq '{state, body}'
gh api "repos/<owner>/<repo>/pulls/<N>/comments" --paginate --jq '.[] | select(.pull_request_review_id == <review-id>) | {line: (.line // .original_line), body}'
The comments endpoint returns pages oldest-first -- without --paginate
a later review's inline comments can sit past the first page and never
reach the filter, making a review with real findings look empty.
-
GitHub:
gh pr view <N> --json comments \
--jq '[.comments[] | select(.author.login | startswith("claude"))] | last | .body'
The reviewer's bot login varies by setup — gh pr view reports it as
claude, the REST API as claude[bot], and some setups post as
github-actions[bot]. startswith("claude") matches across gh pr view
and gh api; broaden it if your reviewer posts under another login, or
you'll silently read null and false-pass. This command captures the
bot review only — for a human reviewer (any login), gather comments
with the ard skill's step 1 (gh pr view <N> --comments plus the
inline-thread API), which collects every reviewer's comments regardless of
login.
Copilot code review doesn't post as a PR comment at all -- it's a
formal GitHub review, invisible to the command above. Request it
(REQUEST_COPILOT_REVIEW -- abstract operation token; resolve to your
model's tool via tool-mappings.md) and check
whether it posted a verdict at the current head. Finding a review
object at the right commit_id only proves Copilot looked -- it says
nothing about whether that review is clean. Fetch the matched review's
own overview and its inline comments (same two-call shape as the
review-link case above; the reviews list itself is READ_PR_REVIEWS)
before treating it as an all-clear:
gh api's own --jq flag has no --arg/--argjson (see
memories/github.md's gh api/jq note) --
pipe the raw paginated output into standalone jq -s instead, which
supports both:
set -o pipefail
head="$(gh pr view "<N>" --json headRefOid -q .headRefOid)"
review_id="$(gh api "repos/<owner>/<repo>/pulls/<N>/reviews" --paginate \
| jq -s --arg head "$head" \
'[.[][] | select(.user.login=="copilot-pull-request-reviewer[bot]" and .commit_id==$head)] | last | .id')"
if [ -n "$review_id" ] && [ "$review_id" != "null" ]; then
gh api "repos/<owner>/<repo>/pulls/<N>/reviews/$review_id" --jq '{state, body}'
gh api "repos/<owner>/<repo>/pulls/<N>/comments" --paginate \
| jq -s --arg rid "$review_id" \
'[.[][] | select(.pull_request_review_id == ($rid | tonumber))] | .[] | {line: (.line // .original_line), body}'
else
echo "no fresh review yet -- wait or re-request"
fi
A genuine clean Copilot overview is not an empty string -- it reads
something like "Copilot reviewed N files and generated no new comments."
Don't require a literally empty body; parse the overview for a
zero-new-findings phrasing and confirm zero matched inline comments
-- both, not either alone, since zero inline comments with no
affirmative zero-findings overview doesn't rule out a non-verdict
formal review.
A "no new comments" overview can still carry real findings in a
collapsed <details><summary>Comments suppressed due to low confidence (N)</summary> block -- these are genuine flagged items
under the fully-clean rule (address every finding regardless of
confidence label), even though they never become formal inline
comment objects the /comments endpoint returns (verified: PR #660's
review 4767752501 read "generated no new comments" in its overview
while its full body carried 3 suppressed findings). A third condition
is required: the raw review body must not contain a "Comments
suppressed" block at all -- checking only the overview's headline
phrasing and the /comments endpoint's inline-comment count both miss
this.
And dispositioning a finding-bearing review's comments
yourself does not make that same review the all-clear -- the
fully-clean bar needs a later review, at the still-current head, that
doesn't re-raise them. So the all-clear is either (a) a review with a
zero-new-findings overview, zero inline comments, and no suppressed-
findings block, or (b) a later review at the same head as a
finding-bearing one, confirming nothing
remains -- never the finding-bearing review itself, however thoroughly
you addressed its findings. A review object existing at the current
commit_id with unresolved findings inside it is not clean, it's just
current. A stub-like non-answer ("ineligible", "reached their quota
limit") is also not a verdict -- treat it the same as a skipped/stub
@claude run (see the "Do the review yourself" fallback in CLAUDE.md)
and retry later or fall back accordingly.
-
GitLab: poll the MR notes (sort=desc) for a review note that
references your latest short SHA before proceeding; if none has appeared,
wait and retry rather than reading a stale verdict.
If the latest review is a cancellation, the live verdict is stale —
don't re-do already-applied fixes. A cancel-in-progress cancellation
(on setups that cancel superseded review runs) means the last
complete review's findings may already have been fixed by a commit that
landed after it, with the confirming re-review killed before it could post.
Before treating those findings as outstanding work, diff the current code
against each one to see what's already addressed — then push only what's
genuinely needed and let a fresh review confirm. Re-applying fixes that are
already in the tree wastes a round and muddies the diff. If nothing
remains outstanding (every finding is already applied), don't push an empty
commit — skip to step 6 and re-request the review directly.
If the reviewer explicitly skips or cannot produce a verdict (for
example, quota exhaustion, an outage, or a policy that prevents a reviewer
from self-reviewing its own work), self-review immediately -- don't stall
the PR waiting on it. In the same round, also check whether a
different configured external reviewer is available (e.g. Copilot
code review, if the repo/org has it) and request it in parallel with
posting the self-review, not after -- the two reviewers can fail
independently, and self-review is a fallback for when no working
external reviewer is reachable, never a substitute once one is. When you
self-review: read the current PR diff against its base, check each
changed call path and edge case, run the focused tests and relevant
lint/documentation checks, and address every finding you identify. Note
the skip in your ARD summary comment. Re-check reviewer availability
every round, not just once -- a reviewer that was unavailable a few
pushes ago can become available mid-session. A skipped review is never a
clean external verdict on its own and does not authorize marking the PR
as approved -- see The bar: "fully clean",
which requires an external verdict at the current head whenever one is
reachable, not just a self-review.
-
ARD every finding — regardless of severity label. "Not a blocker",
"minor", "nit", "optional", "consider", "if you want" are for the user's
prioritization, not a pass for the implementer. For each flagged item,
choose exactly one:
- Address — fix it, commit.
- Rebut — explain why it's correct (with evidence).
- Defer — file a follow-up issue, link it (use the
defer-issue skill).
-
Push fixes (if any). If main moved ahead of the branch, sync it in
before you push, so the next review evaluates against current main:
git fetch origin main
git log --oneline ..origin/main | head
git merge origin/main
Resolve conflicts, run the repo's pre-commit checks, then push. Don't
rebase/squash a published branch — a merge commit matches GitHub's "Update
branch" button. (The sync-pr-branch skill does exactly this.)
Resolve inline threads as you go — including outdated ones. After
pushing fixes for a round, resolve the corresponding inline review threads
immediately (RESOLVE_REVIEW_THREAD) via mcp__github__pull_request_review_write with
method: resolve_thread and the threadId (returned by
READ_PR_REVIEW_COMMENTS — mcp__github__pull_request_read with
method: get_review_comments). Don't wait until fully-clean to do thread
housekeeping. For threads marked outdated in GitHub (the underlying code
changed), confirm the fix is in the current tree, then resolve. Threads
whose fixes are already in the tree but were never resolved still block the
"fully clean" check — clear them as soon as you confirm the code is right.
Opportunistic conflict sweep. After pushing (or after any round where
all findings were Rebutted/Deferred with no push), scan other open PRs in
the same repo for merge conflicts:
gh pr list --state open --json number,title,headRefName,mergeable,mergeStateStatus,comments
For each PR where mergeable == "CONFLICTING" or "UNKNOWN" (see
resolve-conflicts, "Verify before you act" — UNKNOWN can mean GitHub
hasn't finished computing yet, not that there's no conflict), verify with
git merge-tree --write-tree origin/main origin/<branch> (git ≥ 2.38) before acting,
then check claim status (most recent comment) and fix unclaimed ones —
same cascade procedure as post-merge step 1.5 (claim → isolated worktree
→ fetch main → merge → resolve-conflicts skill → push → unclaim). A
merge to main during your ARDI loop can create new conflicts in sibling
PRs; clearing them while waiting for the next verdict is better than
letting them pile up.
-
Post the ARD summary as a comment on the MR/PR (table format per the
ARD skill).
-
Re-request review — but don't double-trigger. How depends on whether
this round pushed code:
- Code was pushed: the push already triggers the review (e.g.
claude-code-review on pull_request sync). Do NOT also post
"@claude review again". On workflows with concurrency: cancel-in-progress, the push-triggered and
mention-triggered runs cancel each other, leaving the latest commit
with a canceled, never-posted verdict. Just wait for the push-triggered
review.
- No code pushed (all Rebut/Defer): no push occurred, so nothing
auto-triggers — you must explicitly re-request (post
@claude review,
or the forge's equivalent). This is the only case where you post the
mention.
- Heads-up — some repos' review workflow is not comment-triggered.
Some Quarto / R-package repos run
claude-code-review.yml on pull_request (opened, synchronize, ready_for_review, reopened) and workflow_dispatch (input pr_number),
not on an @claude comment. A new push auto-fires it; to force a fresh
review on an existing PR without a new commit, prefer
workflow_dispatch (gh workflow run claude-code-review.yml -f pr_number=<N>; without gh, the REST
.../actions/workflows/claude-code-review.yml/dispatches endpoint, or your
GitHub MCP workflow-dispatch tool). Closing+reopening the PR also works
(fires reopened) but adds timeline noise. See
memories/github-actions.md.
- Marking a draft ready seconds after its final push is another
cancel-in-progress race — the ready-event and synchronize runs fire a
second apart and the cancellation can land on the newer (current-head)
run; see
pr-on-claim for the
diagnosis and the gh run rerun remedy.
- A review ends up canceled with no comment: trigger one cleanly via
gh workflow run claude-review.yml -f pr_number=<N> (input is
pr_number) and don't push/comment again until it posts. Note: a review
run on a bot-pushed commit may show as action_required (gated) and
never run — the explicit workflow_dispatch bypasses that.
Don't let the trigger phrase leak into prose. The issue_comment
trigger fires on the bare bot @-mention anywhere in a comment body —
even inside a sentence saying you're not triggering a review. In ARD
summaries and status comments, refer to it obliquely ("re-request review",
"the review-trigger mention") or split the tokens (e.g. @ claude, with a
space, so the raw body never contains the contiguous handle); paste the
literal @-mention only when you actually intend to dispatch. A stray mention
spawns a run that cancels the push-triggered review on cancel-in-progress
setups. On some mention-bot setups it also starts a session whose
residual-commit sweep can churn the branch.
Then wait for the new verdict.
While waiting, keep checking for merge conflicts. Other PRs in this repo
can become conflicting at any time (someone merges to main while the review
runs). Poll every few minutes with /loop or a manual re-check:
gh pr list --state open --json number,title,headRefName,mergeable,mergeStateStatus,comments \
--jq '.[] | select(.mergeable == "CONFLICTING" or .mergeable == "UNKNOWN")'
Verify each candidate with git merge-tree --write-tree origin/main origin/<branch> (git ≥ 2.38; see resolve-conflicts, "Verify before you act") before
claiming — UNKNOWN isn't proof of a real conflict, and CONFLICTING can
be stale if a sibling PR merged since GitHub last computed it. Claim and
fix confirmed conflicts using the cascade procedure in post-merge step
1.5. Re-check after each resolution — new ones can appear at any time.
This turns idle wait time into productive conflict prevention.
If the PR's CI checks are failing (not just the review), investigate and fix
them as part of the ARDI loop — don't declare "clean" with red CI. This
includes:
The goal is green CI + clean review, not just clean review.
Some steps benefit from a subagent rather than blocking the round on the main
thread --- investigating a CI failure whose cause isn't obvious (see above),
verifying a reviewer's factual claim before Addressing/Rebutting it, or
checking a sibling PR for a merge conflict during the opportunistic sweep.
Delegate that via the Agent tool and keep driving the round itself (ARD,
push, post summary, re-request review) on the main thread.
For a judgment-heavy sidecar task (a subtle root-cause hunt, adjudicating a
deadlocked rebuttal before escalating to a human), give the subagent a
stronger model via the Agent tool's model parameter (e.g. model: 'opus'). Symmetrically, drop to a cheaper/faster tier (model: 'fable' or
'haiku') for a mechanical sidecar task --- see
select-model's decision tree for both
directions. For a heavy fan-out investigation/verification pass, prefer a
separately-billed provider (e.g. the codex CLI) first when available --- see
delegate-to-codex.
Always provide a clickable link to the MR/PR in the final message.
Report the final verdict and round count. Don't merge unless asked.