PR: Handle Review ยท git:20261028.e129592 ยท 2026-10-28 ยท sha256 58005b54fb30b732

PR: Handle Review git:20261028.e129592A

Immutable. This exact content never changes and is served at /api/v1/blob/58005b54fb30b732.

---
name: "PR: Handle Review"
description: "Work through a PR's change requests: verify each independently, fix what holds up, reply to every thread"
when_to_use: "When a PR has received review feedback (human or pr-review) and the change requests need triaging, fixing and answering rather than blind acceptance."
model: opus
effort: high
metadata:
  glyph: แ›Ÿ
  family: pr
  goblin-spinner: reading the review|verifying claims|re-reviewing the fixes|replying to threads
disable-model-invocation: true
allowed-tools: ["Read", "Glob", "Grep", "Edit", "Write", "Bash(git:*)", "Bash(gh:*)", "Bash(jq:*)", "Bash(npm:*)", "Bash(bun:*)", "Bash(pnpm:*)", "Bash(deno:*)", "Bash(python3:*)", "Agent"]
arguments: ["pr", "mode"]
argument-hint: "[PR number | URL] [legend]"
---

# Handle a PR Review

Takes the review feedback on a PR and closes the loop: independent verification of every change request, fixes for the ones that hold up, granular commits via `commit-batch`, a fresh-context strict re-review of the result before anything is drafted, and a reply on every thread. Two approval gates: one before any code changes, one before anything is posted to GitHub.

Reviewers are sometimes wrong. The core of this skill is that no change request is implemented on the reviewer's authority alone, and none is dismissed without evidence. Jason's own replies and reactions steer that verification; they never replace it. See `library/references/review-reaction-signals.md` for the full reaction vocabulary and how it interacts with this promise.

## Step 0: Legend mode

Scan `$ARGUMENTS` case-insensitively for the standalone token `legend`, in any position. If found, print the reaction-signal table from `library/references/review-reaction-signals.md` and stop; no PR is touched, and no PR argument is required alongside it. Any other bare word that isn't a PR number or URL is an error, never a silent fallback into treating it as one.

## Step 1: Resolve the PR

Resolve `owner`, `repo`, `pull_number` and the head branch from the PR argument (whatever in `$ARGUMENTS` isn't the `legend` token): `gh pr view {pr} --json number,url,headRefName`, with no `{pr}` at all when there is no argument, which makes gh resolve the current branch's PR. Take `owner` and `repo` from the `url` (`https://github.com/{owner}/{repo}/pull/{n}`): that is the base repository, where the threads live, whereas `headRepository` would name the fork on a cross-repository PR. Resolve the authenticated login with `gh api user --jq .login`. This is the reaction-ownership discriminator, not a reviewer/not-reviewer test: Jason sometimes authors his own review comments (self-review), so author login alone can't separate "reviewer asking" from "Jason directing". Position in the thread carries that distinction; see Step 2.

Check out the PR branch if not already on it (`gh pr checkout {pull_number}`). Run `git status` first; if the working tree holds unrelated uncommitted work, stop and ask rather than mixing it into review fixes.

## Step 2: Gather the change requests

Two sources, both required:

1. **Review threads** (line-anchored). REST doesn't expose resolved state, so use GraphQL, with `reactionGroups` added so Jason's reactions travel with each comment:

   ```bash
   gh api graphql -f query='query($owner:String!,$repo:String!,$pr:Int!){repository(owner:$owner,name:$repo){pullRequest(number:$pr){reviewThreads(first:100){nodes{isResolved isOutdated path line comments(first:50){nodes{databaseId author{login} body createdAt reactionGroups{content reactors(first:50){totalCount nodes{__typename ... on User{login}}}}}}}}}}}' -f owner={owner} -f repo={repo} -F pr={pull_number}
   ```

   `reactionGroups` always returns all 8 groups regardless of use; a reaction is only present when `reactors.totalCount > 0`. Filter reactors to `login == {authenticated login}`; keep `__typename` because `Reactor` is a union over `User`, `Bot`, `Organization` and `Mannequin`, and a node with no `login` must parse as "not Jason" rather than throwing. Report, in one line, the count of reactions from anyone else, ignored.

   Resolved threads stay out of scope. `isOutdated` threads stay in scope: the code moved, but the concern may not have. For every unresolved thread, classify by the last comment and by Jason's reaction on any comment in it:

   - Last comment from a reviewer, no reply from Jason: normal verification path.
   - Last comment from Jason: his text is a **directive**. Verify against it; don't re-argue a point he's already settled.
   - Jason replied, then the reviewer replied again: **live disagreement**. Carry both positions to Gate 1; never pick a side unasked.
   - A reaction from Jason and no reply from him: read it per the vocabulary in `library/references/review-reaction-signals.md`. If Jason also replied, text wins unless the reaction sits on a comment that comes *after* his last reply, which is the fresher signal and makes it a live disagreement (comment order is derivable from position in the thread; reaction timestamps cannot be trusted for this, see the reference doc). Two or more of Jason's signals on the same comment collapses to ๐Ÿ˜• Unclear: surface both at Gate 1, don't pick.

2. **Review bodies**: `gh pr view {pull_number} --json reviews`; a `CHANGES_REQUESTED` or `COMMENTED` review body often carries file-level or cross-file asks that never became threads. Extract each actionable ask as its own item. Review bodies carry no reaction signal, since one reaction can't disambiguate which of several extracted asks it means; if a body has a reaction from Jason, note it and move on rather than guessing which ask it applies to.

Record the login of every author whose asks are in scope: the first comment of each unresolved thread and each review body extracted above. Step 4.5 and Step 6 read this list to decide whether the re-review is posted.

If there is nothing to handle, say so and stop.

## Step 3: Verify each request independently

For every item, read the actual code on the branch (not the diff snippet in the comment) and test the claim: does the bug exist, does the suggested change actually improve things, does it contradict an established project convention (`CLAUDE.md`, `.claude/**/*`, `docs/`)? Where a claim is checkable by running something (a test, a typecheck, a quick script), run it rather than reasoning about it.

A signal from Step 2 sets a prior, never a shortcut: it settles *whether* to fix, not the need for ground truth to write an accurate fix and an accurate reply. Verification still runs on every item.

- ๐Ÿ‘ Accept, โค๏ธ Pattern, ๐Ÿ˜„ Missed it: prior is Valid. Verify anyway.
  - Evidence agrees: proceed as Valid, note the source.
  - Evidence disagrees: don't silently obey the reaction and don't silently ignore it. Record the conflict and raise it at Gate 1 rather than resolving it either way.
- โค๏ธ Pattern, once the fix is confirmed valid: sweep forward for other sites where the same **solution** would apply (currently-fine code that could take the same fix). Name them in the reply.
- ๐Ÿ˜„ Missed it: the elevated-scrutiny signal. Sweep backward for other sites carrying the same **defect** (currently-broken code with the same latent bug), and note why the class was missed. Always its own subagent, never folded into a cluster (see the dispatch rule below).
- ๐Ÿ‘Ž Reject: prior is Invalid. Verify anyway; if the reviewer turns out right, say so rather than forcing the prior.
- ๐Ÿš€ Defer: prior is Valid but out of scope.
- ๐ŸŽ‰ Done: no fix, no sweep. Confirm the claim (it should already be handled) and plan a short acknowledgement reply only.
- ๐Ÿ‘€ Scrutinise: no prior. Verify at full depth, never shortcut, always its own subagent.
- ๐Ÿ˜• Unclear, or two conflicting signals on one comment: no prior. Don't guess; plan a Gate 1 question for Jason instead of a verdict.

**When there are 4 or more items to verify, dispatch one read-only subagent per item** (or per tightly-related cluster, when several items point at the same code) instead of working the loop sequentially. ๐Ÿ‘€ and ๐Ÿ˜„ items are never clustered: each gets its own subagent regardless of what else they'd otherwise group with. Each subagent gets the item's thread/review-body text, its signal and prior (if any), the branch context needed to locate it, and returns: the classification below, the `file:line` evidence, a minimal proposed fix description and any regression test needed. Subagents verify only; none of them edits a file: that stays in Step 4, after Gate 1, so two proposed fixes never collide in the same file before you've seen both. Pass `model: "sonnet"` on each verification subagent, except ๐Ÿ‘€ and ๐Ÿ˜„ items, which take the session model (no override). Below 4 items, verify directly; dispatch overhead outweighs the saving.

Classify each item:

- **Valid**: the request holds; plan a concrete fix.
- **Valid but out of scope**: real, but belongs in a follow-up (new feature ask, pre-existing issue this PR didn't cause). Plan a deferral reply naming where it's tracked (offer to add it to the roadmap if one exists).
- **Invalid**: the request doesn't hold. Record the specific counter-evidence (`file:line`, test output, convention citation); this becomes the pushback reply.

## Gate 1: Triage approval

Present a triage table: thread reference, one-line summary of the ask, verdict, a **Source** column (`๐Ÿ‘ reaction`, `reply`, `verified`, or a combination), evidence and the planned action (fix description / deferral / pushback / acknowledgement). Below the table, three blocks when non-empty: signal-versus-evidence conflicts, ๐Ÿ˜• questions for Jason and live disagreements (reviewer and Jason both replied after each other). **Stop and await approval.** Adjust verdicts the user overrules; they may know context the code doesn't show.

## Step 4: Implement the approved fixes

1. Fix each approved item. **Follow-up commits only; never amend or rebase commits the reviewers have already seen.** One carve-out for stacked PRs (see `~/.claude/library/references/stacked-prs.md`): when this PR sits above a layer that just changed, the cascading rebase (`gh stack rebase --upstack`, then `gh stack push`) is stack maintenance, not history rewriting; the reviewed content survives and only parentage changes. `pr-land`'s Step 1.1 rebase of a PR onto main, for a roadmap-dashboard failure only, is the second exemption on the same reasoning; this skill never performs it. The prohibition protects this PR's own commits from being reworded, squashed or dropped.
2. Discover the project's test, typecheck and lint commands from `package.json` (or ecosystem equivalent) and run all three until green. A red gate never proceeds to commit.
3. Once every approved item is fixed and the gate is green, invoke the `commit-batch` skill to split the fixes into granular commits and push. Its own plan-approval pause applies as normal: that pause belongs to `commit-batch`, not this skill; don't suppress it and don't treat it as a substitute for Gate 2.

## Step 4.5: Inner re-review

The next strict review round would mostly find nits in the fixes just pushed. Find them now, in this round, so the user reads one consolidated result instead of opening a session per round.

Once `commit-batch` has pushed, dispatch one subagent (the `Agent` tool, session model, no override) with fresh context: it loads `pr-review-dry_run` in `strict` mode against the PR and gets, as follow-up context, the prior findings handled above (path, line, body and which Step 4 fixed). It reads the PR, its description and the repo and nothing of this session's reasoning, which is what keeps the review adversarial rather than a check of its own work. It returns the structured findings and verdict; it posts nothing.

- Every ๐Ÿ”ด, ๐ŸŸ  or ๐ŸŸก it raises goes through Step 3's verification like any reviewer's ask (a subagent can be wrong too). Fix the ones that hold, re-run the Step 4 gate, invoke `commit-batch` again, then re-dispatch.
- Two inner rounds at most. A finding still open after the second is carried to Gate 2 as a deferral, with the reason, so the user decides.
- An Approve verdict, or only ๐ŸŸฃ findings, ends the loop.
- A finding you reject is carried to Gate 2 with its counter-evidence, like a pushback reply, so the user sees what the re-review said and why it was not acted on.

Nothing is posted during the loop. Whether the converged round is posted to GitHub at all is Step 6's decision, by who wrote the original review.

## Step 5: Draft the replies

One reply per item, written per the writing-style skill's rules (no em-dashes, no contrastive couplets, lead with specifics):

- **Fixed**: what changed and the commit SHA that carries it. One or two sentences; the diff speaks.
- **Deferred**: acknowledge the point, say where it's now tracked and why it's out of this PR's scope.
- **Pushback**: the evidence, politely: what the code actually does, with `file:line` citations or test output. State the disagreement plainly and leave the door open in your own words, matched to the thread's tone; never a stock closing phrase. The reviewer decides whether to press.
- **Pattern (โค๏ธ)**: what changed, the commit SHA and the other sites found that would benefit from the same fix. If none were found, say that the sweep came up empty rather than omitting it.
- **Missed it (๐Ÿ˜„)**: what changed, the commit SHA, the other sites found carrying the same defect (or that the sweep came up empty), and why the class was missed in the first place.
- **Done (๐ŸŽ‰)**: a short acknowledgement that the concern is already handled. No fix, no sweep, but still a reply: silence isn't the signal Jason gave.

Threads carrying a live disagreement or a ๐Ÿ˜• question are not drafted as replies here; they wait on Jason's Gate 1 answer.

## Gate 2: Reply approval

Show every drafted reply against its thread, then the inner re-review's outcome: each finding it raised with fixed (commit SHA), deferred (reason) or rejected (counter-evidence), and whether Step 6 will post it. When it will, say so plainly: approval also posts a fresh `pr-review strict` run on the final head, which recomputes its findings and re-runs the suite, so the posted review can differ from the round shown here. **Stop and await approval.** Nothing has been posted yet; edits here are free.

## Step 6: Post

Every body goes to GitHub from a file, never from a quoted string on the command line: apostrophes get stripped from quoted bodies, and a word inside one (`git`, in a month of git work) can trip the permission classifier. The `gh` guard hook refuses an inline body regardless, so writing the file first is the only path that runs. Files live in the session's scratchpad directory, never `/tmp`.

- Thread replies: write the reply to `<scratchpad>/reply-{comment_id}.md`, then `gh api --method POST repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies -F body=@<scratchpad>/reply-{comment_id}.md`, where `comment_id` is the `databaseId` of the thread's first comment.
- Review-body asks with no thread: one consolidated reply, quoting each ask above its answer, via `gh pr comment {pull_number} --body-file <scratchpad>/comment.md`.
- **Never resolve threads**; the reviewer closes their own threads when satisfied.
- **The inner re-review, only when the user was the original reviewer.** When every author recorded in Step 2 is the authenticated login (the self-review case: the user's own `pr-review` runs, or a hand-written review of their own PR), run `pr-review strict` on the PR now. When the original was a `pr-review` post, its follow-up mode finds the marker and opens with the "Since my last review" delta, so GitHub carries the same history the manual loop produced; a hand-written original carries no marker, so the posted round is a cold strict review of the current head with no delta. When any handled ask came from another login, post no review: the human reviewer re-reviews their own asks, and the re-request below is the hand-off. The terminal already has the converged findings either way.

Finish by reporting: items fixed / deferred / pushed back, inner rounds run and what each found, commits pushed, replies posted, review posted or withheld and why. When a review was posted, name any finding in it that was absent from the last inner round, since the user approved that round and not this one. Offer to re-request review from the reviewers whose requests were addressed (`gh api --method POST repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers -f 'reviewers[]=...'`); offer, don't do it unasked.

## Red flags

**Never:** implement a request without verifying it first; dismiss a request without `file:line` evidence; treat a reaction as ground truth or resolve a signal-versus-evidence conflict without surfacing it at Gate 1; read a reaction from anyone but Jason as a signal; amend or force-push over reviewed commits (two exemptions: a stack's cascading rebase via `gh stack rebase` + `gh stack push`, and `pr-land` Step 1.1's rebase onto main for a roadmap-dashboard-only failure; both re-parent without rewording, squashing or dropping commits); post anything to GitHub before Gate 2 approval; post a body from an inline `-f body=` or `--body` string; run more than two inner re-review rounds; post a re-review on a PR whose handled asks came from anyone but the authenticated login; resolve a reviewer's thread; push with a red test/typecheck/lint gate.

<raw-arguments value="$ARGUMENTS" />