staged-rollout
Run a large build as many small, resumable sessions by decomposing it into a `.plan/` folder of dependency-ordered stages with an evidence ledger, then executing one stage per fresh session. Use when
SKILL.mdskills/staged-rollout/SKILL.md
GitHubStaged rollout
Run a large build as many small sessions, not one huge one. Decompose the
work once into dependency-ordered stages in a .plan/ folder, then execute one
stage per fresh session. Context can't accumulate across stages because sessions
don't share it; the plan can't drift because every decision lives in exactly one
place; progress is a glanceable ledger, not a transcript.
The file formats are in references/templates/ (PLAN.md, LEDGER.md,
stage-N.md, stage-f-review.md, README.md, RUNNER.md) — copy those
verbatim for structure, then fill every <placeholder> when scaffolding.
RUNNER.md is the stage-runner contract scaffolded into each plan so any
session — cloud included, where this plugin does not load — can run a stage
cold from a one-line instruction pointing at .plan/RUNNER.md. This file is the
method: when to use it, how to decompose, how to set flags. Don't restate
the templates here.
When to use it
All of these true: the work spans multiple sessions (hours/days, roughly four+ sessions of work); it decomposes into ordered units with dependencies; you want to stop and resume freely; you care about keeping per-session token cost flat; and the design is settle-able (there are decisions worth freezing).
When NOT to use it
- Work that fits in one to three sessions. The scaffold has a floor cost; below ~four sessions, just do the work.
- Exploratory work with no settle-able design. If every session would legitimately rewrite the frozen decisions, there's nothing to freeze yet.
- Work that can't be decomposed. One giant inseparable step gains nothing from a ledger around it.
Two honest limits even when it fits: decomposition quality gates everything (bad stage boundaries cause cross-stage churn no protocol fixes — the final review stage catches what leaks, but it can't un-tangle a bad split); and fresh sessions only know what was written down (note discipline replaces the tacit context a long session would carry).
Core principles
- Single source of truth, referenced not copied. All durable decisions live
in
PLAN.mdas frozen decisions. Stage files and prompts point at it; they never restate it. Copies are what drift — a decision that exists in one place cannot diverge. - Session-per-stage = free context control. The primary token-control
mechanism isn't subagents or clever prompting — it's that each stage is small
and runs in its own fresh session. Cost per stage is flat
(
O(PLAN.md + stage file + ledger table)), no matter how many stages came before. No compaction spiral. - The ledger is the resume point and the memory.
LEDGER.mdholds per-stage status plus as-built notes, so "where were we?" is a 10-line table, and a later stage can catch a regression an earlier one introduced because the earlier stage's assumptions were written down. - Verify before done. A stage is done when its acceptance check ran and the real output is pasted into the ledger — not when the model claims success. Evidence, not assertion.
Decomposing the work
- Smallest sensible stage. If a unit has two genuinely different mechanisms, or a design-heavy part plus mechanical parts, split it.
- Group by effort, not just by feature. Several near-identical mechanical units can be one stage; a single design-heavy unit deserves its own.
- Keystone as S0. Identify the piece with no prerequisites that everything else needs, make it S0, and gate the rest behind it.
dependsis a graph, not a queue. An edge means "cannot safely start until", not "written after". Serialising by habit — writingS0 → S1 → S2 → S3where the truth isS0 → {S1, S2, S3}— is the most expensive decomposition mistake available here, because the index still reads as correct while the plan takes three rounds instead of two. For every edge, name the artifact the dependent stage consumes; if you can't name one, drop the edge.- Every stage changes something or feeds a stage that does. A stage whose
only output is a finding, a verification or documentation — "confirm X",
"investigate Y", "record Z" — earns its place only when a later stage lists
it in
dependsand names what it consumes from it. No consumer: fold the investigation into the stage that would act on it, or drop it. A finding that nothing downstream uses spends a session and changes nothing. The standing review stage is the one exception, because its three outcomes are themselves actions. - Group by gate. Alongside "group by effort": put the
gate: humanstages — the ones where frozen decisions get settled or amended, or whose acceptance needs a person — at the front of the dependency graph, and the review stage at the end; keep the middle mechanical,gate: autoand (when the plan opts in)merge: auto. Never interleave a human gate between two auto stages unless a dependency edge genuinely forces it: an unattended runner stops at everyhumanstage, so a human gate in the middle of an auto run cuts the run in two for no reason. Decisions that only surface mid-build still go throughblocked(see Statuses and human-gated stages) — the rule shapes the graph, it doesn't forbid surprises. - Standing final review stage. Always append
SF: plan reviewas the last stage (see below), scaffolded fromstage-f-review.md. Bootstrap adds it; it's not optional.
Flag heuristics
Each stage declares depends / mode / exec / model / effort / gate
in the PLAN.md stage index — the single authoritative home for these
flags, read by /stage-run's weight check and the runnable-set logic both
commands share. The plan as
a whole declares two more, merge and plan-dir, on the plan flags line
directly under that index — those two are the plan's declared defaults, the
answers an unattended session applies where an interactive one would ask (see
Unattended mode). Stage files never restate any of them. The process
defaults are deliberately cheap — escalate mode, exec and gate only
where a stage genuinely warrants it. model/effort are not among them:
those are sized to finish the stage, not to be minimised (see below).
Linking a stage to its issue. A stage file may carry one optional
Issue: line directly under its title, naming the issue the stage came from:
#123, owner/repo#123 or an issue URL. It is a pointer, not a flag — it
changes nothing about how the stage runs, which is why it lives in the stage
file and not in the stage index. Commands read it as follows: the first line
of the stage file that starts with Issue:, above the first ## heading,
whose value is one of those three forms. No such line, a blank value or an
unfilled template placeholder all read as "no linked issue", silently, so a
plan without the field behaves exactly as before. The field is read-only for
the tooling: no command posts to the issue or changes its status because of
it.
mode: directby default (state a one-line plan, implement). Usebrainstormonly where the stage has real open design choices. A full brainstorm on a mechanical one-liner is pure ceremony.exec: inlineby default. Session-per-stage already isolates context, so reservesubagent(<model>)for churn-heavy stages (lots of iteration, config, debugging) where dispatching keeps the churn out of the orchestrator's context.model/effortare launch hints, not switches the agent can flip mid-session. Both are verifiable: the model from the session itself, and the effort level fromCLAUDE_EFFORTin the environment — so state what is actually set rather than restating the flag as a reminder. When that variable is absent, say the effort could not be read instead of assuming it matched. Pick the tier that would carry the stage to its exit criteria without the session having to escalate — not the cheapest tier that could plausibly start it. A stage that stalls and gets re-run costs more than the tier it saved, and because the re-run happens in a fresh session the plan never records that it happened. Where two tiers are arguable, take the higher one. Reserve the top model for the keystone and the one or two design-heavy stages; much staged work still lands atlow/medeffort, but that is an observation about past plans, not a target this one should hit.gate: autoby default.gatesays whether a stage may be launched unattended — by an orchestrator session that fires stages back-to-back with nobody watching them run (seereferences/remote-driver.md); this flag is the contract it reads. Agate: humanorgate: localstage is never launched unattended: the orchestrator stops in front of it and reports rather than firing, and a session that finds itself running one unattended (see Statuses and human-gated stages) reports and stops rather than starting it. Markhumanwhere a person must be present for the stage to get anywhere: everymode: brainstormstage (a design pass is a conversation), and any stage whose acceptance needs a human's eyes or hands (a visual check, a GUI-only action, a credential). Marklocalwhere the stage needs a resource only the local machine has, known at authoring time — local hardware, a LAN-only host, a secret not committed anywhere reachable, or a locally-installed toolchain. The two gates are independent: the driver refuses either exactly the same way, but for a different reason —humanbecause nobody is watching,localbecause the driver could be running anywhere but the machine the stage needs. A stage that only discovers this mid-run, with nothing declared up front, uses theneeds-localblocked reason instead (Statuses and human-gated stages, below). Whyautois the default and nothuman: the flag changes nothing until something runs stages unattended — today, and for any plan that never adopts a driver,merge: manualalready stops at every merge whatevergatesays, so anautodefault costs existing plans nothing and keeps a fresh plan closest to today's fully-manual experience. The conservative alternative (humanby default, opt stages into unattended) would make bootstrap upgrade every mechanical stage by hand instead of downgrading the few that need a person; it was considered and is the right call only if unattended runs turn out to misfire on stages that looked mechanical at decomposition. An absentgatecolumn reads asauto— plans written before the flag existed need no edit.merge: manualby default — plan-level, not per-stage.mergesays what happens to a stage PR once it is open: undermanualthe session offers the merge and waits for your OK (today's behaviour, unchanged); underautoit merges the stage PR into the plan branch itself — still a squash, still only after the sibling re-sync check, and only once every required check is green — then carries straight on to thedonewrite and teardown.autois opt-in because any other default would silently change the merge behaviour of every plan that predates the flag, and it governs stage PRs only: the plan→main PR is manual in every mode, with no override (see Git model). An absent plan-flags line reads asmerge: manual.plan-dir: deleteby default — plan-level, and read only at closeout.plan-dirsays what happens to.plan/when the plan is closed: underdeleteit goes as the last commit on the plan branch (nothing is lost — the full plan history stays in git, and the final PR shows the removal); underkeepit stays, for a project where the plan doubles as its documentation. This is the answer/plan-closealready calls its default, written down in advance so an unattended closeout has it — an interactive closeout still asks, with this value as the recommendation. An absentplan-direntry reads asdelete, so nothing changes for a plan that predates the flag.
Model weight tiers
Every weight check (bootstrap's gate, a stage's model comparison) needs a
mechanical rule for "is this session heavy enough" — not the model's own guess
about itself. Maintain this tier list as model families evolve, and place a new
family by its capability and price, not by where its name sorts:
- Top tier ("Opus-class"): the Opus generation (e.g.
claude-opus-*), plus any frontier family positioned at or above it (e.g.claude-fable-*). - Mid tier ("Sonnet-class"): the Sonnet generation (e.g.
claude-sonnet-*). - Light tier ("Haiku-class"): the Haiku generation (e.g.
claude-haiku-*).
Fail-safe: if the session's disclosed model ID or name doesn't recognizably match a tier above — an unfamiliar family, a third-party model, a future rename — do not guess which tier it belongs to. State the exact model ID/name from the system prompt and ask the user which tier applies, rather than silently passing or failing the gate.
Parallel stages
depends is a real dependency graph, so more than one stage is often runnable
at once. Three rules keep that an advantage rather than a source of confusion:
- Derive, never store. The runnable set, the waves, and the critical path
are views of the
dependscolumn, computed on demand — by bootstrap's post-decomposition summary and by every stage's end announcement. Do not add awaveorparallel-groupcolumn to the stage index: that would be a second copy of the graph, and principle 1 exists precisely because copies drift. - Report the set; don't launch it. The deliverable is telling the operator
what can overlap — every
todostage whosedependsare alldoneorskipped, each with its command, recommended model/effort andgate. Starting them is the operator's action, one session per stage — or an orchestrator's, firing cloud stages and honouringgate. In the desktop app a stage can offer each one as a suggestion chip, but the click is still the operator's: the chip cannot pick its launch mode and can set model and effort only downward (if at all, depending on which chip tool the session has), and it exists nowhere else (not on a phone, not in a terminal), so the printed command stays the handoff every surface can use. - Separate working trees are what make it physical. The semantics below
make concurrent sessions safe; worktree-per-stage (see Git model) makes
them possible. Two sessions sharing one working tree fight over
HEADwhatever the merge rules say.
Four semantics are what make concurrent sessions safe rather than merely
possible. They are specified in full in the template PLAN.md, which owns the
operating protocol; the reasoning behind them is the method:
- The plan branch is the serialization point. Parallel stage PRs merge one at a time, first come first served. The second merger syncs the plan branch into its stage branch and re-runs the acceptance check — "mergeable" means no textual conflict, not that the stage still passes. Squash merge makes that free (the merge commit is discarded), which is why a stage branch is never rebased or force-pushed.
- A sibling's stage branch is not drift. Preflight classifies by whose
stage a mismatch belongs to instead of halting on any
todorow with a committed branch — otherwise every parallel session stops the moment a second one starts. Real drift on the stage you are running still stops you, and a genuinely crashed stage stays visible in every later preflight report and in closeout's gate. - Shared write territory is a
dependsedge, not a new field. Two stages that write the same files are not independent, whatever the feature graph says, anddependsis the only place that can say so. A dedicated "territory" field would be a second record of one constraint — a second thing to drift. - The
doneledger write races. It is a direct commit on the plan branch, so two sessions finishing together collide there: replay the commit on rejection, keep both rows on conflict, never force-push the plan branch.
Is exec: subagent(<model>) fan-out an alternative to parallel sessions?
Only inside a stage — never as a substitute for them. Dispatching a whole
wave from one orchestrator does sidestep git concurrency, but it collapses N
stages into one: a single branch, PR, ledger row, and acceptance check
covering work the decomposition deliberately kept separate (git semantics 1
and 3) — and the orchestrator accumulates every subagent's return, so
per-session cost stops being flat and principle 2, the mechanism the whole
method rests on, goes with it. For a wave of cheap mechanical stages the
honest options are therefore: run them as N sessions (the supported answer),
or decide at decomposition time that they were really one stage, merge
them, and let exec: subagent(<model>) absorb the churn within it. That is a
decomposition decision, not an execution one — "group by effort, not just by
feature" already points at it.
Statuses and human-gated stages
Statuses are todo → doing → done, plus blocked and skipped (full lifecycle
and the checkbox resume mechanism are in the templates). Two are worth calling
out as method, not just vocabulary:
blockedis a first-class state, not a failure. A stage that hits a gate only a human or an external system can clear (a GUI-only action, a credential, an approval) is best written as a runbook: produce exact step-by-step instructions plus the verification check, mark the stageblocked/doing, and let the human complete it. Never fake progress past a gate. Where that record is committed is settled once, in the templatePLAN.md's operating protocol under Recording a block — on the plan branch directly when the block predates the stage branch, and on the stage branch plus a.plan/BLOCKED.mdsection on the plan branch once it exists. That is the single source of truth for the rule. Neither this skill nor/stage-runrestates it — they only name which side of it a given decision point falls on.needs-localis the reason value for one specific case: a stage that discovers mid-run — nothing declared asgate: localup front — that it needs a resource only the local machine has. Sameblockedstate, same commit rule, but the one-line reason is the literal tokenneeds-localrather than free text, so an orchestrator's report can say "re-run this stage locally" instead of a generic failure (PLAN.md, Recording a block, "The discovered case").- Unattended, a stage question that has no declared default becomes
blocked. Mark the stageblockedwith a runbook stating the question and what would unblock it, commit that where Recording a block says, and stop. This is the existing state and the existing mechanism, not new machinery; the only rule unattended mode adds is that waiting on an answer is not an option, because there is nobody to give one. The human answers later by amending the frozen decisions or the ledger and relaunching the stage. Which questions have a declared default and which are hard stops is the table in Unattended mode, below. skippedrecords a one-line reason for work decided against, so the gap is a decision, not a silent hole. It satisfies a dependent'sdependsexactly likedone— the runnable set never deadlocks on a stage that was deliberately dropped. If the skipped stage owned acceptance or verification work (a check nothing else covers), say so in the same note: that coverage is now unowned, and the final review stage (SF) is where it gets reassigned or explicitly accepted as a gap — never silently lost.
Track known gaps and latent hazards explicitly in the ledger notes (things not under version control, footguns, "this script would delete X if run") — writing them down is what stops them becoming surprises, and it's what lets the final review stage catch them.
Unattended mode
One mode, one rule, honoured at every decision point. A session is
unattended when nobody can answer it: a command was told so explicitly with its
--unattended argument, or its opening prompt says so in plain words. That
last path is how a cloud session enters this mode: plugins do not load in
cloud containers, so there is no command and no argument to carry the flag —
the orchestrator that fired the stage (see references/remote-driver.md)
states it in the prompt instead. However a session
learns it, the contract below is identical. That argument is a single switch selecting declared
default over ask — never "proceed anyway". Interactive sessions keep asking
exactly as they always have, and one body of skill text serves both modes. A
fork into interactive and unattended copies is the anti-pattern this contract
exists to prevent: two bodies means every protocol change made twice, and the
seams between the modes are subtle enough that the second copy would be the
one that rots.
Every question the protocol can put to a person is classified once, as one of two kinds:
- Declared default. The answer is fixed ahead of the run — written on
PLAN.md's plan flags line (merge,plan-dir), or a mechanical rule that needs no answer at all. An unattended session applies it and carries on; an interactive one still asks, with the declared value as the recommendation. - Hard stop. There is no defensible default, so an unattended session
does not invent one. It records the question where the next session will
find it — the stage row marked
blockedwith a runbook, committed where Recording a block says so it is readable without waiting for a merge, or, where no stage row owns the question, a report naming the exact state and the command that clears it — and ends. Nothing is faked past a gate and nothing is retried.
| Decision point | Interactive | Unattended |
|---|---|---|
A gate: human stage |
announced — the person at the keyboard is the gate | hard stop, never started |
A gate: local stage |
announced — running it here means this session already has what it needs | hard stop, never started |
| Weight check: lighter model than recommended, or a readable effort that differs from the flag | hard stop — relaunch named, nothing written | hard stop — blocked + runbook |
| Weight check: an unrecognised tier | asked which tier applies | hard stop — blocked + runbook |
| A mid-stage question the frozen decisions don't settle | asked | hard stop — blocked + runbook |
Redo of a done stage |
confirmed first | hard stop — blocked + runbook |
| A stage PR's merge | offered | merge flag — auto merges it, manual is a hard stop |
Checking out the plan branch to reach .plan/ |
offered | default: check it out when exactly one plan branch matches; two or more is a hard stop |
| A stage worktree still present at closeout | offered for removal when its branch is merged and nothing is unpushed | removed on that same condition; anything else is a hard stop |
Deleting .plan/ at closeout |
asked | plan-dir flag |
| Merging the plan→main PR | the PR is proposed; you merge it | the PR is opened; you merge it — no session merges it in any mode, no flag, no override |
Every blocked + runbook cell above means the record Recording a block
defines. The weight-check and redo hard stops fire before the stage branch
exists, so they commit straight onto the plan branch; a mid-stage question
fires after it does, so it lands on the stage branch and is announced on the
plan branch through .plan/BLOCKED.md. The distinction matters more unattended
than anywhere else: a runbook left on an unmerged branch with nothing on the
plan branch pointing at it is one the next pass never reads.
What no mode loosens. The plan→main PR is opened by closeout and merged
by a person, always. A gate: human stage is never launched unattended, and
neither is a gate: local one — same refusal, different reason. A
worktree holding real uncommitted or unpushed work is never removed, and never
with --force. A merge the platform refuses is never forced or retried.
Bootstrap has no unattended mode, deliberately. /plan-stages is design
work — decomposition, frozen decisions, the merge question — and those have
no defensible defaults to declare. A plan decomposed badly costs far more than
the session it would have saved. Where a plan genuinely needs to be bootstrapped
headless, a fully-specified brief that says to make every decision and ask
nothing does the job as an ordinary prompt; that route needs no contract behind
it.
Git model
Branch-per-stage in a worktree-per-stage is the only supported model — it's the model this plugin was built with, and there is no alternative to choose at bootstrap:
main
└── plan-<slug> ← plan branch; .plan/ lives here
├── plan-<slug>-s0 → PR → plan-<slug> (squash merge)
├── plan-<slug>-s1 → PR → plan-<slug> (squash merge)
└── ...
plan-<slug> → final PR → main ← at closeout (normal merge)
.plan/ must be tracked, and the plan branch must have an upstream. Both
are load-bearing invariants, not tidiness. An untracked (or .gitignored)
.plan/ breaks the model in two ways at once: a stage whose only artifacts are
decisions or documentation produces nothing to commit, so it can never open the
PR that semantics 3–4 below require, and every stage depending on it deadlocks
on an unsatisfiable gate; and the whole decision record lives only in a working
directory that a git clean or a deleted worktree takes with it. A local-only
plan branch is the quieter version of the same failure — the preflight's fetch
and fast-forward both succeed and do nothing, forever. Bootstrap refuses to
scaffold into an ignored path and pushes the plan branch with an upstream;
every stage preflight re-checks both.
Seven frozen semantics:
- One branch per stage, cut from the plan branch (
plan-<slug>) — no exceptions. Uniformity keeps each unit reviewable in isolation and contains the classic failure where "one small commit" quietly becomes twenty commits of fixes bleeding into shared history. - Commits are compulsory and incremental — commit at logical units as the stage progresses, not a single commit at stage end. Every stage has something to commit: the ledger evidence and any frozen-decision amendment are tracked files, so even a decision-only stage lands a real commit and a real PR.
- A stage PR into the plan branch is compulsory — the finish protocol creates it; it is never "offered" as optional.
- A stage cannot be closed (marked
done) until its PR is merged into the plan branch. - After the merge, return to the clone and fast-forward the plan branch
before the session ends — the clone is already on it, so there is no
checkout — and record the stage
donein the ledger there: thedoneedit is committed on the plan branch after the merge, never on the stage branch, so adonerow is always visible from a synced plan branch. - Merge type is fixed by position: each stage PR is squash-merged into
the plan branch (one clean commit per stage, no intra-stage churn on the plan
branch); the final PR from the plan branch into
mainis a normal (non-squash) merge, so every stage lands onmainas its own distinct commit and the as-built history survives. This is the one rule the plan cannot enforce, because it is the one merge no session performs: the person merging gets the repo's default merge button, and a default of "Squash and merge" collapses every stage into one commit onmainwhile still looking like a clean, successful merge. Set the repo's default to "Create a merge commit" when the plan is set up — that is the only real control; closeout naming the merge type in the PR body is a reminder. - One worktree per stage, and the clone never leaves the plan branch. A
stage branch is checked out only in its own sibling worktree
(
../<repo>-s<N>); the main clone stays parked onplan-<slug>for the life of the plan. See Worktree-per-stage below.
Also: flat branch names (plan-<slug>-s3, not plan/<slug>/s3) — git
refs can't nest a branch under an existing branch name. And push freely,
offer merges: stage and plan branches are feature branches — the agent
creates and pushes them without asking, and opens the stage PR into
the plan branch as part of the compulsory finish protocol, but offers the
merge for your OK — it never merges without your OK, never pushes to main,
and the final PR to main is always yours to merge.
The one carve-out: merge: auto. A plan that sets merge: auto on its
plan-flags line (see Flag heuristics) has given that OK in advance, for
stage PRs only — so under auto the session squash-merges its own stage PR
into the plan branch once the sibling re-sync check has run and every required
check is green, and continues to the done write and teardown without
stopping. Nothing else loosens: the merge is still a squash, the re-sync rule
still applies, and a merge that GitHub refuses (a red or missing check, a
branch-protection rule the plan branch carries) is not retried or forced —
the session leaves the row doing, reports the refusal and why, and ends; the
next preflight completes the bookkeeping once a person merges it, exactly as
when a merge is declined today. The plan→main PR is manual in every mode.
merge is never read at closeout, and no value of it — nor any future flag —
creates a path that merges into main without a person's explicit OK. That is
the one human gate that survives even a fully unattended plan.
Worktree-per-stage
The clone holds the plan; worktrees hold the work. The main clone is
permanently parked on plan-<slug> — that is the only branch ever checked out
there. Every stage branch lives in its own sibling worktree, created from the
plan branch tip:
~/src/
hive/ ← main clone, always on plan-<slug>, holds .plan/
hive-s1/ ← worktree, branch plan-<slug>-s1
hive-s3/ ← worktree, branch plan-<slug>-s3 (concurrent)
This is fixed, not a choice — the same register as branch-per-stage. Three things follow from it, and they are why it is worth a frozen semantic rather than a suggestion:
- The ledger is always readable and always writable. Because the clone
never moves off the plan branch,
.plan/there is the synced plan-branch copy at every moment. Thedonewrite (finish step 5) is a commit in the clone that needs no checkout and cannot disturb an in-flight stage. - Concurrency stops contending for
HEAD. Parallel stages above makes concurrent sessions semantically safe; separate working trees are what make them physically possible. Two sessions in one directory fight over the checkout no matter how correct the merge rules are. - Provisioning prefers the harness, falls back to git. Use the harness's
native worktree mechanism when there is one (Claude Code's
EnterWorktree, orsuperpowers:using-git-worktreeswhen installed) — but only when it honors the exact branch and path names above (the templatePLAN.mdowns the full rule); otherwisegit worktree add. What it must never do is degrade to checking the stage branch out in the clone — if the harness refuses to work outside its original directory, the honest move is to stop and hand the operator the path to relaunch in.
Two honest costs, named where they bite rather than discovered later. A fresh
worktree contains only tracked files, so untracked local setup a stage needs
(.env, local config, build caches, node_modules) is not there — copy what
the stage needs and note it in the ledger. And a worktree is a real directory
that outlives a crashed session, so teardown is part of the protocol: after
the merge, a clean and fully-pushed worktree is removed along with its merged
branch, while anything uncommitted, unpushed, or stashed is left alone and
reported. Preflight reports orphans; closeout gates on any that survive —
removing the ones that are merged and fully pushed, and refusing to close
while one holds work git cannot recover (see Closeout).
Preflight & sync — verify git state before trusting the ledger. The
ledger is canonical, but only after it's proven fresh: every stage session
and the closeout start with a preflight block, defined once in the template
PLAN.md's operating protocol — confirm .plan/ is tracked and the plan
branch has an upstream, fetch, fast-forward the plan branch (holds
under both squash-merge and merge-commit remotes), require a clean tree in
both the clone and this worktree, apply the two-tree rule to HEAD (the
clone on the plan branch, the stage on its own worktree), and reconcile the
ledger rows against actual branch, PR, and worktree state. One state is
self-healing (a doing row whose PR merged remotely gets its done
recorded); one is expected under concurrency (another
stage's in-flight branch — reported, not fatal, see Parallel stages);
everything else is drift, and the preflight reports and stops — it never
auto-stashes, resets, or deletes branches.
The final review stage
SF is the one stage exempt from the read-scope rule: it reads the entire
ledger — every note, gotcha, shortcut, and known gap — and sweeps for stragglers.
Crucially, it catalogs; it never implements. Each finding becomes exactly one
of three outcomes:
- A new stage in this plan — follow-up work belonging to this project. It gets a PLAN.md stage index row (with its flags — required, since the weight check and next-runnable logic only see stages listed in the index), a ledger row, and a stage file, and runs later as a normal stage in its own fresh session and branch.
- A spin-off candidate — work that has outgrown this plan (a genuinely new project). Recorded in the ledger and surfaced in the final PR body as follow-up; it does not block closeout. Start it later with its own bootstrap.
- An explicit "accepted, won't fix" — with a one-line reason, so the gap is a decision instead of a surprise.
Its acceptance check: every loose end in the notes is either a new stage (a stage index row, a ledger row, and a stage file) or explicitly closed.
Closeout
Closeout refuses to run until every ledger row is done or skipped (including
stages the review spawned) and no stage PR into the plan branch remains
open or unmerged — a done row alone is not enough; the preflight's
reconcile runs first and treats that mismatch as a gate failure. Then it: distills PLAN.md + the ledger into the
final PR body so the why and the as-built story survive on main; deletes
.plan/ as the last commit on the plan branch (nothing is lost — the full plan
history remains in git; keeping .plan/ is the plan-dir: keep option, for a
plan that doubles as documentation); and proposes the PR from plan-<slug> to
main for the human to review and merge.
Stage worktrees are part of closeout's gate, under one rule in both modes.
A surviving worktree whose branch matches plan-<slug>-s* means some stage
never finished its teardown. If that worktree's branch is merged into the plan
branch and it holds nothing unpushed, it is finished work and removal is safe:
interactive closeout offers to remove it, unattended closeout removes it.
Anything else — unpushed commits, an unmerged branch, work that is not
recoverable from git — stops closeout in both modes, with the path and what it
holds reported. An operator's unrelated worktree (any other branch) is none of
the plan's business and never blocks.
Closeout runs unattended too (/plan-close --unattended), and an
orchestrator can fire it once every stage is done or skipped, so a plan can go from
bootstrap to an open plan→main PR with exactly two human gates: a gate: human
stage, and the final merge. See Unattended mode.
Anti-patterns this exists to prevent
- Restating decisions in prompts or stage files — copies drift; point at
PLAN.md. - One giant stage — blows context, can't resume; split it.
- Brainstorming everything — design ceremony on mechanical work;
directis the default. - Subagents everywhere — session-per-stage already isolates context; reserve them for churn.
- Claiming done without evidence — the acceptance output must actually land in the ledger.
- Silent scope creep — "while I'm here…"; note it, spin a stage, move on.
- Editing decisions in two places — frozen decisions change in
PLAN.mdonly. - Skipping the dependency gate — building on an unbuilt prerequisite.
- Checking a stage branch out in the main clone — the clone is the plan's window; moving it hides the ledger and breaks every concurrent session.