---
name: pr-review
description: Adjudicate a pull request as a maintainer. Reads the CI-posted pinned review as the source of truth, refreshes it if stale, and provides an interactive workflow to approve / request changes / make changes / close — with optional auto-merge.
---

# Pull Request Review Command

This is the maintainer adjudication layer on top of the CI review pipeline (`claude-code-review.yml` posts a pinned `<!-- CLAUDE_REVIEW N/M -->` comment with all findings; this skill reads it as the source of truth).

## Usage

`/pr-review [<PR_NUMBER>] [--ai|--no-ai]`

- **PR number**: Optional. If omitted, the workflow infers from the current branch via `gh pr view --json number`. Errors out if no PR is open for the branch.
- `--ai` / `--no-ai`: force AI-suspect ON or OFF for this run.

Works with all PRs (internal, external, bots).

---

## Process

Complete all 10 steps in sequence. Display **[Step X/10]** before each step heading.

Steps 1, 2, 3, 5 are **silent** — no user-facing output. Step 4 is interactive only when the PR has infra changes. Step 6 is the first comprehensive output.

---

### Step 1: Detect contributor, trust axes, risk tier, and AI-suspect

If `{{arg}}` is empty, infer the PR from the current branch:

```bash
gh pr view --json number --jq '.number'
```

If that fails, abort with a clear error asking for an explicit PR number. Otherwise use the inferred number as `PR_NUMBER` for every `{{arg}}` reference below.

Run contributor detection:

```bash
bash .claude/commands/pr-review/scripts/contributor-detection.sh $PR_NUMBER [--ai|--no-ai]
```

The script outputs:

- `AUTHOR` — GitHub username
- `CONTRIBUTOR_TYPE` — bot/internal/external
- `ETIQUETTE_TRUST` — low/standard/high (controls tone, welcome language, merge defaults)
- `CONTENT_SCRUTINY` — standard/heightened
- `AI_SUSPECT` — true/false
- `AI_SUSPECT_REASONS` — comma-separated triggers
- `RISK_TIER` — typo/minor/standard/major/infra
- `PR_METADATA` — JSON with number, title, url
- `FILES_CHANGED` — list of changed file paths
- `LABELS` — comma-separated current labels (drives Step 2's pinned-review state machine)
- `PR_DATA_JSON` — complete PR data

Store all of these. **No user output yet.** Step 6 surfaces them in the unified package.

See `pr-review:references:trust-and-scrutiny`.

### Step 2: Fetch the pinned CI review and classify state

Fetch the diff and the pinned review:

```bash
gh pr diff "$PR_NUMBER"

bash .claude/commands/docs-review/scripts/pinned-comment.sh fetch --pr "$PR_NUMBER"
```

> **v3 surface guard (read before anything else in this step).** If the fetch output contains `<!-- CLAUDE_REVIEW_AUTHOR -->`, the PR is on the v3 surface and the rest of this skill's refresh machinery does not apply to it. The findings are the author card's blocking rows (🚨 / ❓, each with an `F<n>` id; dispositions live in its `REVIEW_STATE` block) plus the reviewer's guide's `### ⚠️ Check these before approving` rows — fetch the guide with `BRIEF_ID=$(bash .claude/commands/docs-review/scripts/pinned-comment.sh find --pr "$PR_NUMBER" --role brief)` and `gh api "repos/$REPO/issues/comments/$BRIEF_ID" --jq .body`. **Never run the local `docs-review:references:update` refresh or `pinned-comment.sh upsert` on a v3 PR** — they write a legacy monolith beside the cards. When a v3 review is `STALE`, comment `@claude #update-review` and wait for the card to re-render (the label returns to `review:outstanding-issues` / `review:no-blockers`), then continue from Step 4 with the card's rows as the findings. The Sentinel check on the PR is the merge gate; approve only when it is green or would be.

Determine the pinned-review state from labels and fetch output. **Labels alone are not a sufficient freshness signal**: pushes made by the Copilot coding agent or with `GITHUB_TOKEN` never fire the `pull_request: synchronize` event, so the `mark-stale` job never runs for them and a PR can sit at `review:no-blockers` while the pinned review describes content a later commit replaced (PR #20556). Before accepting `CURRENT`, run the SHA freshness check:

```bash
HEAD_SHA=$(gh pr view "$PR_NUMBER" --json headRefOid --jq .headRefOid)
# Preferred: the machine-readable sentinel compose-review.py stamps under the header.
REVIEWED_SHA=$(bash .claude/commands/docs-review/scripts/pinned-comment.sh fetch --pr "$PR_NUMBER" \
  | grep -oE '<!-- CLAUDE_REVIEW_HEAD [0-9a-f]+ -->' | tail -1 | grep -oE '[0-9a-f]{7,40}')
# Fallback for reviews composed before the sentinel existed:
[ -z "$REVIEWED_SHA" ] && REVIEWED_SHA=$(bash .claude/commands/docs-review/scripts/pinned-comment.sh last-reviewed-sha --pr "$PR_NUMBER")
```

If `REVIEWED_SHA` is non-empty and is not a prefix-match of `HEAD_SHA`, treat the state as `STALE` regardless of labels (and mention in your output that the label missed a push — likely a suppressed `synchronize` event). If no reviewed SHA is recoverable at all, fall back to comparing the header's `Last updated` timestamp against the head commit's committer date; when the head commit is newer, treat as `STALE`.

| State | Detection | What Step 3 does |
|---|---|---|
| `CURRENT` | `review:outstanding-issues` or `review:no-blockers` set; `review:stale` / `review:in-progress` / `review:error` absent; fetch returns body; **SHA freshness check passes** | Nothing — proceed to Step 4 |
| `STALE` | `review:stale` set | Legacy monolith only: refresh in place by invoking `docs-review:references:update` locally (re-runs claim verification against new commits, then writes via `pinned-comment.sh upsert`). v3 PR: comment `@claude #update-review` (see the guard above) |
| `IN_PROGRESS` | `review:in-progress` set | Wait briefly for the workflow to finish; re-check labels. If it stays >15 min, treat as `ERROR`. |
| `ERROR` | `review:error` set (or `review:in-progress` stuck) | Investigate the Actions logs before proceeding |
| `ABSENT` | Fetch returns no `<!-- CLAUDE_REVIEW -->` markers | Fall back: run a local review (see Step 3 §Absent path) |

Store the parsed pinned-comment findings (🚨 Outstanding, ⚠️ Low-confidence, 💡 Pre-existing, ✅ Resolved, 📜 Review history) for Step 6.

### Step 3: Resolve pinned-review state

Branch on the state from Step 2.

#### CURRENT

Continue to Step 4.

#### STALE

**v3 PR (author card present): do not refresh locally.** Comment `@claude #update-review`, wait for the card to re-render, then re-fetch and continue.

Legacy monolith: refresh the pinned comment in place by invoking `docs-review:references:update` locally with `PR_NUMBER` set. The update procedure re-reads the diff since the last reviewed SHA, classifies as Case 1/2/3, and writes the refreshed body via `pinned-comment.sh upsert`. When it completes, re-fetch the pinned comment and re-parse findings for Step 6.

#### ABSENT

No pinned comment exists. This typically means: the PR is a draft (CI doesn't review drafts), CI failed, or the `review:trivial` short-circuit fired. Ask the user how to proceed via AskUserQuestion:

1. **Run a local review now** — perform a full local style + code review (apply `/docs-review`). Use the findings as the pinned-review findings for Step 6.
2. **Adjudicate without findings** — proceed to Step 6 with no findings; rely on your own diff read and the contributor's PR description.
3. **Cancel** — exit; consider transitioning the PR to ready-for-review to trigger CI, or mention `@claude` to invoke a fresh review.

Only the local-review path produces findings; otherwise Step 6 renders an empty findings block.

### Step 4: Offer infrastructure deployment (only fires for infra changes)

If `RISK_TIER=infra` (PR contains dependency or infrastructure changes), follow `pr-review:references:infrastructure-deployment` to optionally trigger a pulumi-test.io deployment. This is the only step before Step 6 that produces user-facing output. For other risk tiers, skip silently.

### Step 5: PR description accuracy check (silent)

CI doesn't check this; pr-review does. Compare the PR description against the actual diff. Inaccuracies — files mentioned that weren't changed, changes described that aren't in the diff, significant changes omitted, incorrect characterization — are **trivial-fix candidates** that can be applied via `gh pr edit --body` in Step 9 if the user picks Make-changes-and-approve and doesn't veto them.

For each inaccuracy, draft a corrected description that accurately reflects the diff. Store for Step 6 + Step 9.

### Step 6: Present unified review package

This is the **first big user-facing output**. Render in this order, top to bottom:

1. **Confidence gauge** — single line:

   ```text
   Confidence: HIGH · 0 outstanding · 2 low-confidence · contributor: @user (internal) · risk: minor · CI: green · pinned: current
   ```

   Or, when AI-suspect:

   ```text
   Confidence: MEDIUM · 🤖 AI-suspect (allowlist + trailer:claude) · scrutiny: heightened · 2 outstanding · 1 low-confidence · CI: green · pinned: refreshed
   ```

   Computation:

   | Gauge | When |
   |---|---|
   | `HIGH` | No 🚨 Outstanding findings, CI green, scrutiny `standard`, pinned current/refreshed |
   | `MEDIUM` | Any ⚠️ Low-confidence findings, OR scrutiny `heightened` (always caps at MEDIUM), OR CI yellow, OR pinned absent |
   | `LOW` | Any 🚨 Outstanding finding, OR CI red |

2. **Header** — PR title, contributor with etiquette icon (🤖 / 📝 / 🌍), risk tier badge, pinned-review state, deployment URL (or "pending"). Run `test-deployment-guidance.sh` here to fetch the deployment URL and per-page links:

   ```bash
   bash .claude/commands/pr-review/scripts/test-deployment-guidance.sh {{arg}}
   ```

3. **Per-page review links** — direct links + change-aware specific review items from `test-deployment-guidance.sh` output.

4. **Pinned review findings** — render the parsed 🚨 Outstanding, ⚠️ Low-confidence, 💡 Pre-existing, and ✅ Resolved findings from Step 2 verbatim (they're already in the format from `docs-review:references:output-format`). If a refresh ran in Step 3, note "*Pinned comment refreshed at HH:MM*" above the findings block. If absent and the user picked local review in Step 3, render those findings here in the same format.

5. **PR description inaccuracies** (only if Step 5 found any) — itemized so the user can see exactly what would change before Step 8 confirmation:

   ```text
   PR description corrections (2):
     [1] Says "updates content/blog/foo.md" but foo.md was not changed
     [2] Omits significant change: content/docs/bar.md was renamed
   ```

   Each item gets a numeric index for veto in Step 8.

6. **Trivial-fix candidates** (only if any) — applied via Make-changes-and-approve per `pr-review:references:action-preview-templates`. Suppressed when AI-suspect; see action-preview-templates §AI-suspect override.

7. **Overall assessment** — single line: Clean / Minor issues / Issues found / Critical issues. Computed from the pinned 🚨 Outstanding count and any code-correctness findings. Pre-existing alone does not gate approval.

8. **Recommendations** — short, action-oriented. Map directly to a Step 7 menu (e.g., "→ Approve" or "→ Make changes and approve").

Render the whole package in one message.

#### Unresolved-findings check (part of Step 6's output)

Approving is the moment the review's findings stop being actionable, so say what is about to be merged over. Run the enumerator against the pinned comment:

```bash
python3 .claude/commands/docs-review/scripts/review-worklist.py --pr "$PR_NUMBER" --format json
```

Anything it lists that the PR thread shows no outcome for — no fix in the diff, no dispute, no filed issue, no stated reason — is being merged over silently. Report it as one line in the Step 6 package (`Merging over: 2 ⚠️ low-confidence, 4 ✏️ style suggestions`) and carry it into the Step 7 recommendation. Two rules:

- **The maintainer decides.** This is disclosure, not a gate. Merging over advisory findings is a legitimate call and 💡 Pre-existing never counts against a PR.
- **On your own PR, work them first.** When the PR is the maintainer's own (or one you authored in this session), `/address-review $PR_NUMBER` is the right move before adjudication — dispositioning the findings there produces the outcome record the post-merge scrape reads.

### Step 7: Present action menu

Use AskUserQuestion. Adaptive-scenario selection (which menu fires for which finding shape) and per-scenario options live in `pr-review:references:action-menus`. The Step 7 menu chooses *what* to do; auto-merge is decided in Step 8 via the merge toggle, never as a Step 7 option.

### Step 8: Preview action and confirm (with merge toggle)

See `pr-review:references:action-preview-templates`.

The preview shows:

- Chosen action
- Auto-merge toggle with computed default (per the toggle defaults in `pr-review:references:action-preview-templates`)
- For Make-changes-and-approve: file-by-file changes (PR description corrections + trivial fixes + suggested fixes from CI's pinned findings)
- The exact comment text that will be posted (using `pr-review:references:message-templates`)
- The full list of `gh` commands that will run

The posted comment must obey the voice/length rules in `pr-review:references:message-templates`. Step 6's local package is for the maintainer's eyes; the public maintainer comment is its own thing.

Confirmation-menu adaptation (slot 2 changes per pending action; dispute-path opt-in is described below) lives in `pr-review:references:action-preview-templates` §Confirmation Question.

#### Dispute path (opt-in)

When the user picks "Dispute finding(s)", AskUserQuestion prompts for the finding number(s) and the dispute reasoning. pr-review composes a mention body in this shape:

```text
[Maintainer dispute from @{{user}}]

Finding {{N}} (in {{file:line}}, {{summary}}): {{reasoning}}

Adjudicate per Case 2 dispute rules.
```

The body is fed to `docs-review:references:update` locally with `MENTION_BODY` populated. Update.md Case 2 takes over: classifies the dispute (domain-knowledge / verifiable / reframing), concedes or holds with citation, and re-renders the pinned comment via `pinned-comment.sh upsert`. Re-fetch the pinned comment afterwards so the Step 6 view reflects the resolution before the action proceeds.

Maintainer write-access is sufficient evidence for domain-knowledge disputes (per `docs-review:references:update` Case 2).

### Step 9: Execute confirmed action

Execute per the commands and workflow in `pr-review:references:action-preview-templates`, using the merge-toggle state confirmed in Step 8. For Make-changes-and-approve failures: always return to original branch before reporting error.

### Step 10: Report execution results

See `pr-review:references:execution-results`.

Workflow complete.

---

## Error Recovery

If any command fails during execution:

```text
❌ Failed to [action] PR #{{arg}}

Error: [error message]

Recovery options:
- /pr-review {{arg}} (re-run full workflow)
- Or use gh CLI directly: [relevant commands based on failure]
```

For Make-changes-and-approve failures: always return to original branch before reporting error.
