fleet is a lightweight Python supervisor that claims tasks from a
centralized beads queue and runs
them in parallel through a coder (claude, agy, or codex CLI) in a headless loop. Each task
remembers the project working directory it was created in, plus an optional
per-task coder/model override, so a single supervisor can drive work across
many projects — and across multiple agent backends — spawning many concurrent agents - from one machine.
- Installation
- Quick start
- How it works (centralized model)
- First-run setup
- Command reference
- Configuration reference
- Adding a custom coder
- Q&A protocol — for the human
Install fleet as a global tool so it is on $PATH from any directory:
git clone https://github.com/sermakarevich/fleet.git
uv tool install --editable ./fleet
uv tool update-shell # if ~/.local/bin is not on PATH yetThen fleet --help should work from anywhere. Use uv tool upgrade fleet
later to pick up new dependencies; code edits are live because the install is
editable.
Requires:
- Python ≥ 3.11
uvon yourPATH- beads (
bd) on yourPATH giton yourPATH(beads stores its database inside a git repo)- At least one coder CLI on your
PATH:claude(Claude Code),agy, orcodex(OpenAI Codex CLI)
fleet init # initialize ~/.fleet (beads DB + default config)
fleet config set max_concurrent=3 # cap how many agents run in parallel
cd /path/to/your/project # any project you want the agent to work in
# Title + description:
fleet bd create --title "add codex coder" \
--description "wire the OpenAI codex CLI into fleet"
# Pin coder/model for this task only:
fleet bd create --coder agy --model "GPT-OSS 120B" \
--title "insert task.png from assets into README.md" \
--description "promote the screenshot to the Quick start section"
# Positional-title shortcut (cwd is captured automatically):
fleet bd create "context for other coders"
fleet run & # start the supervisor in the background
fleet tasks # render a live table of in-progress tasksSee First-run setup and the Command reference for the full story (per-task coder/model overrides, Q&A protocol, log locations, …).
There is one fleet home directory on your machine — ~/.fleet by default,
override with $FLEET_HOME if you like.
~/.fleet/
├── .beads/ # the centralized bd queue (single Dolt DB)
├── runtime.toml # supervisor config
├── logging/ # supervisor logs (fleet-<date>.jsonl)
└── tasks/<task_id>/
├── task.json # per-task metadata: cwd, coder, model
├── log.jsonl # per-task supervisor log
├── log.stderr # raw subprocess stderr
├── events.jsonl # per-task structured events (agent reads on resume)
├── .failures # failure counter (drives retries)
└── artifacts/
├── PLAN_AND_STATUS.md # agent-owned plan + progress
├── KNOWLEDGE.md # agent-owned persistent notes
└── Q&A.md # agent ↔ human Q&A thread (when blocked)
Each task records the project working directory the agent should run in,
plus the optional coder/model override, inside
$FLEET_HOME/tasks/<task_id>/task.json
({"cwd": "/abs/path", "coder": "claude", "model": "sonnet"}).
The supervisor — which can be started from anywhere — claims tasks from
the central queue and runs each agent subprocess in that cwd. All per-task
artifacts and logs live under $FLEET_HOME/tasks/<task_id>/, so they're
preserved across project moves and shared between coders. If no task.json
exists for a task, the supervisor falls back to running the agent in
$FLEET_HOME itself.
Create tasks with the fleet bd passthrough and write task.json next to
the new task ID (see "Create your first task" below).
fleet init
# → Fleet home initialized at /Users/you/.fleetThis runs bd init inside $FLEET_HOME and writes a default
runtime.toml. Idempotent — safe to re-run.
Run fleet bd create from inside the project you want the agent to work
in — your shell's cwd is captured automatically and stored alongside the
new task:
cd /path/to/your/project
fleet bd create --title "Implement feature X"
# → Created fleet-abc: Implement feature X [cwd: /path/to/your/project]
# Pin coder/model for this task only (overrides config defaults):
fleet bd create --coder agy --model opus --title "Heavy refactor"
# → Created fleet-def: Heavy refactor [cwd: /…, coder: agy, model: opus]fleet bd … forwards verbatim to the bd CLI inside $FLEET_HOME, so any
flag bd create accepts (--description, --priority, dependencies via
bd dep add …, …) works the same way. For create specifically, fleet
also writes $FLEET_HOME/tasks/<id>/task.json with {"cwd": "<your cwd>"}
so the supervisor knows where to spawn the agent. Pass --json to get the
raw bd envelope back instead of the human-friendly summary line.
--coder and --model are intercepted by fleet (not forwarded to bd):
they're validated against the registered coders (claude, agy, codex)
and persisted as per-task overrides in task.json, applied next time the
supervisor claims the task. Always pass both together when overriding —
or omit both to inherit the config defaults.
By default the supervisor uses config.coder (default claude) and
config.model (default sonnet) for every task. To pin a single task to a
different coder/model — e.g. route a heavy refactor to agy while leaving
everyday tasks on claude — pass --coder and --model together at
create time (always specify both so the override is unambiguous):
fleet bd create --coder agy --model opus --title "Heavy refactor"
fleet bd create --coder codex --model o3 --title "OpenAI task on o3"
fleet bd create --coder claude --model opus --title "Tricky task on Opus"The override is persisted in $FLEET_HOME/tasks/<task_id>/task.json and is
applied the first time the supervisor claims the task. Resolution order is
task.coder → --coder CLI flag on fleet run → config.coder
(similarly for model). Confirm what the supervisor will pick with
fleet show <task_id> — explicit overrides are bare, while inherited
values are tagged (default). To change an override after creation,
edit $FLEET_HOME/tasks/<task_id>/task.json directly.
fleet run # uses config.coder / config.model
fleet run --coder claude # override default coder for this processThe supervisor reads from $FLEET_HOME/.beads, claims ready tasks, and spawns
each agent subprocess in that task's working directory with the
per-task (or default) coder/model resolved as described above.
fleet init
fleet init --force # re-run bd init even if .beads already existsCreates $FLEET_HOME (default ~/.fleet) with a beads DB, default
runtime.toml, and an empty tasks/ directory.
fleet ready
fleet ready --limit 10Lists ready tasks. Each line shows the task ID, title, and recorded cwd.
fleet show fleet-abc
fleet show fleet-abc --json # raw bd show JSON envelopePrints id, title, status, cwd, effective coder, effective model, and
description. The coder: and model: lines are tagged (default) when
they come from runtime.toml rather than a per-task override.
fleet tasks
fleet tasks --limit 20Renders a rich table of currently in-progress tasks with: ID, started
time, elapsed, idle, peak context-window usage, event count, coder,
model, title, and cwd. Per-task overrides are bolded; values inherited
from runtime.toml are dim. See the screenshot in Quick start.
fleet task fleet-abc log # → $FLEET_HOME/tasks/fleet-abc/log.jsonl
fleet task fleet-abc plan # → artifacts/PLAN_AND_STATUS.md
fleet task fleet-abc knowledge # → artifacts/KNOWLEDGE.mdPrints the named artifact for one task. fleet task --help additionally
lists currently running tasks with their effective [coder/model], so
you can scan valid IDs without leaving the help screen.
fleet log # whole most-recent supervisor log file
fleet log 200 # tail the last 200 linesPrints the most recently modified fleet-<date>.jsonl from
$FLEET_HOME/logging/. N must be a positive integer when supplied.
Forwards arguments verbatim to the bd CLI, executed inside $FLEET_HOME.
This is the recommended way to drive the centralized beads queue from any
directory.
fleet bd create --title "Implement feature X" --json # → {"data": {"id": "fleet-abc", …}}
fleet bd create --title "Refactor parser" \
--description "Extract tokenizer to its own file"
fleet bd dep add fleet-newtask fleet-abc # add dependencies
fleet bd list # list every task in the central DB
fleet bd list --status=blocked # filter by status
fleet bd comment fleet-abc "note" # comment on a task
fleet bd dolt push # push the beads data to your git remote
fleet bd prime # show beads workflow help
fleet bd --help # bd's own --help (not fleet's)The exit code of bd is propagated. All flags are passed through unmodified,
so fleet bd behaves exactly like running bd from inside $FLEET_HOME.
fleet bd create is special-cased: it captures your shell's invocation
cwd and writes it into $FLEET_HOME/tasks/<task_id>/task.json so the
supervisor knows where to spawn the agent. Without --json you get a
human-friendly summary (Created <id>: <title> [cwd: <path>]); with
--json you get the raw bd envelope as before. Pass --dry-run to skip
the task.json write (useful if you're driving bd test runs).
--coder <name> and --model <name> are also intercepted on create
(and new) — they're stripped from the args before forwarding to bd,
validated, and persisted as per-task overrides in task.json. Unknown
coder names fail fast without invoking bd. The summary line reflects
any overrides applied: Created <id>: <title> [cwd: <path>, coder: agy, model: opus].
fleet run
fleet run --coder claude # override the default coder for this run| Option | Description |
|---|---|
--coder |
Optional override for the default coder this process uses. Falls back to config.coder (default claude). Per-task overrides set on fleet bd create still win. Registered values: claude, agy, codex. |
fleet config show
fleet config show --raw # raw TOML bytes
fleet config set max_concurrent=5The supervisor re-reads $FLEET_HOME/runtime.toml on change and applies updates without restart.
Configurable keys live in $FLEET_HOME/runtime.toml. Edit via fleet config set … or
directly in the file.
| Key | Default | Description |
|---|---|---|
max_concurrent |
3 |
Maximum number of agent subprocesses running at once. |
coder |
claude |
Default coder used when neither the task nor fleet run --coder specifies one. Registered values: claude, agy, codex. |
model |
sonnet |
Default model used when the task does not specify one. Interpreted by the active coder (e.g. claude understands sonnet / opus / haiku; the agy coder ignores it because the agy CLI reads its model from its own settings file; codex passes it as --model, defaulting to o4-mini). |
context_pressure_threshold_pct |
90 |
Terminate an agent session when prompt-side context usage exceeds this percentage of the coder's context limit. Supported by all built-in coders (limits: claude 200K tokens, agy 128K, codex 128K). |
When an agent is blocked by ambiguity, it will:
- Append a
## Q:block to the task'sQ&A.mdin the artifact directory. - Run
bd update <task_id> --status blocked --notes "QUESTION: <summary>". - Exit cleanly.
To answer and resume the task:
-
Find the task:
fleet ready # not listed if blocked bd list --status=blocked # from inside $FLEET_HOME fleet show <task_id> # see the question summary
-
Read the Q&A file from the task's artifact directory:
cat $FLEET_HOME/tasks/<task_id>/artifacts/Q\&A.md
-
Append your answer directly below the
## Q:block:## A: <YYYY-MM-DD HH:MM> <your answer here>
-
Unblock the task:
fleet bd update <task_id> --status open --assignee ""
The supervisor will re-claim the task on the next scheduling cycle. The agent
reads Q&A.md on startup (per the resume protocol inlined from
INSTRUCTION.md) and continues from where it stopped.
Fleet ships with three built-in coders (claude, agy, codex), but you can
wrap any CLI agent in four small steps.
Create a file in src/fleet/coders/, e.g. src/fleet/coders/mycoder.py:
import json
from datetime import datetime, timezone
from pathlib import Path
from fleet.coders.base import Coder
from fleet.schemas import Event, Task
_TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
_INSTRUCTION_PATH = _TEMPLATES_DIR / "INSTRUCTION.md"
_HEADER_PATH = _TEMPLATES_DIR / "coder_header.md.tmpl"
class MyCoder(Coder):
name = "mycoder" # unique name used in fleet bd create --coder
context_limit = 128_000 # used to compute context-pressure threshold
def __init__(self, model: str = "my-default-model") -> None:
self.model = model
def build_argv(self, task: Task, task_dir: Path) -> list[str]:
"""Return the argv list passed to asyncio.create_subprocess_exec()."""
artifacts_dir = task_dir / "artifacts"
instructions = _INSTRUCTION_PATH.read_text(encoding="utf-8").strip()
invocation_line = f"Invocation directory: {task.cwd}" if task.cwd else ""
header = _HEADER_PATH.read_text(encoding="utf-8").format(
task_id=task.id,
task_title=task.title,
task_description=task.description or "",
task_dir=task_dir,
artifacts_dir=artifacts_dir,
invocation_line=invocation_line,
).strip()
prompt = f"{header}\n\n---\n\n{instructions}"
return ["mycli", "--model", self.model, "--json", prompt]
def env(self, task: Task, task_dir: Path) -> dict[str, str]:
"""Return env-var overlay merged on top of os.environ before spawn.
These three keys are REQUIRED — the agent reads them to locate its
artifact directory and write PLAN_AND_STATUS.md / KNOWLEDGE.md.
"""
return {
"FLEET_TASK_ID": task.id,
"FLEET_TASK_DIR": str(task_dir),
"FLEET_ARTIFACT_DIR": str(task_dir / "artifacts"),
}
def normalize_event(self, raw_line: str) -> Event | None:
"""Parse one stdout line from the subprocess into a normalized Event.
Return None for any line you want to discard. Must be pure — no I/O.
"""
if not raw_line.strip():
return None
try:
data = json.loads(raw_line)
except (json.JSONDecodeError, ValueError):
return None
ts = datetime.now(tz=timezone.utc)
kind = data.get("type", "")
if kind == "started":
return Event(kind="session_started", raw=data, ts=ts)
if kind == "finished":
return Event(kind="session_ended", raw=data, ts=ts, usage=data.get("usage"))
return NoneContracts to honour:
build_argv— the last positional element is almost always the full prompt; construct it from the shared templates so the agent receives the Fleet task protocol and artifact-directory instructions.env— always emitFLEET_TASK_ID,FLEET_TASK_DIR,FLEET_ARTIFACT_DIR; never putANTHROPIC_API_KEYhere (the CLI owns that).normalize_event— returnNonefor anything you don't understand; the runner skipsNoneevents safely. Must be pure (no I/O, no logging).
Add one line to src/fleet/coders/__init__.py:
from fleet.coders.mycoder import MyCoder # add this import
_REGISTRY: dict[str, type[Coder]] = {
"claude": ClaudeCoder,
"agy": AgyCoder,
"codex": CodexCoder,
"mycoder": MyCoder, # add this entry
}# set as the default for all tasks
fleet config set coder=mycoder
# or pin it to individual tasks at creation time
fleet bd create --coder mycoder --model my-model --title "Task for my coder"That's it — the supervisor discovers the coder through _REGISTRY, so no
further configuration is needed.

