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(),