| 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 |
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:
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
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:
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:
.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
.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)
.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
.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.