diff --git a/bridged/src/main/java/dev/ltms/bridged/member/ClaudeCodeLauncher.java b/bridged/src/main/java/dev/ltms/bridged/member/ClaudeCodeLauncher.java index b237e44..3a9c42e 100644 --- a/bridged/src/main/java/dev/ltms/bridged/member/ClaudeCodeLauncher.java +++ b/bridged/src/main/java/dev/ltms/bridged/member/ClaudeCodeLauncher.java @@ -181,8 +181,8 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher { // id via -r and passes no --session-id (the two conflict). Both are injected before the // model flag so --model keeps outranking the operator's own argv. // mutableArgv: argvWithBridge may hand back the profile's own (immutable) List.of when it - // has no MCP — session flags must be added into a list we own. - List argv = mutableArgv(argvWithBridge(cfg)); + // has neither MCP nor a charter — session flags must be added into a list we own. + List argv = mutableArgv(argvWithBridge(cfg, spec.charter())); String agentSessionId = applySessionIdentity(argv, spec.sessionName(), spec.resumeSessionId()); return new Launch(workerEnv, argvWithModel(argv, cfg), agentSessionId); } @@ -217,22 +217,26 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher { } /** - * The launch argv, plus — when {@code worker.mcpUrl} is set — inline {@code --mcp-config} for - * the bridge server and {@code --append-system-prompt} for the {@link #REPLY_CHARTER}. Neither - * touches the profile's config; both are pure command-line flags. This inline-flag mount is - * Claude Code specific — other adapters mount MCP and instructions their own way. + * The launch argv, plus an inline {@code --mcp-config} when {@code worker.mcpUrl} is set and + * {@code --append-system-prompt} when the base composed a charter. Neither touches the profile's + * config; both are pure command-line flags. This inline-flag mount is Claude Code specific — + * other adapters mount MCP and instructions their own way. */ - private List argvWithBridge(BridgedConfig.Profile cfg) { - if (!cfg.hasMcp()) { + private List argvWithBridge(BridgedConfig.Profile cfg, String charter) { + if (!cfg.hasMcp() && charter == null) { return cfg.argv(); } - String mcpJson = "{\"mcpServers\":{\"bridge\":{\"type\":\"http\",\"url\":\"" - + cfg.mcpUrl() + "\"}}}"; List argv = mutableArgv(cfg.argv()); - argv.add("--mcp-config"); - argv.add(mcpJson); - argv.add("--append-system-prompt"); - argv.add(REPLY_CHARTER); + if (cfg.hasMcp()) { + String mcpJson = "{\"mcpServers\":{\"bridge\":{\"type\":\"http\",\"url\":\"" + + cfg.mcpUrl() + "\"}}}"; + argv.add("--mcp-config"); + argv.add(mcpJson); + } + if (charter != null) { + argv.add("--append-system-prompt"); + argv.add(charter); + } return argv; } diff --git a/bridged/src/test/java/dev/ltms/bridged/member/ClaudeCodeLauncherTest.java b/bridged/src/test/java/dev/ltms/bridged/member/ClaudeCodeLauncherTest.java index 049ca30..b50dbba 100644 --- a/bridged/src/test/java/dev/ltms/bridged/member/ClaudeCodeLauncherTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/member/ClaudeCodeLauncherTest.java @@ -102,6 +102,53 @@ class ClaudeCodeLauncherTest { "the operator's own args are preserved, in order, ahead of the model flag"); } + @Test + void appendsTheBaseComposedRoleAndReplyCharter() { + FakeHerdr herdr = new FakeHerdr(); + String roleCharter = "You review changes."; + BridgedConfig.Profile cfg = new BridgedConfig.Profile( + "sonnet", "http://gx00.gw:8000", null, null, "BRIDGED_WORKER_TOKEN", + List.of("claude"), "tab", "bridged-workers", "w #{n}", + "http://127.0.0.1:8765/mcp", null, null); + ClaudeCodeLauncher svc = new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr), + new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> null, + 0, 0L, () -> fleet(Map.of("reviewer", roleCharter), null)); + + svc.spawn(new SpawnRequest("sonnet", null, null, null, null, MemberRole.REVIEWER)); + + List args = spawnedArgs(herdr); + int flag = args.indexOf("--append-system-prompt"); + assertEquals(1, args.stream().filter("--append-system-prompt"::equals).count(), + "the composed charter is passed once"); + assertEquals(roleCharter + "\n\n" + HerdrPeerLauncher.REPLY_CHARTER, args.get(flag + 1), + "the role charter comes first and the reply rule comes last"); + } + + @Test + void profileWithoutMcpOrRoleCharterGetsNoSystemPrompt() { + FakeHerdr herdr = new FakeHerdr(); + ClaudeCodeLauncher svc = labelService(herdr, () -> fleet(Map.of(), null)); + + svc.spawn(new SpawnRequest("sonnet", null, null, null, null, MemberRole.DEV)); + + assertFalse(spawnedArgs(herdr).contains("--append-system-prompt")); + } + + @Test + void profileWithoutMcpStillGetsItsRoleCharter() { + FakeHerdr herdr = new FakeHerdr(); + String roleCharter = "You design changes."; + ClaudeCodeLauncher svc = labelService(herdr, () -> fleet(Map.of("architect", roleCharter), null)); + + svc.spawn(new SpawnRequest("sonnet", null, null, null, null, MemberRole.ARCHITECT)); + + List args = spawnedArgs(herdr); + int flag = args.indexOf("--append-system-prompt"); + assertTrue(flag >= 0, "a role charter does not need an MCP mount"); + assertEquals(roleCharter, args.get(flag + 1)); + assertFalse(args.contains("--mcp-config")); + } + private ClaudeCodeLauncher multiProfile(FakeHerdr herdr) { BridgedConfig.Profile gx10 = new BridgedConfig.Profile("gx10", "http://gx10.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN", List.of("claude"), "tab", "bridged-workers", "w #{n}", null, null, null);