CB-632 unit 5: rename the daemon and classes in the docs prose

Part of #145 (CB-632). Documentation only, plus one internal literal.

Unit 1 renamed the package and classes, which left every doc describing
classes that no longer exist. This fixes the prose across README.md,
docs/ and bridged/docs/ -- 18 files.

Renamed: dev.ltms.bridged -> dev.ltms.fleet, the five class names, and
"bridged" where it names the daemon as a product rather than a path.

Also renamed two literals, because a doc that disagrees with the code is
worse than one that is out of date:

  - bridged-local-noauth -> fleetd-local-noauth. A placeholder apiKey
    OpenCodeLauncher sends when a profile resolves no token, to a local
    endpoint that does not check it. No test asserts the old string.
  - the vnd.ltms.bridged.* media type in the M4 design doc. It appears
    in no Java file, so nothing implements it yet.

Deliberately NOT renamed, because each is still literally true today and
changes only at the cutover:

  - paths: bridged/, bridged.yaml, bridged.example.yaml, bridged.jar,
    .bridged-worktrees, deploy/dev.ltms.bridged.plist,
    scripts/redeploy-bridged.sh, bridged-launchd-wrapper.sh
  - bridged_* metric names -- renaming these after the monitoring is
    wired would break dashboard continuity, so they move before it is
  - bridge_* MCP tool names, which answer alongside fleet_* on purpose
  - BRIDGED_* environment variables, read by a file outside this repo

Method note: perl, not sed. BSD sed has no \b and no lookaround, and a
word-boundary expression there fails silently. The prose replace uses
(?<![\w./-])bridged(?![\w./-]) so it cannot touch a path or an
identifier, then every remaining hit was read by hand.

Verified: mvn clean install green, 51 classes, 878 tests, 0 failures.
This commit is contained in:
Dai Ha
2026-08-23 06:46:34 +02:00
parent 3b7cdf9365
commit ecc590f344
18 changed files with 139 additions and 139 deletions
+14 -14
View File
@@ -9,23 +9,23 @@ Sibling of [`crush-bridge`](https://git.ltms.dev/systems/vms) (which drives a he
process*, so it inherits `CLAUDE.md`, hooks, skills, and MCP — just pointed at a
cheaper/local model.
## Leading approach — herdr-centric message server (`bridged`)
## Leading approach — herdr-centric message server (`fleetd`)
A small always-on message server, **`bridged`**, controls
A small always-on message server, **`fleetd`**, controls
[herdr](https://herdr.dev) (an agent multiplexer) over its Unix-socket API and exposes a
clean 2-way messaging API as an **MCP server that both the primary and the workers mount** —
one unified Claude setup and the **sole communication gateway** (REST/SSE stays for non-Claude
clients; any broker is `bridged`-internal, below the gateway).
herdr owns the PTYs, multiplexing, persistence, and **agent-status events**; `bridged` owns
clients; any broker is `fleetd`-internal, below the gateway).
herdr owns the PTYs, multiplexing, persistence, and **agent-status events**; `fleetd` owns
policy (subscription boundary, session lifecycle, status-gated delivery) and the client
contract. A Claude member launches with `ANTHROPIC_BASE_URL` pointed at the gateway,
`https://llm.ltms.dev/anthropic`, plus a bearer token; the lead stays env-clean and calls
`bridged`'s MCP tools. See the wiki's **[13 User Guide](wiki/13-User-Guide.md)** to run it.
`fleetd`'s MCP tools. See the wiki's **[13 User Guide](wiki/13-User-Guide.md)** to run it.
```mermaid
flowchart LR
OPUS["Opus — primary<br/>(Claude Code, env CLEAN)<br/>MCP client"]
subgraph BD["bridged — standalone daemon (not a claude process)"]
subgraph BD["fleetd — standalone daemon (not a claude process)"]
SRV["SERVER face<br/>MCP · REST/SSE · policy"]
CLI["CLIENT face<br/>status-gated injector · herdr socket"]
SRV --> CLI
@@ -47,27 +47,27 @@ flowchart LR
```
- **Subscription boundary:** the *primary* never sets `ANTHROPIC_BASE_URL` (stays on
Pro/Max). Only the *secondary* process is off-subscription — and `bridged` itself is a
Pro/Max). Only the *secondary* process is off-subscription — and `fleetd` itself is a
plain daemon (no Anthropic quota), so it may poll/subscribe freely.
- **One gateway (unified MCP setup):** `bridged` is the **sole communication path** for every
- **One gateway (unified MCP setup):** `fleetd` 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 — `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
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.
- **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
`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
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
blocking call (or, for detached work, `bridged` **injects the primary's idle pane** when it's
- **Worker → primary** rides `fleetd`'s **MCP rendezvous** — the reply resolves the primary's
blocking call (or, for detached work, `fleetd` **injects the primary's idle pane** when it's
ready), so *no keystroke-into-primary and no broker are involved, even single-host*. The one
exception: a split-host primary that isn't a herdr pane wakes via its own `Stop`-hook, which
polls **`bridged`** (never a broker). See the wiki for the two topologies.
polls **`fleetd`** (never a broker). See the wiki for the two topologies.
- **Different model per process** sidesteps Claude Code's lack of per-subagent provider
routing — the worker isn't a subagent, it's its own configured process.
- **AgentAPI** ([`coder/agentapi`](https://github.com/coder/agentapi)) is retained only as a
@@ -90,7 +90,7 @@ Gitea wiki.
## Status
🟢 **Implemented & dogfooded** — the herdr-centric **`bridged`** message server is built and in
🟢 **Implemented & dogfooded** — the herdr-centric **`fleetd`** message server is built and in
real use: an Opus primary delegates tasks to off-subscription workers that reply through the
bridge (code reviews delegated this way have produced committed bug fixes). Selected as the
primary approach 2026-07-11, superseding the AgentAPI plan (2026-07-08); AgentAPI retained as a
@@ -362,7 +362,7 @@ public final class OpenCodeLauncher extends HerdrPeerLauncher {
options.put("baseURL", openAiBaseUrl(cfg.baseUrl()));
// vLLM and friends usually ignore the key, but the AI SDK still requires a non-empty one.
String token = resolveEnv(cfg.tokenEnv());
options.put("apiKey", (token == null || token.isBlank()) ? "bridged-local-noauth" : token);
options.put("apiKey", (token == null || token.isBlank()) ? "fleetd-local-noauth" : token);
provider.putObject("models").putObject(modelId).put("name", modelId);
}
+4 -4
View File
@@ -2,7 +2,7 @@
**Status:** design spec for review → delegate implementation.
**Grounded in:** `WorkerService`, `Injector`/`StatusPoller`/`TurnListener`, `MessageService`,
`BridgeMcp`, `BridgedApp` (see [wiki 9. Implementation](../wiki/9-Implementation.md)).
`FleetMcp`, `FleetApp` (see [wiki 9. Implementation](../wiki/9-Implementation.md)).
## Problem
@@ -43,7 +43,7 @@ Under no-reuse, a released session is terminal. A new `acquire` always creates a
subscription-guarded spawn/teardown mechanics; `SessionManager` adds the registry, lifecycle, and
ownership on top.
**Package:** new `dev.ltms.bridged.session` — keeps the registry/lifecycle concern separate from
**Package:** new `dev.ltms.fleet.session` — keeps the registry/lifecycle concern separate from
the `worker` spawn mechanics. Holds `SessionManager` + `WorkerSession`.
### `WorkerSession` (record or small mutable holder)
@@ -96,10 +96,10 @@ final class SessionManager {
### Integration points
- **`Bridged.main`** — construct `SessionManager(workerService, ...)`; wire it as/decorating the
- **`Fleetd.main`** — construct `SessionManager(workerService, ...)`; wire it as/decorating the
`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`
- **`FleetMcp.spawn` / `FleetApp.spawnWorker`** — route spawn through `SessionManager.acquire`
(carry `callerTerminal` as `ownerTerminal`). **`fleet_stop` / `DELETE /workers/{paneId}`** →
`SessionManager.release`.
- **`fleet_list` / `GET /sessions` (CB-304 later)** — read `SessionManager.roster()`.
+5 -5
View File
@@ -2,12 +2,12 @@
**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`
`Fleetd.main` and configurable via `worktreeRoot` / per-profile `parityOverlay`
(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).
**Grounded in:** `SessionManager`, `WorkerService.spawn/effectiveCwd`, `BridgedConfig.Worker`,
**Grounded in:** `SessionManager`, `WorkerService.spawn/effectiveCwd`, `FleetConfig.Worker`,
`inject/…LsofPeerPidLookup` (the `ProcessBuilder` exec pattern).
## Problem
@@ -43,7 +43,7 @@ provider.
### `WorktreeRequest` (new, nullable = "no worktree")
```java
package dev.ltms.bridged.session;
package dev.ltms.fleet.session;
/** Ask acquire() to provision an isolated worktree. null ⇒ run in the shared primary tree. */
public record WorktreeRequest(String ticketSlug, String baseRef) {
// ticketSlug seeds the branch name; baseRef null/blank ⇒ current HEAD of the repo.
@@ -63,7 +63,7 @@ untouched.
### `Worktrees` seam (new)
```java
package dev.ltms.bridged.session;
package dev.ltms.fleet.session;
public interface Worktrees {
/** git -C <repoRoot> worktree add <path> -b <branch> <baseRef|HEAD>. Returns the worktree path. */
String add(String repoRoot, String branch, String baseRef);
@@ -112,7 +112,7 @@ public void release(String paneId) {
}
```
### Config — `BridgedConfig.Worker.parityOverlay` + a `worktreeRoot`
### Config — `FleetConfig.Worker.parityOverlay` + a `worktreeRoot`
- Add `List<String> parityOverlay` to the `Worker` record (12th field). Compact-constructor default
when null/empty: `[".mcp.json", ".claude/settings.local.json", ".env", ".envrc"]` (missing paths are
+5 -5
View File
@@ -55,7 +55,7 @@ a crash, and a slow start all present as "never becomes injectable" and all corr
or `spawnReadyTimeoutMs` elapses.
3. **Ready** → return the `WorkerHandle(paneId, terminalId)` as today.
4. **Timeout** → the launcher **closes the pane it started** (and its tab, via the same path
`release`/`stop` uses) and throws **`PeerUnreachableException`** (new, in `dev.ltms.bridged.peer`).
`release`/`stop` uses) and throws **`PeerUnreachableException`** (new, in `dev.ltms.fleet.peer`).
No orphan pane is left behind — the launcher cleans up its own failed birth.
`spawnReadyTimeoutMs == 0` (or unset) **disables** the gate = legacy non-blocking behaviour, so the
@@ -80,7 +80,7 @@ Unit tests (add to the existing `ClaudeCodeLauncher` test):
## 5. Config
Add to the launcher-level config (a bridged-level knob, not per-profile) in `bridged.yaml` +
`BridgedConfig`:
`FleetConfig`:
```yaml
spawn_ready_timeout_ms: 20000 # 0 disables the gate (legacy non-blocking spawn)
@@ -88,7 +88,7 @@ spawn_ready_poll_ms: 300
```
Jackson ignores unknown keys, so omitting them in existing YAML is safe; pick sane defaults in code
(`20000` / `300`). Keep the names consistent with existing config field style in `BridgedConfig`.
(`20000` / `300`). Keep the names consistent with existing config field style in `FleetConfig`.
## 6. Core / MCP propagation
@@ -100,7 +100,7 @@ Jackson ignores unknown keys, so omitting them in existing YAML is safe; pick sa
- 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.
- `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
success with a dead session. Trace `FleetMcp`/`FleetApp` 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 `fleet_send` on session `READY` (existing status-gate +
@@ -111,7 +111,7 @@ any config `kind:` discriminator, any second adapter.
- `ClaudeCodeLauncher.spawn` blocks until injectable or throws `PeerUnreachableException` +
self-reaps the pane; gate disabled when timeout is 0.
- New `PeerUnreachableException` in `dev.ltms.bridged.peer`.
- New `PeerUnreachableException` in `dev.ltms.fleet.peer`.
- Config knobs wired (`spawn_ready_timeout_ms`, `spawn_ready_poll_ms`) with safe defaults.
- Existing `SPAWNING→READY` MCP-contact transition untouched.
- New unit tests (ready / timeout+reap / disabled) green; **all existing tests still pass unchanged**.
+11 -11
View File
@@ -19,8 +19,8 @@ is currently open** for that worker:
- `Rendezvous.resolve(session, content)` → `complete(...)` → `waiters.get(session) == null` →
returns `false` (`msg/Rendezvous.java:212-215`).
- The `content` string is **never retained** — it is dropped. The worker is told it failed:
`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`).
`FleetMcp.reply` returns `error("no send is awaiting a reply for this worker")` (`mcp/FleetMcp.java:270-272`);
REST returns `409 no_pending_send` (`rest/FleetApp.java:339-345`).
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
@@ -28,13 +28,13 @@ the message path today.
## 2. What to build
### 2.1 The port — `dev.ltms.bridged.msg.ReplyInbox`
### 2.1 The port — `dev.ltms.fleet.msg.ReplyInbox`
A thin interface owned by the `msg` layer. The in-memory adapter is Stage 1; the AMQP adapter (Stage 2)
implements the **same** interface, so keep it broker-agnostic.
```java
package dev.ltms.bridged.msg;
package dev.ltms.fleet.msg;
import java.util.List;
@@ -70,7 +70,7 @@ public interface ReplyInbox {
seen-set — your call; preserve insertion order).
- `peek` returns an immutable copy; `ack` removes by `msgId`. Thread-safe (concurrent publish vs. drain).
- **This is soft-state, NOT persistence.** Lost on a `java -jar` bounce — that is correct and consistent
with "bridged stays soft-state." Do **not** add any file/DB backing.
with "fleetd stays soft-state." Do **not** add any file/DB backing.
### 2.3 Publish seam — route reply through the service layer
@@ -89,9 +89,9 @@ in `MessageService`, which already owns the `Rendezvous` and will own the `Reply
}
```
- Repoint the two callers off the bare `rendezvous.resolve(...)` onto `messages.reply(...)`:
- `BridgeMcp.reply` (`mcp/BridgeMcp.java:262-273`) — on success return a normal ack; **remove** the
- `FleetMcp.reply` (`mcp/FleetMcp.java:262-273`) — on success return a normal ack; **remove** the
`error("no send is awaiting a reply…")` branch (that case is now a successful queue).
- `BridgedApp.replyMessage` (`rest/BridgedApp.java:330-346`) — return `200` (queued) instead of
- `FleetApp.replyMessage` (`rest/FleetApp.java:330-346`) — return `200` (queued) instead of
`409 no_pending_send`.
**DO NOT touch the QUESTION path.** `fleet_ask` / `rendezvous.resolveQuestion` must keep today's
@@ -120,12 +120,12 @@ The primary re-checks a worker it delegated to. Expose a drain keyed by **worker
## 3. Config
**None for Stage 1.** The in-memory adapter is the unconditional default — wire `new InMemoryReplyInbox()`
into `MessageService` in `Bridged.main`. Do **not** add a `broker:` config block (that arrives with the
into `MessageService` in `Fleetd.main`. Do **not** add a `broker:` config block (that arrives with the
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`.
1. New `ReplyInbox` + `InboxMessage` + `InMemoryReplyInbox` in `dev.ltms.fleet.msg`.
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
@@ -152,7 +152,7 @@ Stage-2 AMQP adapter: absent → in-memory, present → AMQP).
- **`.mcp.json` is `--skip-worktree` in your worktree — never edit, `git add`, or commit it.**
- **`wiki/` is a submodule — never run git in it; never touch it.**
- Commit only your feature changes (the new port/adapter, the `msg`/`mcp`/`rest` wiring, tests, and if
you add config wiring in `Bridged.java`). Nothing else.
you add config wiring in `Fleetd.java`). Nothing else.
- Work only inside your assigned worktree on your feature branch. The primary fast-forwards `main` after
re-gating — do not touch `main`.
- Java 25 idioms are welcome (unnamed `_` params, records). Keep the diff minimal and match surrounding style.
@@ -171,7 +171,7 @@ removal, msgId dedup, cross-restart redelivery). It is `@Tag("contract")`, so th
untouched. Run it explicitly when Docker (or a broker) is available:
```bash
cd bridged
cd fleetd
mvn -Pcontract test -Dtest=AmqpReplyInboxContractTest # local: spins a RabbitMQ Testcontainers fixture
```
+8 -8
View File
@@ -18,7 +18,7 @@ The design rests on three pieces (the shape this ticket proposes):
1. **Dedicated per-agent channels** — every agent has its own addressable inbox on the broker.
2. **A federated agent directory** — a global "who/where/status" lookup, assembled from per-host
presence, not a central database.
3. **A per-host gateway** — each host runs a `bridged` that owns its local herdr, registers/manages
3. **A per-host gateway** — each host runs a `fleetd` that owns its local herdr, registers/manages
its own sessions, and proxies messages to/from other hosts over the broker.
## 2. What is single-host today (the assumptions to break)
@@ -27,7 +27,7 @@ The design rests on three pieces (the shape this ticket proposes):
flowchart TB
subgraph host["Single host (today)"]
primary["primary<br/>(MCP client)"]
daemon["bridged daemon<br/>127.0.0.1:8765"]
daemon["fleetd daemon<br/>127.0.0.1:8765"]
reg["in-process registry<br/>keyed by PeerHandle.id() == paneId"]
herdr["herdr<br/>(local unix-socket PTY mux)"]
w1["worker pane wQ:p1"]
@@ -48,21 +48,21 @@ Three concrete bake-ins assume one host:
|---|---|---|
| **herdr is local** | `herdr/` unix socket `~/.config/herdr/herdr.sock` | You cannot drive another host's PTYs → each host **must** own its herdr. This is why a per-host gateway is mandatory. |
| **registry is in-process, keyed by `paneId`** | `session/SessionManager` | `paneId` (e.g. `wQ:p2B`) is a herdr-local coordinate — meaningless off-host. Routing needs a host-unique id. |
| **loopback, no authn** | `rest/BridgedApp` binds `127.0.0.1:8765` | Fine on one host; the moment a second host can talk to a gateway, that link is a trust boundary. |
| **loopback, no authn** | `rest/FleetApp` binds `127.0.0.1:8765` | Fine on one host; the moment a second host can talk to a gateway, that link is a trust boundary. |
## 3. Target architecture
```mermaid
flowchart TB
subgraph hostA["HOST A"]
gA["gateway = bridged A"]
gA["gateway = fleetd A"]
regA["local registry + herdr"]
primary["primary (MCP client)"]
gA --- regA
primary --- gA
end
subgraph hostB["HOST B"]
gB["gateway = bridged B"]
gB["gateway = fleetd B"]
regB["local registry + herdr"]
wb["worker panes"]
gB --- regB
@@ -96,13 +96,13 @@ host's terminals.*
delayed-message exchange (the remind/backoff loop for free) — the same reasons CB-307 picked it.
- **Federated agent directory** = a **soft-state, bridge-owned** roster, *not* a broker-stored
database. Per the persistence-boundary decision (bridged is soft-state; the broker owns *message*
database. Per the persistence-boundary decision (fleetd is soft-state; the broker owns *message*
durability, not *who/where/status*), each gateway announces its local agents `(globalId, host,
status, capabilities)` on a `roster.*` presence topic with periodic heartbeats. Every gateway
builds an eventually-consistent **union view** — literally CB-304's `rosterView`, federated. A
stale entry expires by missed heartbeat (reuses CB-303's idle/TTL thinking).
- **Per-host gateway** = today's `bridged` daemon, evolved. It already registers/manages sessions
- **Per-host gateway** = today's `fleetd` daemon, evolved. It already registers/manages sessions
and controls its local herdr; multi-host adds exactly two responsibilities: (a) a broker client
that consumes its agents' inboxes and injects into local herdr, and (b) presence announce +
union-roster assembly. Evolution, not rewrite.
@@ -123,7 +123,7 @@ broker. A sender is oblivious to which branch it took.*
## 4. What CB-307 already provides vs. what is net-new
**CB-307 delivers the transport half** and is independently valuable on a single host: the AMQP
broker fabric, the `bridged → broker` client/adapter, at-least-once + idempotent (dedup-by-id)
broker fabric, the `fleetd → broker` client/adapter, at-least-once + idempotent (dedup-by-id)
delivery, DLQ, and delayed-retry (remind). That *is* the "proxy cross-host message" backbone;
extending the same broker from "worker→primary reliability" to "gateway↔gateway" is incremental.
+4 -4
View File
@@ -60,7 +60,7 @@ Where Claude/herdr specifics actually live today:
| `claude-<profile>-<nonce>-<seq>` naming, `WORKER_NAME` regex, orphan reap (CB-117) | `WorkerService` | **→ adapter** (naming is a herdr-label detail) |
| `GITEA_TOKEN` / `GITEA_HOST` injection (CB-302 checkpoint) | `WorkerService.spawn` | **→ adapter** + a **capability** (§6) |
| tab/pane placement, worker space, tab labels | `WorkerService.spawnInTab/spawnAsPane` via herdr `WorkspaceControl` | **→ adapter** (herdr transport detail) |
| `BridgedConfig.Worker` profile shape (`baseUrl`, `model`, `configDir`, …) | `config` | **mostly adapter-shaped** — see §5 |
| `FleetConfig.Worker` profile shape (`baseUrl`, `model`, `configDir`, …) | `config` | **mostly adapter-shaped** — see §5 |
| FSM, registry, roster, `reapIdle`/`drainAll`/`contextCap`, `rosterView` | `SessionManager` | **stays core** |
| turn/completion detection (`TurnListener`, `CompletionResolver`, `StatusPoller`, `WorkerPresence`) | `inject/` | **stays core**, but reads herdr terminal output → transport-coupled (§4b) |
| message store & routing | `msg/` | **stays core** |
@@ -118,7 +118,7 @@ hard-wire "turns come from herdr".
## 5. Config shape
`BridgedConfig.Worker` is Claude-shaped (`baseUrl`, `model`, `configDir`, `tokenEnv`). Rather than
`FleetConfig.Worker` is Claude-shaped (`baseUrl`, `model`, `configDir`, `tokenEnv`). Rather than
break existing YAML, CB-401 keeps `workers:` exactly as-is and treats those fields as the
**ClaudeCodeLauncher's** profile schema. A future peer kind adds a `kind:` discriminator
(default `"claude-code"`) selecting the launcher; unknown-kind → clear config error. No migration of
@@ -185,13 +185,13 @@ messages, not verified facts.
Deliverable for CB-401 Stage A — mechanical, behaviour-preserving:
1. `PeerLauncher` interface + `PeerHandle` (opaque id) + `SpawnRequest` (profile, requestedCwd,
callerCwd) + `Capability` enum, new package `dev.ltms.bridged.peer`.
callerCwd) + `Capability` enum, new package `dev.ltms.fleet.peer`.
2. `ClaudeCodeLauncher implements PeerLauncher` = today's `WorkerService`, adapted: `spawn(...)`
returns a `PeerHandle` (id = paneId), `capabilities()` declares
`MID_TURN_ASK, SELF_PR(when token), WORKTREE, ORPHAN_REAP`.
3. `SessionManager` depends on `PeerLauncher`, not `WorkerService` concretely; routing keys on
`PeerHandle.id()` (== paneId today, so zero value change).
4. `Bridged.main` wires the concrete `ClaudeCodeLauncher` behind the interface.
4. `Fleetd.main` wires the concrete `ClaudeCodeLauncher` behind the interface.
5. **No behaviour change, no config change.** Full green gate: `ide_sync` → `ide_diagnostics`
(0 errors/0 warnings) → `mvn clean install` with `MVN_EXIT` captured (no masking pipe). All
existing tests pass unchanged; add tests only for the new `PeerHandle` indirection.
+11 -11
View File
@@ -11,7 +11,7 @@ All five increments of §4 are done, including increment 5 (the §5 live checkli
## 1. Goal
Prove the [`PeerLauncher`](../bridged/src/main/java/dev/ltms/bridged/peer/PeerLauncher.java) SPI
Prove the [`PeerLauncher`](../bridged/src/main/java/dev/ltms/fleet/peer/PeerLauncher.java) SPI
actually holds for a **non-Claude** coding agent by shipping a second, first-class in-tree
adapter: **opencode** (`opencode` 1.1.31, a provider-agnostic terminal coding agent).
@@ -56,8 +56,8 @@ flowchart TB
*Figure 1 — the two concerns tangled inside today's single launcher; CB-402 splits them.*
There is also a **Stage-A deferral** to finish: `Bridged.main` still casts
`(ClaudeCodeLauncher) workers` at the `BridgeMcp` and `BridgedApp` constructors. Those two
There is also a **Stage-A deferral** to finish: `Fleetd.main` still casts
`(ClaudeCodeLauncher) workers` at the `FleetMcp` and `FleetApp` constructors. Those two
callers only invoke `profiles()`, `defaultProfile()`, and `list()` — **all already on the
`PeerLauncher` interface**. The cast survives for one reason only: `PeerLauncher.list()`
returns `List<?>` (element type erased) while the callers use `Agent` element methods in their
@@ -68,7 +68,7 @@ roster join. Finishing the migration is therefore small and contained (§4.D).
## 3. Target design
Template-Method base + two thin adapters + a routing composite that keeps the Stage-A seam
(one `PeerLauncher` reference held by `SessionManager` / `BridgeMcp` / `BridgedApp`) intact.
(one `PeerLauncher` reference held by `SessionManager` / `FleetMcp` / `FleetApp`) intact.
```mermaid
flowchart TB
@@ -114,7 +114,7 @@ Two adapter **hooks** (abstract):
protected abstract String namePrefix(); // "claude" | "opencode"
/** Build the peer-specific launch: env map + argv. Runs any pre-spawn guard here. */
protected abstract Launch buildLaunch(BridgedConfig.Worker cfg, SpawnRequest req);
protected abstract Launch buildLaunch(FleetConfig.Worker cfg, SpawnRequest req);
record Launch(Map<String,String> env, List<String> argv) {}
```
@@ -126,7 +126,7 @@ so the opencode adapter never reaps a `claude-*` pane and vice-versa. The compos
### B. `kind:` config discriminator
Add one field to `BridgedConfig.Worker`:
Add one field to `FleetConfig.Worker`:
```java
String kind // "claude-code" (default) | "opencode"
@@ -164,13 +164,13 @@ String kind // "claude-code" (default) | "opencode"
### D. `CompositePeerLauncher` + finish the Stage-A migration
- `Bridged.main` groups configured profiles by `kind`, instantiates one launcher per kind
- `Fleetd.main` groups configured profiles by `kind`, instantiates one launcher per kind
present, and wraps them in `CompositePeerLauncher implements PeerLauncher`.
- Routing methods (`spawn(req)`, `effectiveCwd(req)`, `parityOverlay(name)`) dispatch by the
profile's kind. Fan-out methods (`list()`, `reapOrphanWorkers()`, `capabilities()`,
`profiles()`, `defaultProfile()`) merge across sub-launchers. `stop(id)` tries each (teardown
only knows the pane id) — already best-effort/idempotent.
- **Migrate `BridgeMcp` + `BridgedApp` to the `PeerLauncher` interface**, dropping both
- **Migrate `FleetMcp` + `FleetApp` to the `PeerLauncher` interface**, dropping both
`(ClaudeCodeLauncher)` casts. Only friction is `list()`'s `List<?>`; resolve by giving the SPI
a typed roster element (small neutral `PeerAgent` view exposing `id()`/`name()`/status) that
the CB-304 roster join consumes — or, minimally, narrow at the callsite. Prefer the typed view.
@@ -179,7 +179,7 @@ String kind // "claude-code" (default) | "opencode"
sequenceDiagram
autonumber
participant P as Primary
participant M as BridgeMcp / REST
participant M as FleetMcp / REST
participant C as CompositePeerLauncher
participant O as OpenCodeLauncher
participant B as HerdrPeerLauncher (base)
@@ -244,7 +244,7 @@ until it is "major" (Stage-B whole), per the CB-401 bar.
- **Unit (hermetic):** base-extraction regression (existing `ClaudeCodeLauncher` tests pass
unchanged); `OpenCodeLauncher.buildLaunch` env/argv/config assertions; `kind` normalization
in `BridgedConfigTest`; `CompositePeerLauncher` routing + fan-out (merge of `profiles()`,
in `FleetConfigTest`; `CompositePeerLauncher` routing + fan-out (merge of `profiles()`,
summed `reapOrphanWorkers()`, per-kind reap isolation) with fake sub-launchers.
- **Live (dogfood, manual):** the §5 checklist on the running daemon.
- **Gate (primary):** IDE diagnostics 0/0 on every changed file, `mvn clean install` green with
@@ -269,7 +269,7 @@ until it is "major" (Stage-B whole), per the CB-401 bar.
## 8. As-built — live dogfood (2026-07-29)
Run against `bridged` on `127.0.0.1:8766` at main `19cdf8d`, with opencode **1.18.5** installed
Run against `fleetd` on `127.0.0.1:8766` at main `19cdf8d`, with opencode **1.18.5** installed
via Homebrew. Every §5 risk is now a verified fact rather than an assumption.
**The version-drift risk was the real one, and it did not bite.** This adapter was designed against
+12 -12
View File
@@ -38,7 +38,7 @@ never toolchain ownership (§7).
flowchart TB
human["human (types)"]
primary["PRIMARY (Opus)<br/>MCP client — pull-only"]
daemon["bridged daemon<br/>127.0.0.1:8765 (single host)"]
daemon["fleetd daemon<br/>127.0.0.1:8765 (single host)"]
comp["CompositePeerLauncher<br/>routes by kind"]
cc["ClaudeCodeLauncher"]
oc["OpenCodeLauncher"]
@@ -80,7 +80,7 @@ flowchart TB
m1["main A: Opus<br/>MCP client"]
m2["main B: cloud module<br/>MCP client"]
end
subgraph bus["bridged fabric (CB-307/308 substrate)"]
subgraph bus["fleetd fabric (CB-307/308 substrate)"]
chan["per-agent inbox channels<br/>agent.&lt;globalId&gt;.inbox"]
roster["federated roster (union view)"]
end
@@ -146,7 +146,7 @@ env-manager" rule (§7).*
```mermaid
sequenceDiagram
participant M as main (delegator)
participant D as bridged
participant D as fleetd
participant SL as SandboxLauncher
participant SB as sandbox (peer-owned)
participant A as agent in sandbox
@@ -189,7 +189,7 @@ flowchart TB
m2["main B: cloud module<br/>MCP client (pull-only)"]
end
reg["PrimaryRegistry → multi-slot<br/>(terminal per main)"]
subgraph fabric["bridged"]
subgraph fabric["fleetd"]
ca["agent.A.inbox"]
cb["agent.B.inbox"]
push["ReplyPushLoop → N terminals"]
@@ -211,7 +211,7 @@ transport is already peer-neutral — what was missing is N pull endpoints).*
```mermaid
sequenceDiagram
participant MA as main A (Opus)
participant BR as bridged / broker
participant BR as fleetd / broker
participant MB as main B (cloud)
MA->>BR: fleet_send(to = main B, msg)
BR->>BR: publish agent.B.inbox (durable, msg id)
@@ -233,7 +233,7 @@ push-loop fan-out; relax "orchestration tools only the primary calls" to "any re
## 6. Development C — Orchestrator tier
> **SUPERSEDED — do not implement this model.** The operator rejected a supervisor above the lead.
> The human continues to drive the pre-existing lead directly; bridged neither spawns nor resumes that
> The human continues to drive the pre-existing lead directly; fleetd neither spawns nor resumes that
> lead. What replaced this proposal is **one lead, two short-lived advisory architects, and N workers**:
> the lead engages architects sideways for a strong-model assessment, then discards them. Architect
> slots are declared in `architects:` (see Gitea issue #16), rather than making leads managed sessions.
@@ -243,7 +243,7 @@ push-loop fan-out; relax "orchestration tools only the primary calls" to "any re
> **Historical alternative retained.** The text and figures below record the considered model and why it
> was rejected: it re-rooted the human-facing session above the lead, violating the still-true premise
> that configured leaders pre-exist, are recognised, and cannot be resumed by bridged.
> that configured leaders pre-exist, are recognised, and cannot be resumed by fleetd.
The orchestrator is **`SessionManager` recursed one tier up**: today it spawns/names/reaps *worker*
sessions; the orchestrator does the same for *main* sessions, and adds **context scoping**.
@@ -385,7 +385,7 @@ ticket is an extension of an existing pattern (CB-402 for A, CB-308 for B/C).
The follow-up question — *"clarify the architecture when we have distributed agents in sandboxes"* —
resolves the fork left open in §4 and §9. **Decision: Development A (sandbox launcher) and CB-308
(per-host federation) *compose*, not compete — each host runs a `bridged` gateway whose launcher
(per-host federation) *compose*, not compete — each host runs a `fleetd` gateway whose launcher
spawns agents into that host's *local* sandboxes.** A sandbox is never reached across the network; it
is reached by the gateway sitting next to it.
@@ -393,7 +393,7 @@ is reached by the gateway sitting next to it.
The bus delivers a turn by **herdr keystroke-injection** — `Injector → AgentControl.send` writes into
a PTY that its **local** herdr owns. The broker moves *messages and presence*, **never keystrokes**.
So an agent's PTY must live in a herdr that *some* `bridged` instance drives locally: a remote
So an agent's PTY must live in a herdr that *some* `fleetd` instance drives locally: a remote
container with no local herdr **cannot be injected into**. That rules out a central daemon reaching
remote PTYs, and collapses the design to a single identity:
@@ -403,7 +403,7 @@ remote PTYs, and collapses the design to a single identity:
flowchart TB
subgraph hostA["HOST A — gateway"]
mA["main / orchestrator<br/>MCP client → LOCAL gateway"]
gA["bridged A<br/>herdr + CompositePeerLauncher<br/>(incl. SandboxLauncher)"]
gA["fleetd A<br/>herdr + CompositePeerLauncher<br/>(incl. SandboxLauncher)"]
cBEa["sandbox: backend<br/>(local container)"]
cFEa["sandbox: frontend<br/>(local container)"]
mA --- gA
@@ -415,7 +415,7 @@ flowchart TB
roster["roster.* (federated presence)"]
end
subgraph hostB["HOST B — gateway"]
gB["bridged B<br/>herdr + SandboxLauncher"]
gB["fleetd B<br/>herdr + SandboxLauncher"]
cBEb["sandbox: backend<br/>(local container)"]
gB -->|"spawn → PTY in B's herdr"| cBEb
end
@@ -470,7 +470,7 @@ unchanged — the sandbox is transparent to it.*
| Concern | Provided by |
|---|---|
| Per-host gateway (owns local herdr + sessions) | **CB-308** (today's `bridged`, evolved) |
| Per-host gateway (owns local herdr + sessions) | **CB-308** (today's `fleetd`, evolved) |
| Spawn into a local sandbox / role→image | **Development A** `SandboxLauncher` (§4), routed by `CompositePeerLauncher` |
| Addressing a remote sandboxed agent | **CB-308** global id + federated roster (host + role as metadata) |
| Orphan reap after a gateway restart | **CB-117** per-gateway, summed by the composite — each reaps only its **local** herdr |
+4 -4
View File
@@ -51,7 +51,7 @@ Three facts about our side that decide the shape of this work:
`ANTHROPIC_AUTH_TOKEN` (the value is read from a host env var and never stored in config).
`local` sets no `tokenEnv` today, because a direct vLLM needs no token.
2. **`SubscriptionGuard` refuses any host not on an allowlist**, and that allowlist is
`guard.offSubscriptionHosts: [gx00.gw]`. It is built once in `Bridged.java:93` and handed to the
`guard.offSubscriptionHosts: [gx00.gw]`. It is built once in `Fleetd.java:93` and handed to the
launcher, so **it is a restart-required key**, not a hot one. Changing `baseUrl` without changing
this makes every `local` spawn throw.
3. **The wiki names us as a blocker.** Under *Not done yet*: retiring the shared `legacy` token is
@@ -132,7 +132,7 @@ own provider credentials. So 3b needs **no allowlist change**; only 3a does.
**Both still need a restart, for a different reason.** `tokenEnv` is resolved by
`HerdrPeerLauncher.resolveEnv` → `env.apply(name)`, which reads the **daemon's own process
environment**. The running `bridged` inherited its environment when it started, so a variable added to
environment**. The running `fleetd` inherited its environment when it started, so a variable added to
`secrets.sh` afterwards is simply not there — the launcher would inject an empty token and the
gateway would answer 401. This is the same failure as trap 1 in `scripts/redeploy-bridged.sh`
(`WORKER_GITEA_TOKEN`), and it has the same fix: **restart from a login shell**, and use
@@ -207,7 +207,7 @@ worker's honesty rule leans on it ("never claim the result of a check you had no
- **Keep bridge-only.** The invariant stays true and simple. Workers stay cheap and narrow.
- **Add the gateway MCP.** Implementers get context7 documentation lookups, which is genuinely useful
for library work. But `mcpUrl` in `BridgedConfig.Profile` is a **single `String`**, so a
for library work. But `mcpUrl` in `FleetConfig.Profile` is a **single `String`**, so a
claude-code member can mount exactly one MCP — this needs a code change, not a config edit.
Note the invariant is **already inaccurate**: `opencode.json` gives `sol` and `terra` both context7
@@ -383,7 +383,7 @@ one turn; this one costs the whole task and is indistinguishable from a slow wor
- `/v1/models` returns exactly `["deepseek-v4-flash"]`, so trap 3 is clear.
- **Reasoning survives both surfaces** — see §3b above.
- The launcher's generated opencode provider block is correct, carrying a real 48-character `llmk-`
key rather than the `bridged-local-noauth` placeholder.
key rather than the `fleetd-local-noauth` placeholder.
- `SubscriptionGuard` accepted `llm.ltms.dev` after the allowlist edit and the restart: `local`
spawned without throwing, which is the check that catches a missed restart.
+6 -6
View File
@@ -12,12 +12,12 @@ not after it.
## 1. Why this stage is not optional bookkeeping
`bridged` today has **exactly one security control: the loopback bind**. Every other guarantee
`fleetd` today has **exactly one security control: the loopback bind**. Every other guarantee
rests on it.
The identity model (`mcp/ConnectionIdentity.java`) resolves a caller from the connection alone —
the OS reports the connecting PID, herdr owns the PID→pane map, so a worker cannot forge another
worker. Its own javadoc is explicit: *"Single-host only (the herd shares the `bridged` host); the
worker. Its own javadoc is explicit: *"Single-host only (the herd shares the `fleetd` host); the
token path is the split-host fallback."* The token path does not exist yet.
That leaves a seam that is **latent today and load-bearing the moment the bind moves**:
@@ -112,7 +112,7 @@ The roadmap says "systemd unit". **This host is macOS — there is no systemd on
not found), and the daemon that has been dogfooded for weeks runs as a bare foreground
`java -jar`. Ship **both**:
- `deploy/dev.ltms.bridged.plist` — launchd agent, the *actual* runtime here, with `KeepAlive` and
- `deploy/dev.ltms.fleet.plist` — launchd agent, the *actual* runtime here, with `KeepAlive` and
ordered start after herdr.
- `deploy/bridged.service` — systemd unit for the Linux gateways CB-308 introduces.
@@ -158,10 +158,10 @@ configured. It already leaks nothing but herdr's version and up/down.
### 3.1 There are TWO entry paths, and only one of them has identity today
The wiki describes MCP as "a thin adapter over the REST core". **At the code level that is not
literally true, and the difference is security-relevant.** `BridgeMcp` calls `MessageService` /
literally true, and the difference is security-relevant.** `FleetMcp` calls `MessageService` /
`SessionManager` *directly*; it never issues an HTTP request against a Javalin route. And `/mcp` is
mounted as a raw servlet on Jetty's `ServletContextHandler`
(`BridgedApp.build → cfg.jetty.modifyServletContextHandler`), so it does **not** pass through
(`FleetApp.build → cfg.jetty.modifyServletContextHandler`), so it does **not** pass through
Javalin's `before` filters at all.
The current split is the mirror image of what you'd expect:
@@ -226,7 +226,7 @@ was answered from the forge itself. Kept here as the decision record.*
1. ✅ **TLS scope (D3) — confirmed.** Bearer auth + the fail-fast bind guard ship in the daemon;
TLS terminates at a reverse proxy, documented with a worked example. AMQP gets TLS via an
`amqps://` URI. No keystore handling in `bridged`.
`amqps://` URI. No keystore handling in `fleetd`.
2. ✅ **Micrometer (D4) — confirmed dropped.** Zero-dependency Prometheus text renderer, for the
reasons in D4 (pom reconciliation burden + the mandated CVE gate being un-runnable this
session). Revisit if a push-gateway or JVM-metrics requirement appears; the endpoint is the
+5 -5
View File
@@ -72,7 +72,7 @@ post-teardown invariant so a later regression becomes `DELEGATION_ORPHANED`.
### 2.2 Corrections made during design
The first state table missed `BUSY` in bridged plus `IDLE` or `DONE` in herdr. It would have found
The first state table missed `BUSY` in fleetd plus `IDLE` or `DONE` in herdr. It would have found
the real trace only through a late, weak stall timer. The final model adds
`TURN_BOUNDARY_LOST` as a strong disagreement state.
@@ -161,7 +161,7 @@ request or merge state. If committed work appears with `REPLY_STRANDED` or
| State | Exact evidence | Certainty and action |
|---|---|---|
| `CONTROL_LINK_DOWN` | Two full-fleet `agent.list` calls fail across the grace, and herdr `ping` also fails | Certain for the bridged-to-herdr link. Retry calls, record the incident, and use human escalation if no lead can be reached. |
| `CONTROL_LINK_DOWN` | Two full-fleet `agent.list` calls fail across the grace, and herdr `ping` also fails | Certain for the fleetd-to-herdr link. Retry calls, record the incident, and use human escalation if no lead can be reached. |
A failed fleet list alone is not a dead-member claim. A single `_not_found` with a healthy global
link is a target fault, not a control-link fault.
@@ -298,7 +298,7 @@ Compiled brakes apply even if config asks for more:
- at most two pane reads occur in one fleet tick;
- targets rotate fairly;
- only a normalised digest and optional clipped local excerpt are stored;
- no pane excerpt leaves bridged in a human webhook.
- no pane excerpt leaves fleetd in a human webhook.
Use `detection` for tested screen signatures. Use normalised `recent_unwrapped` only for progress
comparison.
@@ -330,7 +330,7 @@ The reader uses AMQP `content_type`, never body sniffing:
```text
Legacy v0: text/plain
Typed family: application/vnd.ltms.bridged.inbox-message+json
Typed family: application/vnd.ltms.fleet.inbox-message+json
```
A legacy reply may begin with `{`. It remains plain text because its media type is `text/plain`.
@@ -520,7 +520,7 @@ A repaired result uses distinct `RECONCILED_COMPLETION` values in `Rendezvous`,
task poll source, and metrics. The lead sees:
```text
[repaired completion - bridged detected a lost turn boundary. The member did not call
[repaired completion - fleetd detected a lost turn boundary. The member did not call
fleet_reply; pane-derived text follows and may be partial]
```
+18 -18
View File
@@ -1,4 +1,4 @@
# MCP Contract — `bridged`'s unified gateway
# MCP Contract — `fleetd`'s unified gateway
> **Status: 🔴 HISTORICAL DESIGN — do NOT use as the tool reference.** Written 2026-07-14, before
> any MCP code existed. The system shipped and this page never caught up, so **its tool names,
@@ -14,13 +14,13 @@
>
> **The authoritative tool surface is the live MCP schema** (each tool's own description and
> parameters, as mounted), with the intent→tool table in `CLAUDE.md` as the short form. Both were
> checked against `mcp/BridgeMcp.java` on 2026-08-17 and are accurate.
> checked against `mcp/FleetMcp.java` on 2026-08-17 and are accurate.
>
> What is still worth reading here is **§6 — the flows and the error model** (rendezvous,
> `fleet_ask`, detached delivery, the turn-done fallback). The shapes it describes are the ones
> that shipped; only the names around them drifted. Rewriting this page is tracked as **CB-609**.
`bridged` is the **sole communication gateway** for every Claude session in the bridge. Both
`fleetd` is the **sole communication gateway** for every Claude session in the bridge. Both
the **primary** (Opus, on subscription) and every **worker** (off-subscription Claude Code)
mount the *same* MCP server with a single `claude mcp add` line, and talk only through its
tools. No Claude session ever addresses a broker, a peer, or the network directly.
@@ -35,18 +35,18 @@ semantics, and how it maps onto the code already in the tree.
These come from the project's core invariants and bound every decision below.
1. **One server, both roles.** The primary and all workers mount an identical server. The
catalog must serve both, and `bridged` must decide *who is calling* from the connection —
catalog must serve both, and `fleetd` must decide *who is calling* from the connection —
never from a caller-supplied argument that could be spoofed.
2. **Subscription-safe by construction.** No MCP tool ever reads, sets, or forwards
`ANTHROPIC_BASE_URL`. Mounting the bridge cannot move a session off subscription.
Enforced today by [`SubscriptionGuard`](1-Architecture).
3. **Blocking rendezvous, no busy-poll.** The primary consumes a worker's reply through a
*single* MCP call that `bridged` holds open — never a cross-turn poll loop that would burn
*single* MCP call that `fleetd` holds open — never a cross-turn poll loop that would burn
subscription quota.
4. **Status-gated delivery.** Anything that puts text into a worker flows through the existing
[`Injector`](1-Architecture): delivered only when the worker is `idle`/`blocked`, at most
one message per turn.
5. **`bridged` owns policy; herdr owns PTYs.** MCP tools express *intent*; `bridged`
5. **`fleetd` owns policy; herdr owns PTYs.** MCP tools express *intent*; `fleetd`
translates it into guard checks, rendezvous bookkeeping, and herdr `agent.*` calls.
---
@@ -59,7 +59,7 @@ face** is the herdr Unix socket. REST/SSE remains only for non-Claude clients an
```mermaid
flowchart LR
OPUS["Opus — primary<br/>(Claude Code, env CLEAN)<br/>MCP client"]
subgraph BD["bridged — standalone daemon"]
subgraph BD["fleetd — standalone daemon"]
MCP["MCP server (north face)<br/>bridge_send · bridge_reply<br/>bridge_ask · bridge_status · lifecycle"]
RDV["rendezvous registry<br/>(blocking-call waiters)"]
INJ["Injector + StatusPoller<br/>(status-gated writer)"]
@@ -87,10 +87,10 @@ flowchart LR
## 3. Identity & addressing
Because the same server is mounted by everyone, `bridged` resolves the caller's role on every
Because the same server is mounted by everyone, `fleetd` resolves the caller's role on every
request — this is the linchpin of the whole contract and has no code yet.
- **Workers are known.** `bridged` spawns every worker
- **Workers are known.** `fleetd` spawns every worker
([`WorkerService`](1-Architecture)) and records its herdr session UUID / `terminal_id` on
the returned [`Agent`]. When a call arrives on a connection that maps to a known worker,
the caller is *that* worker — so **workers never pass a target**; routing is implicit.
@@ -106,13 +106,13 @@ request — this is the linchpin of the whole contract and has no code yet.
## 4. Transport
`bridged` is a long-lived daemon serving **multiple** concurrent clients (one primary + N
`fleetd` is a long-lived daemon serving **multiple** concurrent clients (one primary + N
workers), so a per-client stdio child is the wrong shape. The recommended transport is
**streamable-HTTP / SSE** on the same bind as the REST face:
```bash
# identical on primary and every worker
claude mcp add --transport http bridged http://127.0.0.1:8080/mcp
claude mcp add --transport http fleetd http://127.0.0.1:8080/mcp
```
This adds an MCP-server dependency the pom does not yet carry. See [Open decisions](#10-open-decisions).
@@ -157,7 +157,7 @@ This adds an MCP-server dependency the pom does not yet carry. See [Open decisio
- **Params:** `text` (required); `final` (default `true`).
- **Behavior:** resolve the primary waiter registered against this worker with `text`. If no
waiter exists (detached delegation), `bridged` **injects the primary's idle pane** instead.
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`
@@ -216,7 +216,7 @@ One blocking call, zero polls.
```mermaid
sequenceDiagram
participant P as Primary (Opus)
participant B as bridged (MCP + Injector)
participant B as fleetd (MCP + Injector)
participant H as herdr
participant W as Worker (Claude)
@@ -237,7 +237,7 @@ The worker pauses mid-turn to ask; the primary answers; the worker resumes in th
```mermaid
sequenceDiagram
participant P as Primary
participant B as bridged
participant B as fleetd
participant W as Worker
P->>B: fleet_send("do X", target=w) — blocks
@@ -258,7 +258,7 @@ The primary does not block; the reply arrives later in its idle pane.
```mermaid
sequenceDiagram
participant P as Primary
participant B as bridged
participant B as fleetd
participant W as Worker
P->>B: fleet_send("do X", target=w, block=false)
@@ -272,13 +272,13 @@ sequenceDiagram
### 6.4 Uncooperative worker — turn-done fallback
A worker that never calls `fleet_reply` still returns a result: `bridged` reads its terminal
A worker that never calls `fleet_reply` still returns a result: `fleetd` reads its terminal
tail when the turn completes.
```mermaid
sequenceDiagram
participant P as Primary
participant B as bridged
participant B as fleetd
participant W as Worker
P->>B: fleet_send("do X", target=w) — blocks
@@ -358,7 +358,7 @@ seam. Only the **rendezvous registry** and the **caller-identity resolver** are
| `bridge_spawn` / `list` / `stop` | `WorkerService.{spawn,list,stop}` | MCP adapter only |
| `bridge_read` | `AgentControl.read` | MCP adapter only |
Because the REST routes in `BridgedApp` already exercise the collaborators, MCP tools are
Because the REST routes in `FleetApp` already exercise the collaborators, MCP tools are
validated by **parity** against those routes, not by re-testing behavior.
---
+2 -2
View File
@@ -1,8 +1,8 @@
# v1.0.0 — One leader, one host, complete
This is the first release of **`bridged`**.
This is the first release of **`fleetd`**.
`bridged` lets one main Claude Code session (the **leader**, on your Pro/Max subscription)
`fleetd` lets one main Claude Code session (the **leader**, on your Pro/Max subscription)
run a team of **workers** — extra Claude Code sessions on a cheaper or local model, and
non-Claude agents too. The leader's own session is never touched: it stays on subscription,
with a clean environment.
+19 -19
View File
@@ -1,9 +1,9 @@
# Team — lead orchestrating a mixed Claude + local-LLM fleet
The message server (`bridged`) delivers **one turn into one worker**. A **team** is the
The message server (`fleetd`) delivers **one turn into one worker**. A **team** is the
layer above it: a **Claude team-lead** that fans a job out across a **mixed fleet** of
workers — some on Claude, some on the remote local LLM — and reduces their replies. Same
`bridged` delivery, same subscription boundary; this doc is only about **orchestration** —
`fleetd` delivery, same subscription boundary; this doc is only about **orchestration** —
who the workers are, how the lead picks one, and how it runs many at once.
> Delivery mechanics (blocking `POST /message`, status-gated reply envelope) live in the
@@ -12,9 +12,9 @@ who the workers are, how the lead picks one, and how it runs many at once.
## The team
- **Team-lead** — the primary **Opus** (Claude Code, env **CLEAN**, on Pro/Max). Not a
worker; a **thin client of `bridged`**. It plans, routes, dispatches, and integrates, and
worker; a **thin client of `fleetd`**. It plans, routes, dispatches, and integrates, and
never sets `ANTHROPIC_BASE_URL`.
- **Workers** — a herd of `claude` panes in herdr, each an addressable `bridged` session
- **Workers** — a herd of `claude` panes in herdr, each an addressable `fleetd` session
with its **own model/env**:
- **Claude workers** (clean env, e.g. Sonnet) — reasoning-heavy or high-accuracy subtasks.
- **Local workers** (`ANTHROPIC_BASE_URL=https://llm.ltms.dev/anthropic`) — bulk, cheap, or
@@ -28,7 +28,7 @@ MCP) — only its model differs. Scale each kind horizontally by adding panes.
```mermaid
flowchart TB
LEAD["lead — Opus<br/>(Claude Code, env CLEAN)"]
BD["bridged<br/>message server + router"]
BD["fleetd<br/>message server + router"]
HERDR["herdr<br/>panes · agent-status"]
WC1["w-claude-1<br/>Sonnet · CLEAN"]
WC2["w-claude-2<br/>Sonnet · CLEAN"]
@@ -62,14 +62,14 @@ flowchart TB
| `w-local-*` | `ANTHROPIC_BASE_URL` set | local LLM | task is bulk / cheap / embarrassingly parallel |
The lead applies this rubric itself, guided by its `CLAUDE.md` team charter (below). Worker
selection is **policy in the lead**, not a `bridged` concern — `bridged` just delivers to
selection is **policy in the lead**, not a `fleetd` concern — `fleetd` just delivers to
the session the lead names.
## Subscription boundary in a team
Unchanged from the base architecture, and it scales with the fleet: **only local-worker
panes** launch with `ANTHROPIC_BASE_URL`. The lead and every Claude worker stay env-clean on
the subscription. `bridged` enforces which panes may carry the off-subscription env, so
the subscription. `fleetd` enforces which panes may carry the off-subscription env, so
adding workers never widens the boundary.
## Parallel fan-out (map / reduce)
@@ -80,7 +80,7 @@ different workers at once, then results are gathered.
```mermaid
sequenceDiagram
participant L as lead (Opus)
participant B as bridged
participant B as fleetd
participant WC as w-claude-1
participant WL as w-local-1
@@ -100,26 +100,26 @@ sequenceDiagram
```
- **Map:** the lead issues N concurrent blocking `POST /message` calls (one per subtask → its
chosen worker). Each call blocks only *that* request; `bridged` holds it open until the
chosen worker). Each call blocks only *that* request; `fleetd` holds it open until the
worker's turn completes (status-gated) and returns the reply envelope.
- **Reduce:** the lead collects the N envelopes and integrates. A slow local worker never
blocks a fast Claude worker — wall-clock ≈ the slowest single subtask, not the sum.
- **Detached / long jobs** use the async broker path instead of a held request (Channel 2 in
the base architecture), so the lead never busy-polls across turns.
Fan-out is bounded by the herd size (pane count) and `bridged`'s concurrency policy, not by
Fan-out is bounded by the herd size (pane count) and `fleetd`'s concurrency policy, not by
the lead.
## Knowing the roster
The lead discovers its team from `bridged` (session list / roles) rather than hard-coding
The lead discovers its team from `fleetd` (session list / roles) rather than hard-coding
pane ids, so workers can be added or restarted without editing the lead. A minimal charter
in the lead's `CLAUDE.md` turns Opus into the orchestrator:
```markdown
## Your team (via bridged)
You are the team-lead. Delegate through the bridged client — never launch workers yourself.
Roster: ask bridged for current sessions/roles.
## Your team (via fleetd)
You are the team-lead. Delegate through the fleetd client — never launch workers yourself.
Roster: ask fleetd for current sessions/roles.
- w-claude-* — Claude Sonnet. Reasoning-heavy / high-accuracy subtasks.
- w-local-* — remote local LLM. Bulk, cheap, or parallelizable subtasks.
@@ -133,22 +133,22 @@ tool instead of hand-rolling the HTTP request.
## What this layer does NOT change
- **Delivery** is still `bridged` → herdr `pane.send_text` + status events (Message-Server).
- **Delivery** is still `fleetd` → herdr `pane.send_text` + status events (Message-Server).
- **Completion timing** is still the worker status event; **reply content** still rides the
worker `Stop`-hook envelope.
- **Single-host** still applies: herdr's socket is local, so the whole herd lives on the
`bridged` host. The lead may be remote — it only needs HTTP to `bridged`.
`fleetd` host. The lead may be remote — it only needs HTTP to `fleetd`.
## Open questions
- **Routing intelligence:** rubric-in-`CLAUDE.md` (lead decides) vs. a `bridged` role-router
- **Routing intelligence:** rubric-in-`CLAUDE.md` (lead decides) vs. a `fleetd` role-router
(label-based). Start with the former; promote to the latter if routing logic grows.
- **Backpressure:** per-role concurrency caps in `bridged` so a fan-out can't exhaust the
- **Backpressure:** per-role concurrency caps in `fleetd` so a fan-out can't exhaust the
local gateway.
- **Result schema:** whether reply envelopes should carry structured metadata (worker, model,
tokens) to help the lead's reduce step.
## Status
🟡 Design (2026-07-11). Orchestration layer over the selected `bridged` server; inherits
🟡 Design (2026-07-11). Orchestration layer over the selected `fleetd` server; inherits
herdr (chosen) + AgentAPI (fallback). Delivery unchanged — see the Message-Server design.
+4 -4
View File
@@ -76,7 +76,7 @@ worktree checks out anyway. Amber is the real gap — untracked local config the
3. **Never overlay the git plumbing** — the worktree's own `.git` file/branch is what gives
isolation; that's the *one* thing that must differ from the main tree.
The overlay set lives in config (`BridgedConfig.Worker.parityOverlay` — a list of repo-relative
The overlay set lives in config (`FleetConfig.Worker.parityOverlay` — a list of repo-relative
paths, with sane defaults) so it's auditable and per-repo tunable.
> **Trust note (deliberate).** Hydrating local config means the primary's local secrets/tokens
@@ -117,7 +117,7 @@ earlier `STATE.md` idea — a PR is reviewable, mergeable, and self-describing.*
- **Remote:** `ssh://git@git.ltms.dev:2224/fleet/fleetd.git` (gitea). Push is over **SSH** —
a worker running as the same user with the same keys can `git push` **with no extra credential**.
- **gitea is NOT in the project `.mcp.json`** (only `jetbrains`, `intellij-index`, `bridged`). The
- **gitea is NOT in the project `.mcp.json`** (only `jetbrains`, `intellij-index`, `fleetd`). The
primary's gitea MCP comes from a global/user config, so **workers do not inherit it**. A worker
gets only the `bridge` MCP mounted (via `--mcp-config` launch flag).
- **No gitea CLI** (`tea`) installed; `glab` is present but is the GitLab CLI (wrong backend).
@@ -153,11 +153,11 @@ and the token is a single scoped secret the daemon injects like it already injec
|---|---|---|
| Worktree provision/teardown | **CB-301 ext** — `SessionManager.acquire`/`release`; `WorkerSession` gains `worktree`, `branch` | daemon shells out to `git worktree add/remove` |
| **Config-parity overlay** | **CB-301 ext** — `SessionManager.acquire`, after `git worktree add` | symlink/copy the `parityOverlay` set into the worktree so the worker is a full peer; **this is what makes worktrees viable, not a dead-end** |
| Overlay config | `BridgedConfig.Worker.parityOverlay` — repo-relative paths, sane defaults | auditable, per-repo tunable; keep explicit + minimal (trust) |
| Overlay config | `FleetConfig.Worker.parityOverlay` — repo-relative paths, sane defaults | auditable, per-repo tunable; keep explicit + minimal (trust) |
| Branch naming | `worker/<ticket-slug>-<nonce>` off `main` (or a configured base) | one branch per session |
| Commit + push + PR handoff | **CB-302** — worker-driven, guided by the skill | push = SSH; PR = option A |
| Implementer skill | `.claude/skills/implementer/SKILL.md` | worktree-aware playbook (see below); mounts automatically since workers inherit repo cwd |
| gitea token injection | `WorkerService` env + `BridgedConfig` | repo-scoped, minimal perms |
| gitea token injection | `WorkerService` env + `FleetConfig` | repo-scoped, minimal perms |
| PR review + merge | Primary (has gitea MCP + judgment) | merge on green; the human/primary gate stays |
## Implementer skill (outline)
+6 -6
View File
@@ -1,6 +1,6 @@
# Worker startup: working directory & the folder-trust prompt
When `bridged` spawns a worker, the worker CLI may show an **interactive startup prompt** before it
When `fleetd` spawns a worker, the worker CLI may show an **interactive startup prompt** before it
is ready to accept a task — most importantly a *"Do you trust the files in this folder?"* dialog. An
unattended worker parked on that prompt never becomes injectable: the status-gated injector waits for
`idle`/`blocked`, the task is never delivered, and (worst case) a stray Enter answers the dialog
@@ -18,7 +18,7 @@ flowchart TD
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"]
D -->|"no (REST / off-host)"| F["cwd = bridged daemon cwd<br/>(never $HOME by assumption)"]
D -->|"no (REST / off-host)"| F["cwd = fleetd daemon cwd<br/>(never $HOME by assumption)"]
C --> G["ensureWorkspace → tab.create → agent.start {cwd}"]
E --> G
F --> G
@@ -52,9 +52,9 @@ only affect the seed shell, which the bridge closes).
|---|--------|------|
| 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 `fleet_spawn` caller | normal MCP spawn from the primary |
| 3 | The `bridged` daemon's own cwd | REST spawn / off-host caller — **never `$HOME`** |
| 3 | The `fleetd` 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
The primary's cwd (source 2) is discoverable with no new plumbing: `fleetd` already resolves the MCP
caller's loopback **peer PID** for connection identity (`ConnectionIdentity` → `LsofPeerPidLookup`);
the same PID yields its cwd via `lsof -a -p <pid> -d cwd -Fn` (the `n…` line). The primary maps to no
worker pane (it is not a worker), but its PID and cwd are still readable.
@@ -62,7 +62,7 @@ worker pane (it is not a worker), but its PID and cwd are still readable.
```mermaid
sequenceDiagram
participant P as "Primary (main)"
participant B as "bridged"
participant B as "fleetd"
participant O as "OS (lsof)"
participant H as "herdr"
P->>B: "fleet_spawn {profile} (no cwd)"
@@ -77,7 +77,7 @@ sequenceDiagram
*Figure 2 — a no-cwd spawn inherits the primary's directory from the caller's PID.*
> **Status:** implemented (CB-112). `bridged` threads the resolved `cwd` onto **`agent.start {cwd}`**
> **Status:** implemented (CB-112). `fleetd` threads the resolved `cwd` onto **`agent.start {cwd}`**
> (verified: the worker process is rooted there), keeping the single shared worker space. On an MCP
> `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`)