6. Team
fleetd delivers one turn to one session. This page describes the layer above
that delivery: how a lead splits work, starts members, and collects their work.
The delivery rules belong to Message Server.
The team shape
A primary lead orchestrates the work. It splits a job into units, starts members
for the units, sends each member its own brief, and decides the final result.
Only a primary can start, stop, or drain members. An architect may send work, but
a worker may not (Authz.java:49-70).
Leads may coordinate with peer leads. They do not assign tasks to each other.
A lead addresses a member by its sessionId, not by a role
(FleetMcp.java:1096-1108). fleet_list reports peer leads and members, including
each member's sessionId, role, and profile (FleetMcp.java:1185-1202).
The diagram shows the normal direction of work. A member reply goes back through
fleetd after the member finishes its turn.
flowchart TB
Lead["Lead"]
Fleet["fleetd"]
Peer["Peer lead"]
MemberA["Member A"]
MemberB["Member B"]
Lead -->|"spawn and send"| Fleet
Fleet -->|"deliver work"| MemberA
Fleet -->|"deliver work"| MemberB
MemberA -->|"reply"| Fleet
MemberB -->|"reply"| Fleet
Fleet -->|"return or hold reply"| Lead
Lead <-->|"coordinate"| Peer
The lead delegates downward to members. Peer leads only coordinate.
Addressing and roles
fleet_send has one required field: content. Its optional addressing fields
are sessionId, turnId, and coordId (FleetMcp.java:1096-1108).
| Use | Field | Meaning |
|---|---|---|
| Send work to a member | sessionId |
The member's herdr terminal ID. |
Answer a member's fleet_ask |
turnId |
Routes the answer into that member's same turn. |
| Coordinate with a peer lead on another daemon | coordId |
Routes a coordination message to that lead's mailbox. |
fleet_send also accepts wait and timeoutMs. wait defaults to true.
With wait: false, the call returns a ticket. The lead checks that ticket with
fleet_poll (FleetMcp.java:1088-1108, FleetMcp.java:1125-1134).
Do not use role or prompt with fleet_send. They are not fields in its
schema (FleetMcp.java:1096-1108).
There are two member authorization roles: worker and architect. A worker can
reply, ask, and read. An architect can also send work. Only a primary can spawn,
stop, or drain members (Authz.java:44-71).
The fleet_spawn field named role is separate from that authorization table.
It selects a member contract: dev, reviewer, or architect. profile selects
the backend. The two fields are independent, so a reviewer can use the same
profile as the developer it reviews (FleetMcp.java:1148-1173).
Claude Code and OpenCode members
A profile chooses one launcher kind. The supported kinds are claude-code, which
is the default, and opencode (FleetConfig.java:265-270). Both use the shared
member transport for pane placement, readiness, teardown, listing, and working
directory handling (OpenCodeLauncher.java:30-35).
The launchers differ in important ways:
| Kind | Launch and configuration | MCP servers |
|---|---|---|
claude-code |
Uses command-line flags. The model and other profile arguments come from its launch arguments. | When configured, its inline --mcp-config includes the fleet server. It also includes intellij when the IDE MCP URL is configured. |
opencode |
Writes an ephemeral opencode.json, points OPENCODE_CONFIG to it, and passes the provider/model selector with -m. It reads provider credentials from its global auth.json. |
When configured, its file config includes the remote fleet server. It also includes the remote intellij server when the IDE MCP URL is configured. |
Claude Code mounts MCP through --mcp-config (ClaudeCodeLauncher.java:283-356).
OpenCode mounts MCP through its generated file and OPENCODE_CONFIG
(OpenCodeLauncher.java:39-49, OpenCodeLauncher.java:340-367). The fleet and
intellij servers are conditional on profile configuration in both launchers.
The subscription guard applies to Claude Code profiles. OpenCode has no
ANTHROPIC_BASE_URL injection and no SubscriptionGuard (OpenCodeLauncher.java:37-47).
The guard's allowed off-subscription hosts come from configuration. The default
list is empty, so this page names no backend host (FleetConfig.java:1149-1163).
Start members, then send work
For independent units, the lead starts all needed members before it sends briefs. It then sends all briefs without waiting for each one. This avoids turning independent work into serial work.
1. fleet_spawn for each unit
2. fleet_send with each returned sessionId and wait: false
3. fleet_poll each returned ticket
4. fleet_ack each reply after the lead processes it
5. review the change, then merge if the lead accepts it
Example calls use sessionId and content, not a role address:
fleet_spawn {
role: "dev",
profile: "chosen-profile",
worktree: true,
ticket: "task-123"
}
fleet_send {
sessionId: "returned-session-id",
content: "Implement the assigned unit.",
wait: false
}
fleet_poll { ticket: "returned-ticket" }
fleet_ack { target: "returned-session-id", msgId: "returned-message-id" }
fleet_spawn returns a sessionId for fleet_send and a paneId for
fleet_stop (FleetMcp.java:1156-1173). fleet_ack needs the member session ID
and the reply message ID (FleetMcp.java:1137-1145).
Profile placement and capacity
The lead may name a profile in fleet_spawn. That bypasses automatic placement,
but maxLoad still applies. If that named profile is at its cap, the spawn fails;
it does not silently move to another profile (CompositePeerLauncher.java:260-273,
CompositePeerLauncher.java:326-397).
When a spawn omits profile, fleetd builds candidates from the requested
contract's profile pool. An absent or empty pool uses all configured profiles
(CompositePeerLauncher.java:276-286, CompositePeerLauncher.java:401-431).
Each profile can set:
maxLoad, the maximum number of live members. An absent value means unlimited. A value of0allows no live members (FleetConfig.java:251-264).weight, the relative value for automatic placement. A value of zero or less excludes the profile from automatic placement, but an explicit profile can still select it if it has capacity (FleetConfig.java:242-260).
Available candidates exclude weight-disabled, unreachable, quarantined, and
at-cap profiles (PlacementPolicyUtil.java:14-36). The configured placement policy
then chooses from those candidates. The default is fixed; round-robin and
weighted are also supported (PlacementPolicies.java:12-40).
The weighted policy uses smooth weighted round-robin. It adds weights, picks
the highest score, then subtracts the total weight from that winner. Over time,
the selection follows weight ratios. It is not a cheapest-first policy
(WeightedRoundRobinPolicy.java:7-48).
Limits of this page
This page does not describe the exact worker turn lifecycle or the transport
between fleetd and herdr. See Message Server for delivery
details.
📖 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