--- name: multi-pr description: "Chains multiple OpenSpec changes end-to-end: determines implementation order, runs /cla:spec-to-pr on each change in sequence, enforces that every Critical/Important review finding is actually fixed (never left as a deferred TODO) before moving on, merges each PR a later change needs merged — only on the exact head its review and full test gate covered — and finishes with a full repo cleanup pass. Designed for long, unattended multi-change runs. Triggers on /cla:multi-pr or natural language like 'drive all my openspec changes to PRs', 'chain these changes end to end', 'work through every open change and merge as you go', 'run spec-to-pr on everything'." argument-hint: "[change-name-1 change-name-2 ... | (empty = auto-discover every open change)]" # Merges pull requests, so it must never self-trigger on a description match — # only an explicit `/cla:multi-pr` starts it. Safe to set: nothing invokes this # skill programmatically (it is a top-level entry point that CALLS # `Skill(cla:spec-to-pr)`, which stays model-invocable), and `/cla:multi-spec` # explicitly forbids invoking it. Also keeps this long description out of the # always-loaded skill listing until the command is actually typed. disable-model-invocation: true --- # /cla:multi-pr — chain multiple OpenSpec changes to landed PRs Orchestrates `/cla:spec-to-pr` across a **sequence** of OpenSpec changes rather than one. Where `/cla:spec-to-pr` drives a single change from idea to an opened, archived PR and then stops, `multi-pr` adds the layer on top: figure out which order the open changes must ship in, run the full `/cla:spec-to-pr` workflow on each one, make sure nothing gets left half-resolved, merge every change a later one needs before starting that later one, and end with a clean repo. This is the skill for "I'm about to be away for a few hours — take every open change to done." **Resolving `${CLAUDE_PLUGIN_ROOT}`.** This `SKILL.md` arrives with the placeholder substituted, but a `references/` file opened with `Read` carries it literally, and it is not an environment variable in Bash — an unset variable silently runs the command against `/skills/...`. Before running a command that contains the literal text, replace it with the plugin root: the `Base directory for this skill` path with `/skills/` removed, or the absolute path of any plugin file you have read, cut at `.../plugins/cla`. If neither works, say so and stop. Detail: `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/plugin-root.md`. **This skill does not reimplement `/cla:spec-to-pr`.** Every actual propose/review/implement/test/ship/revise/archive step for a single change is `/cla:spec-to-pr`'s job — invoke it via `Skill(cla:spec-to-pr, args=" ...")`. Its Revise phase sizes the reviewers by risk, per "Review sizing by risk" in `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/model-routing.md`. `multi-pr` owns exactly four things `/cla:spec-to-pr` doesn't: **sequencing**, **the upfront confirmation gate**, **the no-unresolved-issues escalation**, and **inter-change merging + final cleanup**. ## Skill-level rules (hoisted — read first) - **`` means THIS repo's default branch, resolved — never assumed.** See `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/base-branch-resolution.md` (shared verbatim with `multi-lite`/`multi-spec`) for the resolution rule every command below that names `` depends on. - **This is a long unattended run. Get every blocking question answered BEFORE the chain starts, not mid-chain.** A clarifying question fired three changes in defeats the purpose — see Phase 1's pre-flight gate below. Once answered, run start-to-finish with no "ready to continue?" pauses between changes, mirroring `/cla:spec-to-pr`'s own autonomy-gate contract one level up. - **Never end a turn with nothing in flight.** The no-pause rule forbids *asking permission* to continue. This failure asks nothing: the orchestrator announces the next change, ends the turn believing it is continuing, and the chain does not pause — it **stops**, silently, until a human notices. The distinction that matters is not "did I ask a question" but **"is there a pending event that will re-invoke this session?"** A backgrounded `Agent` or `Bash(run_in_background: true)` is safe, because its completion notification wakes the session; so is the ordinary `Skill(cla:spec-to-pr, args="…")` call this loop already makes, for the simpler reason that a tool call does not end the turn at all. An idle turn has no wake signal. **The mechanical form, because the judgement version has lost in practice: status text and the next tool call go in the SAME message.** If you have no tool call to pair the text with, the change is not over — start the next one instead of narrating the last. Ending a turn is legitimate in exactly two cases, the same two `${CLAUDE_PLUGIN_ROOT}/skills/spec-to-pr/SKILL.md` names: a backgrounded dispatch is genuinely in flight, or the run is complete. Nothing else — **announcing the next step is not a mechanism**, and "Is there a tool call in this message?" needs no judgement. A blocker is not a third case: surface it with an `AskUserQuestion`, which is itself a tool call — and after the Phase 1b gate **"a blocker" is this closed list and nothing else**: (a) a dependency cycle; (b) a prerequisite the chain cannot supply; (c) an irreversible or billed action outside what the gate authorised; (d) a Tier A structural halt; (e) local infra still unreachable after the self-remediation attempt, under the hard-block policy; (f) a change that cannot be implemented as written. Everything else — a pre-existing bug a task surfaces, a reviewer re-opening an owner decision, a product follow-up, a scope-trim suggestion that does not block the change in flight — becomes a labelled GitHub issue (analysis + options), named in one status line, and the chain continues (GitHub issue #284). **A background command the harness reaps for low memory is not a blocker either:** retry the same command after a bounded backoff (~60 s, then ~180 s; two retries), record the attempts in the running notes, and ask only once both retries fail. Confirming an unattended chain at the gate is the standing authorisation for that bounded retry — it is what the harness's "do not start it again on your own" notice is waiting for (GitHub issue #287). Where a seam genuinely has nothing to dispatch, arm a watchdog before ending the message — `Bash(command: "python3 -c \"import time; time.sleep(1500)\"", run_in_background: true)`, spelled with `python3` because `sleep` matches no pattern in `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/required-permissions.json` and would prompt, which is the stall it exists to prevent — and re-arm it whenever it fires while work remains. Source for the cost of this gap: GitHub issue #130, a chain that lost ~4.5 hours with 4 of 6 changes unstarted. - **"No unresolved Critical or Important issue" is the default policy — stricter than `/cla:spec-to-pr`'s own default** (which accepts a Deferred-Known-Issue as done). A deferred Critical/Important is not acceptable here unless the user's Phase 1 answer explicitly picks spec-to-pr's own triage — there's no point in the run where the user comes back to triage a leftover list themselves. Suggestions are recorded in `TODO.md`, not fixed, unless the user picks the **full-severity** policy at the Phase 1 gate. - **Never run `git add -A`.** Same rule as `/cla:spec-to-pr`, inherited transitively through every `Skill(cla:spec-to-pr, ...)` call this skill makes, and equally binding on any direct git command `multi-pr` itself runs (merges, branch cleanup, prunes). - **No force or history-rewriting git mid-chain** (`git branch -D`, `git push --force`, `git reset --hard`): `ask-destructive-git.py` prompts on each, and an unattended prompt stalls the chain until a human returns (GitHub issue #281). Leftover squash-merged local branches are harmless. Phase 4 tries a plain `git branch -d` on each merged one before it prunes, while `origin/` still lets `-d` succeed; once that tracking ref is gone only `-D` removes them, so the ones `-d` refuses go into the final report as one `git branch -D …` line for the user. Never `ALLOW_DESTRUCTIVE_GIT=1`. Detail: `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/chain-merge.md`, `references/cleanup.md` step 3. - **Verify `git_state` before every commit `multi-pr` itself makes.** Run `python3 ${CLAUDE_PLUGIN_ROOT}/skills/_shared/scripts/git_state.py [--expect-branch ]` before any direct follow-up commit, such as a change-loop step 4 fix landed on a still-open PR or a small follow-up PR (a non-zero exit halts and surfaces — never special-case it). Resolving an exit-2 in-progress op: `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/conflict-resolution.md`. The per-change `/cla:spec-to-pr` runs already do their own git_state checks internally; this covers the commits `multi-pr` makes directly. Working on a worktree the session is not in: `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/bash-discipline.md`, "Working on a worktree you are not in". - **A structural failure in one change's run halts the chain; a content finding never does.** Ship never opening a PR, Archive not reaching the PR, or an unresolved `git_state` fault stop the run and leave every later change untouched (its prerequisite isn't confirmed shipped) — don't conflate that with "the Revise agents found bugs," which is normal, fix-and-continue operation. Full split: Phase 3 step 3. - **Merging is a real, hard-to-reverse action against shared state, and `/cla:spec-to-pr` itself refuses to do it at all.** `multi-pr` is the one layer explicitly authorized to merge — and only for a change confirmed done, only under the merges confirmed at the Phase 1 gate, and only when a later change needs it merged — independents stay open. **Every merge passes the pre-merge checks in `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/chain-merge.md`** (shared with `/cla:multi-lite`): the full gate green on the exact head the run notes recorded, that head unmoved (`--match-head-commit`), and GitHub reporting it `MERGED`, not queued. **"Needs it merged" is two edges, not one.** `depends_on` records the source-level edge; a change that moves **shared mutable environment state** (a migration applied to a shared database, seeded fixture data, a provisioning step) creates a merge-before-next edge regardless of whether anything depends on its code, because that state has already moved for every later branch. Phase 1 establishes both and records them side by side — `references/discover-and-gate.md` §1b. A run that reads "independents stay open" as keyed on `depends_on` alone leaves a migration unmerged and fails the next several changes on a gate unrelated to their own work. - **If another live session is working in the primary clone, pivot to a dedicated worktree.** Nothing detects this for you any more — the hook that used to block a contended commit was deleted, so the contention shows up as confusing git state rather than a refusal. Follow the pivot in `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/chain-merge.md`, "Bootstrap" (`git fetch origin `, then `git worktree add .claude/worktrees/ -b origin/` — two separate commands — `` as `/cla:spec-to-pr` defines it, which is `feature/` only when nothing overrides the prefix) and continue the chain from inside that worktree. - **Run thin (thin-orchestrator standing discipline).** Delegate bulk raw-material handling so only conclusions return; read slices/`tail`/`head`, not whole files/full output; batch independent tool calls into one message; prefer terse structured agent output over prose. Full rules: `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/runtime-rules.md` (this skill is itself an orchestrator, so the same discipline applies). Cost rules for dispatch, tests and debris: `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/orchestration-economy.md`; the same rules reach other agents through the file in `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/economy-rules-file.md`. ## Mode / argument parsing Bind the invocation argument once: **`` = `$ARGUMENTS`**. Every rule below reads ``. - **`` empty** → auto-discover mode. Every open change under `openspec/changes/` (excluding `archive/`) is in scope. - **`` names one or more changes** → explicit mode. Only the named changes are in scope, in the order given (skip the sequencing step's auto-ordering — the user already ordered them). Still run the Phase 1 gate; still validate each name before starting (fail fast on a typo rather than discovering it mid-chain): a real, not-yet-archived change directory passes, and any other name is rejected. Announce the mode and the resolved change list as the first line of output, e.g. `Mode: auto-discover (3 changes found)` or `Mode: explicit (2 changes given)`. ## Phase 0: Bootstrap + working-tree precheck Run the shared bootstrap exactly once before the chain starts, not once per change: `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/chain-merge.md`, "Bootstrap" — the permissions check (the *only* halt-and-ask outside Phase 1), `git_state.py`, and a clean, current ``. A branch with local commits unrelated to any in-scope change (`git log ..HEAD --oneline`) is outside this run: never merge, rebase or push it as part of the chain. Untracked in-scope change directories under `openspec/changes/` survive the checkout to ``; a `git status --porcelain` after it confirms they are still present. Then, from the repo root, `git check-ignore -q --no-index cla.io/retro/multi-pr-run-notes-x.md`. Non-zero → stop: "run /cla:cla-setup first (run notes would be visible to git)". Any ignore source counts here: this run only needs git to ignore the notes on this machine, while `/cla:cla-setup` checks for the repo's own `.gitignore` line. ## Phase 1: Discover + sequence + pre-flight gate **Read `references/discover-and-gate.md` first** — how to discover and order the changes, the two edges `depends_on` cannot express, the full pre-flight-gate question wording, and the chain-time-estimate recipe. Load-bearing invariants (hold these even if the reference isn't reloaded): - **Two chain edges exist that `depends_on` cannot express, and Phase 1 establishes both.** A change moving **shared environment state** (migration, seeded fixture data, provisioning) creates a merge-before-next edge whatever depends on its code — determined per change from `tasks.md`/proposal artifacts, with a NEGATIVE that carries its derivation, and established under every autonomy mode, since an unattended run is where it costs most. Separately, every change carrying a `## MODIFIED Requirements` block is **re-validated** with `openspec validate --strict` against the live spec as it is then, because the batch was authored against one baseline. The shared-state edge is recorded in the running notes and enforced by multi-pr itself when it merges. - **A dependency cycle is never guessed past** — it's always its own explicit question, before Phase 2 starts, regardless of autonomy mode. - **A prerequisite the chain cannot supply is never guessed past either.** Code that exists nowhere is one case; code that sits only on an open PR this chain did not open is the other, since the chain never merges a PR it did not open. Phase 1a's sweep covers every *change* a proposal names as a prerequisite, not just every doc. Either case is its own explicit question before Phase 2, same standing as the cycle. Skipping it is a quiet failure: the chain looks healthy for hours until a dependent reads code nobody wrote. - **Ask everything upfront, in ONE `AskUserQuestion` call (max 4 questions), then run continuous.** This is the **only** point in the whole run where a multi-question stop happens; every phase after it runs autonomously with no "ready to continue?" pauses. - **Recommended defaults** (applied verbatim under the explicit-autonomy override, and offered as the default choice otherwise): merge each PR before the next change that needs it merged — by `depends_on` **or** by having moved shared environment state — independents left open; Critical/Important no-unresolved-issues (Suggestions recorded in `TODO.md`, not fixed; full-severity is the opt-in); `/cla:spec-to-pr` caps at its own defaults `1/2/3`; an unavailable local-infra hard gate hard-blocks the chain (after a self-remediation attempt to bring the stack up). - **Explicit-autonomy override** (an invocation stating "operate autonomously" / "don't ask, just run") and **bounded-window autonomy** (interactive at the start, autonomous once the user signals departure) both apply the recommended defaults above without calling `AskUserQuestion` — except the cycle question, which is never skipped, and the run-permissions ask (`references/discover-and-gate.md` §1b), which is skipped only when no allow rule is missing. Neither widens merging or pre-answers the shared-state edge (`${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/chain-merge.md`, "The autonomy override and merging"). Mid-chain, both modes still surface the closed blocker list above — never guess past one. ## Phase 2: Task tracking Use `TaskCreate` once at the start to lay down the chain itself — one task per change (`": spec-to-pr + land"`) plus a final `"Final cleanup pass"` task. This is the one place in the `/cla:spec-to-pr` family where task tracking is genuinely the right tool (per spec-to-pr's own hoisted `TaskCreate` rule: *"parallel, externally gated or cross-session sub-work"* — a multi-hour, multi-change chain is exactly that shape), unlike inside a single `/cla:spec-to-pr` run where it's noise. Use `TaskUpdate` to transition each change's task to `in_progress` when its `/cla:spec-to-pr` invocation starts and to `completed` only after that change is merged, or, for a change nothing later needs merged, after it is archived with its PR open. The running notes hold one row per change under `## Changes`, `change | branch | pr_number | head_sha | status | merge_commit | notes`, filled in as the chain runs. `status` moves `running` → `pr-open` (change-loop step 3) → `enforced` (step 4b passed) → `merged` (step 5), or ends as `open — ` or `halted — `. A resume merges only from `enforced`. `notes` holds what `chain-merge.md` records beside a status (`behind base: …`, `gate skipped: …`, `base not updated`, `queued: …`), which the final report lists. ## Phase 3: Per-change loop **Read `references/change-loop.md` first** — the full per-change procedure (resume check, real start/end timestamps, invoking `/cla:spec-to-pr`, the merge commands, the actual-vs-predicted timing report) and the resume-behavior mechanics. Load-bearing invariants (hold these even if the reference isn't reloaded): - **Tier A (structural failure) halts the chain here; Tier B (content findings) never does.** Ship never opening a PR, Archive not reaching the PR, an unresolved `git_state` fault, **a live spec set left failing `openspec validate --specs`** (checked at step 4b, BEFORE the merge, while the branch is still alive to fix on), or **a change a later change needs merged that could not merge** (a pre-merge check failed, or the host refused) → stop, surface, and do not touch any later change in the sequence (its prerequisite isn't confirmed shipped). A Revise/Test finding of any severity, however many rounds it takes, is normal operation — proceed to enforcement below. - **No-unresolved-issues enforcement (per the confirmed Phase 1 policy).** A change never counts done while a Deferred-Known-Issue or (only under the opt-in full-severity policy) `TODO.md` Suggestion residue remains for it — fix it, re-verify narrowly, and land it on the same branch (or, if already merged, a small follow-up PR) before moving on. - **An obligation one change creates for a later one is RECORDED in the running notes and CARRIED in the next `Skill(cla:spec-to-pr, …)` call — the record alone is not the mechanism.** A change's own Review and fix rounds can add a stored field, column, response key or required behaviour that the change itself never consumes; its only justification is that a named later change reads it, and that later change's artifacts were authored before the obligation existed. At close-out (change-loop step 4a) write one row per obligation under `## Carried obligations` in the per-run running notes — grep token, creating change, owed-by change, the one-line failure if dropped — or, for a change that owes nothing, a `none` carrying the counts it was derived from, since a bare `none` is satisfiable by typing it. At the start of every later change (change-loop step 2) turn the rows owed by it into `--inherits`, reading **every** `cla.io/retro/multi-pr-run-notes-*.md`, not just this run's: the file is dated, so a resume on a later date writes a new one and scoping the read to it hides everything an earlier session recorded. The flag obliges that change's Review to answer `HONOURED / VIOLATED / NOT ADDRESSED` for each entry ahead of any other finding, and step 3 counts the lines that come back against the entries sent. **A carry list written and then not fed into the dependent's own Review is indistinguishable from never having written it** — feeding it is the whole fix. Source: GitHub issues #98, #100, #102. - **A later change's `path:line` cites are rechecked once before it runs** (change-loop step 2, `scripts/check_cite_drift.py` against the `## Chain start` commit). Cites whose lines only moved are corrected in place; a gone or ambiguous one becomes an `--inherits` entry keyed on its file path, so Review answers it like any carried obligation, and step 3 confirms with `--list` that the cite is gone. Never run the recheck twice on one change: its fixes would move again. Source: GitHub issue #272. - **Merge only a change a later change genuinely NEEDS MERGED** — by `depends_on`, **or** by having moved shared environment state (a migration, seeded fixture data, a provisioning step), which creates the edge regardless of what depends on its code. Phase 1 establishes both per change and records them under `## Sequence` in the running notes; read that, not `depends_on` alone. Genuine independents — neither edge — stay open. **Never merge a head the change's review and full gate did not cover:** record `head_sha` when the PR opens, and merge only through `chain-merge.md`'s checks with `--match-head-commit` (change-loop step 5). ## Phase 4: Final cleanup pass **Read `references/cleanup.md` first** — the full verify/delete/prune/confirm/report sequence, run once after every change in the sequence is done. Load-bearing invariant: this is the one point worth re-running the repo's full build/lint/test (+ any local-infra hard gate) at the CHAIN level even though every change already passed it standalone — a later merge can interact with an earlier one in ways no single change's own Test phase would catch. **Phase 4 makes no commit.** Its chain-log commit went with the chain ledger (see "What this skill deliberately does not do"), so nothing is committed onto the detached HEAD cleanup step 2 can leave behind; if a follow-up commit is ever needed there, check out a named branch first. ## Resume behavior Re-invoking `/cla:multi-pr` after an interruption picks up cleanly — full mechanics in `references/change-loop.md` ("Resume behavior"). Invariant: skip Phase 1's `AskUserQuestion` gate only on a genuine same-session resume; a fresh session always re-asks, since it has no way to know what the previous session decided. A resume with no running notes stops before any not-yet-shipped change that has an earlier change, since its obligations are unknown. ## What this skill deliberately does not do - Does not reimplement Review/Implement/Test/Ship/Revise/Archive — that's `/cla:spec-to-pr`'s job, invoked as a sub-skill. - Does not merge without the Phase 1 gate explicitly authorizing it for this run — merging is a hard-to-reverse action against shared state, and that authorization is scoped to this one run, not a standing permission. - Does not keep a chain-level ledger, and does not build a `multi-pr-retro` analyzer over one. It kept a log-only ledger until that ledger reached 4 records across five repos with no skill reading it, and was deleted along with the other two unread chain ledgers. Chain-level learnings are carried by `/cla:codify-learnings` plus the per-run running-notes file; per-CHANGE retrospective analysis stays `/cla:spec-to-pr-retro`'s job, over the `spec-to-pr-runs.jsonl` each `/cla:spec-to-pr` invocation appends to. - Does not guess past a dependency cycle or an ambiguous merge decision — those are genuinely the user's calls, surfaced once, up front. ## References - `references/discover-and-gate.md` — Phase 1's full mechanics: discovering and ordering the changes, the shared-environment-state and stale-delta-baseline edges `depends_on` cannot express, the pre-flight-gate question wording (merge/no-unresolved-issues/caps/infra policies), the explicit-autonomy and bounded-window-autonomy modes, infra self-remediation, and the chain-time-estimate recipe (mandatory-read from the Phase 1 stub) - `references/change-loop.md` — Phase 3's full per-change procedure (resume check, timestamps, invoking `/cla:spec-to-pr`, the deferred-finding fix recipe, the merge commands, the actual-vs-predicted timing report) and the Resume-behavior mechanics (mandatory-read from the Phase 3 stub and the Resume-behavior stub) - `references/cleanup.md` — Phase 4's full verify/delete/prune/confirm/report sequence (mandatory-read from the Phase 4 stub) - `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/chain-merge.md` — the merge mechanics shared with `multi-lite`: bootstrap, the autonomy override's limits on merging, recording the head, the pre-merge checks, the merge command and its confirmation, a host refusal, and a fix after a merge (mandatory-read from Phase 0 and change-loop step 5) - `cla.io/overlays/multi-pr.md`, if present — rules specific to this skill in this repo, such as a measured default that overrides one below - `${CLAUDE_PLUGIN_ROOT}/skills/_shared/references/base-branch-resolution.md` — the `` resolution rule, shared verbatim with `multi-lite`/`multi-spec`.