carlos / platform Public

spec: reconcile with what was built

Four amendments from implementation: the mirror refusal ("inherit" at app
scope) and the audit wording; the re-add consequence of set-membership
digests and the two tests it moved; healthByHost's extraction and the
upstream precedence; and the fleet caveat printing every call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gyhxeoXGBW4WCFRnMqBue
Paul Campbell bc23995abad10dccf4995d558d143b87e9de8ea2 parent cc54db2
1 file changed, +51 −7
  • docs/superpowers/specs/2026-08-16-route-scoped-config-delivery-design.md +51 −7
diff --git a/docs/superpowers/specs/2026-08-16-route-scoped-config-delivery-design.md b/docs/superpowers/specs/2026-08-16-route-scoped-config-delivery-design.md
index c8c4e92bb..67c44164e 100644
--- a/docs/superpowers/specs/2026-08-16-route-scoped-config-delivery-design.md
+++ b/docs/superpowers/specs/2026-08-16-route-scoped-config-delivery-design.md
@@ -136,7 +136,16 @@ moves with it (`internal/console/apiconfig.go`):
("exec_delivery is app-level only"). It accepts `"file"` and a new
`"inherit"` at environment scope, and refuses `"off"` there by name —
the same refusal the CLI gives, worded once on the server so the
- operator reads the server's sentence rather than a paraphrase.
+ operator reads the server's sentence rather than a paraphrase. The
+ **mirror refusal** is enforced too: `"inherit"` with no environment is a
+ 400, because the default bundle has nothing above it to inherit from and
+ silently treating the word as `"off"` would be the same class of quiet
+ lie in the other direction.
+
+The audit row records the caller's **own word** (`file` / `off` /
+`inherit`) and the bundle it applied to. Both clearing words store `""`,
+so a row rebuilt from the stored value would lose which scope was acted
+on.
The two-CAS-writes shape of an apply-plus-exec_delivery request is
unchanged; the second write becomes `SetExecDeliveryAt` against the
@@ -251,6 +260,28 @@ spurious digest movement costs one pass that writes nothing — `SyncExec`
and `Sync` both compare rendered bytes and act only on a real change, so it
can never cost a restart.
+### 2.4 A consequence, recorded during implementation
+
+Set membership carrying scope changes means a route **leaving and
+re-entering** the registry now moves the digest, so the tick that re-adds
+it is no longer silent. Two existing tests asserted that silence
+(`TestGenPollPassDropsVanishedApps`, `TestGenPollPassPrunesUnitAppsToo`),
+and their assertion moved to the **settled tick after** the re-add.
+
+The ruling those tests encode is unchanged and still pinned directly: a
+vanished app's etag cache entry is dropped, so a re-add is a first sight
+rather than a comparison against a remembered etag. What is no longer
+separately observable from those tests is the etag half's *silence*, since
+the shape movement covers it either way. Firing there is correct on its own
+terms — a route entering or leaving scope changes what belongs on disk, a
+file to write or one to reap — and the pass it triggers resolves each case
+properly rather than acting on the digest.
+
+`genPollPass`'s signature took a `*genCache` (etags plus the two digests
+plus a `shapeSeen` bool) rather than growing a second parameter. The bool is
+not redundant with an empty digest: a box with no config-carrying routes has
+a real, stable digest of the empty set.
+
## 3. `carlos routes` (#64)
### 3.1 The defect
@@ -281,11 +312,21 @@ boundary the reconciler itself uses, and the account fallback is
`release.RouteAccount`'s, the same resolution every other read of a route's
account performs.
-Response rows carry: `host`, `kind`, `upstream` (addr or socket, whichever
-the row has), `app`, `channel`, `environment`, `status`, `health`,
-`hibernate`, `unit`, `backing`, `provisioned`. `health` is `healthWord`,
-computed the same way the Running tab and `carlos instances list` compute
-it, so the three cannot disagree.
+Response rows carry: `host`, `kind`, `upstream` (socket, then the
+multi-upstream set, then the legacy addr, then `tunnel:<name>`), `app`,
+`channel`, `environment`, `status`, `health`, `hibernate`, `unit`,
+`backing`, `provisioned`. Sorted by host, because registry order is not
+stable across passes.
+
+`health` is `healthWord`, computed the same way the Running tab and
+`carlos instances list` compute it, so the three cannot disagree. To make
+that structural rather than a promise, the metrics half of
+`instanceHealth` is **extracted** as `healthByHost(ctx, routeFor)` and both
+doors call it. The tenant scoping stays in each caller, beside the reason
+that caller's rows are the caller's to see; `healthByHost` does no check of
+its own and carries a comment saying it must never be handed an unfiltered
+registry listing (`OwnerAppMetrics`' allowlist is exactly the hosts and
+units it is given).
A console with no `Registry` configured cannot know any route's state; it
answers with an explicit "this console serves no registry" rather than an
@@ -321,7 +362,10 @@ answering the same question is not built.
The console reads *its own box's* registry, so in a multi-box fleet the
other boxes' routes do not appear. This is the same blind spot
`instanceHealth` has today. `internal/fleetapi` is the eventual fix and is
-unmerged (#145). The API doc comment and the CLI's own help say so.
+unmerged (#145). The API doc comment says so, and the CLI prints the
+caveat as a line **on every call**, not only when the list is empty — a
+short answer on a fleet is not a complete one, and the reader has no way to
+tell from the rows themselves.
## 4. Testing