name: pr-triage description: "Triage a GitHub pull request before committing review effort — fetch the PR thread (description, review comments, linked issues, CI status), assess the diff against whatever architecture/standards the target repo actually carries, and emit a triage disposition (Review · Request changes · Hold · Decline) with security tier and convention drift. Use when the user wants a PR sized up, asks 'should I review/merge this PR', or wants a recommended next step on an incoming PR. Produces triage documents in .rpiv/artifacts/triage/. Read-only — never checks out or mutates the working tree. Stack-agnostic: works in any language or framework, with or without architecture docs." argument-hint: "[PR number | PR URL | empty = current branch]" shell-timeout: 15 contract: produces: kind: produces meta: artifactKind: triage data: type: object required: [security_flag, blockers_count] properties: status: enum: [in-progress, ready] security_flag: type: integer minimum: 0 maximum: 2 blockers_count: type: integer minimum: 0 risk: enum: [low, medium, high] convention_drift: enum: [none, local, structural] consumes: meta: world: github-pr
PR Triage
Size up a pull request before spending review effort: read the PR thread, compare
the diff against whatever standard the target repo carries, and emit a routing verdict.
Triage classifies and routes — it does not adjudicate line by line (that's
code-review, which the routed workflow runs). Read-only: no checkout, no mutation.
Stack-agnostic by design. The skill hard-codes no language, build tool, directory layout, or standards file — it discovers what the repo has and degrades to the code itself when nothing richer exists.
Input
$ARGUMENTS — a PR number (128), a PR URL, or empty (= the open PR of the current
branch). This value is substituted into the skill at render time; Step 1 reads it from
here directly — it is already the user's argument, not a raw token to re-parse.
Metadata
node "${SKILL_DIR}/../_shared/now.mjs"
echo
node "${SKILL_DIR}/../_shared/git-context.mjs"
PR resolution (ref normalisation, current-branch fallback, fuzzy disambiguation) is
LLM-invoked at Step 1 via the bundled _helpers/pr-fetch.mjs — it depends on the
substituted Input and on conversational clarification, which render-time substitution
cannot capture.
Flow
- Resolve + fetch PR → 2. Discover standards → 3. Dispatch assessment agents →
- Triage checkpoint → 5. Write artifact → 6. Present + recommend
Read-only contract (load-bearing): every agent is dispatched with read-only tools.
The skill MUST NOT git checkout, switch branches, or edit files — it reasons about the
PR diff as text fetched through gh. The security gate runs on fetched diff text
before any routed workflow touches the tree.
Steps
Step 1: Resolve the PR and Fetch the Thread
Read the substituted Input above and reduce it to a concrete spec the helper accepts — do not re-parse a raw token:
Input shape Pass to helper empty auto(helper resolves the current branch's PR)integer / #123/ a PR URLthe value verbatim prose / fuzzy ("the auth refactor PR") do NOT pass it. First run gh pr list --json number,title,headRefName,author, present candidates viaask_user_question(one question; options = candidate titles + "restate"), then pass the chosen number verbatim.Fetch via the bundled helper with the concrete spec:
node "${SKILL_DIR}/_helpers/pr-fetch.mjs" "<spec>"The helper shells
ghand emits labelled key/value lines (strategy:,pr_number:,title:,url:,head_ref:,head_owner:,head_label:,base_ref:,author:,files_changed:,additions:,deletions:,linked_issues:,ci_state:,ci_failing:,context_path:,patch_path:) followed by a---changed-files---block. Read those as authoritative. It writes two files:context_path:— the prose PR context (description + linked issues + comments + reviews + diff) for the convention-drift and intent agents; andpatch_path:— the raw unified diff alone, for thediff-auditorsecurity scan. Hand each agent the right path in Step 3; never paste the raw thread.Branch on
strategy:—resolved→ continue.no-pr→ print the helper'snote:and STOP. Do not write an artifact.no-gh→ print:pr-triage needs the GitHub CLI: install gh and run 'gh auth login'.and STOP.
Step 2: Discover the Standards Source (stack-agnostic)
The standard a change is held to is whatever the repo actually carries. Walk from
each changed file (the ---changed-files--- block) up to the repo root and resolve the
strongest available source per touched module — first hit wins, but record all that
exist into a StandardsMap:
- Explicit architecture/convention docs — any of:
ARCHITECTURE.md,CONTRIBUTING.md,docs/adr/**,AGENTS.md,CLAUDE.md,.rpiv/guidance/**/architecture.md, or a repo-local conventions doc. - Machine-enforced rules — any linter/formatter/analyzer config present in the
tree (
.editorconfig, ESLint/Biome/Prettier, Ruff/Flake8/Black, Checkstyle/Spotless, golangci-lint, clippy/rustfmt, .NET analyzers, … — whatever the repo has). - Peer code (universal floor) — when neither of the above covers a module, the standard IS the surrounding code: the patterns, naming, and structure of sibling files in that module.
Emit StandardsMap: per touched module → { source: doc|linter|peer, ref }. A module
resolving to source: peer is normal, not a gap. Record no-standard only when a
module has neither docs, linters, NOR readable peers (e.g. a brand-new top-level dir) —
a genuine coverage hole worth surfacing in the artifact.
Step 3: Dispatch Assessment — Security · Convention Drift · Intent
Spawn ALL three in parallel at T=0 in a single message with multiple Agent calls,
read-only tools only, none checks out code. The security agent reads the raw patch
at patch_path: (it walks a unified diff file-by-file — its contract); the convention
drift and intent agents read the prose context_path: doc.
Agent — Security (diff-auditor, read-only). diff-auditor is a row-only patch
auditor: it emits one row per surface match and assigns no severity and no summary —
the SAFE/REVIEW/BLOCK tier is computed by the skill in Step 4 from these rows, NOT by the
agent. Give it the patch and the numbered sink surface-list:
Walk the patch at <patch_path> file by file. Apply these numbered sink surfaces,
matching the concept in whatever language the diff uses (no stack assumptions):
1. Supply-chain — install/build hooks (post-install scripts, build-step network
fetch), a new dependency from an untrusted/unpinned source, lockfile swap to an
untrusted registry.
2. Secrets — credential/key/token/PEM/connection-string material added in the diff.
3. CI/CD poisoning — workflow/pipeline config that runs PR-controlled input with
elevated scope or exposes long-lived secrets to fork PRs.
4. Code execution / unsafe deserialization — shell/process spawn, eval/dynamic import,
or a deserializer that can execute code, reachable from user-controlled input.
5. Injection — user input concatenated into a query/command interpreted by an engine
(SQL/NoSQL/LDAP/XPath/shell).
6. Path traversal — user-controlled path into a filesystem API without normalization.
7. SSRF — outbound request with user-controlled host or protocol.
Output: one pipe-delimited row per match, per `diff-auditor`'s format —
`file:line | verbatim line | surface-id | note`
Put `confidence: N/10` (that the surface is real and user-reachable) in the note, and
drop any hit below 8. Rows only — no tier, no summary, no recommendations.
Agent — Convention drift (codebase-analyzer, read-only):
Read the PR context doc at <context_path>. StandardsMap (orchestrator-resolved):
{paste StandardsMap — per module: source=doc|linter|peer + ref}
For each module, Read its resolved standard (doc → the relevant section; linter →
the config's enforced rules; peer → 2–3 sibling files in the module) and the diff's
changes there. Emit one row per deviation:
`file:line — \`<verbatim line>\` — standard cited (doc§ / linter-rule / peer file:line) — drift: local|structural`
structural = the change crosses a boundary or breaks a pattern the RESOLVED STANDARD
establishes; local = a contained convention slip. Name the violated concept in the
module's own terms — no assumption of language, framework, or layout. A peer-sourced
module with no readable siblings returns a single `no-standard` row. Evidence only.
Agent — Intent vs. diff (codebase-analyzer, read-only):
Read the PR context doc at <context_path>. From the Description and Linked issues,
enumerate the PR's STATED intent as checkable claims — things the AUTHOR said they
would do. For each claim, cite whether the diff delivers it:
`claim — file:line evidence | <absent>`
Then flag scope creep: substantial diff changes not traceable to any stated claim.
Return two short lists: (A) stated-but-undelivered, (B) delivered-but-unstated.
A claim is something the author ASSERTED (in the description, a commit message, or the
linked issue). Do NOT count ambient repo states — CI/build status, pre-existing test
failures, lint output — as undelivered intent; they are observations, not claims, and
belong in the artifact's Notes, not the intent tally. Evidence only. No checkout.
Wait for all three before Step 4.
Step 4: Tally, Rank, Checkpoint
1. Tally mechanically — count the agent rows ONCE, reuse the numbers verbatim in the artifact frontmatter and body (never re-count in prose; a recount drift is the failure mode this step exists to prevent):
security_flag— derive the SAFE/REVIEW/BLOCK tier from the security agent's rows (the agent emits rows only, no tier): 2 (BLOCK) if any surviving row is surface 1 / 3 / 4 (supply-chain, CI poisoning, code-exec / unsafe deserialization) atconfidence ≥ 8; else 1 (REVIEW) if any security rows remain; else 0 (SAFE)structural_count= convention-drift rows taggeddrift: structurallocal_count= convention-drift rows taggeddrift: localintent_undelivered= stated-but-undelivered claims (NOT states — CI/build status and pre-existing failures are observations, not claims; they go to Notes, never this count)scope_creep_count= delivered-but-unstated itemsblockers_count=structural_count + intent_undelivered(minorlocaldrift is NOT a blocker)convention_drift=structuralifstructural_count > 0, elselocaliflocal_count > 0, elsenonefit=possibly-redundantwhen a maintainer comment flags overlap/redundancy with existing functionality (common for a new top-level component from an outside contributor);needs-scope-decisionwhen the change breaks a structural placement / naming / ownership or versioning convention (where and how it's packaged is in question); elsein-scope
2. Rank the Top Blockers. From the structural-drift rows + undelivered-intent claims,
pick the 3–5 that actually drive the decision, most-decisive first. A fit/scope concern
(a new top-level component, an outside contributor, a maintainer comment flagging redundancy,
a structural convention break in naming / placement / ownership) ranks at the TOP even when it
isn't a single row — it is what the reader most needs. Minor (local) nits NEVER appear here.
3. Checkpoint. Run ONE developer checkpoint via ask_user_question (house
one-question-at-a-time rule). Present:
- the security tier (derived in step 1 from the agent's rows),
- the counts with explicit arithmetic —
{blockers_count} blockers ({structural_count} structural + {intent_undelivered} undelivered intent); {local_count} minor, non-blocking. NEVER show structural / minor / blockers as a bare triple that looks like it should sum. - the Top Blockers (ranked) and the fit judgment,
- the recommended action (below).
Disposition options. This is initial triage, a gate before review — the pipeline is
triage → review → merge, and triage never merges. The positive outcome is proceed to review,
never approve or merge. The next step is a plain action (open a review, send back to the
author, comment, close). The rpiv review workflow vet is the review stage (review → repair →
commit), the one optional mechanism a triage hands forward.
| Disposition (the option label) | When | Next step |
|---|---|---|
| Review | passes triage — security SAFE, in scope, no obvious pre-review blockers | proceed to review: open a code review, or /wf vet "<pr-url>" (the rpiv review → repair → commit loop) |
| Request changes | obvious blockers the author should fix before a full review is worthwhile (structural drift, undelivered intent) | open a Request-changes review with the blockers — back to the author |
| Hold | fit is needs-scope-decision / possibly-redundant; an open question must settle first |
comment the scope question (with the redirect); don't review yet |
| Decline | out of scope / duplicate / superseded; not worth reviewing | close with the reason + where it belongs (relocate) |
/wf vet is offered only with Review — it is the review stage that follows an accepted triage.
The other dispositions are plain actions (back to the author, comment, close), no workflow.
Recommend the FEASIBLE option (the recommended one listed FIRST, marked (recommended)):
- Decline when the change clearly should not merge here (duplicate, out of scope, superseded). It is a settled "no", distinct from Hold's open question.
- Hold when
fitis questionable (the change may not belong as-is). - ALWAYS pair Hold and Decline with a redirect — the reason AND where it belongs (which existing capability, which repo, which issue). Never park or reject without a path.
- Request changes when there are obvious blockers the author should resolve before review.
- Otherwise Review — it passed triage; send it to the review stage. This is the positive outcome of triage; it does NOT mean "merge" (that comes after review).
- Offer the other dispositions too, but exactly ONE is recommended. Mark a clearly-wrong option
not recommended — {reason}, the way a reviewer would. - The security tier is NOT overridable —
BLOCKhalts regardless of the choice.
Step 5: Write the Triage Document
Fill the tally fields from Step 4 — already computed, do NOT recount:
security_flag= 0 SAFE · 1 REVIEW · 2 BLOCKblockers_count,structural_count,local_count,intent_undelivered,scope_creep_count= the Step 4 tallies, verbatim (the body's({N})headings and the Verdict counts read the same numbers)risk,convention_drift= the display enums;status: readyhead_label= the helper'shead_label:field (already qualifiedowner:branchfor a cross-repo fork, so a same-named fork branch doesn't render as an ambiguousmain → main)- the
## VerdictGates row takes the helper'sci_state:+ci_failing:and the security tier (machine-checkable gates, kept out of the human verdict; name the failing checks, not just "failing")
Write the decision-first sections (this is what makes the artifact actionable in under a minute):
## Bottom line— lead with the fit/crux judgment, then the Recommendation as a plain action (open a review / back to the author / comment the scope question / close), then the optional rpiv line (/wf vet "<pr-url>"— Review only), then a Definition of done line — the condition that flips the verdict (resolve N blockers → Review-ready; for Hold/Decline, the answer or redirect that settles it). Any/wfcommand uses the PR URL (pr_url/ the helper'surl:), never a bare#number— the URL resolves from a fresh session.## Top Blockers— the Step 4 ranking, 3–5 items, crux first; never minor nits.## Convention Drift—### Structuralas full blocks;### Minorcollapsed to ONE line per nit (never full blocks forlocalrows).
Write once with the Write tool (no Edit) to
.rpiv/artifacts/triage/<slug>_pr-<number>-<title-kebab>.md, where<slug>is the second tab-separated field on line 1 of the Metadata block (the pre-built<YYYY-MM-DD_HH-MM-SS>slug) and<title-kebab>is the PR title kebab-cased. Readtemplates/triage.md, fill every{placeholder}from Steps 1–4, apply the section-omission rules below (delete the whole section AND its trailing---when its input is empty), strip the leading<!-- -->comment, and Write.Section-omission: drop
## Top Blockerswhen there are none (SAFE + clean + intent delivered); within## Convention Driftdrop### Structural/### Minorwhen its count is 0 (drop the whole section when both are 0); within## Intent Gapsdrop### Stated-but-undelivered/### Scope creepwhen 0 (drop the whole section when both are 0); drop## Unguided Moduleswhen every module resolved to a standard.On
security_flag == 2(BLOCK): still write the artifact — the audit record matters — setstatus: ready, and make the BLOCK finding the lead of## Security.
Step 6: Present + Recommend
Triage written to:
`.rpiv/artifacts/triage/{filename}.md`
PR: #{number} — {title} ({head_label} → {base_ref})
Bottom line: {the one-sentence crux/fit judgment}
Security: {SAFE|REVIEW|BLOCK}
Blockers: {blockers_count} ({structural_count} structural + {intent_undelivered} intent) · {local_count} minor, non-blocking
Intent: {D} delivered · {intent_undelivered} undelivered · {scope_creep_count} scope-creep
CI: {passing|failing|pending|none}
Recommendation: {Review|Request changes|Hold|Decline} — {one-line why, plain prose}
Next step: {the action: open a review / back to the author / comment the scope question / close with a redirect}
{optional rpiv, Review only: `/wf vet "{pr_url}"`}
{BLOCK: STOP — resolve the security finding before any checkout}
> 🆕 Tip: start a fresh session with `/new` before chaining.
Important Notes
Guardrails — the agent MUST obey (the rest of the skill is the happy path)
- ALWAYS read-only. NEVER
git checkout, switch branches, or Edit/Write source — reason about the diff as fetched text only. All three agents get read-only tools. - NEVER let an agent set the tier or the counts.
diff-auditoremits rows only; the skill derivessecurity_flagand every count in Step 4. - ALWAYS count agent rows exactly once and reuse them verbatim. NEVER re-count in prose — the frontmatter, headings, Verdict, and checkpoint read the same numbers.
- NEVER count ambient states as findings. CI/build status, pre-existing failures, lint output
are observations →
## Notes, NEVER the intent or blocker tally. - ALWAYS show blockers with explicit arithmetic (
N = X structural + Y intent). NEVER present structural / minor / blockers as a bare triple that looks like it should sum. - NEVER override the security tier. Derive it honestly from the rows; do not soften a BLOCK.
On a
BLOCK(security_flag: 2) still write the artifact — the audit record matters. - ALWAYS write the artifact exactly once with Write (NEVER Edit).
- NEVER put minor (
local) nits in## Top Blockers. - ALWAYS use the PR URL in any
/wfcommand, never a bare#number. The URL resolves from a fresh session; a number does not. (#numberis fine in display headings only.) - NEVER paste the raw PR thread into a prompt. Hand each agent only its path —
patch_pathto security,context_pathto drift/intent. - NEVER assume the target repo's stack. No language, build-tool, package-manager, or layout assumptions — discover the standard (docs → linter → peer code) and fall back to peer code.
Notes
- Read-only is load-bearing: no checkout, no branch switch, no Edit/Write to source. All three agents inherit read-only tool sets. The security gate runs on fetched diff text before the routed workflow ever touches the tree.
- Standards are discovered, not assumed: the skill never hard-codes a doc path or a
stack. It resolves the strongest available standard per module — explicit docs →
linter rules → peer code — and degrades to peer-code inference, which exists in every
repo.
.rpiv/guidance/is just one possibledocsource among many, never a dependency. - Language/framework-agnostic: convention and security findings name the concept the diff violates in the module's own terms. No assumption of TS/Node, a build tool, or a directory layout.
security_flagis the gate field: an integer the workflowgate(...)reads viaNumber(); aBLOCK(2) halts the run. The enums (risk,convention_drift) and the tally fields are display-only. Keep frontmatter fields verbatim —artifacts-locatorgreps them.- Triage gates; it does not review: no per-line adjudication, no severity reconciliation, no verification pass — that's the review stage. Keep this skill cheap: three agents, one checkpoint, one write.
- GitHub-coupled: requires
ghauth and an open PR. The helper degrades tostrategy: no-gh/no-pr(never a shell error) so the skill body can stop cleanly. - Agent roles:
diff-auditor×1 (Step 3) — walkspatch_pathagainst the numbered sink surfaces; emits rows only (file:line | verbatim | surface-id | note, confidence in note). Per its contract it assigns no severity — the skill derivessecurity_flagfrom the rows in Step 4.codebase-analyzer×1 (Step 3) — convention drift vs the resolvedStandardsMap.codebase-analyzer×1 (Step 3) — intent-vs-diff + scope-creep check.