---
name: process-prs
description: "Run one PR-processing tick on the current repo's open pull requests, then stop. Finalize your own / agent-authored PRs (rebase-if-needed, /code-review then /review-fix if needed, /recommit, green CI), review community and bot PRs, and emit exactly one canonical recommend-* verdict label per PR. File follow-up and idea issues without blocking any merge. Does one tick then stops, so it composes with /loop for a recurring PR loop (e.g. `/loop /process-prs`, `/loop 1h /process-prs`). Never merges, never closes; the verdict is a recommendation, not an action. GitHub-only (uses `gh`). Use when the user wants to process, finalize, triage, or review open pull requests, set up a recurring PR-handling agent, or run one PR-processing pass."
---

# Process PRs (one tick)

One pass over the repo's open PRs per invocation, then stop. Compose with
`/loop` for recurrence (`/loop /process-prs`, `/loop 1h /process-prs`); `/loop`
owns pacing and is the new-PR discovery mechanism. Never schedule yourself.

State derives entirely from GitHub (labels, draft flag, commit/comment
timestamps), so a tick is idempotent and resumable: an empty queue costs one
`gh pr list` and exits.

## Rails

- **Never merge, never close.** The verdict is a recommendation; merging and
  closing stay the maintainer's hand.
- **Never push to a fork.** Only branches in the repo itself are editable.
- **Every PR comment and every issue starts with a disclaimer.** PR comments:
  `> *This was generated by AI during PR processing.*`. Issues:
  `> *This was generated by AI during triage.*`.
- **Skip PRs owned by another active loop.** If another dispatcher already
  drives a PR (its branch is in that loop's queue), leave it untouched. This
  pass handles everything else.

## 0. Preflight

- Abort if not a git repo, or if there is no GitHub remote. This skill is
  **GitHub-only** (`gh`, `authorAssociation`, pull requests). GitLab merge
  requests are a separate concern, not handled here.
- `git fetch`. Identify the default branch, do not assume `main`:

  ```bash
  gh repo view --json defaultBranchRef -q .defaultBranchRef.name
  ```

- Detect optional substrate; use what exists, skip what doesn't:
  - **Verdict labels** (`recommend-merge` / `recommend-close` /
    `recommend-triage`, plus the shared `needs-info`). If absent, offer to run
    `/setup-github-labels`; without them the tick can review but cannot record a
    verdict, so stop and say so.
  - **Triage wiring** (`docs/agents/triage-labels.md`, an issue `type:` axis,
    `ready-for-agent` / `needs-triage`) and an `issue-tracker.md`. Used for
    spin-off issues. If absent, fall back to plain `gh issue create` and skip
    the triage-state labels. A repo with none of this is fine.

## Labels

The three PR verdicts are mutually exclusive; a PR carries exactly one once
finalized:

- `recommend-merge`: finalized and endorsed, for the maintainer to review and merge.
- `recommend-close`: active reject (broken, abandoned, superseded, out of
  scope, or a technical blocker the agent can name).
- `recommend-triage`: code is sound but the merge/close call is a product or
  scope judgment the agent can't make.

A PR parked on an outside human reuses the shared `needs-info` label. A PR still
being iterated stays a **draft** (no verdict label).

## Tick algorithm

1. **Build the queue.** Open, non-draft PRs with no `recommend-*` verdict, minus
   branches owned by another active loop:

   ```bash
   gh pr list --state open --json number,headRefName,isDraft,author,authorAssociation,labels \
     --jq '[.[] | select(.isDraft|not)]'
   ```

   Also pull verdicted PRs to check for staleness (next step).

2. **Re-queue stale verdicts.** For each PR wearing a `recommend-*` label,
   compare its latest commit time to the agent's last verdict comment. If the
   commit is newer, the verdict is stale: remove the label and put the PR back
   in the queue for this tick.

3. **Classify each queued PR by author:**
   - **Own**: `authorAssociation` is `OWNER` or `MEMBER`. Full finalize.
   - **Bot**: author is a bot (Dependabot et al.). Mechanical review.
   - **Community**: everyone else. Review-only.

4. **Process, respecting throughput.** Review all queued community and bot PRs
   (cheap). Full-finalize **at most one** own PR per tick, to avoid two rebase
   chains conflicting. Apply the per-tier pipeline below.

5. **Handle in-flight PRs.** A PR with no verdict whose checks are still pending
   and which the agent already pushed/commented on is "awaiting CI", so skip it;
   the next tick re-checks. This state is derived from GitHub, not stored.

6. **Report.** End with a short dashboard: each queued PR and the action taken,
   verdicts emitted, PRs awaiting CI, spin-off issues filed, anything needing
   the maintainer. `/loop` reads this to pace the next run.

## Per-tier pipeline

### Own PRs: full finalize

Skip the PR this tick if it is already awaiting CI from a prior tick (step 5) —
do not re-finalize while checks are pending. Otherwise:

1. **Rebase only if a fresh CI run against current `main` is actually needed.**
   A rebase forces a push, which re-triggers CI and costs a whole extra tick of
   waiting, so don't pay it reflexively. When CI runs on `pull_request` with a
   bare `actions/checkout` (the common case; confirm in the workflow), a green
   check already tested the PR *merged into `main`*, but only as `main` stood
   when CI last ran, since GitHub does not re-run CI when `main` advances. So the
   real question is whether that green has gone stale **in a way that matters**.
   Compute the divergence (use the default branch and the PR's head ref, not
   literal `main`):

   ```bash
   gh pr view <N> --json mergeStateStatus,files \
     -q '{state: .mergeStateStatus, files: [.files[].path]}'
   base=$(git merge-base origin/<defaultBranch> origin/<headRef>)
   git diff --name-only "$base"..origin/<defaultBranch>   # files main changed since base
   git rev-list --count "$base"..origin/<defaultBranch>   # commits main gained since base
   ```

   **Rebase** (then let the pipeline force-push, re-triggering CI) **iff any holds:**
   - merge state is `DIRTY`/conflicting (a textual conflict to resolve), or
     `BEHIND` (branch protection requires up-to-date before merge);
   - `main`'s post-base files intersect the PR's changed files, a direct overlap,
     so the merged result the PR's tests cover may actually differ;
   - `main` gained **more than 20 commits** since the merge-base, a catch-all:
     a large disjoint advance can still carry a transitive/semantic break (a
     renamed or deleted symbol the PR calls from another file) that the stale
     green never exercised.

   **Otherwise skip the rebase entirely.** `main` moved only in files disjoint
   from the PR and within the cap, so the existing green still reflects the
   merged result; leave the branch untouched and let the PR be verdicted this
   same tick (step 5). (If merge state is `UNKNOWN`, GitHub is still computing
   mergeability; re-check next tick rather than guessing.)
2. **Gate `/review-fix` behind a cheap `/code-review` pass. Don't pay the full
   loop reflexively.** An own PR that is already non-draft has, by convention,
   been through `/review-fix` before it was un-drafted, so re-running the full
   loop (five reviewers, up to five rounds, plus a force-push that re-triggers CI
   and costs another whole tick) is usually wasted motion. So first run a single
   report-only `/code-review` at medium effort (no `--fix`) over the diff as a
   safety net (the same cheap gate the community tier uses, and one of the five
   reviewers `/review-fix` would run), then branch on what it finds:
   - **No meaningful findings** (clean, or only nits already matching repo
     conventions) → skip `/review-fix` entirely; the PR is already settled. Go to
     step 3.
   - **Meaningful findings** (a real correctness or design fix, or several
     concerns at once) → escalate to `/review-fix`, which runs the bundled
     reviewers, applies the meaningful fixes, folds them into clean commits, and
     force-pushes with lease.
3. `/recommit`: reshape into a clean, logical commit sequence.
4. `/verify`: only if the PR changes runtime behavior and a smoke is practical.
   `/verify` picks the method per project type (CLI invocation, server boot,
   library import, GUI launch); skip for docs/refactor/test-only PRs.
5. Resolve CI **non-blocking**. If no push happened this tick (no rebase per
   step 1, and `/review-fix` and `/recommit` each left the branch untouched), the
   existing checks are authoritative: read them directly and verdict now without
   waiting, applying the outcome rules below. Only a push made this tick puts the
   PR into the awaiting-CI state; when its checks are pending, leave the PR
   awaiting CI and move on, and a later tick applies the same rules. The outcome
   rules:
   - green → `recommend-merge`.
   - red, no prior agent fix-attempt on the PR → one fix-and-repush attempt, then
     leave it awaiting CI again.
   - red, a prior agent fix-attempt already exists → `recommend-close` (a red
     build is a nameable technical blocker, not a sound-code scope call) with the
     failure quoted.

   The one-attempt cap is derived from the PR's history, not stored: a **prior
   agent fix-attempt** = a commit the git user pushed to the branch after the
   first red CI, paired with its disclaimer-prefixed comment. Check the current
   CI outcome before the cap — a now-green PR takes the green branch regardless.
6. Emit the verdict with a disclaimer-prefixed comment.

### Community PRs: review-only, no pushing

1. `/code-review` at medium effort, **without `--fix`**.
2. Post one consolidated, disclaimer-prefixed review comment (never inline
   spam).
3. Verdict:
   - mergeable as-is, CI green → `recommend-merge`.
   - sound but a product/scope call → `recommend-triage`.
   - broken, abandoned, superseded, out of scope, or red CI with a technical
     blocker → `recommend-close`, reason quoted.
   - needs changes the author must make (incl. a trivial CI fix, or checks not
     yet authorized) → leave the review comment and park on `needs-info`.

### Bot PRs: mechanical

Review-only; the verdict is mechanical and `needs-info` never applies:

- patch/minor bump, CI green → `recommend-merge`.
- red CI → `recommend-close` (a failing bump is a nameable technical blocker),
  failure quoted.
- major version or breaking changelog, CI green → `recommend-triage` (a sound
  bump, but taking it is a scope call).

## Spin-off issues

While processing a PR, this pass captures two kinds of work as GitHub issues,
autonomously but guarded:

- **Follow-up**: a loose end from this PR that should not block its merge (a
  TODO left behind, a test-coverage gap, a deferred refactor). The PR still gets
  its verdict; note the filed issues in the verdict comment
  (`follow-ups: #N, #M`). A follow-up **never** blocks the parent's merge.
- **Idea**: a standalone improvement noticed while reading the code.

For every spin-off issue:

1. **Dedup-search open issues first**; skip if one already covers it.
2. Create it with `gh issue create` (or the repo's `issue-tracker.md`
   conventions), including the triage disclaimer and a link back to the
   originating PR. Add a `type:` label if the substrate exists, but leave the
   **triage state** to `/process-issues`: file it without a triage-state label so
   the next `/process-issues` tick picks it up and routes it.
3. List it in the tick report.

## Composition

The spin-off issues filed here land unlabeled, so `/process-issues` picks them up
and triages them. An issue that becomes `ready-for-agent` is implemented by
`/implement-issues`, whose resulting PR flows back into this pass. Three
single-purpose passes, each `/loop`-paced, none ever merging: triage, implement,
finalize.
