From 890190263e3b6e94405aeb7c3f7a85a81a67d451 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sat, 15 Aug 2026 04:37:03 +0200 Subject: [PATCH] CB-560/562/563: the shipped block tells the truth again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The canonical CLAUDE.md block is the instruction surface this daemon ships to every agent that mounts it, so a code change that silently invalidates it is an incomplete change. CB-548 added a third principal kind and the block was never revisited. Four statements in it were simply false: * whoami was documented as returning only primary or worker; * "spawn/stop/send/drain are lead-only" — Authz permits SEND to an architect; * delivery was documented as idle/blocked — injectable() is IDLE|BLOCKED|DONE, and a spawned member must also have mounted the bridge MCP, which is the exact condition that made every architect undeliverable for a day; * the tool table said bridge_list returns `workers` — the JSON key is `members`. Also corrected: the fallback ladder claimed each one-way signal identifies a "worker", but an architect gets the same charter, the same mount and the same env, so those signals identify a spawned member and only bridge_whoami separates the two. The safe default stays "act as a worker" — it is the most restricted member role. The turn contract now covers both member kinds, and says why the completion fallback is not a substitute for bridge_reply: it returns at most the last 4000 characters, so a long report reaches the lead with its end cut off. That is not hypothetical — it happened twice today. Found by a reviewer asked whether the block still matches the code. Verified against injectable(), Authz, BridgeMcp.listFleet and LeadLauncher before applying. The wiki template is updated in the same shape and re-checked byte-identical. --- CLAUDE.md | 70 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e2f7ca7..882e951 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,41 +11,45 @@ If no `bridge_*` MCP tools are mounted in this session, this section does not apply — skip it. `bridged` is the **sole communication gateway** between agents here. The orchestrating session (the -**primary**) and every delegated peer (a **worker**) mount the *same* MCP server and talk only +**primary**) and every delegated peer (a **member**) mount the *same* MCP server and talk only through its `bridge_*` tools. No session addresses a peer, a broker, or the network directly. ### Which role am I? — settle this before acting -**Both roles read this file.** A worker runs in a git worktree of this same repo, so it inherits +**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 `bridge_whoami`.** It returns `{"role":"primary"}` or `{"role":"worker","sessionId":…, -"profile":…,"worktree":…,"branch":…}`, resolved by the daemon from your connection — unforgeable, -and the same resolution its authorization gate uses. Don't infer what you can ask. +**Call `bridge_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 an off-subscription worker in the -claude-bridge fleet"*) ⇒ **worker**; bridge tools prefixed `mcp__bridge__*` ⇒ **worker** (the -launcher fixes that mount name; a primary's mount is named by whoever wrote its `.mcp.json`, so it -varies); `ANTHROPIC_BASE_URL` set ⇒ **worker** (Claude-model workers run on a clean env, so its -*absence* proves nothing). **Still unsure ⇒ act as a worker.** The two mistakes are not symmetric: a -primary acting as a worker is refused by the authorization gate — loud and self-correcting — while a -worker acting as the primary ends its turn with no `bridge_reply`, and the sender silently receives -nothing. Fail toward the recoverable error. +claude-bridge fleet"*) ⇒ **spawned member**; bridge tools prefixed `mcp__bridge__*` ⇒ **spawned +member** (the launcher fixes that mount name; a primary's mount is named by whoever wrote its +`.mcp.json`, so it varies); `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 `bridge_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 `bridge_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 worker off it, at spawn. Mounting the bridge must + 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 `bridge_*` 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/send/drain are lead-only; 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 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`. + 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 drive the terminal multiplexer directly** (no `herdr` CLI, no socket). The bridge owns policy; the multiplexer owns PTYs. Going around the bridge bypasses every rule above. @@ -101,26 +105,26 @@ the merge — and merging on a reviewer's word is delegating it by proxy. |---|---| | Confirm your own role | `bridge_whoami` | | See backends available | `bridge_profiles` | -| Start a worker | `bridge_spawn{profile?, cwd?, worktree?, ticket?}` → `sessionId` + `paneId` | -| See the fleet | `bridge_list` → `leads` (your peers) + `workers` · one peer's state: `bridge_status{sessionId}` | +| Start a member | `bridge_spawn{role?, profile?, cwd?, worktree?, ticket?}` → `sessionId` + `paneId` | +| See the fleet | `bridge_list` → `leads` (your peers) + `members` · one peer's state: `bridge_status{sessionId}` | | Delegate (blocking) | `bridge_send{sessionId, content}` | | Delegate (long task) | `bridge_send{sessionId, content, wait:false}` → ticket → `bridge_poll{ticket}` | -| Answer a worker's `bridge_ask` | `bridge_send{turnId, content}` — **not** `sessionId` | +| Answer a member's `bridge_ask` | `bridge_send{turnId, content}` — **not** `sessionId` | | Message a **peer lead** | `bridge_send{sessionId: , content}` — `bridge_list` → `leads` reports it. Coordination only, **never** a task | | Answer a peer lead that messaged you | `bridge_reply{content}` — the one case a lead replies | | Collect a held reply | `bridge_poll{target}` · then `bridge_ack{target, msgId}` | -| Tear down | `bridge_stop{paneId}` | +| Tear down a member | `bridge_stop{paneId}` | ### Lead ↔ lead — coordinate, never delegate -`bridge_list` returns `leads` alongside `workers`; your own row carries `self: true`. Every other row -is a peer — an orchestrator with its own context, its own workers, and its own judgment. An empty -`workers` array means no workers are spawned; it says nothing about peers. +`bridge_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 workers — only ever downward, never -sideways. Sending a peer a brief with acceptance criteria is a category error: a brief is a worker's +**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 -worker and delegate it yourself; if it falls in the peer's area, say so and let the peer assign it. +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 @@ -138,7 +142,7 @@ Being messaged by a peer does not make you its worker: answer with `bridge_reply the substance if it is wrong. A peer that simply complies has thrown away the reason there are two of you. -### Worker — the turn contract +### 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. @@ -147,6 +151,10 @@ you. Don't ask what you could decide yourself. 4. **End the turn with exactly one `bridge_reply{content}`**, carrying your complete answer. This is the whole handoff. No `bridge_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. 5. **Report honestly.** State only what you actually ran and its real output, including failures. You mount **only** the bridge MCP — the primary's other servers (IDE, forge, docs) are not yours, so never claim the result of a check you had no way to run. @@ -157,9 +165,9 @@ you. | Layer | Scope | Reaches | |---|---|---| -| the launcher's reply charter | the one rule that must survive with no repo: *end every turn with `bridge_reply`* | every worker, at launch, every peer kind | -| **this section** | protocol + orchestration policy | primary **and** every Claude worker — tracked in git, so worktrees inherit it | -| role playbook skills | per-job procedure (commit/PR recipe, finding format) | a worker told to load one | +| the launcher's reply charter | the one rule that must survive with no repo: *end every turn with `bridge_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 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. Peers that don't read