carlos / platform Public

spec rev 8 (manifest cache bound) and edge asset cache plan 1

Summary

Spec rev 8 bounds the home's manifest cache to builds a local route references (Astra R5-1). Adds plan 1: declare and publish, dark (11 tasks plus copy).
claude agent, operated by paul@keymail.dev 91debc186500e7f98fe9dc8ccec6fb9fafd6e522 parent b55ff71
3 files changed, +342 −2
  • docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md +325 −0
  • docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md +10 −2
  • docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-R5.md +7 −0

CI cancelled — superseded by a newer push — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (0 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.017s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (1 s)
=== gofmt (0 s)
gofmt -l .: clean
=== rest (147 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.921s
ok  	github.com/carlosframework/platform/cmd/moveprobe	12.023s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.750s
ok  	github.com/carlosframework/platform/internal/activator	58.162s
ok  	github.com/carlosframework/platform/internal/agentsock	0.031s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.027s
ok  	github.com/carlosframework/platform/internal/appconfig	3.110s
ok  	github.com/carlosframework/platform/internal/apperr	0.173s
ok  	github.com/carlosframework/platform/internal/applog	0.202s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.012s
ok  	github.com/carlosframework/platform/internal/boxid	0.827s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.021s
ok  	github.com/carlosframework/platform/internal/caps	0.025s
ok  	github.com/carlosframework/platform/internal/certwarm	0.004s
ok  	github.com/carlosframework/platform/internal/cgroup	0.788s
ok  	github.com/carlosframework/platform/internal/changelog	0.474s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.275s
ok  	github.com/carlosframework/platform/internal/clamd	0.062s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	7.930s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.010s
ok  	github.com/carlosframework/platform/internal/dbquery	0.005s
ok  	github.com/carlosframework/platform/internal/dbreads	2.162s
?   	github.com/carlosframework/platform/internal/dbreads/dbreadstest	[no test files]
ok  	github.com/carlosframework/platform/internal/dbverify	0.006s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.005s
ok  	github.com/carlosframework/platform/internal/docsite	0.009s
ok  	github.com/carlosframework/platform/internal/domains	2.943s
ok  	github.com/carlosframework/platform/internal/doorkey	0.008s
ok  	github.com/carlosframework/platform/internal/economics	0.006s
ok  	github.com/carlosframework/platform/internal/edge	45.495s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.014s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.253s
ok  	github.com/carlosframework/platform/internal/gate	1.832s
ok  	github.com/carlosframework/platform/internal/genselect	0.036s
ok  	github.com/carlosframework/platform/internal/geo	0.021s
ok  	github.com/carlosframework/platform/internal/geoip	0.059s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.216s
ok  	github.com/carlosframework/platform/internal/host	143.768s
ok  	github.com/carlosframework/platform/internal/hostactors	6.251s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.011s
ok  	github.com/carlosframework/platform/internal/hostbox	3.339s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.910s
ok  	github.com/carlosframework/platform/internal/hostenv	0.009s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.009s
ok  	github.com/carlosframework/platform/internal/hostexec	5.734s
ok  	github.com/carlosframework/platform/internal/hostfence	0.047s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.295s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.024s
ok  	github.com/carlosframework/platform/internal/hostguard	1.052s
ok  	github.com/carlosframework/platform/internal/hostops	2.258s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.007s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.004s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.693s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.243s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.005s
ok  	github.com/carlosframework/platform/internal/infraview	0.007s
ok  	github.com/carlosframework/platform/internal/instancectl	5.254s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.833s
ok  	github.com/carlosframework/platform/internal/instancefs	0.010s
ok  	github.com/carlosframework/platform/internal/instancekey	0.005s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.015s
ok  	github.com/carlosframework/platform/internal/layout	1.968s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.003s
ok  	github.com/carlosframework/platform/internal/lease	10.091s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.575s
ok  	github.com/carlosframework/platform/internal/ledger	0.012s
ok  	github.com/carlosframework/platform/internal/mail	0.255s
ok  	github.com/carlosframework/platform/internal/maintenance	0.081s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.572s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.159s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.330s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.024s
ok  	github.com/carlosframework/platform/internal/objstore	0.292s
ok  	github.com/carlosframework/platform/internal/ops	0.222s
ok  	github.com/carlosframework/platform/internal/park	0.251s
ok  	github.com/carlosframework/platform/internal/plans	0.011s
ok  	github.com/carlosframework/platform/internal/privatefile	0.008s
ok  	github.com/carlosframework/platform/internal/provision	14.578s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.010s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.527s
ok  	github.com/carlosframework/platform/internal/relay	7.231s
ok  	github.com/carlosframework/platform/internal/release	25.465s
ok  	github.com/carlosframework/platform/internal/rollout	3.834s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.008s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.062s
ok  	github.com/carlosframework/platform/internal/rootstate	0.028s
ok  	github.com/carlosframework/platform/internal/runs	0.573s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.703s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/engines	2.801s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	11.065s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.066s
ok  	github.com/carlosframework/platform/internal/status	0.067s
ok  	github.com/carlosframework/platform/internal/steering	0.008s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.186s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.006s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.005s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.005s
ok  	github.com/carlosframework/platform/internal/usage	0.006s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.663s
=== internal_console@0 (81 s)
ok  	github.com/carlosframework/platform/internal/console	79.107s
=== internal_console@1 (44 s)
ok  	github.com/carlosframework/platform/internal/console	41.706s
=== internal_console@2 (42 s)
ok  	github.com/carlosframework/platform/internal/console	39.598s
=== internal_console@3 (41 s)
ok  	github.com/carlosframework/platform/internal/console	38.785s
=== internal_console@4 (43 s)
ok  	github.com/carlosframework/platform/internal/console	40.774s
=== internal_console@5 (41 s)
ok  	github.com/carlosframework/platform/internal/console	38.603s
=== internal_console@6 (37 s)
ok  	github.com/carlosframework/platform/internal/console	35.228s
=== internal_console@7 (47 s)
ok  	github.com/carlosframework/platform/internal/console	44.394s
=== root@0 (109 s)
ok  	github.com/carlosframework/platform	107.629s
=== root@1 (84 s)
ok  	github.com/carlosframework/platform	82.967s
=== root@2 (91 s)
ok  	github.com/carlosframework/platform	89.588s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64

amadan: job cancelled — a newer push superseded it
--- shard 1 of 5 ---
cross-compiling darwin/amd64

amadan: job cancelled — a newer push superseded it
--- shard 2 of 5 ---
cross-compiling linux/arm64

amadan: job cancelled — a newer push superseded it
--- shard 3 of 5 ---
cross-compiling windows/amd64

amadan: job cancelled — a newer push superseded it
--- shard 4 of 5 ---
cross-compiling windows/arm64

amadan: job cancelled — a newer push superseded it
diff --git a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md
new file mode 100644
index 000000000..484b65ee8
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md
@@ -0,0 +1,325 @@
+# Edge asset cache, plan 1: declare and publish, dark Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Let an app declare build-asset path prefixes at ship and deploy, give every instance an asset generation and an off switch, compute each host's authority time on its home box, and publish `assets` blocks in the edges file and parse them on every edge, with nothing served from a cache yet (plan 3 serves).
+
+**Architecture:** The declaration is an additive `assets` block in the release manifest (`internal/release/assets.go`), written by `carlos ship`, `carlos deploy` and the console's ship door, each checking one grammar. Instance records gain `Assets *bool` and `AssetGen string`, carried to two new route columns. The box tracks two authority clocks in `internal/host/assetauthority.go`: a per-app records confirmed time fed by a new completeness report from `provision.Reconcile`, and a domain confirmed time fed by a new completeness report from `domains.ReconcileAliases`. The edges-file writer publishes `assets` for armed boxes only; the edge's home map parses it and answers `AssetsFor(host)` with the one-hour freshness rule. Purge, opt-out and status are console doors with CLI verbs.
+
+**Tech Stack:** Go 1.26, SQLite registry (`internal/registry`), the release store (`release.Store`, `DirStore` in tests), `crypto/rand`.
+
+**Spec:** `docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md` (rev 7; rev 6 accepted by Astra in round 4, rev 7's corrections under round 5; rulings R1 to R3 in §13). Read §3, §4, §6, §9 and §10 before starting. Where this plan and the spec disagree, the spec wins and the plan is the bug.
+
+**Branch:** `edge-asset-cache-1`, cut from amadan `main` after the spec branch `edge-asset-cache` has merged (the spec lands first, alone). Plan 2 (`2026-10-24-edge-asset-cache-plan-2-disk-store.md`) does not depend on this plan and can be built at the same time. Plan 3 needs both.
+
+## Global Constraints
+
+Every task's requirements include this section.
+
+- **Production-wired acceptance.** Publication tests drive the real `edgesFilePass` writer over a `DirStore`, then parse the written object with the real home map (`internal/edge` `refreshHosts`) or `internal/edgesfile` readers. A stand-in is allowed in unit tests only, and every such test says "not acceptance" in its doc comment.
+- **Targeted local runs; CI runs the full suite.** `go test ./internal/<pkg>/ -run 'TestName' -count=1`. Never `make test`, never a whole `internal/console`, `internal/host` or root package. `go build ./...`, `go vet ./...`, `gofmt -l .` and the cross-compiles run locally. Prefix go commands with `GOFLAGS=-buildvcs=false` if the sandbox refuses VCS stamping. A green CI run on the branch tip is the merge gate.
+- **Cross-compile gate.** Before every push: `GOOS=darwin GOARCH=arm64 go build ./...` and `GOOS=windows GOARCH=amd64 go build ./...`. Nothing in this plan needs a Linux-only syscall.
+- **Copy needs review before it ships, with no em dashes.** No user-facing string reaches code or docs until Paul approves it through the `copy-review` skill (Task 0). The record is `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md`, gated by `approved_asset_cache_copy_test.go`. A full stop and a new sentence, continuing in lower case. Internal error fragments, log lines and diagnostics are not copy rows.
+- **New refusal sentences stay out of the legacy console files.** `internal/console/apicli.go` and `apiinstances.go` are in `movesLegacyFiles` (`approved_moves_copy_test.go:1492-1493`); a new `jsonError` sentence there fails `TestMovesLegacyExemptionsAreLegacy`. Put them in `internal/console/apiship_assets.go` and `internal/console/apiassets.go`, the way `apiship_swap.go` does for swap.
+- **No tenant names.** Fixtures use `shop`, `shop.example.com`, account sqid `abc`, regions `ie` and `uk`.
+- **Go cache.** Leave `GOCACHE`, `GOMODCACHE` and `GOTMPDIR` at their defaults; run go with the sandbox off if `~/.cache/go-build` is read-only.
+- **Census overlay per branch, loaded LAST.** `internal/provision/testdata/plan14-root-reads-edge-asset-cache-1.tsv`, `rev3=PROPOSED-edge-asset-cache-1`, loaded after whatever overlay main loads last at the merge (at the time of writing, `wake-latency-1`, `plan14_root_reads_test.go:1455`), with the previous seal checked first. Re-read the test after every merge of main.
+- **Merge main first.** Before review and again before merging: `git fetch origin && git merge origin/main`, rebuild the census overlay, push, wait for CI on that tip. Merge with `git merge-base --is-ancestor origin/main HEAD && amadan branch merge carlos/platform edge-asset-cache-1 -squash -expect <tip> -message "..."`. Never a bare `amadan branch merge`.
+- **Amadan tasks.** One amadan task per plan task on the branch, advanced as each closes.
+- **Test weights.** A new top-level test in `internal/console` or the root package taking a second or more gets a row in `.amadan/test-weights.tsv`.
+- **New host tests call `t.Parallel()`** unless they touch env, cwd, umask or package vars.
+- **Wire shapes are additive.** Every new field is `omitempty`. A file, manifest or record written before this plan parses byte-identically.
+- **Spec numbers, verbatim.** Prefix grammar: starts and ends with `/`, at most 128 bytes, `path.Clean`'d apart from the trailing slash, none of `%`, `?`, `#`, `*`, `..`; `/` alone refused; at most 8 entries; none a prefix of another. Generation: random 64-bit token, 16 lowercase hex. Freshness: an `at` older than one hour serves nothing; the publisher rewrites when an `at` has advanced 20 minutes or more since the last write. Arming env: `CARLOS_ASSET_CACHE=1`.
+
+## Review Focus
+
+1. **An app that already exists, with instances created before this plan.** Its records have no `AssetGen`. A person who deploys with `--assets` expects catching to start without a separate step. Rule: an empty `AssetGen` publishes nothing, and the ship door, on a release that declares assets, mints a generation for every record of the app that lacks one (spec §6: a token is drawn at create; this covers records created before the field existed). Test: Task 5, `TestShipWithAssetsMintsMissingGenerations`.
+2. **A saved `.carlos/config` from an older CLI.** No `assets` key, or a destinations block that predates it. A person expects deploy to behave exactly as before. Test: Task 3, `TestDeployWithoutSavedAssetsDeclaresNone`.
+3. **Two prefixes that differ only in case or a trailing segment** (`/Assets/` and `/assets/`, `/a/` and `/a/b/`). The grammar is byte-wise: case-distinct prefixes are both allowed, nested ones refused. Test: Task 1, `TestValidAssetPathsTable` rows.
+4. **The console box itself.** It homes most apps and runs the console; its reconcile and alias passes run on the same cadence as any box. A person expects its hosts to publish like any other box's. Test: Task 8 runs the writer with a registry holding a console-class route beside an instance route and checks only the instance route publishes.
+5. **A purge interrupted half way.** Three instances, the second write fails. A person expects to be told which instance did not change and that running the purge again finishes it. Test: Task 5, `TestPurgeReportsPartialAndRetryCompletes`.
+
+## Census impact
+
+- **Plan 14 root-read census:** one overlay, `plan14-root-reads-edge-asset-cache-1.tsv`, rows `rev3=PROPOSED-edge-asset-cache-1`.
+ - Changed frozen units, re-fingerprinted as `@legacy` with the same class: `internal/host/edgesfile.go` `edgesFileWriteClaims` (authority F,J1; it now reads manifests through the injected cache), `internal/domains/aliases.go` `ReconcileAliases` and `deriveInstanceAliases` (legacy).
+ - New units: `internal/host/assetmanifests.go` `assetManifestCache.paths` (reads `release.GetManifest`, class `authority`, task J1, the same class as other manifest readers).
+ - Envelopes re-pinned: `.`, `internal/release`, `internal/provision`, `internal/registry`, `internal/domains`, `internal/host`, `internal/edge`, `internal/edgesfile`, `internal/console`.
+ - Notes file: `docs/superpowers/notes/census-proposal-edge-asset-cache-1.md`, ruling asked on #280.
+
+## Copy strings
+
+Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text is in the copy record, not here):
+
+| id | where | what it says |
+|---|---|---|
+| EAC-1 | `cli.go`, `deploy.go` | `--assets` flag help |
+| EAC-2 | `deploy.go` | `--no-assets` flag help |
+| EAC-3 to EAC-7 | `internal/release/assets.go` | the five grammar refusals (slashes, `/` alone, forbidden characters, length or not clean, more than 8 or nested) |
+| EAC-8 | `cli.go`, `deploy.go`, `internal/console/apiship_assets.go` | `--assets` refused for a static release |
+| EAC-9 | `deploy.go` | a saved asset list ignored for a static deploy |
+| EAC-10, EAC-11 | `instances_assets.go` | `set-assets` result, on and off |
+| EAC-12, EAC-13 | `assets_cli.go` | purge complete; purge partly applied, naming what to run |
+| EAC-14 | `assets_cli.go` | the status table's column heads and its bound sentence (spec §6) |
+| EAC-15 | `internal/console/apiassets.go` | refusal for a set-assets body that is not `on` or `off` |
+| EAC-16 to EAC-19 | `docs/site/cli.md` | `### carlos assets`, `### carlos instances set-assets`, and the `--assets` paragraphs under `carlos ship` and `carlos deploy` |
+
+## File structure
+
+| File | Responsibility |
+|---|---|
+| `internal/release/assets.go` (create) | `Assets` type, `ValidAssetPaths`, the grammar. |
+| `internal/release/release.go` (modify) | `Manifest.Assets`; `validateManifest` calls the grammar. |
+| `cli.go` (modify) | `carlos ship --assets`; direct-mode manifest; API-mode meta. |
+| `apimode.go` (modify) | `apiSession.shipAssets` field, meta `assets`, echo check; `instanceSetAssets`; `assetsPurge`; `assetsStatus`. |
+| `deploy.go`, `config.go` (modify) | `--assets`, `--no-assets`, the `assets` project default. |
+| `internal/console/apiship_assets.go` (create) | Ship-door grammar check, refusal, echo, minting missing generations. |
+| `internal/console/apicli.go` (modify) | `handleAPIShip` calls into `apiship_assets.go`; nothing else. |
+| `internal/provision/provision.go` (modify) | `Record.Assets`, `Record.AssetGen`, `NewAssetGen`, `SetRecordAssetGen`. |
+| `internal/console/apiinstances.go` and the record creators (modify) | Mint `AssetGen` at create; PATCH `assets`. |
+| `internal/console/apiassets.go` (create) | Purge and status doors, set-assets refusal text. |
+| `internal/registry/registry.go` (modify) | Route columns `assets_off`, `asset_gen`; `SetAssets`; Fingerprint. |
+| `internal/provision/reconcile.go` (modify) | `RouteFor`, `convergeOwned`; `ReconcileConfig.Report`. |
+| `internal/domains/aliases.go` (modify) | `ReconcileReport`; completeness. |
+| `internal/host/assetauthority.go` (create) | The two clocks and `HostAt`. |
+| `internal/host/assetmanifests.go` (create) | Manifest cache, `paths(acct, app, version)`. |
+| `internal/host/edgesfile.go`, `agent.go`, `changeindex.go` (modify) | Publication, arming, feeding the clocks. |
+| `internal/edgesfile/edgesfile.go`, `snapshot.go` (modify) | `Host.Assets` for console readers. |
+| `internal/edge/homemap.go` (modify) | Parse `assets`; `AssetsFor`. |
+| `assets_cli.go`, `instances_assets.go` (create), `main.go`, `cli.go` (modify) | `carlos assets purge|status`, `carlos instances set-assets`. |
+| `approved_asset_cache_copy_test.go`, `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` (create) | Copy gate and record. |
+| `docs/site/cli.md` (modify) | Docs. |
+
+---
+
+### Task 0: Copy record, gate and review
+
+**Files:**
+- Create: `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md`, `approved_asset_cache_copy_test.go`
+
+**Interfaces:**
+- Produces: `assetCacheCopyBuilt []string` in the gate (ids a task has built), extended by each task that writes a row.
+
+- [ ] **Step 1:** Write the copy record: a table `| id | file | text | status |` with rows EAC-1 to EAC-19 drafted from spec §3, §6 and the table above, every row `pending`. No em dashes.
+- [ ] **Step 2:** Write `TestAssetCacheCopyInventory` and `TestAssetCacheCopyBuiltRowsAreApprovedAndPresent` in `approved_asset_cache_copy_test.go`, reusing `parseCopyTable` and `checkMovesNoEmDash` from `approved_moves_copy_test.go`: the id set is exactly EAC-1 to EAC-19; every id in `assetCacheCopyBuilt` is `approved` and its text appears verbatim in its file. Start with `assetCacheCopyBuilt` empty.
+- [ ] **Step 3:** Run `go test . -run 'TestAssetCacheCopy' -count=1`. Expected: PASS.
+- [ ] **Step 4:** Run the `copy-review` skill on the 19 rows. Apply Paul's edits verbatim and mark each row `approved`. Rows he defers stay `pending`, and the task that builds them waits.
+- [ ] **Step 5:** Commit.
+
+### Task 1: The manifest's `assets` block and its grammar
+
+**Files:**
+- Create: `internal/release/assets.go`, `internal/release/assets_test.go`
+- Modify: `internal/release/release.go:47-107` (Manifest), `:322-366` (validateManifest)
+
+**Interfaces:**
+- Produces:
+ - `type Assets struct { Paths []string \`json:"paths"\` }`
+ - `func ValidAssetPaths(paths []string) error` (nil for an empty list; the error text is EAC-3 to EAC-7)
+ - `Manifest.Assets *Assets \`json:"assets,omitempty"\``
+
+- [ ] **Step 1: Write the failing tests.** `TestValidAssetPathsTable`: accepts `/assets/`, `/_app/immutable/`, eight distinct siblings, `/Assets/` beside `/assets/`; refuses `/`, `assets/`, `/assets`, `/a//b/`, `/a/./b/`, `/a/../b/`, `/a%2f/`, `/a?/`, `/a#/`, `/a*/`, a 129-byte prefix, nine entries, `/a/` with `/a/b/`, and a duplicate. `TestManifestWithoutAssetsIsByteIdentical`: marshalling a manifest with `Assets == nil` matches the golden from `golden_test.go`. `TestValidateManifestRefusesBadAssets` and `TestValidateManifestRefusesAssetsOnStatic` (kind `static` with a non-nil `Assets`).
+- [ ] **Step 2:** Run `go test ./internal/release/ -run 'TestValidAssetPaths|TestManifestWithoutAssets|TestValidateManifestRefuses' -count=1`. Expected: FAIL (undefined).
+- [ ] **Step 3:** Implement `Assets`, `ValidAssetPaths` and the field; call `ValidAssetPaths` from `validateManifest` after the capabilities check, and refuse `Assets != nil` on `kind == "static"` with EAC-8.
+- [ ] **Step 4:** Run the same command. Expected: PASS.
+- [ ] **Step 5:** Commit.
+
+### Task 2: `carlos ship --assets` and the console's ship door
+
+**Files:**
+- Modify: `cli.go:2158-2350` (runShip), `cli.go:2444` (runShipAPI unchanged signature), `apimode.go:56` (apiSession), `apimode.go:4447-4476` (ship meta), `internal/console/apicli.go:1900-1926`, `:2167-2178`, `:2216-2221`, `:2238`, `:2293`
+- Create: `internal/console/apiship_assets.go`, `internal/console/apiship_assets_test.go`, `ship_assets_test.go`
+
+**Interfaces:**
+- Consumes: `release.Assets`, `release.ValidAssetPaths` (Task 1).
+- Produces:
+ - `type assetPathsFlag []string` (root package) with `String`/`Set`, repeatable; `Set` does not validate.
+ - `apiSession.shipAssets []string` (sent as meta `"assets": {"paths": [...]}` when non-nil, the way `shipSwap` travels)
+ - `func checkAssetsEcho(sent []string, echoed *release.Assets) error` (root package)
+ - console: `func refuseBadAssets(w http.ResponseWriter, paths []string, kind string) bool`, `func shipAssetsMatch(stored, asked *release.Assets) bool`, `func addAssetsEcho(resp map[string]any, a *release.Assets)` in `apiship_assets.go`
+
+- [ ] **Step 1: Write the failing tests.**
+ - `TestShipDirectModeEmbedsAssets` (root): `carlos ship --assets /assets/ --assets /img/` in direct mode writes a manifest whose `Assets.Paths` is `["/assets/","/img/"]`.
+ - `TestShipRefusesBadAssetsBeforeUpload` (root): `--assets /` exits non-zero with EAC-4 and uploads nothing.
+ - `TestAPISessionShipCarriesAssetsInMeta` (root, `newTestSession`): the meta part has `assets.paths`.
+ - `TestAPIShipCarriesAssetsIntoTheManifest`, `TestAPIShipRefusesBadAssets` (a CLI that skipped the check; body with `/`), `TestAPIShipRefusesAssetsOnStatic`, `TestAPIShipRefusesReassetingAReship` (same version, different paths, 409 like capabilities), `TestAPIShipEchoesTheStoredAssets` (console, `newConsoleFixture`, `postShip`, a `metaPartAssets` helper beside `metaPartSwap`).
+- [ ] **Step 2:** Run `go test . -run 'TestShip.*Assets|TestAPISessionShipCarriesAssets' -count=1` and `go test ./internal/console/ -run 'TestAPIShip.*Assets' -count=1`. Expected: FAIL.
+- [ ] **Step 3:** Implement the flag (help EAC-1), the direct-mode manifest field, `shipAssets` and the meta field, the echo check; in the console, decode meta `assets`, call `refuseBadAssets` before any write, put it in the manifest, compare it in the reship short-circuit, echo it. Add EAC-1 to EAC-8 to `assetCacheCopyBuilt`.
+- [ ] **Step 4:** Run both commands. Expected: PASS.
+- [ ] **Step 5:** Commit.
+
+### Task 3: `carlos deploy --assets`, `--no-assets` and the saved default
+
+**Files:**
+- Modify: `deploy.go:63-334`, `config.go:53-77` (cliConfig), `:249-299` (loadConfig, overlayDestination), `apimode.go:705` (offerSaveProjectDefaults)
+- Test: `deploy_assets_test.go` (create), `config_test.go`
+
+**Interfaces:**
+- Consumes: `assetPathsFlag`, `apiSession.shipAssets` (Task 2).
+- Produces: `cliConfig.Assets []string \`json:"assets,omitempty"\``; `func deployAssets(flagSet bool, flag []string, noAssets bool, saved []string, kind string) (paths []string, notice string, err error)` in `deploy.go`.
+
+- [ ] **Step 1: Write the failing tests.** `TestDeployAssetsResolution` (table over `deployAssets`): flag beats saved; `--no-assets` gives `nil` even with a saved list; both flags together is an error; saved list used when neither flag is set; static kind with a flag is EAC-8, static kind with only a saved list returns `nil` and notice EAC-9. `TestConfigLoadsAssetsList` and `TestConfigRewritesPreserveAssets` (a `writeConfigKeys` round trip keeps the list). `TestDeployWithoutSavedAssetsDeclaresNone`: a config written by today's CLI (no `assets` key) yields `nil`. `TestZeroArgDeployCarriesSavedAssets` (subprocess, `buildCarlos`, the pattern of `TestZeroArgDeployRunsFromSavedDefaults`): the console fixture receives meta `assets.paths` equal to the saved list.
+- [ ] **Step 2:** Run `go test . -run 'TestDeployAssets|TestConfigLoadsAssets|TestConfigRewritesPreserveAssets|TestDeployWithoutSavedAssets|TestZeroArgDeployCarriesSavedAssets' -count=1`. Expected: FAIL.
+- [ ] **Step 3:** Implement. `loadConfig` decodes `assets` as a list (it is the first non-string field: trim each entry, drop empties); `overlayDestination` copies a destination's list when non-nil; `offerSaveProjectDefaults` offers to save the list when the run used `--assets`. Add EAC-2 and EAC-9 to `assetCacheCopyBuilt`.
+- [ ] **Step 4:** Run the same command. Expected: PASS.
+- [ ] **Step 5:** Commit.
+
+### Task 4: Record fields, route columns and convergence
+
+**Files:**
+- Modify: `internal/provision/provision.go:38-224` (Record), `internal/registry/registry.go` (Route, migrate `:571-616`, routeCols `:892`, routeColCount `:901`, scanRoute `:908`, routeArgs `:1261`, Upsert `:1170`, Fingerprint `:3055-3073`, `remote_dispatch.go`), `internal/provision/reconcile.go:436-451` (RouteFor), `:706-776` (convergeOwned)
+- Test: `internal/provision/provision_test.go`, `internal/registry/registry_test.go`, `internal/provision/reconcile_test.go`
+
+**Interfaces:**
+- Produces:
+ - `Record.Assets *bool \`json:"assets,omitempty"\``, `Record.AssetGen string \`json:"asset_gen,omitempty"\``, `func (r Record) AssetsOn() bool` (true unless `Assets` is non-nil and false)
+ - `func NewAssetGen() (string, error)` (16 lowercase hex from `crypto/rand`)
+ - `Route.AssetsOff bool`, `Route.AssetGen string`; `func (r *Registry) SetAssets(host string, off bool, gen string) error`
+ - Fingerprint hashes `assets_off` and `asset_gen`.
+
+- [ ] **Step 1: Write the failing tests.** `TestRecordWireShapeIsAdditive` gains the two fields (absent marshals byte-identically). `TestNewAssetGenShape` (16 hex, two calls differ). `TestUpsertRoundTripsAllColumns` covers the columns. `TestSetAssetsRoundTrips`. `TestFingerprintSeesAssetColumns` (unlike steering: changing either column changes `Fingerprint`). `TestRouteForCarriesAssets` and `TestReconcileConvergesAssets` (a record whose `AssetGen` changes converges the row through `convergeOwned`). `TestShapeMismatchIgnoresAssets`.
+- [ ] **Step 2:** Run `go test ./internal/provision/ -run 'TestRecordWireShape|TestNewAssetGen|TestRouteForCarriesAssets|TestReconcileConvergesAssets|TestShapeMismatchIgnoresAssets' -count=1` and `go test ./internal/registry/ -run 'TestUpsertRoundTripsAllColumns|TestSetAssets|TestFingerprintSeesAssetColumns' -count=1`. Expected: FAIL.
+- [ ] **Step 3:** Implement, following the six-edit column recipe in the `registry.go:885-891` comment and the `SetSteering`/`Steering` convergence shape in `convergeOwned`.
+- [ ] **Step 4:** Run both commands. Expected: PASS.
+- [ ] **Step 5:** Commit.
+
+### Task 5: Minting, opt-out and purge doors
+
+**Files:**
+- Modify: the record creators: `internal/console/apiinstances.go:1421-1480` (create), `apiinstanceseed.go:341`, `apiinstanceimport.go:172`, `domainattach.go:250`, `internal/provision/replica.go:84`, `internal/hostops/record.go:159`; the PATCH body `apiinstances.go:1992-2010`, apply `:2049-2078`, audit `:2165-2185`; `internal/console/authcli.go` (routes); `internal/console/testdata/claim-routes.txt`; `claim_invariant_fixtures_test.go`; `apiship_assets.go` (mint on declare)
+- Create: `internal/console/apiassets.go`, `internal/console/apiassets_test.go`, `internal/provision/assetgen.go` (with `SetRecordAssetGen`)
+
+**Interfaces:**
+- Consumes: `NewAssetGen`, `Record.Assets`, `Record.AssetGen` (Task 4).
+- Produces:
+ - `func SetRecordAssetGen(ctx context.Context, s release.Store, acct, app, host, gen string) error` (on `updateRecord`, `errRecordUnchanged` when equal)
+ - PATCH body field `Assets *string \`json:"assets"\`` taking `"on"` or `"off"` (stored as `Record.Assets`: nil for on, false for off)
+ - `POST /api/cli/apps/{account}/{app}/assets/purge`, role `Deploy` and `Publish` and `Admin`; reply `{"instances":[{"host":..., "gen":..., "ok":bool, "error":...}], "complete":bool}`
+ - `GET /api/cli/apps/{account}/{app}/assets`, any app role; reply per instance `{"host", "paths", "assets", "gen", "published_gen", "published_at"}`
+ - `func mintMissingAssetGens(ctx context.Context, s release.Store, acct, app string) error` (ship door, after a manifest with `Assets` is stored)
+
+- [ ] **Step 1: Write the failing tests.**
+ - `TestInstanceCreateMintsAssetGen` and one per creator (seed, import, attach, replica, prepare): the new record has a 16-hex `AssetGen`.
+ - `TestAPIInstanceUpdateAssetsRoundTrip` (`on`, `off`, and a bad value refused with EAC-15 and nothing written), `TestAPIInstanceUpdateAuditNamesAssets`.
+ - `TestPurgeDrawsAFreshGenPerInstance` (three instances; every `AssetGen` changes and they differ from each other).
+ - `TestPurgeReportsPartialAndRetryCompletes` (a store wrapper failing the second record's write: reply `complete:false` naming it; a second call completes).
+ - `TestShipWithAssetsMintsMissingGenerations` (records without `AssetGen` get one; records with one keep it).
+ - `TestAssetsStatusJoinsPublishedGeneration` (an edges file under `control/edges/` with an `assets` block is joined by host; uses `readEdgesFiles`).
+ - Route census and invariant fixtures for both new routes; `TestEveryCLIDoorNamesItsRoles` passes.
+- [ ] **Step 2:** Run `go test ./internal/console/ -run 'TestInstanceCreateMintsAssetGen|TestAPIInstanceUpdateAssets|TestPurge|TestShipWithAssetsMints|TestAssetsStatus|TestClaimConsoleRouteCensus|TestEveryCLIDoorNamesItsRoles' -count=1` and `go test ./internal/provision/ -run 'TestSetRecordAssetGen' -count=1`. Expected: FAIL.
+- [ ] **Step 3:** Implement. The purge walks `provision.RecordsPrefix` like `placeAppTarget` (`fleets.go:578`) but continues past a failed write, reports each, and audits once with the list. Each door bumps the serial after writing. Add EAC-15 to `assetCacheCopyBuilt`.
+- [ ] **Step 4:** Run both commands. Expected: PASS.
+- [ ] **Step 5:** Commit.
+
+### Task 6: Reconcile reports which apps it read completely and applied
+
+**Files:**
+- Modify: `internal/provision/reconcile.go:818-1000` (Reconcile, the record scan at `:836-900`, the owned and adoption branches)
+- Test: `internal/provision/reconcile_report_test.go` (create)
+
+**Interfaces:**
+- Produces:
+ - `type ReconcileReport struct { Complete map[string]bool }` (keys `"<acct>/<app>"`)
+ - `ReconcileConfig.Report *ReconcileReport` (nil is today's behaviour; when set, `Reconcile` fills it before returning)
+
+An app is complete when: the listing was not `partial`, no record body under the app failed to read, no record under it was `protect`ed, and every route write for its in-scope records returned nil. `readFailed` from an unreadable account manifest makes every app of that account incomplete. A pass that returns an error leaves `Complete` empty.
+
+- [ ] **Step 1: Write the failing tests** (`setup`, `acmeRecord`, `putReplace` helpers): `TestReportCompleteOnACleanPass`; `TestReportOmitsAnAppWithAnUnreadableRecord` (a `viewCountingStore`-style wrapper failing one Get; the other app is still complete); `TestReportOmitsAProtectedRecordsApp` (malformed body); `TestReportOmitsEveryAppOnAPartialListing`; `TestReportEmptyWhenThePassFails`; `TestReportNilKeepsTodaysBehaviour`.
+- [ ] **Step 2:** Run `go test ./internal/provision/ -run 'TestReport' -count=1`. Expected: FAIL.
+- [ ] **Step 3:** Implement by tracking per-app failure inside the existing scan and convergence loops; no new reads.
+- [ ] **Step 4:** Run the same command. Expected: PASS.
+- [ ] **Step 5:** Commit.
+
+### Task 7: ReconcileAliases reports completeness
+
+**Files:**
+- Modify: `internal/domains/aliases.go:162-171`, `:204`, `:232`, `:310`, `:531`, `:541`, `:561`, `:568`, `:584`, `:590`; `internal/domains/claimcache.go:107-145`; the `edgesfile.DeploymentRoutes` call in the pass
+- Test: `internal/domains/aliases_complete_test.go` (create), `instancecatchall_test.go:507`
+
+**Interfaces:**
+- Produces: `type AliasReport struct { Complete bool }`; `func ReconcileAliasesReport(ctx context.Context, s release.Store, reg *registry.Registry, now func() time.Time, deploymentRoutes map[string]bool, cache *ClaimCache, rep *AliasReport) error`. `ReconcileAliases` keeps its signature and calls it with a nil report. `ClaimCache.listWithHosts` gains a `dropped int` result. The caller passes a `deploymentRoutes` read error through so the pass can see it: `ReconcileAliasesReport` treats a nil `deploymentRoutes` as incomplete.
+
+- [ ] **Step 1: Write the failing tests.** `TestAliasReportCompleteOnACleanPass`, then one test per incomplete cause, each with the pass still returning nil: label count fails (extend `TestInstanceCatchallKeepsNamesWhenTheListingFails` to assert `Complete == false`), alias upsert fails, catch-all upsert fails, alias delete fails, wildcard upsert or delete fails, path upsert fails, path delete fails, a claim body unreadable, `deploymentRoutes` nil. Registry write failures are injected with a registry wrapper or a closed-handle trick already used in `aliases_test.go`; if none exists, add `aliasRegFailing` in the test file.
+- [ ] **Step 2:** Run `go test ./internal/domains/ -run 'TestAliasReport|TestInstanceCatchallKeepsNamesWhenTheListingFails' -count=1`. Expected: FAIL.
+- [ ] **Step 3:** Implement: every log-and-continue point also sets a local `incomplete`; the report is written once at the end.
+- [ ] **Step 4:** Run the same command. Expected: PASS.
+- [ ] **Step 5:** Commit.
+
+### Task 8: The box's authority clocks and publication
+
+**Files:**
+- Create: `internal/host/assetauthority.go`, `internal/host/assetmanifests.go`, `internal/host/assetpublish_test.go`
+- Modify: `internal/host/edgesfile.go:44-91` (edgeHost), `:420-668` (edgesFileWriteClaims and the dirty check), `:802-972` (edgesFileLoop, schedule); `internal/host/agent.go:1604-1650` (reconcileSerializedWith passes a `ReconcileReport`), `:3615` and `internal/host/changeindex.go:316` (alias calls pass an `AliasReport`), `:835` (edgesFileLoop wiring)
+
+**Interfaces:**
+- Consumes: `ReconcileReport` (Task 6), `AliasReport` (Task 7), `Route.AssetsOff`, `Route.AssetGen` (Task 4), `release.GetManifest`.
+- Produces:
+ - `type assetAuthority struct` with `func (a *assetAuthority) recordsPass(start time.Time, rep provision.ReconcileReport)`, `func (a *assetAuthority) aliasPass(start time.Time, rep domains.AliasReport)`, `func (a *assetAuthority) hostAt(acct, app string) (time.Time, bool)` (the older of the two; false until both have one)
+ - `type assetManifestCache struct` with `func (c *assetManifestCache) paths(ctx context.Context, acct, app, version string) ([]string, bool)` (bucket read through `release.GetManifest`; keeps only the declared paths per key once read; false on a read failure, never cached) and `func (c *assetManifestCache) prune(keep map[string]bool)` (drops every key not in `keep`; the writer calls it at the end of each pass with the installed and serving builds of its local routes, spec §4.1, R5-1)
+ - `type servingBuild func(host string) (build string, ok bool)` handed to the writer; `ServingGen(host).Build` on boxes with generations, always false otherwise
+ - `edgeHost.Assets *edgeAssets \`json:"assets,omitempty"\`` with `type edgeAssets struct { Paths []string \`json:"paths"\`; Gen string \`json:"gen"\`; At int64 \`json:"at"\` }`
+ - `func assetCacheArmed(getenv func(string) string) bool` (`CARLOS_ASSET_CACHE == "1"`)
+
+Publication rule for a host, applied in `edgesFileWriteClaims` (spec §3.3, §4.1, §4.2): armed; route not `AssetsOff`; `AssetGen` non-empty; `Gate == ""`; `ImportGate == ""` for the row and the row it resolves to; `Pool == ""`; no `route_paths` row for the hostname; single owner by `servedOwners`; `hostAt` known; paths = union of `paths(Version)` and `paths(servingBuild(host))`, non-empty. Aliases (exact names, never `*.`) get their primary's block with the same `At`. The loop also rewrites when any published `At` would advance by 20 minutes or more over the last written one.
+
+- [ ] **Step 1: Write the failing tests** (`edgesTestRegistry`, `seedRoute`, `ownedRoute`, `putCountingStore`, `publishHosts`; manifests put in the `DirStore` with `release.Ship`).
+ - `TestAssetAuthorityHostAtIsTheOlderClock`, `TestAssetAuthorityIncompletePassDoesNotAdvance`, `TestAssetAuthorityUnknownUntilBothClocks`, `TestAssetAuthorityRecordsTheStartTime`.
+ - `TestAssetManifestCacheReadsTheBucketOnce`, `TestAssetManifestCacheDoesNotCacheAFailure`, `TestAssetManifestCacheStaysBoundedAcrossDeploys` (one route cycled through 50 versions with passes between: at most two entries retained, the installed and serving builds' paths always available, and a rollback to a dropped version re-reads the bucket).
+ - `TestPublishesAssetsForAnArmedHome`: the written object's host entry has `paths`, `gen` and `at`.
+ - `TestUnarmedBoxPublishesNoAssets`.
+ - One test per exclusion: opted out, no generation, gated, import-gated, pooled, a path row under the host, split owners, a manifest read failing, no authority yet.
+ - `TestPublishesTheUnionOfInstalledAndServingBuilds` (installed declares `/a/`, serving declares `/b/`); `TestSleepingInstanceKeepsTheInstalledBuildsPaths` (`servingBuild` false).
+ - `TestAliasPublishesItsPrimarysBlock`; `TestCatchallAliasNeverPublishes`.
+ - `TestConsoleClassRouteDoesNotPublish` (Review Focus 4).
+ - `TestRewritesWhenAtAdvancesTwentyMinutes` and `TestDoesNotRewriteForASmallerAdvance` (`putCountingStore` counts PUTs).
+ - `TestFailedRecordReadsStopAtFromAdvancing`: a `ReconcileReport` omitting the app for 61 minutes of fake time while edges-file writes succeed; the published `at` stays at the last complete pass.
+- [ ] **Step 2:** Run `go test ./internal/host/ -run 'TestAssetAuthority|TestAssetManifestCache|TestPublishes|TestUnarmedBoxPublishesNoAssets|TestSleepingInstance|TestAliasPublishes|TestCatchallAliasNever|TestConsoleClassRoute|TestRewritesWhenAt|TestDoesNotRewrite|TestFailedRecordReads' -count=1`. Expected: FAIL.
+- [ ] **Step 3:** Implement; wire the clocks into `reconcileSerializedWith` and both alias call sites, record pass start times, hand the writer the authority, the manifest cache and `servingBuild`.
+- [ ] **Step 4:** Run the same command. Expected: PASS.
+- [ ] **Step 5:** Commit.
+
+### Task 9: Every edge parses `assets`; console readers see it
+
+**Files:**
+- Modify: `internal/edge/homemap.go:177-220` (edgesHostView, homeEntry), `:706-834` (foldHomes copies the block from the winning entry), add `AssetsFor`; `internal/edgesfile/edgesfile.go:57` (Host), `snapshot.go:28` (PlacementView)
+- Test: `internal/edge/homemap_assets_test.go` (create), `internal/edgesfile/edgesfile_test.go`
+
+**Interfaces:**
+- Produces:
+ - `type HostAssets struct { Account, App string; Paths []string; Gen string; At time.Time }` in `internal/edge`
+ - `func (m *homeMap) AssetsFor(host string, now time.Time) (HostAssets, bool)`: exact hosts only (no one-label wildcard match), owner present, `now.Sub(At) <= time.Hour`
+ - `edgesfile.Host.Assets *edgesfile.HostAssets` with the same wire shape as `edgeAssets`
+
+- [ ] **Step 1: Write the failing tests.** `TestHomeMapParsesAssets` (the JSON shape Task 8's writer emits, copied from a writer test's golden so both sides pin one shape); `TestAssetsForRefusesAStaleAt` (61 minutes); `TestAssetsForKeepsAStaleParseStale` (an unreadable file keeps its previous parse, and its `at` still ages out, `homemap.go:459-471`); `TestAssetsForNeverMatchesAWildcard`; `TestAssetsForFollowsTheWinningEntry` (two boxes claim a host; the dial winner's block is the one); `TestOldFileWithoutAssetsParses`. In `internal/edgesfile`: `TestParseKeepsAssets`.
+- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestHomeMapParsesAssets|TestAssetsFor|TestOldFileWithoutAssets' -count=1` and `go test ./internal/edgesfile/ -run 'TestParseKeepsAssets' -count=1`. Expected: FAIL.
+- [ ] **Step 3:** Implement.
+- [ ] **Step 4:** Run both commands. Expected: PASS.
+- [ ] **Step 5:** Commit.
+
+### Task 10: CLI verbs and docs
+
+**Files:**
+- Create: `assets_cli.go`, `instances_assets.go`, `assets_cli_test.go`
+- Modify: `main.go:43` (case `assets`), `main.go:211` (usage), `cli.go:362-400` (`set-assets` in `runInstances`), `apimode.go` (`instanceSetAssets`, `assetsPurge`, `assetsStatus`), `docs/site/cli.md`
+
+**Interfaces:**
+- Consumes: the doors from Task 5.
+- Produces: `func runAssets(args []string)` (`purge`, `status`), `func runInstancesSetAssets(args []string)`; `func (s *apiSession) instanceSetAssets(ctx context.Context, a apiAcct, app, host string, on bool, messages ...string) (apiInstanceRecord, error)`, `assetsPurge(ctx, a, app string, messages ...string) (apiAssetsPurgeResult, error)`, `assetsStatus(ctx, a, app string) (apiAssetsStatus, error)`.
+
+- [ ] **Step 1: Write the failing tests.** `TestInstanceSetAssetsPatches` (the shape of `TestInstanceSetSteeringPatches`); `TestAssetsPurgePrintsPartial` (EAC-13 names the instance and says to run it again) and `TestAssetsPurgePrintsComplete` (EAC-12); `TestAssetsStatusPrintsTheBound` (EAC-14); `TestAssetsPurgeOlderConsole` (404 maps to the "console predates" form the restart verb uses). The docsite gates `TestCLICommandsAreDocumented` and `TestFencedCarlosCommandsAreReal` cover the new heading.
+- [ ] **Step 2:** Run `go test . -run 'TestInstanceSetAssets|TestAssetsPurge|TestAssetsStatus' -count=1` and `go test ./internal/docsite/ -count=1`. Expected: FAIL.
+- [ ] **Step 3:** Implement on the `features_cli.go` pattern (mutation message flags, `consoleOption`, `resolvedAccount`, `resolvedApp`); write the docs rows EAC-16 to EAC-19; add EAC-10 to EAC-14 and EAC-16 to EAC-19 to `assetCacheCopyBuilt`.
+- [ ] **Step 4:** Run both commands and `go test . -run 'TestAssetCacheCopy' -count=1`. Expected: PASS.
+- [ ] **Step 5:** Commit.
+
+### Task 11: Census, weights, merge and CI
+
+**Files:**
+- Create: `internal/provision/testdata/plan14-root-reads-edge-asset-cache-1.tsv`, `docs/superpowers/notes/census-proposal-edge-asset-cache-1.md`
+- Modify: `internal/provision/plan14_root_reads_test.go` (load the overlay last, check the previous seal), `.amadan/test-weights.tsv`
+
+- [ ] **Step 1:** `git fetch origin && git merge origin/main`. Re-read which overlay loads last.
+- [ ] **Step 2:** Run `go test ./internal/provision/ -run TestPlan14RootReadCensus -count=1`. Expected: FAIL naming exactly the units and envelopes in "Census impact".
+- [ ] **Step 3:** Write the overlay rows and the notes file; post the ruling request on #280.
+- [ ] **Step 4:** Run the census test. Expected: PASS. Run the cross-compiles and `gofmt -l .`. Expected: clean.
+- [ ] **Step 5:** Add weights for any new top-level test over a second (time it with `-run` and `-v`). Commit, push, wait for CI on the tip, then merge per Global Constraints.
diff --git a/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md b/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md
index 2952a8983..b1ca59a40 100644
--- a/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md
+++ b/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md
@@ -1,6 +1,6 @@
# Edge asset cache: versioned build assets served from every edge
-Branch `edge-asset-cache`. Status: rev 7, 2026-10-24, builder check folded in; rulings R1 to R3 (§13). Approved by Paul 2026-10-24; Astra accepted rev 6 in round 4; rev 7 corrections under review (round 5).
+Branch `edge-asset-cache`. Status: rev 8, 2026-10-24, builder check folded in; rulings R1 to R3 (§13). Approved by Paul 2026-10-24; Astra accepted rev 6 in round 4; rev 7 and 8 corrections under review (round 6).
Related: media roadmap (`2026-09-07-media-platform-roadmap-design.md`),
which this does not amend; #368 (the shared cache stores 206 responses).
@@ -231,7 +231,12 @@ that catches gains:
assets are caught, and the old build's prefix drops out when its
generation retires. The home reads each build's manifest from the
bucket with `release.GetManifest` (`internal/release/release.go:710`)
- once per version and keeps it in memory, since a manifest is immutable.
+ once per version and keeps only the declared paths, since a manifest is
+ immutable. It keeps them only for builds a local route currently
+ references, as its installed version or its serving build; each
+ edges-file pass drops the rest, and a rollback to a dropped build reads
+ its manifest again. Retained state is therefore bounded by twice the
+ box's routes, whatever the deploy history **(astra R5-1)**.
It does not use the staged copy under `/opt/carlos/versions`
(`release.ReadStagedManifest`, `versions.go:261`), which an older binary
may have re-marshalled without the `assets` key. A manifest that cannot
@@ -854,3 +859,6 @@ On flagship, the same from a small box with the disk figures recorded.
every `ReconcileAliases` log-and-continue point and silent input is
listed; the opt-out verb is `carlos instances set-assets`; two citation
fixes.
+- rev 8, 2026-10-24: Astra round 5 (`CODEX-FINDINGS-ASSET-CACHE-R5.md`,
+ reject on one P2): the manifest cache keeps only declared paths, only
+ for builds a local route references, pruned every pass (R5-1).
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-R5.md b/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-R5.md
new file mode 100644
index 000000000..e22020046
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-R5.md
@@ -0,0 +1,7 @@
+1. **P2 — The new manifest cache has no retention bound.** §4.1 now requires reading each version once and keeping its manifest in memory ([spec:232](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:232)). `GetManifest` returns a newly decoded manifest; it supplies no cache or eviction policy ([release.go:710](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/release/release.go:710)). Repeated deployments therefore accumulate retained manifests even for one route, after those builds stop serving. The disk budget and fill limits do not bound this memory.
+
+ **Smallest fix:** retain only the bounded asset declaration for builds currently referenced by a local route’s installed version or serving generation; discard unreferenced entries and permit refetching on rollback. Add a test that cycles one route through many versions and verifies retained state stays bounded while both currently needed declarations remain available.
+
+No other delta findings. The remaining corrections match the code and do not reopen the round 4 acceptance.
+
+reject
\ No newline at end of file