diff --git a/.claude/agents/architect.md b/.claude/agents/architect.md index 5be684b..76081f8 100644 --- a/.claude/agents/architect.md +++ b/.claude/agents/architect.md @@ -1,4 +1,5 @@ --- +name: architect description: Refine work into clear, independent units before implementation. --- diff --git a/.claude/agents/dev.md b/.claude/agents/dev.md index c336bee..21c5095 100644 --- a/.claude/agents/dev.md +++ b/.claude/agents/dev.md @@ -1,4 +1,5 @@ --- +name: dev description: Implement one assigned unit, test it, and open a pull request. --- diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md index 1c0162a..d4fdd1a 100644 --- a/.claude/agents/reviewer.md +++ b/.claude/agents/reviewer.md @@ -1,4 +1,5 @@ --- +name: reviewer description: Review one assigned scope and report the most important real issue. --- 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 2d190d7..f1e196e 100644 --- a/bridged/src/main/java/dev/ltms/bridged/member/ClaudeCodeLauncher.java +++ b/bridged/src/main/java/dev/ltms/bridged/member/ClaudeCodeLauncher.java @@ -279,9 +279,15 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher { * single inline argv element — herdr refuses to shell-encode a multi-line argument * ({@code invalid_agent_argument}). It is written to a temp file instead and mounted with * {@code --append-system-prompt-file}, which this host confirms Claude Code accepts for a - * multi-line file. The reply charter is always exactly one line and demonstrably encodes, and it - * must reach a member with no repo checkout (nowhere to put a file), so it keeps its inline - * {@code --append-system-prompt} delivery. + * multi-line file. + * + *

CB-618: Claude Code refuses to start when BOTH {@code --append-system-prompt} and + * {@code --append-system-prompt-file} are on the command line ("Cannot use both ... Please use + * only one"), so the two charters can never travel on separate flags. When both are present they + * are concatenated into the one file, role charter first and reply charter last — last is where + * the reply rule must sit, because it is the rule that must survive. When only the reply charter + * is present it keeps its proven inline {@code --append-system-prompt} delivery, which is also + * the only form that reaches a member with no repo checkout. */ private List argvWithBridge(BridgedConfig.Profile cfg, LaunchSpec spec) { String roleCharter = nonBlank(spec.roleCharter()); @@ -298,10 +304,10 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher { argv.add(mcpJson); } if (roleCharter != null) { + String combined = replyCharter == null ? roleCharter : roleCharter + "\n\n" + replyCharter; argv.add("--append-system-prompt-file"); - argv.add(writeRoleCharterFile(roleCharter).toString()); - } - if (replyCharter != null) { + argv.add(writeCharterFile(combined).toString()); + } else if (replyCharter != null) { argv.add("--append-system-prompt"); argv.add(replyCharter); } @@ -325,10 +331,10 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher { * reads this file (the spawned peer) outlives this JVM call and there is no spawn-scoped teardown * hook to delete it synchronously. */ - private static Path writeRoleCharterFile(String roleCharter) { + private static Path writeCharterFile(String charterText) { try { Path file = Files.createTempFile("bridged-role-charter-", ".md"); - Files.writeString(file, roleCharter); + Files.writeString(file, charterText); file.toFile().deleteOnExit(); return file; } catch (IOException e) { 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 8087626..144c279 100644 --- a/bridged/src/test/java/dev/ltms/bridged/member/ClaudeCodeLauncherTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/member/ClaudeCodeLauncherTest.java @@ -109,17 +109,22 @@ class ClaudeCodeLauncherTest { "the operator's own args are preserved, in order, ahead of the model flag"); } - // --- CB-617: the role charter travels by file, never inline; the reply charter stays inline --- + // --- CB-617/CB-618: both charters travel in ONE file; the reply charter alone stays inline --- /** * CB-617: herdr refuses to shell-encode a multi-line inline argv argument * ({@code invalid_agent_argument}) — the exact failure this reproduced on profile {@code opus}. * The role charter is operator-authored and often multi-line, so it must never appear as an argv - * element; the one-line reply charter still may. This drives the real launcher entry point - * ({@code spawn}), the same path a live spawn takes — not the argv builder in isolation. + * element. + * + *

CB-618: and Claude Code itself refuses to start when both {@code --append-system-prompt} and + * {@code --append-system-prompt-file} are given ("Cannot use both ... Please use only one"), so + * the reply charter cannot ride inline alongside a role charter either. Both go in the one file, + * reply charter last. This drives the real launcher entry point ({@code spawn}), the same path a + * live spawn takes — not the argv builder in isolation. */ @Test - void roleCharterTravelsByFileAndReplyCharterStaysInline() { + void bothChartersTravelInOneFileAndNeverOnBothFlags() { FakeHerdr herdr = new FakeHerdr(); String roleCharter = "You review changes.\nLine two.\nLine three."; BridgedConfig.Profile cfg = new BridgedConfig.Profile( @@ -138,15 +143,15 @@ class ClaudeCodeLauncherTest { int fileFlag = args.indexOf("--append-system-prompt-file"); assertTrue(fileFlag >= 0, "the role charter is mounted via --append-system-prompt-file: " + args); + assertFalse(args.contains("--append-system-prompt"), + "CB-618: Claude Code refuses to start with both flags — the reply charter must not " + + "ride inline beside a role charter: " + args); assertDoesNotThrow(() -> { String written = Files.readString(Path.of(args.get(fileFlag + 1))); - assertEquals(roleCharter, written, "the file holds exactly the role charter text"); + assertTrue(written.startsWith(roleCharter), "the file opens with the role charter: " + written); + assertTrue(written.endsWith(HerdrPeerLauncher.REPLY_CHARTER), + "the reply charter is last — it is the rule that must survive: " + written); }, "the --append-system-prompt-file path must be a readable file"); - - int inlineFlag = args.indexOf("--append-system-prompt"); - assertTrue(inlineFlag >= 0, "the reply charter still travels inline: " + args); - assertEquals(HerdrPeerLauncher.REPLY_CHARTER, args.get(inlineFlag + 1), - "only the reply charter (one line) rides inline — the role charter never does"); } @Test