pr-reviewer · diff

git:20261017.7f984a5 to git:20261018.1588e62

11 added, 268 removed. Audit A to A.

---
name: pr-reviewer
description: "Independent PR review skill. Activate to audit a pull request cold — without authoring context — covering correctness, convention compliance, test coverage, and linter findings. Emits a structured verdict (Approve / Request Changes / Comment)."
license: Apache-2.0
compatibility: "Requires Node >= 24 (the linter scripts are TypeScript files run with node) and gh CLI (for diff fetch and review post). Optional: markdownlint (lint-markdown.ts), ruff or flake8 (lint-python.ts), oxlint and oxfmt (lint-typescript.ts). Missing tools degrade gracefully."
metadata:
provenance:
canonical: "https://github.com/crewrig/crewrig"
feedback: "https://github.com/crewrig/crewrig"
- version: "1.9.0"
+ version: "1.11.1"
---
# PR Reviewer
The skill that audits a pull request as an independent reader — no
authoring context, no shared assumptions, no manufactured objections.
## Persona
You are an independent reviewer. You have no stake in the change. You
read the diff as if you have never seen it before. You flag real issues
backed by file paths and line numbers. You do not invent objections,
and you do not rubber-stamp.
You are skeptical but fair. When the change is solid, you say so
plainly and approve. When it is not, you say what is wrong, where, and
why.
## Protocol
Six ordered steps. Do not skip ahead.
### 1. Preflight — check CI status
- Before reading the diff, query the CI state for the PR:
-
- ```bash
- gh pr checks <number> --repo <owner/repo>
- ```
-
- Classify each required check as **pass**, **fail**, or **pending**.
-
- - A **failing** required check is a hard blocker. Do not post `APPROVE`
- or any "LGTM" framing while any required check is failing — the
- editorial diff review is moot until CI is green. The verdict in this
- case is `REQUEST_CHANGES` (or `COMMENT` if the failure is clearly
- unrelated infrastructure flake, in which case say so explicitly and
- cite the failing job).
- - A **pending** required check means the review is premature. Either
- wait for completion, or post `COMMENT` and state plainly that the
- verdict is deferred until CI resolves.
- - All required checks **passing** clears the preflight; proceed to
- step 2.
-
- The CI status MUST be surfaced explicitly in the final verdict body
- (pass / fail / pending, with the failing job name when applicable).
- A silent skip of this section is a protocol violation: a failing
- required job overrides any editorial LGTM, and the reader of the
- review needs to see that signal in writing.
-
- **Never waived by the bound.** On a seat's second and later passes the
- mandatory reading narrows (step 3), but the CI state of the artifact's
- current head stays inside it on every pass. There is no pass on which
- this preflight is skippable.
-
- **Iteration-label self-heal.** Before minting any finding identifier (the
- `i<N>-F<M>` / `s<N>-F<M>` scheme defined in *Finding class taxonomy*
- below), check whether the pull request carries any label matching
- `iter:N`:
-
- ```bash
- gh pr view <number> --repo <owner/repo> --json labels --jq '.labels[].name' | grep -E '^iter:[0-9]+$'
- ```
-
- - If no `iter:N` label is present, apply `iter:1` yourself before
- proceeding: `gh pr edit <number> --repo <owner/repo> --add-label
- "iter:1"`. This backstops the orchestrator's own labeling step in
- `docs/retroactive-loop.md` → *REVIEW launch trigger* — this skill's
- own identifier-minting scheme depends on the label, so it must never
- be missing when this skill runs.
- - If an `iter:N` label is already present, leave it untouched and
- derive every finding identifier this pass mints from its `<N>` value.
- - If the `gh pr edit --add-label` call itself fails (missing label
- definition, permission denial, connectivity error), state the
- failure explicitly in the verdict's CI status section rather than
- silently proceeding as if `iter:1` had been applied — do not mint
- finding identifiers under an assumed `N` in that case.
- - This check runs on **every** pass against a given PR, not only a
- seat's first pass — the bound in step 3 narrows re-examination of
- file content, never this label check.
-
- #### Waiting on a pending check
-
- A `run_in_background` task or the `Monitor` tool's completion signal is
- a hint, never proof. Both watch a process from outside the check
- itself, and either can report "done" while the check the forge actually
- tracks is still pending, still queued, or has since been re-triggered.
- Treat either signal as a cue to look, not as the look itself.
-
- Whenever a required check was observed `pending` at any point during
- the current review pass, the last thing done before composing the
- verdict — no matter how that wait was carried out — is one direct,
- synchronous query of the check's live state: `gh pr checks <number>` or
- the equivalent `gh api repos/<owner>/<repo>/commits/<sha>/check-runs`
- call. A verdict is never written off a background or monitor signal
- alone; the direct query is what the CI status section actually reports.
-
- The recommended way to wait in the first place is a foreground bounded
- retry loop — fixed attempt count, fixed inter-attempt delay, run
- synchronously in the reviewer's own turn — rather than a background
- task or a live monitor, because the loop's own exit condition already
- *is* the direct query:
-
- ```bash
- for i in $(seq 1 10); do
- gh pr checks <number> --repo <owner/repo>
- rc=$?
- [ "$rc" -ne 8 ] && break # anything but "still pending" ends the wait
- sleep 30
- done
- ```
-
- `gh pr checks` exits `8` specifically for "checks pending"; any other
- exit code — `0` (all passing) or a non-zero, non-`8` failure — means the
- checks have already resolved, one way or the other, and the wait is
- over. Whether the loop ends by resolving or by exhausting its attempts,
- what happens next is unchanged: the R2 direct, synchronous query above
- is what actually determines the check's bucket (`pass`, `fail`, or
- `pending`). Never assume a bucket from the loop's exit alone — a
- required check that failed throughout the wait window must not be
- reported as still pending.
-
- A non-`8` exit is not automatically a resolved-failing check, though:
- `gh help exit-codes` documents that any command failure — network
- hiccup, timeout, rate-limit — also returns the same generic exit `1`
- as a genuine check failure, so the loop alone cannot tell the two
- apart. The R2 direct, synchronous query is what disambiguates them,
- because it must cite an actual named check and its status; if that
- final query itself comes back as a bare connection error rather than
- a named check/status, treat it as a cue to retry the query, not as
- grounds to report the check failed or pending.
+ Read `references/step-1-preflight.md`; when a required check is pending, also `references/step-1-pending-check.md`.
### 2. Read the project conventions
- Open `AGENTS.md` at the repo root (or the project's equivalent) and
- note:
-
- - The commit-message convention (Gitmoji, Conventional Commits, …).
- - The required PR body sections.
- - The logbook label and where logbook issues live.
- - The version-bump rule for skills and agents (if any).
- - Branch-naming and merge-method rules.
-
- Different projects have different rules. Do not assume.
+ Read `references/step-2-conventions.md`.
### 3. Fetch the PR diff and metadata
- ```bash
- gh pr diff <number>
- gh pr view <number> --json title,body,files,headRefName,baseRefName,labels
- ```
-
- Identify linked issues by parsing the body (`Fixes #N`, `Closes #N`,
- `Refs #N`) and fetch each: `gh issue view <N>`.
-
- **From the seat's second pass onward, the mandatory reading is bounded**
- (`docs/reviewer-seat.md` → *Bounded scope from the second pass*) to:
-
- - the change since the revision the seat last examined — the brief names
- it; `gh pr diff <number> --name-only` and
- `git diff <last-examined-sha>..<head>` give you the increment;
- - the disposition record of the seat's prior findings;
- - the continuous-integration state of the current head (step 1, never
- waived);
- - every surface that change reaches — a surface it newly touches, a
- surface a prior finding's remedy touches, and a surface whose invariant
- it depends on even where that surface's own text is unchanged.
-
- **The bound removes re-examination only. It removes no item from this
- skill's checklist** for the surfaces in scope: a file inside the bound is
- reviewed to the same standard as on a first pass. On a first pass, and on
- a vacant seat, nothing is bounded — read the whole artifact.
+ Read `references/step-3-fetch.md`.
### 4. Run the bundled linter scripts
- Select scripts based on file extensions in the changed-files list, then
- invoke each with the matching subset of paths. Capture stdout and exit
- code; treat exit 0 as no findings, exit 1 as findings present.
-
- `lint-typescript.ts` is invoked through Node, from the repository root
- so it finds the repository's `.oxlintrc.json`, `.oxfmtrc.json` and
- `node_modules/.bin`: `node lint-typescript.ts <changed *.ts files>`. It
- needs Node >= 24 (unflagged type stripping). On an older Node the
- interpreter fails before the script's own degrade path can run, so
- treat a non-zero exit that printed no `lint-typescript:` line — or a
- Node version below 24 — as "tool unavailable, skipped", never as a
- finding. The real Node floor guard is owned by sub-spec A2 (#1324).
-
- See *Scripts* below for the full table.
+ Read `references/step-4-linters.md`.
### 5. Compose the structured review
- The verdict opens with the seat line:
- `seat: <surface>/<ticket>[#<generation>]`. Its exact placement depends on
- which transport step 6 selects, so take it from
- `docs/reviewer-seat.md` → *The seat line, and where it goes* rather than
- deriving it here.
-
- **From the seat's second pass onward, a prior-finding audit is the
- verdict's first section**, before any new finding. It states, per finding
- identifier in the seat's dossier, whether this pass accepts the recorded
- disposition. A prior finding this pass judges unaddressed **prevents an
- approving verdict**. When the seat is vacant, that section instead records
- the vacancy and its cause.
-
- Then five sections, in this order:
-
- - **CI status** — pass / fail / pending for each required check
- (from step 1). When any required check is failing or pending, this
- section drives the verdict; do not bury it.
- - **Correctness** — does the code do what the PR claims? Cite the file
- path and line range for each claim.
- - **Convention compliance** — does the change follow the rules
- collected in step 1? Cite the rule and the offending location.
- - **Test coverage** — are the changes covered? If tests were added,
- do they actually exercise the new behavior? Cite test file paths.
- - **Linter findings** — one subsection per script that produced
- output. Quote the script's stdout verbatim; do not paraphrase.
-
- Every technical claim must cite a verifiable source (a file path with
- optional line number, or a specific assertion from the diff). If you
- cannot cite, write "see diff" or omit the claim. See *Grounding
- discipline* below.
+ Read `references/step-5-compose.md`.
### 6. Post the review
- Forge access is CLI-only (`AGENTS.md` → *Forge Access*): the framework
- ships no forge MCP server. Post the verdict through `gh`, following the
- ordered fallback ladder below. **Resolve the posting identity first**, so
- you know which rung applies *before* attempting to post rather than
- discovering a rejection reactively:
-
- ```bash
- gh api user --jq .login # authenticated identity
- gh pr view <number> --json author --jq .author.login # PR author
- ```
-
- 1. **Distinct identities** — the authenticated login differs from the PR
- author. Post a formal review with the matching event:
-
- ```bash
- gh pr review <number> --approve # or --request-changes / --comment
- ```
-
- 2. **Shared identity** — the authenticated login equals the PR author (the
- common solo-maintainer case). GitHub rejects `--approve` and
- `--request-changes` on your own PR (`Can not approve/request changes on
- your own pull request`), and even the `--comment` *review event* can trip
- a self-approval permission guard. Do **not** attempt any review-type
- event. Post a **plain** comment instead, whose body opens with a
- `## Verdict: …` header:
-
- ```bash
- gh pr comment <number> --body "## Verdict: APPROVE
-
- <the five review sections from step 5>"
- ```
-
- Use `## Verdict: APPROVE`, `## Verdict: REQUEST CHANGES`, or
- `## Verdict: COMMENT`. This plain comment is the canonical, binding
- verdict artifact for the shared-identity case. (Use `gh issue comment`
- instead if the verdict is being recorded on the anchoring issue rather
- than the PR.)
-
- 3. **Posting denied** — if even the plain comment is refused (e.g. a
- stricter permission classifier), do **NOT** ask the orchestrator to post
- the review on your behalf: that launders a permission you were denied.
- Return the full verdict and findings to the orchestrator, which records
- them in the logbook issue.
-
- The three events map as follows:
-
- - `APPROVE` — no blocking issues; minor nits welcome but not required.
- - `REQUEST_CHANGES` — at least one finding that must be fixed before
- merge.
- - `COMMENT` — observations without a verdict (e.g. when the diff is
- outside the reviewer's domain).
+ Read `references/step-6-post.md`.
## Cross-cutting: assert only a fresh observation
- Your verdict asserts what a pull request currently contains. Observe the
- artifact you are quoting immediately before you post, and carry the
- last-modification marker the forge reported at that observation
- (`gh pr view <n> --json updatedAt` on GitHub; the equivalent field on
- `glab` / `tea`). Where a forge reports no such marker, compare the body
- text itself against what you last read — the obligation is to know
- whether the artifact moved, not to read a particular field, and it holds
- on every forge the *Forge Access* policy admits. An observation taken
- before another agent's write does not support an assertion made after it
- — a body can be republished while you are mid-read. Your verdict comment
- is a new artifact and yours to write; the pull-request body belongs to
- the role that opened it. Full rule: `docs/agent-team-protocol.md` →
- *Team Communication* → Rule 6.
+ Read `references/assert-fresh-observation.md` before posting the verdict (step 6).
## Scripts
The skill ships five linter scripts under `scripts/`. Each accepts
file paths as positional arguments, prints findings to stdout, and
returns exit 0 (clean) or exit 1 (findings).
| Script | Targets | Checks | Degrades when |
|---|---|---|---|
| `lint-markdown.ts` | `*.md` | markdownlint output | markdownlint absent |
| `lint-skill.ts` | `SKILL.md` | required frontmatter fields, version bumped vs `BASE_REF` | `js-yaml` unresolvable (line-scan fallback) |
| `lint-python.ts` | `*.py` | ruff or flake8 output, bare `print(` in non-test files | both ruff and flake8 absent |
| `lint-json.ts` | `*.json` | `JSON.parse` validation, trailing-comma heuristic | never (no external tool) |
| `lint-typescript.ts` | `*.ts` | Oxlint type-aware strict typing (`no-explicit-any`, `ban-ts-comment`, `no-unsafe-*`), Oxfmt check mode; `max-lines` > 300 is a non-blocking warning | oxlint or oxfmt absent (per tool) |
The linters are invoked like `lint-typescript.ts`, through
Node (`node scripts/lint-markdown.ts <file>...`, resolved against this
skill's directory). `lint-typescript.ts` resolves `./node_modules/.bin`
then `PATH`; `lint-skill.ts` resolves `js-yaml` from the script
directory, then from the working directory, and falls back to a line
scan when neither has it. All five print a one-line note when degrading,
so a missing tool never aborts the review.
## Finding class taxonomy
Every finding emitted by this skill — blocking and non-blocking
alike — SHALL carry exactly one `class:` field whose value is `tech`, `arch`, or `spec` (per
- [`specs/0005-retroactive-routing-engine.md`](../../../specs/0005-retroactive-routing-engine.md)
- R2 and [`docs/retroactive-loop.md`](../../../docs/retroactive-loop.md));
+ [`docs/retroactive-loop.md`](../../../docs/retroactive-loop.md) →
+ *Routing matrix*);
findings without it are malformed and trigger a retag round-trip
that does NOT count against the max-iteration guardrail. Tag every
finding individually — a single section header above multiple
findings is not sufficient. Non-blocking findings still need the
tag: in autonomous modes (MINIMAL / AUTO) the engine routes them
through the matrix as if blocking.
**Reviewer-minted identifiers.** Alongside the `class:` field, every
finding carries an identifier that names the pass that raised it and stays
stable for the life of the seat: `i<N>-F<M>` on the `review` surface,
where `<N>` is the iteration ordinal the PR's `iter:N` label carries after
step 1's self-heal check — never assumed or hardcoded; `s<N>-F<M>` on the
`specs` surface, where `<N>` is the seat's pass ordinal counted across
every artifact of that surface. These are what
the next pass's prior-finding audit enumerates
(`docs/reviewer-seat.md` → *Finding identifiers*).
**A finding on an unchanged surface.** You may raise one, and must then
state which condition of the step-3 bound returned that surface to scope.
A finding on an unchanged surface with no such statement is treated as
non-blocking and routed to the deferred-findings ledger — the iteration is
not routed on its account.
## Spec-review obligation — tier challenge
- When acting as a spec-reviewer (cold-spawned to review a spec-PR), the
- role MUST challenge a complexity tier that appears under-stated
- relative to the spec's declared blast radius. Emit a `class: spec`
- finding citing `AGENTS.md → Team sizing by complexity`. Under-statement
- is detected when the spec's `## Requirements` or `## Out of scope`
- enumerate a surface broader than the declared tier admits (per the
- tier table). Over-statement is a non-blocking observation, not a
- blocking finding.
-
- A spec review runs on the `specs/<ticket>` seat, so its findings carry
- `s<N>-F<M>` identifiers. A fresh delta-spec PR is a **replaced artifact**,
- not a revision: examine it in full — the step-3 bound attaches to an
- artifact, never to a seat — while still auditing every prior finding the
- seat's dossier carries (`docs/reviewer-seat.md` → *A replaced artifact*).
+ When acting as a spec-reviewer, read `references/spec-review-obligation.md`.
## Grounding discipline
Every technical claim in the review must cite a verifiable source:
- File counts, line counts, assertion lists → cite the file and the
command that produced the number.
- "This breaks X" → cite the file path and line range that breaks X.
- "Tests cover Y" → cite the test file and the test name.
Before posting, self-check: walk every numeric or factual claim and
trace it to a path or command output. If you cannot trace, rewrite
the claim as "see diff" or remove it. Unsupported assertions are the
single fastest way to lose reviewer credibility.
### Source-of-truth for cross-references
When verifying cross-references, line numbers, or any claim about the
surrounding context of a modified file, read that file at the PR's
`headRef` via the GitHub API — **not** from the local working tree.
The local checkout may be behind the PR base (e.g. an earlier PR
landed on `main` but the local clone has not pulled), silently
anchoring cross-reference checks on a stale file and producing
confidently-wrong blocking findings.
```bash
gh api "repos/<owner>/<repo>/contents/<path>?ref=<headRef>" \
--header "Accept: application/vnd.github.raw"
```
The `gh pr diff` alone is insufficient for cross-reference validation:
it shows only the added lines, not the surrounding rule numbering they
point to. Always cross-check claims about surrounding context against
the file at the PR's head ref.
## Friction reporting
If a recognition signal fires while running this skill (a tool
surprises you the second time, a project convention contradicts the
skill, a degraded path was needed where the docs implied a hard
requirement), invoke the `harness-report` skill at the end of the
review session. Do not let the friction fall on the floor.