Fleet Manager
fleet-manager gives one host a status view of several fleetd fleets. A fleet entry has a name, a REST address, and an optional project path. The manager is a separate Python package. It uses only REST requests and does not import or run inside fleetd (README.md:3-12, fleet_manager/probe.py:10-13, pyproject.toml:1-9).
This is not multi-host federation. Entries marked remote are not probed. The manager reports them as remote instead (fleet_manager/probe.py:117-121, tests/test_probe.py:31-34).
Install and run
The package needs Python 3.11 or later. Its runtime dependencies are Python standard-library modules only (pyproject.toml:3-6).
For development, create a virtual environment and install the package with its test and lint tools:
python -m venv .venv && .venv/bin/pip install -e '.[dev]'
This command is the documented development setup (README.md:54-59).
The package registers the fleet-status command (pyproject.toml:8-9). It also runs as a Python module:
fleet-status
python -m fleet_manager.cli
python -m fleet_manager.cli path/to/fleets.json
The module command accepts at most the first command-line argument as the registry path. Without an argument, it reads fleets.json beside the package source, not the current directory (fleet_manager/cli.py:54-57). If that file does not exist, it prints setup help to standard error and exits with status 2 (fleet_manager/cli.py:58-62).
fleets.json registry
The registry is JSON with a required top-level fleets array. The command reads reg["fleets"], so a missing fleets key stops the command with a key lookup error (fleet_manager/cli.py:49-51, fleet_manager/cli.py:63-65).
Each array item is one fleet. The following example uses only the sample values:
{
"fleets": [
{
"name": "fleetd",
"rest": "http://127.0.0.1:8765",
"project": "/path/to/the/project/this/fleet/works/on"
},
{
"name": "other-host-fleet",
"remote": true,
"project": "/path/on/that/host",
"note": "REST is loopback-only, not reachable from here yet"
}
]
}
The sample shape is from fleets.example.json:1-16.
| Field | Required | Meaning and missing-field result |
|---|---|---|
name |
Yes | The displayed fleet name. A missing value stops probing that entry with a key lookup error (fleet_manager/probe.py:114-115). |
rest |
Yes for a local fleet | The base REST URL. A missing value stops a local probe with a key lookup error. A remote entry does not read it (fleet_manager/probe.py:114-115, fleet_manager/probe.py:117-124). |
project |
No | An optional project path shown in the status output. When absent, no project line is shown (fleet_manager/probe.py:114-115, fleet_manager/cli.py:32-33). |
remote |
No | When true, the manager does not call the fleet and reports remote. When absent or false, it treats the entry as local (fleet_manager/probe.py:117-123). |
note |
No | An optional message for a remote fleet. If absent, the manager uses on another host; REST is loopback-only, not probed from here (fleet_manager/probe.py:117-121). |
fleet-status
fleet-status is the installed status command. It loads every entry in fleets, probes each one, and prints the result (pyproject.toml:8-9, fleet_manager/cli.py:63-66).
The output has a time-stamped FLEET STATUS heading. For each fleet, it shows the name, status, REST URL, optional project and note, a verdict when the fleet is reachable, and each returned member's role, profile, state, live status, branch, and progress result (fleet_manager/cli.py:27-46).
python -m fleet_manager.cli
This module command runs the same main function as fleet-status (fleet_manager/cli.py:54-70, pyproject.toml:8-9). It uses the package-side default registry with no path, or the first supplied path with one (fleet_manager/cli.py:54-57).
How a probe works
For a local fleet, the manager requests /healthz first. If the request fails, it reports down, adds an /healthz unreachable note, and does not request members (fleet_manager/probe.py:123-129, tests/test_probe.py:37-42). A successful health response supplies the displayed status and optional herdr version (fleet_manager/probe.py:130-131).
It then requests /members. It reads the workers array when present. Each worker can supply profile, role, state, liveStatus, worktree, and branch (fleet_manager/probe.py:133-145).
The manager does not trust a busy liveStatus by itself. For members whose live status is working or busy, it records the newest modification time in the worktree, waits four seconds by default, then records it again (fleet_manager/probe.py:36-55, fleet_manager/probe.py:81, fleet_manager/probe.py:136-150). A later time means working. No change means stalled, with the newest file age. An unavailable worktree gives unknown, not stalled (fleet_manager/probe.py:64-78, tests/test_probe.py:86-92).
A fleet is WORKING when any watched member changed files. It is STALLED when a watched member did not change files. It is busy (no worktree progress signal) when a busy member has no usable worktree. A fleet with no members is idle (no members); otherwise it can be idle (fleet_manager/probe.py:156-166, tests/test_probe.py:45-57).
Limits of the probe
The manager handles several fleet entries on one host. It does not probe a remote entry, so it is not a cross-host status or federation tool (README.md:3-7, fleet_manager/probe.py:117-121, tests/test_probe.py:31-34).
The probe cannot prove that a busy member is making useful progress. It only compares worktree modification times. A member can be busy without a usable worktree, and the result is unknown rather than a progress decision (fleet_manager/probe.py:64-78, fleet_manager/probe.py:143-150, tests/test_probe.py:86-92).
Two limits that come from fleetd, not from this tool
Neither limit is visible in the fleet-manager source, because neither belongs to it. Both were
measured against running daemons, and both shape how the manager can be used.
Two fleetd daemons must not share one herdr session. When they do, each daemon's reaper
treats the other's panes as orphans and tears them down, so the two fleets kill each other's
members. Give each daemon its own herdr session.
The manager must not run inside a herdr pane. fleetd resolves a caller's role from the
connection, and it identifies its lead by a tab label. A Claude session sitting in a pane that some
other daemon does not name as its lead is resolved as a worker by that daemon, and every
orchestration call is then refused. A process outside any pane is resolved as the primary, which is
why the manager sits outside and speaks REST.
Together these are the reason the manager is a separate REST client rather than a second daemon or an in-pane session.
What this page could not confirm
The fleet-manager source alone does not describe either limit above, and it was the only source
read for the rest of this page. Issues #160 and
#156 carry the measurements behind them.
📖 fleet
Home — overview & the decision
Chapters
- Architecture — system · 2 invariants · 2 modes
- Message Server — the
fleetddesign - Approaches — transports compared, why herdr
- Setup — ⚫ superseded by 13
- Operations — ⚫ superseded by 13
- Team — orchestrating a mixed fleet
- Use Cases — the review scenario + mechanisms
- Roadmap — delivery record: what is live, what is off, what was dropped
- Implementation — as-built code map · classes · flows · state machines
- Cross-Host Messaging — broker topology · exchanges · queues per entity
- Features — what it can do · the knob that turns it on · why · the gotcha
- Claude → OpenCode — porting a workspace to a second host
- User Guide — 🟢 install · configure · run · delegate · the traps
- Fleet Manager — many fleets on one host, over REST
- REST API Reference — all 14 routes, roles, and bodies
- Security & Trust Boundary — the guard · authz · what a member inherits
Design proposals (not built)
- CB-548 Lead Quorum — a deterministic decision procedure around a lead's judgment
🟢 herdr-centric fleetd · AgentAPI = research, never built