Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32bf324a1e | |||
| 65c9deb4d1 | |||
| 28ae27b8e1 | |||
| 0a2b3a4a56 |
@@ -88,15 +88,28 @@ bind:
|
||||
# backoffMs: 60000
|
||||
# quietNudgeCap: 3
|
||||
|
||||
# Fleet health detection is dormant unless enabled. It reads one whole-fleet agent list per tick.
|
||||
# It can run without a webhook; bridge_list then reports healthCoverage: detection-only.
|
||||
# Fleet health detection is dormant unless enabled (CB-573). It reads one whole-fleet agent list
|
||||
# per tick.
|
||||
# intervalSeconds → how often a tick runs (default 30). ENFORCED floor of 15: the code computes
|
||||
# Math.max(15, intervalSeconds), so a lower value is silently raised, not
|
||||
# rejected.
|
||||
# workingSuspectAfterSeconds, paneProbeIntervalSeconds → accepted and parsed, but NOT YET READ by
|
||||
# anything — the dormant monitor only consumes intervalSeconds today (CB-573
|
||||
# shipped ahead of the evidence publishers these two knobs are for). Setting
|
||||
# them changes nothing right now, and no minimum is enforced on either, because
|
||||
# nothing reads them to enforce one. They exist so a later build can start
|
||||
# honouring them without another config-shape change.
|
||||
# notifications.mode → "webhook" flips what bridge_list REPORTS (healthCoverage: "full" instead
|
||||
# of "detection-only") — it does NOT make bridged send any webhook call; no
|
||||
# delivery mechanism is implemented yet. Any other value, or omitting the
|
||||
# block, reports "detection-only".
|
||||
# health:
|
||||
# enabled: true
|
||||
# intervalSeconds: 30 # minimum 15
|
||||
# workingSuspectAfterSeconds: 600 # minimum 300
|
||||
# paneProbeIntervalSeconds: 60 # minimum 60
|
||||
# intervalSeconds: 30
|
||||
# workingSuspectAfterSeconds: 600
|
||||
# paneProbeIntervalSeconds: 60
|
||||
# notifications:
|
||||
# mode: disabled # disabled (default) or webhook
|
||||
# mode: disabled
|
||||
|
||||
# herdr Unix socket. Omit to use the client default
|
||||
# (${HERDR_SOCKET_PATH:-~/.config/herdr/herdr.sock}).
|
||||
@@ -286,6 +299,11 @@ placement: weighted
|
||||
# / credentialId. Those are hot because the placement policy (and, for credentialId,
|
||||
# the CB-578 stage B quarantine check) reads them through a supplier — being config is
|
||||
# not by itself enough to make a key hot.
|
||||
# EXCEPT `fleet.leaders`: Bridged.main reads it once at startup to build the lead tab
|
||||
# scanner and launcher, and neither is rebuilt on reload. A changed/added/removed
|
||||
# `fleet.leaders` entry is silently accepted — the reload reports "config reloaded"
|
||||
# with nothing in the deferred list — but has NO effect until you restart. Treat it
|
||||
# as deferred in practice, even though today's reload output does not say so.
|
||||
# DEFERRED → accepted into the new config, but the wiring built at startup keeps the old value
|
||||
# until you restart: `lifecycle:`, `leadHeartbeat:`, `guard:`, `worktreeRoot:`,
|
||||
# `spawnReadyTimeoutMs` / `spawnReadyPollMs`, `quarantineCooldownSeconds` (CB-578
|
||||
@@ -368,6 +386,12 @@ fleet:
|
||||
# An auto-launched lead is NOT a member: it gets no worker reply charter, is never registered with
|
||||
# the session lifecycle (the idle reaper would kill your orchestrator), and stays on the
|
||||
# subscription — ANTHROPIC_BASE_URL/AUTH_TOKEN are stripped from its env whatever the profile says.
|
||||
#
|
||||
# GET THE `tab:` VALUE RIGHT. A pane that does not match any configured `tab:` (a typo, a renamed
|
||||
# tab, a pane no entry names at all) is not recognised as a lead — it resolves as an ordinary
|
||||
# WORKER instead, silently, and every orchestration call it makes (spawn/stop/send/drain) is
|
||||
# refused. There is no error at startup for this: an unmatched pane is simply not a lead. If your
|
||||
# primary suddenly can't spawn or send, check this section first.
|
||||
# leaders:
|
||||
# opus-5.0:
|
||||
# profile: opus # omit to never create this lead, only recognise it
|
||||
@@ -423,10 +447,15 @@ guard:
|
||||
# idleTtlSeconds → reap READY/DONE sessions idle longer than this (never BUSY/SPAWNING)
|
||||
# contextCap → force-release a session after this many delegated turns
|
||||
# drainTimeoutSeconds → seconds to wait for BUSY sessions on shutdown before forced teardown
|
||||
# clearAfterTurn → whether a reusable worker discards its conversation context after every
|
||||
# completed delegated turn (default false). Works for claude-code workers
|
||||
# only — any other peer kind (e.g. opencode) logs "context reset is
|
||||
# unsupported for peer kind …" once and the reset is a no-op.
|
||||
# lifecycle:
|
||||
# idleTtlSeconds: 300
|
||||
# contextCap: 10
|
||||
# drainTimeoutSeconds: 5
|
||||
# clearAfterTurn: false
|
||||
|
||||
# Durable reply delivery (CB-307 Stage 2). OMIT this block entirely to keep the default
|
||||
# in-memory, soft-state reply inbox (late worker replies are held only until a daemon bounce).
|
||||
|
||||
@@ -967,8 +967,12 @@ public record BridgedConfig(
|
||||
/**
|
||||
* Top-level keys this version understands. Used only to warn about the rest — see
|
||||
* {@link #warnUnknownTopLevelKeys}. Keep in step with the record components.
|
||||
*
|
||||
* <p>Package-private (not {@code private}) so a test can assert every key here is documented in
|
||||
* {@code bridged.example.yaml} — the only committed description of the config schema, since
|
||||
* {@code bridged.yaml} itself is gitignored.
|
||||
*/
|
||||
private static final Set<String> KNOWN_TOP_LEVEL_KEYS = Set.of(
|
||||
static final Set<String> KNOWN_TOP_LEVEL_KEYS = Set.of(
|
||||
"bind", "herdrSocket", "profiles", "guard", "worktreeRoot",
|
||||
"lifecycle", "spawnReadyTimeoutMs", "spawnReadyPollMs", "broker", "primary", "fleet",
|
||||
"leadHeartbeat", "health", "placement", "auth", "configReload", "quarantineCooldownSeconds");
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@@ -1189,6 +1190,79 @@ class BridgedConfigTest {
|
||||
assertEquals(5, cfg.leadHeartbeat().quietNudgeCap());
|
||||
}
|
||||
|
||||
/**
|
||||
* A top-level key {@code BridgedConfig} reads but that appears nowhere in
|
||||
* {@code bridged.example.yaml} — live or commented — is invisible drift: {@code bridged.yaml}
|
||||
* is gitignored, so the example is the ONLY committed description of the config schema, and
|
||||
* neither {@link #shippedExampleConfigParses} (example → code: does the example still parse)
|
||||
* nor {@link #everyOptionalKnobDocumentedInTheExampleBinds} (a hand-maintained list of keys
|
||||
* that must bind) can catch a brand-new key nobody added to either.
|
||||
*
|
||||
* <p>This test compares the OTHER direction: every key in {@link BridgedConfig#KNOWN_TOP_LEVEL_KEYS}
|
||||
* (the parser's own accepted set, which backs the unknown-key WARN) must appear as a top-level
|
||||
* key in the example text, live or commented-out — see {@link #topLevelKeyDocumented}.
|
||||
*/
|
||||
@Test
|
||||
void everyKnownTopLevelKeyIsDocumentedInTheExample() throws Exception {
|
||||
Path example = Path.of("bridged.example.yaml");
|
||||
assertTrue(Files.exists(example), "bridged.example.yaml must ship next to the pom");
|
||||
String text = Files.readString(example);
|
||||
|
||||
List<String> undocumented = BridgedConfig.KNOWN_TOP_LEVEL_KEYS.stream()
|
||||
.filter(key -> !topLevelKeyDocumented(text, key))
|
||||
.sorted()
|
||||
.toList();
|
||||
|
||||
assertTrue(undocumented.isEmpty(), () -> "key(s) " + undocumented
|
||||
+ " are read by BridgedConfig but appear nowhere in bridged.example.yaml — "
|
||||
+ "document each one there, commented out if optional. bridged.yaml is "
|
||||
+ "gitignored, so this file is the only committed description of the config "
|
||||
+ "schema an operator or a worker can see.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Most of {@code bridged.example.yaml} is deliberately commented out — optional sections are
|
||||
* documented as commented blocks so the shipped file stays a working minimal config. A key
|
||||
* documented ONLY as a comment must still count as documented; parsing the file as YAML and
|
||||
* reading its live key set (as an earlier attempt at this guard did) gets this wrong, because
|
||||
* every commented section then looks entirely absent.
|
||||
*/
|
||||
@Test
|
||||
void commentedOnlyTopLevelKeyCountsAsDocumented() {
|
||||
String yaml = """
|
||||
bind:
|
||||
port: 8765
|
||||
# broker:
|
||||
# uri: amqp://guest:guest@127.0.0.1:5672
|
||||
""";
|
||||
assertTrue(topLevelKeyDocumented(yaml, "broker"),
|
||||
"a key documented only inside a commented-out block must still count as documented");
|
||||
}
|
||||
|
||||
/** A key that appears in neither a live nor a commented top-level line must NOT count. */
|
||||
@Test
|
||||
void absentTopLevelKeyIsNotDocumented() {
|
||||
String yaml = """
|
||||
bind:
|
||||
port: 8765
|
||||
""";
|
||||
assertFalse(topLevelKeyDocumented(yaml, "broker"),
|
||||
"a key mentioned nowhere in the example must not be reported as documented");
|
||||
}
|
||||
|
||||
/**
|
||||
* True when {@code key} appears as a top-level YAML key in {@code yaml} — either live
|
||||
* ({@code key:} at column 0) or commented out ({@code # key:}, also at column 0, with only
|
||||
* whitespace between the {@code #} and the key). Anchoring on column 0 is what keeps this a
|
||||
* top-level check: an indented occurrence (a nested field, or prose inside a comment that
|
||||
* happens to end in a colon) never matches, because {@code ^} requires the key's own first
|
||||
* character — or the sole leading {@code #} — to sit at the very start of the line.
|
||||
*/
|
||||
private static boolean topLevelKeyDocumented(String yaml, String key) {
|
||||
Pattern p = Pattern.compile("(?m)^(?:#\\s*)?" + Pattern.quote(key) + ":");
|
||||
return p.matcher(yaml).find();
|
||||
}
|
||||
|
||||
@Test
|
||||
void placementDefaultsToFixedForExistingConfigs(@TempDir Path dir) throws Exception {
|
||||
Path f = dir.resolve("no-placement.yaml");
|
||||
|
||||
Reference in New Issue
Block a user