Fix a backlog in parallel

A list of independent fixes and one afternoon. Decompose it into a fleet plan, run the workers concurrently under file claims, and review what actually landed.

You have a list. Eleven clippy warnings across four crates, a dozen modules with no test coverage, every TODO(deprecate) left over from a migration. Each item is small, none of them depend on each other, and doing them one at a time would eat your whole afternoon watching a progress bar.

This is what stella fleet is for: many tasks at once, each one handled by a full stella worker, coordinated so they don't overwrite each other.

The whole thing, first

stella --spend-limit 6 fleet \
  "Fix the clippy warnings in stella-store" \
  "Fix the clippy warnings in stella-graph" \
  "Add unit tests for the plan parser in stella-fleet"

Three workers run together in your repository root, committing to your current branch as each one finishes. That's all a short list needs.

Workers commit to the branch you're on. Start from a branch you're willing to have things committed to — not main — and start from a clean working tree, so git log afterward clearly separates their work from yours.

Check for shared files first

Everything else follows from one question, and it's worth thirty seconds before you type anything.

Disjoint files → inline prompts

Each item touches its own crate, module, or folder. Nothing to declare. Pass the prompts on the command line and let claim-on-first-write handle the rest.

Overlapping files → a plan with claims

Two items might both edit src/store/schema.rs. Declare the paths up front, so the second attempt fails by name instead of racing the first. This is the common case for anything over about five items.

Ordered work → depends_on

Item B only makes sense after item A lands — a database migration before the code that reads from it. Dependencies control the order tasks run in. They are not just a speed hint.

Competing answers → isolation

Two attempts at the same file, where you want to compare them. This is the one case for isolation = "isolated"; see when to isolate.

Turning a backlog into a plan

For anything beyond a handful of prompts, write the list down. A plan file is a .toml (or .json) file with one [[tasks]] entry per item:

cleanup.toml
[[tasks]]
id = "store-clippy"
title = "Clippy: stella-store"
prompt = "Fix every clippy warning in stella-store. Do not silence with #[allow]."
claims = ["crates/stella-store/src/lib.rs", "crates/stella-store/src/write.rs"]

[[tasks]]
id = "graph-clippy"
title = "Clippy: stella-graph"
prompt = "Fix every clippy warning in stella-graph. Do not silence with #[allow]."
claims = ["crates/stella-graph/src/lib.rs"]

[[tasks]]
id = "workspace-lints"
title = "Promote the shared lints"
prompt = "Move the now-common lint config into [workspace.lints] and drop the per-crate duplicates"
depends_on = ["store-clippy", "graph-clippy"]
claims = ["Cargo.toml"]
stella --spend-limit 6 fleet --plan cleanup.toml --max-concurrency 3

Two crates get cleaned up at the same time in the first wave. The workspace-wide tidy-up runs alone in the second wave, once both earlier tasks have actually succeeded. The full plan schema has every field.

Plans get checked before anything runs. A duplicate id, a task that depends on itself, an unknown dependency, or a cycle gets rejected up front, with the problem task named — so a typo costs you one message, not half a run.

Writing prompts that survive being run alone

The mistake specific to fleets is a prompt that reads fine in a list but turns out ambiguous by itself. Each worker sees only its own prompt — not the other tasks, not your intent, not the conversation you'd have had with a person.

Three habits that pay off:

  • Name the scope in the prompt itself. Write "Fix the clippy warnings in stella-store," not "fix the clippy warnings" — which invites a worker into the whole codebase and straight into another worker's claimed files.
  • Say what a fix should not do. "Do not silence with #[allow]," "do not delete the test." Left unsaid, the cheapest way to get to a green build is sometimes the wrong one.
  • Give it something it can check on its own. A prompt that names a test command lets the worker's verify stage finish based on evidence instead of an opinion. This is the single biggest lever on both cost and trust.

Claims are what makes it safe

By default, every worker runs in one shared folder — your repository root. That's a deliberate trade-off: the build cache stays warm and commits interleave on one branch, but two workers editing the same file would silently race each other.

claims removes that risk. A declared path is locked for the length of that task; a second task trying to claim a locked path fails right away, by name, in under a second, naming which task is holding it. Paths you didn't declare get claimed automatically on first write, so you're protected even for files you didn't think to list.

Shell commands count as writes too. A command line does not say which files it will write, so stella looks at the folder before the command runs and again after, and claims whatever changed. That covers a sed -i, a redirect, a formatter, or an install that rewrites a lock file. It never blocks the command itself — by then it has already run — so what you get is a warning for the next worker. One gap: a folder that is not a git repository has nothing to compare, so nothing is claimed there.

Claims: one shared tree, one lock tabletask aclaims error.rstask bclaims error.rsthe claim tableone shared tree · exact pathsrunsholds it for the attemptfails by nameunder a second, rival namedthe collision surfaces at dispatch, not at integration — and undeclared paths are claimed on first write too

Claims are matched as exact strings — they are not folder prefixes. Claiming src/store/ does not protect src/store/write.rs. Name each file directly, and spell it the same way everywhere in the plan.

The locks live in .stella/private/store.db, separate from the fleet's own records, so they also protect you across separate fleet runs — a second stella fleet running in another terminal can't step on the first one's files.

When to isolate instead

isolation = "isolated" gives a task its own git worktree, under .stella/worktrees/<slug>, on a fleet/<slug>-<hash> branch. Use it when the work is genuinely different attempts at the same thing — two takes on the same rewrite, where you want to see both and pick one — or when a task needs to change files outside the normal checkout.

It is not the safer default. Isolation means a cold build cache for every tree, and it pushes every merge conflict to later, when you combine the work. For a backlog of independent fixes, claims are cheaper, and they catch a collision an hour earlier.

What the spend limit actually does here

--spend-limit is a global flag, and in a fleet it gets divided, not shared: each worker is limited to spend limit ÷ max-concurrency. The fleet stops starting new waves once total spend crosses the cap.

In practice, this means concurrency and per-task budget trade off against each other. --spend-limit 6 --max-concurrency 3 gives each worker $2. Raising concurrency to 6 cuts that in half, and a task that needed $1.50 will now stop partway through. If tasks are dying early, lower the concurrency before raising the spend limit.

Reading the result

This is the part that separates a fleet from a script, and it's where the time you saved gets spent back if you skip it.

Nothing gets retried automatically. A failed task does not unblock the tasks that depend on it — they end the run marked skipped, a different outcome from "failed" that means the work was never even attempted.

Start with the ledger, .stella/private/fleet.db — a local SQLite file, like all stella telemetry:

# What failed in the most recent run, and what the worker said about it.
sqlite3 .stella/private/fleet.db \
  "SELECT a.task_id, a.branch, a.summary
     FROM attempts a
    WHERE a.run_id = (SELECT id FROM runs ORDER BY created_at_ms DESC LIMIT 1)
      AND a.success = 0;"

Then read the diff. Eleven workers each making a small commit on one branch is exactly the shape that lets a bad one hide:

git log --oneline main..HEAD
git diff main...HEAD --stat

Past runs also show up on the Fleet runs card of the Observatory dashboard, if you'd rather not write SQL.

Worktrees and fleet/* branches from isolated tasks are left in place on purpose — those branches are the finished work. git worktree list shows all of them. Nothing gets merged for you automatically.

When it doesn't go cleanly

A task failed and took its dependents with it

This is expected, and it's by design: a dependent task that ran anyway would be building on work that isn't actually there. Fix the cause, then run it again — either the whole plan (finished work is cheap to redo, and the claims still protect you) or a trimmed plan with just the failed branch of tasks.

A dispatch failed, naming another task

Your claims overlap. That's the system working as intended — it caught at launch time what would otherwise have corrupted a file. Either split the work so the two tasks own separate files, or add a depends_on link so they run in different waves.

Everything is fighting over one file

If three tasks all need Cargo.toml, they aren't really three tasks. Combine them into one prompt, or order them with depends_on. A fleet speeds up work that's already independent. It can't turn work that's inherently sequential into work that runs at the same time.

The workers went wider than you meant

This is almost always a prompt that didn't say enough. Re-read the item as if you knew nothing else about the codebase — that's exactly the worker's position. Naming the crate, folder, or file in the prompt fixes this more reliably than any flag does.

Following it through to CI

If your task prompts push branches and open pull requests, --watch keeps the fleet run alive afterward, watching each branch's CI until it finishes and checking pull request status through gh. It exits with an error if any watched branch ends up red.

stella --spend-limit 8 fleet --plan cleanup.toml --watch

It needs gh installed and signed in, and it can only watch a branch once it's actually pushed — there's nothing to watch on a branch that never left your machine.

Next