Use after a PR has been labeled `first pass reviewed` for at least 4 minutes — verifies CI is still green and no new commits landed since the label, aggregates outstanding non-blocking observations, posts an approval comment, replaces the label with `approved`, merges, and labels every issue referenced by `Closes #N` with `merged to dev`.
Scanned 5/27/2026
Install via CLI
openskills install normalled/apijack---
name: final-review
description: Use after a PR has been labeled `first pass reviewed` for at least 4 minutes — verifies CI is still green and no new commits landed since the label, aggregates outstanding non-blocking observations, posts an approval comment, replaces the label with `approved`, merges, and labels every issue referenced by `Closes #N` with `merged to dev`.
---
# Final Review
Phase 4 of the apijack issue workflow. The first-pass reviewer leaves a PR with `first pass reviewed`. After a 4-minute soak window — long enough for a human to intervene, short enough to keep the loop moving — the final reviewer takes the PR across the line.
## When to Use
- A PR is labeled `first pass reviewed` and the label has been there for at least 4 minutes
- CI is fully green and the PR is mergeable
- No new commits have landed on the head since the first-pass-reviewed label was applied
This skill is invoked automatically by the `final-review` cron, or manually via `/final-review <pr>`.
## Steps
### 1. Verify the gate is still satisfied
The cron's `prompt_cmd` already gates on label-applied-time and head-vs-label staleness. Re-verify the rest before any write, **and capture the head SHA** — step 6 will pin the merge to it so a commit landing mid-flow can't be merged in unreviewed:
```bash
HEAD_SHA=$(gh pr view <pr> --json headRefOid --jq '.headRefOid')
gh pr view <pr> --json state,mergeable,mergeStateStatus,labels
gh pr checks <pr>
gh api "repos/normalled/apijack/pulls/<pr>/reviews" \
--jq 'map(select(.state == "CHANGES_REQUESTED" and .dismissed_at == null)) | length'
```
Abort and STOP — without writing anything — if any of:
- PR state is not `OPEN`
- `first pass reviewed` is not currently on the PR
- Any check status is `fail`, `pending`, `queued`, or `in_progress`
- `mergeable` is `false` or `mergeStateStatus` indicates conflicts
- Any active "request changes" review (the count above is non-zero)
If the gate fails, leave a brief comment naming the failed check via `scripts/pr-comment.sh <pr> <body-file>` and stop.
### 2. Read the prior first-pass-review body
```bash
gh api "repos/normalled/apijack/pulls/<pr>/reviews" \
--jq 'map(select(.body // "" | startswith("> 🤖 **Automated review by claude**")))
| sort_by(.submitted_at) | reverse | .[0].body // empty'
```
Extract the bullets inside `<details><summary>Non-blocking observations</summary> ... </details>`. These are the items to surface in the approval comment. Ignore the `Nitpicks` block — too noisy for this summary.
### 3. Cross-reference open issues
For each non-blocking observation, check whether an open issue already tracks it:
```bash
gh issue list --repo normalled/apijack --state open --limit 50 --json number,title,body,labels
```
Match by topical keywords from the observation (function names, file paths, behaviors). If a single open issue clearly covers the bullet, append `(#NN)`. If unclear or no match, leave the bullet alone — do **not** invent issue links.
### 4. Build the approval body
Use the `Write` tool — never inline in a heredoc — to avoid backtick-escaping artifacts.
Write the body to `.claude-jobs/review-bodies/final-<pr>.md`. Then:
```bash
.claude/skills/review-issue/scripts/post-review.sh <pr> comment .claude-jobs/review-bodies/final-<pr>.md
```
You don't need to **literally** approve the PR. Just post a comment that _says you approve_. The workflow signal is the `approved` label moved in step 5.
#### Body template
```
> 🤖 **Final review by claude** — generated by the `final-review` skill. CI is green, no blockers, soak window elapsed. Marking approved and merging.
Approved.
### Outstanding observations from first-pass review
- <bullet copied verbatim from the first-pass body> [optional `(#NN)` link if a tracking issue exists]
- <next bullet>
```
If the first-pass review had no `Non-blocking observations` block at all, omit the `### Outstanding observations` heading entirely — keep the body to two short lines.
### 5. Move the label
```bash
.claude/skills/review-issue/scripts/set-review-label.sh <pr> "approved"
```
This atomically removes `first pass reviewed` and adds `approved`. Other PR labels are untouched.
### 6. Merge (SHA-pinned)
```bash
.claude/skills/final-review/scripts/merge-pr.sh <pr> "$HEAD_SHA"
```
The script pins the merge to `$HEAD_SHA`, so a commit landing between step 1 and step 6 causes GitHub to reject the merge with 409. **If the script exits non-zero, STOP** — do not retry. The cron will pick the PR up again next tick after the new commit gets a fresh first-pass review.
### 7. Label referenced issues with `merged to dev`
```bash
.claude/skills/final-review/scripts/label-merged-issues.sh <pr>
```
Parses the PR body for `Closes #N` / `Fixes #N` / `Resolves #N` and adds the `merged to dev` label to each referenced issue. The label marks the issue as shipped to dev, pending release to main. Idempotent and best-effort — per-issue failures are logged but do not fail the run.
### 8. Stop
This skill does NOT invoke `next-deployer`. Publishing to the rolling `next` channel is a separate concern; if you want it automated, that belongs in its own cron.
## Red Flags — Do Not Approve
- "CI has one flaky test, the rest pass" → STOP, leave a comment, ask a human
- "There's a `request-changes` review but it's old / unresolved" → STOP
- "The first-pass label is only 8 minutes old, close enough" → STOP, the cron will catch it next tick
- "There's a commit right after the first-pass label, but it's just a typo fix" → STOP, the first-pass review is stale; force a re-review
- "There's no prior first-pass body to read" → STOP, the cron should have filtered this out; abort defensively
## Output
Either: an approving review + merged PR + deleted branch, or: a comment naming the failed gate and STOP.
No comments yet. Be the first to comment!