stella fleet

Fan a batch of tasks out to a fleet of worker agents in one shared tree, coordinated by cooperative file claims.

Run many tasks in parallel in one shared working tree, coordinated by cooperative file claims and scheduled in dependency-ordered waves. A task only gets its own git worktree when a plan file opts it in with isolation = "isolated". Every attempt, commit, and dollar spent is recorded in .stella/private/fleet.db, and any isolated worktrees and their fleet/<slug>-<hash> branches are left in place for you to review.

Synopsis

stella fleet <task>... [--plan <FILE>] [--max-concurrency <N>] [--base-ref <ref>] [--watch] [--pipeline <VARIANT>] [--task-timeout <SECS>] [--require-verdict]
stella fleet clean [--dry-run] [--force] [--age <DAYS>] [--base-ref <ref>]
stella fleet claims [--all] [--format <text|json>]

A prompt whose first word exactly matches a subcommand name (clean, claims) is read as that subcommand. If that happens, put your prompts after --: stella fleet -- clean up the dead code.

What it does

Each task prompt becomes an independent task running in the shared repository root. Workers coordinate through cooperative file claims: paths declared upfront in a plan (claims), plus a claim taken the first time a tool writes to a file. A conflict surfaces in under a second at write time, naming the other task involved, and commits interleave on one branch with no merge-back needed. A task a plan file opts into isolation = "isolated" gets a dedicated git worktree instead, under .stella/worktrees/<slug>. It branches from the current HEAD (or --base-ref), pinned to a commit at the start, onto a fleet/<slug>-<hash> branch (the hash suffix keeps re-runs from colliding with branches you kept).

Tasks run in dependency-ordered waves, up to --max-concurrency at once within a wave. Each worker is a full engine run through the plain step loop, or, with --pipeline <variant>, through an installed wrapper plugin bound once per worker attempt. The plugin comes from the workspace you invoked stella fleet in, and is granted the tree the attempt actually runs in. Every attempt's execution row records which variant it used. Unlike stella goal, a fleet run applies no separate approval step of its own: a fleet attempt's result is its turn's own outcome, so only a wrapper's own hold logic can pause a round.

Progress, attempts, commits, and cost are saved to .stella/private/fleet.db and appear on the Observatory dashboard's Fleet runs card.

Isolated worktrees and fleet branches are not cleaned up automatically. They stay on disk so you can inspect, test, and merge each result yourself. Reclaim the finished ones with stella fleet clean once you're done reviewing. A failed task is never retried automatically, and any tasks that depend on it end the run marked skipped.

A task can only be run by one session at a time. Before starting a worker, the fleet takes a dispatch claim on the task: a short-lived lease in .stella/private/fleet.db, held under the run's id. If a second stella fleet in the same workspace reaches the same task while the first is still working on it, that task is reported as a dispatch failure naming the run that holds it, instead of running twice. The claim is a lease, not a lock. A live run renews it while its worker runs and releases it the instant the attempt finishes. A run that gets killed outright loses its claim automatically once the lease runs out, after 15 minutes. Nothing needs to be cleaned up by hand, and there's no override flag.

For anything beyond a handful of independent prompts, use a plan file (--plan). It lets you declare a task id, title, prompt, depends_on (to order the waves), isolation (shared_tree, the default, runs in the repo root under the claim rules above; isolated gives a task its own worktree and branch, for work that genuinely diverges, like best-of-N attempts on the same files), claims (file paths held as cooperative locks while a task runs, so two tasks never edit the same file at once), and test_command (the check that decides that task under --pipeline <variant>). A plan file is the only way to declare a test command: a plain prompt on the command line builds a task with none, so a wrapper plugin has nothing to check against it. See Agent Fleets for the full plan format.

Flags

<task>...

One or more task prompts, each an independent shared-tree task. Required unless --plan is given.

--plan <FILE>

A .json or .toml plan file with [[tasks]] entries — id, title, prompt, and optional depends_on, isolation, claims, and test_command — instead of prompts on the command line.

--max-concurrency <N>

Maximum tasks running at once within one wave. The default is the self-driving governor's number: machine probes crossed with the learned calibration, capped at two cores short of the machine and never below 4. SELF_DRIVING_PARALLEL_MAX overrides the cap. So an idle 16-core box fans out wider than a busy laptop, with nobody choosing a width.

Default the governor's number

--base-ref <ref>

Git ref the isolated worktrees branch from. The default is pinned to a commit at the start, so commits made mid-run can't move the base.

Default current HEAD

--watch

After the fan-out, watch each successful branch's CI until it finishes, and reconcile its PR status with gh — checking every 30 seconds, with a 10-minute startup grace period, a 20-minute stall limit, and a 2-hour overall cap. Exits non-zero if any watched branch ends red.

--pipeline <VARIANT>

Run each worker attempt through the installed wrapper plugin whose manifest declares this [wrapper] id (stella plugin list). The variant is resolved once before the fan-out, so a typo fails the whole command rather than each task on its own; each attempt then runs its own copy of the plugin in its own tree. Omit the flag to run the plain loop.

--task-timeout <SECS>

Wall-clock limit per worker attempt. When it expires, the task gets the same clean stop the dashboard's [x] button sends, and the attempt is reported as failed instead of holding its concurrency slot forever.

Default unbounded

--require-verdict

Fail any attempt whose --pipeline wrapper didn't declare its requirements met. Per attempt: an unmet or undecided result fails that attempt by name, and a failed attempt fails the run, the same rule any failed task already follows. If the attempt already aborted on its own, that reason wins instead. Refused without --pipeline, since nothing declares a result there.

--output-format <text|json|stream-json>

json / stream-json keep the run headless: the live grid stays off and stdout stays machine-readable, including the error message on failure. A flag of fleet itself, placed after the subcommand. Env STELLA_OUTPUT_FORMAT.

Default text

Spend limit

The spend cap comes from the global --spend-limit flag (or STELLA_SPEND_LIMIT), not a flag of fleet's own. It reads most clearly placed before the subcommand, though either position works. Each worker is limited to a share of the total cap divided across the concurrency width (spend limit ÷ max-concurrency), and the fleet stops starting new waves once total spend crosses the cap.

stella --spend-limit 5 fleet --plan release-prep.toml

stella fleet clean

Isolated worktrees and their fleet/* branches build up over time: every isolated task in every run leaves behind a directory under .stella/worktrees/ and a branch, and nothing removes them on its own. stella fleet clean is the explicit way to remove them. No part of a fan-out calls it for you.

A worktree is reclaimed only when all of these are true, and anything kept is printed along with the reason:

  • the ledger (.stella/private/fleet.db) shows no unfinished attempt using it — an in-flight worktree is kept even with --force;
  • git status in it is clean;
  • its branch's commits are already part of the base ref (checked with git merge-base --is-ancestor), so nothing is lost by deleting it.

fleet/* branches whose worktree is already gone are cleaned up by the same rule. The main checkout is never a candidate, and no branch outside the fleet/ namespace is ever touched by a git command, with or without --force. When git can't answer a question, such as an unresolvable ref or a merge-base check that fails for its own reasons, the answer is always "keep."

stella fleet clean --dry-run          # what would go, and why the rest stays
stella fleet clean                    # reclaim the ones proven finished
stella fleet clean --age 14           # only what last finished 14+ days ago
stella fleet clean --age 30 --force   # also month-old dirty/unmerged work — destructive
--dry-run

Decide and print the results, but remove nothing.

--force

Also reclaim worktrees with uncommitted changes and branches whose commits aren't in the base ref. This destroys that work. It never touches a worktree whose attempt is still in flight, and never touches anything outside the fleet/ namespace.

--age <DAYS>

Only consider worktrees whose last attempt finished at least this many days ago. A worktree with no recorded finish time can't be shown to be old enough, so it's kept.

--base-ref <ref>

The ref a branch's commits must already be part of to count as reclaimable. The sweep stops instead of guessing if none of the defaults can be found.

Default origin/HEAD, else main, else master

stella fleet claims

A dispatch claim stops two sessions from taking the same task, but a claim nobody can see is a claim someone else runs into by hand, which is exactly the problem this system was built to prevent. stella fleet claims is the way to read it: what's claimed, who holds it, how long they've held it, and when the lease runs out.

Two different kinds of claims share the same word. A file claim asks "may I write this path?" and coordinates writers inside one run. Those are the cooperative locks described above, and this command doesn't list them. A dispatch claim asks "is another session already doing this work?", and only this kind stops two sessions from duplicating an entire run.

stella fleet claims                   # what this workspace is currently working on
stella fleet claims --all             # including claims whose holder died or moved on
stella fleet claims --format json     # the same rows in a versioned JSON format
Stella — fleet dispatch claims

    ● task:fix-1136   run-8f21a3-41207  held  3m 00s  expires in 12m 00s
    ○ issue:1136      run-2b90cc-41999  held 18m 04s  lapsed 2m 10s ago

  2 claim(s), 1 live

Read-only and offline: it opens .stella/private/fleet.db, never writes to it, and needs no API key. A workspace where no fan-out has ever run reports no claims rather than an error.

There is deliberately no flag to break someone else's claim. A lease expires on its own: a session that gets killed outright stops holding its claims once the lease time runs out, with nothing to clean up by hand. "Unwedge a stuck claim" is not something that needs to exist as a command, since one that did would be a supported way to hand the same task to two workers at once.

--all

Also list expired claims. A live listing answers "what is being worked on right now." Only this flag answers "who was last on this, and did they die holding it," the question left behind by a killed session. A claim that was released cleanly leaves no row at all.

--format <text|json>

text (default) prints the table shown above. json gives the same rows in a versioned format (schema_version, then rows), with each row carrying live, expires_in_ms (negative once expired), and held_for_ms measured against the time the listing was read.

Default text

The claim key means nothing special to the lease itself: a fan-out claims task:<id>, and a dispatcher working from a tracker would claim issue:<n>. The ledger is per-workspace, so it coordinates the sessions sharing one checkout, not two clones on two machines.

Examples

Run three independent tasks in parallel in the shared tree. Claim-on-first-write keeps them off each other's files:

stella fleet \
  "Add unit tests for the parser module" \
  "Document every public function in src/api/" \
  "Fix the clippy warnings in the store crate"

Run a dependency-ordered plan and cap concurrency at two:

stella fleet --plan .stella/plans/release-prep.toml --max-concurrency 2

Branch from a release branch and watch each branch's CI after the run:

stella fleet --plan release.toml --base-ref release/1.4 --watch

--watch (and its PR reconciliation) only does something once the fleet branches are pushed, for example when your task prompts push their branch and open a PR. It uses the GitHub gh CLI, which must be installed and signed in.