ARDI — ARD + Iterate (single PR/MR)
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.
Procedure
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@claudeCI 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_PRand the other bracketed tokens below are abstract operation tokens — resolve to your model’s tool viatool-mappings.md.)Read the latest review. Pull the most recent reviewer comment — the
@claudebot’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 listgoing 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
--paginatea 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' # READ_PR_COMMENTSThe reviewer’s bot login varies by setup —
gh pr viewreports it asclaude, the REST API asclaude[bot], and some setups post asgithub-actions[bot].startswith("claude")matches acrossgh pr viewandgh api; broaden it if your reviewer posts under another login, or you’ll silently readnulland false-pass. This command captures the bot review only — for a human reviewer (any login), gather comments with theardskill’s step 1 (gh pr view <N> --commentsplus 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 viatool-mappings.md) and check whether it posted a verdict at the current head. Finding a review object at the rightcommit_idonly 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 isREAD_PR_REVIEWS) before treating it as an all-clear:gh api’s own--jqflag has no--arg/--argjson(seememories/github.md’sgh api/jqnote) – pipe the raw paginated output into standalonejq -sinstead, 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" fiA 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 suppression 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
/commentsendpoint returns (verified: PR #660’s review 4767752501 read “generated no new comments” in its overview while its full body carried 3 suppressed findings; PR #1029 repeated the shape from round 3 onward). A third condition is required: the raw review body must not contain a suppression block at all. Match the suppression block inside its<summary>heading, case-insensitively onsuppressed– not on either exact phrase, and not anywhere in the body. GitHub changed the wording and dropped the reason: PR #660 emits<summary>Comments suppressed due to low confidence (3)</summary>, while PRs #1029 and #1031 emit<summary>Suppressed comments (4)</summary>. So a literal grep forComments suppressedreturns zero against a current body that plainly has the block, which produced a real false negative during the ai-config#1029 loop. A body-wide match over-corrects, though, and would keep a clean PR permanently non-clean: ordinary overview prose contains the word, verified on review 4837572117, whose summary table reads “suppressed Copilot findings” outside any collapsed block. Scope the match to<summary>elements (or parse the<details>block), and accept both headings.The stakes are why this matters: from round 3 of ai-config#1029 onward every substantive finding arrived suppressed, under a “generated no new comments” overview with zero inline comments – including CRLF silently disabling a failure path repo-wide. So a suppressed finding is not a lower-value one, on the evidence available here — which is a run of valuable suppressed findings, not a measured correlation. GitHub’s own docs do not document suppression at all, so expect the label to keep moving and key on the stable token within that scope. 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_idwith 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@clauderun (see the “Do the review yourself” fallback inCLAUDE.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-progresscancellation (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-issueskill).
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 # any commits? merge them in git merge origin/mainResolve conflicts, run the repo’s pre-commit checks, then re-scan the PR’s touched files with whitespace-normalizing search for merge-status hedges that
mainmay have falsified (still open,not yet merged,once that merges,as of,will live at,proposed in) before pushing. Do not use line-oriented literal grep; semantic line breaks can split the phrase this check needs to find. Don’t rebase/squash a published branch – a merge commit matches GitHub’s “Update branch” button. (Thesync-pr-branchskill 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) viamcp__github__pull_request_review_writewithmethod: resolve_threadand thethreadId(returned byREAD_PR_REVIEW_COMMENTS—mcp__github__pull_request_readwithmethod: 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 # LIST_PRSFor each PR where
mergeable == "CONFLICTING"or"UNKNOWN"(seeresolve-conflicts, “Verify before you act” —UNKNOWNcan mean GitHub hasn’t finished computing yet, not that there’s no conflict), verify withgit 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 aspost-mergestep 1.5 (claim → isolated worktree → fetch main → merge →resolve-conflictsskill → push → unclaim). A merge tomainduring 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-reviewonpull_requestsync). Do NOT also post “@claude review again”. On workflows withconcurrency: 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.ymlonpull_request(opened, synchronize, ready_for_review, reopened) andworkflow_dispatch(inputpr_number), not on an@claudecomment. A new push auto-fires it; to force a fresh review on an existing PR without a new commit, preferworkflow_dispatch(gh workflow run claude-code-review.yml -f pr_number=<N>; withoutgh, the REST.../actions/workflows/claude-code-review.yml/dispatchesendpoint, or your GitHub MCP workflow-dispatch tool). Closing+reopening the PR also works (firesreopened) but adds timeline noise. Seememories/claude-bot-workflows.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-claimfor the diagnosis and thegh run rerunremedy. - A review ends up canceled with no comment: trigger one cleanly via
gh workflow run claude-review.yml -f pr_number=<N>(input ispr_number) and don’t push/comment again until it posts. Note: a review run on a bot-pushed commit may show asaction_required(gated) and never run — the explicitworkflow_dispatchbypasses that.
Don’t let the trigger phrase leak into prose. The
issue_commenttrigger 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 oncancel-in-progresssetups. 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
mainwhile the review runs). Poll every few minutes with/loopor a manual re-check:gh pr list --state open --json number,title,headRefName,mergeable,mergeStateStatus,comments \ --jq '.[] | select(.mergeable == "CONFLICTING" or .mergeable == "UNKNOWN")' # LIST_PRSVerify each candidate with
git merge-tree --write-tree origin/main origin/<branch>(git ≥ 2.38; seeresolve-conflicts, “Verify before you act”) before claiming —UNKNOWNisn’t proof of a real conflict, andCONFLICTINGcan be stale if a sibling PR merged since GitHub last computed it. Claim and fix confirmed conflicts using the cascade procedure inpost-mergestep 1.5. Re-check after each resolution — new ones can appear at any time. This turns idle wait time into productive conflict prevention.- Code was pushed: the push already triggers the review (e.g.
Per-round checklist
Pause point: before advancing to the next round. Do-Confirm; per shared/workflow/skill-checklists.md.
- Repeat from step 2 until the PR/MR is fully clean (see The bar: “fully clean” – zero findings and all CI workflows and check runs green and completed and every inline thread resolved). Don’t exit on a clean review body alone.
Fix broken CI/workflows too
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:
- Workflow syntax errors — fix them in this repo.
- Upstream template bugs — if the failure is in a reusable workflow from a shared CI library (e.g., HACtions) or a GitHub Action, file an issue (or open a PR) upstream using the
supskill, then either pin a working version or apply a local workaround until the upstream fix lands. - Flaky / infra failures — retry once; if it persists, investigate root cause.
The goal is green CI + clean review, not just clean review.
Delegating sidecar work
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.
The bar: “fully clean”
The loop ends only at fully clean, which means both:
- All CI workflows and check runs are green and completed — every check, not just required ones and not just the review job; never still queued or in progress (see Fix broken CI/workflows too above, and
shared/workflow/fully-clean.mdfor the check-run-vs-workflow-run and API-casing gotchas). - The latest review is totally clean — zero flagged items under any heading. “Looks good” / “no findings” / “approved” with no follow-on bullets. Every item that wasn’t directly Addressed is either Deferred to a tracked issue or Rebutted with a rebuttal that actually convinced the reviewer (they didn’t re-raise it on the next round). A rebuttal the reviewer still disputes does not count as clean. Don’t stop at “ready with one minor nit.” That review must be a genuine posted verdict at the current head, from an external reviewer if one is reachable – check availability again right before declaring clean, not just at the round where self-review first started; an inferred “probably clean” from green CI and resolved threads does not satisfy this.
Threads: at fully-clean, every inline review thread is resolved, and the only conversation left open is the final all-clear exchange — the reviewer’s all-clear comment (usually a top-level PR comment, not an inline thread) and your reply to it. (Thread mechanics live in the ard skill, step 4b.)
Fully-clean exit checklist
Pause point: before declaring “clean” or reporting the PR ready. Do-Confirm; per shared/workflow/skill-checklists.md.
Stopping conditions
There is no round limit. Always request another review. The loop on a single PR ends on exactly three things:
- A totally clean review — no nits, no non-blocking comments, everything Addressed or agreed Deferred. See The bar: “fully clean”.
- Nothing actionable remains — every open item has been escalated to a human and is waiting on their decision, so there is no next action you can take. Not “some items are deadlocked”; all of them.
- The user says stop.
Nothing else. Not a round count, not a sense that findings are getting smaller, not a judgment that the reviewer is nitpicking.
Deadlock is per-item, and it does not stop the loop. If you and the reviewer can’t reach consensus on one finding (your rebuttal didn’t convince them, and their re-raise didn’t convince you), escalate that item to a human reviewer rather than looping on it or unilaterally overriding. Request d-morrison via the request-pr-review skill (or gh pr edit <N> --add-reviewer d-morrison), @-mention them in a comment summarizing the impasse, and surface the open item to the user. Then keep driving the PR: address every other finding, push, and request the next review. Only when every remaining item is an escalated deadlock does condition 2 above fire, and even then the loop resumes the moment the human rules.
Sweep-level scheduling is a different question
ardia and gia drive many PRs. When one of those is waiting on a human — a deadlocked item, a blocked dependency, an unresolvable conflict — the sweep records it and moves to the next PR so the batch keeps moving. That is scheduling, not a stopping condition for the loop: the sweep returns when the human rules, and nothing about it licenses accepting unaddressed findings on the PR itself.
“Asymptotic noise” is an anti-pattern, not a signal
This skill used to carry a guard saying that after 3-4 rounds of new nits you should surface the pattern and ask whether to continue. That guard is removed, and reasoning of that shape must not be reintroduced. It fails three ways:
- It fires on round count, not on finding quality. A round producing genuine, reproducible correctness bugs is indistinguishable from a round producing style churn if all you count is rounds.
- It reads as diligence, which is exactly why it goes unexamined. Stopping to ask feels like respecting the user’s time.
- It hands triage back to the user — the precise move
address-every-commentalready forbids for individual findings. The guard reintroduced at loop scale the thing that fragment bans at item scale.
The tell is any sentence of the form “the reviewer keeps finding things, so maybe we should stop.” Replace it with another review request.
Two things that are not this anti-pattern and stay:
- The per-item hold. When a reviewer re-raises one already-deferred item verbatim each round, reply once pointing at the tracked issue and hold on that item, while continuing to fix every new finding. That is about not re-litigating one item; it never stops the loop.
- Reporting the round count. Saying “round 7, 23 findings, all Addressed” is useful information. Attaching “shall I stop?” to it is the anti-pattern.
(ai-config#1029 is the case record: six rounds, 23 findings, all Addressed, with rounds 2-6 each finding real bugs in earlier rounds’ own fixes — including CRLF silently disabling a failure path repo-wide, a --compare reporting a zero delta, and a traversal-order-dependent measurement. The loop stopped to ask twice under the old guard; both times the answer was to keep going, and the next round found four more real bugs.)
On clean
Post an unclaim comment (COMMENT_PR — gh pr comment <N> --body "Done --- PR is free.") to unblock any parallel sessions that backed off in step 1.
Then run ums, before reporting ready. The clean verdict is the proactive-UMS checkpoint for this PR, not the merge; see CLAUDE.md’s “Run UMS proactively, as learnings accumulate”. The loop’s whole point is that it ends here and hands the merge to a human, so a pass deferred to the merge is deferred to a moment this session may never see. Everything the review lifecycle taught – recurring findings, corrections, guidance given along the way – is complete as of the verdict.
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.