Review PRs Skill
A single, repeatable pass over everything that needs PR-review attention right now —
the Perri daemon queue, which now includes a dedicated dependabot bucket for
bot-authored PRs alongside the human-review buckets.
Designed to run from a fresh context: each cycle is mostly self-contained and does not
need prior conversation history. Start it after a /clear (or in a fresh session) for
maximum room.
The Approval Principle (never violated)
Every action posted to GitHub — approve, comment, request-changes, merge — must be
covered by an explicit approval from the user, given in the same session. One
approval MAY cover a batch, provided the approval request clearly enumerates exactly
which PRs and what action it authorizes.
Corollaries:
- A batch approval is valid only when the request lists the exact PR numbers and the
exact action ("Approve these 5: #x, #y, #z, #a, #b?"). Vague asks ("approve everything?")
do not satisfy it.
- Comments are almost always 1:1 (the body differs per PR), so they go individually — but
the same rule holds: clear ask, explicit yes.
- Merges are actions too. If a group will be approved and merged (typical for green
dependabot PRs), the approval request must say so.
- Submissions that carry a comment or a request-changes go through the
submit-review
skill, which provides the per-PR confirmation UI. The batch shortcut (one confirmation →
loop the approvals) is reserved for trivial, clean, and green-dependabot
groups, where the action is a bare approve (± merge) with no comment.
Step 1 — Gather the queue
Single source — the Perri daemon queue is now the unified source of truth for all
PRs, including dependabot. Fetch it the same way perri.md's Startup does:
In Nostromo (MCP available): perri.list_pr_queue().
Otherwise (standalone): ~/.claude/bin/perri-queue-pane --json.
The field list is identical either way — perri.list_pr_queue()'s items match
perri-queue-pane --json's .items[] field-for-field, so Steps 2–4 below don't care which
source Step 1 used:
Parse the items: each has repo, number, title, author, bucket,
new_activity, url, ci_state, is_bot.
Bucket values:
"requested", "needs_review", "changes_req" — human-review PRs (unchanged)
"dependabot" — bot-authored PRs; is_bot == true (daemon-sourced, no separate fetch needed)
Do NOT run a separate gh search prs --author app/dependabot query.
The daemon (is_bot field in perri_queue_native.rs) is the single source of truth
for what counts as a bot PR. Running a parallel search would re-introduce the two-source
divergence this integration was designed to close.
Closed/merged PRs are automatically excluded by the daemon (it checks the PR detail
state / merged_at fields independently of the GitHub search index). Do not filter
on those fields yourself.
Report a one-line summary by bucket before proceeding.
Step 2 — CI gate + classify every PR
Check each PR's status (gh pr view <n> --repo <r> --json statusCheckRollup,mergeable).
Classify into exactly one class:
| Class |
Criteria |
Action |
| dependabot-green |
bucket == "dependabot", CI passing, mergeable |
approve + merge (batch) |
| dependabot-flake |
bucket == "dependabot", only failing checks are env-specific iac-plan (OpenTofu state-lock) |
offer to rerun failed jobs; recheck |
| dependabot-blocked |
bucket == "dependabot", real failing checks (lint/build/test/eresolve) |
diagnose briefly, report, defer — do not approve |
| trivial |
passes the full trivial gate (below): every changed file in one named trivial category, no sensitive surface touched, inside that category's size treatment |
approve unread (batch) — each PR with a basis line |
| clean |
real changes, but review finds nothing worth a comment |
approve (batch, after showing verdicts) |
| comment |
approve, but with a note worth leaving |
approve with comment, individual via submit-review |
| discuss |
needs the user's call (request-changes candidate, design question) |
surface only, no action |
| skip |
non-dependabot PR with failing CI (except an approval-only check) |
skip + report at end |
Note: the daemon already filters out dependabot PRs with hard Actions CI failures
(dependabot-blocked). If a bucket == "dependabot" PR appears in the queue,
it passed the daemon's CI gate — you still check for flaky iac-plan failures
specifically, but hard failures are pre-filtered.
The trivial gate
trivial is the one class approved without being read for logic, so it is
gated rather than guessed. perri.md → ## Trivial PR Batch is the authority and
this skill must not diverge from it; the gate in short, tested in this order:
- Homogeneity — disqualifies alone. Every changed file belongs to the one
claimed trivial category (machine-generated / hand-authored trivial / bulk
mechanical). Not the dominant category, and never on the strength of the title.
One out-of-category file demotes the PR — name the file.
- Sensitive surfaces — absolute. Authentication, authorization, permission or
policy gates, payments or billing, DB schema or migrations, CI/CD or deploy
configuration, secrets or credentials, rate limiting or throttling, crypto or
signature verification. No category and no line count overrides this. A
one-line config-only flip of a permission gate default is not trivial.
- Size — only after 1 and 2 pass, and only ever subtractively.
Machine-generated diffs (lockfiles, resolved-dependency files) are size-exempt
at any length, but more than 30 changed lines outside the generated files
demotes. Hand-authored trivial diffs (docs, prose, comments, config values)
have a ceiling of 200 changed lines. Bulk mechanical diffs (formatter
sweeps, codemods, mechanical renames) are size-exempt when every sampled hunk
shows the same transformation — state the transformation and the number of
hunks sampled, and demote the whole PR on any non-conforming hunk.
Size never promotes. There is no line count small enough to make a PR trivial.
Smallness is not a category: if you cannot name the trivial category a PR matches,
it is not trivial no matter how few lines it changes. Never justify a trivial
classification with "only N lines" or "small change".
Every demotion here is a read, never a rejection. Homogeneity, sensitivity and
size demotions all route to clean — never to discuss, never to skip. The
PR gets read and can still be batch-approved with the clean group, so state it as
"reading it as a clean candidate", not as blocked or deferred. Where the
classification is genuinely ambiguous, treat the PR as non-trivial and say the
classification was uncertain rather than giving an unqualified verdict.
These ceilings are unrelated to the >1k lines → delegate the deep read guidance
under "Per-PR analysis" below. That one decides how a read is performed once a
read is happening; these decide whether a read happens at all. Never state
either in terms of the other.
CI gate rule: never review on top of red CI except when the only red is the
approval check itself. List everything skipped at the end so nothing is silently dropped.
Step 3 — Work the groups in order
Order: dependabot-green → dependabot-flake → trivial → amber (changes_req + new_activity)
→ requested → needs_review (clean) → comment → discuss.
For each group:
In Nostromo, before reading any PR individually — i.e. every clean, comment,
and discuss PR; not the unread trivial batch, which is deliberately never
opened — pick it up the same way perri.md's Per-PR Review Workflow step 1 does:
perri.load_pr({ number, repo })
nostromo.show({ type: "pr_conversation", target: { repo, number } })
nostromo.show({ type: "pr_diff", target: { repo, number } })
This puts the PR's conversation and diff on screen while you form the verdict, and tells
the daemon which PR is under review (drives pinning and the reset rule). Never pass
highlights to load_pr on this path — see perri.md for why. Standalone (no
Nostromo), skip straight to reading the PR the existing way.
dependabot-green
- Enumerate the PRs (number, repo, title).
- One batch approval request: "Approve and merge these N dependabot PRs: …?"
- On yes:
gh pr review --approve then merge per repo convention:
- admin-portal uses a merge queue →
gh pr merge <n> --repo <r> --auto
- family-portal / payments →
gh pr merge <n> --repo <r> --squash --auto
- Refresh:
~/.claude/lib/perri-refresh.sh --clear
dependabot-flake
- Rerun failed
iac-plan jobs (gh run rerun <run-id> --failed --repo <r>).
- Recheck after; promote to dependabot-green if it clears. Do not block the rest waiting.
trivial / clean
- For trivial, present each PR with a one-line basis line: matched category,
file count, changed-line count, sensitive-path clearance — plus the transformation
and hunks sampled for bulk mechanical. E.g.
• operations #211 — bump axios 1.7.2 → 1.7.9 / [dep bump · 2 files · manifest +2/-2, lock +2,104/-1,880 · no sensitive paths]. This is required, not a nicety: the user is authorizing an unread
approval, and the basis line is the only thing that makes a misclassification
visible to him before he confirms. Never drop it to save space.
- List anything demoted out of trivial alongside the batch, each with its reason and
the words "reading it as a clean candidate". A demotion moves the PR into the
clean group below — it is read, and it can still be batch-approved. It is never
a block, a deferral, or a
skip.
- For clean, show the per-PR verdict first (one or two lines each) so the user sees
what they're approving.
- One batch approval request enumerating the exact PRs + "Approve (no comment)?".
- On yes: loop
gh pr review <n> --repo <r> --approve, then perri-refresh.sh --clear.
comment
- In Nostromo, when raising a specific finding, show it rather than narrating it —
nostromo.show({ type: "file", target: { path }, anchor: { kind: "line", line }, emphasis: [...], reason: "<short phrase>" }),
same as perri.md's Per-PR Review Workflow step 3. reason is required in practice: it
becomes the tab's caption. Say what's wrong; let the shown file carry the code.
- Draft the full comment body and show it before asking.
- Invoke the
submit-review skill per PR (it provides the confirmation UI, and inside
Nostromo poses the decision via nostromo.ask_decision). Never call gh pr review
directly for a comment/request-changes.
discuss
- In Nostromo, same as comment above: show the file at the line (or the ticket, for
an acceptance-criteria question) instead of quoting it back, with a
reason.
- Summarize the concern, your recommendation, and the options. Take no action until the
user decides. If the verdict becomes request-changes, route through
submit-review.
Step 4 — Report what's left
End every run with a short status:
- Approved / merged this run (with numbers)
- Reruns kicked off (dependabot-flake)
- Skipped — CI red (with the failing check names)
- Deferred — dependabot-blocked (with the real root cause, one line each)
- Discuss — awaiting your decision
Per-PR analysis — Carefeed conventions to check
When reviewing a non-trivial PR, check against the project rules (see admin-portal
CLAUDE.md / docs/claude/project-rules.md). High-signal ones:
- Migrations have
#[PreDeploy] / #[PostDeploy], real timestamps, reversible or
honestly-noted-irreversible down(), literal values (no enum refs).
- No
Session:: in Services / Jobs / Actions (Controllers + Middleware only).
- Controllers delegate to services — no static model calls, no instantiating controllers.
- Carbon only for dates (no
time(), date(), new DateTime(), strtotime()).
Log facade only (no error_log() / logger()).
- Vue Composition API,
<script setup lang="ts">, no any without justification.
- Tests:
DatabaseTransactions (not RefreshDatabase); behavioral assertions; bug-fix
PRs include a red→green test.
- Security:
hash_equals() for secret comparison; PHI never in logs; fail-closed
permission defaults (?? false, not ?? true).
For very large PRs (roughly >1k lines or many files), delegate the deep read to a
subagent (feature-dev:code-explorer, pr-review-toolkit:review-pr, or a general agent)
so the orchestrator context stays lean — then synthesize its findings yourself before
forming a verdict. Never delegate the verdict itself.
Hard rules
- The Approval Principle above is absolute.
- Comments and request-changes always go through
submit-review.
- Never review on top of failing CI (except an approval-only check).
- Trust-but-verify: confirm what was actually posted (
gh pr view <n> --json reviews)
rather than assuming the action landed.
1---2name: review-prs3description: Work the PR review queue end-to-end the Perri way — fetch the actionable queue (all buckets including dependabot), gate on CI, triage into trivial/clean/comment/discuss groups, and drive each group to a decision with explicit batch-or-individual approval. Invoke when the user says "review PRs", "let's review", "work the queue", "do PR review", or similar, or when they type /review-prs.4---56# Review PRs Skill78A single, repeatable pass over everything that needs PR-review attention right now —9the Perri daemon queue, which now includes a dedicated `dependabot` bucket for10bot-authored PRs alongside the human-review buckets.1112Designed to run from a fresh context: each cycle is mostly self-contained and does not13need prior conversation history. Start it after a `/clear` (or in a fresh session) for14maximum room.1516## The Approval Principle (never violated)1718> Every action posted to GitHub — approve, comment, request-changes, merge — must be19> covered by an **explicit approval from the user, given in the same session**. One20> approval MAY cover a batch, provided the approval request **clearly enumerates exactly21> which PRs and what action** it authorizes.2223Corollaries:24- A batch approval is valid only when the request lists the exact PR numbers and the25 exact action ("Approve these 5: #x, #y, #z, #a, #b?"). Vague asks ("approve everything?")26 do **not** satisfy it.27- Comments are almost always 1:1 (the body differs per PR), so they go individually — but28 the same rule holds: clear ask, explicit yes.29- Merges are actions too. If a group will be approved **and merged** (typical for green30 dependabot PRs), the approval request must say so.31- Submissions that carry a comment or a request-changes go through the `submit-review`32 skill, which provides the per-PR confirmation UI. The batch shortcut (one confirmation →33 loop the approvals) is reserved for **trivial**, **clean**, and **green-dependabot**34 groups, where the action is a bare approve (± merge) with no comment.3536## Step 1 — Gather the queue3738**Single source** — the Perri daemon queue is now the unified source of truth for all39PRs, including dependabot. Fetch it the same way `perri.md`'s Startup does:4041**In Nostromo** (MCP available): `perri.list_pr_queue()`.42**Otherwise** (standalone): `~/.claude/bin/perri-queue-pane --json`.4344The field list is identical either way — `perri.list_pr_queue()`'s items match45`perri-queue-pane --json`'s `.items[]` field-for-field, so Steps 2–4 below don't care which46source Step 1 used:4748Parse the items: each has `repo`, `number`, `title`, `author`, `bucket`,49`new_activity`, `url`, `ci_state`, `is_bot`.5051Bucket values:52- `"requested"`, `"needs_review"`, `"changes_req"` — human-review PRs (unchanged)53- `"dependabot"` — bot-authored PRs; `is_bot == true` (daemon-sourced, no separate fetch needed)5455**Do NOT run a separate `gh search prs --author app/dependabot` query.**56The daemon (`is_bot` field in `perri_queue_native.rs`) is the single source of truth57for what counts as a bot PR. Running a parallel search would re-introduce the two-source58divergence this integration was designed to close.5960Closed/merged PRs are automatically excluded by the daemon (it checks the PR detail61`state` / `merged_at` fields independently of the GitHub search index). Do not filter62on those fields yourself.6364Report a one-line summary by bucket before proceeding.6566## Step 2 — CI gate + classify every PR6768Check each PR's status (`gh pr view <n> --repo <r> --json statusCheckRollup,mergeable`).69Classify into exactly one class:7071| Class | Criteria | Action |72|---|---|---|73| **dependabot-green** | `bucket == "dependabot"`, CI passing, mergeable | approve **+ merge** (batch) |74| **dependabot-flake** | `bucket == "dependabot"`, only failing checks are env-specific `iac-plan` (OpenTofu state-lock) | offer to rerun failed jobs; recheck |75| **dependabot-blocked** | `bucket == "dependabot"`, real failing checks (lint/build/test/eresolve) | diagnose briefly, report, defer — do not approve |76| **trivial** | passes the full trivial gate (below): every changed file in one named trivial category, no sensitive surface touched, inside that category's size treatment | approve **unread** (batch) — each PR with a basis line |77| **clean** | real changes, but review finds nothing worth a comment | approve (batch, after showing verdicts) |78| **comment** | approve, but with a note worth leaving | approve **with comment**, individual via submit-review |79| **discuss** | needs the user's call (request-changes candidate, design question) | surface only, no action |80| **skip** | non-dependabot PR with failing CI (except an approval-only check) | skip + report at end |8182Note: the daemon already filters out dependabot PRs with hard Actions CI failures83(`dependabot-blocked`). If a `bucket == "dependabot"` PR appears in the queue,84it passed the daemon's CI gate — you still check for flaky `iac-plan` failures85specifically, but hard failures are pre-filtered.8687### The trivial gate8889`trivial` is the one class approved **without being read for logic**, so it is90gated rather than guessed. `perri.md` → `## Trivial PR Batch` is the authority and91this skill must not diverge from it; the gate in short, tested in this order:92931. **Homogeneity — disqualifies alone.** *Every* changed file belongs to the one94 claimed trivial category (machine-generated / hand-authored trivial / bulk95 mechanical). Not the dominant category, and never on the strength of the title.96 One out-of-category file demotes the PR — name the file.972. **Sensitive surfaces — absolute.** Authentication, authorization, permission or98 policy gates, payments or billing, DB schema or migrations, CI/CD or deploy99 configuration, secrets or credentials, rate limiting or throttling, crypto or100 signature verification. **No category and no line count overrides this.** A101 one-line config-only flip of a permission gate default is not trivial.1023. **Size — only after 1 and 2 pass, and only ever subtractively.**103 Machine-generated diffs (lockfiles, resolved-dependency files) are size-exempt104 at any length, but **more than 30 changed lines outside the generated files**105 demotes. Hand-authored trivial diffs (docs, prose, comments, config values)106 have a ceiling of **200 changed lines**. Bulk mechanical diffs (formatter107 sweeps, codemods, mechanical renames) are size-exempt when **every sampled hunk108 shows the same transformation** — state the transformation and the number of109 hunks sampled, and demote the whole PR on any non-conforming hunk.110111**Size never promotes. There is no line count small enough to make a PR trivial.**112Smallness is not a category: if you cannot name the trivial category a PR matches,113it is not trivial no matter how few lines it changes. Never justify a trivial114classification with "only N lines" or "small change".115116**Every demotion here is a read, never a rejection.** Homogeneity, sensitivity and117size demotions all route to **`clean`** — never to `discuss`, never to `skip`. The118PR gets read and can still be batch-approved with the clean group, so state it as119"reading it as a clean candidate", not as blocked or deferred. Where the120classification is genuinely ambiguous, treat the PR as non-trivial and say the121classification was uncertain rather than giving an unqualified verdict.122123These ceilings are unrelated to the `>1k lines → delegate the deep read` guidance124under "Per-PR analysis" below. That one decides *how* a read is performed once a125read is happening; these decide *whether* a read happens at all. Never state126either in terms of the other.127128**CI gate rule:** never review on top of red CI except when the only red is the129approval check itself. List everything skipped at the end so nothing is silently dropped.130131## Step 3 — Work the groups in order132133Order: **dependabot-green → dependabot-flake → trivial → amber (changes_req + new_activity)134→ requested → needs_review (clean) → comment → discuss.**135136For each group:137138**In Nostromo, before reading any PR individually** — i.e. every **clean**, **comment**,139and **discuss** PR; not the unread **trivial** batch, which is deliberately never140opened — pick it up the same way `perri.md`'s Per-PR Review Workflow step 1 does:141142```143perri.load_pr({ number, repo })144nostromo.show({ type: "pr_conversation", target: { repo, number } })145nostromo.show({ type: "pr_diff", target: { repo, number } })146```147148This puts the PR's conversation and diff on screen while you form the verdict, and tells149the daemon which PR is under review (drives pinning and the reset rule). Never pass150`highlights` to `load_pr` on this path — see `perri.md` for why. Standalone (no151Nostromo), skip straight to reading the PR the existing way.152153### dependabot-green154- Enumerate the PRs (number, repo, title).155- One batch approval request: "Approve **and merge** these N dependabot PRs: …?"156- On yes: `gh pr review --approve` then merge per repo convention:157 - **admin-portal** uses a merge queue → `gh pr merge <n> --repo <r> --auto`158 - **family-portal / payments** → `gh pr merge <n> --repo <r> --squash --auto`159- Refresh: `~/.claude/lib/perri-refresh.sh --clear`160161### dependabot-flake162- Rerun failed `iac-plan` jobs (`gh run rerun <run-id> --failed --repo <r>`).163- Recheck after; promote to dependabot-green if it clears. Do not block the rest waiting.164165### trivial / clean166- For **trivial**, present each PR with a one-line **basis line**: matched category,167 file count, changed-line count, sensitive-path clearance — plus the transformation168 and hunks sampled for bulk mechanical. E.g. `• operations #211 — bump axios 1.7.2169 → 1.7.9` / `[dep bump · 2 files · manifest +2/-2, lock +2,104/-1,880 · no sensitive170 paths]`. This is required, not a nicety: the user is authorizing an **unread**171 approval, and the basis line is the only thing that makes a misclassification172 visible to him before he confirms. Never drop it to save space.173- List anything demoted out of trivial alongside the batch, each with its reason and174 the words "reading it as a clean candidate". A demotion moves the PR into the175 **clean** group below — it is read, and it can still be batch-approved. It is never176 a block, a deferral, or a `skip`.177- For **clean**, show the per-PR verdict first (one or two lines each) so the user sees178 what they're approving.179- One batch approval request enumerating the exact PRs + "Approve (no comment)?".180- On yes: loop `gh pr review <n> --repo <r> --approve`, then `perri-refresh.sh --clear`.181182### comment183- **In Nostromo**, when raising a specific finding, show it rather than narrating it —184 `nostromo.show({ type: "file", target: { path }, anchor: { kind: "line", line }, emphasis: [...], reason: "<short phrase>" })`,185 same as `perri.md`'s Per-PR Review Workflow step 3. `reason` is required in practice: it186 becomes the tab's caption. Say what's wrong; let the shown file carry the code.187- Draft the full comment body and show it before asking.188- Invoke the `submit-review` skill per PR (it provides the confirmation UI, and inside189 Nostromo poses the decision via `nostromo.ask_decision`). Never call `gh pr review`190 directly for a comment/request-changes.191192### discuss193- **In Nostromo**, same as **comment** above: show the file at the line (or the ticket, for194 an acceptance-criteria question) instead of quoting it back, with a `reason`.195- Summarize the concern, your recommendation, and the options. Take no action until the196 user decides. If the verdict becomes request-changes, route through `submit-review`.197198## Step 4 — Report what's left199200End every run with a short status:201- Approved / merged this run (with numbers)202- Reruns kicked off (dependabot-flake)203- Skipped — CI red (with the failing check names)204- Deferred — dependabot-blocked (with the real root cause, one line each)205- Discuss — awaiting your decision206207## Per-PR analysis — Carefeed conventions to check208209When reviewing a non-trivial PR, check against the project rules (see admin-portal210CLAUDE.md / `docs/claude/project-rules.md`). High-signal ones:211212- Migrations have `#[PreDeploy]` / `#[PostDeploy]`, real timestamps, reversible or213 honestly-noted-irreversible `down()`, literal values (no enum refs).214- No `Session::` in Services / Jobs / Actions (Controllers + Middleware only).215- Controllers delegate to services — no static model calls, no instantiating controllers.216- Carbon only for dates (no `time()`, `date()`, `new DateTime()`, `strtotime()`).217- `Log` facade only (no `error_log()` / `logger()`).218- Vue Composition API, `<script setup lang="ts">`, no `any` without justification.219- Tests: `DatabaseTransactions` (not `RefreshDatabase`); behavioral assertions; bug-fix220 PRs include a red→green test.221- Security: `hash_equals()` for secret comparison; PHI never in logs; fail-closed222 permission defaults (`?? false`, not `?? true`).223224For very large PRs (roughly >1k lines or many files), delegate the deep read to a225subagent (`feature-dev:code-explorer`, `pr-review-toolkit:review-pr`, or a general agent)226so the orchestrator context stays lean — then synthesize its findings yourself before227forming a verdict. Never delegate the verdict itself.228229## Hard rules230231- The Approval Principle above is absolute.232- Comments and request-changes always go through `submit-review`.233- Never review on top of failing CI (except an approval-only check).234- Trust-but-verify: confirm what was actually posted (`gh pr view <n> --json reviews`)235 rather than assuming the action landed.