Fleet and local sub-agents
The Flux fleet is a durable supervisor connecting planning BoardRefs to isolated
local Flux sub-agent sessions. It replaces coordinator socket clients, terminal scraping, Python
board generators, status collectors, and hand-built progress reports with one versioned CLI.
Native flux fleet landed after v0.55.0. It is available in source installs from current main;
packaged-release users need v0.56.0 or newer.
Board and Fleet are paused as of 2026-08-08 and are not under active development in this
repository. The work is moving to ~/projects/autodev, where it will continue as its own project.
What that means in practice: the commands documented here still exist and still work — nothing was
removed and nothing was deprecated in place — but no new board or fleet capability will land in
flux, open stories against them are not scheduled, and the known gaps recorded on the board stay
open here rather than being fixed here. Treat this page as a description of what shipped, not as a
roadmap.
The fleet is local in V1. It does not require remote A2A workers, containers, automatic publication, or automatic worktree deletion.
Domain model and ownership
Fleet executes work selected by the Board domain model. It does not own epics, story status, milestones or the program schedule.
| Entity | Meaning | Durable authority |
|---|---|---|
| Fleet | One local execution supervisor rooted at a workspace. | Fleet config plus runtime journal |
| Main coordinator | The single reserved agent that accepts requirements and orchestrates dispatch. It selects from the Board; it does not replace Board authority. | Fleet runtime state |
| Worker | One admitted sub-agent with one role, capability ceiling, persistent session and bounded assignment. In the normal story path, worker and sub-agent are 1:1. Its recorded status is reconciled against the process supervising its turn, so a worker whose supervisor is gone reads interrupted rather than staying busy forever. | Fleet admission/runtime state |
| Configured wave | Board-owned repository-local dispatch template. It has no worker, worktree or runtime lifecycle. | .flux/board.toml |
| Dispatched wave instance | One pinned execution of selected BoardRefs, with integration bases, workers, receipts, reviews, gate and apply status. Integration cherry-picks each story's whole commit range from its pinned base to its cited commit, because a worker legitimately makes several commits and applying only the cited one silently drops the rest. A failed integration is retryable once its cause is fixed. | .flux/fleet/state.json and events |
| Handoff | Typed candidate result for one story: exact commit, write set and test evidence. The host re-runs the cited argv at the pinned base and at the commit, in a checkout pinned at that base which integration never touches. It verifies the argv it is given, so a commit can still break a different test in the same story — a green handoff is not a green story, which is what the repository gate is for. It is not completion. | Fleet runtime state/events |
| Review | Fresh read-only assessment of the exact candidate commit and story contract. A result is PASS, REWORK or PARK. | Fleet runtime state/events |
| Candidate preparation | Optional per-repository step that regenerates whatever the repository derives from a whole candidate, run after every cherry-pick and before the gate. Its output is committed into the candidate. | Fleet receipt/runtime state |
| Gate | Repository command run against the assembled wave candidate. Green makes the wave apply-eligible; it does not publish it. One gate run per candidate: a retry that recomputes an identical candidate is refused rather than re-gated. | Fleet receipt/runtime state |
| Accept (apply) | Pins a green candidate with an annotated fleet/accepted/<wave>/<repository> tag. It does not merge, does not touch a working tree, and does not require the canonical ref to have stood still. | Git tag plus Fleet runtime state |
| Land (promote) | Writing a member's local canonical branch from its accepted candidates, re-gated against whatever that branch has become. Separate from acceptance, because a candidate is green against the base it was gated on. | Git plus Fleet runtime state |
| Release/deploy | Separate publication boundary after landing. Fleet never implies either from a green gate. | Release/deployment system |
The word “wave” should therefore be qualified when it matters: a configured wave is planning configuration; a dispatched wave instance is mutable execution state.
Configure the workspace
Configure the cross-repository program in .flux/board.toml first; see the
workspace Board example. flux board check --output json works without
Fleet. flux fleet init then creates a closed execution-policy scaffold and durable runtime state.
Limits default to three concurrent workers, ten stories per wave, and two rework deliveries.
flux fleet init --max-workers 3 --max-wave 10 --max-rework 2
flux fleet doctor --output json
flux fleet validate --output json
Fleet configuration declares execution identity, repository roots, canonical refs, final gates, write fences, concurrency and worker admission. Milestones, program lanes and configured waves do not belong here. A representative execution configuration is:
schema = "flux.fleet/v1"
max_workers = 3
max_wave = 10
max_rework = 2
decision_mode = "human" # or "auto"
allow_ad_hoc_agents = true
worktree_root = ".flux/fleet/worktrees"
[loop_profiles.implementation]
revision = "1"
source = ".flux/fleet/loops/story-implementation.flux"
entry = "work"
[loop_profiles.research]
revision = "1"
source = ".flux/fleet/loops/research.flux"
entry = "research"
[loop_policy]
implementation = "implementation"
documentation = "implementation"
maintenance = "implementation"
research = "research"
[main]
instructions = ".flux/fleet/main.md"
model = "codex/gpt-5.6-sol"
loop = ".flux/fleet/loops/main-coordinator.flux"
research_loop = ".flux/fleet/loops/research.flux"
[[agent_templates]]
id = "story-worker"
role = "writer"
task_kind = "implementation"
instructions = ".flux/fleet/agents/story-worker.md"
model = "codex/gpt-5.6-sol"
mode = "write"
capabilities = ["read", "edit", "git", "shell", "rust"]
fences = [".flux/fleet/**"]
max_instances = 3
[promote]
# Accepted candidates a member accumulates before promotion gates and lands it. Defaults to 1.
threshold = 1
[[repositories]]
id = "api"
root = "../api"
# A LOCAL branch. Promotion writes this ref, and a remote-tracking ref such as `origin/main` moves
# only on a push, which no Fleet operation performs.
canonical_ref = "main"
board = "product"
gate = ["cargo", "test", "--workspace"]
[[repositories]]
id = "web"
root = "../web"
canonical_ref = "main"
board = "product"
# Promotion order, as a graph rather than a list: `api` lands before `web`. A single-repository fleet
# declares nothing, and declaration order breaks ties.
depends_on = ["api"]
# Optional. Regenerates whatever this repository derives from a whole candidate, once, after every
# cherry-pick and before the gate. Committed into the candidate so it survives into the accepted tag.
prepare = ["npm", "run", "build:docs"]
gate = ["npm", "test"]
A prepare step exists because some checked-in artifacts are derived from the whole wave rather than
from any one story — a documentation mirror, a generated index, an aggregated manifest. No story can
produce a correct one: two stories regenerating the same artifact collide, and regenerating it on
either branch alone yields an artifact missing the other story's contribution. Preparation runs at the
only point where the inputs are complete. A preparation failure is that repository's failure, so a
stale derived artifact never reaches the gate disguised as the stories being wrong.
Instruction paths are confined under the fleet root. Validation rejects duplicate/reserved ids, another coordinator role, invalid instance limits, overlapping roots, missing boards, invalid refs, missing task-kind loop policy, unsupported loop runtime features, loop operations outside the template's capability ceiling, and unsupported fields. Board validation separately rejects program dependency cycles, cross-repository configured waves, or a wave over ten. Refresh and other read commands report dirty, stale, or diverged checkouts without fetching or modifying them.
Fleet workers do not use the coordinator's loop and do not fall back to the general adaptive loop.
Each template declares a task_kind; [loop_policy] maps that kind to an operator-authored
[loop_profiles.*] binding. Admission validates the exact source and snapshots it with bounded
profile/revision/digest metadata in the worker's fenced runtime directory. Message, restart, resume
and rework reconstruct that snapshot, so later config or file edits affect new workers only. A new
admission/session is required to change a live worker's loop.
Configuration is not state
The durable files have disjoint write ownership:
| Path | Contains | Mutated by runtime operations? |
|---|---|---|
.flux/board.toml | Board members, documents, active milestone, program lanes, configured waves | No |
.flux/fleet.toml | Worker templates, models, capabilities, gates, fences, concurrency, worktree policy | No |
.flux/fleet/state.json | Coordinator/workers and dispatched wave instances | Yes |
.flux/fleet/events.ndjson | Append-only lifecycle receipts and evidence | Yes |
Read surfaces may join these sources in one response. Fleet state stores BoardRefs and the minimum pinned dispatch snapshot needed for reproducibility; it is not a copied or mutable program schedule.
Capabilities are an admission ceiling
Each template must declare the authority its workers need. A read-only template requires read; a
writing template requires read, edit, and the safe story-sized git bundle. Add only the
optional bundles the work requires: shell for arbitrary guarded processes, rust, node, go,
python, or make for a native toolchain, and task for nested sub-agent delegation. Shell and
toolchain processes still run inside bounded-autonomy's fail-closed workspace sandbox with the
network closed. There is no implicit network capability.
Fleet validates the declared names before dispatch, expands them to one exact host-owned operation
set, and records a bounded flux.fleet-capability-set/v1 digest in status and turn receipts. The
same mode, operations, writable root, read roots, and fences survive message delivery, process
restart, resume, and rework. Editing a template affects only workers admitted afterward; it never
widens an existing worker. Admit a new worker explicitly when the required authority changes.
One main coordinator, goals, and intake
Every fleet has exactly one reserved main coordinator. It is the only agent that owns requirement
intake and execution orchestration. The Board owns roadmap and scheduling truth. All user tasks and worker follow-ups route through it;
worker records carry parent: main and no template or ad-hoc request may use the coordinator role.
For an interactive operator surface, explicitly attach the TUI to this coordinator:
flux fleet start --output json
flux tui --fleet # current directory is the Fleet root
flux tui --fleet=../roadmap # or name the Fleet root
This opens the exact durable main session and keeps the conversation primary. F2, /fleet, or
/board opens bounded native Board, worker, decision, failure, planning-document, and exact-stats
views; wide terminals also show an attention rail. Typed requirements are journalled through
accepted, delivered, and completed/failed acknowledgement states. A stopped Fleet is observable but
cannot accept input. See the TUI guide for navigation,
decision confirmation, restart behavior, and the deliberately narrow mutation boundary.
The attached main agent has a closed coordinator catalog: typed board.* operations for showing,
checking, selecting and updating authoritative work; typed fleet.* operations for bounded status,
schedule, complete worker enumeration, run, message, cancel and resume; and task for bounded
read-only research. It does not receive shell, editing, git mutation, web/plugin, eval, pane, or the
legacy transient-process Fleet operations. fleet.agents reads the same durable admissions shown by
the Workers view and flux fleet agents, so the coordinator can discover ids before acting. Safe
Board/Fleet reads are pre-authorized unless an operator-authored deny rule wins. A task child has
its own read-only catalog and cannot inherit coordinator mutations or delegate again.
[main].loop and [main].research_loop are required. Each free-form coordinator turn runs the
first operator-authored Flux-Lang loop with only the current request and the closed coordinator
catalog. Every task child runs the second operator-authored loop over its independent read-only
catalog. Neither path falls back to the general adaptive intent/explore, create_plan, or
retained-history budget path. Missing or invalid loop configuration refuses before a model call.
The main agent plans against revisioned context rather than an untracked system prompt:
flux fleet goal set values engineering "Prefer evidence and reversible changes" --output json
flux fleet goal set company product "Make Flux the agent automation substrate" --output json
flux fleet goal set project flux "Replace repository helper scripts" --output json
flux fleet goal list --output json
flux fleet ingest "Add a cross-repository board" --source user --output json
flux fleet ingest "Reviewer found a stale gate" --source agent --from reviewer-2 --output json
Reusable roles are admitted from templates. The coordinator can also create a temporary specialist on the fly; both receive the same durable registration, capability/mode/fence validation, limits and lease, and neither path can create a second main agent:
flux fleet spawn --template story-worker --item api/C-41 --name writer-C-41 --output json
flux fleet spawn --role critic --instructions "Challenge D-12 against project goals" \
--mode read-only --name critic-D-12 --output json
Configuration makes an agent available for admission; it does not silently register a live member. Future CLI-harness and remote A2A task backends use this same admission record without changing who owns the roadmap.
Decisions without stopping autopilot
flux fleet decisions aggregates open board decisions. Human mode prints each question, structured
options/trade-offs and recommendation so the operator usually only needs to pick. Linked stories
stay blocked, while unrelated eligible work continues.
flux fleet decisions --output json
flux fleet decisions --auto --output json
Auto mode admits a fresh adversarial decision agent. It sees the applicable values/company/project goals, must challenge the proposing agent's recommendation, and records a rationale. It does not reuse the proposer context or turn every worker into a coordinator.
Schedule and dispatch
The scheduler reads the Board's active-milestone projection and configured waves. It preserves
program order, combines story/program dependencies, and never falls back to an unrelated ready
story when the workspace has a program catalogue. Explicit BOARD/ITEM arguments go through the
same authority and readiness checks.
Before an item is dispatchable, every boundary in this chain must hold:
repository story exists
│
▼
Goal and Acceptance define done
│
▼
workspace schedule authorizes the BoardRef
│
▼
dependencies are satisfied
│
▼
story is ready on the pinned canonical ref
│
▼
worker capacity + write-set independence are valid
│
▼
dispatch
Fleet refuses rather than borrowing a dirty checkout, guessing a missing contract, or widening a worker's scope to make the schedule fit.
flux fleet refresh --output json
flux fleet schedule --output json
flux fleet start --idempotency-key fleet-start --output json
# Let Flux choose the top eligible wave.
flux fleet run --idempotency-key next-wave --output json
# Or name a bounded set explicitly.
flux fleet run api/C-41 web/C-12 --idempotency-key aug-05-wave --output json
One configured wave contains at most ten stories. Dispatch creates a distinct wave instance. For
each repository, run pins the canonical commit and
creates one integration branch/worktree. Every writing story receives one child branch/worktree
from that exact base, one writer, one persistent Flux session and story-sized commits:
canonical base
└── wave integration worktree
├── story C-41 worktree
└── story C-42 worktree
Targeted/cheap checks run in story children. Accepted exact commits integrate into the wave in dependency order; the configured full gate runs once only after the assembled tree is final. Overlapping or uncertain write sets serialize or refuse before integration. Read-only maintenance tasks are the default and need no story worktree:
flux fleet task api "audit the next ready contract" --mode read-only --output json
flux fleet task api/C-41 "implement the accepted story" --mode write --output json
Unattended driving
flux fleet drive is the driver itself, as a verb. One tick runs four phases in a fixed order and
nothing else:
flux fleet drive --tick --output json # exactly one tick
flux fleet drive --loop --interval 60 --output json # ticks until stopped or quiesced
flux fleet drive --loop --interval 60 --max-ticks 20 --output json
| phase | what it does |
|---|---|
| report | fingerprints exactly the facts a tick acts on — revision, admission window, every wave and worker status |
| advance | a wave whose every story holds an accepted handoff leaves awaiting-handoffs for handoffs-ready |
| accumulate | records the tick on fleet state (drive.ticks, drive.fingerprint) and journals fleet.drive.tick |
| dispatch | sends workers at the schedule, up to the free width |
Judgment stays outside the driver: planner, retro and scribe remain authored .flux loops, and
drive owns only these mechanics. Every read goes through the native store and every write through
the revision-guarded delta path, so a tick can never race a coordinator or an operator.
Dispatch consults flux board reconcile and fails closed. If reconcile cannot be read the
tick dispatches nothing and says so, because an empty already-built set is indistinguishable from
"nothing is already built" — the case that once sent ten workers at stories whose work was already
merged. Anything held back is named with its reason rather than dropped:
{
"schema": "flux.fleet-drive-tick/v1",
"tick": 12,
"idle": false,
"dispatch": {
"width": 2,
"items": ["api/C-41"],
"withheld": [
{"item": "api/C-40", "reason": "already-built", "signals": ["commit-subject"],
"detail": "6 of 6 Acceptance criteria ticked and every named artifact is present in this checkout (RequestEnvelope)",
"evidence": [{"acceptance_ticked": 6, "acceptance_total": 6, "reviewed_complete": true},
{"artifact": "RequestEnvelope", "present": true}]},
{"item": "web/C-12", "reason": "claimed", "detail": "wave-88 still holds an attempt at this item"},
{"item": "db/C-7", "reason": "parked", "detail": "wave-91 is parked pending a decision: waiting on the API decision"}
],
"released": [
{"item": "api/C-70", "reason": "already-built",
"detail": "the Acceptance names 1 artifact(s) this checkout does not have (AgentReport); the signal is a mention of the id, not its implementation",
"evidence": [{"acceptance_ticked": 1, "acceptance_total": 8, "reviewed_complete": false},
{"artifact": "AgentReport", "present": false},
{"artifact": "SpawnActivity", "present": true}]}
]
}
}
already-built is the one reason the driver does not take on trust. Reconcile answers "does some
commit name this id?", which fires on a docs commit, on a sibling story's commit, and on a doc
comment forward-referencing the id — so a tick with eight free slots and nine ready items once
dispatched one, and both stories it held back were unbuilt. Each finding is now verified against the
story it names before it may withhold. A wholly unticked Acceptance settles it outright — the story
saying, in its own words, that it still owes everything, outranks a commit that only mentions the id.
Otherwise every symbol and path the story's own ## Acceptance names must be present in that
member's checkout, found outside Markdown and outside comments. One absent name releases the item:
absence is conclusive, while presence is weak enough that it never withholds on its own.
A signal that fails verification appears under released with the evidence that failed it, and the
item dispatches. Withholding is the strongest and the only silent action a tick takes, so it fails
closed toward dispatching: a redundant turn is recoverable, a silently skipped story is not. And
because a withhold is invisible from the outside, drive records how long each one has run —
flux fleet doctor reports any ready item withheld across five consecutive ticks as
item-withheld-persistently, so a permanent withhold cannot masquerade as an empty queue.
Dispatch reads waves[].eligible, so a [[waves]] entry used to be the only way an item could be
seen at all: an item the milestone program marked eligible and no wave named reached no dispatch and
no withhold either, which made an operator's hand-written wave a precondition for scheduling any
story. The schedule now composes the leftovers itself. Eligible items no [[waves]] entry names are
grouped into a synthesized unit under the same bounds a configured wave carries — one repository,
at most max_wave stories — reported with "synthesized": true so the view never credits an
operator with a wave they did not write. A configured wave still decides every item it names,
whatever state it is in; this widens what dispatch can see, it does not reinterpret a wave somebody
composed.
Whatever a wave cannot carry appears under unschedulable with the reason — wave-capacity,
wave-dependencies, wave-done, or repository-unconfigured for an item whose repository has no
[[repositories]] entry to cut a worktree in — and the tick turns each into an ordinary withheld
record. Every eligible item a tick sees is therefore dispatched or explained. drive also counts,
per item, how many consecutive ticks sent nothing at all, and flux fleet doctor reports a run of
five as item-never-dispatched: that count comes from the eligible pool minus what dispatch sent,
so it still fires for an item nothing ever explained.
--loop runs under a durable single-instance guard, so a second driver refuses and names the pid
holding it; a lock naming a process that is gone is not a lock and needs no hand cleanup. The loop
is stopped by durable facts rather than a signal: flux fleet stop and flux fleet quiesce both
end it at the next tick boundary, and a quiesced fleet refuses drive outright because a tick
dispatches.
The typed handoff
A worker result is not parsed from prose. Its FleetHandoff names:
- the exact
BoardRef, worker, session, worktree, and branch; - an exact commit, never only a branch name;
- the normalized approved and observed write sets;
- test argv;
- host-observed failing-before and passing-after evidence;
- a short summary.
For behavioral work, the host runs the test before implementation and requires it to fail, then runs the same typed argv at the returned commit and requires it to pass. Documentation-only work must declare validation argv and a reason no failing test applies. The host compares the commit diff with the approved write set; a worker cannot widen its own fence by claiming it did not.
Malformed or contradictory handoffs are refusals. Cancellation or a crash leaves the worktree, commit, event log, and evidence intact for inspection and resume.
The ergonomic command is fully typed; --test-arg is repeated so no shell string is parsed:
flux fleet handoff wave-7 api/C-41 --commit FULL_SHA \
--write-set crates/api/src/lib.rs --write-set crates/api/tests/contract.rs \
--test-arg cargo --test-arg test --test-arg=-p --test-arg api \
--failing-before --passing-after --summary "Implemented the accepted contract" --output json
--from-worktree replaces the repeated --write-set when the commit range already proves it: the
host derives the write set from base..HEAD in the story worktree, and the owning worker from the
agent that wave assigned to that worktree. Neither recorded fact is retyped, so neither can be
mistyped.
Review and bounded rework
flux fleet review admits a fresh read-only agent for each candidate and gives it one packet: the
story's Goal and Acceptance as they stood at the reviewed commit, the exact normalized diff, the
host-observed write set, and the candidate/base identities. That packet is its entire workspace — it
holds no repository checkout, no fleet state, and no part of the writer's conversation, and its
read-only admission means it cannot modify the change it is judging. Only the story's own writer
applies a finding.
flux fleet review wave-7 --output json # every candidate still owed a review
flux fleet review wave-7 --item api/C-41 --output json # one candidate
flux fleet review wave-7 --item api/C-41 --from review.json # an external reviewer's typed document
It returns one typed verdict, PASS or REWORK. It cannot park, cancel or accept work: the rework
budget below is a host invariant, and a reviewer that could end work would not be bound by it.
exact commit + typed handoff
│
▼
fresh read-only review over the exact commit
├── PASS ──→ dependency-order integration
└── REWORK ──→ same writer session (at most two deliveries)
│
└── third failure ──→ parked
Integration refuses a candidate no independent review passed at that exact commit. A moved commit
makes its review stale, and a review that could not run is not a pass — every record carries
examined beside verdict, so "the reviewer looked and found nothing" and "nothing looked" are
different rows rather than two readings of an empty findings list:
| situation | state | verdict | examined |
|---|---|---|---|
| the reviewer examined it and found nothing | reviewed | PASS | true |
| the reviewer examined it and found something | reviewed | REWORK | true |
| no contract at that commit, or the fleet is stopped | not-run | BLOCKED | false |
| the candidate does not fit a reviewable packet | incomplete-context | REWORK | false |
| the turn failed, or returned no typed document | failed | BLOCKED | false |
| the bounded retry run is spent | attention | BLOCKED | false |
Findings are structured rather than prose, so they can be counted and routed. Each carries a
category (contract, correctness, safety, evidence, scope, regression,
maintainability), a severity (blocker, major, minor), a confidence, the affected
component, and exactly one piece of evidence — a path/line, a command, or a named invariant.
source separates a reviewer's assessment from a fact the host derived.
{
"schema": "flux.fleet-review/v1",
"reviewer": "reviewer-2",
"reviewed_commit": "FULL_SHA",
"verdict": "REWORK",
"findings": [
{
"category": "contract", "severity": "blocker", "confidence": "high",
"component": "crates/api/src/lib.rs",
"evidence": {"path": "crates/api/src/lib.rs", "line": 91},
"detail": "Preserve the prior error class"
}
]
}
--from records a review produced outside the fleet — by a human, or by another harness — through
the same parser and the same validation, and refuses a document naming the story's own writer as its
reviewer. flux fleet drive reviews every ready candidate on its own, so an unattended fleet needs
neither call.
The host allows two rework deliveries. A third request parks the item with unresolved findings; a
board transition, cancellation, restart, or new CLI call cannot reset the counter. flux fleet rework delivers findings directly for the same effect:
flux fleet rework wave-7 api/C-41 --reviewer reviewer-2 --reviewed-commit FULL_SHA \
--path 'crates/api/src/lib.rs:91:Preserve the prior error class' \
--invariant 'No partial board evidence on refusal' --output json
message uses the same acknowledged steering channel:
flux fleet message worker-1 "address review r-17" --wait accepted --output json
flux fleet message worker-1 "address review r-17" --wait delivered --output json
flux fleet message worker-1 "address review r-17" --wait completed --output json
accepted means durably journalled, delivered means the persistent session acknowledged the
steer, and completed means that follow-up turn reached a terminal result. Idempotent replay does
not consume another rework attempt.
Integration, the final gate, and apply
Accepted story commits are integrated in dependency order onto one local candidate. Inputs carry BoardRef, writer/worktree identity, exact commit, write sets, and targeted evidence. Duplicate stories/writers, unsafe overlap, and more than ten inputs refuse before integration.
After the final accepted commit, the declared full repository gate runs exactly once. A missing or unrunnable gate is red. A conflict or red gate records the exact candidate and preserves its history; planning stories do not become done.
A green gate records a local fleet/<wave> branch as apply-eligible. Nothing is published yet:
targeted checks green
│
▼
story commit accepted by review
│
▼
wave integration gate green
│
▼
local candidate recorded
│
▼
explicit local apply
│
▼
candidate pinned by an accepted tag
│
▼
promotion gates the accumulation
│
▼
member's LOCAL canonical branch advanced
│
▼
canonical story status done
│
▼
push / release / deployment / milestone exit
remain separate operator actions
flux fleet status --output json
flux fleet inspect integration wave-7 --output json
flux fleet integrate wave-7 --if-revision 17 --idempotency-key integrate-wave-7 --output json
flux fleet apply wave-7 --if-revision 18 --idempotency-key apply-wave-7 --output json
flux fleet apply wave-7 --only api --output json
flux fleet promote --output json
apply accepts a candidate: it checks that the repository recorded exactly one green final gate
and that the candidate branch still points at the commit that was gated, then pins that commit with
an annotated fleet/accepted/<wave>/<repository> tag. It does not merge, and it does not require the
canonical ref to have stood still — ordinary work continues while a wave is gated, and a candidate is
no less valid for it. The tag is what makes accepted-but-unlanded work impossible to lose.
Acceptance is deliberately not landing. Writing the canonical branch is a separate step with its own
verification, because a candidate is green against the base it was gated on — the acceptance record
names that base, and whether the canonical ref has moved since, precisely so the landing step knows it
must re-gate. That step is fleet promote,
and until it runs, apply reports the wave as awaiting-delivery rather than claiming delivery.
apply <wave> --only <repository> accepts one repository's candidate from a wave whose other
repositories failed. Integration assembles and gates one candidate per repository, so a wave can hold
a green candidate beside a conflicted one; without this, an independent repository's delivered work
would be stranded by a collision it had no part in. A named apply reports what it left behind and does
not change the wave's own verdict.
apply never pushes, opens a pull request, pushes a tag, releases, deploys, or deletes a worktree.
Those are separate operator decisions.
Promotion: landing accepted work on a local canonical branch
flux fleet promote is the step that writes the branch. It is the only Fleet operation that does.
flux fleet promote --dry-run --output json # the exact merges it would make, per member
flux fleet promote --output json # accumulate, gate, land
flux fleet promote --only api --output json # one member
Per member, in the order the depends_on graph declares:
- Accumulate. Every candidate
applyaccepted for that member whose canonical ref does not already contain it. A member with fewer than[promote] thresholdwaiting is reported and left alone. The threshold defaults to1— land as soon as anything is accepted; raise it to batch several waves into one gate run, at the cost of leaving accepted work off the branch for longer. - Merge in a throwaway worktree branched from the canonical ref, never in a working checkout, so a long gate cannot be disturbed and a red gate leaves the branch untouched. A candidate that conflicts with the accumulated tree is left out and reported by name; it stays pinned by its accepted tag and the remaining candidates still promote. Forcing it would land an ungated tree.
- Anchor and gate. The accumulation is pinned with an annotated
fleet/promote/<member>/<wave>tag before the gate runs, so a red gate leaves something to triage rather than an unreachable commit. Then the repository's configured gate runs against that exact tree. A member with no configured gate is red: promotion does not land an unproven tree. - Land by a compare-and-swap ref update. The branch is advanced with
git update-ref, which is atomic, refuses if the branch moved while the gate ran, and writes no working tree anywhere — promotion never runs a merge inside a checkout you are working in. - Verify by re-reading git. The canonical ref is resolved again and each merged candidate's
containment is asked of git.
landeddescribes the ref, never the merge's exit code, and a wave becomesappliedonly where that containment is observed.
Two refusals are worth knowing before you configure a member:
- A red gate anywhere leaves every member's branch untouched. The members are a release train ordered by the dependency graph; a half-promoted train is a state no later operation can reason about. Refusals and below-threshold skips are not red gates and withhold nobody.
- A member whose
canonical_refis remote-tracking cannot be promoted at all, and is refused by name. Only a push movesorigin/main, and promotion never pushes. Declare a local branch.
Promotion advances the ref without touching any working tree, so a checkout sitting on that branch
keeps the index and files of the previous tip. Promotion names every such checkout in its
warnings: git commit -am there would revert the work just landed, so reconcile it before
committing in it.
Nothing in promotion pushes, opens a pull request, tags a release, or deploys. Landing, publication and release are three separate events; Fleet performs only the first.
Reclaiming storage
A wave's build directories are the largest thing the fleet leaves on disk, and disk is what actually
caps how many workers can run. Acceptance reclaims automatically; fleet reclaim covers the waves
that ended some other way.
flux fleet reclaim --dry-run --output json # what would be freed
flux fleet reclaim wave-7 --output json # one wave
flux fleet reclaim --output json # every wave that is not in flight
Build output is regenerable and always goes. A worktree is removed only for a wave that is applied or cancelled — an unfinished wave keeps its shape, because a worktree that happens to be empty is not the same as one that is no longer needed, and a wave with deliverable work and nowhere to assemble a candidate cannot be repaired. A worktree that still holds a commit or an uncommitted change is retained with the reason, and a branch is deleted only when git agrees it holds nothing unique.
A wave that can still advance is refused rather than reclaimed: deleting a build it is about to use would cost work rather than space.
Repairing a wave's structure
Reclamation, a hand-deleted directory or an interrupted assembly can leave a wave whose topology names
a worktree disk no longer has, or whose integration checkout sits somewhere other than the base it is
pinned to. Both are mechanical to fix from records the fleet already holds — the source checkout, the
branch, the pinned base — and both were previously hand-written git under pressure.
flux fleet repair wave-7 --dry-run --output json # what it would rebuild
flux fleet repair wave-7 --output json
A missing checkout is recreated on its recorded branch, so a worker's committed work comes back with it; only a branch that is gone too is created afresh at the pinned base. A derived worktree — the integration assembly and the pinned verification checkout — is put back on its base when it has drifted off. A story worktree is never rewound: its commits are the deliverable, and being ahead of the base is its correct state.
Repair refuses anything that would discard work and reports the reason instead of acting: a worktree holding an uncommitted change, one git cannot inspect, and one sitting on the commit its gate recorded as the candidate. An applied or cancelled wave is refused outright, because its worktrees are gone on purpose.
Parking a wave
Some waves have to wait for a human — an open question, an unavailable dependency, a review that has
not happened. park records that pause on the wave itself, with the reason and the state the wave
returns to.
flux fleet park wave-7 --reason "waiting on the API decision" --output json
flux fleet status --output json # the pause and its reason are on the wave row
flux fleet unpark wave-7 --output json # back to the state it held
Parking is a lifecycle state, not an annotation. fleet status reports the reason on the wave row in
both its JSON and human forms, so a paused wave is legible without reading durable state and nothing
re-decides it every minute; unpark restores the recorded previous status, so leaving the pause is a
verb rather than an edit. A wave parked by exhausted rework rounds carries no recorded previous
status and returns to awaiting-handoffs.
Parking harvests first. A worker that committed its deliverable and then ran out of turn holds
that work in a worktree nothing has recorded, and a pause written on top of it buries a finished
story under a decision. park records those handoffs from the worktrees before it writes the pause,
reports them as data.harvested, and journals wave.park.harvested.
Unparking resets the budget the park exhausted. The pause has been answered, so every story the
park froze returns to its accepted handoff and its rework_attempts counter is cleared, reported as
data.budget_reset. Without that, the story stayed parked — so the wave could never earn
handoffs-ready again — and the very next review parked it again on its first round, buying the
human's answer nothing.
A parked wave keeps its claim on its items: the work it holds is still on disk and the pause is a
decision a human has yet to answer. fleet drive therefore withholds those items with
"reason": "parked" and the recorded reason, rather than reporting them as live work. The claim is
released only when the wave provably ends — applied or cancelled — and the items become dispatchable
again.
Parking never overwrites an existing reason: a second park is a conflict/precondition, as is
unparking a wave that is not parked. A parked wave is not counted as requiring attention — the
decision to pause has already been made — and its build output stays reclaimable, because its commits
live on branches.
Restart and resume
The durable state/event log pins board revisions, source commits, write sets, worktrees, sessions, attempts, handoffs, reviews, gates, and candidates. On restart the supervisor folds those records and execution boards; process memory is not authoritative.
flux fleet status --output json
flux fleet resume --output json
flux fleet resume worker-1 --output json
flux fleet cancel worker-1 --output json
flux fleet stop --output json
Status has an independent read lane, so a busy or stuck worker cannot make fleet inspection hang.
Resume is addressed. Each pending item records the agent it was sent to — coordinator intake and
tasks belong to main, a message belongs to the worker it names — and a resumed agent is asked to
continue its own assignment plus the items addressed to it and nothing else. Resuming a worker
therefore neither shows nor acknowledges the coordinator's queue, so every item is still delivered at
most once, to the agent it was addressed to.
Quiescing before an install
Installing a new Flux binary while a wave is in flight is the one maintenance act that can corrupt a
run in progress. fleet quiesce makes it a verb instead of a procedure.
flux fleet quiesce --reason "install 0.9.0" --output json
flux fleet resume --output json
quiesce records a durable maintenance window on fleet state and then reports what is still moving.
The window is recorded first and unconditionally: run, spawn and task refuse while it is set,
so no wave can be dispatched between the check and the install. The command itself fails with
conflict/precondition while any worker turn is still in flight, naming each one, so
flux fleet quiesce && install cannot walk past a live worker. Re-run it once they settle to
confirm; data.safe_to_install is the machine-readable form of that confirmation.
Inspection, handoff, integrate, apply and reclaim stay available while quiesced — the window
stops new work, it does not blindfold the operator. fleet status reports the window under
quiesce, and only fleet resume lifts it.
status and dashboard are bounded projections, not a dump of durable state. They report main
state, active and attention worker counts, wave and item state, exact board references,
repositories, current sessions, the last transition or error summary and the current revision, and
they stay inside a fixed byte budget however much turn history the fleet has retained. Answers, tool
events, diffs, instruction bodies and repository contents are never copied into them; anything the
budget drops is reported as payload-free omission metadata, and the human form names the next useful
inspection or recovery command. A worker counts as active only while its recorded transition says a
turn is in flight, so a completed, failed, cancelled or interrupted process is never active merely
because an old receipt still says working.
Bounded inspection replaces helper scripts
Every inspect command has a stable JSON form and an explicit bound:
flux fleet agents --output json
flux fleet worktrees --output json
flux fleet events --limit 200 --output json
flux fleet events --follow --output ndjson
flux fleet logs worker-1 --limit 200 --output json
flux fleet inspect snapshot --limit 100 --output json
flux fleet inspect wave wave-7 --limit 100 --output json
flux fleet inspect worker worker-1 --limit 100 --output json
flux fleet inspect result api/C-41 --limit 100 --output json
flux fleet inspect activity --limit 100 --output json
flux fleet inspect worktree worker-1 --limit 100 --output json
flux fleet inspect integration wave-7 --limit 100 --output json
flux fleet inspect gate wave-7 --limit 100 --output json
flux fleet inspect gate wave-7 --repository api --limit 100 --output json
flux fleet inspect source api --limit 100 --output json
flux fleet inspect search C-41 --limit 100 --output json
flux fleet inspect story api/C-41 --limit 100 --output json
flux fleet inspect pull-request wave-7 --limit 100 --output json
flux fleet note "Candidate preserved while CI is unavailable" --output json
flux fleet dashboard --output json
flux fleet agents lists every durable worker as a bounded summary: id, role, task kind, status,
Board ref, wave, session and resolved loop identity. It never returns worker instructions or full
historical turn receipts; use a targeted inspect view when deeper evidence is needed.
flux fleet inspect gate <wave> answers the most-wanted question about a red wave — why did the gate
fail — without knowing the shape of state.json. It returns each repository's gate status,
candidate, argv, exit code and the gate's own captured stdout/stderr, tail first: the last
line the gate wrote is the first line reported, because that is where the failure is and a gate log
is far longer than any bound. Each stream reports line_count and truncated, so a bounded tail is
never mistaken for a short log. --repository <id> narrows a multi-repository wave to one gate, and
a gate that never ran reports its reason instead of evidence.
flux fleet status and dashboard use the same bounded operational projection. They report current
coordinator, worker, wave, Board-ref, session, repository and attention state without copying
answers, tool events or intake bodies from durable receipts. The response names the targeted
inspect worker|wave|activity|snapshot command for deeper evidence; terminal workers are not made
active or attention-worthy merely because an older receipt or error said otherwise.
Targeted inspection has a fixed structural byte ceiling in addition to its --limit item bound.
Small terminal identity, status, outcome and session fields are retained first. Oversized strings,
event collections, handoffs or reviews become indexed omission records that report the source path
and reason; string omissions also report encoded size and digest, while collection omissions report
kept and omitted counts. Structured JSON is never sliced into an invalid fragment.
Events are redacted before persistence, not merely at display time. The corpus covers credentials,
.env, key files, model commentary, commands, diffs, and JSON fields. Follow mode emits NDJSON so
an agent does not parse terminal decoration.
Fleet also bounds evidence before the supervisor retains it. Adaptive tool results larger than
64 KiB become payload-free omission records; accumulated adaptive message history refuses above
512 KiB and a complete provider request refuses above 1 MiB. An operator-authored ai_segment is
bounded differently, because a long implementation loop accumulates many in-budget results and may
already have committed by the time it reaches the ceiling: crossing its history budget sheds the
oldest tool-result payloads into digest receipts and continues, and if elision cannot free enough the
segment returns its evidence ledger as a result rather than failing the turn. Author
max_history_bytes on the segment to raise that ceiling. Child NDJSON lines are limited to
240 KiB (including the newline), below the 256 KiB parser boundary. An oversized nonterminal event
becomes event_omitted; an oversized turn_end keeps its session, outcome, usage and cost while its
answer/error payload becomes payload_omitted. turn.budget, model.call and the Fleet receipt's
stream_budget report only the affected domain, byte count, limit and post-redaction digest—never
the omitted repository content. Use inspect worker or inspect result to correlate that digest;
do not expect status to reproduce an omitted payload.
The paired board commands replace progress collectors and report generators:
flux board stats --history --output json
flux board report --format html -o progress.html
flux board report --format svg -o progress.svg
note appends redacted coordinator context to the same durable event stream. It is for facts that
must survive a restart but are not a worker instruction; use acknowledged message for steering.
The stable agent API
Fleet uses the same flux.cli/v1 envelope, revision preconditions, idempotency keys and closed
request format as board. schema is the complete installed-version contract, while call is the
automation escape hatch into the same validated implementation:
flux fleet skill
flux fleet schema --output json
flux fleet call status --request status-request.json --output json
For call, the request document supplies only the operation arguments, for example
{"schema":"flux.cli/v1","request_id":"status-1","args":[]}. It never invokes a shell or bypasses
fleet state validation.
Safe agent operating loop
An AI coordinator can keep only this compact loop in context:
- Run
flux fleet schema --output jsononce for the installed version. - Run
validate,schedule, andstatusbefore dispatch. - Dispatch only dependency-satisfied, explicitly authorized board items.
- Inspect status/activity through bounded JSON; use acknowledged
messagefor follow-ups. - Resume durable state after interruption; do not reconstruct it from terminal history.
- Apply only a recorded green candidate, with its current revision.
- Treat push, release, deploy, and cleanup as separate operator actions.
flux fleet skill renders that loop as a concise Agent Skill for Claude, Codex, and other harnesses.
Deliberate follow-ups
V1 workers are native local Flux sub-agents. Three later epics preserve clean boundaries:
- a generic task-agent backend plus local Codex, Claude, Hermes and Pi CLI adapters;
- authenticated invitation/hello/admission/lease for remote A2A members, followed by an A2A task backend; and
- a polished TUI centered on the main coordinator conversation, with read-only worker-channel peeks and native board, decision and statistics views.
Those transports and views reuse the same BoardRefs, admission records, evidence, decisions and publication fence. They are not hidden capabilities of the local V1 and are not required to call the board/fleet CLI from Claude or Codex today.