pr-address-review
Answer human review feedback on your own PR end to end: find the latest comments left by real people (bots filtered out), validate each claim against the code, fix the valid ones, resolve merge confli
Files1
View on GitHubSKILL.mdskills/pr-address-review/SKILL.md
GitHubAddress Human Review
The PR is the user's. A teammate commented. The job: work out which comments are right, fix those, answer every one in the user's voice, ship it, keep CI green.
Pairs with pr-post-comments (voice rules and the paced posting driver) and
pr-verify-review (the same loop from the reviewer's side). Uses a personal
voice skill for every reply when one is installed, and hands off to a
pr-babysit skill for CI and bot comments when one is installed.
POST=~/.claude/skills/pr-post-comments/scripts/post_pr_comments.py # adjust if installed elsewhere
Hard stops
- NEVER merge. NEVER approve or request changes. Green + answered = report and stop.
- NEVER resolve a human's thread. The reviewer closes their own. Reply, leave open.
- NEVER post without one explicit go on the exact drafts (Step 6). That single go covers push and posting; nothing after it needs re-confirmation unless the drafts change.
- Every reply goes through
$POSTin the background. Nothing else creates comments: no directgh apicalls, nogh pr comment, no loop. - No attribution, no AI or tool mention in commits, replies, or PR body.
- Commits carry only the fix. No CLAUDE.md, agent configs, planning files.
Step 0 - Pin the PR and the account
gh pr view ${PR:-} --json number,url,headRefName,headRefOid,baseRefName,author,state
No number given: use the PR for the current branch. OWNER/REPO from the URL.
- Account:
MEisgh api user --jq .login. If the repo needs a different account than the active one, switch (gh auth switch) or setGH_TOKENfirst, then re-check the login. - The PR author must be
ME. If not, stop - this is someone else's PR, use pr-review (first pass) or pr-verify-review (after your review). - The checkout must be the head branch, tree clean, up to date with the remote
(
git status -sb,git fetch && git status). In a multi-worktree repo, find the worktree that holds the branch (git worktree list) and work there. Never switch branches in the main worktree with uncommitted work in it.
Step 0.5 - Merge conflicts
Check before touching review feedback. Fixes written on a branch that conflicts with its base get rewritten twice.
gh pr view $PR --json mergeable,mergeStateStatus
mergeable can read UNKNOWN for a few seconds after a push while GitHub
computes it; re-query until it's MERGEABLE or CONFLICTING. Also check
locally, which catches a base that moved since GitHub last looked:
git fetch origin <base>
git merge --no-commit --no-ff origin/<base>; git merge --abort
Conflicting: merge the base into the branch. Don't rebase - a rebase needs a force-push, which strands the reviewer's inline threads on dead commits and rewrites history they already read.
git merge origin/<base>
Resolve each conflicted file by understanding both sides, not by picking one:
- Read what the base changed (
git log -p origin/<base> -- <file>since the merge base) and why. The resolution has to keep the base's intent and the PR's intent. Taking--oursor--theirswholesale is only right when one side's change is fully superseded, and that needs saying. - Lockfiles and generated files (lockfiles, codegen output, schemas, snapshots): take the base's version, then regenerate with the repo's own command. Never hand-merge them.
- Migrations: two heads after the merge means the PR's migration gets re-parented onto the base's latest one. Check the chain is linear.
- Code that merged cleanly can still be wrong. A renamed function on base that the PR still calls by the old name is a conflict git won't show. Run types, lint, and the affected tests after every merge, not just for conflicted files.
- A conflict where the two intents genuinely clash (base removed what the PR builds on, both sides redesigned the same thing): stop and ask the user.
Commit with git's default merge message, no trailers. Record which files conflicted and how each was resolved - it goes into the Step 6 summary. The merge commit stays local until Step 7 like everything else.
Step 1 - Collect what the humans said
Three places people leave feedback. Fetch all three once, into the scratchpad.
gh api graphql -F owner=$OWNER -F repo=$REPO -F pr=$PR -f query='
query($owner:String!,$repo:String!,$pr:Int!){repository(owner:$owner,name:$repo){pullRequest(number:$pr){
reviewThreads(first:100){nodes{id isResolved isOutdated path line originalLine
comments(last:30){nodes{databaseId createdAt body url author{__typename login}}}}}
reviews(last:50){nodes{databaseId state body createdAt url author{__typename login}}}
comments(last:100){nodes{databaseId body createdAt url author{__typename login}}}
}}}' > $S/feedback.json
A person is author.__typename == "User", login not ending in [bot], login not
ME. Everything else is a bot and is out of scope here.
What counts as open, per source:
- Review thread: unresolved, a human wrote in it, and the last comment is not
ME's. A thread whereMEalready answered and the reviewer hasn't come back is waiting on them - skip it. - Review body (
COMMENTED/CHANGES_REQUESTEDwith text): newer thanME's last PR-level comment or push, whichever is later. - Conversation comment: same cutoff as review bodies.
--since <ISO> from the user overrides the cutoff. Print the list as a table
(source, reviewer, path:line, first 100 chars) before doing anything else. Zero
open items: say so, go to Step 8, done.
Step 2 - Validate every comment
Reviewers are often right and sometimes wrong. Neither is assumed. For each item:
- Read the code it points at at the current head, plus callers and tests. Outdated threads: find where that code moved to, or confirm it's gone.
- Reproduce the claim. A bug claim gets a failing test or a concrete input. A design claim gets traced through every caller. A "why" question gets the actual reason from code, git log, or the linked issue.
- Assign one verdict:
| Verdict | Meaning | Action |
|---|---|---|
| FIX | claim holds | change the code, reply saying what changed |
| PARTIAL | right problem, wrong remedy or too broad | fix the real part, reply explaining the narrower fix |
| ANSWER | question, no change needed | reply with the answer |
| DISAGREE | claim is wrong | no change; reply with the evidence (file:line, test, input) |
| DONE | already fixed at head | reply pointing at the commit |
| DEFER | real, but outside this PR's scope | only if the user agrees; open an issue, reply with the link |
DISAGREE needs proof you could paste into the reply. "I think it's fine" is not a verdict - that's FIX by default. When a human reviewer and a bot contradict each other, the human wins; the bot thread stays open.
Two reviewers asking for opposite things: stop and ask the user which way to go.
Step 3 - Fix
- Follow the repo's own rules first (AGENTS.md / CLAUDE.md: impact analysis, test commands, lint config).
- Behaviour change: failing test first, then the fix. A fix without a test that would have caught the original problem is half a fix.
- Keep each fix to what the comment asked. Opportunistic refactors start new review threads.
- Run lint, types, and the affected tests locally. Red stays red until fixed.
- Commit per concern, conventional message (
fix: ...,test: ...), no trailers. Grouping every reviewer fix into one "address review" commit is fine when they're small. Record the short SHA per thread - replies cite it.
Commit locally now. Do not push yet.
Step 4 - Draft the replies
Load the user's voice skill first if one is installed (for example
Skill("voice-of-er")). The voice rules in pr-post-comments Step 3 apply either way.
One reply per item. Review threads get {"in_reply_to": <databaseId>, "body": ...}
on the thread's first comment id. Review bodies and conversation comments get one
PR-level {"body": ...}; if several reviewers, one each, opening with their
@login.
What a good reply looks like:
- Lead with what happened. "moved the retry into the client, abc1234" beats "Great catch! I've addressed this by...".
- Cite the SHA for FIX/PARTIAL/DONE. Cite file:line or the test name for DISAGREE.
- DISAGREE stays warm and specific. The reviewer should be able to check the claim in ten seconds.
- No "thanks for the review" on every reply. Maybe once, if it's natural.
- Length follows the item. A rename gets one line.
- No tags here -
[r]/[o]/[f]/[c]are for reviewing, not answering. - No process narration ("checked the code", "I looked into it"), no em dashes, no So/Okay/Well/Yeah openers.
Write the JSON list to $S/replies.json (python or jq -n --arg so quoting
survives), then lint:
python3 "$POST" $OWNER/$REPO $PR $S/replies.json --check
Fix any flagged body, re-check until clean.
Step 5 - Self-review the diff
Before showing anything, read git diff origin/<head>..HEAD as a hostile reviewer:
does each hunk trace back to a comment? Anything unrelated leaked in? Did a
DISAGREE item accidentally get a code change? Does a reply promise something the
diff doesn't do?
Step 6 - The one approval gate
Show the user:
- The item table: reviewer, location, verdict, commit SHA, one-line reason.
- The diff summary (
git diff --statplus anything non-obvious), and if Step 0.5 merged the base: each conflicted file and how it was resolved. - Every reply body, bare text, not in a blockquote.
Wait for an explicit go ("post", "ship it", "go"). Edits to a body: apply, re-run
--check, show just the changed ones. A DEFER item needs the user's yes before
anything is filed.
Step 7 - Push, then post
Order matters: the fix has to be on the PR before the reply that says it is.
git fetch origin <base>once more. If the base moved since Step 0.5 and conflicts again, redo Step 0.5 and show the user the new resolutions before pushing.- Pre-push gate: the repo's lint, type check and affected tests, green. If a repo-specific gate skill or hook exists, it runs here instead.
git push. Never force-push unless the branch was already rewritten and the user said so.- Post in the background:
python3 "$POST" $OWNER/$REPO $PR $S/replies.json
with run_in_background: true. It paces each reply by its length and keeps
the clock across runs. Do not wait on it - move on to Step 8.
Step 8 - Watch CI
With a pr-babysit skill installed, hand off to it on the new head (CI watch,
bot sweep, re-arm after each push). Without one:
gh pr checks $PR --watch --fail-fast
A red check: read the failing job's log, fix it if it's this PR's (it isn't if the same check is red on base), commit, pre-push gate, push, watch again. Green is not proof on its own - skim the test job for 0 collected or skipped suites.
What carries over from this skill while watching:
- A new human comment that lands while watching re-enters this skill at Step 1. Its drafts go through Step 6 again - the earlier go doesn't cover new text.
- The PR turns
CONFLICTING(base moved meanwhile): run Step 0.5, then the pre-push gate, push, re-arm. A clean mechanical merge doesn't need a new go; a resolution that changed behaviour on either side does. - A bot comment that contradicts a human reviewer's request is left open, not fixed, not resolved.
Step 9 - Report
When the posting log shows every html_url and CI is green:
- Table: reviewer, location, verdict, SHA, reply link.
- CI state, head SHA, threads still open (human ones are expected to be open - they're the reviewer's to close).
- Anything posted partially or failed, by number.
Then stop. No merge.