diff --git a/.agents/skills/triage-prs/SKILL.md b/.agents/skills/triage-prs/SKILL.md new file mode 100644 index 00000000..2b83b946 --- /dev/null +++ b/.agents/skills/triage-prs/SKILL.md @@ -0,0 +1,299 @@ +--- +name: triage-prs +description: Use this skill to triage the open PR queue before a release. Classifies every open PR into must-merge, candidate, superseded, or deferred; writes a working triage doc; and runs the merge loop end-to-end. Designed for the pre-release "PR speedrun" pass where a solo maintainer wants to clear the inbound backlog in a single session. +--- + +# Triage PRs + +## Goal + +Turn a backlog of open PRs into a shipped set of merges in a single focused session. Produce a tracked, resumable plan (`_PR_TRIAGE.md`), then work it — rebasing where needed, merging in isolation-safe batches, applying post-merge follow-ups, and closing superseded or partially-applicable PRs with credit to their authors. + +This skill pairs with `draft-release-notes` and `release-bump`: triage first, then draft notes against the new main, then cut the release. + +## When to use + +- Before a minor or major release when 10+ open PRs have accumulated +- When you want to unblock merging without losing the narrative of what's landing +- When you know you can't personally review every PR deeply, but need to land the critical subset fast + +## Prerequisites + +- `gh` CLI authenticated against the repo +- A dedicated worktree for PR review (avoid contaminating `main` with checkouts of contributor branches) +- Clarity on the target version — the triage doc is named after it (e.g. `0.4.0_PR_TRIAGE.md`) + +## Workflow + +### 1. Set up an isolated PR-review worktree + +```bash +git worktree list # check for stale ones first +git worktree prune +git worktree add ../voicebox-pr-review -b pr-review- main +``` + +Keep the main worktree for release-prep work (changelog drafts, direct-to-main follow-ups). Keep the review worktree for `gh pr checkout` — each checkout moves HEAD to a contributor branch, which you don't want to do in the main worktree. + +### 2. Gather metadata for every open PR + +```bash +gh pr list --state open --limit 50 --json \ + number,title,author,isDraft,mergeable,mergeStateStatus,files,additions,deletions,reviewDecision,statusCheckRollup,maintainerCanModify \ + --jq '.[] | {num: .number, title, author: .author.login, mergeable, state: .mergeStateStatus, canModify: .maintainerCanModify, changes: "+\(.additions)/-\(.deletions)", files: [.files[].path]}' +``` + +You want, for each PR: +- Size (`+additions/-deletions`) +- Mergeable state (`CLEAN`, `UNSTABLE`, `DIRTY` = conflicts, `UNKNOWN` = GitHub still computing) +- Whether maintainer edits are allowed on the branch (needed later if you rebase for the author) +- File paths touched (helps spot overlaps between PRs) + +`UNKNOWN` is common right after a push to main — just try the merge and see. + +### 3. Classify into tiers + +Sort each PR into exactly one bucket: + +**Tier 1 — Merge:** small, mergeable, fixes a real bug, clean CI, low review cost. One-liners, dependency relaxations, targeted safety hardening. These are the easy wins. + +**Tier 2 — Candidate, review:** medium size (50-200 lines), touches more surface area, looks sound but needs a closer read. New user-facing features that fit the product direction. + +**Supersede:** the fix or feature is already covered by something merged. Close with a comment pointing to the superseding PR. Check carefully — "similar title" isn't proof; compare the actual diffs. + +**Defer to next release:** big features, dirty conflicts, draft PRs, anything touching the release pipeline in ways that would introduce risk. Don't merge these in a speedrun — they need dedicated focus. + +### 4. Write the triage doc + +Create `_PR_TRIAGE.md` in the PR-review worktree root. Structure: + +```markdown +# — PR Triage + +Working doc for tracking which open PRs land in . Delete after release cut. + +Last updated: + +## Progress + +**Tier 1: 0 / N merged** +**Tier 2: 0 / M handled** +**Supersede triage: pending** + +--- + +## Merge for — critical bug fixes + +| PR | Status | Size | What it fixes | Why must-have | +|---|---|---|---|---| +| [#123](url) | [ ] | +5/-0 | ... | ... | + +## Strong candidate — needs a quick review + +| PR | Status | Size | Summary | +|---|---|---|---| + +## Close as superseded + +| PR | Status | Reason | +|---|---|---| + +## Defer to + +- [#xxx](url) ... — reason + +--- + +## Order of attack + +1. Close superseded PRs (one-liner comments) +2. Merge tier-1 in dependency-free batches — check file paths don't overlap +3. Review tier-2 individually +4. Rerun `draft-release-notes` to pick up everything +5. Run `release-bump` +``` + +The **Progress** header is the most important part — it's your scoreboard and lets you resume cleanly if the session gets interrupted. + +### 5. Work the loop — per PR + +For each PR in the tier-1 / tier-2 list: + +**a. Checkout in the review worktree:** +```bash +cd ../voicebox-pr-review +git checkout pr-review- # reset to neutral base +gh pr checkout +``` + +**b. Read the *actual* commit, not `main..HEAD`:** + +```bash +git show HEAD # the PR's actual changes +git show --stat HEAD # files touched + line counts +``` + +**Do NOT review via `git diff main..HEAD`** if the PR branch is older than main. That diff includes *every commit that landed on main after the PR was forked* as `-` (deletion) lines. A 3-line PR can look like a 700-line revert. This is the single easiest way to misjudge a PR. + +**c. Evaluate concerns:** correctness, scope, interaction with already-merged work, version compatibility (e.g. can't use an API that requires a dependency version we don't yet pin). + +**d. Rebase if the branch is behind main:** +```bash +git fetch origin main +git rebase origin/main +``` + +This is **essential** before squash-merging. GitHub's squash computes `diff(PR-head, merge-base)` — on a stale branch, that diff includes reverting every in-between commit. Rebasing moves the merge-base forward so the squash is clean. + +**e. If maintainer edits are allowed, push the rebase back to the contributor's fork:** +```bash +git remote add https://github.com//.git +git fetch # get their ref first +git push HEAD: --force-with-lease +``` + +This keeps GitHub's PR UI in sync with the rebased state and makes the merge clean from the GitHub side. + +**f. Merge:** +```bash +gh pr merge --squash +``` + +**g. Update the triage doc** — flip the checkbox to `✅ merged ` (use the short SHA from `gh pr view --json mergeCommit --jq '.mergeCommit.oid[0:7]'`). Update the Progress header. + +### 6. Batch tiny fixes + +PRs with ≤5 line changes, clean CI, non-overlapping file paths, and obviously-correct intent (e.g. one-line dependency relax, env var add, import path fix) can be merged in a single loop without the review-per-PR ceremony: + +```bash +for pr in 425 384 416 429; do + echo "=== Merging PR $pr ===" + gh pr merge $pr --squash +done +``` + +Verify afterward that each landed cleanly: +```bash +for pr in 425 384 416 429; do + gh pr view $pr --json state,mergeCommit --jq "{pr: $pr, state, sha: .mergeCommit.oid[0:7]}" +done +``` + +### 7. Post-merge follow-ups + +Sometimes a PR is worth merging despite a known minor issue (e.g. incomplete dtype map, stale sentinel cleanup). Don't block the merge; apply the follow-up as a normal branch + PR right after: + +```bash +cd +git pull --ff-only origin main +git checkout -b fix/ +# edit... +git commit -m "fix(): " +git push -u origin fix/ +gh pr create --title "..." --body "Follow-up to #. ..." +``` + +Record both SHAs in the triage doc (`✅ merged + follow-up `). + +**Direct-to-main exception:** only under an explicit, scoped policy (e.g. "release speedrun"). Don't default to it. + +### 8. Supersede: close with a credit-pointing comment + +```bash +gh pr close --comment "Closing — superseded by merged # which landed . Thanks!" +``` + +Check the diffs first — "similar title" is not enough. If the PR is *partially* superseded (the diagnosis is right but only half the changes are still needed), do a partial-apply instead. + +### 9. Partial-apply pattern + +When a PR has both valuable and questionable changes bundled: + +```bash +cd +git pull --ff-only origin main + +# Cherry-pick specific files from the PR branch +git checkout -- + +# Review the staged changes, adjust as needed +git diff --cached + +# Apply any surgical edits to files you don't want to bulk-replace +# (e.g. the PR's file predates a recent main commit you need to preserve) + +# Commit with a trailer crediting the original author +git commit -m "$(cat <<'EOF' + + + + +Co-Authored-By: +EOF +)" +git push ... # branch + PR, unless under the direct-to-main exception +``` + +Then close the PR with a comment explaining what was applied and what was dropped, referencing the commit SHA. + +### 10. Keep the doc current + +Every merge, every close, every follow-up → update `_PR_TRIAGE.md`. The doc is your session log. If you're interrupted and resume tomorrow, the doc is the only source of truth for "where am I." + +### 11. When triage is done + +- Every PR in the doc has a terminal status (✅ merged / ✅ closed / deferred) +- Progress header shows N/N for each tier +- Next skill to run is `draft-release-notes` (to regenerate `[Unreleased]` against the new main), then `release-bump` + +You can delete the triage doc after the release ships, or keep it in version history as a record. + +## Gotchas + +- **`main..HEAD` on a stale branch lies.** It shows everything main gained since the branch split as deletions. Always review via `git show HEAD` for the PR's actual commit. +- **Squash-merging an unrebased branch reverts in-between work.** The squash computes `diff(PR-head, merge-base)`. Rebase moves the merge-base forward. +- **`mergeable=UNKNOWN`** is transient — GitHub is recomputing after a push. Just try the merge. +- **Route ordering matters (FastAPI and similar):** `DELETE /history/failed` must be registered *before* `DELETE /history/{id}`, or the parameterized path will consume `"failed"` as an ID. +- **Apple's `-weak_framework` overrides `-framework`** for the same framework, regardless of order — use it via `cargo:rustc-link-arg=-Wl,-weak_framework,Name` when a dependency hard-links something optional. +- **Dependency version floors constrain what you can apply.** Before accepting a kwarg rename like `torch_dtype=` → `dtype=`, check the min-version pin supports it. Sometimes the right move is to cherry-pick half the PR. +- **`cpal::Stream` and similar `!Send` audio types** can't cross `await` points or `spawn_blocking`. Sometimes a "not-ideal but correct" sync wait is the best available fix; flag but don't block. +- **PyTorch nightly builds are not shippable for releases** — non-deterministic, can regress between runs. If a PR suggests switching to nightly to fix a GPU issue, prefer `TORCH_CUDA_ARCH_LIST=...+PTX` or wait for stable support instead. + +## Canonical commands reference + +```bash +# Bulk PR metadata +gh pr list --state open --limit 50 --json number,title,author,mergeable,mergeStateStatus,additions,deletions,maintainerCanModify,files + +# Detailed single-PR view +gh pr view --json body,author,headRefName,baseRefName,mergeable,maintainerCanModify,files,statusCheckRollup + +# The actual commit, not the branch-vs-main diff +git show HEAD +git show --stat HEAD +gh pr diff + +# Rebase contributor branch onto current main +git fetch origin main && git rebase origin/main + +# Push rebase back to contributor fork (maintainerCanModify=true required) +git remote add https://github.com//.git +git fetch +git push HEAD: --force-with-lease + +# Merge +gh pr merge --squash + +# Confirm merge SHA for triage doc +gh pr view --json state,mergeCommit --jq '{state, sha: .mergeCommit.oid[0:7]}' + +# Close superseded +gh pr close --comment "Closing — superseded by merged #. Thanks!" +``` + +## Notes + +- **Never review a stale branch via `main..HEAD`.** This is the single most important line in this skill. +- **The triage doc is the session state.** Lose the doc, lose the session. Update it after every action. +- **Credit contributors even on partial-applies.** Use `Co-Authored-By:` trailers and close comments that link to the applied commit. +- **Don't let perfect be the enemy of shipped.** A fix that goes from "broken" to "works with a minor known issue" is a strict improvement. Flag the issue, file a follow-up, merge the fix.