diff --git a/11-Features.md b/11-Features.md index 7b427d9..ec64da1 100644 --- a/11-Features.md +++ b/11-Features.md @@ -31,6 +31,7 @@ six weeks, and the table alone will not carry it. | [Re-read the config without a restart](#re-read-the-config-without-a-restart) | `configReload.enabled: true` | CB-559 | `config/ConfigRef` | | [Leads talk to each other](#leads-talk-to-each-other) | (always on, two leads) | CB-532 | `auth/Principal` | | [A lead can be delivered to](#a-lead-can-be-delivered-to) | automatic | CB-534 | `Bridged.deliverableTo` | +| [Know when a completion fallback is partial](#know-when-a-completion-fallback-is-partial) | automatic | CB-563 | `inject/CompletionResolver` | | [Leads are visible in bridge_list](#leads-are-visible-in-bridge_list) | automatic | CB-535 | `mcp/BridgeMcp.listFleet` | | [Reply nudges follow the delegating lead](#reply-nudges-follow-the-delegating-lead) | automatic (retires `primary:`) | CB-532 | `mcp/PrimaryRegistry` | | [Advisory architect slots](#advisory-architect-slots) | `fleet.architects:` | CB-548 | `auth/MemberRegistry` | @@ -166,7 +167,9 @@ the placement policy gains an exclusion setting. **What.** Declares named, strong-model advisory slots. A bound architect resolves as `architect`, can send, reply, ask, and read, but cannot spawn, stop, or drain the fleet. The operating shape is one human-driven lead, two short-lived architects, and N workers: the lead consults the architects sideways -and discards them, rather than creating a supervisor above the lead. +and discards them, rather than creating a supervisor above the lead. Like every spawned member, an +architect becomes deliverable once it mounts the bridge MCP — until then a brief sent to it is held at +the readiness gate and never typed into its pane (CB-560). **On.** Declare each slot against a configured worker profile: @@ -189,6 +192,12 @@ an architect until the lifecycle binds a live terminal to a slot. The profile na as are duplicate slot names. An architect has delegation authority but no lifecycle authority, by design. +Shipping the binding is not the same as shipping the role. For one day an architect bound correctly, +resolved as `architect`, and could not receive a single message: the presence map that opens the +readiness gate was keyed on `Role.WORKER`, so an architect was never marked available. It built clean +and passed two reviewers. When a change adds a role, check every place that assumes a member *is* a +worker — `bridge_status` tells you whether a member ever became ready. + ## Keep a worker conversation **What.** Carries a bridge logical session name and a peer-owned resume id through `SpawnRequest`, then @@ -537,9 +546,9 @@ addressed to a lead is actually typed into its pane. **Why.** The gate (CB-113) holds a delivery out of a *spawned* worker's boot window: herdr reports `idle` while the agent is still starting, and a paste into that window is lost. Membership in it -comes from `WorkerPresence`, which `BridgeMcp` populates **only for a worker** — deliberately, since -that map doubles as the roster's availability signal and a lead counted there would appear as an -available worker. The two rules composed into a dead end: a lead is never marked present, so the +comes from `MemberPresence`, which `BridgeMcp` populates **for every spawned member** — worker or +architect, but never a lead, since that map doubles as the roster's availability signal and a lead +counted there would appear as an available member. The two rules composed into a dead end: a lead is never marked present, so the gate never opened for one, so [lead-to-lead messaging](#leads-talk-to-each-other) — shipped and authorized in CB-532 — still could not deliver a single keystroke. The gate's premise simply does not apply to a lead: a lead is never spawned, so it has no boot window to guard. @@ -549,6 +558,12 @@ another form: the send was accepted, the pane stayed `idle`, nothing was ever ty (`READINESS_GRACE_POLLS`, 240 × 250ms) it failed through the same path as a stalled turn — so the log said *turn-stall fallback* while the truth was that delivery had never been attempted. A repeating 62-second gap between send and failure is the signature of the gate, not of a peer that ignored you. + +The gate is no longer mute (CB-562). When the grace expires it logs a WARN naming the target, the +poll count, the grace in seconds and how many queued messages it is failing because the target never +became deliverable — so a readiness failure now reads differently from a turn stall. The seconds are +derived from `Injector.POLL_INTERVAL_MILLIS`, the single source the poller is also built from, so a +cadence change cannot leave the log confidently stating a wrong duration. It was also intermittently masked: presence is a sticky set, so a pane that was seen as a worker *before* being recognised as a lead stayed deliverable until the next restart cleared the set. @@ -741,6 +756,29 @@ earns a fresh attempt. --- +## Know when a completion fallback is partial + +**What.** If a member ends a turn without `bridge_reply`, bridged scrapes its pane and resolves the +waiting send with that tail, so the sender is not left hanging. The tail is capped at +`MAX_SCRAPE_CHARS` (4000). When the cap bites, the returned text now ends with +`[Pane tail clipped: member did not call bridge_reply.]` and bridged logs a WARN with the original +length and the cap. + +**On.** Automatic, whenever the completion fallback reads more than 4000 characters. + +**Why.** A clipped transcript used to be indistinguishable from a complete report. A delegating lead +could act on an engineering report whose end had been cut off and never know — the only trace was a +DEBUG line reading `(4000 chars scraped)`, which reads like a size, not a warning. The cap itself is +deliberate and unchanged; the defect was silence, not the number. + +**Gotcha.** The marker does not recover the missing text, and it is not a licence to skip the reply. +The fallback is a liveness net, not a channel: it returns only the pane tail, stripped to the last +assistant block. A member must still end every delegated turn with exactly one `bridge_reply`. The +marker is appended **after** the CB-115 misattribution guard compares the scrape to its baseline, so +marking cannot make an unchanged pane look like new output. + +--- + ## Backfill status This page was started after the fact, so it is **not yet complete**. Entries above are written from diff --git a/7-Use-Cases.md b/7-Use-Cases.md index f938dd0..bb87fa6 100644 --- a/7-Use-Cases.md +++ b/7-Use-Cases.md @@ -299,41 +299,45 @@ notes after the block). Improvements land *here* first, then propagate to each p 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. @@ -389,26 +393,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 @@ -426,7 +430,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. @@ -435,6 +439,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. @@ -445,9 +453,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