Claude-code-ultimate-guide pr-triage
4-phase PR backlog management with audit, deep code review, validated comments, and optional worktree setup. Use when triaging pull requests, catching up on pending code reviews, or managing a backlog of open PRs. Args: 'all' to review all, PR numbers to focus (e.g. '42 57'), 'en'/'fr' for language, no arg = audit only.
git clone https://github.com/FlorianBruniaux/claude-code-ultimate-guide
T=$(mktemp -d) && git clone --depth=1 https://github.com/FlorianBruniaux/claude-code-ultimate-guide "$T" && mkdir -p ~/.claude/skills && cp -r "$T/examples/skills/pr-triage" ~/.claude/skills/florianbruniaux-claude-code-ultimate-guide-pr-triage && rm -rf "$T"
examples/skills/pr-triage/SKILL.mdPR Triage
4-phase workflow for maintainers: automated audit of all open PRs, opt-in deep review via parallel agents, validated comment posting, and optional worktree setup for local review.
When to Use This Skill
| Skill | Usage | Output |
|---|---|---|
| Sort, review, and comment on a PR backlog | Triage table + reviews + posted comments |
| Review a single PR in depth | Inline PR review |
Triggers:
- Manually:
or/pr-triage
or/pr-triage all/pr-triage 42 57 - Proactively: when >5 PRs open without review, or stale PR >14 days detected
Language
- Check the argument passed to the skill
- If
oren
→ tables and summary in Englishenglish - If
,fr
, or no argument → French (default)french - Note: GitHub comments (Phase 3) are ALWAYS in English (international audience)
Configuration
Thresholds used throughout the workflow. Edit to match your project:
| Parameter | Default | Description |
|---|---|---|
| 14 | Days without activity before flagging as stale |
| 50% | Shared files % to flag as overlapping |
| 3 | Author PR count to trigger cluster suggestion |
| 1000 | Additions above which a PR is classified XL |
| 10 | Changed files above which a PR is "too large" |
Preconditions
git rev-parse --is-inside-work-tree gh auth status
If either fails, stop and explain what is missing.
Phase 1 — Audit (always executed)
Data Gathering (parallel commands)
gh repo view --json nameWithOwner -q .nameWithOwner gh pr list --state open --limit 50 \ --json number,title,author,createdAt,updatedAt,additions,deletions,changedFiles,isDraft,mergeable,reviewDecision,statusCheckRollup,body gh api "repos/{owner}/{repo}/collaborators" --jq '.[].login'
Collaborators fallback: if
gh api .../collaborators returns 403/404:
gh pr list --state merged --limit 10 --json author --jq '.[].author.login' | sort -u
If still ambiguous, ask via
AskUserQuestion.
For each PR, fetch reviews and changed files:
gh api "repos/{owner}/{repo}/pulls/{num}/reviews" \ --jq '[.[] | .user.login + ":" + .state] | join(", ")' gh pr view {num} --json files --jq '[.files[].path] | join(",")'
Notes: Fetching files requires 1 API call per PR — for 20+ PRs, prioritize overlap candidates. The
author field is an object; always extract .author.login.
Analysis
Size classification:
| Label | Additions |
|---|---|
| XS | < 50 |
| S | 50–200 |
| M | 200–500 |
| L | 500–1000 |
| XL | > 1000 |
Size format:
+{additions}/-{deletions}, {files} files ({label})
Detections:
- Overlaps: compare file lists across PRs — if >50% files in common → cross-reference
- Clusters: author with 3+ open PRs → suggest review order (smallest first)
- Staleness: no activity for >14 days → flag "stale"
- CI status: via
→statusCheckRollup
/clean
/unstabledirty - Reviews: approved / changes_requested / none
PR ↔ Issue linking:
- Scan each PR
forbody
,fixes #N
,closes #N
(case-insensitive)resolves #N - If found, display in the table:
in the Action/Status columnFixes #42
Categorization:
Internal PRs: author in collaborators list
External — Ready: additions ≤ 1000 AND files ≤ 10 AND
mergeable ≠ CONFLICTING AND CI clean/unstable
External — Problematic: any of:
- additions > 1000 OR files > 10
- OR
==mergeable
(merge conflict)CONFLICTING - OR CI dirty (statusCheckRollup contains failures)
- OR overlap with another open PR (>50% shared files)
Output — Triage Table
## Open PRs ({count}) ### Internal PRs | PR | Title | Size | CI | Status | | -- | ----- | ---- | -- | ------ | ### External — Ready for Review | PR | Author | Title | Size | CI | Reviews | Action | | -- | ------ | ----- | ---- | -- | ------- | ------ | ### External — Problematic | PR | Author | Title | Size | Problem | Recommended Action | | -- | ------ | ----- | ---- | ------- | ------------------ | ### Summary - Quick wins: {XS/S PRs ready to merge} - Risks: {overlaps, XL sizes, CI dirty} - Clusters: {authors with 3+ PRs} - Stale: {PRs with no activity >14d} - Overlaps: {PRs touching the same files}
0 PRs → display
No open PRs. and stop.
Navigation Post-Phase 1
After displaying the triage table, ask via
AskUserQuestion:
question: "What would you like to do next?" header: "Next Step" options: - label: "Phase 2 — Deep review" description: "Analyze selected PRs with code-reviewer agents and generate comment drafts" - label: "Phase 4 — Create worktrees" description: "Set up local worktrees for hands-on review (skips comment generation)" - label: "Done" description: "End the workflow here"
Note: Phase 3 (posting comments) is NOT offered here — it requires the drafts generated in Phase 2. If the user picks "Phase 4", Phase 2 → Phase 3 remains accessible afterward.
Automatic Copy
After displaying the triage table, copy to clipboard using platform-appropriate command:
UNAME=$(uname -s) if [ "$UNAME" = "Darwin" ]; then pbcopy <<'EOF' {full triage table} EOF elif command -v xclip &>/dev/null; then echo "{full triage table}" | xclip -selection clipboard elif command -v wl-copy &>/dev/null; then echo "{full triage table}" | wl-copy elif command -v clip.exe &>/dev/null; then echo "{full triage table}" | clip.exe fi
Confirm:
Triage table copied to clipboard. (EN) / Tableau copié dans le presse-papier. (FR)
Phase 2 — Deep Review (opt-in)
PR Selection
If argument passed:
→ all external PRs"all"- Numbers (
) → only those PRs"42 57" - No argument → propose via
AskUserQuestion
If no argument, display:
question: "Which PRs do you want to review in depth?" header: "Deep Review" multiSelect: true options: - label: "All external" description: "Review {N} external PRs with parallel code-reviewer agents" - label: "Problematic only" description: "Focus on {M} risky PRs (CI dirty, too large, overlaps)" - label: "Ready only" description: "Review {K} PRs ready to merge" - label: "Skip" description: "Stop here — audit only"
Draft PR behavior:
- Draft PRs are EXCLUDED from "All external" and "Ready only"
- Draft PRs are INCLUDED in "Problematic only" (they need attention)
- To review a draft: type its number explicitly (e.g.
)42
If "Skip" → end workflow.
Executing Reviews
For each selected PR, launch a
code-reviewer agent via Task tool in parallel:
subagent_type: code-reviewer model: sonnet prompt: | Review PR #{num}: "{title}" by @{author} **Metadata**: +{additions}/-{deletions}, {changedFiles} files ({size_label}) **CI**: {ci_status} | **Reviews**: {existing_reviews} | **Draft**: {isDraft} **PR Body**: {body} **Diff**: {gh pr diff {num} output} Apply your security and architecture expertise. Use the project-specific checklist from the SKILL.md Configuration section if available. Return structured review: ### Critical Issues ### Important Issues ### Suggestions ### What's Good Be specific: quote file:line, explain the issue, suggest the fix.
Fallback if parallel agents unavailable: run reviews sequentially, one PR at a time. Notify user:
Running sequential review (parallel agents not available).
Fetch diff via:
gh pr diff {num} gh pr view {num} --json body,title,author -q '{body: .body, title: .title, author: .author.login}'
Aggregate all reports. Display a summary after all reviews complete.
Phase 3 — Comments (mandatory validation)
Draft Generation
For each reviewed PR, generate a GitHub comment using the template
templates/review-comment.md.
Rules:
- Language: English (international audience)
- Tone: professional, constructive, factual
- Always include at least 1 positive point
- Quote code lines when relevant (format
)file:42
Display and Validation
Display ALL drafted comments in format:
--- ### Draft — PR #{num}: {title} {full comment} ---
Then request validation via
AskUserQuestion:
question: "These comments are ready. Which ones do you want to post?" header: "Post Comments" multiSelect: true options: - label: "All ({N} comments)" description: "Post on all reviewed PRs" - label: "PR #{x} — {title_truncated}" description: "Post only on this PR" - label: "None" description: "Cancel — post nothing"
(Generate one option per PR + "All" + "None")
Posting
For each validated comment:
gh pr comment {num} --body-file - <<'REVIEW_EOF' {comment} REVIEW_EOF
Confirm each post:
Comment posted on PR #{num}: {title}
If "None" →
No comments posted. Workflow complete.
Project-Specific Checklist
Add your stack's checklist to the agent prompt in Phase 2. Examples by stack:
Node.js / TypeScript:
- No
type without explicit justificationany
error handling (try/catch orasync/await
).catch()- No unhandled promise rejections
- Input validation at API boundaries
Python:
- Type hints on all public functions
- Exception specificity (no bare
)except: - Resource cleanup (
statements, context managers)with - No mutable default arguments
Rust:
withResult<T, E>
for error chain (no.context()
in production code).unwrap()- No
on hot paths without justificationclone()
orlazy_static!
for static regexonce_cell- Lifetime annotations where ownership is non-obvious
Go:
- Explicit error handling (no
discard without comment)_
for resource cleanupdefer- Context propagation in concurrent code
- No goroutine leaks
Generic (stack-agnostic):
- No secrets or hardcoded credentials
- New public functions have tests
- Breaking changes documented in PR body
- Dependencies added have clear justification
Phase 4 — Worktree Setup (opt-in)
Creates local git worktrees for each selected PR so you can run, test, or review code without switching branches.
Never triggered automatically — only via Phase 1 navigation or explicit user request.
Step 4.1 — Cache check + PR list
Cache check: before using data from Phase 1, verify it is less than 30 minutes old:
CACHE_FILE="/tmp/pr-triage-prs.json" CACHE_AGE=$(( $(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0) )) if [ "$CACHE_AGE" -gt 1800 ]; then echo "STALE_CACHE" fi
If
STALE_CACHE → re-run the Phase 1 data gathering before continuing.
Filter: exclude Draft PRs and bot PRs (Dependabot, renovate, etc.):
python3 -c " import json prs = json.load(open('/tmp/pr-triage-prs.json')) filtered = [ p for p in prs if not p['isDraft'] and not any(bot in p['author']['login'].lower() for bot in ['dependabot', 'renovate', 'snyk']) ] import sys; json.dump(filtered, sys.stdout, indent=2) " > /tmp/pr-triage-phase4.json
If 0 PRs after filtering → display
No reviewable PRs available for worktree (all are drafts or bots). + end Phase 4.
Display grouped by author (use display name if available, fallback to login):
## PRs available for worktree (non-draft) ### Alice Martin (@alice) [1] #123 — feat(auth): add OAuth2 support Branch: feat/oauth2 | Size: M | CI: clean ### Bob Chen (@bob) [2] #456 — fix(api): handle empty response Branch: fix/empty-response | Size: S | CI: dirty ⚠️
Step 4.2 — Selection
Ask via
AskUserQuestion (multiSelect):
question: "Which PRs do you want to create a worktree for?" header: "Worktree Setup" multiSelect: true options: - label: "All" description: "Create worktrees for all {N} listed PRs" - label: "[1] #{num} — {title} ({author})" description: "Branch: {branch} | Size: {size} | CI: {ci}" - label: "None" description: "Cancel — return to menu"
If "None" → end Phase 4.
Step 4.3 — Sequential creation
Execution model: Claude runs one bash command per PR, reads its output, updates its internal state (created / existing / failed), then moves to the next. Never a bash loop wrapping all PRs.
For each selected PR, Claude sets variables explicitly then runs:
PR_NUM="123" BRANCH_NAME="feat/oauth2" WORKTREE_NAME="${BRANCH_NAME//\//-}" REPO_ROOT="$(cd "$(git rev-parse --git-common-dir)/.." && pwd)" WORKTREE_DIR="$REPO_ROOT/.worktrees/$WORKTREE_NAME" # Already exists? if [ -d "$WORKTREE_DIR" ]; then echo "STATUS:EXISTING:$PR_NUM:$WORKTREE_DIR" exit 0 fi # .gitignore check (fail-fast) if ! grep -qE "^\.worktrees/?$" "$REPO_ROOT/.gitignore" 2>/dev/null; then echo "STATUS:GITIGNORE_MISSING:$PR_NUM" exit 1 fi # Fetch remote branch if ! git fetch origin "$BRANCH_NAME" 2>/tmp/wt-fetch-$PR_NUM.log; then echo "STATUS:FETCH_FAILED:$PR_NUM" exit 1 fi mkdir -p "$REPO_ROOT/.worktrees" # Create worktree (branch local exists or not) if ! git branch --list "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then git worktree add -b "$BRANCH_NAME" "$WORKTREE_DIR" "origin/$BRANCH_NAME" \ 2>/tmp/wt-err-$PR_NUM.log else git worktree add "$WORKTREE_DIR" "$BRANCH_NAME" \ 2>/tmp/wt-err-$PR_NUM.log fi if [ $? -ne 0 ]; then if grep -q "already checked out" /tmp/wt-err-$PR_NUM.log; then echo "STATUS:ALREADY_CHECKED_OUT:$PR_NUM" else echo "STATUS:CREATE_FAILED:$PR_NUM" fi exit 1 fi # Optional: symlink node_modules (Node.js projects — avoids reinstall) [ -d "$REPO_ROOT/node_modules" ] && ln -sf "$REPO_ROOT/node_modules" "$WORKTREE_DIR/node_modules" # Copy project-specific files listed in .worktreeinclude (if present) if [ -f "$REPO_ROOT/.worktreeinclude" ]; then while IFS= read -r entry || [ -n "$entry" ]; do [[ "$entry" =~ ^#.*$ || -z "$entry" ]] && continue entry="$(echo "$entry" | xargs)" [ -e "$REPO_ROOT/$entry" ] && { mkdir -p "$(dirname "$WORKTREE_DIR/$entry")" cp -R "$REPO_ROOT/$entry" "$WORKTREE_DIR/$entry" } done < "$REPO_ROOT/.worktreeinclude" fi echo "STATUS:CREATED:$PR_NUM:$WORKTREE_DIR"
Status handling (Claude maintains internal state between PRs):
| Status | Claude action |
|---|---|
| Add to "created" list |
| Add to "existing" list → offer pull in Step 4.4 |
| Warn + continue to next PR |
| Fail-fast: show fix instructions + stop Phase 4 |
| Warn: "Branch already checked out in another worktree. Run to locate it." |
| Warn + continue to next PR |
GITIGNORE_MISSING fix instructions:
.worktrees/ is not in .gitignore. Add it to avoid accidentally committing worktree files: echo ".worktrees/" >> .gitignore Then re-run Phase 4.
Step 4.4 — Update existing worktrees
If any
STATUS:EXISTING collected, offer a single prompt:
Existing worktrees detected: PR #123 — .worktrees/feat-oauth2 PR #789 — .worktrees/fix-session-leak - [Pull all] git pull --ff-only in all existing worktrees - [#123] Pull PR #123 only - [Skip] Leave as-is
For each selected pull, Claude runs (one command per worktree):
PR_NUM="123" BRANCH_NAME="feat/oauth2" WORKTREE_DIR="/abs/path/.worktrees/feat-oauth2" cd "$WORKTREE_DIR" && git pull origin "$BRANCH_NAME" --ff-only 2>/tmp/wt-pull-$PR_NUM.log echo "PULL_STATUS:$?:$PR_NUM"
If
PULL_STATUS ≠ 0:
⚠️ PR #123 — --ff-only failed (branches have diverged) Manual fix: cd .worktrees/feat-oauth2 && git pull --rebase
Step 4.5 — Summary
## Worktrees ready | PR | Author | Branch | Path | Status | |----|--------|--------|------|--------| | #123 | Alice | feat/oauth2 | .worktrees/feat-oauth2 | Created | | #456 | Bob | fix/empty-response | .worktrees/fix-empty-response | Created | | #789 | Alice | fix/session-leak | .worktrees/fix-session-leak | Updated (pull) | | #321 | Carol | feat/chat | .worktrees/feat-chat | Fetch failed ⚠️ | Note: if a PR modifies package.json, install dependencies manually: cd .worktrees/<branch-name> && npm install # or pnpm/yarn/bun Next steps: cd .worktrees/<branch-name> claude
.worktreeinclude
convention
.worktreeincludeCreate a
.worktreeinclude file at the repo root to list files Phase 4 copies into each new worktree. Useful for local config files not tracked in git:
# .worktreeinclude .env.local .env.test config/local.json
Edge Cases
| Situation | Behavior |
|---|---|
| 0 open PRs | Display + stop |
| Draft PR | Show in table, skip for review unless explicitly selected |
| Unknown CI | Display in CI column |
| Review agent timeout | Show partial error, continue with others |
empty | Skip this PR, notify user |
| Very large PR (>5000 additions) | Warn: "Partial review, diff truncated" |
| Collaborators API 403/404 | Fallback to last 10 merged PR authors |
| Parallel agents unavailable | Run sequential reviews, notify user |
Phase 4: missing | Fail-fast, show fix instructions, stop Phase 4 |
| Phase 4: branch already checked out | Warn with hint, skip this PR |
| Phase 4: stale cache (>30min) | Re-fetch PR list before creating worktrees |
Phase 4: PR modifies | Warn in summary to run install manually |
| Phase 4: 0 non-draft PRs | Display message + end Phase 4 |
Notes
- Always derive owner/repo via
, never hardcodegh repo view - Use
CLI (notgh
GitHub API) except for collaborators listcurl
can be null → treat asstatusCheckRollup?
can bemergeable
,MERGEABLE
, orCONFLICTING
→ treatUNKNOWN
asUNKNOWN?- Never post without explicit user validation in chat
- Drafted comments must be visible BEFORE any
gh pr comment
Related: /review-pr
| | |
|---|---|---|
| Scope | Full PR backlog | Single PR |
| Use when | Catching up after accumulation, periodic triage | Reviewing a specific incoming PR |
| Phases | 4 (audit + deep review + comments + worktrees) | 1 (review only) |
| Agents | Parallel sub-agents per PR | Single session |
| Output | Triage table + review reports + GitHub comments + local worktrees | Inline review |
| Validation | AskUserQuestion before posting | Manual decision |
Decision rule: use
/pr-triage for backlog triage (5+ PRs), /review-pr for focused review of a single PR. Use Phase 4 when you want to run the code locally rather than just reading the diff.