Documentation
Everything needed to run a fleet on your own machine. Every command below is copied from the project README, unchanged.
Quick start
Requirements
fleetmux drives real CLIs and a private Redis, so the host has to provide them.
| What | Minimum or note |
|---|---|
| macOS with cmux | cmux ≥ 0.64.25 (new-surface --command). Run start inside a cmux terminal, or set cmux's automation → socket control mode away from cmuxOnly; by default cmux only accepts commands from processes it started. |
| Codex CLI (for Codex agents) | ≥ 0.156 (per-file profiles, check_for_update_on_startup). The daemon parses Codex's rollout .jsonl files (token_count, task_complete, subagent metadata); if a Codex release changes that format, pin the version that works for you. |
redis-server on PATH |
brew install redis. Each run starts its own private instance under the checkout's .runtime/redis/<token>/ (socket, PID, log and data); TCP is disabled and it never touches localhost:6379. The binary is still an external dependency, not bundled. |
Python 3.14 + requirements.txt |
3.14 is the version the test suite runs on; deps.yaml lists every external program and its minimum version. |
| A model Codex can talk to | OpenAI API, ChatGPT login, or a local OpenAI-compatible server. The daemon reads the context window from the rollout, so no window size has to be configured. |
Agents run in an isolated Codex home (--codex-home, default ~/.fleetmux/codex-home): auth.json and the profile file are symlinked from your ~/.codex, so the login is shared, but your hooks, MCP servers, plugins and skills do not run inside agents (skills.include_instructions=false). Per-agent "trust this folder" entries are written there automatically.
Install
One command: fleetmux <subcommand> (e.g. fleetmux start, fleetmux status, fleetmux channel). The binary runs its own terminal subcommands (server, attach, reap, web, gui --runtime D) and hands every fleet subcommand to the Python CLI of the same release. Setups made under the project's earlier name keep working: its command, environment variable and release directory names stay accepted as aliases.
git clone https://github.com/justindbca/fleetmux && cd fleetmux
pip install -e .
Add a Codex profile for the agents. Codex ≥ 0.156 keeps each profile in its own file (see codex.config.example.toml):
# ~/.codex/fleet.config.toml (selected with `codex --profile fleet`)
model = "gpt-5.5"
Nothing else: the fleet creates its own Codex home for the agents and answers Codex's "trust this folder?" prompt for each agent directory itself.
Source, releases and issues live on GitHub: github.com/justindbca/fleetmux.
Start a fleet
Open a cmux terminal, configure your Pi provider/model and replace local/my-model below. New starts require Redis v2 and an explicit development opt-in. Without --supervisor-cli / --worker-cli, start takes the default pair from fleetmux/config/fleet.yaml#cli_pairs: a Pi supervisor on openai-codex/gpt-6-astra (thinking high) with official Claude Code workers. That table lists every supported pair and its status; a pair it does not list is refused. start also requires --lifecycle <mode> (service, dev_task, program, scheduled, review_loop, smoke); an unverified pair starts only with --lifecycle smoke.
fleetmux start --workers 2 --supervisor-cli pi --worker-cli pi --lifecycle <mode> \
--pi-model local/my-model --allow-unsandboxed-pi --allow-development-v2
echo '{"goal": "Survey the three most-cited papers on X and write a comparison",
"hours": 4,
"artifacts": [{"path": "worker-1/report.md", "min_chars": 800, "contains": ["Conclusion"]}]}' \
| fleetmux task - # hand the goal to the supervisor
fleetmux status # task + per-agent state, context usage, inbox depth
fleetmux stop # cancel; confirm runtime retirement before Redis shutdown
fleetmux task -hands the goal to the supervisor.fleetmux statusshows the task, each agent's state, context usage and inbox depth.fleetmux stopcancels the run and confirms runtime retirement before Redis is shut down.
The goal JSON declares the artifacts the daemon verifies: path, min_chars and the contains strings. A task ends only when every declared artifact exists, is long enough and contains the required text; three false COMPLETED claims block the task.
Two run shapes: task and service
Every start names a --lifecycle; it decides what "done" means and how the run closes (fleetmux/config/fleet.yaml#lifecycle_modes). The values are service, dev_task, program, scheduled, review_loop and smoke. Two shapes cover the first day.
A task run
You hand one goal and the run ends. task puts the goal in the supervisor's inbox; the daemon pastes it into the supervisor tab; the supervisor replies with one JSON object (status, completion, reason, dispatch, artifacts); each dispatch goes to that worker's inbox and is delivered when the worker is idle; the worker's reply comes back tagged [worker-1 report]. COMPLETED is checked against the declared artifacts and failures bounce back as "Independent verification failed"; three bounces block the task; BLOCKED stops the task and leaves the reason in status for a human. Bad JSON three times in a row blocks the task.
A service run
--lifecycle service keeps the fleet running with no goal and no end: it answers a stream of requests. The supervisor only routes; each worker can have its own prompt; answers are JSON checked by the daemon, not by a model.
fleetmux start --lifecycle service --worker-names exit-1,reentry-1,rung-1 --roles ~/my-roles \
--supervisor-cli pi --worker-cli pi --pi-model local/my-model \
--allow-unsandboxed-pi --allow-development-v2 --max-busy 2
echo '{"id": "cu-1:r7", "to": "exit-1", "payload": {...},
"reply": {"required": ["verdict", "param", "evidence"], "allowed": ["verdict", "param", "evidence", "note"]}}' \
| fleetmux request -
fleetmux results --drain # one JSON line per finished request
fleetmux pause --reason "market closed"; fleetmux resume
~/my-roles is the README's example roles directory; point it at your own.
--worker-names: worker slots named by role (lowercase, no dots; dots mark generations). Each name ends with-<number>(proto-1, notproto).--roles DIR:DIR/<agent>.mdis appended to that agent'sAGENTS.mdas "Your role". One prompt per worker.--max-busy N: at most N workers work at once; the supervisor is not counted.- A request:
id(single use),payload(any JSON, ≤ 200 000 chars),reply.required, optionalreply.allowed, optionalto(routes straight to that worker, no supervisor turn), optionalinstruction, optionaltimeout_s. The answer must be one JSON object; a bad answer gets one correction; a second bad answer is published asinvalid. Result statuses:ok,invalid,skipped,timeout,stalled. pausestops delivery and rotation; requests keep queuing. Calendars (market hours, holidays) are the caller's business: pause and resume from outside.
Naming rules and adding workers without a restart are in Roster YAML.
Roster YAML (hot add / remove)
start --roster roster.yaml (instead of --workers / --worker-names) takes the workers from a file, and the daemon re-reads it while the fleet runs.
workers:
- name: exit-1 # same rules as --worker-names (ends with -<number>)
role: roles/exit.md # optional, relative to the yaml; overrides --roles DIR/exit-1.md
- name: reentry-1
- Add: a new name is written to
config.json(andlatest.jsonif it points at this run), then spawned with the READY prompt as a tab at the end of the worker column. The supervisor sees the new list appended to its next message ([Roster change] Workers you can dispatch to now: ...). - Remove: the worker takes no new work (dispatch to it is rejected), finishes what it already has, and when idle with an empty queue its screen goes to the history page and its tab is closed. Its workspace directory stays. Putting it back in the file before that cancels the removal.
- Role change for an existing worker applies at its next generation; it is not restarted.
- The supervisor edits it too: every supervisor prompt has an "Adding and retiring workers (the roster)" section naming this run's roster file. It adds a row
- {name: <role>-<n>, role: <path>}when more work is ready than free workers, deletes one when a worker has no more work, within the limits of its task brief. - Rejected as a whole (roster kept, reasons in the daemon tab as
roster.rejected): invalid YAML, unknown keys, bad or duplicate names, zero workers, a missing role file, or a name already used in this run. Changes are logged asroster.changed. - Handoff threshold per model: optional
handoff:table and per-workerwindow/handoff_ratio; edits apply at the next delivery check, no restart.
Needs PyYAML in the Python that runs the daemon.
Resume an interrupted run
fleetmux start --resume <run-dir> reattaches an unfinished Redis v2 run instead of creating one; --resume auto takes the run in ~/.fleetmux/latest.json and starts a new run when that one is finished (<run>/result.json exists) or missing. A live daemon makes it a no-op.
fleetmux start --resume <run-dir>
- Redis restarts from the run's own directory and socket; AOF brings back everything up to the last fsync (about 1 s). A Redis that is still up is reused.
- Official Claude workers whose tab still runs the CLI are reattached; every other agent (the task supervisor, Pi agents, closed tabs) starts as its next generation (
worker-1->worker-1.1) in place. - Task: verified artifacts and accepted reports are kept; dispatches that were queued or in flight are re-queued by the daemon, and the new supervisor gets a summary (goal, artifacts verified/missing, completed dispatches with report excerpts, re-queued dispatches). Service: accepted requests without a result are routed again; a request with
"replay": falseis published astimeout. --max-attempts N(default 2, stored in config, can be changed on resume): one dispatch / request runs at most N times in total; over the cap the task isblocked/ the request istimeout, naming the item.- A finished task (any terminal status) or an explicit
stopwrites<run>/result.json; such a run is never resumed. Without--resume, a restarted daemon still holds uncertain work and pauses (no replay).
Handoff window and ratio per model
The session is rotated when used >= ratio × window; --handoff-ratio defaults to 0.6, and each agent resolves window and ratio separately, first hit wins.
handoff:
claude:opus: {window: 1000000}
claude: {window: 200000, ratio: 0.6}
codex: {ratio: 0.6}
Terminal backend: three choices
fleet talks to the terminal through one pluggable backend. start --backend cmux|wezterm|fleetmux picks it, and the choice is written into the run's config.json: the daemon, the actors, start --resume and every sub-fleet follow that value, and a run started before this flag existed stays on cmux. Without the flag macOS is unchanged — cmux when cmux is installed, WezTerm on a Mac without cmux — and Linux (including WSL2) defaults to fleetmux since M3; --backend wezterm (or --backend cmux) still selects the other backends explicitly. An explicitly chosen backend that cannot run on the machine is refused before anything is provisioned, naming what is missing.
| Backend | Default on | Layout | Needs |
|---|---|---|---|
cmux |
macOS, when cmux is installed | Supervisor top-left, history browser below, workers as tabs on the right. | cmux ≥ 0.64.25 |
wezterm |
Linux / WSL2 when chosen explicitly; the default on a Mac without cmux | One window, tab 1 with the supervisor on the left and daemon split below it, one tab per worker; no browser surface and no tabs inside a split, so the history page is not embedded — fleet prints <run>/history.html in daemon and opens it with open / xdg-open when a desktop is available. |
wezterm and wezterm-mux-server ≥ 20240203 on PATH; each run starts its own private mux server, never your WezTerm. |
fleetmux |
Linux (including WSL2) since M3; on macOS only with --backend fleetmux |
fleet's own fixed-layout terminal written in Rust (rust/, MIT); one private fleetmux server owns every agent's PTY and screen, and you look at it with fleetmux attach — open and close it whenever you like, open several at once, run it over ssh; closing a viewer never stops an agent. |
fleetmux on PATH (or $FLEETMUX_BIN); fleetmux --version must print fleetmux <x.y.z> (protocol 1) with a version of at least deps.yaml's minimum. |
fleetmux start --backend wezterm --workers 2 --supervisor-cli pi --worker-cli pi \
--lifecycle <mode> --pi-model local/my-model --allow-unsandboxed-pi --allow-development-v2
fleetmux start --backend fleetmux --workers 2 ... # prints: Watch it with: fleetmux attach --runtime <dir>
fleetmux start --backend fleetmux --attach ... # same, then becomes that `fleetmux attach`
fleetmux attach --runtime <dir> [--no-mouse] # from any terminal, as many times as you like
Build the fleetmux backend from this checkout (cd rust && cargo install --path crates/app), or run scripts/install_fleetmux.sh [--dest DIR] to install the prebuilt binary pinned by deps.yaml.
Known limitations
- On macOS the default backend is cmux (WezTerm without cmux); on Linux it is fleetmux's own terminal. On macOS the daemon uses
caffeinatewhen available so the machine does not sleep. - A new cmux workspace briefly steals focus when it is created (cmux ignores
--focus falsefornew-workspace); the daemon switches you back. - Codex keeps no streaming log of a tab; when a tab is retired its final screen is saved next to the handoff note. The full transcript is Codex's rollout file under
~/.codex/sessions. - Models occasionally do more than one dispatched step in a turn. The artifact check catches the outcome, not the overreach.