Table of Contents
- 7. Use Cases
- Flagship — spawning a reviewer and having a conversation
- Mechanism 1 — delegating work (fleet_send)
- Mechanism 2 — knowing your choices (fleet_profiles and fleet_list)
- Mechanism 3 — spawning a member (fleet_spawn and profiles:)
- Mechanism 4 — the member's own lifecycle limits
- Ideas that were never built
- Primary-side directive — when to delegate
- More use cases (catalogue)
- The portable CLAUDE.md block
- Related
7. Use Cases
This page shows what a lead actually does with fleet, using the tools fleetd really ships.
Every flow below uses real tool names and real parameters, taken from FleetMcp.java — the file
where every fleet_* tool is registered and its schema is defined.
For the full tool reference, see Message Server.
The whole page rests on two invariants: the lead never sets ANTHROPIC_BASE_URL (it stays on
its own subscription), and both the lead and every member talk to each other only through
fleetd's MCP tools. See Architecture for those invariants in full.
Flagship — spawning a reviewer and having a conversation
You are working in the lead session and want a second opinion on a diff — an actual back-and-forth, not a one-shot lint — from a member running on a different backend. The lead never leaves its own session; it only calls MCP tools.
sequenceDiagram
autonumber
participant L as Lead
participant F as fleetd
participant M as Member (reviewer)
L->>F: fleet_spawn(role="reviewer", profile="terra")
F-->>L: { sessionId, paneId, role: "reviewer", profile: "terra", status: "ready" }
L->>F: fleet_send(sessionId, content="Load the reviewer skill. Review PR #42 ...") — call blocks
F->>M: deliver content when the pane is idle
activate M
M->>M: reviews the diff
M->>F: fleet_reply(content="3 findings: ...")
deactivate M
F-->>L: tool result = the review
Note over L: reads the findings, wants to dig into one
L->>F: fleet_send(sessionId, content="why is finding 2 high severity?") — call blocks
F->>M: deliver — same pane, same warm context
activate M
M->>F: fleet_reply(content="because ...")
deactivate M
F-->>L: tool result = the answer
Note over L,M: same member session reused across turns —<br/>the diff stays in its context until the lead calls fleet_stop
Figure: spawn → delegate → structured reply → a follow-up on the same warm session. The shape
follows the real schemas of fleet_spawn and fleet_send (FleetMcp.java:1148-1174,
1088-1109) and of fleet_reply (FleetMcp.java:1213-1223).
sessionId is the value fleet_spawn returns, and it is what identifies "the same member" on
the follow-up call — there is no separate reuse mechanism.
The rest of this page walks through the pieces this flow is built from.
Mechanism 1 — delegating work (fleet_send)
The lead delegates with one tool call. fleet_send's schema (FleetMcp.java:1088-1109) has
these parameters:
fleet_send({
"sessionId": "term_a7", // the member to delegate to (from fleet_spawn / fleet_list)
"content": "Load the reviewer skill. Review PR #42 for correctness and security.",
"wait": true, // default true → blocks for the reply; false → returns a ticket
"timeoutMs": 25000 // optional; how long to block (default), capped at 120000
})
There is no to, kind, body, or block field. content is plain text — the lead writes
clear instructions into it, the same way a person would write a task in chat. There is no
structured envelope underneath it; see Ideas that were never built, below.
fleetd holds the call open until the member replies with fleet_reply, or (a fallback) its
turn ends without ever calling fleet_reply — in which case the tool result is the scraped
transcript tail instead, clearly marked as such. A short review turn can block; a long or
detached job passes wait:false and is collected later with fleet_poll. Both are covered in
full, with the exact outcome list, in Message Server.
To answer a member's fleet_ask question, or to message a peer lead, fleet_send takes
turnId or coordId instead of sessionId — also covered on that page.
Mechanism 2 — knowing your choices (fleet_profiles and fleet_list)
The old version of this page said fleet_list() returns a profiles array. That call does not
exist. The two tools answer different questions:
-
fleet_profiles(FleetMcp.java:874-895) answers "what backends can I spawn onto?" — the configured profiles, which one is the default, and which are currently quarantined after a usage-limit refusal:fleet_profiles() → { "profiles": ["terra", "sonnet", "local-llama"], "default": "sonnet", "quarantined": { // present only if something is actually quarantined "terra": { "credentialId": "terra-key", "quarantinedForSeconds": 900 } } } -
fleet_list(FleetMcp.java:920-973) answers "who is actually running right now?" — the live roster, split intoleads(peer orchestrators) andmembers(spawned sessions):fleet_list() → { "leads": [ { "sessionId": "term_p1", "name": "opus", "status": "idle", "self": true } ], "members": [ { "sessionId": "term_a7", "paneId": "w9:pW", "role": "reviewer", "profile": "terra", "state": "ready", "worktree": "/wt/cb-42", "branch": "worker/cb-42-3f2a" } ], "healthCoverage": "off" }I confirmed the
membersrow's field names (sessionId,paneId,profile,role,state, and — when set —worktree/branch/owner/agentSessionId) inSessionManager.rosterView(SessionManager.java:552-582), whichfleet_listbuilds each row from.
fleet_profiles is the catalogue of what you could spawn; fleet_list is what is actually
running. An empty members array means no member is spawned right now — it says nothing about
which profiles exist.
Mechanism 3 — spawning a member (fleet_spawn and profiles:)
A member's backend is a configured profile, not a ccs profile. There is no ccs anywhere
in the shipped configuration. The FleetConfig record (FleetConfig.java:81-101) has a
profiles: field instead — a Map<String, Profile>, documented at FleetConfig.java:37-41.
- Config.
fleetd.yaml(orbridged.yaml, depending on the host) has aprofiles:block. Each named profile carries its ownbaseUrl,model,tokenEnv(the host env var holding the auth token), andargv(the launch command —claudeby default). The full field list isFleetConfig.Profile(FleetConfig.java:314-330). - Spawn.
fleet_spawntakesrole(what the member is for —dev,reviewer, orarchitect; defaultdev) andprofile(which backend — omit it for the default). These are independent: a reviewer can run on the same profile as the developer it reviews. The tool's own description says so (FleetMcp.java:1148-1174). - Guard. Before spawning,
fleetdchecks the resolvedANTHROPIC_BASE_URLhost against an allowlist. A profile that resolves to a subscription host is refused — that would burn the lead's own quota. The check isSubscriptionGuard.assertWorker(SubscriptionGuard.java), called fromfleet_spawn's handler (FleetMcp.java:825-826, turning aGuardExceptioninto asubscription boundary: …tool error). - Add a backend = add a config entry. Adding a profile to
profiles:makes it appear infleet_profiles's output — no code change.
flowchart LR
CFG["fleetd.yaml<br/>profiles: block"] --> PICK["fleet_spawn(profile)"]
PICK --> GUARD{"resolved baseUrl host<br/>on the allowlist?"}
GUARD -->|"no"| REJ["refused — subscription boundary"]
GUARD -->|"yes"| SPAWN["herdr starts the profile's argv"]
SPAWN --> PANE["member pane<br/>ready"]
classDef ok fill:#2f855a,stroke:#22543d,color:#ffffff;
classDef bad fill:#b7791f,stroke:#7b341e,color:#ffffff;
class SPAWN,PANE ok
class REJ bad
Figure: profiles: is the spawn contract; the subscription guard checks the resolved host
before a pane is ever started. Verified against FleetConfig.java:314-330 (the Profile
record) and SubscriptionGuard.java (read in full).
Resuming a member, instead of respawning cold. fleet_spawn also takes resumeSessionId —
a prior member's agentSessionId (shown by fleet_list) — to relaunch onto that same
conversation instead of starting fresh. This only works for a profile whose backend adapter
declares Capability.SESSION_RESUME; otherwise the spawn is refused rather than silently
starting cold (SessionManager.java:220-235).
Mechanism 4 — the member's own lifecycle limits
A deployment can bound how long a member session lives. FleetConfig.Lifecycle has two
independent knobs (FleetConfig.java:637-639):
idleTtlSeconds— aSessionReaper(SessionReaper.java) tears down a member session that has sat idle (no delegated work) longer than this.contextCap— a member is force-released once it has served this many delegated turns, win or lose. InSessionManager.completeTurn(SessionManager.java:635-645), hitting the cap callsrelease(paneId)— the pane is torn down, not checkpointed or automatically respawned.
I looked for an automatic "checkpoint state to disk, then respawn and continue the same task"
mechanism (the old page called this a "Ralph loop") and did not find one in the session
lifecycle code. What the code actually does when a member's session ends is closer to nothing
automatic at all: continuity, when it exists, comes from what the member itself left behind —
a git commit on its branch, a pull request — or from an explicit fleet_spawn{resumeSessionId}
call the lead makes on purpose (Mechanism 3, above). There is no automatic recycle step between
turns, so do not plan around one.
Ideas that were never built
The earlier version of this page described a structured message envelope — fields like v,
from, to, session, turn, corr, kind, body — as if every fleet_send carried one.
It does not exist. fleet_send's content parameter is a plain string
(FleetMcp.java:1096-1108: "content", stringProp(...)), and there is no envelope-parsing code
behind it. In practice, structure comes from what the lead writes into
content — for example, naming a skill on the first line ("Load the reviewer skill.") so the
member knows the procedure to follow. This is documented practice (see the primary-side
directive, below), not a wire format.
If a structured envelope is ever built, it belongs back on this page as a real mechanism with
its own file:line citations. Until then, this section is a marker for readers who remember
the old design, not a thing to build against.
Primary-side directive — when to delegate
A lead on a metered subscription should reach for a member before doing bulk or mechanical work
itself. The shipped version of this reminder lives in the project's own CLAUDE.md, not in an
environment variable — a lead confirms it is a lead (as opposed to a spawned member) by calling
fleet_whoami, not by checking a marker string.
flowchart TD
T["a task arrives"] --> Q{"is the fleet MCP<br/>mounted at all?"}
Q -->|"no"| SELF["do it in this session"]
Q -->|"yes"| J{"needs YOUR judgment,<br/>or bulk / mechanical / parallel?"}
J -->|"judgment / interactive"| SELF
J -->|"can I write a brief a member<br/>could succeed on?"| DEL["fleet_spawn + fleet_send"]
classDef self fill:#2f855a,stroke:#22543d,color:#ffffff;
classDef del fill:#2b6cb0,stroke:#1a365d,color:#ffffff;
class SELF self
class DEL del
Figure: whether the fleet MCP is mounted flips the default from "do it myself" to "delegate unless it needs my judgment or I cannot write a brief the member can succeed on."
As-built — the CLAUDE.md "Bridge communication" section
This reminder is shipped as the first section of the project's own CLAUDE.md, ahead of
everything else — because a member runs in a git worktree of the same repository and inherits
that file verbatim. One file serves both roles, so the section starts by making the reader
establish which role it is, rather than assuming.
The section is split into layers, each reaching a different audience:
| Layer | Carries | Reaches |
|---|---|---|
| the launcher's reply charter | the one rule that must survive with no repo checkout: end every turn with fleet_reply |
every spawned member, at launch, whatever its backend |
CLAUDE.md → Bridge communication |
protocol invariants and orchestration policy | the lead and every member that reads the repo |
role playbook skills (e.g. implementer, reviewer) |
per-job procedure | a member told to load one |
the bridge's own docs (docs/MCP-Contract.md §6, this wiki) |
design detail, flows | anyone who goes looking |
A rule lives in exactly one of these layers — the outermost one that must obey it. Putting a rule in the wrong layer is how a skill and the shipped section end up disagreeing.
What the section actually tells a reader to do:
- Confirm your role with
fleet_whoamibefore anything else, rather than guessing from a side channel. - Never set or forward
ANTHROPIC_BASE_URL. Only the daemon puts a member off-subscription, at spawn. - The bridge is the only channel. Text printed to a terminal reaches nobody.
- A lead: split work into units with clear acceptance criteria, spawn every delegated unit
first, then send them all with
wait:false, poll for results, verify a member's claim by re-running the build rather than trusting a "clean" report, and never delegate the merge. - A member: load the named skill, stay in the assigned scope, use
fleet_askonly for a decision that is genuinely the lead's, and end every turn with exactly onefleet_reply.
An opencode member never reads CLAUDE.md at all — it only receives the reply charter as an
instructions file. Any rule a non-Claude peer must obey has to live in the charter, not in this
section.
fleet_whoami — asking instead of guessing
Shipped. fleetd already resolves every caller's identity for its own authorization checks
— fleet_whoami (no parameters) just reports that same resolution back as data, instead of
making the caller infer its own role from a side channel. Its handler is FleetMcp.java:739-786,
and it returns:
// called by a member
{ "role": "worker", "sessionId": "term_a7", "paneId": "w9:pW", "profile": "terra",
"state": "ready", "worktree": "/wt/cb-42", "branch": "worker/cb-42-3f2a" }
// called by the lead
{ "role": "primary" }
The lead's own row deliberately carries no sessionId beyond what a lead session already has —
handing it one it does not own would invite the same forged-identity problem the connection-based
resolution exists to prevent. A member the session registry has no record of (one that outlived
a daemon restart) still gets role and sessionId — the fields that matter — with the registry
fields simply absent rather than invented.
More use cases (catalogue)
Same mechanisms, different tasks. Every row below uses only the tools covered above.
| Use case | Shape |
|---|---|
| Delegated refactor or codegen | fleet_spawn(role="dev", profile=…, worktree=true, ticket=…), then fleet_send(sessionId, content). The member edits, commits on its branch, and opens its own PR (see the implementer skill); the reply is a summary. The lead reviews and merges. |
| Bulk test-writing or log triage | Cheap, parallelizable work on a local profile: fleet_spawn several members, fleet_send{wait:false} each one, then fleet_poll each ticket. |
| Parallel multi-file review | One reviewer member per file or area, each fleet_spawn{role="reviewer"}, dispatched with wait:false and collected with fleet_poll — see Team. |
| A member resumed onto its prior conversation | fleet_spawn{profile, resumeSessionId} with the agentSessionId fleet_list reported for a member that already finished a turn — only on a profile whose adapter supports it (Mechanism 3). |
| Cross-host lead coordination | fleet_send{coordId, content} to a peer lead on another daemon, over a configured coordinator: broker — coordination between leads, never a task. See Message Server. |
The portable CLAUDE.md block
This is the canonical text, verbatim. Copy it into any project whose agents mount the fleet MCP
server. It needs no editing — every project-specific detail was deliberately pushed out of it, into
the two notes below the block. Improvements land here first, then go out to each project's
CLAUDE.md.
The block still calls its own section "Bridge communication", and it still names the legacy
mcp__bridge__* mount in the role ladder. Both are kept on purpose: the text must stay
byte-identical with the copy in the fleet/fleetd repo, and a member spawned before the rename
really does report the old mount name.
## Bridge communication (enforced — read this first)
> **Canonical block.** Everything down to §Layering is the portable bridge charter, copied verbatim
> into every project that mounts the bridge MCP. Keep it byte-identical with the template in the
> wiki ([Use Cases](https://git.ltms.dev/fleet/fleetd/wiki/7-Use-Cases) → *The portable
> CLAUDE.md block*); improvements go to the template first, then out to each project. Anything
> specific to *this* repo lives under §Project addendum below, never inline above it.
>
> **Anything you measure in an addendum is perishable.** Date it, give the command that
> re-measures it and what each outcome means, and tell the reader to delete the section once
> it stops reproducing. The four parts work together: deciding what would falsify a claim is
> the expensive step, and a reader in the middle of another task will not pay it, so a bare
> "verify before relying on this" costs the same space and does nothing. The case this is for
> is a note that goes stale as a live restriction — it will tell a future session it cannot do
> the thing at the moment doing it becomes the job.
If no `fleet_*` MCP tools are mounted in this session, this section does not apply — skip it.
`fleetd` is the **sole communication gateway** between agents here. The orchestrating session (the
**primary**) and every delegated peer (a **member**) mount the *same* MCP server and talk only
through its `fleet_*` tools. No session addresses a peer, a broker, or the network directly.
### Which role am I? — settle this before acting
**Every role reads this file.** A member runs in a git worktree of this same repo, so it inherits
this `CLAUDE.md` verbatim, and every rule below is role-conditional.
**Call `fleet_whoami`.** It returns `primary`, `worker`, or `architect`, resolved by the daemon from
your connection — unforgeable, and the same resolution its authorization gate uses. A worker also
carries its `sessionId`, `profile`, `worktree` and `branch`; an architect carries the slot name it
was bound to. Don't infer what you can ask.
Only if that call is unavailable, fall back to these — each is one-way, so keep reading until one
fires: the reply charter in your system prompt (*"You are a spawned member in the
claude-bridge fleet"*) ⇒ **spawned member**; fleet tools prefixed `mcp__fleet__*` ⇒ **spawned
member** (the launcher fixes that mount name; a primary's mount is named by whoever wrote its
`.mcp.json`, so it varies — and a member spawned before CB-632 still says `mcp__bridge__*`); `ANTHROPIC_BASE_URL` set ⇒ **spawned member** (Claude-model members run
on a clean env, so its *absence* proves nothing). None of these separate a worker from an architect —
only `fleet_whoami` does. **Still unsure ⇒ act as a worker**, the most restricted member role. The
two mistakes are not symmetric: a primary acting as a worker is refused by the authorization gate —
loud and self-correcting — while a member acting as the primary ends its turn with no `fleet_reply`,
and the sender silently receives nothing. Fail toward the recoverable error.
### Invariants — both roles, no exceptions
1. **Never set, export, or forward `ANTHROPIC_BASE_URL`** (or `ANTHROPIC_AUTH_TOKEN`). The primary
stays on subscription; only the bridge puts a member off it, at spawn. Mounting the bridge must
never move a session across that boundary.
2. **The bridge is the only channel.** Text you print in your terminal reaches nobody — the other
side cannot see your screen. An answer that isn't in a `fleet_*` call is silently discarded.
3. **Identity comes from the connection, never an argument.** Workers never pass a target; you
cannot act as another session. Spawn/stop/drain are lead-only; **send is lead or architect**;
reply/ask are only-as-itself — any peer may answer for its own pane, and for no other. A call
outside your role is refused, not queued.
4. **Delivery is status-gated: one message per turn.** Don't busy-poll a peer's terminal and don't
re-send because a call looks slow — the bridge delivers when the peer is `idle`, `blocked` or
`done`. A spawned member must **also** have mounted the bridge MCP: until it has, it is not
deliverable, and a send waits on that gate for ~60s and then fails without ever reaching its pane.
5. **Never move a fleet session, pane or peer except through the bridge.** The bridge owns policy;
the multiplexer owns PTYs. Any route that changes fleet state without the bridge's checks
bypasses every rule above — the `herdr` CLI and its socket are the usual example.
### Primary (lead) — run this on every task, in order
**Delegate by default — that is the job.** With the bridge mounted you are an orchestrator on a
metered subscription, and workers are cheap, parallel, and disposable. The default answer to "who
does this?" is **a worker**, not you. Reach for `fleet_send` before you reach for `Edit`. The steps
below are the procedure — run them in order, every task, not only the big ones.
0. **Know your role** — `fleet_whoami`, once per session, before anything else.
1. **Split.** Write the unit list. Every unit carries: scope · the files or PR in question ·
acceptance criteria · exactly what to report back. A unit with no acceptance criteria is not
ready to delegate — refine it or keep it.
2. **Gate each unit** on one question: **"can I write a brief good enough for a worker to
succeed?"** — *not* "could I do this faster myself?" (usually you could; doing it yourself costs
your context and your subscription, while a wasted worker turn costs a worker turn). Yes ⇒
delegate. The keep-list is closed: the conversation with the user, decomposition and planning,
the final judgment call, verification, merges, and anything that depends on context only you
hold. Nothing else is yours by default.
3. **Spawn every delegated unit first** — `fleet_spawn{profile, worktree:true, ticket}`, one per
unit, *before* sending any. Pass `profile` explicitly: profiles differ in model, cost and
LIVENESS, not in tier, so the default is rarely what you want. The default is whatever the
daemon reports, and on a host where it sits on an exhausted or withdrawn credential every
unqualified spawn fails — sometimes loudly, sometimes as a member that spawns fine and then
produces nothing. `fleet_profiles` reports the default; check it once per session.
4. **Then send them all** — `fleet_send{sessionId, content, wait:false}`. Line 1 of every brief is
`Load the <name> skill.` naming the worker's playbook; those skills are opt-in and that line is
what makes them reliable. Where the project ships no such skill, spell the procedure out in the
brief instead. The brief is self-contained — the worker sees your message and the repo, nothing
of your context, your plan, or your screen.
5. **Collect** — `fleet_poll{ticket}` → `fleet_ack{target, msgId}`. Answer a worker's `fleet_ask`
with `fleet_send{turnId, content}` — **not** `sessionId`. A worker gone quiet is diagnosed with
`fleet_status`, never by reading its terminal; it also reports an open question and the `turnId`
that answers it. **A worker's ask waits ~55 seconds, and no nudge makes that longer** — so never
brief a worker to "ask me". Decide before you delegate, or give it an explicit default.
**A correction cannot reach a busy member.** A `fleet_send` to a working member is *accepted* and
returns a ticket, and is then never delivered — measured here three times in one session, and the
member was released still executing a brief that had been retracted twice. The receipt is true and
it is a fact about the *mailbox*; what you needed was a fact about the *pane*. **A push delivery
needs the recipient free at send time; a pull channel needs only that they look before acting.** So
put every correction on the **ticket**, which they can read whenever they look, and send as the
notification. That obliges you, not them: **all corrections go to the ticket, and the brief is
write-once.** The member cannot check which source is newer — it just always prefers the ticket —
so the day you revise a brief in place instead of commenting, it obeys your rule and does the wrong
thing. A *first* brief for a unit not yet running is not a correction, and may be the whole spec.
6. **Verify yourself.** Re-run the build and the checks. A worker cannot run your IDE tooling, any
forge MCP server it appears to have holds a blocked credential and fails every call, and a piped
command (`… | tail`) hides failures behind a zero exit — never promote a worker's "clean" to a
fact. Its injected repo-scoped `GITEA_TOKEN` is a different credential and does work, so a worker
reporting that it opened its own PR is reporting something it really can do.
7. **Review — fan out.** Spawn reviewers against the diff, one per dimension or per file, with
`wait:false`. Never the implementer of the scope it reviews, and brief them from the diff — not
from the implementer's rationale, which carries its own blind spot. Dispatch each PR's reviewers
as it lands; don't wait for the last implementer. Under ~50 changed lines, skip the fan-out and
read it yourself.
8. **Adjudicate, merge, tear down — yours alone.** Read the diff yourself: fully if it is small,
targeted at the reported findings and the risky paths if it is large. Reviewer findings direct
your attention; they never substitute for it. Then merge, then `fleet_stop{paneId}`.
**If the forge refuses you the merge** — a protected branch, a token without the grant — the
adjudication is still yours. Read the diff, decide, and hand the operator a merge-ready queue
with the refusal quoted. Never report a PR as merged, and never call one "ready to merge"
without having read the diff yourself. A refusal is exactly when that shortcut is tempting,
because no action is left that forces you to look, and taking it turns this step into
forwarding a reviewer's verdict — which is delegating the merge by proxy, two lines above.
**Test a refusal; do not read it off a permissions field.** A protected branch holds its merge
rights separately from the repository permissions, so that field can say yes while the merge is
refused, and still say no after a grant makes it work. Probe instead, with a request that cannot
succeed on its merits, so a rejection can only mean the refusal. Treat a transport failure as a
third answer that proves nothing: a timeout, a DNS error or a bad URL is not a refusal, and
counting it as one makes you sure of something you never measured.
**Steps 3 and 4 are separate on purpose** — spawning and sending in one loop is how parallel work
silently becomes serial, and it is the most common way this layer is wasted. For the same reason,
prefer `wait:false` + `fleet_poll` for anything non-trivial: a blocking `fleet_send` is capped by
*your own* MCP client call timeout (~60s), well below the task's real runtime.
**Delegating does not delegate responsibility.** Workers open PRs; you are the gate. Never delegate
the merge — and merging on a reviewer's word is delegating it by proxy.
**When a decision blocks you, consult architects — not the operator.** Spawn one or more architect
members, give them the question and the evidence you have, and act on what they agree. They are
authorized to settle it, not only to advise. Architects first form independent positions, then
compare them. If they still disagree after that comparison, they return both positions and their
checked evidence; the lead decides. Go to the operator only for an action the fleet has no
authority to take, such as spending money, granting access, or making a promise to someone else.
**Then write the decision on the ticket.** Taking the operator out of the loop also removes the
signal they used to get, because that signal was the block itself — work stopped, so they found
out. A ticket comment replaces it, and it reaches them whether or not they are at a terminal when
you decide.
| Intent | Tool |
|---|---|
| Confirm your own role | `fleet_whoami` |
| See backends available | `fleet_profiles` |
| Start a member | `fleet_spawn{role?, profile?, cwd?, worktree?, ticket?, sessionName?, resumeSessionId?}` → `sessionId` + `paneId` |
| See the fleet | `fleet_list` → `leads` (your peers) + `members` (each carries `agentSessionId` when its backend knows one) + `loopHealth` (`RUNNING`, `STALLED`, or `STOPPED` for `statusPoller` and `sessionReaper`) · one peer's state: `fleet_status{sessionId}` |
| Delegate (blocking) | `fleet_send{sessionId, content}` |
| Delegate (long task) | `fleet_send{sessionId, content, wait:false}` → ticket → `fleet_poll{ticket}` |
| Answer a member's `fleet_ask` | `fleet_send{turnId, content}` — **not** `sessionId` |
| Message a **peer lead** on this host | `fleet_send{sessionId: <their terminal>, content}` — `fleet_list` → `leads` reports it. Coordination only, **never** a task |
| Message a **peer lead** on another daemon or host | `fleet_send{coordId: <their coord-id>, content}` — needs a `coordinator:` block; your own coord-id is in `fleet_list`. Coordination only, **never** a task |
| Answer a peer lead that messaged you | `fleet_send{coordId}` — or `{sessionId}` if they are on this host. **Not** `fleet_reply`: it has no peer route and the publish is refused |
| Read your own held lead-to-lead mail (no ack) | `fleet_poll{coordId: <your own coord-id, from fleet_list's coordinator.selfId>}` — primary-only; never acks, so `fleet_list`'s `held[]` still shows it after. `fleet_list`'s `held[]` gives only a truncated preview — this is the only way to read the full body |
| Collect a held reply | `fleet_poll{target}` · then `fleet_ack{target, msgId}` |
| Tear down a member | `fleet_stop{paneId}` |
| Replace your OWN lead session when its context is full | `fleet_handover{action:"open", reason?}` → write the handover file it names → `fleet_handover{action:"confirm", token, operatorConfirmed}`. Primary-only. **In that order**: the file must be modified *after* `open`, or `confirm` refuses it as stale. There is no terminal parameter — the pane is always your own, so you can never roll another lead. `{action:"cancel", token}` drops a pending request |
### Lead ↔ lead — coordinate, never delegate
`fleet_list` returns `leads` alongside `members`; your own row carries `self: true`. Every other row
is a peer — an orchestrator with its own context, its own members, and its own judgment. An empty
`members` array means no members are spawned; it says nothing about peers.
**A lead never assigns a task to another lead.** Work goes to members — only ever downward, never
sideways. Sending a peer a brief with acceptance criteria is a category error: a brief is a member's
artefact, and a peer is not yours to task. If a unit needs doing and it falls in your area, spawn a
member and delegate it yourself; if it falls in the peer's area, say so and let the peer assign it.
The traffic between leads is coordination and nothing else:
1. **Divide the map, not the work.** Agree who owns which area, then each of you assigns inside your
own. Split by **context ownership** — whoever already holds the context owns that area — and say
who takes what, in one message, before either of you starts. Two leads silently working the same
unit is the failure mode here, and neither notices until the merge.
2. **Share findings, hazards, and corrections.** What you have already discovered, what broke, what
the next person will trip on. This is the traffic that actually pays for the channel: it costs one
message and saves a peer a rediscovery.
3. **Verify a peer exactly as you verify yourself.** Peer status buys nothing: check the claim
against the code, and re-run the build. A peer's correction gets the same treatment — right or
wrong on the evidence, not on who said it. Neither of you merges the other's work unreviewed.
**N observations are N data points only if they differ in the axis you are trusting.** This cuts
both ways. N *failures* blamed on one cause are one data point when the cases share what you are
not varying. N *agreeing measurements* are also one data point when they share an instrument —
two hosts, two operators and the same formula is one formula, not two confirmations.
4. **Ask a peer to read your project addendum.** Your addendum is instruction surface: every future
session on your host obeys it, and a wrong one is obeyed just as faithfully as a right one. The
author is the worst reader of their own qualifier placement — measured here, one addendum carried
two defects and a non-author found both. If you have no peer, at least re-read it asking "which
sentence goes false first, and would a reader reach the caveat before acting?"
Being messaged by a peer does not make you its worker: answer the way you would open —
`fleet_send{coordId}` for another daemon, `fleet_send{sessionId}` on this host — and push back on
the substance if it is wrong. `fleet_reply` resolves a member's blocked `fleet_send`; a peer's
coord-id message is durable and non-blocking, so there is nothing for it to resolve. A peer that
simply complies has thrown away the reason there are two of you.
### Member (worker or architect) — the turn contract
1. **Load the playbook skill the lead named** before doing anything else.
2. **Do the assigned scope only.** Note anything you spot outside it in one line; don't go hunt it.
3. **`fleet_ask{question}`** when a decision is genuinely the lead's (ambiguous requirement, two
defensible fixes, "bug or intended?"). It blocks and you resume the *same* turn with the answer.
Don't ask what you could decide yourself.
4. **Re-read the ticket before you act on anything you were told earlier**, and again before you
commit. A message reaches you only while you are free to receive it; the ticket is there whenever
you look. **If a ticket comment contradicts your brief, the ticket comment is newer and it wins.**
5. **End the turn with exactly one `fleet_reply{content}`**, carrying your complete answer. This is
the whole handoff. No `fleet_reply` ⇒ the sender gets nothing and the exchange stalls.
Do **not** lean on the completion fallback to carry your answer for you: when you end a turn
without replying, the bridge scrapes your pane, and it can return only the last 4000 characters.
A clipped scrape is marked as partial, but the missing text is gone — your report reaches the
lead with its end cut off.
6. **Report honestly.** State only what you actually ran and its real output, including failures,
and never claim the result of a check you had no way to run. **Measure your own tools; do not
assume them.** What you mount depends on your backend: an opencode member gets the bridge and
nothing else, while a Claude Code member also inherits the operator's user-scope MCP servers,
which the bridge never chose for you. Two rules follow. The primary's IDE tooling is still not
yours, whatever you see. And **a mounted tool is not a working tool** — the forge MCP server you
may find there holds a deliberately blocked credential and fails every call, by design. That is
not your only forge route, and the two must not be confused: the repo-scoped `GITEA_TOKEN` the
daemon injects into your environment does work, and using it to open your own PR is part of the
job. A blocked MCP tool is never a reason to skip that step.
7. **Never merge.** Stage files explicitly — never `git add -A` — and leave alone anything the
project marks as not-yours-to-commit.
### Where each rule lives (don't duplicate — extend the right layer)
| Layer | Scope | Reaches |
|---|---|---|
| the launcher's reply charter | the one rule that must survive with no repo: *end every turn with `fleet_reply`* | every spawned member, at launch, every peer kind — never a lead |
| **this section** | protocol + orchestration policy | primary **and** every member that reads the repo — tracked in git, so worktrees inherit it |
| role agent definition files | role contract and per-job procedure | a member whose launcher binds its role to the matching file in its worktree |
| role playbook skills | per-job procedure (commit/PR recipe, finding format) | a member told to load one |
| the bridge's own docs | design detail, flows, error model | on demand |
A rule belongs in **exactly one** layer — the outermost one that must obey it. A member without a
repo checkout still gets the launcher's reply charter, which is why that one rule stays there.
Peers that don't read `CLAUDE.md` (non-Claude adapters) get the charter only, so any rule *they*
must obey belongs in the charter, not here.
Two rules keep this portable, and both were learned by getting them wrong first:
- Nothing repo-local inside the block. The first draft named
auth/Authz.java, the.mcp.jsonandwiki/commit exclusions, and theimplementerandreviewerskills by name — all meaningless in another project. Each moved to a Project addendum section that sits below the block and never mixes into it, so the block can be replaced whole without reading it. - Every fallback signal must be one-way. The role ladder first read the MCP mount name in both
directions: one name meant primary, another meant worker. Only one half is real. The launcher
hard-codes the member's mount name (
PeerLauncher.java:34,MCP_MOUNT_NAME = "fleet"), but a primary's mount is named by whoever wrote that project's.mcp.json, so it can be anything. A two-way reading of a one-way signal is a confident wrong answer, so the block states only the direction that holds.
In the fleet/fleetd repo itself the block is treated as shipped surface, not documentation.
That repo's CLAUDE.md carries a change-checklist mapping each part of the code — the tool
catalogue, Authz, ConnectionIdentity, REPLY_CHARTER, the injector, the worktree overlay, the
skills — to the part of the block that change can invalidate. It also carries a check that fails if
this template and that copy have drifted apart. A code change that silently makes the block false is
an incomplete change, because the agents reading it have no other source.
Related
- Architecture — the invariants every flow on this page holds to.
- Message Server — the full
fleet_*tool reference and REST surface. - Team — running more than one member at once.
📖 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