Documentation

Everything needed to run a fleet on your own machine. Every command below is copied from the project README, unchanged.

Python 3.14 redis-server cmux ≥ 0.64.25

Quick start

Requirements

fleetmux drives real CLIs and a private Redis, so the host has to provide them.

What the host has to provide.
WhatMinimum 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.

terminal
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
# ~/.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.

terminal
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 status shows the task, each agent's state, context usage and inbox depth.
  • fleetmux stop cancels 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.

service mode
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, not proto).
  • --roles DIR: DIR/<agent>.md is appended to that agent's AGENTS.md as "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, optional reply.allowed, optional to (routes straight to that worker, no supervisor turn), optional instruction, optional timeout_s. The answer must be one JSON object; a bad answer gets one correction; a second bad answer is published as invalid. Result statuses: ok, invalid, skipped, timeout, stalled.
  • pause stops 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.

roster.yaml
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 (and latest.json if 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 as roster.changed.
  • Handoff threshold per model: optional handoff: table and per-worker window / 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.

terminal
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": false is published as timeout.
  • --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 is blocked / the request is timeout, naming the item.
  • A finished task (any terminal status) or an explicit stop writes <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
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.

The three backends.
BackendDefault onLayoutNeeds
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.
wezterm
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 backend
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 caffeinate when available so the machine does not sleep.
  • A new cmux workspace briefly steals focus when it is created (cmux ignores --focus false for new-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.