diff --git a/CLAUDE.md b/CLAUDE.md index 812acbd7..f9dd5f2c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,10 +27,11 @@ through its `fleet_*` tools. No session addresses a peer, a broker, or the netwo **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. +**Call `fleet_whoami`.** It returns `primary`, `worker`, `architect`, or `collaborator`, 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; a collaborator carries its registry name and its own `sessionId`, and +**no `leader` key** — a collaborator is a named peer, not a primary. 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 @@ -38,12 +39,17 @@ claude-bridge fleet"*) ⇒ **spawned member**; fleet tools prefixed `mcp__fleet_ 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 +only `fleet_whoami` does. **And none of them fires for a collaborator at all**: every signal in the +ladder detects a *spawned* member, while a collaborator is a tab a person opened by hand, so it has +no charter, no fixed mount name and a normal environment. A collaborator that cannot call +`fleet_whoami` therefore falls to the line below and acts as a worker. That is the safe direction — +it under-privileges, and the refusals are loud — but it means a collaborator has no way to learn +what it is except by asking. **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 +### Invariants — every role, 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 @@ -51,9 +57,10 @@ and the sender silently receives nothing. Fail toward the recoverable error. 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. + cannot act as another session. Spawn/stop/drain are lead-only; **send is lead, architect, or + collaborator** — and a collaborator may send only to a lead or another collaborator, never to a + spawned member's terminal; 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 @@ -161,6 +168,7 @@ you decide. | Answer a member's `fleet_ask` | `fleet_send{turnId, content}` — **not** `sessionId` | | Message a **peer lead** on this host | `fleet_send{sessionId: , content}` — `fleet_list` → `leads` reports it. Coordination only, **never** a task | | Message a **peer lead** on another daemon or host | `fleet_send{coordId: , content}` — needs a `coordinator:` block; your own coord-id is in `fleet_list`. Coordination only, **never** a task | +| Message a **collaborator** on this host | `fleet_send{sessionId: , content}` — but **`fleet_list` does not report collaborators**, so you cannot discover one: it must tell you its `sessionId`, which its own `fleet_whoami` gives it. 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: }` — 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}` | @@ -234,6 +242,27 @@ simply complies has thrown away the reason there are two of you. 7. **Never merge.** Stage files explicitly — never `git add -A` — and leave alone anything the project marks as not-yours-to-commit. +### Collaborator — a named peer, not a member + +`fleet_whoami` answered `collaborator`, so your pane's tab matches a `fleet.collaborators..tab` +entry. You are **not** a member: nothing delegates to you, you have no brief, no worktree and no +ticket, and **you owe no `fleet_reply`** — the turn contract above is for a session a lead spawned, +and it does not apply to you. Read it only to understand what the members around you are doing. + +What you may do: observe the fleet (`fleet_list`, `fleet_profiles`, `fleet_whoami`), and send to a +lead or to another collaborator. What you may not: spawn, stop or drain anything, roll a lead's +session, answer a member's `fleet_ask`, poll a ticket, or send to a spawned member's terminal. Each +of those is refused at the gate, not queued. + +Two limits worth knowing before you hit them. **You cannot reach a worker** — not even to help one — +because a worker belongs to the lead that spawned it, and routing around that would make you a +second orchestrator with no plan. Send to the lead instead. And **you cannot read a ticket**, so you +cannot collect a delegation's reply; ticket ids are a plain counter with no owner check, so holding +one would let you walk every other session's answers. + +Being named buys you a channel, not authority. Your `fleet_send` to a lead is coordination between +peers: the lead owes you no obedience, and you owe it none. + ### Where each rule lives (don't duplicate — extend the right layer) | Layer | Scope | Reaches | @@ -359,7 +388,7 @@ Before you call any work done, check the row that matches what you touched: | `ConnectionIdentity` / how a caller is resolved | the `fleet_whoami` paragraph and the fallback ladder | | `REPLY_CHARTER`, or a launcher's mount/flags | the fallback ladder (`mcp__fleet__*`), and the layering table's top row | | the injector / status gating | invariant 4 | -| worktree provisioning or the parity overlay | the "both roles read this file" premise — it rests on the worker's worktree being a checkout of this repo | +| worktree provisioning or the parity overlay | the "every role reads this file" premise — it rests on the worker's worktree being a checkout of this repo | | `.claude/skills/**` | the addendum's skill list, and the "name the playbook" rule | | a new peer kind (non-Claude adapter) | what that peer can read — anything it must obey belongs in its charter, not in the block | | **anything an operator can use, configure, or observe** — an MCP tool, a `fleetd.yaml` knob, an endpoint, a visible behaviour | **[Features](wiki/11-Features.md)** — one entry: what it does · the knob that turns it on · **why it exists** · the gotcha |