PR Fleet — Parallel PR Orchestrator
Replaces sequential ralph-loop merges with concurrent subagent workers. One coordinator + N parallel workers per PR via git worktrees. Runs in two modes (see below).
When to invoke
- Batch clearing of PR backlog, overnight or interactive
- Schedulable via cron through
claude -p "/pr-fleet"headless mode (autonomous mode only) - Repo has open PR queue (>3 PRs makes parallelism worthwhile)
Modes
- Autonomous (default). Unattended overnight run. Dispatches the whole queue (chunked to the 20-PR cap), never merges, writes a morning report for the human to action. No pauses.
- Interactive (
--interactive, or triggered by “merge queue” / “pr backlog” / “/pr-merge-queue”). Same triage + worker protocol, but processes in batches of 5 with a checkpoint after each batch: post a summary, pause for user confirmation, and only then merge the confirmed PRs. Use this when the user wants to stay in the loop rather than wake up to a fait accompli. See Step 3a.
Shared State
.omc/state/pr-swarm.json (per-repo) tracks every PR in the run:
{ "prs": { "8669": { "status": "in_progress", "claimed_by": "worker-2", "attempts": 1, "blocker_reason": null } } }
Statuses: pending | in_progress | done | blocked. Atomic claim/release via state-helpers.sh (mkdir-mutex lock). Workers MUST claim before acting; if claim returns non-zero, skip to next PR.
Coordinator Protocol
Step 0 — Pre-flight (REQUIRED)
Run bash ~/.claude/skills/pr-fleet/state-helpers.sh preflight. Hard-fails on missing gh/jq, unauthenticated gh, or git fetch failure.
Additionally:
- If any queued PR touches
prisma/**or.github/workflows/**, setOMC_SKIP_HOOKS=protect-pathsfor the run AND surface to user before dispatch. - Verify
git push --dry-run --force-with-leaseagainst an arbitrary head returns no firewall block.
If pre-flight fails: STOP. Do not dispatch — workers will all stall on the same blocker.
Step 1 — Triage
gh pr list --state open --json number,title,headRefName,baseRefName,mergeable,isDraft,statusCheckRollup,reviewDecision,files \
--limit 50
Filter:
- SKIP drafts
- SKIP PRs touching
prisma/schema.prisma,migrations/**,.github/workflows/**, or any protect-paths target → add to ESCALATIONS list - SKIP PRs with reviewDecision=
CHANGES_REQUESTED→ add to BLOCKED list - QUEUE remainder
Step 2 — Dispatch (parallel, cap 3 concurrent)
- Autonomous mode: dispatch the full queue in waves of 3 concurrent, no pause between waves.
- Interactive mode: dispatch in batches of 5 PRs (still capped at 3 concurrent within the batch); after each batch of 5 finishes, stop and run Step 3a before dispatching the next batch.
Coordinator calls state-helpers.sh init <pr#> <pr#>... once, then spawns Agent subagents (subagent_type=executor, model=sonnet) with this worker prompt:
PR #<N>: <title>
Base: <baseRefName> Head: <headRefName>
Worker-id: <unique-id>
Steps:
1. Claim: bash ~/.claude/skills/pr-fleet/state-helpers.sh claim <N> <worker-id>
- exit 1 → another worker has it; pick next PR
2. Create worktree: git worktree add ../wt-pr-<N> origin/<headRefName>
3. cd into worktree, install deps if needed (npm ci)
4. Rebase onto origin/<baseRefName>; if non-trivial conflict → block "<reason>" + skip
5. Run: tsc -b && npm test && npm run lint
6. If failures trivial (lint auto-fixable, type mismatch in your PR's files only) — fix + commit "ci: auto-fix"
7. If failures persist → block "<paste 30 lines>" + skip
8. PRE-MERGE LIVE RE-VERIFY (never trust triage-time state — it goes stale):
gh pr view <N> --json reviewDecision,statusCheckRollup,mergeable
- reviewDecision == CHANGES_REQUESTED → block "CHANGES_REQUESTED still live (sticky until the reviewer dismisses)" + skip. NEVER dismiss a reviewer's review.
- any required check != SUCCESS, or mergeable != MERGEABLE → block "not green/mergeable: <paste>" + skip
9. If clean → push to origin/<headRefName> ONLY. Do NOT merge. (Verify-first: the human merges from the morning report. No `gh pr merge`, no `--auto`.)
10. Cleanup: cd back, git worktree remove ../wt-pr-<N>
11. Mark done: bash ~/.claude/skills/pr-fleet/state-helpers.sh done <N>
12. Return one line: PR-<N>: READY-TO-MERGE | BLOCKED-<reason>
On any hook/firewall block: state-helpers.sh block <N> "<reason>" + exit cleanly. NEVER stall waiting for human.
HARD RULES:
- NEVER edit prisma/schema.prisma, migrations/**, or .github/workflows/**
- NEVER push --force, --no-verify, or bypass hooks
- NEVER merge a PR or dismiss a review — push + report READY-TO-MERGE only; the human merges
- NEVER touch files outside this PR's diff
- Use tsc -b (NOT --noEmit) — CI parity
Wait for all subagents (cap 3 concurrent — batch the rest).
Step 3 — Synthesize
Run bash ~/.claude/skills/pr-fleet/state-helpers.sh report for the per-status roll-up.
Distinguish three exit buckets:
ready— pushed, CI green + review clear (re-verified live), awaiting YOUR merge. List these first with the one-linegh pr merge <N> --squashcommand so the human can merge in seconds.needs_human_decision— worker reported BLOCKED with non-environmental reason (conflict, CHANGES_REQUESTED still live, CI red on legit failure)environmental_blocker— hook/firewall/auth blocker; reproducible mechanical fix
Compose morning report:
PR FLEET RUN <timestamp>
================
MERGED (<count>):
- PR-<n>: <title>
- ...
SKIPPED (<count>) — re-run candidates:
- PR-<n>: <reason>
- ...
FAILED (<count>) — needs human:
- PR-<n>: <reason summary>
ESCALATIONS (<count>) — protected-path, user auth required:
- PR-<n>: touches <paths>
BLOCKED (<count>) — changes-requested:
- PR-<n>: awaiting author
Write to ~/logs/pr-fleet-$(date +%F-%H%M).log.
Interactive mode: skip the morning-report format above and use Step 3a’s checkpoint report instead.
Step 3a — Interactive Checkpoint (interactive mode only)
After each batch of 5 (or when the queue runs out, whichever is first):
- Post a checkpoint summary:
[merged: N] [skipped: M reasons] [escalated: K reasons]— one line per PR, reason inline for skipped/escalated. - Pause for user confirmation. Do not dispatch the next batch or merge anything until the user responds, unless the user pre-authorized the whole run to auto-resume batch-to-batch at the start of the session — in that case still post the summary, just don’t block on it.
- On confirmation, merge the confirmed-ready PRs: for each, re-confirm target branch (
gh pr view <n> --json baseRefName) and that the coordinator’s local checkout is not sitting on the PR’s head branch, thengh pr merge <n> --squash. This is the one place interactive mode differs from autonomous mode — autonomous never merges (push-only, human merges from the log); interactive merges live at the checkpoint once the user says go. - Stop conditions (in addition to the Hard Rules below): backlog cleared; protected-path PR encountered → stop and ask (don’t just log it to ESCALATIONS and continue); usage approaching limit → post the checkpoint summary and exit cleanly rather than starting another batch.
Step 4 — Exit cleanly
git worktree pruneto remove any orphan worktrees- DO NOT cancel any active autonomous mode; the coordinator exits naturally
Hard Rules (coordinator)
- Max 3 concurrent subagents (CC quota)
- Max 20 PRs per run (chunk larger backlogs)
- Stop if 3 consecutive workers report FAILED — likely systemic issue
- Never approve or self-merge own PRs (authorship check via
gh pr view <n> --json author) - Headless mode (
claude -p): no interactive prompts; escalations go to log only. (Interactive mode is never run headless — checkpoints require a live user.)
Headless invocation
~/.claude/scripts/pr-fleet.sh <repo-path>
See script for cron setup.