CB-618: one charter file, and agent files need a name:
CI / contract (push) Successful in 1m10s
CI / build (push) Successful in 1m27s

Two defects in CB-617 that only a live spawn could find. Both were shipped
green: every test passed because every test read the argv we built, and none
ran the binary that has to accept it.

1. Claude Code refuses to start when both --append-system-prompt and
   --append-system-prompt-file are on the command line:

     Error: Cannot use both --append-system-prompt and
     --append-system-prompt-file. Please use only one.

   CB-617 put the role charter on the file flag and left the reply charter on
   the inline flag, so every Claude-profile spawn with a role charter died at
   launch. The pane exited on its own and bridged reported it as
   spawn_timeout ("did not reach injectable state within 20000ms"), which
   hides the real cause.

   Both charters now go in 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. A member with only a reply charter keeps the proven inline
   flag, which is also the only form that reaches a member with no repo
   checkout.

2. A Claude Code agent definition needs `name:` in its frontmatter. Ours had
   only `description:`, so the files were skipped and --agent architect failed
   with "not found. Available agents: claude, Explore, ...". Added to all
   three. The OpenCode files take their name from the filename and are
   unchanged.

Checked on this host, in this repo, with the real binary:

  claude --model claude-sonnet-5 --agent architect \
    --append-system-prompt-file /tmp/combined.md -p '...'
  -> ROLEOK, REPLYOK, yes

so the agent definition, the role charter and the reply charter all compose.

The updated test now asserts the constraint that actually binds: with a role
charter present, --append-system-prompt must be absent, and the file must open
with the role charter and end with the reply charter.

874 tests pass.
This commit is contained in:
Dai Ha
2026-08-22 12:32:44 +02:00
parent 37edd9134b
commit cc47672b7c
5 changed files with 32 additions and 18 deletions
@@ -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.
*
* <p>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<String> 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) {