diff --git a/bridged/src/main/java/dev/ltms/fleet/metrics/FleetMetrics.java b/bridged/src/main/java/dev/ltms/fleet/metrics/FleetMetrics.java
index f6ad8f4..3961755 100644
--- a/bridged/src/main/java/dev/ltms/fleet/metrics/FleetMetrics.java
+++ b/bridged/src/main/java/dev/ltms/fleet/metrics/FleetMetrics.java
@@ -13,31 +13,31 @@ import java.util.Map;
*
*
The set is deliberately small: each series maps to a failure mode this project has actually
* hit, not to whatever was easy to count. The two worth watching in practice are
- * {@code bridged_sends_total{outcome="completion_fallback"}} — a rising share means turn detection
+ * {@code fleet_sends_total{outcome="completion_fallback"}} — a rising share means turn detection
* is degrading, the CB-115/116/118 failure family — and
- * {@code bridged_push_nudges_total{outcome="exhausted"}}, which means the primary stopped draining
+ * {@code fleet_push_nudges_total{outcome="exhausted"}}, which means the primary stopped draining
* its inbox and CB-307's active push gave up.
*/
public final class FleetMetrics {
/** Counter: delegated sends by terminal outcome. */
- public static final String SENDS = "bridged_sends_total";
+ public static final String SENDS = "fleet_sends_total";
/** Counter: worker replies by the path that carried them (rendezvous vs stranded-to-inbox). */
- public static final String REPLIES = "bridged_replies_total";
+ public static final String REPLIES = "fleet_replies_total";
/** Counter: push-loop nudges to the primary, by outcome. */
- public static final String PUSH_NUDGES = "bridged_push_nudges_total";
+ public static final String PUSH_NUDGES = "fleet_push_nudges_total";
/** Counter: idle-lead heartbeat nudges to the lead, by outcome (CB-551). */
- public static final String HEARTBEAT_NUDGES = "bridged_lead_heartbeat_nudges_total";
+ public static final String HEARTBEAT_NUDGES = "fleet_lead_heartbeat_nudges_total";
/** Counter: spawn attempts by peer kind and outcome. */
- public static final String SPAWNS = "bridged_spawns_total";
+ public static final String SPAWNS = "fleet_spawns_total";
/** Counter: herdr socket calls by method and outcome. */
- public static final String HERDR_CALLS = "bridged_herdr_calls_total";
+ public static final String HERDR_CALLS = "fleet_herdr_calls_total";
/** Counter: rejected requests by reason (CB-501). */
- public static final String AUTH_FAILURES = "bridged_auth_failures_total";
+ public static final String AUTH_FAILURES = "fleet_auth_failures_total";
/** Gauge: session census by lifecycle state. */
- public static final String SESSIONS = "bridged_sessions";
+ public static final String SESSIONS = "fleet_sessions";
/** Gauge: undrained replies held per target. */
- public static final String INBOX_DEPTH = "bridged_inbox_depth";
+ public static final String INBOX_DEPTH = "fleet_inbox_depth";
private FleetMetrics() {
}
diff --git a/bridged/src/main/java/dev/ltms/fleet/rest/FleetApp.java b/bridged/src/main/java/dev/ltms/fleet/rest/FleetApp.java
index ba78fc8..04f1b49 100644
--- a/bridged/src/main/java/dev/ltms/fleet/rest/FleetApp.java
+++ b/bridged/src/main/java/dev/ltms/fleet/rest/FleetApp.java
@@ -7,6 +7,7 @@ import dev.ltms.fleet.auth.Authz;
import dev.ltms.fleet.auth.CallerResolver;
import dev.ltms.fleet.auth.Principal;
import dev.ltms.fleet.guard.GuardException;
+import dev.ltms.fleet.metrics.FleetMetrics;
import dev.ltms.fleet.metrics.Metrics;
import dev.ltms.fleet.herdr.Agent;
import dev.ltms.fleet.herdr.HerdrClient;
@@ -166,7 +167,7 @@ public final class FleetApp {
private void countAuthFailure(String reason) {
if (metrics != null) {
- metrics.inc("bridged_auth_failures_total", "reason", reason);
+ metrics.inc(FleetMetrics.AUTH_FAILURES, "reason", reason);
}
}
diff --git a/bridged/src/test/java/dev/ltms/fleet/herdr/AgentControlContractTest.java b/bridged/src/test/java/dev/ltms/fleet/herdr/AgentControlContractTest.java
index 5ced79c..957fd30 100644
--- a/bridged/src/test/java/dev/ltms/fleet/herdr/AgentControlContractTest.java
+++ b/bridged/src/test/java/dev/ltms/fleet/herdr/AgentControlContractTest.java
@@ -31,7 +31,7 @@ class AgentControlContractTest {
assumeTrue(!noSocket(), "no herdr socket — skipping");
try (UnixSocketHerdrClient herdr = UnixSocketHerdrClient.connect()) {
WorkspaceControl spaces = new WorkspaceControl(herdr);
- Workspace space = spaces.ensureWorkspace("__bridged_env_contract__");
+ Workspace space = spaces.ensureWorkspace("__fleet_env_contract__");
Tab.Created tab = spaces.createTab(space.workspaceId(), null,
Map.of("ANTHROPIC_BASE_URL", "http://gx00.gw:8000"));
try {
diff --git a/bridged/src/test/java/dev/ltms/fleet/herdr/PaneLocatorContractTest.java b/bridged/src/test/java/dev/ltms/fleet/herdr/PaneLocatorContractTest.java
index 1ffcd1a..0a8ea4d 100644
--- a/bridged/src/test/java/dev/ltms/fleet/herdr/PaneLocatorContractTest.java
+++ b/bridged/src/test/java/dev/ltms/fleet/herdr/PaneLocatorContractTest.java
@@ -25,7 +25,7 @@ class PaneLocatorContractTest {
assumeTrue(Files.exists(UnixSocketHerdrClient.defaultSocketPath()), "no herdr socket — skipping");
try (UnixSocketHerdrClient herdr = UnixSocketHerdrClient.connect()) {
WorkspaceControl spaces = new WorkspaceControl(herdr);
- Workspace space = spaces.ensureWorkspace("__bridged_pid_contract__");
+ Workspace space = spaces.ensureWorkspace("__fleet_pid_contract__");
Tab.Created tab = spaces.createTab(space.workspaceId(), null, Map.of());
try {
JsonNode info = herdr.call("pane.process_info", Map.of("pane_id", tab.rootPaneId()))
diff --git a/bridged/src/test/java/dev/ltms/fleet/herdr/WorkspacePlacementContractTest.java b/bridged/src/test/java/dev/ltms/fleet/herdr/WorkspacePlacementContractTest.java
index 40d46e9..82006d5 100644
--- a/bridged/src/test/java/dev/ltms/fleet/herdr/WorkspacePlacementContractTest.java
+++ b/bridged/src/test/java/dev/ltms/fleet/herdr/WorkspacePlacementContractTest.java
@@ -22,7 +22,7 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
@Tag("contract")
class WorkspacePlacementContractTest {
- private static final String LABEL = "__bridged_contract__";
+ private static final String LABEL = "__fleet_contract__";
private boolean noSocket() {
return !Files.exists(UnixSocketHerdrClient.defaultSocketPath());
diff --git a/bridged/src/test/java/dev/ltms/fleet/metrics/MetricNamesTest.java b/bridged/src/test/java/dev/ltms/fleet/metrics/MetricNamesTest.java
new file mode 100644
index 0000000..28d4e5a
--- /dev/null
+++ b/bridged/src/test/java/dev/ltms/fleet/metrics/MetricNamesTest.java
@@ -0,0 +1,87 @@
+package dev.ltms.fleet.metrics;
+
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CB-632: every exported series is named {@code fleet_*}, and nothing still says {@code bridged_}.
+ *
+ *
Why this test exists. The rename from {@code bridged_} to {@code fleet_} had nothing watching
+ * it. {@link FleetMetrics} holds the names as constants, but one caller had written
+ * {@code "bridged_auth_failures_total"} as a literal instead of using {@link
+ * FleetMetrics#AUTH_FAILURES} — a second hand-written copy of a name, which is how these drift. A
+ * rename that updated the constants and missed that literal would have shipped a daemon exporting
+ * eight series under one prefix and one under another, and no test would have failed.
+ *
+ *
The check is reflective on purpose. A test that lists the nine names is itself a second
+ * hand-written copy, so it would pass while a tenth constant added later went unchecked. Reading
+ * the fields means a new series is covered the moment it is declared.
+ *
+ *
It also asserts its own denominator. "No name starts with bridged_" is true of an empty set,
+ * so a reflective sweep that silently found nothing would pass loudly. Asserting the count means a
+ * broken sweep fails instead of reporting success.
+ */
+class MetricNamesTest {
+
+ /** The number of series {@link FleetMetrics} declares. Update deliberately when adding one. */
+ private static final int EXPECTED_SERIES = 9;
+
+ private static List nameConstants() {
+ List out = new ArrayList<>();
+ for (Field f : FleetMetrics.class.getDeclaredFields()) {
+ int m = f.getModifiers();
+ if (Modifier.isPublic(m) && Modifier.isStatic(m) && Modifier.isFinal(m)
+ && f.getType() == String.class) {
+ out.add(f);
+ }
+ }
+ return out;
+ }
+
+ private static String valueOf(Field f) {
+ try {
+ return (String) f.get(null);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError("cannot read " + f.getName(), e);
+ }
+ }
+
+ @Test
+ void theSweepActuallyFoundTheSeriesItClaimsToCheck() {
+ assertEquals(EXPECTED_SERIES, nameConstants().size(),
+ "this test reads FleetMetrics' name constants reflectively; if the count moved, "
+ + "either a series was added (update EXPECTED_SERIES) or the sweep broke "
+ + "and every assertion below is now vacuously true");
+ }
+
+ @Test
+ void everyDeclaredSeriesUsesTheFleetPrefix() {
+ for (Field f : nameConstants()) {
+ String name = valueOf(f);
+ assertTrue(name.startsWith("fleet_"),
+ "FleetMetrics." + f.getName() + " is \"" + name + "\" — every exported series "
+ + "must be named fleet_* since CB-632");
+ assertFalse(name.contains("bridged"),
+ "FleetMetrics." + f.getName() + " still says bridged: \"" + name + "\"");
+ }
+ }
+
+ /**
+ * The names must also be unique. Two constants sharing a value would collide on the wire, and
+ * a copy-paste when adding a series is exactly how that happens.
+ */
+ @Test
+ void noTwoSeriesShareAName() {
+ List names = nameConstants().stream().map(MetricNamesTest::valueOf).toList();
+ assertEquals(names.size(), names.stream().distinct().count(),
+ "two FleetMetrics constants hold the same series name: " + names);
+ }
+}
diff --git a/bridged/src/test/java/dev/ltms/fleet/metrics/MetricsTest.java b/bridged/src/test/java/dev/ltms/fleet/metrics/MetricsTest.java
index c2f062b..e9a4fc3 100644
--- a/bridged/src/test/java/dev/ltms/fleet/metrics/MetricsTest.java
+++ b/bridged/src/test/java/dev/ltms/fleet/metrics/MetricsTest.java
@@ -13,29 +13,29 @@ class MetricsTest {
@Test
void countersAccumulatePerLabelSet() {
Metrics m = new Metrics();
- m.inc("bridged_sends_total", "outcome", "replied");
- m.inc("bridged_sends_total", "outcome", "replied");
- m.inc("bridged_sends_total", "outcome", "timeout");
+ m.inc("fleet_sends_total", "outcome", "replied");
+ m.inc("fleet_sends_total", "outcome", "replied");
+ m.inc("fleet_sends_total", "outcome", "timeout");
- assertEquals(2, m.count("bridged_sends_total", "outcome", "replied"));
- assertEquals(1, m.count("bridged_sends_total", "outcome", "timeout"));
- assertEquals(0, m.count("bridged_sends_total", "outcome", "failed"),
+ assertEquals(2, m.count("fleet_sends_total", "outcome", "replied"));
+ assertEquals(1, m.count("fleet_sends_total", "outcome", "timeout"));
+ assertEquals(0, m.count("fleet_sends_total", "outcome", "failed"),
"an untouched series reads as zero, not an error");
}
@Test
void rendersHelpAndTypeOncePerFamily() {
Metrics m = new Metrics();
- m.describe("bridged_sends_total", "counter", "Delegated sends by outcome.");
- m.inc("bridged_sends_total", "outcome", "replied");
- m.inc("bridged_sends_total", "outcome", "timeout");
+ m.describe("fleet_sends_total", "counter", "Delegated sends by outcome.");
+ m.inc("fleet_sends_total", "outcome", "replied");
+ m.inc("fleet_sends_total", "outcome", "timeout");
String out = m.render();
- assertEquals(1, countOccurrences(out, "# HELP bridged_sends_total"),
+ assertEquals(1, countOccurrences(out, "# HELP fleet_sends_total"),
"HELP is per family, not per series");
- assertEquals(1, countOccurrences(out, "# TYPE bridged_sends_total counter"));
- assertTrue(out.contains("bridged_sends_total{outcome=\"replied\"} 1"));
- assertTrue(out.contains("bridged_sends_total{outcome=\"timeout\"} 1"));
+ assertEquals(1, countOccurrences(out, "# TYPE fleet_sends_total counter"));
+ assertTrue(out.contains("fleet_sends_total{outcome=\"replied\"} 1"));
+ assertTrue(out.contains("fleet_sends_total{outcome=\"timeout\"} 1"));
}
@Test
@@ -53,11 +53,11 @@ class MetricsTest {
void gaugesAreEvaluatedAtScrapeTimeNotRegistrationTime() {
Metrics m = new Metrics();
int[] live = {1};
- m.gauge("bridged_sessions", () -> live[0], "state", "ready");
+ m.gauge("fleet_sessions", () -> live[0], "state", "ready");
- assertTrue(m.render().contains("bridged_sessions{state=\"ready\"} 1"));
+ assertTrue(m.render().contains("fleet_sessions{state=\"ready\"} 1"));
live[0] = 5;
- assertTrue(m.render().contains("bridged_sessions{state=\"ready\"} 5"),
+ assertTrue(m.render().contains("fleet_sessions{state=\"ready\"} 5"),
"the gauge must read current state on every scrape");
}
@@ -78,15 +78,15 @@ class MetricsTest {
void collectorsDiscoverTheirLabelSetPerScrape() {
Metrics m = new Metrics();
Map depths = new LinkedHashMap<>();
- m.collector("bridged_inbox_depth", "target", () -> depths);
+ m.collector("fleet_inbox_depth", "target", () -> depths);
- assertFalse(m.render().contains("bridged_inbox_depth"), "no targets yet ⇒ no series");
+ assertFalse(m.render().contains("fleet_inbox_depth"), "no targets yet ⇒ no series");
depths.put("term_a", 2);
depths.put("term_b", 0);
String out = m.render();
- assertTrue(out.contains("bridged_inbox_depth{target=\"term_a\"} 2"));
- assertTrue(out.contains("bridged_inbox_depth{target=\"term_b\"} 0"));
+ assertTrue(out.contains("fleet_inbox_depth{target=\"term_a\"} 2"));
+ assertTrue(out.contains("fleet_inbox_depth{target=\"term_b\"} 0"));
}
@Test
diff --git a/bridged/src/test/java/dev/ltms/fleet/rest/FleetAppAuthTest.java b/bridged/src/test/java/dev/ltms/fleet/rest/FleetAppAuthTest.java
index 33c9479..881db6f 100644
--- a/bridged/src/test/java/dev/ltms/fleet/rest/FleetAppAuthTest.java
+++ b/bridged/src/test/java/dev/ltms/fleet/rest/FleetAppAuthTest.java
@@ -184,7 +184,7 @@ class FleetAppAuthTest {
HttpResponse ok = send(port, "GET", "/metrics", null, "Bearer s3cret");
assertEquals(200, ok.statusCode());
assertTrue(ok.headers().firstValue("Content-Type").orElse("").startsWith("text/plain"));
- assertTrue(ok.body().contains("bridged_sessions{state=\"ready\"}"),
+ assertTrue(ok.body().contains("fleet_sessions{state=\"ready\"}"),
"the session census gauge is exported even when empty");
}
diff --git a/docs/CB-402-OpenCode-Adapter.md b/docs/CB-402-OpenCode-Adapter.md
index c1de564..5fe59f0 100644
--- a/docs/CB-402-OpenCode-Adapter.md
+++ b/docs/CB-402-OpenCode-Adapter.md
@@ -301,5 +301,5 @@ identity (loopback peer PID → herdr pane) classified an **opencode** process a
opencode-specific handling — confirming the identity model is peer-kind-agnostic, which is exactly
what CB-308 needs when it stretches the roster across hosts.
-CB-502 counters for the same run: `bridged_sends_total{outcome="replied"} 1`,
-`bridged_replies_total{path="rendezvous"} 1`, `bridged_inbox_depth{...} 0`.
+CB-502 counters for the same run: `fleet_sends_total{outcome="replied"} 1`,
+`fleet_replies_total{path="rendezvous"} 1`, `fleet_inbox_depth{...} 0`.
diff --git a/docs/CB-5xx-Hardening.md b/docs/CB-5xx-Hardening.md
index ada5f87..cda5a75 100644
--- a/docs/CB-5xx-Hardening.md
+++ b/docs/CB-5xx-Hardening.md
@@ -188,15 +188,15 @@ Deliberately small; every one maps to a failure mode we have actually hit.
| Metric | Type | Why it exists |
|---|---|---|
-| `bridged_sends_total{outcome}` | counter | outcome ∈ replied\|completion_fallback\|timeout\|failed — the completion-fallback rate is the health signal for turn detection (CB-115/116/118) |
-| `bridged_send_duration_seconds` | histogram | delegated turn latency |
-| `bridged_replies_total{path}` | counter | path ∈ rendezvous\|inbox — how often a reply strands (CB-307's whole reason to exist) |
-| `bridged_inbox_depth{target}` | gauge | undrained replies; steady-state should be 0 |
-| `bridged_push_nudges_total{outcome}` | counter | outcome ∈ delivered\|exhausted — a rising `exhausted` means the primary is not draining |
-| `bridged_spawns_total{kind,outcome}` | counter | outcome ∈ ready\|timeout\|guard_rejected; per peer kind (CB-402) |
-| `bridged_sessions{state}` | gauge | SPAWNING/READY/BUSY/DONE census |
-| `bridged_herdr_calls_total{method,outcome}` | counter | socket health — the dependency everything rests on |
-| `bridged_auth_failures_total{reason}` | counter | only meaningful once CB-501 lands; catches misconfigured workers |
+| `fleet_sends_total{outcome}` | counter | outcome ∈ replied\|completion_fallback\|timeout\|failed — the completion-fallback rate is the health signal for turn detection (CB-115/116/118) |
+| `fleet_send_duration_seconds` | histogram | delegated turn latency |
+| `fleet_replies_total{path}` | counter | path ∈ rendezvous\|inbox — how often a reply strands (CB-307's whole reason to exist) |
+| `fleet_inbox_depth{target}` | gauge | undrained replies; steady-state should be 0 |
+| `fleet_push_nudges_total{outcome}` | counter | outcome ∈ delivered\|exhausted — a rising `exhausted` means the primary is not draining |
+| `fleet_spawns_total{kind,outcome}` | counter | outcome ∈ ready\|timeout\|guard_rejected; per peer kind (CB-402) |
+| `fleet_sessions{state}` | gauge | SPAWNING/READY/BUSY/DONE census |
+| `fleet_herdr_calls_total{method,outcome}` | counter | socket health — the dependency everything rests on |
+| `fleet_auth_failures_total{reason}` | counter | only meaningful once CB-501 lands; catches misconfigured workers |
---
diff --git a/docs/M4-Fleet-Health.md b/docs/M4-Fleet-Health.md
index dd9da54..ff1343b 100644
--- a/docs/M4-Fleet-Health.md
+++ b/docs/M4-Fleet-Health.md
@@ -687,14 +687,14 @@ The sink response body is ignored. A webhook cannot direct recovery. n8n remains
M4 adds bounded-label series:
```text
-bridged_health_incidents{scope,state,severity}
-bridged_health_incidents_total{event}
-bridged_health_notifications_total{event,outcome}
-bridged_health_notification_queue_depth
-bridged_health_notification_last_success_seconds
-bridged_health_notification_capability{mode,status}
-bridged_lead_health{lead,state}
-bridged_lead_assigned_incidents{lead}
+fleet_health_incidents{scope,state,severity}
+fleet_health_incidents_total{event}
+fleet_health_notifications_total{event,outcome}
+fleet_health_notification_queue_depth
+fleet_health_notification_last_success_seconds
+fleet_health_notification_capability{mode,status}
+fleet_lead_health{lead,state}
+fleet_lead_assigned_incidents{lead}
```
Metric labels never include terminal ids, incident ids, URLs, or error text.