Flow --auto - the build loop
/flow-next:flow --auto is the unattended build driver. One invocation selects one ready spec and carries it to a pull request, hop after hop, and ends with one terminal PILOT_VERDICT line. It classifies every hop from the same routing references the attended flow conductor reads, so the route --explain shows is the route the unattended run takes. By default it stops before merge. Add --until=merge to continue through land for the selected item.
Build and merge authority are separate. The spec content, its dependencies, and the ready flag you set are the consent boundary. --auto accepts no intent, path, branch, or free text; the only positional argument is a spec id. Goal mode (--goal) is the exception: it takes a spec id or any source, and the goal you name is its consent. It executes the mechanical pipeline and never prompts interactively. In ready mode an ambiguity that needs a person ends the run with NEEDS_HUMAN; in backlog mode the run parks the item behind an async question and reports ASKED.
Chart is outside the build loop. Optional /flow-next:chart is pre-capture discovery and is never a stage of flow --auto. Attended chart decisions (prototype, interview, attended task) terminate CHART_VERDICT=NEEDS_HUMAN under unattended drivers; they never self-resolve. Get the effort briefed, captured, and ready before the run selects it.
Flags and the machine contract are on the skill reference. How to run it under a host loop is Driving a loop. The /flow-next:pilot command was removed in 6.1.0, and 7.0 removed the hidden pilot and interview skill stubs that kept the old names working; Migrating from pilot maps its arguments.
The two shapes
Section titled “The two shapes”| Shape | What one invocation does | Fits |
|---|---|---|
/flow-next:flow --auto [<spec-id>] | Hops until a terminal: the PR exists, deferred to land, asked, blocked, needs human, or no work | A host with stable long sessions; run it again for the next item |
/flow-next:flow --auto --tick [<spec-id>] | Exactly one hop, then stop; what a pilot tick was | A host /loop or /goal that owns repetition, or a host without stable long sessions |
Both shapes end with the same terminal line, so a driver that parsed pilot’s verdict parses these unchanged. A long-horizon run names every dispatched stage joined by + and carries the last hop’s verdict; a --tick run names one stage.
What one run does
Section titled “What one run does”- Select. The first
open+readyspec whosedepends_on_epicsare all satisfied (done, or a chain parent that is open with every task done and its branch on origin, asflowctl spec chainreports) and that carries no other-actor task claims, or the spec named on the command line if it passes the same predicate. Selection runs once per run and fixes the item. A spec selected through its chain parent is built as a chain layer: its branch forks from the parent’s tip, its PR targets the parent’s branch, and the verdict reason beginschained on <parent-id>;. Two open parents, or a sibling already chained on the same parent, park the spec with a reason and no strike (chains and stacks). - Classify. Read the routing references:
route-matrix.mdfor the spec-state row,plan-vs-no-plan.mdfor a ready zero-task spec with no recorded route, andgate-selection.mdfor the review, QA, and completion-review gates. The build stage set is{plan, plan-review, work, qa, make-pr}, withqaonly when gate-selection selected it;--until=mergeadds the scopedlandstage. The run echoes the row and the gate section the stage came from. - Dispatch. Invoke exactly one stage skill (
plan,plan-review,work,qa,make-pr) withmode:autonomous. The run never re-implements their logic. - Verify. Re-read observed state: flowctl status fields and task transitions, the fresh
qa_verdictreceipt for QA, and a gh-confirmed OPEN PR URL for make-pr. Narration from the stage skill never counts. - Record. Write the receipts, echo the evidence block and the
stage:line into the transcript, and update the strikes ledger. - Re-classify. An
ADVANCEDhop on a stage other thanmake-prre-runs the dirty-tree guard and returns to step 2 for the same spec. With--until=merge, make-pr advances to land and the long run may continue through land progress and waits. Existing blockers still stop it. Under--tickthe run stops after step 5.
Sub-skills run autonomously. The mode:autonomous token (plus FLOW_AUTONOMOUS=1 for process-level drivers) suppresses questions: the run never asks, decides from evidence, and stops only for a call only a human can make or an irreversible action. Work branches deterministically from the spec’s branch_name, and make-pr hard-errors instead of prompting. Before the terminal line the run prints a Decisions: list, each default it chose, finding it declined and review it skipped on your behalf, with the evidence behind it, and the same list goes into the pull request body.
Review loops until SHIP. With nobody there to decide what is left, an unattended run fixes, re-reviews the fixes, and repeats until the reviewer returns SHIP, with flowctl’s round cap and stall check as the backstop (an ESCALATE: from either is a stop). Each declined hardening, scope-creep or pre-existing finding gets one Declined #<n>: <reason> line in the fix commit. When the reviewer keeps only findings like that, all below Major, the run may end the loop itself and records each disagreement, with both sides’ reasons, in the Decisions list and the pull request. A finding that shows a stated requirement broken is always fixed. The verdict stays the reviewer’s.
The no-plan route is recorded spec state. A ready zero-task spec with no recorded route is classified from plan-vs-no-plan.md; the run records the answer on the spec with flowctl spec set-no-plan or clear-no-plan before any task is created and echoes the deciding signal (route: direct - <signal absent> or route: plan - <signal>). There is no --no-plan flag; the run never infers consent from a flag. Existing planned tasks keep their route.
A later run may resume the same actor’s sole implicit execution owner only after positive evidence that the previous run has ended. An active run or uncertain ownership stays NEEDS_HUMAN; the direct route never authorizes stealing a claim. Since 5.5.0 that evidence check is the only way back in: a same-actor flowctl start on an in_progress task refuses with a typed error, and the run passes --reclaim only after the check, never on its own.
The plan and plan-review stages do not require the default branch. They probe the current branch for an open PR and plan in place when none exists, so a one-worktree-per-spec setup composes with the full loop.
The rails it keeps
Section titled “The rails it keeps”The rails:
- The consent boundary. Only
readyspecs are selected; the run never sets the flag. - The dirty-tree refusal. A tree dirty outside
.flow/at run start, or after any hop, ends the run withNEEDS_HUMANand leaves state untouched. No cleanup, no claim reset, no strike. - The two-strike ledger. A spec that fails to advance on two healthy hops is taken out of selection (
flowctl spec unready); see Don’t-thrash guard. - The all-done PR probe. Before an all-done spec is routed, the run reads PR state once through
ghand evaluates the branch’s PRs in order. An open PR defers the spec to land by default, or enters scoped landing with--until=merge. No PR routes on through QA to make-pr. A merged PR outranks any historical closed PR on the branch; the run compares the branch head with the head of the most recently merged PR, and additional commits beyond that merged head route to make-pr, while a branch head equal to it isNEEDS_HUMAN(the spec is still open with nothing new). A closed-without-merge PR isNEEDS_HUMANonly when the branch has neither an open nor a merged PR, because the run never silently reopens human-rejected work. - The merge-consent boundary. The default terminus is
make-pr.--until=mergeauthorizes land for the selected item’s pull request; land retains every merge gate. make-pr already closed the spec on the branch, so land writes nothing to the repository after the merge. A closed spec with an open pull request stays eligible for landing, a closed spec whose pull request is merged ends the run, and a closed spec with no pull request isNEEDS_HUMAN. Releases stay outside land. - The decision log. In backlog mode,
flowctl pilot-logappends one row per dispatched stage under.flow/pilot-runs/. - Ready PRs unless something is open. The pull request opens ready for review. A human call that blocks nothing else (refreshing a frozen fixture, a requirement only CI can prove) goes into the pull request as an open item, and then the pull request opens as a draft; the run finishes the rest instead of stopping. A reviewer’s question that blocks nothing else is handled the same way.
- Calls it can make when it holds the merge. With
--until=mergethe run makes a human-type call itself when it is reversible, inside the spec and backed by evidence (updating a snapshot or golden file the change legitimately altered), and records it in the Decisions list. A call that is irreversible, a product choice the spec does not settle, or one that makes merging unsafe stops the runNEEDS_HUMANbefore the merge. Land never marks such a run’s draft pull request ready; it stopsNEEDS_HUMANand names the open items. - Stops instead of guesses (6.1.0). One read-only
flowctl pilot snapshotcall supplies the guards, config, candidates, and PR observations for the hop. A missing or unreadable snapshot, an unresolved scope, or a failed PR observation ends the runNEEDS_HUMANwith the error. A spec named by its tracker key resolves through the same lookup as every other command. When every remaining task is blocked or escalated, the run stopsNEEDS_HUMANwith their reasons before dispatch and records no strike. Branch setup uses the resolved default base and stops on a git failure. A per-task review override stays explicit, and a review receipt whose verdict or counts contradict the review it records is refused.
The verdict grammar
Section titled “The verdict grammar”The /goal validator is transcript-blind. It reads conversation output and never runs tools. Every hop therefore echoes its verification evidence into the transcript, and every run ends with exactly one terminal line, the last line of output with nothing after it:
PILOT_VERDICT=<ADVANCED|NO_WORK|DEFERRED_TO_LAND|BLOCKED|NEEDS_HUMAN> spec=<id> stage=<stage> reason="<one line>"Backlog mode adds ASKED; goal mode adds GOAL_MET and starts every reason with its goal.
stage= names every dispatched stage in order joined by +; the reason names the last hop’s outcome. A --tick run names one stage. A long-horizon run that carried a spec from work through QA to its PR ends like this:
PILOT_VERDICT=ADVANCED spec=fn-12 stage=work+qa+make-pr reason="make-pr: open PR <url>"| Verdict | Meaning | Continues the loop? |
|---|---|---|
ADVANCED | The last hop advanced the spec; make-pr confirms an open PR, while land evidence distinguishes progress from completed landing | Yes, until the chosen destination |
NO_WORK | No open, ready spec with satisfied dependencies and no foreign claim; also the --explain terminal in ready mode | No |
DEFERRED_TO_LAND | Default: remaining candidates have open PRs for land. With a merge destination: a landing tick stopped on an external wait | No; inspect the reported land evidence before resuming |
BLOCKED | A healthy hop advanced nothing (strike 1/2 leaves the spec ready; strike 2/2 unreadies it and names flowctl pilot strikes clear <id>), or a backlog dep wait | No |
NEEDS_HUMAN | Crash-class: a stage crashed or asked for judgment, a dirty tree, a gh probe failure, a branch inconsistency, a closed-unmerged PR, a stale claim, or a review that exits NOT_RETRYABLE | No |
ASKED | Backlog mode only: the item is parked behind an open async question | No; the next invocation selects the next item |
TRIAGED | Backlog mode under --explain only: the diagnostic classification line; never a live terminal | No |
Terminal names are preserved. A merge-destination run also echoes the original LAND_VERDICT, selected PR state, and merge commit. ADVANCED alone never proves a merge. Long-running landing waits at the driver’s cadence; those waits consume no pilot strikes.
Don’t-thrash guard
Section titled “Don’t-thrash guard”A spec that fails to advance on two healthy hops is taken out of selection. The run clears its ready flag (flowctl spec unready) and the BLOCKED verdict carries the reason and the recovery command. Strikes live in a ledger under the git common dir, shared across worktrees and never committable; flowctl pilot strikes list shows what is struck and flowctl pilot strikes clear <spec-id> (or clear --all) resets it. Re-blessing the spec on a local repo clears its strikes on the next selection, which is an explicit human reset (flowctl spec ready). The finite stage set bounds a spec that advances; the two-strike rule bounds one that does not.
Readiness as the control plane
Section titled “Readiness as the control plane”flow --auto consumes exactly one gate: the ready flag. Where that flag comes from depends on your setup, and it changes how you steer a running loop.
Local repos (no tracker, or tracker.readyState unset). flowctl spec ready / unready is authoritative. You bless work on the command line; the two-strike unready sticks until you re-bless the same way, and a re-blessed spec clears its strikes on the next selection.
Tracker-connected repos (tracker.readyState configured). The board is the control plane. The readiness projection pulls one-way from the tracker: a Linear issue in the configured workflow state (or a GitHub / GitLab issue carrying the configured label, or a Jira issue in the configured workflow status) means ready=true; anything else means ready=false. Move an issue into that state to feed the loop work, out to starve it. Local spec ready writes are overwritten on the next pull, so bless on the board.
One interplay matters when driving against a board. The two-strike spec unready is a local write, advisory until the board reflects it, and the next tracker pull re-readies the spec. That projection-set ready never clears the strike. A board echo re-grants readiness with nobody acting, and a deliberate move back into the ready lane is indistinguishable from that echo in every durable artifact, so no board move can serve as the re-bless. When the run strikes a spec out (BLOCKED … strike 2/2, spec unreadied), move the issue out of the ready state to keep the board honest, fix whatever blocked it, and clear the strike with flowctl pilot strikes clear <spec-id>. The verdict line names the command. Clearing a strike never changes readiness. Strikes are driver state, the board owns readiness.
This keeps a clean division of labor. The board decides what the loop may touch; flow --auto decides how far each run advances it; verdicts report back in the transcript, and the tracker-sync lifecycle events mirror progress onto the issue so the board view stays current while the loop runs.
Resume from disk
Section titled “Resume from disk”Every hop ends with the same receipts, evidence echo, and ledger write a pilot tick ended with. A run that dies mid-way (crash, kill, session limit) leaves exactly what a dead tick leaves: committed receipts, a ledger entry, a branch. The next invocation classifies from disk and picks up at the stage the state names; nothing is resumed from transcript. Check an idle dispatched agent through its commits, receipts, and status fields; a resume message restarts it, so a merely slow agent becomes two runs.
--explain under --auto
Section titled “--explain under --auto”/flow-next:flow --auto --explain [<spec-id>] (with --dry-run accepted as its one-release alias) stops after classification. It prints the selected spec, the classified stage with its routing row and gate section, the review backend, task counts, the consulted status fields, the resolved zero-task route as would-record with its signal, the PR probe result, skipped candidates, would-clear ledger entries, and chain=<off|on>. It writes nothing under .flow/, creates no ledger file, checks out no branch, and dispatches nothing. Its terminal line in ready mode:
PILOT_VERDICT=NO_WORK spec=<id> stage=<stage> reason="dry-run: classification only, nothing dispatched"In backlog mode an explain run is inspection-only as well. It skips the tracker-sync dispatches (reconcile, list-open, list-comments, list-relations), selects from the flow-side ready --all facts alone, triages the picked item, and stops before any routing with the diagnostic line:
PILOT_VERDICT=TRIAGED spec=<id> stage=triage reason="dry-run: classified <class>, nothing dispatched or parked"A /loop or /goal driver never runs --explain.
Backlog mode
Section titled “Backlog mode”By default the consent boundary sits before the loop. flow --auto picks only from the already-ready queue and assumes specs are triaged, dep-clear, and unambiguous. Everything in front of that gate is manual prompting: enumerating the whole open backlog, triaging raw items, sequencing deps, deciding what’s next, and unblocking the things that need a person.
Backlog mode (pilot.autonomy=backlog, default off; the key keeps its pilot. spelling) makes flow --auto a standing floor scheduler for the entire open backlog:
flowctl config set pilot.autonomy backlog # or per-run: /flow-next:flow --auto --backlogOnly the literal backlog activates it; any other value (ready, null, a bool true, a typo) leaves the run in ready mode byte-for-byte. Each run then:
- Enumerates the full open set: flow specs (
flowctl ready --all) plus normalized tracker issues from thelist-openread, filtered to the exact configuredtracker.readyState. A pull-before-scan (reconcile) runs first, so a board move is reflected on the next run. - Orders tracker dependencies: for each tracker candidate, the
list-relationsread supplies directed dependency rows and fails closed rather than sorting a truncated graph. - Selects the top dep-ordered actionable item across both sources, skipping anything already parked behind an open question.
- Triages it with an agentic read of the spec, never a deterministic score: workable (ready signal + complete spec) flows into the normal hop loop; dep-unsatisfied sequences behind the blocker (
BLOCKED); ready-but-thin is kicked back with an async question (ASKED). - Drives that one item to its terminal, or parks it. In long-horizon mode a workable item is carried hop after hop to its PR and the run stops; under
--tickit advances one stage. Either way the run handles one item, and the next invocation selects the next item.
This is the one place the consent boundary moves, from before the loop to inside the loop, on block. When the run cannot safely proceed it surfaces a precise async question rather than stalling, writes it into the spec’s ## Open Questions section, and projects the text through the tracker-sync question wrapper. It then parks the item (ASKED) and stops. A human answers in the spec or tracker; a later run re-triages and proceeds. It never prompts interactively.
The load-bearing boundaries hold unchanged:
- Never authors a spec. A thin or missing spec is surfaced as a “run
/flow-next:captureor/flow-next:refine” gap. It may fill an obvious blank in an existing spec, never write one from a bare ticket. - Never sets the
readyflag. Promotion is the human’s board act; un-promoted backlog items are skipped silently, never worked and never nagged. The agent’s spec-read can only withhold by kicking a promoted-but-thin item back with a question, never force work. - No implied merge authority. Backlog mode alone never grants landing permission.
--until=mergeadds land for the one selected item under its existing gates. - One item per run. Selection sets exactly one subject; a selection that yields more is
NEEDS_HUMAN.
Full-auto by default, with an optional force-gate. Workable, dep-clear, unambiguous items advance with no pre-gate. For extra caution you can name classes that must be force-surfaced as a question before action, even when otherwise workable, via the sibling config key pilot.gateClasses (default [], which gates nothing):
flowctl config set pilot.gateClasses risky # a single class# or a JSON array for several: ["risky","prod-config"]Whether a selected item belongs to a gate class is the agent’s read of the item, like triage and never a score.
Backlog mode is a leftward extension of the same driver: one loop target, one verdict grammar with ASKED added, one mental model. It manages the existing open backlog rather than inventing work the way prospect does. A per-run decision log (flowctl pilot-log, stored under .flow/pilot-runs/) records one row per dispatched stage plus host-reported token cost on the last row, which gives you the factory readout: how many items moved with no question, with one async answer, or parked, and cost per change. With the gate off, flow --auto behaves byte-for-byte as documented above.
Two drivers, never nested
Section titled “Two drivers, never nested”Attended flow refuses under every autonomy marker (FLOW_AUTONOMOUS, AUTONOMOUS=1, a mode:autonomous token) and stops before any read with this line:
NEEDS_HUMAN: /flow-next:flow is attended - run /flow-next:flow --auto for unattended runsflow --auto has no marker refusal, because it sets FLOW_AUTONOMOUS and mode:autonomous itself for the stages it dispatches. Attended flow and flow --auto are two drivers for the same pipeline and are never nested. Flow may invoke land only as its authorized, scoped landing stage. Recursive drivers remain forbidden. Default DEFERRED_TO_LAND behavior is unchanged.
Live QA
Section titled “Live QA”pipeline.qa=auto now takes effect unattended. flow --auto reads the key through the same gate-selection reference attended flow reads: on runs /flow-next:qa at all-tasks-done on every spec, auto runs it when the acceptance describes UI behaviour on a drivable surface and a target can be started, and off leaves QA to a user invocation. A skipped auto hop records one line in the evidence and the route advances to make-pr:
stage: qa - skipped(config: pipeline.qa=auto: no drivable surface)QA never hard-blocks the loop; NEEDS_WORK and BLOCKED outcomes advance to the pull request with their findings.
pipeline.chainStages was removed in 7.0. Under --tick, make-pr runs on the next tick; in long-horizon mode the hop loop already runs make-pr as the next hop. flowctl ignores a leftover key with a one-line note; delete it from .flow/config.json.
Migrating from pilot
Section titled “Migrating from pilot”The /flow-next:pilot command was removed in 6.1.0. Replace it with /flow-next:flow --auto --tick in /loop and /goal recipes; Driving a loop has the replacements. Every pilot argument has a place:
| Pilot argument | Flow argument |
|---|---|
--spec <id> or --spec=<id> | the positional <id> |
--backlog, or pilot’s old --auto backlog switch | --backlog |
--dry-run | --explain (flow --auto still accepts --dry-run) |
--review=<backend>, --depth=<level> | passed through unchanged |
Config keys (pilot.autonomy, pilot.gateClasses), the flowctl verbs (pilot strikes, pilot-log, pilot snapshot), the ledger path, .flow/pilot-runs/, and the PILOT_VERDICT name keep their spelling.
Migrating from Ralph
Section titled “Migrating from Ralph”Ralph, the repo-local shell harness, was removed in 7.0; flow --auto is the one unattended mode. If you still run Ralph, pin flow-next 6.7.x. Otherwise delete scripts/ralph/ and any ralph-guard hook entries from your project settings, and run /flow-next:flow --auto (with --until=merge or --tick) where your Ralph loop ran.