carlos / platform Public

docs+cli: the re-review's three minors — name the daemon start, the whole fix, the stranded grants

The -backing unit stamp now warns at the terminal that the hourly adopt
pass STARTS a daemon route (F4's guard belongs at the moment of typing,
not only in §5); the missing-echo refusal names the full fix including
the new-version re-ship; both §5 rollbacks note that grants stay in the
caps column after an un-stamp and what the clearing order is.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell e7deec8cf32739b30eb5edf7605bc2dcf75bf8b2 parent bb3dd4c
3 files changed, +15 −3
  • apimode.go +5 −1
  • cli.go +1 −0
  • docs/superpowers/specs/2026-08-03-app-capabilities-design.md +9 −2
diff --git a/apimode.go b/apimode.go
index e116fea96..048b15b25 100644
--- a/apimode.go
+++ b/apimode.go
@@ -477,7 +477,11 @@ func checkCapabilitiesEcho(sent *caps.Set, body []byte) error {
Capabilities json.RawMessage `json:"capabilities"`
}
if err := json.Unmarshal(body, &echo); err != nil || len(echo.Capabilities) == 0 {
- return fmt.Errorf("ship: the console accepted this release but its response did not echo the capability block back: %s", updateFirst)
+ // The version is already in the bucket WITHOUT its declaration
+ // (immutable, so a same-version re-ship after the update meets the
+ // 409) — name the whole fix here, not just the console half.
+ return fmt.Errorf("ship: the console accepted this release but its response did not echo the capability block back — "+
+ "the stored manifest carries no capabilities and must not be promoted: %s, then ship again under a new version", updateFirst)
}
var got *caps.Set
if err := json.Unmarshal(echo.Capabilities, &got); err != nil || got == nil {
diff --git a/cli.go b/cli.go
index cf9f6c5f5..58a595993 100644
--- a/cli.go
+++ b/cli.go
@@ -1043,6 +1043,7 @@ func runRouteBacking(r *registry.Registry, rt registry.Route, backing string, dr
// column change sides together.
if backing == registry.BackingUnit {
fmt.Printf(" the next adopt pass converges this route's capability drop-ins and config sync writes /etc/carlos/apps/%s.env; the agent stops materializing /data/%s.config.env\n", rt.Host, rt.Host)
+ fmt.Printf(" for a daemon route that pass also enables and STARTS carlos-app@%s.service — the adopt timer is hourly, so stamp this only once the data it will serve is in place\n", rt.Host)
} else {
fmt.Printf(" the next adopt pass stops converging this route's capabilities (any drop-ins already written are left exactly as they are, for an operator to remove) and config delivery returns to the agent's /data/%s.config.env\n", rt.Host)
}
diff --git a/docs/superpowers/specs/2026-08-03-app-capabilities-design.md b/docs/superpowers/specs/2026-08-03-app-capabilities-design.md
index 5d0e14676..bdf66435a 100644
--- a/docs/superpowers/specs/2026-08-03-app-capabilities-design.md
+++ b/docs/superpowers/specs/2026-08-03-app-capabilities-design.md
@@ -675,7 +675,12 @@ Rollback: stop and disable the templated unit,
`carlos route -host <host> -backing ""`, re-enable `console.service` (its
data is untouched at `/data/console`). Console writes made after cutover are
lost by rolling back — keep the window short, the same posture the keymail
-cutover plan takes about its fork window.
+cutover plan takes about its fork window. Note the grants: they stay in the
+caps column after the un-stamp, reading like live authority, and `-revoke`
+refuses non-unit routes — so to actually clear them the order is re-stamp
+`-backing unit` → `-revoke` each token → `-backing ""`. Harmless if left
+(nothing materializes off a non-unit route), but say so in the ops notes
+rather than let a later reader wonder who granted what.
### 5c. keymail relay
@@ -743,7 +748,9 @@ Rollback: stop the templated unit, `carlos route -host keymail.dev -backing ""`,
re-enable `keymail-relay.service` — its data is still at `/opt/keymail/data`
and its own embedded litestream resumes from local files. Mail accepted
during the window exists only in the streamed lineage; short window, quiet
-hour, same caveat the cutover plan already carries.
+hour, same caveat the cutover plan already carries. The same stranded-grants
+note as §5b's rollback applies: `port:25`/`host-role` stay in the caps
+column and clearing them means re-stamp → revoke → un-stamp.
**Implementation note (2026-08-03): `-backing` and `-grant`/`-revoke` are
real, and this is their exact syntax.** Both §5b and §5c above reference