A review queue that knows which pull requests are actually yours.
GitHub gives you one list sorted by nothing useful. pr-queue pulls every pull
request waiting on you, judges each one against rules you wrote, and serves the
result on localhost where you can approve, comment, and flag.
The judgement happens in the pr-triage Claude Code skill: it fetches the
reviews assigned to you, then applies your profile and your rules on top.
Node 20, gh, and Claude Code. No dependencies, no build step.
gh auth login # if you have not already
node setup.js # discovers your login, org, and teamsThen write the two files that make the queue yours:
reference/editorial-rules.md— everything you care about, in plain sentences. Triage refuses to run against the untouched template.reference/profile.md— what you know deeply, what you do not, and the voice your comments are written in.
In Claude Code:
triage the PR queue
The skill refreshes the queue from GitHub, reads your rules, and writes a verdict per pull request: which bucket, what to do, one sentence on where the risk sits, and comment copy when something is missing.
Then read it:
node server.js # http://127.0.0.1:8788Tabs are your buckets. The repository picker and Changed since last refresh narrow further, and all three compose. Each row shows the score, chips for the mechanical signals, an effort estimate, and the triage reasoning.
Per row: Approve (gh pr review --approve), Comment (pre-filled with the
drafted copy, shows you the body before posting), Flag (feeds the learning loop).
Every write to GitHub takes a confirmation.
Refresh re-fetches facts and reports what changed. It does not re-judge — verdicts whose commit moved are dropped, and triage is how they come back. So re-run triage after editing your rules or your buckets.
editorial-rules.md is the file that matters. Each rule gets an e-NNN id so
triage can cite it and a bad call is traceable to the sentence that caused it:
### e-001 · API and contract changes
Anything that changes, adds, or proposes an API — a REST payload, a GraphQL schema,
a protobuf, an event shape, or a public client method. I own these contracts and a
mistake here is expensive to unwind. Applies even when the diff is tiny.Escalations, things to bury, standards like "I want screenshots on anything visual", and the epics that matter all live there as prose. Say when a rule applies as well as what to conclude — a rule that fires on everything is worse than no rule. Do not write point values; triage decides the weight.
No regex, no keyword lists. Write "anything touching the provider inventory layer" and the skill decides what satisfies it. Every point it awards has to trace back to one of your rules — an invented preference gets dropped.
| Path | What it is |
|---|---|
reference/editorial-rules.md |
Everything you care about |
reference/profile.md |
What you know, and your review voice |
reference/buckets.json |
Your buckets and what fills them |
reference/learned-rules.md |
What your flags have taught the queue |
reference/config.json |
Identity and bot accounts, written by setup |
The Settings page edits the first four in place. All of them are gitignored, so each person's queue stays their own.
Buckets are yours, not built in. A bucket is filled by a when condition on the
signals, by a prose rule the skill applies, or by both.
Order matters: first match wins, top to bottom. A bucket showing zero almost always means one above it matched first.
when is "always", one condition, or an all / any / not group. A condition
is a field plus one of is, eq, neq, gt, gte, lt, lte, in,
includes (array contains), endsWith (case-insensitive), matches (regex).
Fields: ageDays, staleDays, humanReviewCount, approvalCount,
myReviewState, myReviewStale, ciState, mergeable, sizeLabel, sizeLines,
fileCount, filesTruncated, isDraft, isDependencyBump, hasImageOrVideo,
hasLinkedIssue, descriptionChars, descriptionHeadingCount, sourceKind,
familiarity.score, repo, repoName, author, title, labels, files,
score.
Some buckets cannot be a condition without lying. No file extension tells you
whether a change alters what a user sees. Give those a rule instead:
{
"id": "needs-screenshots",
"label": "Needs screenshots",
"rule": "The change alters what a user sees and there is no image or video on the pull request. Judge it from the files and the description, not the extension: a rename or a test-only change with no visual result does not belong here."
}A prose-only bucket reads zero until triage runs — that is the point of it, not a broken condition. Giving a bucket both makes the condition its default and the rule triage's licence to move things out.
Saving validates: bad JSON, unknown comparisons, duplicate ids, a bucket with
neither a when nor a rule, and a missing catch-all are all refused with a
message, and the file on disk is left alone.
Two scores add up. Mechanical answers how ready and how neglected is this — facts, identical for every reviewer, computed on every fetch:
| Signal | Effect |
|---|---|
| Requested directly / re-review / via a team | +20 / +15 / +5 |
| Nobody has reviewed it, or exactly one has | +20 / +5 |
| Two or more reviews, or two approvals | −10 / −25 |
| Age, up to three weeks | +0…+31 |
| Untouched for a fortnight | −10 |
| Author pushed since your review | +25 |
| Your change request still stands | −40 |
| Conflicts, failing CI, pending CI | −15 / −15 / −5 |
| Draft, or you approved and nothing moved | −1000 (a floor) |
Editorial answers why is this one yours — taste, from your rules, clamped to [−25, +50]. The dividing line is whether two reviewers would agree: "nobody has reviewed this in six weeks" is a fact; "this touches an API" is a judgement, so it is not in the JavaScript at all.
Size, familiarity, a thin description, and whether a bump is worth your time are measured but not scored — they say which pull request you would rather read, not which one is neglected. They reach the ranking through your rules.
Without triage you still get a useful queue: longest-waiting and least-reviewed first.
The pr-queue-learn skill turns flags into durable rules. A flag stores your
note plus the signals at that moment and the rules that were applied — which is what
lets a rule that keeps being wrong get retired instead of quietly reapplied.
Flag good calls too. A queue that only hears about failures gets steadily more anxious.
- Editing rules changes nothing until you re-run triage. The prose is read at triage time, not at page load.
- A new bucket reading zero is an ordering problem, or a prose bucket waiting
for triage. Check for a
whenbefore debugging the condition. - Renaming a bucket id orphans existing verdicts. Re-run triage.
- A regex in JSON needs doubled backslashes, and a wrongly escaped pattern is
still valid regex — it just silently matches nothing. Prefer
endsWithfor extensions. - Bot logins differ by API: GraphQL says
dependabot, REST saysdependabot[bot].config.jsonregisters both; a missed bot inflates the review count and demotes real work. - The refresh log is
state/refresh.log.
{ "id": "kotlin-conversions", "label": "Kotlin conversions", "hint": "Java to Kotlin work. You know this well and it goes wrong quietly.", "tone": "review", // review | warn | safe | muted — colour only "suggestedAction": "comment", // approve | comment | read "when": { "all": [ { "field": "files", "endsWith": ".kt" }, { "field": "title", "matches": "kotlin|convert|migrat" } ] } }