ARD — Address, Rebut, Defer, or Acknowledge

Every review comment gets exactly one disposition: A, R, D, or K. Ignoring is not an option.

What counts as a finding

A finding is anything the reviewer requests or implies a change to — including items tagged “nit”, “minor”, “non-blocker”, “optional”, or “consider”. All of these require Address, Rebut, or Defer.

Pure praise or neutral observations with no requested change (“nice refactor”, “TIL”) still get a row — disposition Acknowledge — so the summary accounts for every comment the reviewer made and nothing reads as silently dropped.

The four dispositions

Code Meaning Action required
A — Address Valid and in-scope. Fix it in this PR/MR and commit.
R — Rebut Incorrect, already handled, or a misunderstanding. Explain why, citing concrete evidence (line, test, doc, spec). Specific enough that the reviewer can verify it without re-reading the whole PR.
D — Defer Valid but out of scope (new feature, broad refactor, needs design discussion). File a follow-up issue (CREATE_ISSUE on GitHub — gh issue create; glab issue create on GitLab), link it, and add it to the PR/MR’s Deferred / Out-of-Scope section.
K — Acknowledge Praise or a neutral observation with no change requested. Give it a row so it’s accounted for; no code change, no rebuttal needed. Don’t stretch this to dodge a real finding.

Decision order

For anything that requests a change, choose among the first three (Acknowledge is only for no-ask comments):

  1. Address — the default. Most findings are 1–5 line fixes; if it takes under ~2 min, just fix it.
  2. Rebut — only when you’re confident the reviewer is mistaken. A rebuttal that isn’t falsifiable (“I think it’s fine”) is not a rebuttal. Before applying or rebutting any factual claim about an API, deprecation, signature, or behavior, TEST it — run the code both ways. Reviewers (including the @claude bot) state such claims confidently but are sometimes exactly backwards, and blindly “Addressing” a wrong one breaks the build. (Seen on rme #873 with igraph ≥ 2.x: the bot insisted bfs(mode=) was deprecated in favor of neimode=; the reverse is true — neimode is a hard deprecate_stop error, mode is current. Re-running the script revealed it instantly. The deprecation direction can differ across igraph major versions, so test on the locked version.) The same testing discipline applies when the claim is about the PR’s own state or metadata rather than code behavior — which footer/URL convention a given post uses, whether a file matches main, whether a check still exists — not just runnable code. Fetch the actual PR data (the API call, not memory) before Addressing or Rebutting. (Seen on ai-config#688: a review claimed a memory bullet’s cited attribution-footer text was wrong, reasoning from the PR’s own body — but the PR body and a reply comment get their footer through two different code paths that don’t match. Fetching both directly via the API before responding showed the bullet was right and the reviewer had compared the wrong artifact; rebutted with that evidence instead of either implementing the suggested fix or dismissing the finding unchecked.)
  3. Defer — only when the fix genuinely expands scope. Never defer just because a fix is “minor” — minor fixes get Addressed.

“You deleted/removed X” findings — check for branch-behind-main first. When a reviewer says the PR removed something you don’t recall touching, run git log HEAD..origin/main and git show HEAD:<file> before Addressing. Main may have added X after you branched, so X is absent only because the branch is stale — not because your diff deleted it. The fix is merge origin/main in (the standing keep-synced rule), not “restore” a line you never removed. Verify the actual committed diff (git show <commit> -- <file>) rather than trusting the finding’s framing. (Seen on ai-config PR #52.)

“Blocking: missing X” findings that cite a CI job or policy — check the job still exists on main first. A review can flag a CI failure (e.g. “the Require Changelog Entry job is failing, add a CHANGELOG.md entry”) as blocking, but the requirement itself can be removed from main in the time between when the review ran and when you address it — a different PR deletes the CI job and/or the file it checks. This isn’t the branch-behind-main case above (nothing was deleted from this PR); the requirement stopped existing project-wide. Before Addressing, check whether the cited job/file is still on main (git log --oneline origin/main -- <path>, or git show origin/main:<path>). If it’s gone, Rebut with the removal commit(s) as evidence rather than resurrecting a dead requirement. (Seen on ai-config PR #376: a “blocking” CHANGELOG.md finding referenced require-changelog.yml, which had been deleted from main in #385/#387 by the time the fix round ran.)

Procedure

1. Gather every finding

Collect the full set before dispositioning, so none slips through. Pull both the summary comment and the inline review threads — READ_PR_COMMENTS and READ_PR_REVIEW_COMMENTS (abstract operation tokens; resolve to your model’s tool via tool-mappings.md).

GitHub

gh pr view <N> --comments                            # READ_PR_COMMENTS — top-level + summary comments
gh api repos/{owner}/{repo}/pulls/<N>/comments       # READ_PR_REVIEW_COMMENTS — inline review-thread comments

GitLab

glab mr view <N> --comments                          # discussion notes
glab api "projects/:id/merge_requests/<N>/discussions"   # inline threads

Bots often post the same finding twice — once inline and once in the summary comment. Collect the union and dedupe before numbering, so one issue doesn’t get two rows (or two conflicting dispositions). Then number 1..n; every number must end up with a row in the summary.

A PR can also carry more than one review surface from different reviewers: an inline code-review bot, a separate agent post-step that posts its own top-level summary, and Copilot are each distinct. gh pr view <N> --comments returns every top-level comment, so disposition each reviewer’s findings — not just the inline review or the most recent comment. A whole review summary left un-dispositioned reads as ignored even when every inline thread was handled.

2. Disposition each one

Apply the decision order above. For Address items, make the edits now.

3. Commit Addressed fixes (one commit per review round)

git add -p                                           # stage deliberately
git commit -m "fix: address round <k> review findings"   # COMMIT
git push                                                  # PUSH
  • One commit per round, not one per finding — reference its SHA in every Address row.
  • Never --amend the already-reviewed commits: the reviewer (and CI) ran against them and others may have pulled. A fresh commit keeps the audit trail.
  • Don’t push generated cruft (.Rout, build artifacts); respect the repo’s .gitignore.
  • If every finding is Rebut/Defer, there’s nothing to commit — skip to step 4 and still post the summary.

4. Post one summary comment

Write the summary to a file and post it from the file — never inline on GitLab, because glab mis-parses backticks (e.g. commit SHAs in code spans) as shell subcommands:

# write the summary to ard-summary.md, then:
gh pr comment <N> --body-file ard-summary.md         # COMMENT_PR — GitHub
glab mr note <N> -F ard-summary.md                   # GitLab

Keep the bot’s trigger phrase out of the summary body. The issue_comment trigger fires on the bare bot @-mention anywhere in a comment — even in a sentence saying you’re not re-requesting a review. 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 review. 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.

Summary format:

Addressed findings from review of <commit-or-range>:

| # | Finding | Disposition | Detail |
|---|---------|-------------|--------|
| 1 | <summary> | ✅ Address | Fixed in <commit-sha> |
| 2 | <summary> | 🔄 Rebut | <one-line reason> |
| 3 | <summary> | 📌 Defer | <issue-link> |
| 4 | <summary> | 👍 Acknowledge | <one-line thanks / note> |

### Rebuttal: Finding 2
<full explanation with evidence>

Expand each Rebut below the table. Deferred rows must carry a real issue link.

4b. Reply to every inline review thread — and resolve where appropriate

(The standalone resolve-pr-threads skill runs just the resolve half of this step on its own — useful for sweeping already-settled threads without a full ARD pass.)

The one summary comment is not enough on its own. A reviewer who left inline comments wants a response on each thread, not just a table posted elsewhere. For every inline comment, post a short reply on its own thread with the disposition (and the commit SHA for an Address), then resolve the thread once the item is genuinely settled.

GitHub — reply on the comment’s thread (REPLY_REVIEW_COMMENT), then resolve via GraphQL (RESOLVE_REVIEW_THREAD):

# Reply on the same thread as inline comment <comment_id>:
# (quote every argument containing a <placeholder> -- bash treats a bare `<`
# as a redirection operator even mid-word, so an unquoted path like
# /tmp/reply-<comment_id>.md silently writes to the wrong file)
cat > "/tmp/reply-<comment_id>.md" <<'EOF'
✅ Addressed in `<sha>`.
EOF
gh api "repos/{owner}/{repo}/pulls/<N>/comments" \
  -F in_reply_to="<comment_id>" -F body="@/tmp/reply-<comment_id>.md"   # REPLY_REVIEW_COMMENT

# List threads to get the node id, then resolve the settled one:
gh api graphql -f query='query { repository(owner:"<owner>",name:"<repo>") {
  pullRequest(number:<N>) { reviewThreads(first:100) { nodes {
    id isResolved comments(first:1){ nodes { databaseId body } } } } } } }'
gh api graphql -f query='mutation {
  resolveReviewThread(input:{threadId:"<thread_node_id>"}) { thread { isResolved } } }'   # RESOLVE_REVIEW_THREAD

In a remote/web session without gh, resolve RESOLVE_REVIEW_THREAD via mcp__github__resolve_review_thread instead (see tool-mappings.md).

GitLab — reply to the discussion, then resolve it:

cat > "/tmp/reply-<discussion_id>.md" <<'EOF'
Addressed in `<sha>`.
EOF
glab api -X POST "projects/:id/merge_requests/<N>/discussions/<discussion_id>/notes" \
  -F body="@/tmp/reply-<discussion_id>.md"
glab api -X PUT "projects/:id/merge_requests/<N>/discussions/<discussion_id>?resolved=true"

Resolve only when the item is actually settled:

  • Address — resolve after the fix is pushed (reply names the SHA). Never resolve an Address whose fix isn’t on the branch yet. Not confident the fix fully settles the concern? When the finding is ambiguous, turns on a design choice, or admits only a partial fix, reply explaining what you did and ask for confirmation instead of resolving. Leave the thread open for the reviewer to verify.
  • Defer — reply with the tracked issue link, then resolve (work lives elsewhere now).
  • Acknowledge — reply briefly, then resolve.
  • Rebut — reply with the falsifiable evidence. A rebuttal only counts as settled once it convinces the reviewer — i.e. they don’t re-raise it on the next round. While that’s still in question, leave the thread open: resolve a bot’s thread once it drops the item, and leave a human’s thread open if they may want to respond. Resolving on acceptance happens a round later than the rebuttal, so it’s easy to forget — at the start of each new round, first sweep your still-open rebuttal threads and resolve any the latest review didn’t re-raise, before dispositioning the new findings. If you and the reviewer reach an impasse (your rebuttal didn’t convince them, their re-raise didn’t convince you), escalate to a human reviewer — request d-morrison via the request-pr-review skill (or gh pr edit <N> --add-reviewer d-morrison) and @-mention them with the impasse — rather than resolving unilaterally or looping forever. Not every re-raise is an impasse, though: if the reviewer accepts your fact but objects that the claim is over-generalized (you verified one case and stated a sweeping rule), narrow the claim to what you actually observed — that’s an Address, not a continued Rebut. Reserve escalation for genuine factual disputes. (Seen on ai-config PR #183: a tested project-root {{< include >}} behavior was correct but asserted as a universal Quarto-projects rule; the reviewer granted the test and asked only that it be scoped to the observed case, so the fix was to soften the wording, not re-argue.)

Don’t resolve a thread you haven’t replied to. Every inline comment ends with both a reply and (where appropriate) a resolution — silence on a thread reads as ignored, exactly the failure ARD exists to prevent.

End-state (fully clean): when the PR/MR is 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. A leftover open inline thread (an unaccepted rebuttal, an un-resolved Address) means you’re not clean yet.

ARD round-close checklist

Per shared/workflow/skill-checklists.md, confirm each box before reporting the round done:

Rules

  • Every reviewer comment appears in the table AND gets a reply on its own inline thread (step 4b). The summary table is the overview; the per-thread reply is what the reviewer sees in context. A thread with no reply reads as ignored.
  • Resolve threads once settled (Addressed-and-pushed / Deferred-with-issue / Acknowledged), but never resolve a thread you haven’t replied to or whose fix isn’t pushed — and leave a human reviewer’s thread open if they may want to respond.
  • Severity never exempts. “Nit” / “optional” / “consider” still require A, R, or D — never K.
  • Rebuttals must be falsifiable — point to specific code, behavior, or documentation.
  • Deferrals must be tracked. A defer without a filed issue is just ignoring with extra words.
  • Push before you post. The reviewer should be able to verify Addressed fixes are on the branch.
  • A suggestion block is a hint, not gospel. Before committing a reviewer’s suggested fix, verify it actually works and handles the general case — not just the flagged spot. A confident suggestion can overcorrect (e.g. a regex tweak that fixes one form but breaks all the others). If the correct fix differs, apply that and note the divergence in your reply, so the reviewer sees why.

Delegating sidecar work

When dispositioning a finding (step 2) turns up something that needs independent digging — testing a reviewer’s factual claim before Addressing or Rebutting it (see the testing rule above), researching how a prior PR handled the same pattern, or checking whether a cited CI job still exists on main — hand it to a subagent via the Agent tool rather than blocking the rest of the round on it. Keep dispositioning the remaining findings, posting the summary, and pushing fixes on the main thread.

Give the subagent a stronger model (e.g. model: 'opus' on the Agent tool call) when the check is judgment-heavy — a subtle behavioral claim, a disputed rebuttal — rather than leaving it at the session default. Symmetrically, drop to a cheaper/faster tier (model: 'fable' or 'haiku') for a mechanical check (a lookup, confirming a file/symbol still exists); see select-model’s decision tree for both directions. For a heavy fan-out verification pass, prefer a separately-billed provider (e.g. the codex CLI) first when available — see delegate-to-codex.

Integration with ardi

Inside the ardi loop (also reachable as iterate):

  1. Read the latest review (ardi step 2)
  2. Apply ARD to each finding (this skill: steps 1–2)
  3. Commit + push fixes (this skill: step 3)
  4. Post the ARD summary and per-thread replies (this skill: steps 4–4b)
  5. Re-request review (ardi step 6) — even if this round was Rebut/Defer only, so the reviewer re-evaluates.

The loop continues until the PR/MR is fully clean — zero findings, all CI workflows green, and every inline review thread resolved (the only open conversation being the final all-clear exchange — the reviewer’s all-clear and your reply to it). A rebuttal counts only once it convinces the reviewer; on an impasse, escalate to a human reviewer (d-morrison; see step 4b for how).

Edge cases

  • Ambiguous finding — if you can’t tell whether it’s a change request or an observation, treat it as a finding and Address or Rebut. Don’t assume it’s informational.
  • Reviewers contradict each other — Address the most recent reviewer; note the contradiction in your response so the user can arbitrate.
  • Finding duplicates a deferred item — Rebut by pointing to the existing issue. Keeping the Deferred / Out-of-Scope section current in the PR/MR description should prevent re-flagging; if it recurs, update that section.
  • Findings sourced from a different PR/MR than the target branch — treat the cited review as the finding source, but apply dispositions on the target PR/MR branch the user names. Post the ARD summary on the target PR/MR, then reply on each original inline thread with the target PR/MR link plus disposition-specific evidence: Address -> target fix commit SHA; Rebut -> target PR/MR comment link with the falsifiable evidence; Defer -> follow-up issue link; Acknowledge -> brief acknowledgment. This keeps provenance auditable across both threads without requiring a commit in non-Address rounds.
Back to top