From 4b822731e6838058afe0def41e039a31bfdb85de Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sun, 23 Aug 2026 07:04:44 +0200 Subject: [PATCH] CB-632: rename the member's MCP mount bridge -> fleet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every launcher writes the bridge's MCP server into the config it hands its peer, and it named that server "bridge". So a member addressed its tools as mcp__bridge__fleet_send while the tools themselves are already fleet_*. The mount is named "fleet" now, and a member's tools are mcp__fleet__*. The name was a bare literal in three files: ClaudeCodeLauncher and LeadLauncher build a --mcp-config JSON string, OpenCodeLauncher writes an opencode.json node. Three hand-written copies of one name is how a rename lands in two of them, so the name is now one constant, PeerLauncher.MCP_MOUNT_NAME. The mount name is local to the peer — it is the label its own client puts on the server, and nothing in the daemon reads it back. Renaming it changes no wire call. Tests. Each launcher's test now asserts the mount is named fleet AND that nothing writes "bridge"; the second half is the part that would have caught a half-done rename. LeadLauncherTest never checked the name at all, only the URL, so it gained the assertion rather than had one updated. CLAUDE.md's role-detection ladder quoted mcp__bridge__* as the marker of a spawned member. It names mcp__fleet__* now, and says that a member spawned before this change still reports the old prefix. The portable block stays byte-identical with the wiki template (wiki 569a917). Build: cd bridged && mvn clean install, then read target/surefire-reports/*.xml directly — 884 tests, 0 failures, 0 errors. --- CLAUDE.md | 6 +++--- .../java/dev/ltms/fleet/lead/LeadLauncher.java | 4 +++- .../ltms/fleet/member/ClaudeCodeLauncher.java | 4 +++- .../dev/ltms/fleet/member/OpenCodeLauncher.java | 9 +++++---- .../java/dev/ltms/fleet/peer/PeerLauncher.java | 12 ++++++++++++ .../dev/ltms/fleet/lead/LeadLauncherTest.java | 5 ++++- .../fleet/member/ClaudeCodeLauncherTest.java | 7 +++++-- .../ltms/fleet/member/OpenCodeLauncherTest.java | 16 +++++++++------- 8 files changed, 44 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d5c0622..5dcf182 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,9 +26,9 @@ was bound to. Don't infer what you can ask. Only if that call is unavailable, fall back to these — each is one-way, so keep reading until one fires: the reply charter in your system prompt (*"You are a spawned member in the -claude-bridge fleet"*) ⇒ **spawned member**; bridge tools prefixed `mcp__bridge__*` ⇒ **spawned +claude-bridge fleet"*) ⇒ **spawned member**; fleet tools prefixed `mcp__fleet__*` ⇒ **spawned member** (the launcher fixes that mount name; a primary's mount is named by whoever wrote its -`.mcp.json`, so it varies); `ANTHROPIC_BASE_URL` set ⇒ **spawned member** (Claude-model members run +`.mcp.json`, so it varies — and a member spawned before CB-632 still says `mcp__bridge__*`); `ANTHROPIC_BASE_URL` set ⇒ **spawned member** (Claude-model members run on a clean env, so its *absence* proves nothing). None of these separate a worker from an architect — only `fleet_whoami` does. **Still unsure ⇒ act as a worker**, the most restricted member role. The two mistakes are not symmetric: a primary acting as a worker is refused by the authorization gate — @@ -271,7 +271,7 @@ Before you call any work done, check the row that matches what you touched: | a `fleet_*` tool — added, removed, renamed, or its params/semantics | the primary's intent→tool table; any rule that names that tool | | `Authz` / the role table | invariant 3, and the primary-only vs worker-only claims | | `ConnectionIdentity` / how a caller is resolved | the `fleet_whoami` paragraph and the fallback ladder | -| `REPLY_CHARTER`, or a launcher's mount/flags | the fallback ladder (`mcp__bridge__*`), and the layering table's top row | +| `REPLY_CHARTER`, or a launcher's mount/flags | the fallback ladder (`mcp__fleet__*`), and the layering table's top row | | the injector / status gating | invariant 4 | | worktree provisioning or the parity overlay | the "both roles read this file" premise — it rests on the worker's worktree being a checkout of this repo | | `.claude/skills/**` | the addendum's skill list, and the "name the playbook" rule | diff --git a/bridged/src/main/java/dev/ltms/fleet/lead/LeadLauncher.java b/bridged/src/main/java/dev/ltms/fleet/lead/LeadLauncher.java index de36d1e..dd5e565 100644 --- a/bridged/src/main/java/dev/ltms/fleet/lead/LeadLauncher.java +++ b/bridged/src/main/java/dev/ltms/fleet/lead/LeadLauncher.java @@ -17,6 +17,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import dev.ltms.fleet.peer.PeerLauncher; /** * Starts the leads {@code fleet.leaders:} declares, when none is already running (CB-558). @@ -252,7 +253,8 @@ public final class LeadLauncher { List argv = new ArrayList<>(profile.argv()); if (profile.hasMcp()) { argv.add("--mcp-config"); - argv.add("{\"mcpServers\":{\"bridge\":{\"type\":\"http\",\"url\":\"" + argv.add("{\"mcpServers\":{\"" + PeerLauncher.MCP_MOUNT_NAME + + "\":{\"type\":\"http\",\"url\":\"" + profile.mcpUrl() + "\"}}}"); } // Appended last, for the same reason the member launcher does it (CB-533): the argv is diff --git a/bridged/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java b/bridged/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java index 3a39d11..c972731 100644 --- a/bridged/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java +++ b/bridged/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java @@ -6,6 +6,7 @@ import dev.ltms.fleet.herdr.Agent; import dev.ltms.fleet.herdr.AgentControl; import dev.ltms.fleet.herdr.WorkspaceControl; import dev.ltms.fleet.peer.Capability; +import dev.ltms.fleet.peer.PeerLauncher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -298,7 +299,8 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher { } List argv = mutableArgv(cfg.argv()); if (cfg.hasMcp()) { - String mcpJson = "{\"mcpServers\":{\"bridge\":{\"type\":\"http\",\"url\":\"" + String mcpJson = "{\"mcpServers\":{\"" + PeerLauncher.MCP_MOUNT_NAME + + "\":{\"type\":\"http\",\"url\":\"" + cfg.mcpUrl() + "\"}}}"; argv.add("--mcp-config"); argv.add(mcpJson); diff --git a/bridged/src/main/java/dev/ltms/fleet/member/OpenCodeLauncher.java b/bridged/src/main/java/dev/ltms/fleet/member/OpenCodeLauncher.java index f018f9b..ff9b31b 100644 --- a/bridged/src/main/java/dev/ltms/fleet/member/OpenCodeLauncher.java +++ b/bridged/src/main/java/dev/ltms/fleet/member/OpenCodeLauncher.java @@ -9,6 +9,7 @@ import dev.ltms.fleet.herdr.WorkspaceControl; import dev.ltms.fleet.peer.Capability; import dev.ltms.fleet.peer.CharterReceipt; import dev.ltms.fleet.peer.PeerHandle; +import dev.ltms.fleet.peer.PeerLauncher; import dev.ltms.fleet.peer.SpawnRequest; import java.io.IOException; @@ -321,10 +322,10 @@ public final class OpenCodeLauncher extends HerdrPeerLauncher { } if (cfg.hasMcp()) { - ObjectNode bridge = root.putObject("mcp").putObject("bridge"); - bridge.put("type", "remote"); - bridge.put("url", cfg.mcpUrl()); - bridge.put("enabled", true); + ObjectNode mount = root.putObject("mcp").putObject(PeerLauncher.MCP_MOUNT_NAME); + mount.put("type", "remote"); + mount.put("url", cfg.mcpUrl()); + mount.put("enabled", true); } if (hasCustomProvider(cfg)) { addCustomProvider(root, cfg); diff --git a/bridged/src/main/java/dev/ltms/fleet/peer/PeerLauncher.java b/bridged/src/main/java/dev/ltms/fleet/peer/PeerLauncher.java index e3ef717..dd4d583 100644 --- a/bridged/src/main/java/dev/ltms/fleet/peer/PeerLauncher.java +++ b/bridged/src/main/java/dev/ltms/fleet/peer/PeerLauncher.java @@ -18,6 +18,18 @@ import java.util.Set; */ public interface PeerLauncher { + /** + * The name every launcher gives the bridge's MCP server in the config it writes for its peer. + * The peer's tools are addressed as {@code mcp____fleet_*}, and {@code CLAUDE.md}'s + * role-detection ladder names that prefix, so the two must agree. + * + *

It is a constant because three launchers write it — {@code ClaudeCodeLauncher} and + * {@code LeadLauncher} into a {@code --mcp-config} literal, {@code OpenCodeLauncher} into an + * {@code opencode.json} node. Three hand-written copies of one name is how a rename lands in + * two of them (CB-632). + */ + String MCP_MOUNT_NAME = "fleet"; + /** * The set of {@link Capability capabilities} this launcher declares. A peer whose profile * opts into a git-forge token should include {@link Capability#SELF_PR}; the base set for diff --git a/bridged/src/test/java/dev/ltms/fleet/lead/LeadLauncherTest.java b/bridged/src/test/java/dev/ltms/fleet/lead/LeadLauncherTest.java index 7df86c6..1616a92 100644 --- a/bridged/src/test/java/dev/ltms/fleet/lead/LeadLauncherTest.java +++ b/bridged/src/test/java/dev/ltms/fleet/lead/LeadLauncherTest.java @@ -185,7 +185,10 @@ class LeadLauncherTest { List args = startedArgs(herdr); assertTrue(args.contains("--mcp-config")); - assertTrue(args.stream().anyMatch(a -> a.contains("http://127.0.0.1:8765/mcp")), args.toString()); + assertTrue(args.stream().anyMatch(a -> a.contains("\"fleet\"") + && a.contains("http://127.0.0.1:8765/mcp")), args.toString()); + assertTrue(args.stream().noneMatch(a -> a.contains("\"bridge\"")), + "the mount is named fleet since CB-632"); assertEquals("claude-opus-5", args.get(args.indexOf("--model") + 1)); assertTrue(args.indexOf("--model") > args.indexOf("--mcp-config"), "--model is appended last so it outranks the ccs wrapper (CB-533)"); diff --git a/bridged/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java b/bridged/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java index 8d5af20..1659739 100644 --- a/bridged/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java +++ b/bridged/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java @@ -54,8 +54,11 @@ class ClaudeCodeLauncherTest { List args = spawnedArgs(herdr); assertTrue(args.contains("--mcp-config")); - assertTrue(args.stream().anyMatch(a -> a.contains("\"bridge\"") && a.contains("http://127.0.0.1:8765/mcp")), - "inline bridge MCP config present"); + assertTrue(args.stream().anyMatch(a -> a.contains("\"fleet\"") && a.contains("http://127.0.0.1:8765/mcp")), + "inline fleet MCP config present"); + assertTrue(args.stream().noneMatch(a -> a.contains("\"bridge\"")), + "the mount is named fleet since CB-632 — a member addresses its tools as " + + "mcp__fleet__*, and CLAUDE.md's role-detection ladder names that prefix"); assertTrue(args.contains("--append-system-prompt")); assertTrue(args.stream().anyMatch(a -> a.contains("fleet_reply")), "reply charter present"); } diff --git a/bridged/src/test/java/dev/ltms/fleet/member/OpenCodeLauncherTest.java b/bridged/src/test/java/dev/ltms/fleet/member/OpenCodeLauncherTest.java index c52b751..9643b3c 100644 --- a/bridged/src/test/java/dev/ltms/fleet/member/OpenCodeLauncherTest.java +++ b/bridged/src/test/java/dev/ltms/fleet/member/OpenCodeLauncherTest.java @@ -99,11 +99,13 @@ class OpenCodeLauncherTest { JsonNode json = new ObjectMapper().readTree(Path.of(cfgPath).toFile()); assertTrue(json.path("compaction").path("auto").asBoolean(), "spawned opencode peers explicitly enable automatic compaction"); - JsonNode bridge = json.path("mcp").path("bridge"); - assertEquals("remote", bridge.path("type").asText(), "bridge is mounted as a remote MCP server"); - assertEquals("http://127.0.0.1:8765/mcp", bridge.path("url").asText(), - "the profile's bridge MCP url is present"); - assertTrue(bridge.path("enabled").asBoolean(), "the bridge server is enabled"); + JsonNode mount = json.path("mcp").path("fleet"); + assertEquals("remote", mount.path("type").asText(), "the fleet MCP server is mounted as remote"); + assertEquals("http://127.0.0.1:8765/mcp", mount.path("url").asText(), + "the profile's fleet MCP url is present"); + assertTrue(mount.path("enabled").asBoolean(), "the fleet server is enabled"); + assertTrue(json.path("mcp").path("bridge").isMissingNode(), + "the mount is named fleet since CB-632, not bridge"); assertTrue(json.path("instructions").isArray() && !json.path("instructions").isEmpty(), "the member charter is mounted via instructions"); @@ -473,8 +475,8 @@ class OpenCodeLauncherTest { JsonNode json = new ObjectMapper() .readTree(Path.of(startEnv(herdr).get("OPENCODE_CONFIG")).toFile()); - assertEquals("remote", json.path("mcp").path("bridge").path("type").asText(), - "pinning an endpoint must not drop the bridge MCP mount"); + assertEquals("remote", json.path("mcp").path("fleet").path("type").asText(), + "pinning an endpoint must not drop the fleet MCP mount"); assertFalse(json.path("provider").path("local-vllm").isMissingNode(), "and the provider block is still declared alongside it"); assertTrue(json.path("instructions").isArray() && !json.path("instructions").isEmpty(),