edge asset cache plans: Astra plan review round 2 folded in
Summary
Round 2 fixes: no equality shortcut in the reconcile report, read leases keep retired bodies counted, the authority table moves to internal/host, generation fixtures select generation routing, an explicit old-edge hop simulation, no replay onto a started response, copy ids renumbered EAC-1 to EAC-24, and complete test selectors.
Agent notes
Findings committed unedited as CODEX-FINDINGS-ASSET-CACHE-PLANS-R2.md.
4 files changed,
+67
−31
docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md+14 −13docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md+3 −3docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md+18 −15docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-PLANS-R2.md+32 −0
CI passed — run details
CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (0 s)
ok amadan.net/carlos/platform.git/launchctx 0.013s
? 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 (149 s)
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 4.044s
ok github.com/carlosframework/platform/cmd/moveprobe 11.950s
ok github.com/carlosframework/platform/cmd/zddprobe 0.757s
ok github.com/carlosframework/platform/internal/activator 58.123s
ok github.com/carlosframework/platform/internal/agentsock 0.021s
ok github.com/carlosframework/platform/internal/alerting 0.005s
ok github.com/carlosframework/platform/internal/analytics 0.027s
ok github.com/carlosframework/platform/internal/appconfig 3.353s
ok github.com/carlosframework/platform/internal/apperr 0.216s
ok github.com/carlosframework/platform/internal/applog 0.335s
ok github.com/carlosframework/platform/internal/awsinv 0.004s
ok github.com/carlosframework/platform/internal/boxchoice 0.006s
ok github.com/carlosframework/platform/internal/boxid 1.250s
? 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.019s
ok github.com/carlosframework/platform/internal/caps 0.043s
ok github.com/carlosframework/platform/internal/certwarm 0.003s
ok github.com/carlosframework/platform/internal/cgroup 0.788s
ok github.com/carlosframework/platform/internal/changelog 0.532s
ok github.com/carlosframework/platform/internal/claimactivation 1.336s
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 8.005s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.021s
ok github.com/carlosframework/platform/internal/dbquery 0.010s
ok github.com/carlosframework/platform/internal/dbreads 2.055s
? github.com/carlosframework/platform/internal/dbreads/dbreadstest [no test files]
ok github.com/carlosframework/platform/internal/dbverify 0.007s
ok github.com/carlosframework/platform/internal/dnsrec 0.005s
ok github.com/carlosframework/platform/internal/docsite 0.014s
ok github.com/carlosframework/platform/internal/domains 3.030s
ok github.com/carlosframework/platform/internal/doorkey 0.011s
ok github.com/carlosframework/platform/internal/economics 0.005s
ok github.com/carlosframework/platform/internal/edge 45.612s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.009s
ok github.com/carlosframework/platform/internal/fleetapi 0.186s
ok github.com/carlosframework/platform/internal/gate 1.849s
ok github.com/carlosframework/platform/internal/genselect 0.034s
ok github.com/carlosframework/platform/internal/geo 0.038s
ok github.com/carlosframework/platform/internal/geoip 0.064s
ok github.com/carlosframework/platform/internal/historyreadback 0.281s
ok github.com/carlosframework/platform/internal/host 145.918s
ok github.com/carlosframework/platform/internal/hostactors 6.741s
ok github.com/carlosframework/platform/internal/hostactors/awsmembership 0.009s
ok github.com/carlosframework/platform/internal/hostbox 3.776s
ok github.com/carlosframework/platform/internal/hostclaim 0.895s
ok github.com/carlosframework/platform/internal/hostenv 0.007s
ok github.com/carlosframework/platform/internal/hostevidence 0.007s
ok github.com/carlosframework/platform/internal/hostexec 5.872s
ok github.com/carlosframework/platform/internal/hostfence 0.049s
ok github.com/carlosframework/platform/internal/hostfiles 10.401s
ok github.com/carlosframework/platform/internal/hostfloor 0.024s
ok github.com/carlosframework/platform/internal/hostguard 1.068s
ok github.com/carlosframework/platform/internal/hostops 2.341s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.003s
ok github.com/carlosframework/platform/internal/hostquarantine 0.005s
ok github.com/carlosframework/platform/internal/hostrelocate 0.758s
ok github.com/carlosframework/platform/internal/hostresolve 0.245s
ok github.com/carlosframework/platform/internal/hostwrite 0.003s
ok github.com/carlosframework/platform/internal/infraview 0.004s
ok github.com/carlosframework/platform/internal/instancectl 5.246s
ok github.com/carlosframework/platform/internal/instanceexec 2.851s
ok github.com/carlosframework/platform/internal/instancefs 0.012s
ok github.com/carlosframework/platform/internal/instancekey 0.006s
ok github.com/carlosframework/platform/internal/instanceupdates 0.014s
ok github.com/carlosframework/platform/internal/layout 1.991s
ok github.com/carlosframework/platform/internal/lbhealth 0.008s
ok github.com/carlosframework/platform/internal/lease 10.140s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.638s
ok github.com/carlosframework/platform/internal/ledger 0.021s
ok github.com/carlosframework/platform/internal/mail 0.271s
ok github.com/carlosframework/platform/internal/maintenance 0.087s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.649s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.071s
ok github.com/carlosframework/platform/internal/metrics 0.008s
ok github.com/carlosframework/platform/internal/moves 21.675s
ok github.com/carlosframework/platform/internal/moves/archive 1.087s
ok github.com/carlosframework/platform/internal/objstore 0.372s
ok github.com/carlosframework/platform/internal/ops 0.212s
ok github.com/carlosframework/platform/internal/park 0.278s
ok github.com/carlosframework/platform/internal/plans 0.006s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 15.117s
ok github.com/carlosframework/platform/internal/publicrelease 0.030s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 5.692s
ok github.com/carlosframework/platform/internal/relay 7.208s
ok github.com/carlosframework/platform/internal/release 25.627s
ok github.com/carlosframework/platform/internal/rollout 3.917s
ok github.com/carlosframework/platform/internal/rolloutwords 0.004s
ok github.com/carlosframework/platform/internal/rootaws 0.010s
ok github.com/carlosframework/platform/internal/rootparticipation 0.070s
ok github.com/carlosframework/platform/internal/rootstate 0.030s
ok github.com/carlosframework/platform/internal/runs 0.563s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.005s
ok github.com/carlosframework/platform/internal/schedule 2.834s
ok github.com/carlosframework/platform/internal/seeds 0.015s
ok github.com/carlosframework/platform/internal/sim/engines 2.934s
ok github.com/carlosframework/platform/internal/sim/hostlife 11.827s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.987s
ok github.com/carlosframework/platform/internal/status 0.085s
ok github.com/carlosframework/platform/internal/steering 0.011s
ok github.com/carlosframework/platform/internal/stopevidence 0.231s
ok github.com/carlosframework/platform/internal/svcrole 0.006s
ok github.com/carlosframework/platform/internal/telemetry 0.007s
ok github.com/carlosframework/platform/internal/termfence 0.005s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.006s
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.761s
=== internal_console@0 (82 s)
ok github.com/carlosframework/platform/internal/console 79.579s
=== internal_console@1 (44 s)
ok github.com/carlosframework/platform/internal/console 42.102s
=== internal_console@2 (42 s)
ok github.com/carlosframework/platform/internal/console 40.076s
=== internal_console@3 (41 s)
ok github.com/carlosframework/platform/internal/console 39.333s
=== internal_console@4 (44 s)
ok github.com/carlosframework/platform/internal/console 42.134s
=== internal_console@5 (42 s)
ok github.com/carlosframework/platform/internal/console 39.449s
=== internal_console@6 (38 s)
ok github.com/carlosframework/platform/internal/console 35.326s
=== internal_console@7 (47 s)
ok github.com/carlosframework/platform/internal/console 44.638s
=== root@0 (110 s)
ok github.com/carlosframework/platform 107.710s
=== root@1 (86 s)
ok github.com/carlosframework/platform 83.354s
=== root@2 (93 s)
ok github.com/carlosframework/platform 90.709s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 50528418 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: cached, digest verified
mc: cached, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.027s
=== RUN TestT4StorageProbeMinio
2026/10/24 18:56:47 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E18C22B38938C4, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.023s
=== RUN TestS3StoreMinioContract
=== RUN TestS3StoreMinioContract/conditional_get_and_etag_listing
=== RUN TestS3StoreMinioContract/bump_through_every_mutation_path
--- PASS: TestS3StoreMinioContract (0.03s)
--- PASS: TestS3StoreMinioContract/conditional_get_and_etag_listing (0.01s)
--- PASS: TestS3StoreMinioContract/bump_through_every_mutation_path (0.02s)
PASS
ok github.com/carlosframework/platform/internal/release 0.035s
Bucket created successfully `local/dbf-deploy`.
Bucket created successfully `local/dbf-data-ie`.
Bucket created successfully `local/dbf-data-uk`.
local/dbf-deploy versioning is enabled
Created policy `dbf-console` successfully.
Created policy `dbf-ukbox` successfully.
Added user `dbfconsole` successfully.
Added user `dbfukbox` successfully.
Attached Policies: [dbf-console]
To User: dbfconsole
Attached Policies: [dbf-ukbox]
To User: dbfukbox
=== RUN TestDBForwardMinio
2026/10/24 18:56:49 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 18:56:49 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 18:56:49 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 18:56:49 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000002
2026/10/24 18:56:49 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000002 maxTXID=0000000000000002 size=370
2026/10/24 18:56:49 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000002
2026/10/24 18:56:49 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000003
2026/10/24 18:56:49 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000003 maxTXID=0000000000000003 size=454
2026/10/24 18:56:49 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000003 txid.db=0000000000000003
2026/10/24 18:56:49 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 18:56:49 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 18:56:49 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 18:56:49 console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically
2026/10/24 18:56:49 console: could not notify member@example.com of a CLI token issue: keymail: lookup member@example.com: keymail: GET /api/federation/lookup?addr=member%40example.com&from=console%40127.0.0.1%3A40897: could not determine target mailbox
agent.go:135: dbreads: dbr_ciwuiymb34mpllp3v4sxg3ayd4: planned 3 files (1544 bytes) and restored to 3 in 10ms
agent.go:135: dbreads: dbr_ciwuiymb34mpllp3v4sxg3ayd4: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:56:50 console: db read dbr_ciwuiymb34mpllp3v4sxg3ayd4: relayed 1 chunks (102 bytes) in 1.005s, ended done
agent.go:135: dbreads: dbr_6gvh4lampoys344kjgypov6tje: planned 1 files (720 bytes) and restored to 1 in 9ms
agent.go:135: dbreads: dbr_6gvh4lampoys344kjgypov6tje: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:56:51 console: db read dbr_6gvh4lampoys344kjgypov6tje: relayed 1 chunks (102 bytes) in 1.004s, ended done
agent.go:135: dbreads: dbr_i47uelmrkiw4fwj7mxjjuwodl4: planned 3 files (1544 bytes) and held through 3 in 10ms
agent.go:135: dbreads: dbr_i47uelmrkiw4fwj7mxjjuwodl4: output 1544 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:56:52 console: db read dbr_i47uelmrkiw4fwj7mxjjuwodl4: relayed 1 chunks (1641 bytes) in 1.004s, ended done
=== RUN TestDBForwardMinio/by_disconnect
=== NAME TestDBForwardMinio
agent.go:135: dbreads: dbr_pgogrjuic6ngkckk3xlnyb2m5u: planned 3 files (1544 bytes) and restored to 3 in 10ms
2026/10/24 18:56:53 console: db read dbr_pgogrjuic6ngkckk3xlnyb2m5u: relayed 0 chunks (0 bytes) in 40ms, ended client gone
agent.go:135: dbreads: dbr_pgogrjuic6ngkckk3xlnyb2m5u: output 5 bytes in 1 confirmed chunks in 104ms
=== RUN TestDBForwardMinio/by_timeout
=== NAME TestDBForwardMinio
agent.go:135: dbreads: dbr_6solq45qqvl6aealakgeaohp6m: planned 3 files (1544 bytes) and restored to 3 in 10ms
2026/10/24 18:57:13 console: db read dbr_6solq45qqvl6aealakgeaohp6m: relayed 0 chunks (0 bytes) in 19.81s, ended no-result
agent.go:135: dbreads: dbr_6solq45qqvl6aealakgeaohp6m: output 5 bytes in 1 confirmed chunks in 19.784s
--- PASS: TestDBForwardMinio (23.25s)
--- PASS: TestDBForwardMinio/by_disconnect (0.23s)
--- PASS: TestDBForwardMinio/by_timeout (19.92s)
PASS
ok github.com/carlosframework/platform/internal/console 23.272s
minio contract: PASS
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| index 5efd47a00..0db4145e9 100644 |
| --- 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 |
| @@ -49,7 +49,7 @@ Every task's requirements include this section. |
| ## Copy strings |
| -Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text is in the copy record, not here). The inventory is EAC-1 to EAC-24; plan 3 adds EAC-25 and EAC-26 (astra P1-16): |
| +Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text is in the copy record, not here). The inventory is EAC-1 to EAC-22; plan 3 adds EAC-23 and EAC-24 (astra P1-16): |
| | id | where | what it says | |
| |---|---|---| |
| @@ -63,9 +63,9 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text |
| | 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` | |
| -| EAC-22 | `deploy.go` | `--assets` and `--no-assets` given together | |
| -| EAC-23 | `internal/console/apiship_assets.go` | a re-ship of a version with different asset paths refused | |
| -| EAC-24 | `apimode.go` (`checkAssetsEcho`) | the console did not echo the asset paths: it predates them | |
| +| EAC-20 | `deploy.go` | `--assets` and `--no-assets` given together | |
| +| EAC-21 | `internal/console/apiship_assets.go` | a re-ship of a version with different asset paths refused | |
| +| EAC-22 | `apimode.go` (`checkAssetsEcho`) | the console did not echo the asset paths: it predates them | |
| ## File structure |
| @@ -103,10 +103,10 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text |
| **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-24 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-24 (plan 3 Task 0 extends it to EAC-26); every id in `assetCacheCopyBuilt` is `approved` and its text appears verbatim in its file. Start with `assetCacheCopyBuilt` empty. |
| +- [ ] **Step 1:** Write the copy record: a table `| id | file | text | status |` with rows EAC-1 to EAC-22 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-22 (plan 3 Task 0 extends it to EAC-24); 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 24 rows. Apply Paul's edits verbatim and mark each row `approved`. Rows he defers stay `pending`, and the task that builds them waits. |
| +- [ ] **Step 4:** Run the `copy-review` skill on the 22 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 |
| @@ -147,7 +147,7 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text |
| - `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, EAC-23 and EAC-24 to `assetCacheCopyBuilt`. |
| +- [ ] **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, EAC-21 and EAC-22 to `assetCacheCopyBuilt`. |
| - [ ] **Step 4:** Run both commands. Expected: PASS. |
| - [ ] **Step 5:** Commit. |
| @@ -161,9 +161,9 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text |
| - 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 EAC-22; 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 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 EAC-20; 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, EAC-9 and EAC-22 to `assetCacheCopyBuilt`. |
| +- [ ] **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, EAC-9 and EAC-20 to `assetCacheCopyBuilt`. |
| - [ ] **Step 4:** Run the same command. Expected: PASS. |
| - [ ] **Step 5:** Commit. |
| @@ -225,9 +225,9 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text |
| - `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** in-scope record was positively applied, meaning `convergeOwned`, a mint or an adoption ran for it without error, or its existing row's `AssetsOff` and `AssetGen` already equal `RouteFor`'s. A record the pass skipped for any other reason (a trash marker that could not be read sets `declineMint`, `reconcile.go:1137`, and the loop skips the record at `:1174` with no failing write; a held claim; a deferred adoption) makes its app incomplete (astra P1-7). `readFailed` from an unreadable account manifest makes every app of that account incomplete. A pass that returns an error leaves `Complete` empty. |
| +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** in-scope record was positively applied, meaning every convergence step the pass runs for it (`convergeOwned`, the gate convergence at `reconcile.go:1366-1374`, a mint or an adoption) ran and returned nil, including steps that found nothing to change. There is no equality shortcut on a subset of columns: a record whose gate convergence failed is not applied even if its asset columns already match (astra P2-R1-7). A record the pass skipped for any other reason (a trash marker that could not be read sets `declineMint`, `reconcile.go:1137`, and the loop skips the record at `:1174` with no failing write; a held claim; a deferred adoption) makes its app incomplete (astra P1-7). `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`; `TestReportOmitsAnAppWhoseRecordWasSkipped` (an unreadable trash marker with the record's `AssetGen` changed: the row keeps the old generation and the app is not complete); `TestReportNilKeepsTodaysBehaviour`. |
| +- [ ] **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`; `TestReportOmitsAnAppWhoseRecordWasSkipped` (an unreadable trash marker with the record's `AssetGen` changed: the row keeps the old generation and the app is not complete); `TestReportOmitsAnAppWhoseGateConvergenceFailed` (asset columns already equal, `SetGate` failing through a SQLite trigger as in Task 7); `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. |
| @@ -279,8 +279,9 @@ Publication rule for a host, applied in `edgesFileWriteClaims` through `assetPol |
| - `TestConsoleClassRouteDoesNotPublish` (Review Focus 4). |
| - `TestRewritesWhenAtAdvancesTwentyMinutes` and `TestDoesNotRewriteForASmallerAdvance` (`putCountingStore` counts PUTs). |
| - `TestServingBuildSwitchRepublishesWithAnUnchangedFingerprint`. |
| + - **The authority table** (spec §12; moved here from plan 3 so it runs where the real policy lives, with no import cycle, astra P2-3): for each input (record scope incomplete, claim listing failing, catch-all label count failing, instance alias upsert failing, alias upsert failing, catch-all upsert failing, alias delete failing, wildcard upsert or delete failing, path upsert failing, path delete failing, an unreadable claim body, `deploymentRoutes` nil), held failing for 61 minutes of fake time with every other input and the edges-file writes healthy: `TestAuthorityTable/<input>` asserts the published `at` stopped advancing, so it is over an hour old, and that `assetPolicy.For` returns false for the home's own catch. Plus `TestExactClaimReplacingADerivedAliasStopsIt` and `TestPathRowAddedUnderAPrimaryStopsIt` (a gated target), each checked through `Publish` and `For`. The edge's half (an `at` over an hour old forwards; a changed owner or generation serves no old entry) is plan 3's Tasks 6 and 8. |
| - `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 2:** Run `go test ./internal/host/ -run 'TestAssetAuthority|TestAuthorityTable|TestExactClaimReplacing|TestPathRowAdded|TestServingBuildSwitch|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. |
diff --git a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md| index 22fa137ea..e0453e86e 100644 |
| --- a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md |
| +++ b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md |
| @@ -126,12 +126,12 @@ Every task's requirements include this section. |
| - Produces: |
| - `const MaxMeta = metaReserve` (64 KiB) |
| - `func (f *Fill) Commit(m Meta, gzipCopy bool) error`: encodes the meta first and returns `ErrTooLarge` (behaving as `Abort`) when it exceeds `MaxMeta`, so no write passes the reservation (astra P1-8); gzips the identity body into a temporary when `gzipCopy`, abandoning the copy (`GzipSize = 0`) if it would exceed the identity size or the reservation's gzip half; draws a fresh `Version`; fsyncs and renames the bodies to their versioned names, then renames the meta temporary over `<id>.meta`, then deletes the previous version's bodies; sets `Size`, `GzipSize`, `StoredAt = Now()`; counts the meta's bytes in the store's used bytes; releases the reservation; on any error behaves as `Abort`. A `Commit` for a key that already has an entry replaces it. A reader holding the old files keeps reading them (unlinked files stay readable on Linux). |
| - - `type Entry struct { Meta Meta }`; `func (e *Entry) Open(gzip bool) (*os.File, error)` |
| + - `type Entry struct { Meta Meta }`; `func (e *Entry) Open(gzip bool) (*Reader, error)`; `type Reader struct` wrapping the `*os.File` (`Read`, `Seek`, `ReadAt`, `Stat`, so `http.ServeContent` takes it) whose `Close` releases a **lease** on the body version (astra P2-2). A version that is replaced, evicted, expired or swept while leased is unlinked at once but its bytes stay counted in the store's used bytes until its last lease closes, because an unlinked open file still holds its disk space. `Stats` gains `Retired int64` (bytes held only by open readers). |
| - `func (s *Store) Get(k Key) (*Entry, bool)`: false and the entry deleted when `Now()` is at or past `Meta.Expires`; marks the entry recently used. |
| - `func (m Meta) Age(now time.Time) time.Duration` (`Lifetime - Expires.Sub(now)`, never below zero) |
| -- [ ] **Step 1: Write the failing tests.** `TestCommitThenGetRoundTrips` (header and body bytes equal); `TestCommitWritesMetaLast` (a crash simulated by stopping before the meta rename leaves `Open` with no entry); `TestReplacementCrashKeepsTheOldEntry` (an existing entry, a replacement stopped after its bodies are renamed but before the meta: `Open` serves the old bytes with the old headers and removes the new bodies); `TestOversizedMetaIsRefused` (a 70 KiB header value and a 70 KiB query each give `ErrTooLarge`, nothing left on disk, reservation released); `TestGzipCopyIsAbandonedWhenLarger` (random bytes); `TestGzipCopyIsKept` (CSS text; reading `.gz` through `gzip.NewReader` gives the identity bytes); `TestGetExpires` (fake `Now` past `Expires`: false, files gone); `TestCommitReplacesAnEntry`; `TestAgeNeverNegative`. |
| -- [ ] **Step 2:** Run `go test ./internal/assetcache/ -run 'TestCommit|TestGzipCopy|TestGetExpires|TestAge' -count=1`. Expected: FAIL. |
| +- [ ] **Step 1: Write the failing tests.** `TestCommitThenGetRoundTrips` (header and body bytes equal); `TestCommitWritesMetaLast` (a crash simulated by stopping before the meta rename leaves `Open` with no entry); `TestReplacementCrashKeepsTheOldEntry` (an existing entry, a replacement stopped after its bodies are renamed but before the meta: `Open` serves the old bytes with the old headers and removes the new bodies); `TestOversizedMetaIsRefused` (a 70 KiB header value and a 70 KiB query each give `ErrTooLarge`, nothing left on disk, reservation released); `TestEvictedBodyStaysCountedWhileAReaderHoldsIt` and `TestReplacedBodyStaysCountedWhileAReaderHoldsIt` (`Stats().Retired` is the old body's size until the reader closes, and a fill that would only fit by counting those bytes as free is refused); `TestGzipCopyIsAbandonedWhenLarger` (random bytes); `TestGzipCopyIsKept` (CSS text; reading `.gz` through `gzip.NewReader` gives the identity bytes); `TestGetExpires` (fake `Now` past `Expires`: false, files gone); `TestCommitReplacesAnEntry`; `TestAgeNeverNegative`. |
| +- [ ] **Step 2:** Run `go test ./internal/assetcache/ -run 'TestCommit|TestReplacementCrash|TestOversizedMeta|TestEvictedBody|TestReplacedBody|TestGzipCopy|TestGetExpires|TestAge' -count=1`. Expected: FAIL. |
| - [ ] **Step 3:** Implement. |
| - [ ] **Step 4:** Run the same command. Expected: PASS. |
| - [ ] **Step 5:** Commit. |
diff --git a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md| index 9896fa90d..a296dae44 100644 |
| --- a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md |
| +++ b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md |
| @@ -44,8 +44,8 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0 |
| | id | where | what it says | |
| |---|---|---| |
| -| EAC-25 | `docs/site/cli.md`, under `carlos ship --assets` | files under an asset path are fetched without the visitor's cookies (spec §5.2) | |
| -| EAC-26 | same | what `Carlos-Cache: hit`, `miss` and `bypass` mean, and that only responses marked cacheable, status 200, not HTML and without `Set-Cookie` are kept | |
| +| EAC-23 | `docs/site/cli.md`, under `carlos ship --assets` | files under an asset path are fetched without the visitor's cookies (spec §5.2) | |
| +| EAC-24 | same | what `Carlos-Cache: hit`, `miss` and `bypass` mean, and that only responses marked cacheable, status 200, not HTML and without `Set-Cookie` are kept | |
| ## File structure |
| @@ -65,7 +65,7 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0 |
| ### Task 0: Copy rows |
| -- [ ] **Step 1:** Add EAC-25 and EAC-26 to the copy record as `pending` and extend the gate's inventory to EAC-1 to EAC-26. |
| +- [ ] **Step 1:** Add EAC-23 and EAC-24 to the copy record as `pending` and extend the gate's inventory to EAC-1 to EAC-24. |
| - [ ] **Step 2:** Run the `copy-review` skill on the two rows; apply Paul's edits verbatim; mark them `approved`. |
| - [ ] **Step 3:** `go test . -run 'TestAssetCacheCopy' -count=1`. Expected: PASS. Commit. |
| @@ -85,7 +85,7 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0 |
| - `func verifyAssetAck(key []byte, assertion string, expires int64, ack string) bool` (the far edge checks with the key it signed the assertion with) |
| - [ ] **Step 1: Write the failing tests.** `TestAssetFillRoundTrips`; `TestAssetFillRefusesEachChangedClaim` (table: host, path-query, account, app, gen, timestamp outside skew); `TestHopSignatureIsNeverAnAssetFill` (a `signHop` value for the same host and time fails `verifyAssetFill`); `TestAssetFillAcceptsTheNextKey`; `TestAssetAckBindsAssertionAndExpiry` (changing either fails); `TestAckUsesTheKeyTheAssertionMatched` (home holds current and next, far signs with current only: the ack verifies at the far edge). |
| -- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestAssetFill|TestHopSignatureIsNever|TestAssetAck' -count=1`. Expected: FAIL. |
| +- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestAssetFill|TestHopSignatureIsNever|TestAssetAck|TestAckUsesTheKey' -count=1`. Expected: FAIL. |
| - [ ] **Step 3:** Implement. |
| - [ ] **Step 4:** Run the same command. Expected: PASS. |
| - [ ] **Step 5:** Commit. |
| @@ -157,7 +157,7 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0 |
| - `func (e *Edge) serveAssetFillForHop(w http.ResponseWriter, r *http.Request, host string, up upstream) bool` (true when it handled a verified, valid fill; it marks the request so the meter defer skips it) |
| - `func (e *Edge) fillFromApp(ctx context.Context, w http.ResponseWriter, r *http.Request, up upstream, k assetcache.Key, a HostAssets, onHeader func(v assetVerdict, h http.Header)) (*assetcache.Entry, error)`: builds the allow-listed request (`Host`, `User-Agent`, `Accept`, `Accept-Encoding: identity`; method `GET` even for `HEAD`), sends it through the activator for a hibernating route or `up.proxy` otherwise, reads the answering build from the response's `X-Carlos-Version` and asks `e.assets.local.Declared(host, build, r.URL.Path)` with the request hostname (astra P1-2), calls `assetAdmit`, calls `onHeader` before the first body byte, writes the body to a `Fill` while the verdict says store while hashing it with SHA-256, abandons the fill at the 60 s deadline, on a short body or on `assetTrailerVeto`, and on success sets a strong `ETag` of `"<hex sha256>"` in the stored header when the app sent none (spec §5.4; the far edge computes the same over the same bytes, so home and far validators match) and commits (astra P1-14). |
| -**The initiating request.** When the initiator's request has no `Range`, `If-None-Match`, `If-Modified-Since` or `If-Range`, the body is teed to it as it fills. When it has any of them, nothing is teed: the initiator waits for the commit and is answered by `serveAssetEntry`, so its conditional or range is honoured on a cold miss as on a hit (astra P1-10). If the verdict refuses at header time, the full response is streamed to it unmodified (a 200 to a range request is valid HTTP) with `Carlos-Cache: bypass`; if the fill is abandoned after the body (deadline, short body, trailer veto), the initiator's request is forwarded again as today and answered `bypass`. |
| +**The initiating request.** When the initiator's request has no `Range`, `If-None-Match`, `If-Modified-Since` or `If-Range`, the body is teed to it as it fills. When it has any of them, nothing is teed: the initiator waits for the commit and is answered by `serveAssetEntry`, so its conditional or range is honoured on a cold miss as on a hit (astra P1-10). If the verdict refuses at header time, the full response is streamed to it unmodified (a 200 to a range request is valid HTTP) with `Carlos-Cache: bypass`. If the fill is abandoned after the headers (deadline, short body, trailer veto), what happens depends on whether the initiator's response has started: a **streamed** initiator's delivery simply continues to the end of the upstream body exactly as an uncached response would, and only the caching is abandoned; nothing is ever replayed onto a started response. A **buffered** initiator (one with `Range` or a conditional, which nothing has been written to yet) has its request forwarded again as today and answered `bypass` (astra P2-1). |
| On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Carlos-Asset-Fill`; it checks `verifyAssetFill`, that `e.assets.local.For(host)` gives the asserted account, app and gen and catches the path; on any failure it returns false and the request continues as an ordinary hop. It answers a hit from the store with `X-Carlos-Asset-Expires` set to the entry's expiry, or fills; in both cases it deletes any app-set `X-Carlos-Asset-Fill-Ack` and `X-Carlos-Asset-Expires`, sets `X-Carlos-Asset-Expires` (`0` when the verdict does not store) and `X-Carlos-Asset-Fill-Ack`, and never touches the shared LRU. |
| @@ -171,13 +171,14 @@ On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Car |
| - `TestColdMissAnswersARangeFromTheEntry` and `TestColdMissAnswersIfNoneMatchFromTheEntry` (the initiator gets 206 or 304, not the fetched 200). |
| - `TestFallbackETagIsTheBodyHash` (no app `ETag`: the entry's is `"<sha256>"`, a conditional hit with it is 304). |
| - `TestSetCookieTrailerVetoesTheFill` (announced and unannounced). |
| + - `TestStreamedInitiatorIsNotReplayedAfterATrailerVeto` and `TestStreamedInitiatorIsNotReplayedAfterTheDeadline` (the client receives exactly one status line and one body, byte-identical to the upstream's). |
| - `TestStalledFillIsAbandonedAtTheDeadline` (deadline hook `assetFillDeadline`; reservation released, `Stats().Reserved == 0`, waiters forwarded). |
| - `TestNonCanonicalPathBypasses`, `TestPostUnderAPrefixPassesThrough`. |
| - `TestHomeFillAcknowledgesAndSkipsTheMeter` (a verified hop with a valid assertion: response has a verifiable ack, the usage meter recorded nothing, the shared LRU has no entry). |
| - `TestHomeFillWithStaleGenGetsNoAck` and `TestHomeFillWithChangedOwnerGetsNoAck` (metered as an ordinary hop). |
| - `TestHomeFillUncacheableIsAcknowledgedWithZeroExpiry`. |
| - `TestAppSetAckHeadersAreStripped` (the backend sets both headers; the home's own replace them; on a plain request neither reaches the client). |
| -- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestLocal|TestHibernatingRouteHit|TestHeadFills|TestQueryIsPart|TestFillSurvives|TestShortBody|TestStalledFill|TestNonCanonicalPath|TestPostUnder|TestHomeFill|TestAppSetAck' -count=1`. Expected: FAIL. |
| +- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestLocal|TestHibernatingRouteHit|TestHeadFills|TestQueryIsPart|TestFillSurvives|TestShortBody|TestStalledFill|TestNonCanonicalPath|TestPostUnder|TestHomeFill|TestAppSetAck|TestColdMiss|TestFallbackETag|TestSetCookieTrailer|TestStreamedInitiator' -count=1`. Expected: FAIL. |
| - [ ] **Step 3:** Implement. |
| - [ ] **Step 4:** Run the same command. Expected: PASS. |
| - [ ] **Step 5:** Commit. |
| @@ -204,7 +205,7 @@ On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Car |
| - `TestFarColdMissAnswersARangeFromTheEntry`. |
| - `TestFarFallbackETagMatchesTheHomes` (no app `ETag`: home and far entries carry the same value). |
| - `TestFarCoalescedWaitersGetTheEntry` and `TestFarRefusedFillReachesOnlyTheInitiator` (differing `Accept`, a `Set-Cookie` response, an unacknowledged fill; each waiter's request reaches the fake home on its own). |
| -- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestFar|TestClientSentFillHeader' -count=1`. Expected: FAIL. |
| +- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestFar|TestClientSentFillHeader' -count=1` (every Task 6 test name starts with one of these). Expected: FAIL. |
| - [ ] **Step 3:** Implement. |
| - [ ] **Step 4:** Run the same command. Expected: PASS. |
| - [ ] **Step 5:** Commit. |
| @@ -228,15 +229,15 @@ On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Car |
| - [ ] **Step 4:** Run both commands. Expected: PASS. |
| - [ ] **Step 5:** Commit. |
| -### Task 8: The authority table, through the edge |
| +### Task 8: Owner and generation changes on a warm cache |
| **Files:** |
| -- Test: `internal/edge/asset_authority_test.go` |
| +- Test: `internal/edge/asset_owner_test.go` |
| -This task adds no production code unless a test fails. It drives spec §12's authority table through the serving path: a home `Edge` whose `LocalAssets` is plan 1's real `assetPolicy` (built through an exported test constructor in `internal/host`, `NewAssetPolicyForTest`, added here if plan 1 did not) over a real registry and `DirStore`, and a far `Edge` reading the edges file plan 1's writer produced. |
| +The authority table itself runs in plan 1 Task 8 (`internal/host`, the real `assetPolicy`), because a test here importing `internal/host` would be an import cycle (`internal/host/agent.go:184` imports `internal/edge`) and an external test package would lose this package's private fixtures (astra P2-3). This task covers the edge's half with the edge's own fixtures: what a far and a home edge do when the published owner or generation changes under a warm cache. |
| -- [ ] **Step 1: Write the tests.** For each input (record scope incomplete, claim listing failing, catch-all label count failing, alias upsert failing, path delete failing), held failing for 61 minutes of fake time with every other input healthy: the home's local catch stops and the far edge forwards. Plus: an exact claim replacing a derived catch-all alias stops serving the old alias's entries on both; a claim adding a path row to a gated target under a primary hostname stops catching it on both; a hostname moved to another account serves none of the old owner's entries on a warm cache; a deleted and recreated hostname serves no old entry. |
| -- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestAssetAuthority' -count=1`. Expected: PASS, or a failure that becomes a fix in the owning plan's code, committed here with its own test. |
| +- [ ] **Step 1: Write the tests.** `TestHostMovedToAnotherAccountServesNoOldEntry` (a warm far entry; the published owner changes; the next request misses and the sweep deletes the old entry); `TestRecreatedHostWithANewGenerationServesNoOldEntry`; `TestHomeLocalCatchStopsWhenForReturnsFalse` (a warm home entry; `LocalAssets.For` turns false; the request is forwarded, not served from disk). |
| +- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestHostMovedToAnotherAccount|TestRecreatedHost|TestHomeLocalCatchStops' -count=1`. Expected: PASS, or a failure that becomes a fix in Tasks 4 to 7, committed with its own test. |
| - [ ] **Step 3:** Commit. |
| ### Task 9: Two-edge acceptance |
| @@ -245,11 +246,11 @@ This task adds no production code unless a test fails. It drives spec §12's aut |
| - Create: `internal/edge/asset_e2e_test.go` |
| - Modify: `internal/edge/backhaul_e2e_test.go` (scene routes gain `Account`, `App`, `AssetGen`; `publishEdgesFile` gains an option to write the golden asset entry) |
| -- [ ] **Step 1: Write the tests** on `newBackhaulScene` with real stores on both edges. Extend `newBackhaulEdge` to take an activator, so the home can run the `genActivator` fixture (`genedge_test.go:16`) for the generation cases. Spec §12's attack matrix runs here through production wiring, not only in Tasks 5 and 6's unit tests (astra P1-15): |
| +- [ ] **Step 1: Write the tests** on `newBackhaulScene` with real stores on both edges. Extend `newBackhaulEdge` to take an activator and, for the generation cases, a route of kind `instance` with `Hibernate` set and a unix socket backend, which is what selects generation routing (`edge.go:740-748`); a TCP `service` route never does (`backhaul_e2e_test.go:267`). Use the `genActivator` fixture (`genedge_test.go:16`) and `startGenApp` (`genproxy_test.go:131`), and have each generation test first assert that the home's `lookupUpstreamPath` result has `genManaged` true, so a fixture that silently fell back to plain proxying fails loudly (astra P2-4). Spec §12's attack matrix runs here through production wiring, not only in Tasks 5 and 6's unit tests (astra P1-15): |
| - `TestAssetE2EFarMissThenHit`: an asset requested through the far edge is `miss` once and `hit` after; the home's backend saw exactly one request. |
| - `TestAssetE2EGzipFromTheFarEdge`: a 5 MiB CSS bundle is served gzip-compressed from the far edge with `Vary: Accept-Encoding` and a weak `ETag`. |
| - `TestAssetE2EPurgeMakesEveryEdgeMiss`: changing the generation in the published entry and converging makes both edges miss once. |
| - - `TestAssetE2EOldFarEdgeForgedMarkerIsBilledAsAHop`: a far edge with the cache unarmed forwards a client-sent `X-Carlos-Asset-Fill`; the home bills it as an ordinary hop (`TestHopServedRequestMetersOnceWithHopCounters`'s counters) and sends no ack. |
| + - `TestAssetE2EOldFarEdgeForgedMarkerIsBilledAsAHop`: an old far edge is simulated explicitly, because a new edge strips the client's marker whether or not it is armed: the test sends the home a hop it builds itself, with a valid `X-Carlos-Hop-Auth` from `signHop` under the scene's mesh key and a forged `X-Carlos-Asset-Fill` (a well-formed timestamp with a random MAC), which is exactly what an old edge forwarding a client header would send. The home's backend observes that the hop verified and the marker arrived; the home bills it as an ordinary hop (`TestHopServedRequestMetersOnceWithHopCounters`'s counters) and the response carries no acknowledgement (astra P2-5). |
| - `TestAssetE2EDisarmedHomeFillsNothing`: the home unarmed with a stale published entry still on the far edge: responses arrive `bypass`, metered once at the home, nothing stored. |
| - `TestAssetE2EFillCarriesNoCredentials`: a client `Cookie` and `Authorization` sent to the far edge never reach the home's backend. |
| - `TestAssetE2ERefusedFillsAreStoredNowhere`: `Set-Cookie`, a `Set-Cookie` trailer, `Vary: Origin`, a long-lived HTML fallback and a 404, each through the far edge: answered `bypass`, both stores empty, metered once. |
| @@ -257,12 +258,14 @@ This task adds no production code unless a test fails. It drives spec §12's aut |
| - `TestAssetE2EStalledBodyIsAbandoned`: the home's backend stalls after headers; the far fill is abandoned at the deadline with both reservations released. |
| - `TestAssetE2EUndeclaringBuildIsNotStored`: during a generation switch where only the installed build declares the prefix, the serving build's cacheable fallback is stored on neither edge. |
| - `TestAssetE2EWarmSleepRestart`: a generation-managed instance warms, sleeps and restarts its edge process; hits continue on both edges with no wake. |
| + - `TestAssetE2EOversizedResponseIsStoredNowhere` (a 33 MiB body, with and without `Content-Length`). |
| + - `TestAssetE2EGenerationOrOwnerMismatchGetsNoAck` (the far edge's published view names a generation, then an owner, that the home no longer holds: no acknowledgement, metered once at the home, stored nowhere) (astra P2-R1-15). |
| - [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestAssetE2E' -count=1` with the sandbox off. Expected: PASS. |
| - [ ] **Step 3:** Commit. |
| ### Task 10: Docs, census, merge and CI |
| -- [ ] **Step 1:** Write EAC-25 and EAC-26 into `docs/site/cli.md`; add them to `assetCacheCopyBuilt`; `go test . -run 'TestAssetCacheCopy' -count=1` and `go test ./internal/docsite/ -count=1` PASS. |
| +- [ ] **Step 1:** Write EAC-23 and EAC-24 into `docs/site/cli.md`; add them to `assetCacheCopyBuilt`; `go test . -run 'TestAssetCacheCopy' -count=1` and `go test ./internal/docsite/ -count=1` PASS. |
| - [ ] **Step 2:** `git fetch origin && git merge origin/main`; census test FAILs naming the units and envelopes above; write the overlay and notes file; post on #280; census PASS. |
| - [ ] **Step 3:** Provider boundary (spec §9): `TestAssetCodeImportsNoAWSSDK` in `internal/edge` runs `go list -deps ./internal/assetcache` and checks no `github.com/aws/` package appears, and parses the imports of this plan's `internal/edge/asset*.go` files with `go/parser` and checks the same. It does not check `internal/edge` as a whole, which imports the AWS SDK for its Route 53 adapter (`route53dns.go:25`) (astra P1-4). Task 9's e2e is the no-AWS-configuration run. |
| - [ ] **Step 4:** Cross-compiles and `gofmt -l .` clean; weights for any new test over a second; commit, push, CI green on the tip, merge per plan 1's Global Constraints. |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-PLANS-R2.md b/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-PLANS-R2.md| new file mode 100644 |
| index 000000000..d3af3d299 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-PLANS-R2.md |
| @@ -0,0 +1,32 @@ |
| +| Round-1 finding | Disposition | Why | |
| +|---|---|---| |
| +| 1. Shared types introduced too late | Fixed | `HostAssets`/`LocalAssets` move into Plan 1 Task 8; `FS` moves into Plan 2 Task 1. | |
| +| 2. `Declared` called on the wrong interface | Fixed | Plan 3 Task 5 now calls `e.assets.local.Declared` with the request hostname. | |
| +| 3. Invalid census dispositions | Fixed | The revised rows use valid classes, legacy formats and kept-family ownership. | |
| +| 4. Impossible AWS dependency assertion | Fixed | The check now targets `assetcache` and the new asset implementation files. | |
| +| 5. Unusable registry failure fixtures | Fixed | Selective SQLite triggers replace the wrapper/closed-handle proposals. | |
| +| 6. Missing cookie-trailer veto | Fixed | Both fill paths now require a trailer check before committing. | |
| +| 7. Confirming unapplied records | Partly fixed | The skipped-generation case is covered, but [the equality shortcut](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:228) checks only asset fields and can overlook failed gate convergence. | |
| +| 8. Unbounded metadata writes | Fixed | Plan 2 specifies a 64 KiB encoded-metadata limit, accounting and oversized-input tests. | |
| +| 9. Unsafe replacement sequence | Fixed | Versioned bodies and an atomic metadata replacement preserve the previous entry until publication. | |
| +| 10. Cold conditional/range handling | Fixed | Admitted cold fills now answer these requests through `serveAssetEntry`. | |
| +| 11. Acknowledgement key rotation | Fixed | Verification returns the matched key, which signs the acknowledgement. | |
| +| 12. Far cache catches upgrades | Fixed | The far predicate explicitly excludes upgrade requests. | |
| +| 13. Serving-generation publication trigger | Fixed | A serving-build digest now triggers publication independently of the registry fingerprint. | |
| +| 14. Missing fallback ETag | Fixed | Both fill paths specify the same SHA-256 fallback validator. | |
| +| 15. Missing production-wired attacks | Partly fixed | [Task 9](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:248) adds coverage but still leaves required cases, including oversized responses and generation/owner mismatches, outside its explicit acceptance matrix. | |
| +| 16. Incomplete copy inventory | Partly fixed | The missing refusals are added, but [Task 0](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:106) requires EAC-1–24 while the inventory never defines EAC-20 or EAC-21. | |
| + |
| +1. **P2 — Late fill failure can replay a response after delivery has started.** Plan 3 Task 5 tees ordinary initiating requests directly to the client, then instructs the implementation to forward the request again after a deadline, short body or trailer veto ([plan 3:160](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:160)). Task 6 inherits this rule. For a streamed response, headers and body bytes have already been sent: replaying can append a second body and cannot replace the status or headers. The spec instead requires abandonment of caching while the original delivery continues ([spec:409](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:409)). **Smallest fix:** distinguish buffered initiators from responses already started; never replay onto a started response. Test a streaming GET that receives a cookie trailer and another that passes the cache deadline. |
| + |
| +2. **P2 — Disk accounting has no reader-lifetime contract.** Plan 2 Tasks 4–5 explicitly allow readers to retain unlinked old bodies, expose them as raw `*os.File` handles, and evict entries to make budget room ([plan 2:128](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:128), [plan 2:147](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:147)). Unlinking an open file does not release its disk space. Slow readers can therefore retain evicted bodies while new fills consume the supposedly reclaimed budget; the filesystem floor does not enforce the separate cache budget. **Smallest fix:** specify reader leases and a release notification; retain accounting for retired bodies until their last reader closes, or prevent eviction while pinned. Test eviction and replacement while an old reader remains open. |
| + |
| +3. **P2 — The authority acceptance task needs an explicit package boundary.** Plan 3 Task 8 places the test in `internal/edge` and imports an exported constructor from `internal/host` ([plan 3:234](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:234)). The existing edge fixtures use `package edge`, while host already imports edge ([fixture:1](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/backhaul_e2e_test.go:1), [agent.go:184](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/host/agent.go:184)). Following that fixture structure creates an import cycle. Choosing `edge_test` avoids the cycle but loses access to the private home-map and fixture helpers; the constructor alone does not resolve that. **Smallest fix:** specify an external integration-test package and its exported harness, including publication, clock advancement and home-map refresh. |
| + |
| +4. **P2 — The proposed generation fixture never selects generation routing.** Plan 3 Task 9 adds `Account`, `App` and `AssetGen` to scene routes and passes an activator to `newBackhaulEdge` ([plan 3:246](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:246)). That helper creates TCP `service` routes ([backhaul_e2e_test.go:267](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/backhaul_e2e_test.go:267)). Generation routing requires a socket-backed `instance` with `Hibernate` enabled ([edge.go:740](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/edge.go:740)). Merely supplying `genActivator` cannot exercise the answering-build or sleep cases. **Smallest fix:** prescribe generation-managed instance routes and sockets for those cases, and assert that the fixture actually selects `genManaged` before testing cache behaviour. |
| + |
| +5. **P2 — The “old far edge” test removes the forged marker before the home sees it.** Plan 3 Task 9 models the old edge by disabling its asset cache ([plan 3:252](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:252)). However, Task 6 strips client-supplied fill headers from **every** non-hop request, independently of arming ([plan 3:188](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:188)). The test can pass without exercising rejection of a forged assertion inside a legitimate hop. **Smallest fix:** simulate legacy forwarding explicitly, preserving the forged marker while adding valid hop authentication; assert that the home receives both before checking billing and acknowledgement refusal. |
| + |
| +6. **P3 — Several corrective tests are excluded by their task’s test command.** Plan 2 Task 4 omits `TestOversizedMetaIsRefused` and `TestReplacementCrashKeepsTheOldEntry` from its selector ([plan 2:133](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:133)). Plan 3 Task 1 omits `TestAckUsesTheKeyTheAssertionMatched` ([plan 3:87](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:87)); Task 5 omits the new cold-request, fallback-ETag and trailer-veto tests ([plan 3:171](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:171), [plan 3:180](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:180)). **Smallest fix:** update the selectors so each task’s prescribed fail/pass cycle actually runs every test it introduces. |
| + |
| +reject |
| \ No newline at end of file |