CB-622: rename bridge_* tool names to fleet_* in Markdown docs #133

Merged
ltms merged 1 commits from worker/cb-622b-717c67-2 into main 2026-08-22 21:58:16 +02:00
27 changed files with 137 additions and 134 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ A design task is worked by two architects. Design alone first, then exchange and
say plainly where you disagree. Do not concede just to agree.
Do only the assigned scope. Note anything outside that scope in one line and do not
investigate it further. Use `bridge_ask{question}` only when a decision belongs to
investigate it further. Use `fleet_ask{question}` only when a decision belongs to
the lead, such as an unclear requirement or two defensible fixes. Do not ask about
something you can decide by reading more code.
+1 -1
View File
@@ -10,7 +10,7 @@ git worktree and branch. Never check out, rebase onto, or push to `main`. Confir
the worktree root and branch before you edit. Use only paths under that root.
Do only the assigned scope. Note anything outside that scope in one line and do not
investigate it further. Use `bridge_ask{question}` only when a decision belongs to
investigate it further. Use `fleet_ask{question}` only when a decision belongs to
the lead, such as an unclear requirement or two defensible fixes. Do not ask about
something you can decide by reading more code.
+1 -1
View File
@@ -12,7 +12,7 @@ Read the whole assigned scope before judging it. Review only that scope. If you
something outside it, note it in one line and do not investigate it further. Do not
run the build. The owner makes changes and runs checks.
Use `bridge_ask{question}` only when a decision belongs to the lead, such as an
Use `fleet_ask{question}` only when a decision belongs to the lead, such as an
unclear requirement or two defensible fixes. Do not ask about something you can
decide by reading more code.
+3 -3
View File
@@ -5,7 +5,7 @@ description: Implementer-role procedure for a bridged worker — verify your wor
# Implementer worker — procedure
The turn contract (one `bridge_reply`, `bridge_ask` for the lead's decisions, honest reporting,
The turn contract (one `fleet_reply`, `fleet_ask` for the lead's decisions, honest reporting,
never merge, never commit `.mcp.json` or `wiki/`) is in **`CLAUDE.md` → Bridge communication →
Worker** and already applies. This skill is only the *implement-and-hand-off procedure*.
@@ -103,7 +103,7 @@ fix it if the cause is yours (e.g. branch not pushed yet), and report the failur
inventing a URL. If `GITEA_TOKEN` is unset your profile was not granted PR-create: push the branch
and report its name so the lead opens the PR.
## 6. Hand off — what goes in `bridge_reply`
## 6. Hand off — what goes in `fleet_reply`
The reply is the entire handoff; the lead cannot see your terminal.
@@ -130,7 +130,7 @@ sequenceDiagram
I->>G: git push -u origin HEAD
I->>G: POST /pulls (GITEA_TOKEN) — open PR to main
G-->>I: html_url
I->>L: bridge_reply(PR url, branch, files, tests)
I->>L: fleet_reply(PR url, branch, files, tests)
Note over L,G: the lead reviews the PR and merges on green — you never merge
```
+3 -3
View File
@@ -5,7 +5,7 @@ description: Reviewer-role procedure for a bridged worker — how to work a revi
# Reviewer worker — procedure
The turn contract (one `bridge_reply`, `bridge_ask` for the lead's decisions, honest reporting,
The turn contract (one `fleet_reply`, `fleet_ask` for the lead's decisions, honest reporting,
never merge) is in **`CLAUDE.md` → Bridge communication → Worker** and already applies. This
skill is only the *review procedure*: how to work the scope, and the exact shape of what you
send back.
@@ -24,13 +24,13 @@ wrong.
covers what it was given.
- Do **not** edit files or run the build. You review; the owner acts.
## 3. Reach for `bridge_ask` only for a genuine fork
## 3. Reach for `fleet_ask` only for a genuine fork
Ambiguous requirement, a missing acceptance criterion, "intended or a bug?", or two defensible
fixes with different consequences — those are the lead's call, and guessing produces a
confident-but-wrong finding. Anything you could settle by reading more code is yours to settle.
## 4. The finding — what goes in `bridge_reply`
## 4. The finding — what goes in `fleet_reply`
Report the **single most important** real issue in the scope, in these four lines, under
~90 words:
+1 -1
View File
@@ -13,7 +13,7 @@ A design task is worked by two architects. Design alone first, then exchange and
say plainly where you disagree. Do not concede just to agree.
Do only the assigned scope. Note anything outside that scope in one line and do not
investigate it further. Use `bridge_ask{question}` only when a decision belongs to
investigate it further. Use `fleet_ask{question}` only when a decision belongs to
the lead, such as an unclear requirement or two defensible fixes. Do not ask about
something you can decide by reading more code.
+1 -1
View File
@@ -10,7 +10,7 @@ git worktree and branch. Never check out, rebase onto, or push to `main`. Confir
the worktree root and branch before you edit. Use only paths under that root.
Do only the assigned scope. Note anything outside that scope in one line and do not
investigate it further. Use `bridge_ask{question}` only when a decision belongs to
investigate it further. Use `fleet_ask{question}` only when a decision belongs to
the lead, such as an unclear requirement or two defensible fixes. Do not ask about
something you can decide by reading more code.
+1 -1
View File
@@ -12,7 +12,7 @@ Read the whole assigned scope before judging it. Review only that scope. If you
something outside it, note it in one line and do not investigate it further. Do not
run the build. The owner makes changes and runs checks.
Use `bridge_ask{question}` only when a decision belongs to the lead, such as an
Use `fleet_ask{question}` only when a decision belongs to the lead, such as an
unclear requirement or two defensible fixes. Do not ask about something you can
decide by reading more code.
+16 -13
View File
@@ -34,8 +34,8 @@ flowchart LR
W["worker claude pane<br/>ANTHROPIC_BASE_URL set<br/>MCP client"]
M["llm.ltms.dev<br/>(the one gateway)"]
OPUS -->|"MCP bridge_send (blocks)"| SRV
W -.->|"MCP bridge_reply"| SRV
OPUS -->|"MCP fleet_send (blocks)"| SRV
W -.->|"MCP fleet_reply"| SRV
CLI -->|"Unix socket<br/>send_text · events.subscribe"| HERDR
HERDR -->|"drives PTY"| W
W -->|"inference"| M
@@ -51,13 +51,16 @@ flowchart LR
plain daemon (no Anthropic quota), so it may poll/subscribe freely.
- **One gateway (unified MCP setup):** `bridged` is the **sole communication path** for every
Claude session. Primary and workers each mount it as an MCP server (one `claude mcp add`
line, same on both) and talk over MCP tools — `bridge_send` / `bridge_reply` /
`bridge_status` (with `bridge_ask` planned for the blocked-worker path). **No Claude session
ever addresses a broker, a peer, or the network
directly**; any queue is `bridged`-internal. MCP tool I/O never sets `ANTHROPIC_BASE_URL`, so
mounting the bridge is subscription-safe by construction.
- **How the primary consumes a reply:** a single **blocking MCP call** (`bridge_send`);
`bridged` holds it open until the worker calls `bridge_reply` or its turn hits
line, same on both) and talk over MCP tools — `fleet_send` / `fleet_reply` /
`fleet_status` (with `fleet_ask` planned for the blocked-worker path). **No Claude session
ever addresses a broker, a peer, or the network
directly**; any queue is `bridged`-internal. MCP tool I/O never sets `ANTHROPIC_BASE_URL`, so
mounting the bridge is subscription-safe by construction.
**Tool naming:** the tools were renamed from `bridge_*` to `fleet_*` (CB-622). The daemon
still answers the old `bridge_*` names for one release, but they are deprecated — use the
`fleet_*` names.
- **How the primary consumes a reply:** a single **blocking MCP call** (`fleet_send`);
`bridged` holds it open until the worker calls `fleet_reply` or its turn hits
`agent_status=done`, then returns the reply as the tool result. No cross-turn busy-poll, so
no quota burn. SSE is an optional side-channel for humans/dashboards watching status.
- **Worker → primary** rides `bridged`'s **MCP rendezvous** — the reply resolves the primary's
@@ -98,9 +101,9 @@ tests run separately via `mvn test -Pcontract`):
- **Core gateway** — herdr socket client (contract-tested vs live 0.7.0); guard-checked worker
spawn with `ANTHROPIC_BASE_URL` injected only into the worker's env; status-gated injector;
blocking `bridge_send` with reply rendezvous; MCP server as a thin adapter over the REST core.
- **MCP tools** — `bridge_send` / `bridge_reply` / `bridge_status` (messaging) and `bridge_spawn`
/ `bridge_list` / `bridge_stop` / `bridge_profiles` / `bridge_poll` (fleet). Caller identity is
blocking `fleet_send` with reply rendezvous; MCP server as a thin adapter over the REST core.
- **MCP tools** — `fleet_send` / `fleet_reply` / `fleet_status` (messaging) and `fleet_spawn`
/ `fleet_list` / `fleet_stop` / `fleet_profiles` / `fleet_poll` (fleet). Caller identity is
connection-based (loopback peer PID → herdr pane), so the same mount serves primary and workers.
- **Delivery reliability** — completion fallback (a confirmed `working→idle` turn resolves a
send); async fire-and-poll (beats the caller's MCP call timeout for long tasks); and failure
@@ -108,7 +111,7 @@ tests run separately via `mvn test -Pcontract`):
- **Fleet** — multiple worker profiles, each with an independent base_url guard check; workers
inherit the primary's working directory (never `$HOME`); a readiness gate holds delivery until
a worker's Claude has connected the bridge MCP (no paste lost into its boot window).
- **Blocked-worker path** — `bridge_ask` reverse rendezvous: a worker pauses its delegated turn to
- **Blocked-worker path** — `fleet_ask` reverse rendezvous: a worker pauses its delegated turn to
ask the primary and resumes the *same* turn with the answer (CB-205).
- **Session lifecycle** — session manager with spawn/reuse/recycle, `idle_ttl` reaper, `context_cap`,
and graceful drain on shutdown (CB-301/CB-303); per-worker git worktrees on their own branch with
+12 -12
View File
@@ -5,10 +5,10 @@
## Why this exists
Stage 2 gave a worker→primary reply a **durable place to wait** when no `bridge_send` is
Stage 2 gave a worker→primary reply a **durable place to wait** when no `fleet_send` is
open: it lands in `agent.<target>.inbox` on the broker and survives a daemon bounce. But
delivery is still **pull** — the primary only sees the reply if it happens to call
`bridge_poll(target)` / `GET /sessions/{id}/replies`. A reply can sit indefinitely while
`fleet_poll(target)` / `GET /sessions/{id}/replies`. A reply can sit indefinitely while
the primary works on something else.
This layer makes delivery **active**: the bridge *pushes* a nudge to the primary the moment
@@ -26,11 +26,11 @@ pointed at the primary's pane instead.
```mermaid
flowchart LR
W["worker"] -->|"bridge_reply (no open send)"| MS["MessageService.reply"]
W["worker"] -->|"fleet_reply (no open send)"| MS["MessageService.reply"]
MS -->|"inbox.publish"| INBOX[("agent.&lt;target&gt;.inbox<br/>(durable, LavinMQ)")]
MS -->|"notify"| LOOP["ReplyPushLoop"]
LOOP -->|"status-gated inject"| PANE["primary's herdr pane"]
PANE -->|"primary drains"| DRAIN["bridge_poll(target)<br/>= peek + ack"]
PANE -->|"primary drains"| DRAIN["fleet_poll(target)<br/>= peek + ack"]
DRAIN -->|"inbox now empty"| LOOP
LOOP -.->|"still non-empty →<br/>re-inject on backoff"| PANE
classDef store fill:#2c5282,stroke:#1a365d,color:#ffffff;
@@ -51,11 +51,11 @@ the caller runs in a herdr pane on this host. Today it's discarded for the prima
(`presence.markPresent` is a no-op on it).
**Plan:** a single-slot `PrimaryRegistry` (thread-safe) holding the primary's `terminal_id`.
Populate it from the **orchestration-side** MCP tools — `bridge_send`, `bridge_spawn`,
`bridge_poll`, `bridge_list`, `bridge_status`, `bridge_profiles` — capturing
Populate it from the **orchestration-side** MCP tools — `fleet_send`, `fleet_spawn`,
`fleet_poll`, `fleet_list`, `fleet_status`, `fleet_profiles` — capturing
`callerTerminal(exchange)` when it is (a) non-null and (b) **not** a registered worker
session in `SessionManager`. That caller is, by construction, the primary. Worker-side tools
(`bridge_reply`, `bridge_ask`) never set it.
(`fleet_reply`, `fleet_ask`) never set it.
- **Config override / pin:** a `primary: { terminal: "<id>" }` block in `BridgedConfig`
(nested record, same shape as `Broker`). Lets an operator pin it, or supply it when
@@ -71,13 +71,13 @@ A `ReplyPushLoop` component, notified at the single no-waiter call site
(`MessageService.reply` → the `inbox.publish` branch, `MessageService.java:192`).
- **Inject a nudge, not the payload.** The injected turn tells the primary *to drain*
(e.g. "Worker `<target>` returned a reply — run `bridge_poll(target=<target>)` to collect
(e.g. "Worker `<target>` returned a reply — run `fleet_poll(target=<target>)` to collect
it"), it does **not** carry the reply text. Rationale: replies can be large/multiline and
terminal injection would mangle them; the drain response is the clean transport. Keeps the
push idempotent — re-nudging is harmless.
- **Ack = drain.** The primary draining (`drainReplies` = peek + ack) is the acknowledgement.
The loop's **stop condition is `inbox.peek(target).isEmpty()`** — the reply is gone from the
inbox because it was acked. No new `bridge_ack` tool needed for v1 (see Increment 3).
inbox because it was acked. No new `fleet_ack` tool needed for v1 (see Increment 3).
- **Status-gated injection (mechanism (b), chosen).** A dedicated lightweight scheduled loop,
**not** the worker `Injector`. It injects via `AgentControl.send(primaryTerminal, nudge)`
(the same herdr `agent.send` = `pane send-text` + submit that delivers to workers) only when
@@ -93,10 +93,10 @@ A `ReplyPushLoop` component, notified at the single no-waiter call site
the reply remains in the durable inbox and the next natural poll (or a later worker reply's
nudge) still surfaces it. Bounded so the bridge never spams the primary.
### Increment 3 — optional per-`msgId` `bridge_ack` tool (deferred)
### Increment 3 — optional per-`msgId` `fleet_ack` tool (deferred)
Drain-as-ack is coarse: it clears *all* pending replies for a target at once. If finer
control is ever needed (ack one reply, leave others held), add a `bridge_ack(msgId)` tool
control is ever needed (ack one reply, leave others held), add a `fleet_ack(msgId)` tool
mapping to `inbox.ack(target, msgId)` — the port already supports per-`msgId` ack. Not built
in v1; the stop-on-empty loop is sufficient.
@@ -132,7 +132,7 @@ boundary**. The bridge is signalling the primary that it has mail — not drivin
non-null terminal AND not a registered session" predicate; the loop's stop-on-empty and
bounded-reminder logic with an injected clock + a fake injector (no real herdr).
- **Live dogfood (primary-side):** with the daemon on the broker jar + a real worker,
delegate a task, let the worker reply after the `bridge_send` window closes, and observe the
delegate a task, let the worker reply after the `fleet_send` window closes, and observe the
bridge inject a drain nudge into *this* primary pane; confirm draining stops the reminders;
confirm an unreachable primary (registry empty) degrades to pull with no loss.
+4 -4
View File
@@ -15,7 +15,7 @@ Consequences today:
since when?" without shelling to herdr for a raw agent list (no state, no ownership, no age).
- Cleanup of a worker that outlived its owning process depends entirely on the boot-time
name-nonce **reaper** (CB-117) — there is no live, authoritative roster during a run.
- `bridge_list` (CB-304) can only surface herdr's view, not a bridge-owned roster.
- `fleet_list` (CB-304) can only surface herdr's view, not a bridge-owned roster.
- There is no seam for per-session policy (checkpoint on teardown → CB-302; idle_ttl /
context_cap / drain → CB-303).
@@ -100,9 +100,9 @@ final class SessionManager {
`TurnListener` alongside `CompletionResolver` so it sees turn boundaries, and give it the
`WorkerPresence` signal for `READY`.
- **`BridgeMcp.spawn` / `BridgedApp.spawnWorker`** — route spawn through `SessionManager.acquire`
(carry `callerTerminal` as `ownerTerminal`). **`bridge_stop` / `DELETE /workers/{paneId}`** →
(carry `callerTerminal` as `ownerTerminal`). **`fleet_stop` / `DELETE /workers/{paneId}`** →
`SessionManager.release`.
- **`bridge_list` / `GET /sessions` (CB-304 later)** — read `SessionManager.roster()`.
- **`fleet_list` / `GET /sessions` (CB-304 later)** — read `SessionManager.roster()`.
- **`MessageService`** — no change required for one-shot; a later CB-303 auto-release hook can call
`release` from `onTurnComplete` under policy.
@@ -121,4 +121,4 @@ final class SessionManager {
- **CB-302** — attach a checkpoint step (`STATE.md` + commit) to the `release` path.
- **CB-303** — a policy loop over `roster()` using `spawnedAtNanos`/state to auto-`release` on
`idle_ttl`, or drain on `context_cap`.
- **CB-304** — `bridge_list` reads `roster()` for a bridge-owned roster + live join.
- **CB-304** — `fleet_list` reads `roster()` for a bridge-owned roster + live join.
+2 -2
View File
@@ -3,7 +3,7 @@
**Status:** ✅ shipped — implemented at commit `97ecc71` (per-worker git worktree + config-parity
overlay). As-built: `session/GitWorktrees.java` behind the `Worktrees` port, wired in
`Bridged.main` and configurable via `worktreeRoot` / per-profile `parityOverlay`
(see `bridged.example.yaml`). Branch/worktree surface in `bridge_list` landed with CB-304
(see `bridged.example.yaml`). Branch/worktree surface in `fleet_list` landed with CB-304
(`9fe04bf`); the worker-opened-PR checkpoint landed as CB-302 (`64e70ef`).
**Extends:** [CB-301 Session Manager](CB-301-Session-Manager.md) (shipped, commit `54d907c`).
**Realizes:** the config-parity requirement in [Worker Git Workflow](Worker-Git-Workflow.md).
@@ -122,7 +122,7 @@ public void release(String paneId) {
### Surface: MCP + REST
- `bridge_spawn` gains an optional `worktree` arg: `true`, or a ticket slug string. Truthy ⇒ build a
- `fleet_spawn` gains an optional `worktree` arg: `true`, or a ticket slug string. Truthy ⇒ build a
`WorktreeRequest(slug, null)` and call the 5-arg `acquire`.
- `POST /workers` gains `worktree` (+ optional `ticket`) in the body/query, same mapping.
- `workerView`/`view(WorkerSession)` include `worktree` and `branch` **when non-null** (omit for
+4 -4
View File
@@ -6,11 +6,11 @@
## 1. Problem
`bridge_spawn` today returns a session the instant the herdr pane is started. The pane is not
`fleet_spawn` today returns a session the instant the herdr pane is started. The pane is not
yet a usable Claude REPL — it may still be sitting at the folder-trust prompt, or the CLI may
never come up at all. Nothing blocks or times out on that. Consequences:
- A `bridge_send` to a not-yet-ready worker surfaces as a **~60 s MCP-client timeout** (the send
- A `fleet_send` to a not-yet-ready worker surfaces as a **~60 s MCP-client timeout** (the send
blocks waiting for a turn that can't start) instead of a fast, explicit spawn failure.
- A worker stuck at the folder-trust prompt lingers in `SPAWNING` forever; nothing fails it.
@@ -99,11 +99,11 @@ Jackson ignores unknown keys, so omitting them in existing YAML is safe; pick sa
`spawn` throws — verify the new exception flows through it (worktree removed, nothing registered).
- The **non-worktree** path registers the session only *after* `spawn` returns, so a throw means no
half-live `SPAWNING` session is ever registered — confirm this and add a test.
- `bridge_spawn` (MCP verb) must return an **error result** carrying the exception message, not a
- `fleet_spawn` (MCP verb) must return an **error result** carrying the exception message, not a
success with a dead session. Trace `BridgeMcp`/`BridgedApp` spawn handlers and make sure the
exception becomes a clean tool error, not an uncaught 500 with a stack trace.
**Out of scope (do NOT do here):** gating `bridge_send` on session `READY` (existing status-gate +
**Out of scope (do NOT do here):** gating `fleet_send` on session `READY` (existing status-gate +
this spawn gate already close the window), MCP-handshake-as-readiness signal, the CB-307 broker,
any config `kind:` discriminator, any second adapter.
+8 -8
View File
@@ -13,7 +13,7 @@ Stage 2 (the AMQP/LavinMQ adapter behind the same port) is explicitly **out of s
## 1. The bug this fixes (grounded in current code)
The reverse (worker→primary) path is `Rendezvous` — a `ConcurrentHashMap<session, CompletableFuture<Resolution>>`
of **live blocking waiters only**. No queue, no store. When a worker calls `bridge_reply` and **no send
of **live blocking waiters only**. No queue, no store. When a worker calls `fleet_reply` and **no send
is currently open** for that worker:
- `Rendezvous.resolve(session, content)` → `complete(...)` → `waiters.get(session) == null` →
@@ -22,7 +22,7 @@ is currently open** for that worker:
`BridgeMcp.reply` returns `error("no send is awaiting a reply for this worker")` (`mcp/BridgeMcp.java:270-272`);
REST returns `409 no_pending_send` (`rest/BridgedApp.java:339-345`).
This is the observed "communication break": a worker that finishes just after its `bridge_send` timed
This is the observed "communication break": a worker that finishes just after its `fleet_send` timed
out (the ~60s sync window) replies into the void. There is **no message-id, dedup, or ack** anywhere in
the message path today.
@@ -79,7 +79,7 @@ in `MessageService`, which already owns the `Rendezvous` and will own the `Reply
- Add `MessageService.reply(String session, String content)`:
```java
/** Route a worker's explicit bridge_reply: resolve an open send, or queue it in the inbox if none. */
/** Route a worker's explicit fleet_reply: resolve an open send, or queue it in the inbox if none. */
public boolean reply(String session, String content) {
if (rendezvous.resolve(session, content)) {
return true; // a live send took it — unchanged fast path
@@ -94,7 +94,7 @@ in `MessageService`, which already owns the `Rendezvous` and will own the `Reply
- `BridgedApp.replyMessage` (`rest/BridgedApp.java:330-346`) — return `200` (queued) instead of
`409 no_pending_send`.
**DO NOT touch the QUESTION path.** `bridge_ask` / `rendezvous.resolveQuestion` must keep today's
**DO NOT touch the QUESTION path.** `fleet_ask` / `rendezvous.resolveQuestion` must keep today's
`NO_WAITER` behaviour — a mid-turn question is **interactive** (the worker blocks synchronously and cannot
consume a late answer), so it must **never** be queued. Only terminal `REPLY`s go to the inbox.
@@ -110,11 +110,11 @@ The primary re-checks a worker it delegated to. Expose a drain keyed by **worker
- Add `MessageService.drainReplies(String target)`: `peek` the inbox, `ack` each returned `msgId`, hand
back the `List<InboxMessage>` (or just the contents). At-least-once: peek→deliver→ack (ack only after
the caller has them, so an in-flight failure re-surfaces them).
- **DECISION (required default): expose via the existing poll verb, keyed by target.** Extend `bridge_poll`
- **DECISION (required default): expose via the existing poll verb, keyed by target.** Extend `fleet_poll`
to accept an optional `target` (worker session) and, when present, return that worker's drained replies —
alongside a matching REST route `GET /sessions/{id}/replies`. Do **not** change `send`/`answer` semantics
(do not drain inside `send` — that conflates "deliver to worker" with "collect its mail"). Keep the
existing ticket-based `bridge_poll(ticket)` path working unchanged. If you see a cleaner surface, still
existing ticket-based `fleet_poll(ticket)` path working unchanged. If you see a cleaner surface, still
ship this default and note the alternative for review.
## 3. Config
@@ -126,11 +126,11 @@ Stage-2 AMQP adapter: absent → in-memory, present → AMQP).
## 4. Acceptance criteria (what the primary will verify)
1. New `ReplyInbox` + `InboxMessage` + `InMemoryReplyInbox` in `dev.ltms.bridged.msg`.
2. `bridge_reply` with **no open send** now **succeeds and queues** (no more `error` / `409`); the reply is
2. `fleet_reply` with **no open send** now **succeeds and queues** (no more `error` / `409`); the reply is
later retrievable and identical.
3. The queued reply is drainable by the primary keyed by target; draining **acks** it (a second drain
returns nothing); dedup by `msgId` (re-publishing the same id does not double-queue).
4. **QUESTION path unchanged** — `bridge_ask` with no open send still returns `NO_WAITER` (add/keep a test
4. **QUESTION path unchanged** — `fleet_ask` with no open send still returns `NO_WAITER` (add/keep a test
proving a question is never queued).
5. Completion/failure fallbacks unchanged.
6. Unit tests covering: `InMemoryReplyInbox` publish/peek/ack/dedup/FIFO/concurrency; `MessageService.reply`
+6 -6
View File
@@ -111,7 +111,7 @@ host's terminals.*
```mermaid
flowchart LR
send["bridge_send(globalId, msg)"] --> lookup{"directory:<br/>is globalId local?"}
send["fleet_send(globalId, msg)"] --> lookup{"directory:<br/>is globalId local?"}
lookup -->|"yes"| local["inject via local herdr<br/>(today's Injector path)"]
lookup -->|"no"| pub["publish agent.&lt;id&gt;.inbox<br/>(broker routes to owning gateway)"]
pub --> consume["owning gateway consumes<br/>→ injects into its local herdr"]
@@ -158,17 +158,17 @@ sequenceDiagram
participant BR as broker
participant GA as gateway A
participant P as primary (host A, MCP client)
W->>GB: bridge_reply
W->>GB: fleet_reply
GB->>BR: publish primary-bound (durable, msg id)
BR->>GA: route to A's primary inbox
Note over GA: held durably until the primary pulls
P->>GA: blocking bridge_send resolves / bridge_poll
P->>GA: blocking fleet_send resolves / fleet_poll
GA-->>P: reply (then ACK to broker)
```
*Figure 4 — the broker makes the middle hop lossless, ordered, and idempotent; the **final** hop
into the primary is still a **pull** (gateway A holds the message until the primary's blocking
`bridge_send` or `bridge_poll`). Cross-host neither improves nor worsens this — it just spans hosts.
`fleet_send` or `fleet_poll`). Cross-host neither improves nor worsens this — it just spans hosts.
This is precisely the gap CB-307 closes on one host and CB-308 stretches across hosts.*
## 6. Staging & dependencies
@@ -207,7 +207,7 @@ they overlap (notably: the envelope is no longer optional, and dedup is split by
gateway's host. This extends the single-host invariant — *identity comes from the connection,
never an argument* — across the broker: cross-host, identity comes from the key. Complements
(not replaces) per-gateway broker logins over TLS.
2. **Profiles are owned by the worker's host.** `bridge_spawn(profile, host)` resolves the name in
2. **Profiles are owned by the worker's host.** `fleet_spawn(profile, host)` resolves the name in
the *target* gateway's `bridged.yaml`. Gateways advertise their profile names in presence
heartbeats, so a leader sees what each host offers before spawning; an unknown name is a clear
error from the target. Secrets (base URLs, tokens) never leave the host that uses them.
@@ -259,7 +259,7 @@ they overlap (notably: the envelope is no longer optional, and dedup is split by
serialize acks fleet-wide. Ordering caveat: a *return* (unroutable) arrives **before** the
confirm, so "confirmed" ≠ "routed"; the sender checks the returned-set at confirm time.
`mandatory` is false only for `BROADCAST`, where an empty group is legal silence.
10. **Queue lifecycle is session lifecycle.** `bridge_stop`/reap deletes the worker's inbox queue
10. **Queue lifecycle is session lifecycle.** `fleet_stop`/reap deletes the worker's inbox queue
(its `broadcast.*` bindings die with it — no broadcasts to the dead); `x-expires` collects
queues orphaned by a crashed gateway (long for main/orchestrator inboxes, short for workers).
Queue names carry a version suffix (`.v2`): AMQP refuses to redeclare an existing durable
+3 -3
View File
@@ -7,7 +7,7 @@ the core learning any one peer's environment.
## 1. Why
`claude-bridge` is a **communication bus between heterogeneous AI agents** — its stable surface is
the protocol (`bridge_spawn / send / poll / reply / ask / list / stop / status`), and that surface
the protocol (`fleet_spawn / send / poll / reply / ask / list / stop / status`), and that surface
should stay provider-neutral. Today the daemon can only materialize one kind of peer: an
off-subscription Claude Code CLI over herdr. Everything specific to *how that peer is set up*
(`ANTHROPIC_BASE_URL`, the subscription guard, `--mcp-config`/system-prompt flags, `claude-*`
@@ -126,7 +126,7 @@ existing configs. (Jackson already ignores unknown keys, so adding `kind` is bac
```mermaid
sequenceDiagram
participant MCP as bridge_spawn (MCP/REST)
participant MCP as fleet_spawn (MCP/REST)
participant SM as SessionManager
participant L as PeerLauncher (by profile.kind)
participant T as transport (herdr)
@@ -150,7 +150,7 @@ degrades gracefully when a launcher lacks one:
| Capability | Meaning | Claude Code | Codex (likely) | Human |
|---|---|---|---|---|
| `MID_TURN_ASK` | supports `bridge_ask` rendezvous | ✓ | ? | ✗ |
| `MID_TURN_ASK` | supports `fleet_ask` rendezvous | ✓ | ? | ✗ |
| `SELF_PR` | can open its own PR at checkpoint (CB-302) | ✓ (opt-in token) | ? | ✗ |
| `WORKTREE` | can run in a provisioned git worktree | ✓ | ✓ | ✗ |
| `ORPHAN_REAP` | spawner can reconcile orphaned peers on boot | ✓ | ? | ✗ |
+6 -6
View File
@@ -184,7 +184,7 @@ sequenceDiagram
participant O as OpenCodeLauncher
participant B as HerdrPeerLauncher (base)
participant H as herdr
P->>M: bridge_spawn(profile="oc-impl")
P->>M: fleet_spawn(profile="oc-impl")
M->>C: spawn(SpawnRequest)
C->>C: kind(profile)=="opencode"
C->>O: spawn(req)
@@ -226,11 +226,11 @@ until it is "major" (Stage-B whole), per the CB-401 bar.
- **opencode TUI ⇄ herdr injection.** herdr drives a pane by typing into a TUI. Must confirm
opencode's TUI accepts injected keystrokes/submit the way `claude` does, and reaches an
`injectable` status the CB-306 gate recognizes. *Validation:* spawn one opencode worker,
watch the readiness gate pass, `bridge_send` a trivial task.
watch the readiness gate pass, `fleet_send` a trivial task.
- **Bridge MCP visibility in opencode.** Confirm `OPENCODE_CONFIG` (or `opencode mcp add`)
actually surfaces the `bridge_*` tools inside the opencode session, and that `bridge_reply`
actually surfaces the `fleet_*` tools inside the opencode session, and that `fleet_reply`
is callable — the reply-charter is worthless if the tool isn't mounted. *Validation:* the
worker completes a task by calling `bridge_reply`; the reply lands via the CB-307 path.
worker completes a task by calling `fleet_reply`; the reply lands via the CB-307 path.
- **opencode MCP/config schema drift.** opencode is fast-moving (1.1.31 today). Pin the config
schema we generate against the installed version; treat the exact keys (`type: "remote"` vs
`"http"`, `instructions` shape) as a dogfood-verified fact, not an assumption.
@@ -281,7 +281,7 @@ as a dogfood-verified fact for 1.18.5.
| §5 risk | Result |
|---|---|
| opencode TUI ⇄ herdr injection; CB-306 gate | ✅ `peer pane=wD:p3 reached injectable state` ~0.6s after `agent.start` |
| Bridge MCP visible + `bridge_reply` callable | ✅ MCP `initialize` from `Implementation[name=opencode, version=1.18.5]`; worker replied through the tool |
| Bridge MCP visible + `fleet_reply` callable | ✅ MCP `initialize` from `Implementation[name=opencode, version=1.18.5]`; worker replied through the tool |
| Config schema drift (1.1.31 → 1.18.5) | ✅ unchanged, see above |
| Provider credentials | ✅ free tier, zero credentials |
@@ -291,7 +291,7 @@ Full lifecycle exercised through the REST surface:
to `OpenCodeLauncher` (`spawning opencode profile=opencode-free`), pane `wD:p3`.
2. Readiness: `{"ready":true,"status":"idle"}`, roster state `ready`.
3. `POST /sessions/{id}/message` → **`{"replySource":"reply","reply":"391"}`** — a *structured*
`bridge_reply`, not the CB-115 completion-fallback transcript scrape. The clean path.
`fleet_reply`, not the CB-115 completion-fallback transcript scrape. The clean path.
4. `DELETE /workers/wD:p3` → `204`, roster empty, tolerant teardown (`tab_not_found` ignored —
opencode had already closed its own tab).
+8 -8
View File
@@ -45,7 +45,7 @@ flowchart TB
w1["worker pane (gx00 vLLM)"]
w2["worker pane (ollama)"]
human --> primary
primary -->|"bridge_send / spawn / ask"| daemon
primary -->|"fleet_send / spawn / ask"| daemon
daemon --> comp
comp --> cc
comp --> oc
@@ -150,7 +150,7 @@ sequenceDiagram
participant SL as SandboxLauncher
participant SB as sandbox (peer-owned)
participant A as agent in sandbox
M->>D: bridge_spawn(profile=backend, role=backend)
M->>D: fleet_spawn(profile=backend, role=backend)
D->>SL: spawn(SpawnRequest)
SL->>SB: start entrypoint (image = backend role)
Note over SL,SB: bridge injects guarded ANTHROPIC_BASE_URL,<br/>mounts bridge MCP url + reply charter
@@ -213,12 +213,12 @@ sequenceDiagram
participant MA as main A (Opus)
participant BR as bridged / broker
participant MB as main B (cloud)
MA->>BR: bridge_send(to = main B, msg)
MA->>BR: fleet_send(to = main B, msg)
BR->>BR: publish agent.B.inbox (durable, msg id)
Note over BR: held until B pulls (B is a client too)
MB->>BR: blocking bridge_send / poll resolves
MB->>BR: blocking fleet_send / poll resolves
BR-->>MB: msg (then ACK)
MB->>BR: bridge_reply(to = main A)
MB->>BR: fleet_reply(to = main A)
BR->>BR: publish agent.A.inbox
MA->>BR: poll resolves
BR-->>MA: reply
@@ -287,7 +287,7 @@ sequenceDiagram
H->>O: high-level goal (large context)
O->>O: name/resume main A session
O->>MA: task + SCOPED context slice (not the whole history)
MA->>W: bridge_send(delegation, carrying only the relevant slice)
MA->>W: fleet_send(delegation, carrying only the relevant slice)
W-->>MA: result
MA-->>O: rollup
O->>O: fold into orchestrator context, pick next main/turn
@@ -440,13 +440,13 @@ sequenceDiagram
participant BR as broker
participant GB as gateway B
participant SB as sandbox agent (host B, container)
MA->>GA: bridge_send(globalId on B, msg)
MA->>GA: fleet_send(globalId on B, msg)
GA->>GA: directory lookup - is globalId local? NO
GA->>BR: publish agent.ID.inbox (durable)
BR->>GB: route to the owning gateway
GB->>SB: inject via B's LOCAL herdr (keystrokes)
Note over GB,SB: SandboxLauncher already spawned the container -<br/>its PTY is in B's herdr, CB-306 readiness passed
SB-->>GB: bridge_reply (to B's LOCAL MCP endpoint)
SB-->>GB: fleet_reply (to B's LOCAL MCP endpoint)
GB->>BR: publish primary-bound (durable, msg id)
BR->>GA: route back to A
Note over GA: held until the main pulls (the main is a client)
+7 -7
View File
@@ -196,7 +196,7 @@ every member spawn. The wiki keeps the direct route open precisely because "if t
nothing that matters is blocked."
So keep it. Add a second profile `local-direct` pointing at `http://gx00.gw:8000` with **`weight: 0`**
— never auto-selected, still spawnable with an explicit `bridge_spawn{profile: "local-direct"}`.
— never auto-selected, still spawnable with an explicit `fleet_spawn{profile: "local-direct"}`.
That is exactly what CB-554 made `weight: 0` mean, and it turns the escape hatch into something the
lead can actually reach during an incident.
@@ -266,7 +266,7 @@ Each of these cost someone real debugging time upstream. They apply to us.
1. **Rotating a token restarts the auth proxy, which drops in-flight streaming responses.** For us
that means rotating `AI_GATEWAY_TOKEN` kills every live member mid-turn, and an async ticket's
report goes with it. This is the same rule as a daemon redeploy: **drain the fleet first**
(`bridge_list` → `bridge_poll` anything wanted → `bridge_stop`), then rotate.
(`fleet_list` → `fleet_poll` anything wanted → `fleet_stop`), then rotate.
2. **The gateway's own `SecurityPolicy` fails open.** Standalone `aigw run` accepts it and silently
ignores it — an unauthenticated request returned **200**. Auth is the Caddy proxy in front, and
nothing else. Never reason as if the gateway authenticates.
@@ -283,10 +283,10 @@ Each of these cost someone real debugging time upstream. They apply to us.
Merging config is not proving it. The checks, in order:
1. `bridge_spawn{profile: "gx"}` succeeds and the member completes a real turn ending in
`bridge_reply`. This is the first proof of the token, the URL and the model name, and it risks
1. `fleet_spawn{profile: "gx"}` succeeds and the member completes a real turn ending in
`fleet_reply`. This is the first proof of the token, the URL and the model name, and it risks
nothing the fleet depends on.
2. `bridge_spawn{profile: "local"}` succeeds. If the guard allowlist was missed, this **throws** — a
2. `fleet_spawn{profile: "local"}` succeeds. If the guard allowlist was missed, this **throws** — a
loud, self-correcting failure, which is the good kind. If the restart was missed, it also throws,
for the same reason.
3. A `local` member completes a turn. That exercises streaming through two TLS edges, the auth proxy
@@ -297,9 +297,9 @@ Merging config is not proving it. The checks, in order:
deltas. For `gx` on `/v1`, this answers the open question in §3 rather than assuming it.
5. The cockpit at `auth.ltms.dev` shows requests counted against the `claude-bridge` consumer, not
`legacy`. That is the whole point of taking our own token.
6. `bridge_spawn{profile: "local-direct"}` still works, so the escape hatch is real rather than
6. `fleet_spawn{profile: "local-direct"}` still works, so the escape hatch is real rather than
theoretical.
7. `bridge_list` shows `gx` carrying no `credentialId`, so a `sol`/`terra` exhaustion cannot
7. `fleet_list` shows `gx` carrying no `credentialId`, so a `sol`/`terra` exhaustion cannot
quarantine it. This is the single-point-of-failure claim in §3b, checked rather than asserted.
---
+1 -1
View File
@@ -172,7 +172,7 @@ The current split is the mirror image of what you'd expect:
| REST routes | ❌ **none at all** — the session id is taken from the URL path and trusted | ❌ none |
So REST is the *more* exposed surface: `POST /sessions/{id}/reply` accepts any `{id}` from the
path, whereas the MCP `bridge_reply` derives the worker from the connection and refuses to read it
path, whereas the MCP `fleet_reply` derives the worker from the connection and refuses to read it
from an argument. Loopback-only bind is what makes this safe today.
**Therefore CB-505 must enforce on both paths against one shared resolver** — not at a single
+19 -19
View File
@@ -1,7 +1,7 @@
# M4 - Fleet health, recovery, routing, and capacity
**Status:** Design accepted on 2026-08-15. CB-573 part 1 has shipped the classification model and
the `bridge_list` capacity view; the remaining M4 units are not yet shipped. See
the `fleet_list` capacity view; the remaining M4 units are not yet shipped. See
[Unit 2 — what has landed so far](#unit-2---what-has-landed-so-far) before planning Unit 2 work:
some of its criteria were met by separate CB tickets, and one of them contradicts the unit text.
**Scope:** Fleet evidence, safe mechanical repair, lead routing, capacity reporting, and optional
@@ -145,7 +145,7 @@ Idle may drive configured resource cleanup. It never opens an incident and never
| `TURN_BOUNDARY_LOST` | Same session turn stays `BUSY`; same accepted task stays open; two raw snapshots show `IDLE` or `DONE` | Strong disagreement. Strict reconciliation may repair it. |
| `ERROR_ON_SCREEN` | Suspicious non-working state survives grace; `detection` matches a tested adapter-specific fatal signature | Certain only for the matched signature. A bare word such as `Exception` is not enough. |
| `STALL_SUSPECTED` | Open turn is older than the configured threshold; two normalised `recent_unwrapped` digests are unchanged; no boundary or reply occurs | Not certain. A long valid API call can look the same. Lead decides. |
| `MUTE` | Turn resolves through completion fallback instead of `bridge_reply` | Certain that no structured reply won. It does not prove an MCP failure. A single event is a metric, not an incident. |
| `MUTE` | Turn resolves through completion fallback instead of `fleet_reply` | Certain that no structured reply won. It does not prove an MCP failure. A single event is a metric, not an incident. |
| `REPLY_STRANDED` | Typed reply or health message remains after owning-lead push reaches its cap | Collection failed. This does not explain whether the lead is busy, dead, or ignoring the nudge. |
| `DELEGATION_ORPHANED` | Target is gone, failed, or released, but one or more tasks remain `PENDING` after reconciliation grace | Certain bridge invariant failure. This is not an inbox-drain fault. |
| `WORK_PRODUCT_AT_RISK` | Provisioned branch has commits after its recorded base; member is `DONE`, `FAILED`, or preserved after release; no turn or inbox item remains; long-idle threshold passed | A warning, not proof of loss. Work may already have an open pull request or a squash merge. |
@@ -230,8 +230,8 @@ Only the lead may:
- choose how to use partial work in a worktree;
- restart herdr or change network, model, credentials, backend, or configuration.
Reports include literal safe tool calls such as `bridge_status(sessionId="...")`,
`bridge_poll(ticket="...")`, `bridge_list()`, and optional `bridge_stop(paneId="...")`. A judgement
Reports include literal safe tool calls such as `fleet_status(sessionId="...")`,
`fleet_poll(ticket="...")`, `fleet_list()`, and optional `fleet_stop(paneId="...")`. A judgement
state never presents stop as the only action.
### 5.3 Release causes and worktree safety
@@ -251,7 +251,7 @@ preserving cause.
Before abnormal release removes the live session, M4 writes an atomic manifest under the worktree
root. It records session identity, owner, role, profile, repository, path, branch, base commit,
release cause, release time, state, and pending task ids. `bridge_list.preservedWorktrees` loads these
release cause, release time, state, and pending task ids. `fleet_list.preservedWorktrees` loads these
manifests after restart. Stop output and WARN logs also name the path and cause. M4 never
auto-deletes a preserved worktree.
@@ -348,7 +348,7 @@ Invalid known-format data is copied byte-for-byte to durable queue
before the original is acknowledged. A failed quarantine handoff leaves the original unacknowledged.
The raw body never enters logs.
Decode failure creates a redacted WARN, metric, `bridge_list` summary, and routed health incident.
Decode failure creates a redacted WARN, metric, `fleet_list` summary, and routed health incident.
One bad entry never escapes the consumer callback and never stops later valid messages.
Safe downgrade is not supported. The previous build ignores `content_type` and would show typed JSON
@@ -399,13 +399,13 @@ Choose the candidate with the fewest assigned foreign incidents. Break ties by s
then terminal id. Pin the recipient. Reassign only if that peer becomes unhealthy or retires. A
routing generation marks a reassignment, and old pending assignments become superseded.
`bridge_list` lead rows show health, health age, assigned foreign incident count, and a bounded list
`fleet_list` lead rows show health, health age, assigned foreign incident count, and a bounded list
of incident id, subject, state, severity, age, and routing generation. The top-level view also shows
owner, recipient, and routing reason.
A peer incident is published under the recipient lead's inbox key, not the failed subject's key. Its
status-gated nudge names the failed lead and gives the exact
`bridge_poll(target="<recipient-terminal>")` call.
`fleet_poll(target="<recipient-terminal>")` call.
### 7.5 Lead inbox ownership
@@ -427,7 +427,7 @@ An idle, reachable lead may receive the existing bounded nudge. An unreachable o
has no safe in-loop recovery. The bridge must not restart or replace it. A new lead would not have the
failed lead's plan or context, and an uncertain relaunch could create two orchestrators.
With no webhook, only `bridge_list`, `/healthz`, metrics, WARN logs, and the incident journal remain.
With no webhook, only `fleet_list`, `/healthz`, metrics, WARN logs, and the incident journal remain.
These are passive surfaces. They are not a human notification.
## 8. Lost-boundary reconciliation
@@ -521,7 +521,7 @@ task poll source, and metrics. The lead sees:
```text
[repaired completion - bridged detected a lost turn boundary. The member did not call
bridge_reply; pane-derived text follows and may be partial]
fleet_reply; pane-derived text follows and may be partial]
```
Clipped text also keeps the existing clipped-tail marker.
@@ -551,7 +551,7 @@ failure operation once. It never recreates the task.
Capacity is a view, not a health state.
`bridge_list` adds one block per profile:
`fleet_list` adds one block per profile:
```text
profile, maxLoad, live, free, reclaimable
@@ -612,7 +612,7 @@ incident feeds lead-health evidence. If the lead then becomes unhealthy, peer or
Webhook mode requires a resolved environment variable. Turning notification off stops outbound
attempts but keeps incidents. Turning it back on resumes still-open human incidents.
Without a sink, `bridge_list.healthCoverage` states that human escalation is unavailable. `/healthz`
Without a sink, `fleet_list.healthCoverage` states that human escalation is unavailable. `/healthz`
keeps its existing HTTP liveness result and adds a nested `fleetHealth.status=partial` component.
Metrics and one startup or reload WARN expose the same limit.
@@ -792,7 +792,7 @@ Acceptance criteria:
them.
16. Explicit stop is state-aware. Any pending task or non-terminal state preserves the worktree.
17. Atomic preserved-worktree manifests reload after restart and appear in lead-only
`bridge_list.preservedWorktrees`.
`fleet_list.preservedWorktrees`.
18. Manifest failure preserves the worktree and opens an operator-visible health failure.
19. Provision records the base commit. Terminal, long-idle worktrees report
`WORK_PRODUCT_AT_RISK` only under the evidence in Section 4.2 and never auto-delete work.
@@ -820,7 +820,7 @@ met, and I did not run one.
| 14 | `DELEGATION_ORPHANED` | 3 files | **Partial.** The health state exists. The teardown-invariant check that creates it, and the retry rule, do not. |
| 15 | `SPAWN_ROLLBACK` | 0 files | **Contradicted — see below.** |
| 16 | — | — | Partial at best. CB-576 made release preserve a dirty worktree; whether explicit stop is state-aware is not checked. |
| 17, 18 | `preservedWorktrees` | 0 files | Not started. No manifest, and no lead-only `bridge_list` field. |
| 17, 18 | `preservedWorktrees` | 0 files | Not started. No manifest, and no lead-only `fleet_list` field. |
| 19 | `WORK_PRODUCT_AT_RISK` | 0 files | Not started. |
**Criterion 15 no longer matches the code, and the code is right.** It says "normal `COMPLETED`
@@ -837,7 +837,7 @@ preserve it. `SPAWN_ROLLBACK` itself does not exist yet.
### Unit 3 - Typed inbox and member routing
Scope: semantic record, AMQP migration, both adapters, member routing, polling, and member health in
`bridge_list`.
`fleet_list`.
Acceptance criteria:
@@ -852,7 +852,7 @@ Acceptance criteria:
6. Invalid known data never escapes the callback, appears as a reply, or blocks later valid messages.
7. Invalid data reaches durable per-target quarantine before original ack. Failed handoff leaves the
original unacked.
8. Decode failures create redacted WARN, metric, `bridge_list` summary, and routed incident without
8. Decode failures create redacted WARN, metric, `fleet_list` summary, and routed incident without
raw content.
9. Both adapters pass one semantic contract for fields, FIFO, dedup, ownership, ack, and release.
10. Lead keys require explicit ownership. Publication never claims a queue.
@@ -865,7 +865,7 @@ Acceptance criteria:
15. RabbitMQ contract tests pass with `mvn test -Pcontract`. The same cases run once on production
LavinMQ, or the release states that LavinMQ was not checked.
16. Member incidents route to the exact delegating lead and never resolve a task rendezvous.
17. `bridge_list` shows compact member health and capacity without pane content. Member
17. `fleet_list` shows compact member health and capacity without pane content. Member
`idleForSeconds` is present only when no accepted turn or inbox item exists.
### Unit 4 - Lead health and peer routing
@@ -887,7 +887,7 @@ Acceptance criteria:
8. A selected working peer is not interrupted. Its push waits for an injectable window.
9. Recipient assignment stays pinned. Reassignment increments generation and supersedes old pending
assignment.
10. `bridge_list` shows bounded foreign assignments, recipient, reason, and generation without pane
10. `fleet_list` shows bounded foreign assignments, recipient, reason, and generation without pane
content.
11. `LeadInboxRegistry` owns configured and discovered lead keys before publication.
12. Missing leads keep ownership. Retirement needs an empty queue and handled incidents.
@@ -911,7 +911,7 @@ Acceptance criteria:
3. Webhook mode requires a resolved environment value. Bad notification config does not disable an
already valid detector.
4. Mode changes keep open incidents. Re-enable resumes eligible incidents.
5. `bridge_list`, `/healthz`, metrics, and one WARN show partial coverage without a sink. HTTP
5. `fleet_list`, `/healthz`, metrics, and one WARN show partial coverage without a sink. HTTP
liveness behavior stays unchanged.
6. One-lead, no-sink coverage states that lead failure has no active notification or recovery.
7. Incident and outbound dedupe use the stable keys in Section 10.3.
+13 -13
View File
@@ -220,17 +220,17 @@ sequenceDiagram
participant H as herdr
participant W as Worker (Claude)
P->>B: bridge_send("do X", target=w) — blocks
P->>B: fleet_send("do X", target=w) — blocks
B->>B: register waiter(w)
B->>H: agent.send(w, "do X") (idle window)
H-->>W: prompt injected
W->>W: works the turn
W->>B: bridge_reply("result")
W->>B: fleet_reply("result")
B->>B: resolve waiter(w)
B-->>P: { outcome:"reply", text:"result" }
```
### 6.2 Clarification — reverse rendezvous (`bridge_ask`)
### 6.2 Clarification — reverse rendezvous (`fleet_ask`)
The worker pauses mid-turn to ask; the primary answers; the worker resumes in the same turn.
@@ -240,14 +240,14 @@ sequenceDiagram
participant B as bridged
participant W as Worker
P->>B: bridge_send("do X", target=w) — blocks
P->>B: fleet_send("do X", target=w) — blocks
B-->>W: "do X" (injected)
W->>B: bridge_ask("which config?") — worker blocks
W->>B: fleet_ask("which config?") — worker blocks
B-->>P: { outcome:"question", text:"which config?", turn_id }
P->>B: bridge_send("config.yaml", target=w, turn_id) — blocks again
B-->>W: resolve bridge_ask → { answer:"config.yaml" }
P->>B: fleet_send("config.yaml", target=w, turn_id) — blocks again
B-->>W: resolve fleet_ask → { answer:"config.yaml" }
W->>W: resumes same turn
W->>B: bridge_reply("done")
W->>B: fleet_reply("done")
B-->>P: { outcome:"reply", text:"done" }
```
@@ -261,10 +261,10 @@ sequenceDiagram
participant B as bridged
participant W as Worker
P->>B: bridge_send("do X", target=w, block=false)
P->>B: fleet_send("do X", target=w, block=false)
B-->>P: { outcome:"dispatched", dispatch_id }
P->>P: continues its own work
W->>B: bridge_reply("result")
W->>B: fleet_reply("result")
Note over B: no waiter → detached path
B->>B: Injector.enqueue(primary_pane, "result")
B-->>P: injected into idle pane (status-gated)
@@ -272,7 +272,7 @@ sequenceDiagram
### 6.4 Uncooperative worker — turn-done fallback
A worker that never calls `bridge_reply` still returns a result: `bridged` reads its terminal
A worker that never calls `fleet_reply` still returns a result: `bridged` reads its terminal
tail when the turn completes.
```mermaid
@@ -281,9 +281,9 @@ sequenceDiagram
participant B as bridged
participant W as Worker
P->>B: bridge_send("do X", target=w) — blocks
P->>B: fleet_send("do X", target=w) — blocks
B-->>W: "do X" (injected)
W->>W: works, never calls bridge_reply
W->>W: works, never calls fleet_reply
B->>B: StatusPoller sees agent_status → idle/done
B->>B: AgentControl.read(w, "recent")
B-->>P: { outcome:"turn_done", text:<terminal tail> }
+5 -5
View File
@@ -19,12 +19,12 @@ of this one.
## One gateway for all messages
- **Everyone talks through the same door.** The leader and every worker connect to the same
MCP server and use only its tools: `bridge_whoami` · `bridge_profiles` · `bridge_spawn` ·
`bridge_list` · `bridge_status` · `bridge_send` · `bridge_reply` · `bridge_ask` ·
`bridge_poll` · `bridge_ack` · `bridge_stop`.
MCP server and use only its tools: `fleet_whoami` · `fleet_profiles` · `fleet_spawn` ·
`fleet_list` · `fleet_status` · `fleet_send` · `fleet_reply` · `fleet_ask` ·
`fleet_poll` · `fleet_ack` · `fleet_stop`.
- **You are who your connection says you are.** The bridge finds out who is calling from the
connection itself, never from a name the caller sends. So a worker cannot pretend to be
someone else, and `bridge_whoami` tells each agent its own role — no guessing.
someone else, and `fleet_whoami` tells each agent its own role — no guessing.
- **The subscription line cannot be crossed.** Only a spawned worker gets
`ANTHROPIC_BASE_URL`; the leader never does. Each worker profile has a list of allowed
model hosts, checked before anything starts.
@@ -59,7 +59,7 @@ had to ask for its replies. That gap is now closed on a single machine:
- **The leader gets a tap on the shoulder.** When a reply lands, the bridge nudges the
leader's own pane — only when the leader is free, and only a few times. If the leader is on
another machine, this quietly falls back to pick-up mode; the reply still waits.
- **Workers can ask questions.** With `bridge_ask`, a worker can pause mid-task, ask the
- **Workers can ask questions.** With `fleet_ask`, a worker can pause mid-task, ask the
leader something, and continue the *same* task with the answer.
## More than one kind of worker
+3 -3
View File
@@ -20,7 +20,7 @@ requirement, not a nice-to-have.**
- **Worktree provisioned by the daemon** — `SessionManager` creates a dedicated git worktree +
branch per session, **hydrates it to full config parity** (below), and tears it down on release.
- **Worker opens its own PR** — the worker commits, pushes its branch, and opens the PR/MR itself,
returning the PR URL in its `bridge_reply`.
returning the PR URL in its `fleet_reply`.
## Why worktrees (the hazard being fixed)
@@ -103,7 +103,7 @@ sequenceDiagram
Note over W: implement in the isolated worktree
W->>G: git commit + git push (SSH, same user)
W->>G: open PR (branch to main)
W-->>P: bridge_reply (prUrl, branch, summary, tests)
W-->>P: fleet_reply (prUrl, branch, summary, tests)
P->>SM: release(paneId)
SM->>G: git worktree remove wt
Note over G: branch + PR persist for review/merge
@@ -170,7 +170,7 @@ A worker-facing playbook (sibling to the existing `reviewer` skill):
3. **Push** your branch (`git push -u origin HEAD`).
4. **Open a PR** to `main` (option A `curl`, or the decided mechanism) with a title/body describing
the change and referencing the ticket.
5. **Reply** via `bridge_reply` with the **PR URL**, branch name, files changed, and test names —
5. **Reply** via `fleet_reply` with the **PR URL**, branch name, files changed, and test names —
that reply is the whole handoff.
6. Do **not** merge; do **not** touch `.mcp.json` or `wiki/`.
+5 -5
View File
@@ -14,7 +14,7 @@ rule that **a worker inherits the primary's directory** (never `$HOME`), and how
```mermaid
flowchart TD
A["bridge_spawn / POST /workers"] --> B{"explicit cwd?<br/>(profile cwd or spawn arg)"}
A["fleet_spawn / POST /workers"] --> B{"explicit cwd?<br/>(profile cwd or spawn arg)"}
B -->|"yes — told otherwise"| C["use that cwd"]
B -->|"no"| D{"caller PID resolvable?<br/>(MCP peer PID)"}
D -->|"yes"| E["cwd = the primary's cwd<br/>lsof -a -p PID -d cwd"]
@@ -51,7 +51,7 @@ only affect the seed shell, which the bridge closes).
| # | Source | When |
|---|--------|------|
| 1 | Explicit `cwd` — a per-profile `cwd:` in config, or a spawn argument | "told otherwise" — pin a fixed workdir |
| 2 | The **primary's cwd**, auto-detected from the `bridge_spawn` caller | normal MCP spawn from the primary |
| 2 | The **primary's cwd**, auto-detected from the `fleet_spawn` caller | normal MCP spawn from the primary |
| 3 | The `bridged` daemon's own cwd | REST spawn / off-host caller — **never `$HOME`** |
The primary's cwd (source 2) is discoverable with no new plumbing: `bridged` already resolves the MCP
@@ -65,7 +65,7 @@ sequenceDiagram
participant B as "bridged"
participant O as "OS (lsof)"
participant H as "herdr"
P->>B: "bridge_spawn {profile} (no cwd)"
P->>B: "fleet_spawn {profile} (no cwd)"
B->>O: "peer PID for this connection's port"
O-->>B: "pid"
B->>O: "cwd of pid (lsof -d cwd)"
@@ -79,7 +79,7 @@ sequenceDiagram
> **Status:** implemented (CB-112). `bridged` threads the resolved `cwd` onto **`agent.start {cwd}`**
> (verified: the worker process is rooted there), keeping the single shared worker space. On an MCP
> `bridge_spawn` the primary's cwd is auto-detected from the caller's PID; over REST (no MCP caller)
> `fleet_spawn` the primary's cwd is auto-detected from the caller's PID; over REST (no MCP caller)
> it is the explicit `cwd` param else the daemon's cwd. Both placements (`tab` and legacy `pane`)
> carry it, since it rides `agent.start`.
@@ -133,5 +133,5 @@ unattended.
## See also
- `docs/MCP-Contract.md` — the tool surface (`bridge_spawn`, `bridge_profiles`, …).
- `docs/MCP-Contract.md` — the tool surface (`fleet_spawn`, `fleet_profiles`, …).
- `wiki/2-Message-Server.md` — the herdr `agent.*` / `workspace.*` schema (`workspace.create {cwd}`).
+3 -3
View File
@@ -6,7 +6,7 @@ running `bridged` daemon**, captures the full transcript, and grades the channel
This is the committed form of the ad-hoc channel test that discovered the CB-115 gaps
(herdr `unknown` misclassification wedging delivery, dirty completion scrapes, and workers
never calling `bridge_reply` in conversation). Run it after any change to the injector,
never calling `fleet_reply` in conversation). Run it after any change to the injector,
status handling, completion/failure paths, or the worker reply charter.
## What it exercises
@@ -28,7 +28,7 @@ sequenceDiagram
T->>B: POST /sessions/{id}/message {wait:false}
B-->>T: ticket
B->>W: inject prompt (status-gated)
W-->>B: bridge_reply
W-->>B: fleet_reply
T->>B: GET /tasks/{ticket} (poll)
B-->>T: done + reply
end
@@ -68,7 +68,7 @@ Per-turn grade:
| Grade | Meaning |
|------------|---------------------------------------------------------------------|
| `OK` | delivered and resolved by an explicit `bridge_reply` (`source=reply`) |
| `OK` | delivered and resolved by an explicit `fleet_reply` (`source=reply`) |
| `DEGRADED` | delivered and answered, but resolved via completion-scrape fallback |
| `EMPTY` | turn completed but the reply was empty |
| `FAILED` | the worker's turn ended in failure (`phase=failed`) |