Skip to content

Flight Plan

Flight Plan — Persisted, Ordered Warp Run Plan

Section titled “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).

Terminal window
/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 goes

The plan is a GitHub Issue labelled flight-plan — never a local file.

/flightplan is a universal command (commands/flightplan.md, #1872): bob init links it into every project alongside /warp-drive, so it needs no manifest entry. A manifest that still names it is reported by bob provision as [already-global] … (universal) and bob provision add command flightplan is a no-op — drop the entry; nothing breaks either way.

  • An issue, labelled flight-plan, whose body is an ordered checklist:
    - [ ] #1068 Issue-backed flight-plan artifact — priority p3-medium, req
    - [ ] #1069 Cachable warp-drive discovery — priority p3-medium, req
    plus machine markers (<!-- flightplan:scope … -->, fingerprint, cut, and — when declared — the integration branch, #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.

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-only in GitHub Issues. /flightplan reuses that ordering (discover-queue.sh, the same queue /what-next and /groom derive from) and only records it. It does not re-implement cluster reconciliation.
Terminal window
/flightplan # whole approved project queue
/flightplan area:<slug> # one workstream
/flightplan 1068,1069 # a specific ordered issue list

An 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) dropped

Naming 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-unapproved

It 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:

Terminal window
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).

What a refresh carries across (#2283). Rows are re-derived from the live issues, but every declaration the existing body already records is carried over, matched by issue number: the integration branch (#1181), the gate mode (#1415), depends_on edges (#1613), the recorded name (#2309) — and each row’s tick. A ticked-but-open row is normal on an integration stream (warp-drive ticks an item at requirement_complete; its issue closes only when the stream is finalized), and a ticked-closed row is simply done; a refresh keeps both ticked, so remaining never lists finished work a second time. A newly derived row starts unticked. The JSON result reports the count as carriedTicks (0 on a create). The L3+ kickoff auto-refresh of a stale plan (#1667) runs this same build path, so a routine kickoff never resets a plan’s progress either.

An issues: plan’s scope token is its issue list, so re-building with a longer list is a new token: build finds no matching plan and creates a second one, while the running session stays bound to the old number. To add (or drop) issues on the plan you already have, name it:

Terminal window
node ~/.claude/scripts/warp-drive/flightplan.js build --plan 2092 --issues 2034,2037,2275 --no-interview --json
# → { "plan": 2092, "refreshed": true, "scope": "issues:2034,2037,2275", "rescopedFrom": "issues:2034,2037", "carriedTicks": 2, … }
bob plan build --plan 2092 --from-issues 2034,2037,2275 # the same, from a shell

--plan <N> refreshes that plan: its scope marker is rewritten to the new list, every tick, the branch, gate mode, depends_on edges and name are carried across as on any refresh, the new row is seated in list order and starts unticked, and no second plan is created. rescopedFrom names the token it replaced (null when the list did not change — a repeated call is an idempotent refresh). It is refused, with nothing written, when the plan is closed, is not an issues: plan (an area / project / farm plan is re-derived by its own scope flags), or when the new list already belongs to a different open plan — two plans never claim one scope (#1722, from the other direction).

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:

Terminal window
node ~/.claude/scripts/warp-drive/flightplan.js build --area <slug> --branch stream/<slug> --json

The 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 bob 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 (bob warp session end / bob 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 bob warp status shows flight-plan #N → stream <branch> during the run.

A plan can wait for another plan to finish. Declare the predecessor at build time:

Terminal window
node ~/.claude/scripts/warp-drive/flightplan.js build --issues 2233,2236 --after 2092 --json
# → { "plan": 2285, "after": [2092], … }

--after takes one number, a comma-separated list, or repeats. The plan body gets a Runs after line and one <!-- flightplan:after #N --> marker per predecessor. A refresh keeps the declaration when neither flag is passed, like the branch marker. --no-after clears it.

build refuses three declarations and writes nothing. Each exits 2 with a named error (predecessorErrors in --json):

Code When
not-a-flight-plan the predecessor issue has no flight-plan label
self-reference the plan names itself
cycle a predecessor waits, directly or through its own predecessors, on this plan. The message shows the path, e.g. #2285 -> #2092 -> #2285

While any predecessor is open and still has unfinished items, the plan is waiting:

  • remaining returns no runnable items. --json is [], and --json --all carries waitingOn: [{ plan, done, total }].
  • status, lifecycle and audit classify the plan waiting, with the reason waiting on #2092: 38/57 done.

A predecessor releases the wait when it closes, whether finished or abandoned, or when every one of its items is done. An unreadable predecessor keeps the plan waiting: nothing is released on evidence that could not be checked. reconcile reports each release once (see Plan lifecycle).

Groom first if needed. /flightplan records the order that already exists — it does not compute it. If the cluster’s dependency chain, serial-only, or approved labels aren’t settled yet, run /groom [area:<slug>] first.

/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_remote when RDB is enabled, AskUserQuestion otherwise. 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 /flightplan for 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 assigned todo filed — see Gating human-action todos below.
  • Headless: /flightplan <scope> --no-interview (engine: build --no-interview) skips the interview. Without the flag, build refuses (exit 3) while open questions remain — the gate that keeps interview-on the default.

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 build re-derives gating rows from live open todo issues: 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 completed label is the only release signal. While the todo is open without it, the work it gates is skipped, not failed — remaining simply doesn’t return it, and the run proceeds to non-gated items. Once the human adds completed, the gated work is runnable again on the next cycle. A todo closed without completed never releases: the gated work is reported moot (cancelled), not resumed.
  • Release is recorded in the plan. When remaining sees 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. status reports gated vs runnable counts and each todo’s gate state (pending / released / cancelled).

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 #1848

serial-only already declares “this must not run out of band” — it is what excludes an issue from bob fork --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:

<!-- 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.

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:

- [ ] #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 #402

This 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. Both parsers import the one definition in scripts/warp-drive/depends-on.js (#2297), so they cannot diverge.

Grammar (#2297). A declaration is the keyword — depends_on or Depends on, any case, an optional colon — followed by a list of #NN refs. Refs may be separated by whitespace, a comma, and or &, in any combination, so these all declare every ref:

depends_on: #401 #402
Depends on #401, #402
Depends on #401 and #402
Depends on #401, #402 and #403 (and the Oxford form: #401, #402, and #403)
Depends on #401 & #402

The list ends at the first token that is not a #NN ref. So and followed by a word ends it: Depends on #2231 and farm tick (#2054) declares #2231 only — the parenthesised #2054 is a mention, not an edge. A declaration must start with a ref (Depends on and #9 declares nothing), and a bare #NN mention anywhere else is never a dependency.

A negated mention is not a declaration (#2310). A keyword directly preceded by no longer, never, not, n't or nor declares nothing, and the whole ref list after it is dropped:

Groomed 2026-09-13: no longer depends on #2054 (declares nothing)
Depends on #2045, #2048 (declares #2045 and #2048)

This matters because grooming records edge removals in prose. Without the guard, a note saying an edge was dropped re-created it, which manufactured depends_on cycle refusals out of the very notes that recorded the fix. Only the words immediately before the keyword negate it, so a negative earlier in the sentence is harmless: Not blocked by review. Depends on #12 still declares #12.

Edges come from three places, all merged at build:

  1. 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-mode declarations.

  2. Issue bodies: a Depends on #NN line in a scoped issue’s own body seeds the edge automatically.

  3. Refs to items outside the plan are dropped — deps are plan-internal (out-of-plan ordering is the gating-todo domain) — but never silently (#2297): build prints one line per dropped ref, naming both ends and why —

    [dep-dropped] #2240: depends on #2234 — not in this plan, not recorded
    [dep-dropped] #8: depends on #8 — self-reference, not recorded

    and --json carries the same list as droppedDeps ([{ issue, dependsOn, reason }], empty when every declared edge landed); the summary line counts them (N depends_on ref(s) dropped) beside the recorded depends_on edge(s). A dropped ref is therefore always distinguishable from one that was never declared: seat the target in the plan (or grow an issues: plan with --plan) if the edge matters.

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 (bob fork fork --from-flightplan <N>, see cdfork.md) run the whole independent wave at once, up to _workflow.fork_max_parallel live windows. When a forked item lands, bob fork watch re-reads the plan and starts each item it was blocking, so a fan-out runs to the end of the plan without being re-run by hand (#3550).

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.

Terminal window
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:

  1. 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 reqs waiting on unmet depends_on prerequisites are excluded — see Gating human-action todos and DAG ordering).
  2. Works the first remaining item; never widens beyond the plan.
  3. On each requirement’s completion, ticks its checkbox and appends a progress note (flightplan.js check <N> <issue> --note …).
  4. Ends when the plan is exhausted — it never falls back to the approved queue.

A plan that is waiting on a predecessor does not start. init prints {"status":"waiting","plan":N,"note":"Flight plan #N: waiting on #2092: 38/57 done — …"}, exits 0 and writes no state. This is a clean end, not a failure. Re-kick once the predecessor finishes.

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.

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:

Terminal window
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 markdown

Consecutive 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 bob fork 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.

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).
(farm) flightplan.js build --from farm / bob plan build --from farm scopes the plan to what the unattended night shift may take (#2052) — the same membership discover-queue.sh --farm yields. No slash-command spelling: a farm plan is built by the tick, not by a human at a prompt.
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.

The plan lifecycle is a bob noun, so a timer (bob tick, #2054) or a human at a terminal can plan without a Claude Code session. scripts/bob/plan.sh is a thin router over the engine above — it maps flags and execs flightplan.js; it holds no plan logic of its own, so the #1666 engine stays the single source of truth.

Terminal window
bob plan build # the approved queue → flightplan.js build --no-interview
bob plan build --from-issues 261,262 --json # → build --issues 261,262 --no-interview --json
bob plan build --area warp-drive --branch stream/warp-drive
bob plan build --from-issues 2225,2226 --name "Session kits" # → … --name "Session kits" (#2309)
bob plan build --from-issues 2233,2236 --after 2092 # → … --after 2092 (#2298)
bob plan remaining <N> [--json] [--all] # → flightplan.js remaining <N> … (verbatim)
bob plan status <N> [--json] # → flightplan.js status <N> … (verbatim)
bob plan items <N> [--json] # → flightplan.js items <N> … (verbatim)
bob plan build flag Engine argv Notes
--from approved (default) (no scope flag) the approved queue — the engine’s project scope
--from farm --from farm the unattended scope (#2052): the approved queue narrowed by the eligibility rule in scripts/warp-drive/eligibility.js — farm label or _workflow.farm_eligible: true; never a cap, serial-only, blocked, needs-clarification, vision/CLAUDE.md narrative, or an unverified bug. Every exclusion is printed with its reason ([dropped] #N … — no farm label); the engine refuses --allow-unapproved for it. The rule itself is in farm-tick.md
--from-issues N,N --issues N,N an explicit issue list
--area <slug> --area <slug> one workstream
--branch <name> --branch <name> declares the integration branch (#1181)
--name <name> --name <name> names the plan — title Flight plan: <name> plus a flightplan:name marker; omitted, a new plan takes the derived name (its dominant area:* labels) and a refresh keeps its recorded one; on an existing plan it renames (#2309)
--after <N> --after <N> the plan waits on flight plan #N (#2298); repeated values are joined N,N
--no-after --no-after clears the declared predecessor(s)
--allow-unapproved --allow-unapproved off by default: the engine never seats un-approved work and prints each [dropped] issue (stderr; dropped in --json)
--json --json the engine’s document — plan, scope, items, dropped, unapproved, …
(always) --no-interview a shell run has nobody to interview: open questions are carried into the plan body instead of blocking, so the engine’s exit 3 never happens here

Exactly one scope source (--from approved / --from farm / --from-issues / --area); two is a usage error (exit 2) before the engine runs. Without --json, build prints the engine’s own line — Created flight-plan #N … / Refreshed flight-plan #N … — so the plan number is on stdout either way. Exit codes are the engine’s own on exec; 2 from the wrapper itself means a usage error (unknown verb or flag, two scope sources, a bad --from). tests/test-bob-plan.sh asserts the exact flightplan.js argv for each verb against a stub engine.

Command Purpose
flightplan.js build [--area <slug>] [--issues N,N] [--from approved|farm] [--plan <N>] [--name <name>] [--branch <name> | --no-branch] [--after <N> | --no-after] [--no-interview] [--allow-unapproved] [--json] Derive once + write/refresh the plan issue (idempotent per scope; exit 3 while open questions remain, unless --no-interview); --plan <N> with --issues grows or re-scopes THAT open issues: plan in place — scope marker rewritten, ticks and declarations carried, rescopedFrom reported, refused for a closed or non-issues plan or a list another open plan claims (#2283); --name titles the plan Flight plan: <name> and records it in a flightplan:name marker — without it a new plan takes the derived name (its dominant area:* labels, else N issues / area:<slug> / whole project), a refresh keeps the recorded name and never rewrites a hand-edited title, and --name on an existing plan renames it (#2309) — flightplan.js name [scope flags] [--json] prints the name a build would write, read-only, as the /flightplan interview’s proposal; --from farm is the unattended scope — token farm, one plan per project, seated by eligibility.js with every exclusion reported and --allow-unapproved refused (#2052); --branch declares the integration branch (#1181), a refresh preserves it, --no-branch clears it; --after <N> declares a predecessor plan, preserved on refresh and cleared by --no-after, refused with exit 2 when it is not a flight plan, is the plan itself or closes a cycle (#2298); a refresh also carries each row’s tick across, matched by issue number, and reports the count as carriedTicks (#2283); depends_on refs are read with the shared grammar (and, &, commas, whitespace — #2297) and every out-of-plan or self ref that cannot be recorded is printed as a [dep-dropped] line and carried as droppedDeps in --json; --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 "…"] [--boundary] Tick an item (req or todo) + append a progress note (idempotent — no double-post); --boundary, what the item-boundary tick passes, holds an open item labelled blocked or gated by an open todo (#3694)
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

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.
waiting A predecessor plan (--after, #2298) is open and not exhausted. Nothing runs. The reason names it and its progress: waiting on #2092: 38/57 done.
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 > waiting > stale > active > drained. A waiting plan is not re-derived for drift, because drift cannot change its state. 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. A waiting plan is stamped <!-- flightplan-state: waiting --> with a comment naming the predecessor. When a predecessor closes (finished or abandoned) or has every item done, reconcile comments the release once and records it as <!-- flightplan:released #N -->. 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).