Appearance
Flight Plan — Persisted, Ordered Warp Run Plan
A flight plan amortizes warp-drive's discovery: derive the ordered "what to work on, in what order" once, persist it as a GitHub Issue, and run a warp session against it — instead of re-discovering the queue every cycle. It also leaves a durable, human-readable record of what a session set out to do.
Part of the amortized warp planning capability (#1067). This page covers the artifact and its --flightplan consumer (#1068); cheap mid-run revalidation of the plan is the sibling requirement (#1069).
TL;DR
bash
/flightplan area:amortized-warp-planning # derive + persist the ordered plan → prints #N
warp-drive --flightplan <N> # run it: in order, never widening, ticked as it goesThe plan is a GitHub Issue labelled flight-plan — never a local file.
/flightplan is a universal command (commands/flightplan.md, #1872): cdi links it into every project alongside /warp-drive, so it needs no manifest entry. A manifest that still names it is reported by cdprov as [already-global] … (universal) and cdprov add command flightplan is a no-op — drop the entry; nothing breaks either way.
What it is
- An issue, labelled
flight-plan, whose body is an ordered checklist:plus machine markers (- [ ] #1068 Issue-backed flight-plan artifact — priority p3-medium, req - [ ] #1069 Cachable warp-drive discovery — priority p3-medium, req<!-- flightplan:scope … -->,fingerprint,cut, and — when declared — the integrationbranch, #1181). - A fixed scope + order a warp run consumes — the same "ordered list that never widens" semantics as
--issue N,N,N, plus check-off and progress notes. - A live record: as each requirement completes, its checkbox is ticked and a one-line progress note is appended, so the issue shows the run's progress at a glance.
Why it exists
Warp-drive re-derives "what's next" on every cycle. That is deliberate (project state changes as work lands), but it burns tokens, and the ordering/rationale it produces is thrown away each cycle — leaving no readable record of a session's intent. A flight plan derives the order once, persists it in the existing source of truth (GitHub Issues), and lets the run consume it — cutting repeated discovery cost and leaving a durable trail.
It is not a new source of truth. The ordered queue already exists as approved
- priority +
serial-onlyin GitHub Issues./flightplanreuses that ordering (discover-queue.sh, the same queue/what-nextand/groomderive from) and only records it. It does not re-implement cluster reconciliation.
Creating a plan
bash
/flightplan # whole approved project queue
/flightplan area:<slug> # one workstream
/flightplan 1068,1069 # a specific ordered issue listAn explicit issue list keeps its given order verbatim (#1508) — the list is the ordering decision, which is exactly what you reach for when the label-derived order is wrong (e.g. a body-level "Blocked by #NN" the queue rule can't see). Area and whole-project scopes order by the shared queue rule (priority → bug before req → issue number). The membership filter still applies to an explicit list: an issue that isn't req/bug, carries blocked, or isn't approved, is dropped — and dropped issues are named, with the reason, never silently omitted (#1794):
$ /flightplan 8,9
[dropped] #9 proposed item — not `approved` — pass --allow-unapproved to seat it anyway
Created flight-plan #4242 (issues:8,9) — 1 item(s), …, 1 issue(s) droppedNaming an issue number is not the same as approving it. A warp-drive --flightplan run works its items in order and never re-checks approval at pickup, so an un-approved issue seated into a plan gets worked autonomously with no further human touchpoint. The approved label is that touchpoint, and an explicit list does not substitute for it.
When you genuinely intend to plan un-approved work, --allow-unapproved is the opt-in — and it is loud rather than silent, reporting every issue it admits:
$ /flightplan 8,9 --allow-unapproved
[un-approved] #9 proposed item — un-approved, seated by --allow-unapprovedIt relaxes only the approved half of the rule; blocked and the req/bug type filter still apply. With --json, both accounts are in the payload as dropped[] and unapproved[], so a caller can assert on them instead of parsing prose.
Under the hood the command calls the engine:
bash
node ~/.claude/scripts/warp-drive/flightplan.js build --area <slug> --json
# → { "plan": 1070, "scope": "area:<slug>", "items": 2, "fingerprint": "69c8…", "refreshed": false, "carried": 0 }build is idempotent per scope: it finds the existing open flight-plan issue whose embedded scope marker matches and refreshes it in place rather than creating a duplicate ("refreshed": true).
Declaring an integration branch (#1181)
A plan can declare the stream its work accumulates on, so warp-drive --flightplan <N> resumes the whole workstream — scope and branch — from one flag:
bash
node ~/.claude/scripts/warp-drive/flightplan.js build --area <slug> --branch stream/<slug> --jsonThe declaration is recorded in the plan body (a human-readable Integration branch line plus a <!-- flightplan:branch … --> marker) — the plan issue stays the single durable record of the workstream's intent. A refresh preserves the declaration when neither flag is passed; --no-branch clears it. /flightplan asks about the branch as an interview question when the checkout already has a stream bound (_branch.session) or you request one.
At kickoff, a flightplan-scoped run with a declared branch opens/resumes that stream automatically — the same code path as warp session start <branch> --flightplan <N> (#1177), including the start-of-work freshness sync (#1178). A declaration conflicting with a different already-pinned stream in the checkout is refused with a clear message (never a silent rebind): end the stream (warp session end / warp finalize), re-bind explicitly, or update the plan. A plan with no declaration behaves exactly as before — per-requirement merge, or whatever stream is manually bound. flightplan.js status reports the declared branch, and warp status shows flight-plan #N → stream <branch> during the run.
Groom first if needed.
/flightplanrecords the order that already exists — it does not compute it. If the cluster's dependency chain,serial-only, orapprovedlabels aren't settled yet, run/groom [area:<slug>]first.
Pre-flight interview (#1087)
/flightplan does not record the scoped issues blindly: before the plan is cut it scans them for open questions and interviews you to close as many as possible, so warp-drive launches with maximum resolved context.
- What's detected: "Open question(s)" / "Decisions needed" sections and unchecked decision checkboxes (
flightplan.js scan), plus ambiguity the model spots while reviewing the issues (vague or contradictory ACs, unmade decisions in the Notes). - How you're asked: one question at a time, with proposed answers where inferable — via
ask_remotewhen RDB is enabled,AskUserQuestionotherwise. Every question can also be deferred. - Where answers land: in the target issue, never a local file. Scanned questions get a dated
**Resolved (flight-plan interview YYYY-MM-DD):**marker on their own line (flightplan.js resolve); model-detected ambiguity is resolved via a clearly-attributed issue comment. - Idempotent: resolved and deferred questions carry their marker in the issue body, so re-running
/flightplanfor the same scope never re-asks them. - Carried, not dropped: deferred questions (and still-open ones under
--no-interview) are listed in the plan body under "Open questions carried into this plan". A deferral that awaits a human-only action (paid account, API key, sign-off) additionally gets a conforming assignedtodofiled — see Gating human-action todos below. - Headless:
/flightplan <scope> --no-interview(engine:build --no-interview) skips the interview. Without the flag,buildrefuses (exit 3) whileopenquestions remain — the gate that keeps interview-on the default.
Gating human-action todos (#1088)
Some blockers surfaced at plan-build time are human-only actions — enable a setting, provision an account, flip a switch. Instead of stalling the run, /flightplan files each as a todo issue assigned to the operator (conforming to the todo format contract, with a Blocked by #NN link to each issue it gates), and build seats it in the plan as a gating item:
- [ ] #900 **[human todo]** Enable Remote Login on the Mac — todo, gates #602
- [ ] #602 Signal extraction — priority p2-high, req- Seating is automatic and idempotent. Every
buildre-derives gating rows from live opentodoissues: a todo gating an in-scope item lands immediately before the earliest item it gates; todos gating nothing in scope are left out (the plan never widens). - Gates follow the canonical TODO lifecycle. The
completedlabel is the only release signal. While the todo is open without it, the work it gates is skipped, not failed —remainingsimply doesn't return it, and the run proceeds to non-gated items. Once the human addscompleted, the gated work is runnable again on the next cycle. A todo closed withoutcompletednever releases: the gated work is reported moot (cancelled), not resumed. - Release is recorded in the plan. When
remainingsees a released todo whose row is still unchecked, it ticks the row and appends a progress note ("Human action #NN completed — gate released") — the same check-off treatment as a requirement item.statusreports gated vs runnable counts and each todo's gate state (pending/released/cancelled).
Barrier vs skip: ordered plans (#1415)
Skipping a gated item is right for a plan of independent items — one req parks while unrelated work proceeds. It is unsafe for an ordered chain: skipping the gated item hands the runner the next one, so the run walks straight past a human checkpoint into work that was supposed to be downstream of it.
A gated item labelled serial-only is therefore treated as a barrier. The run order is truncated there, and every later item is withheld too — whatever its own gate state:
#1845 stage 1
#1846 stage 2
#1847 stage 3
(gated) #1848 stage 4 — waiting on todo #1854
(withheld) #1849 stage 5 — downstream of barrier #1848
(withheld) #1851 stage 7 — downstream of barrier #1848serial-only already declares "this must not run out of band" — it is what excludes an issue from cdfork --from-issues fan-out. The flight-plan engine now reads the same declaration one step further, so no authoring change is needed: label the stages serial-only, as an ordered plan should be anyway.
A barrier is anchored to the gate's plan position, not to the gated item's liveness. Closing an item does not release the chain below it while its todo is still pending — the pending todo is the plan saying the human action has not happened, and that is the fact the run order honours. This holds for every close reason: completed (the stage's work happened out of band; the checkpoint deploy did not), not planned (dropping a stage from an ordered migration is a human call, not a silent continuation), and duplicate (the work moved, the checkpoint did not). To free the chain, clear the todo — closing the stage is not a substitute.
| Status | Meaning | What clears it |
|---|---|---|
runnable | Nothing holds it | — |
gated | Held by its own pending todo | Add completed to that todo |
withheld | Held by an upstream barrier — it names the barrier, not a todo of its own | Clear the barrier; the whole chain below it frees in one step |
moot | Its gating todo was closed without completed, or it sits downstream of one that was | Nothing — the chain is broken, not paused |
An item that is closed on GitHub but still holding is reported in its normal bucket (gated or moot) with closed: true on the row and a [closed] marker in the printed line — there is no fifth status, so the --all shape is stable for consumers. The actionable fact is unchanged: by still names the pending todo, and clearing that todo is still what frees the chain. A closed item that holds nothing is simply skipped and is not reported at all.
Releasing a gate un-withholds everything down to the next barrier in one step — you never clear a chain one item at a time. remaining --json --all prints the full { gateMode, runnable, gated, withheld, moot } classification (plain --json stays the runnable array the runner consumes).
status and remaining resolve gates from the same state. They share one resolution path, so the operator's read of a plan and what the runner actually consumes cannot disagree: same gate map — including gates from open todos that are not plan rows (a Blocked by #NN in any open todo counts) — same handling of items closed outside the plan, same barrier position. If status shows a chain withheld behind a barrier, the run will stop exactly there; if it shows an item runnable, the runner will pick it up. status is read-only, so inspecting a plan never changes it — the reconcile tick that ticks released todo rows belongs to remaining alone.
Default is unchanged. A plan with no serial-only items keeps pure skip semantics — a gated req parks and later work still runs, and a closed item there is simply skipped, barrier anchoring included.
Overriding. Add a marker to the plan body to force either mode:
markdown
<!-- flightplan:gate-mode barrier --> <!-- every gated item is a barrier -->
<!-- flightplan:gate-mode skip --> <!-- nothing is a barrier (pre-#1415) -->Use barrier for an ordered plan whose items were never labelled serial-only; use skip to opt a serial plan back out. The marker is hand-authored, and build preserves it across an idempotent refresh — an unrecognised value is ignored and the serial-only inference applies.
DAG ordering: depends_on (#1613)
By default a plan is a strict total order — item N waits for item N-1 even when they are independent. Declaring dependencies turns it into a DAG: only real prerequisites order the run, so independent items fan out in parallel and the plan completes in ~critical-path time instead of sum-of-items time.
An item line may carry depends_on: #NN [#MM] in its rationale:
markdown
- [ ] #401 Build the parser — priority p2-high, req
- [ ] #402 Build the emitter — priority p2-high, req
- [ ] #403 Wire parser to emitter — priority p2-high, req, depends_on: #401 #402This is the same grammar the fake-edge audit (depends_on: #NN, Depends on #NN) treats as a real edge — a declared dependency is never flagged as fake. Edges come from three places, all merged at build:
- Hand-authored into the plan body (the natural response to a fake-edge audit finding) — preserved across an idempotent refresh, exactly like the
branch/gate-modedeclarations. - Issue bodies: a
Depends on #NNline in a scoped issue's own body seeds the edge automatically. - Refs to items outside the plan are dropped — deps are plan-internal (out-of-plan ordering is the gating-todo domain).
Consumption. remaining classifies an item with unmet prerequisites as waiting (its unmet siblings named in by) and excludes it from the runnable set; a prerequisite counts as complete when its checkbox is ticked or its issue is closed. The moment the last prerequisite lands, the dependent is runnable — no re-build needed. Items with no unmet prerequisites are runnable immediately, so warp-drive --flightplan consumes in dependency order and parallel consumers (cdfork fork --from-flightplan <N>, see cdfork.md) run the whole independent wave at once.
Validation. Parsing rejects broken declarations loudly: a self-reference, a ref to a non-sibling item, or a cycle (reported with the path spelled out, e.g. depends_on cycle: #401 → #403 → #401) fails build, remaining, and status with exit 1 — a DAG plan is never silently consumed in the wrong order.
Composition. A waiting item that is serial-only (or under gate-mode barrier) is a barrier — everything after it is withheld. An item both todo-gated and dep-waiting reports gated (the human todo is the actionable fact). A plan with no declarations behaves byte-for-byte as before: same body, same order, empty waiting bucket.
status reports the DAG's critical path (waves a parallel consumer needs vs pending items a serial run needs) — with the plan's per-item check-off timestamps, that is the evidence a graphed run finished in ~critical-path time.
Running a plan
bash
warp-drive --flightplan <N> # from a terminal
/warp-drive --flightplan <N> # from a Claude Code session--flightplan is the narrowest, most explicit scope — it wins over --issue and --area. The run:
- Reads the plan's remaining runnable items in order —
flightplan.js remaining <N> --json(already-checked or already-closed items are skipped; todo rows, reqs gated by a pending todo, and reqswaitingon unmetdepends_onprerequisites are excluded — see Gating human-action todos and DAG ordering). - Works the first remaining item; never widens beyond the plan.
- On each requirement's completion, ticks its checkbox and appends a progress note (
flightplan.js check <N> <issue> --note …). - Ends when the plan is exhausted — it never falls back to the approved queue.
This is a strict extension of the existing --issue N,N,N fixed-ordered-list scope (#992): same never-widen guarantee, sourced from the plan issue instead of the flag.
Fake-edge audit (#1612)
A plan written top-to-bottom accumulates fake edges — sequential ordering between items that carry no data between them, so the run waits in sum-of-items time where critical-path time was available. The report-only detector surfaces that slack:
bash
node ~/.claude/scripts/warp-drive/fake-edge-audit.js audit # every open flight-plan
node ~/.claude/scripts/warp-drive/fake-edge-audit.js plan <N> # one plan
node ~/.claude/scripts/warp-drive/fake-edge-audit.js --file <path> # a multi-step command/skill markdownConsecutive unchecked items with no data dependency — no declared Blocked by #NN / depends_on edge, no reference to the earlier item, no shared file/artifact token, and neither serial-only — are grouped into mutually independent, disjoint parallelizable runs, each safe to fan out as-is (e.g. via cdfork or a Workflow graph — see the graph-orchestration skill's fake-edge test, which this detector mechanizes for plans). Exit codes follow the detector convention: 0 clean, 3 candidates found (signal, not failure), 2 error. It never rewrites a plan — acting on a flag is a human/groom decision. Declared depends_on edges (#1613) are always respected, never flagged.
How it composes
| With | Behaviour |
|---|---|
/groom | Groom settles order/labels; /flightplan records that order as the plan. Groom first, then plan. Groom is also the plan's reconciler of last resort: its Step 5f sweeps open plans via audit/reconcile --plan — mechanical convergence auto-applied, stale refreshes propose-only, in-flight plans skipped (#1668). |
--area <slug> | /flightplan area:<slug> scopes the plan to that workstream's approved req+bug queue (same membership as warp-drive --area). |
--issue N,N | /flightplan N,N pins an explicit ordered list; warp-drive --flightplan then consumes it with check-off (where bare --issue has no persisted record or progress notes). |
approved | The gate applies to every scope, explicit lists included (#1794). Un-approved issues are dropped with a named reason; --allow-unapproved opts in and reports what it seated. |
Engine reference
| Command | Purpose |
|---|---|
flightplan.js build [--area <slug>] [--issues N,N] [--branch <name> | --no-branch] [--no-interview] [--allow-unapproved] [--json] | Derive once + write/refresh the plan issue (idempotent per scope; exit 3 while open questions remain, unless --no-interview); --branch declares the integration branch (#1181), a refresh preserves it, --no-branch clears it; --allow-unapproved seats issues lacking approved and reports each one (#1794) |
flightplan.js scan [--area <slug>] [--issues N,N] [--json] | Detect open questions in the scoped issues (pre-flight interview input, #1087) |
flightplan.js resolve <issue> <idx> --answer "…" | --defer [--note "…"] | Write a dated resolution/deferral marker back to the target issue body (idempotent) |
flightplan.js items <N> [--json] | Print the plan's ordered items (todo rows marked) |
flightplan.js remaining <N> [--json] [--all] | Runnable items in run order — todo rows and gated/moot/withheld/waiting reqs excluded; a held serial-only item is a barrier that withholds everything after it (#1415), and the barrier holds at the gate's position even if the item it names is closed (#1422); released todos reconcile-ticked; broken depends_on declarations rejected with exit 1 (#1613). --all widens --json to { gateMode, runnable, gated, withheld, moot, waiting } |
flightplan.js check <N> <issue> [--note "…"] | Tick an item (req or todo) + append a progress note (idempotent — no double-post) |
flightplan.js status <N> [--json] | Progress incl. runnable vs gated vs withheld vs moot vs waiting, per-todo gate state, the declared gate mode, the declared integration branch, and the DAG criticalPath (#1613). Read-only, and resolves gates from the same state remaining does — the operator's view is what the runner will consume, never a second opinion. Folds in the cheap lifecycle classification (#1666, no drift check) |
flightplan.js lifecycle <N> [--json] | Full lifecycle classification (#1666) — see below |
flightplan.js audit [--json] | Report-only lifecycle sweep of every open plan; exit 3 when any is non-active |
flightplan.js reconcile [--apply] [--json] | Converge open plans toward their derived state — dry-run by default |
Plan lifecycle (#1666)
A plan's overall state is derived, never hand-maintained (mirroring cap-status, #989). Every open plan classifies as exactly one of:
| State | Meaning |
|---|---|
active | At least one runnable item, per the same remaining semantics the runner consumes. |
exhausted | Every item checked or closed — the plan is closeable. |
drained | Unchecked remainder exists but none of it is runnable — all gated / waiting / withheld / moot. The reason names each hold (the gating todo, the unmet dependency, the barrier). |
stale | Scope drift: the recorded scope re-derives differently (items added, dropped, or reordered), or off-plan closures have left unticked-but-closed items. |
Precedence is exhausted > stale > active > drained. Raw fingerprint inequality is deliberately not the stale test — a mid-run plan legitimately re-derives smaller as its items close; only a meaningful delta over still-open items counts as drift.
reconcile [--apply] converges each open plan: it ticks released todos and closed-off-plan items (with a note comment), closes an exhausted plan with a dated final summary (item count + run span), stamps a drained plan with a dated gate-naming comment plus the machine-readable <!-- flightplan-state: drained --> marker (the plan stays open; the marker clears if the plan comes back active), and reports a stale plan with the exact build refresh command. Destructive actions are propose-only (AC-04): it never closes a plan with unchecked open items and never auto-rebuilds a stale plan — those are surfaced for a human or groom's propose-then-apply flow. Idempotent: a second run against a converged repo makes zero mutations.
Consumers: warp-drive run boundaries and the /groom sweep call audit / reconcile rather than reimplementing lifecycle logic (#1667, #1668).
See also
- warp-drive.md — the loop that consumes the plan
- kickoff-flags.md —
--flightplanalongside the other scope flags