33
7 Use Cases
Dai Ha edited this page 2026-09-20 16:13:31 +07:00

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 into leads (peer orchestrators) and members (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 members row's field names (sessionId, paneId, profile, role, state, and — when set — worktree/branch/owner/agentSessionId) in SessionManager.rosterView (SessionManager.java:552-582), which fleet_list builds 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 (or bridged.yaml, depending on the host) has a profiles: block. Each named profile carries its own baseUrl, model, tokenEnv (the host env var holding the auth token), and argv (the launch command — claude by default). The full field list is FleetConfig.Profile (FleetConfig.java:314-330).
  • Spawn. fleet_spawn takes role (what the member is for — dev, reviewer, or architect; default dev) and profile (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, fleetd checks the resolved ANTHROPIC_BASE_URL host against an allowlist. A profile that resolves to a subscription host is refused — that would burn the lead's own quota. The check is SubscriptionGuard.assertWorker (SubscriptionGuard.java), called from fleet_spawn's handler (FleetMcp.java:825-826, turning a GuardException into a subscription boundary: … tool error).
  • Add a backend = add a config entry. Adding a profile to profiles: makes it appear in fleet_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 — a SessionReaper (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. In SessionManager.completeTurn (SessionManager.java:635-645), hitting the cap calls release(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_whoami before 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_ask only for a decision that is genuinely the lead's, and end every turn with exactly one fleet_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:

  1. Nothing repo-local inside the block. The first draft named auth/Authz.java, the .mcp.json and wiki/ commit exclusions, and the implementer and reviewer skills 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.
  2. 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.

  • 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.