Files
claude-plugin/plugins/reviews/skills/review-pr/references/pr-review-field-guide.md
T
mroberts 129354cda8 Read hook payload from fd 0, not /dev/stdin
Claude Code delivers the hook payload on a socket. Opening it by path
('/dev/stdin' -> /proc/self/fd/0) fails with ENXIO, so bash-guard.mjs threw on
every invocation and its bare catch exited 0 silently. The guard looked like it
was never dispatched; it was dying on line 29 each time.

readFileSync(0) is read() on the descriptor with no open(), which works on a
socket. The catch now logs instead of swallowing, so this failure mode can never
again masquerade as non-dispatch.

Adds test-bash-guard.py, which drives the guard over a socketpair. A pipe would
not reproduce the bug, so the socket is load-bearing. Verified failing against
the pre-fix guard (silent, no output) and passing after.

Removes the five diagnostic probes and probe.mjs; they served their purpose.
Bumps to 1.0.2 because the plugin cache is keyed by version and an unchanged
version silently skips reinstall.
2026-07-21 10:24:06 -05:00

9.3 KiB
Raw Blame History

PR Review Field Guide

A repeatable process for reviewing PRs without losing the big picture. No AI required — just git, gh (optional), and an editor with go-to-definition / find-references. The core move: build a mental model first, then use the diff to verify it — never the reverse.


Phase 1 — Orient (before opening a single diff) · ~5 min

  1. Read the PR description and the linked issue. The issue is usually the truer statement of intent.
  2. Write a two-sentence hypothesis: what should this change touch, and where should the hard part live? Actually write it down — it's your anchor for the whole review.
  3. Skim only the changed-file list (not contents). Compare against your hypothesis:
    • Files you didn't expect → possible scope creep or coupling you didn't know about. Mark them.
    • Files you expected but don't see → possibly incomplete change. Mark those too.

If the description is empty and the file list is confusing, stop and ask the author for a summary. That's a review comment in itself.

Phase 2 — Get out of the diff viewer · ~2 min

gh pr checkout <number>        # or: git fetch origin <branch> && git switch <branch>

Open the repo in your editor. From here on, the web diff is a map you glance at — the territory is the checked-out code, where go-to-definition and find-references work.

Phase 3 — Find the load-bearing change · ~5 min

One change forces most of the others. Find it. Priority order of suspects:

schema / migration → public interface, type, or API contract → core algorithm → everything else

Test: if this change were reverted, would most of the rest of the diff become unnecessary? Read that file in full, in context — not the hunk.

Phase 4 — Read outward, in dependency order · bulk of the review

Contracts → core logic → mechanical ripples → tests. At each stop ask:

  • Does this change follow necessarily from the load-bearing one?
  • Is anything extra hiding here that isn't part of the stated intent?

Batch the mechanical stuff (renames, import churn, call-site updates, lockfiles) into one fast skim pass — just confirm nothing substantive is buried in it. Spending equal attention per file is how reviewers burn out and miss the real issue.

Phase 5 — Trace the blast radius · ~10 min

The classic integration bug: unchanged callers of changed code.

  1. For every changed public symbol (signature, type, endpoint, event, config key): find-references. Check each call site that is not in the diff — is it still correct under the new behavior?
  2. Grep for the old name / old pattern. Anything left over that should be gone?
  3. Check the seams the diff doesn't show: serialization boundaries, DB reads of migrated data, consumers in other services.

Phase 6 — Run it · ~10 min

  • Run the targeted tests for the changed area (not just CI-green — read what they assert).
  • Exercise the changed path once yourself — app, REPL, or debugger. Step through the load-bearing function with real values.
  • Trigger the error path at least once. Error handling is the least-reviewed, most-shipped-broken code there is.

Phase 7 — Step back (the holistic pass) · ~5 min

Close the diff. Answer from memory:

  • Does the whole accomplish the stated intent — and nothing meaningfully more or less?
  • Is there a simpler design that occurs to you now that you understand it? (Mention it; don't demand it.)
  • What breaks at 10× load / 10× data / concurrent use?
  • What is not tested that worries you?
  • Will someone (you) understand this in six months without the PR description?

If you can't answer these, the review isn't done — or the PR is too big, which is feedback in itself.

Phase 8 — Write the review

  1. Lead with your understanding: "My read: this does X by changing Y, with Z as the tricky part." If you've misread it, the author corrects the model, not just the comments.
  2. Separate blocking from take-it-or-leave-it, explicitly.
  3. Surface Phase 1 surprises and Phase 5 blast-radius findings — those are your highest-value comments, and the ones a hunk-by-hunk reviewer can't make.

Calibration

  • Small PR (< ~200 lines): Phases 1, 3, 5, 8 — maybe 15 minutes total.
  • Large PR (> ~800 substantive lines): do Phases 1–3, then tell the author what you're reviewing first and what needs a second pass — or ask for a split. Reviewing 2,000 lines in one sitting produces approval, not review.
  • Repeat offender friction (huge PRs, empty descriptions, tangled commits): fix upstream — PR templates, stacked PRs, self-review annotations by the author.

Appendix — this workflow in your Neovim

Grounded in your LazyVim config after the PR-review upgrades (verified against your pinned plugin sources plus the implemented brief). Two facts shape everything: your <leader>g* keys dispatch git vs. jj per-repo via plugins.custom.vcs.is_jj(), and your localleader is \, which all octo.nvim bindings hang off.

Getting the branch (Phase 2)

  • git repo: gh pr checkout <n> in a terminal (<c-/> toggles one at repo root).
  • jj-colocated repo: stay jj-native — jj git fetch, then jj new <head-branch>@origin. Your working copy @ is now an empty child of the PR head, which makes "diff against trunk" mean exactly "the PR."

The map (Phase 3–4)

<leader>gR is the review map: diffview against the resolved base (origin/HEAD, falling back to origin/main) in git repos, Jdiff trunk() in jj repos. Keep the distinction deliberate: <leader>gd stays your local working-tree diff — mid-review it shows nothing on a clean checkout, and that's correct, not broken. <leader>gD gives the same PR-vs-base content as a grouped hunks picker (it now shares gR's base resolution), useful when you want to sample hunks rather than walk files.

Reading path, blast radius (Phase 4–5)

<leader>ga seeds the arglist with the PR's changed files (both VCSs); prune and reorder with :args into the briefing's dependency order, then walk with :n / :prev. For blast radius, gr (references) populates the quickfix list — ]q / [q walk it everywhere now, including inside octo review tabs, reopen with <leader>xq. Any snacks picker sends results to quickfix with <c-q>. Project grep is <leader>sg (root) / <leader>sG (cwd); "is the old pattern really gone" sweeps go through <leader>sr (grug-far). History and blame dispatch too: <leader>gf / <leader>gF (file / repo history), <leader>gl / <leader>gL (log — J log in jj), <leader>gb (blame line — jj annotate in jj repos).

The GitHub layer — octo.nvim (Phase 8)

Octo now uses the snacks picker, and <localleader> = \. :Octo pr edit <n> opens the PR buffer (description, threads — Phase 1 orientation without leaving nvim). \vs starts the review, \vr resumes a pending one. In the review diff: \ca adds a comment (visual mode gives a range comment), \sa a suggestion, ]t / [t jump threads, ]u / [u jump unviewed files, \e focuses the file panel, \b toggles it. Changed-file navigation is ]o / [o (]O / [O for last/first) — remapped across review diff, thread view, and file panel so the quickfix keys stay yours. \vs again opens the submit window: <C-a> approve, <C-m> comment, <C-r> request changes; <C-c> closes the review tab.

The briefing pane

<leader>aR prompts for a PR number and launches your right-side claude terminal with /review-pr <n> already running; <leader>at toggles the same pane bare. Keep it open as the map and walk the reading path in the main window — <leader>ga then :args reorder takes the briefing's file list into the walk.

Cheat sheet

Task Git repo jj-colocated repo
Orient (desc + threads) :Octo pr edit <n> same
Get the branch gh pr checkout <n> jj git fetch → jj new <branch>@origin
PR-vs-base map <leader>gR same (dispatched → Jdiff trunk())
PR hunks picker <leader>gD same (dispatched)
Local working-tree diff <leader>gd same (dispatched)
Changed files → arglist <leader>ga same (dispatched)
Reading path :args reorder → :n / :prev same
Status <leader>gs same (dispatched)
File / repo history <leader>gf / <leader>gF same (dispatched)
Log / blame line <leader>gl, <leader>gL / <leader>gb same (→ J log / jj annotate)
Definition / references gd / gr (→ quickfix) same
Implementation / type / hover gI / gy / K same
Walk quickfix ]q / [q · list: <leader>xq same
Picker → quickfix <c-q> in any snacks picker same
Project grep <leader>sg (root) / <leader>sG (cwd) same
Old-pattern sweep <leader>sr (grug-far) same
Start / resume review \vs / \vr in PR buffer same
Comment / suggestion at line \ca / \sa (visual = range) same
Jump threads / unviewed files ]t [t / ]u [u same
Changed-file nav (octo) ]o [o · ]O [O last/first same
Submit \vs → <C-a> / <C-m> / <C-r> same
Close review tab <C-c> same
Launch review briefing (PR #) <leader>aR same
Briefing pane (toggle) <leader>at same

(\ = your localleader. Octo rows apply inside octo buffers; ]q/[q are quickfix everywhere, including review tabs.)