From 3ce76a5d69d9f60416055ad80e548243a093e9a3 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Tue, 25 Aug 2026 04:06:43 +0200 Subject: [PATCH] CB-634: finish the bridge_* -> fleet_* tool rename in docs and comments The alias removal left bridge_* tool names in prose. Fix them: - README no longer claims the old bridge_* names still answer (they were removed). - pom + LeadTabScanner comments name fleet_* tools. - FleetMcp comment no longer mentions the removed deprecated twin. - docs/MCP-Contract.md and e2e swept bridge_* -> fleet_*; e2e ask files renamed. The historical mcp__bridge__* mount-name note in CLAUDE.md is kept on purpose. 949 tests pass. --- README.md | 5 +- docs/MCP-Contract.md | 100 +++++++++--------- e2e/{bridge_ask_test.py => fleet_ask_test.py} | 2 +- ..._transcript.md => fleet_ask_transcript.md} | 6 +- fleetd/pom.xml | 2 +- .../dev/ltms/fleet/herdr/LeadTabScanner.java | 2 +- .../java/dev/ltms/fleet/mcp/FleetMcp.java | 3 +- 7 files changed, 59 insertions(+), 61 deletions(-) rename e2e/{bridge_ask_test.py => fleet_ask_test.py} (99%) rename e2e/{bridge_ask_transcript.md => fleet_ask_transcript.md} (52%) diff --git a/README.md b/README.md index 4154fee..1ba3762 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,8 @@ flowchart LR ever addresses a broker, a peer, or the network directly**; any queue is `fleetd`-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. + **Tool naming:** the tools are `fleet_*` (renamed from `bridge_*` in CB-622). The old + `bridge_*` names were removed in CB-634 — only `fleet_*` answers now. - **How the primary consumes a reply:** a single **blocking MCP call** (`fleet_send`); `fleetd` 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 diff --git a/docs/MCP-Contract.md b/docs/MCP-Contract.md index 2e79765..5281764 100644 --- a/docs/MCP-Contract.md +++ b/docs/MCP-Contract.md @@ -4,11 +4,11 @@ > any MCP code existed. The system shipped and this page never caught up, so **its tool names, > parameter names and REST paths are wrong today**. Audited 2026-08-17; the specific drift: > -> - **Tools it names that do not exist:** `bridge_read`, `bridge_cancel`. -> - **Shipped tools it omits:** `bridge_poll`, `bridge_ack`, `bridge_profiles`, `bridge_whoami`. +> - **Tools it names that do not exist:** `fleet_read`, `fleet_cancel`. +> - **Shipped tools it omits:** `fleet_poll`, `fleet_ack`, `fleet_profiles`, `fleet_whoami`. > - **Parameter names are wrong nearly everywhere** — it says `message`/`target`/`timeout_seconds`/ > `block` where the code takes `content`/`sessionId`/`timeoutMs`/`wait`; `text` where -> `bridge_reply` takes `content`; `target` where `bridge_stop` takes `paneId`. +> `fleet_reply` takes `content`; `target` where `fleet_stop` takes `paneId`. > - **REST paths are wrong:** it says `POST /workers` and `DELETE /workers/{paneId}`; the daemon > serves `POST /members` and `DELETE /members/{paneId}`. > @@ -60,7 +60,7 @@ face** is the herdr Unix socket. REST/SSE remains only for non-Claude clients an flowchart LR OPUS["Opus — primary
(Claude Code, env CLEAN)
MCP client"] subgraph BD["fleetd — standalone daemon"] - MCP["MCP server (north face)
bridge_send · bridge_reply
bridge_ask · bridge_status · lifecycle"] + MCP["MCP server (north face)
fleet_send · fleet_reply
fleet_ask · fleet_status · lifecycle"] RDV["rendezvous registry
(blocking-call waiters)"] INJ["Injector + StatusPoller
(status-gated writer)"] SOCK["herdr socket client (south face)"] @@ -72,8 +72,8 @@ flowchart LR HERDR["herdr
panes · agent-status"] W["worker claude pane
ANTHROPIC_BASE_URL set
MCP client"] - OPUS -->|"bridge_send (blocks)"| MCP - W -.->|"bridge_reply / bridge_ask"| MCP + OPUS -->|"fleet_send (blocks)"| MCP + W -.->|"fleet_reply / fleet_ask"| MCP SOCK -->|"agent.start · agent.send
agent.get · pane.close"| HERDR HERDR -->|"drives PTY"| W @@ -97,8 +97,8 @@ request — this is the linchpin of the whole contract and has no code yet. - **The primary is "not a worker".** Any connection that does not map to a known worker is treated as a primary. It addresses workers **explicitly** by `target` — a session UUID, a `terminal_id`, or a friendly `profile` name. -- **Turn correlation.** A blocking `bridge_send` registers a *waiter* keyed by worker - identity. A worker's later `bridge_reply` / `bridge_ask` on the same identity resolves that +- **Turn correlation.** A blocking `fleet_send` registers a *waiter* keyed by worker + identity. A worker's later `fleet_reply` / `fleet_ask` on the same identity resolves that waiter. A `turn_id` is minted per exchange so a clarification round-trip (§6.2) rejoins the right turn. @@ -123,36 +123,36 @@ This adds an MCP-server dependency the pom does not yet carry. See [Open decisio | Tool | Caller | Blocks? | Backing (exists today?) | |---|---|---|---| -| [`bridge_send`](#bridge_send) | primary | yes (default) | `Injector.enqueue` ✅ · rendezvous registry ❌ (CB-104) | -| [`bridge_reply`](#bridge_reply) | worker | no | rendezvous ❌ · pane injection via `Injector` ✅ | -| [`bridge_ask`](#bridge_ask) | worker | yes | reverse rendezvous ❌ | -| [`bridge_status`](#bridge_status) | either | no | `AgentControl.status` ✅ · `Injector.activeTargets` ✅ | -| [`bridge_spawn`](#lifecycle) | primary | no | `WorkerService.spawn` ✅ (`POST /workers`) | -| [`bridge_list`](#lifecycle) | either | no | `WorkerService.list` ✅ (`/agents`) | -| [`bridge_stop`](#lifecycle) | primary | no | `WorkerService.stop` ✅ (`DELETE /workers/{paneId}`) | -| [`bridge_read`](#bridge_read) | primary | no | `AgentControl.read` ✅ | -| [`bridge_cancel`](#bridge_cancel) | primary | no | — ❌ (future) | +| [`fleet_send`](#fleet_send) | primary | yes (default) | `Injector.enqueue` ✅ · rendezvous registry ❌ (CB-104) | +| [`fleet_reply`](#fleet_reply) | worker | no | rendezvous ❌ · pane injection via `Injector` ✅ | +| [`fleet_ask`](#fleet_ask) | worker | yes | reverse rendezvous ❌ | +| [`fleet_status`](#fleet_status) | either | no | `AgentControl.status` ✅ · `Injector.activeTargets` ✅ | +| [`fleet_spawn`](#lifecycle) | primary | no | `WorkerService.spawn` ✅ (`POST /workers`) | +| [`fleet_list`](#lifecycle) | either | no | `WorkerService.list` ✅ (`/agents`) | +| [`fleet_stop`](#lifecycle) | primary | no | `WorkerService.stop` ✅ (`DELETE /workers/{paneId}`) | +| [`fleet_read`](#fleet_read) | primary | no | `AgentControl.read` ✅ | +| [`fleet_cancel`](#fleet_cancel) | primary | no | — ❌ (future) | ### Core: delegation & rendezvous -#### `bridge_send` +#### `fleet_send` *(primary → worker — the headline tool, CB-104)* - **Params:** `message` (required); `target` (optional — defaults to the sole worker / default profile); `timeout_seconds` (default 600); `block` (default `true`); `auto_spawn` - (default `true`); `turn_id` (optional — supplied when answering a worker's `bridge_ask`). + (default `true`); `turn_id` (optional — supplied when answering a worker's `fleet_ask`). - **Blocking (`block:true`):** enqueue `message` via the `Injector`, then hold the call open until exactly one of: - - worker calls `bridge_reply` → `{ outcome:"reply", text }` - - worker calls `bridge_ask` → `{ outcome:"question", text, turn_id }` + - worker calls `fleet_reply` → `{ outcome:"reply", text }` + - worker calls `fleet_ask` → `{ outcome:"question", text, turn_id }` - worker's `agent_status` reaches done/idle with no reply → `{ outcome:"turn_done", text: }` - deadline elapses → `{ outcome:"timeout" }` - worker gone → error `worker_gone` - **Detached (`block:false`):** enqueue and return `{ outcome:"dispatched", dispatch_id }` immediately. The eventual reply is injected into the primary's idle pane (§6.3), or drained - via `bridge_status` on a split-host primary. + via `fleet_status` on a split-host primary. -#### `bridge_reply` +#### `fleet_reply` *(worker → primary)* - **Params:** `text` (required); `final` (default `true`). @@ -160,28 +160,28 @@ This adds an MCP-server dependency the pom does not yet carry. See [Open decisio waiter exists (detached delegation), `fleetd` **injects the primary's idle pane** instead. Returns `{ delivered:true, mode:"resolved"|"injected" }`. No `target` — identity is implicit. -#### `bridge_ask` +#### `fleet_ask` *(worker → primary — the reverse rendezvous)* - **Params:** `question` (required); `timeout_seconds`. - **Behavior:** blocks the *worker's* call. Surfaces the question to the primary (resolving its - open `bridge_send` with `outcome:"question"`, or injecting its pane). When the primary - answers — a `bridge_send` carrying the matching `turn_id` — that unblocks this call and + open `fleet_send` with `outcome:"question"`, or injecting its pane). When the primary + answers — a `fleet_send` carrying the matching `turn_id` — that unblocks this call and returns `{ answer }` to the worker, which continues **in the same turn**. ### Worker lifecycle Thin adapters over [`WorkerService`](1-Architecture) — parity with the existing REST routes. -- **`bridge_spawn`** — `{ profile? }` → worker view (`sessionId`, `terminalId`, `paneId`, +- **`fleet_spawn`** — `{ profile? }` → worker view (`sessionId`, `terminalId`, `paneId`, `status`). Guard-checked; a boundary breach returns error `subscription_boundary` (the REST `403`). -- **`bridge_list`** — no params → all workers + `agent_status`. Read-only, either role. -- **`bridge_stop`** — `{ target }` → tears down the pane and its dedicated tab. Idempotent. +- **`fleet_list`** — no params → all workers + `agent_status`. Read-only, either role. +- **`fleet_stop`** — `{ target }` → tears down the pane and its dedicated tab. Idempotent. ### Observability -#### `bridge_status` +#### `fleet_status` *(either role — the README's 4th named tool)* - **Params:** `target?`. @@ -190,7 +190,7 @@ Thin adapters over [`WorkerService`](1-Architecture) — parity with the existin messages addressed to me** — the path a split-host primary's `Stop`-hook uses to wake and collect replies without being injectable. Read-only, non-blocking. -#### `bridge_read` +#### `fleet_read` *(primary)* - **Params:** `target`; `source` ∈ `visible | recent | recent_unwrapped | detection`. @@ -199,7 +199,7 @@ Thin adapters over [`WorkerService`](1-Architecture) — parity with the existin ### Control (future) -#### `bridge_cancel` +#### `fleet_cancel` *(primary)* - **Params:** `target`. Interrupt the worker's current turn / abandon the rendezvous. No @@ -294,7 +294,7 @@ sequenceDiagram ## 7. Status gating Delivery only happens in a safe window. This is the state machine the `Injector` already -enforces via `AgentStatus.injectable()`; MCP `bridge_send` is simply its producer. +enforces via `AgentStatus.injectable()`; MCP `fleet_send` is simply its producer. ```mermaid stateDiagram-v2 @@ -330,17 +330,17 @@ touching this state machine. ## 8. Error model -| Condition | `bridge_send` result | Notes | +| Condition | `fleet_send` result | Notes | |---|---|---| | Worker replies | `{ outcome:"reply" }` | normal | -| Worker asks | `{ outcome:"question", turn_id }` | answer with `bridge_send(turn_id)` | +| Worker asks | `{ outcome:"question", turn_id }` | answer with `fleet_send(turn_id)` | | Turn ends, no reply | `{ outcome:"turn_done" }` | terminal tail as text | | Deadline elapsed | `{ outcome:"timeout" }` | message may still be queued/delivered | | Worker vanished | error `worker_gone` | `Injector.drop` fails the queued future | | Guard breach on spawn | error `subscription_boundary` | REST `403` parity | | Delivery failed at herdr | error, message dropped | poisoned message not left blocking the FIFO | -`bridge_reply` from a worker with no open waiter is **not** an error — it falls through to +`fleet_reply` from a worker with no open waiter is **not** an error — it falls through to detached pane injection (§6.3). --- @@ -352,11 +352,11 @@ seam. Only the **rendezvous registry** and the **caller-identity resolver** are | MCP tool | Existing collaborator | New work | |---|---|---| -| `bridge_send` | `Injector.enqueue`, `AgentControl.send` | waiter registry, timeout, outcome mux (CB-104) | -| `bridge_reply` / `bridge_ask` | `Injector` (pane injection) | reverse rendezvous, identity resolver | -| `bridge_status` | `AgentControl.status`, `Injector.activeTargets` | pending-drain projection | -| `bridge_spawn` / `list` / `stop` | `WorkerService.{spawn,list,stop}` | MCP adapter only | -| `bridge_read` | `AgentControl.read` | MCP adapter only | +| `fleet_send` | `Injector.enqueue`, `AgentControl.send` | waiter registry, timeout, outcome mux (CB-104) | +| `fleet_reply` / `fleet_ask` | `Injector` (pane injection) | reverse rendezvous, identity resolver | +| `fleet_status` | `AgentControl.status`, `Injector.activeTargets` | pending-drain projection | +| `fleet_spawn` / `list` / `stop` | `WorkerService.{spawn,list,stop}` | MCP adapter only | +| `fleet_read` | `AgentControl.read` | MCP adapter only | Because the REST routes in `FleetApp` already exercise the collaborators, MCP tools are validated by **parity** against those routes, not by re-testing behavior. @@ -365,14 +365,14 @@ validated by **parity** against those routes, not by re-testing behavior. ## 10. Open decisions -1. **`bridge_ask` direction.** This page defines it as *worker-asks-primary* (a genuine reverse +1. **`fleet_ask` direction.** This page defines it as *worker-asks-primary* (a genuine reverse channel, matching the "inject the primary's pane" language). The alternative — a synonym for a blocking primary→worker send — is weaker and produces different plumbing. **Recommend worker-asks-primary.** -2. **Detached delivery shape.** A `block:false` param on `bridge_send` (keeps the catalog - small) vs. a separate `bridge_dispatch` tool. **Recommend the param.** -3. **Auto-spawn on send.** `bridge_send` provisions a worker per profile when none exists - (simplest primary UX) vs. requiring an explicit `bridge_spawn` first. **Recommend +2. **Detached delivery shape.** A `block:false` param on `fleet_send` (keeps the catalog + small) vs. a separate `fleet_dispatch` tool. **Recommend the param.** +3. **Auto-spawn on send.** `fleet_send` provisions a worker per profile when none exists + (simplest primary UX) vs. requiring an explicit `fleet_spawn` first. **Recommend auto-spawn, defaulting on.** 4. **Transport & SDK.** Streamable-HTTP/SSE co-located with the REST bind (recommended) vs. stdio. Requires choosing a Java MCP server SDK and adding it to the pom. @@ -381,9 +381,9 @@ validated by **parity** against those routes, not by re-testing behavior. ## 11. Implementation staging -- **CB-104** — blocking `bridge_send` + rendezvous registry + caller-identity resolver +- **CB-104** — blocking `fleet_send` + rendezvous registry + caller-identity resolver (the producer that finally drives the inert `StatusPoller`). -- **CB-1xx** — `bridge_reply` / `bridge_ask` reverse rendezvous + detached pane injection. -- **CB-1xx** — lifecycle + observability adapters (`bridge_spawn/list/stop/status/read`). +- **CB-1xx** — `fleet_reply` / `fleet_ask` reverse rendezvous + detached pane injection. +- **CB-1xx** — lifecycle + observability adapters (`fleet_spawn/list/stop/status/read`). - **CB-1xx** — transport wiring + `claude mcp add` docs; parity tests vs. REST. -- **Later** — `bridge_cancel`; swap `StatusPoller` for herdr `events.subscribe`. +- **Later** — `fleet_cancel`; swap `StatusPoller` for herdr `events.subscribe`. diff --git a/e2e/bridge_ask_test.py b/e2e/fleet_ask_test.py similarity index 99% rename from e2e/bridge_ask_test.py rename to e2e/fleet_ask_test.py index 7044fee..0dccc3c 100644 --- a/e2e/bridge_ask_test.py +++ b/e2e/fleet_ask_test.py @@ -24,7 +24,7 @@ Like the rest of the suite it talks ONLY to the bridge's REST face on loopback ANTHROPIC_BASE_URL and never touches herdr, so it is subscription-safe by construction. Usage: - python3 bridge_ask_test.py [--base URL] [--profile NAME] [--repo DIR] [--out DIR] + python3 fleet_ask_test.py [--base URL] [--profile NAME] [--repo DIR] [--out DIR] [--send-timeout SECS] [--answer-timeout SECS] [--keep-worker] --base bridge REST base URL (default http://127.0.0.1:8765) diff --git a/e2e/bridge_ask_transcript.md b/e2e/fleet_ask_transcript.md similarity index 52% rename from e2e/bridge_ask_transcript.md rename to e2e/fleet_ask_transcript.md index bfcc73c..546e944 100644 --- a/e2e/bridge_ask_transcript.md +++ b/e2e/fleet_ask_transcript.md @@ -1,12 +1,12 @@ -# Live bridge_ask — reverse rendezvous — 2026-07-16 16:30 +# Live fleet_ask — reverse rendezvous — 2026-07-16 16:30 One worker paused its delegated turn to ask the primary, then resumed with the answer (profile `default`). Result: **`OK`**. ## Round-trip 1. **primary → worker** (delegation): the ask-forcing task. -2. **worker → primary** (`bridge_ask`, 6.6s): 'PICK A COLOR: red or blue?' — surfaced on the primary's blocked send as a `question` with `turnId=term_656bb47d2c42a9e#1`. +2. **worker → primary** (`fleet_ask`, 6.6s): 'PICK A COLOR: red or blue?' — surfaced on the primary's blocked send as a `question` with `turnId=term_656bb47d2c42a9e#1`. 3. **primary → worker** (answer on that turn): `blue`. -4. **worker → primary** (`bridge_reply`, 7.9s, source=reply): 'CHOSEN=BLUE' +4. **worker → primary** (`fleet_reply`, 7.9s, source=reply): 'CHOSEN=BLUE' > **OK:** asked, resumed the same turn, and the reply reflected the primary's answer diff --git a/fleetd/pom.xml b/fleetd/pom.xml index 18d48cb..b294c01 100644 --- a/fleetd/pom.xml +++ b/fleetd/pom.xml @@ -106,7 +106,7 @@ + /mcp, exposing fleet_send/fleet_reply/fleet_status as thin adapters over REST. --> io.modelcontextprotocol.sdk mcp diff --git a/fleetd/src/main/java/dev/ltms/fleet/herdr/LeadTabScanner.java b/fleetd/src/main/java/dev/ltms/fleet/herdr/LeadTabScanner.java index 9b0a3c4..6a64b50 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/herdr/LeadTabScanner.java +++ b/fleetd/src/main/java/dev/ltms/fleet/herdr/LeadTabScanner.java @@ -39,7 +39,7 @@ import java.util.function.Supplier; *
  • Worker spaces are excluded wholesale ({@code excludedWorkspaceLabels}), so a worker cannot * become a lead by being placed — as a split, say — inside a matching tab.
  • *
  • A worker cannot rename a tab: {@code tab.rename} is reachable only through - * {@link WorkspaceControl}, which no {@code bridge_*} tool exposes. The label is writable by + * {@link WorkspaceControl}, which no {@code fleet_*} tool exposes. The label is writable by * the human at the terminal and by nobody the bridge is defending against.
  • *
  • The label is a name, not a capability. What a pane may do is decided by * {@code Authz} against the role {@code CallerResolver} returns; a tab that calls itself a diff --git a/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java b/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java index 4572f5e..efff9dc 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java +++ b/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java @@ -172,8 +172,7 @@ public final class FleetMcp { CALLER_NAME, orEmpty(p.name()))); }) .build(); - // CB-622: each handler is built once and reused for BOTH its fleet_* tool and its - // deprecated bridge_* twin (registered below), so the two names can never drift apart. + // Each handler is built once and wired to its fleet_* tool below. BiFunction sendHandler = (exchange, req) -> { McpSchema.CallToolResult denied = deny(exchange, Authz.Action.SEND,