CB-618: one charter file, and agent files need a name:
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:
@@ -1,4 +1,5 @@
|
||||
---
|
||||
name: architect
|
||||
description: Refine work into clear, independent units before implementation.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
name: dev
|
||||
description: Implement one assigned unit, test it, and open a pull request.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
name: reviewer
|
||||
description: Review one assigned scope and report the most important real issue.
|
||||
---
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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
|
||||
|
||||
Reference in New Issue
Block a user