edge asset cache plans: Astra plan review round 1 folded in
Summary
Accepts all 16 plan-review findings: type ordering, census dispositions, trigger-based write-failure fixtures, positive application in the reconcile report, bounded and crash-safe metadata, trailer veto, cold-miss conditionals, ack key selection, upgrade exclusion, serving-build publication trigger, fallback ETag, production-wired attack matrix, copy inventory to EAC-26.
Agent notes
Findings committed unedited as CODEX-FINDINGS-ASSET-CACHE-PLANS.md.
4 files changed,
+92
−37
docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md+19 −14docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md+12 −10docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md+28 −13docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-PLANS.md+33 −0
CI passed — run details
CI log
=== gate ===
.amadan/test-gate.sh
go: downloading github.com/aws/aws-sdk-go-v2 v1.44.0
go: downloading github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0
go: downloading github.com/aws/smithy-go v1.28.1
go: downloading github.com/aws/aws-sdk-go-v2/service/costexplorer v1.67.5
go: downloading github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.68.0
go: downloading github.com/superfly/ltx v0.5.1
go: downloading golang.org/x/net v0.56.0
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/config v1.32.6
go: downloading github.com/aws/aws-sdk-go-v2/service/sns v1.43.0
go: downloading github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1
go: downloading github.com/benbjohnson/litestream v0.5.14
go: downloading golang.org/x/crypto v0.53.0
go: downloading golang.org/x/sys v0.46.0
go: downloading github.com/oschwald/maxminddb-golang v1.13.1
go: downloading github.com/aws/aws-sdk-go-v2/service/autoscaling v1.74.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/aws/aws-sdk-go-v2/credentials v1.19.6
go: downloading github.com/aws/aws-sdk-go-v2/service/sts v1.45.6
go: downloading github.com/aws/aws-sdk-go-v2/service/acm v1.45.0
go: downloading github.com/aws/aws-sdk-go-v2/service/iam v1.58.2
go: downloading github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.37.0
go: downloading github.com/aws/aws-sdk-go-v2/service/sesv2 v1.66.6
go: downloading github.com/pion/logging v0.2.4
go: downloading github.com/pion/stun/v3 v3.0.1
go: downloading github.com/pion/turn/v4 v4.1.4
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40
go: downloading github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40
go: downloading github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40
go: downloading github.com/prometheus/client_golang v1.17.0
go: downloading golang.org/x/sync v0.21.0
go: downloading github.com/lmittmann/tint v1.1.3
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18
go: downloading github.com/pion/dtls/v3 v3.0.7
go: downloading github.com/pion/transport/v3 v3.0.8
go: downloading github.com/pion/randutil v0.1.0
go: downloading github.com/pion/transport/v4 v4.0.1
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31
go: downloading github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14
go: downloading github.com/wlynxg/anet v0.0.5
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
go: downloading golang.org/x/text v0.38.0
go: downloading github.com/klauspost/compress v1.18.0
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12
go: downloading github.com/aws/aws-sdk-go-v2/service/signin v1.0.4
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading golang.org/x/text v0.38.0
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading modernc.org/mathutil v1.7.1
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/google/uuid v1.6.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
=== launchctx (4 s)
ok amadan.net/carlos/platform.git/launchctx 0.012s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
=== vet (46 s)
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
go: downloading github.com/klauspost/compress v1.18.0
go: downloading golang.org/x/tools v0.47.0
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading golang.org/x/text v0.38.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading golang.org/x/mod v0.37.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/google/uuid v1.6.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
=== gofmt (1 s)
gofmt -l .: clean
=== rest (201 s)
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.236s
ok github.com/carlosframework/platform/cmd/moveprobe 11.790s
ok github.com/carlosframework/platform/cmd/zddprobe 0.741s
ok github.com/carlosframework/platform/internal/activator 59.703s
ok github.com/carlosframework/platform/internal/agentsock 0.030s
ok github.com/carlosframework/platform/internal/alerting 0.006s
ok github.com/carlosframework/platform/internal/analytics 0.035s
ok github.com/carlosframework/platform/internal/appconfig 2.919s
ok github.com/carlosframework/platform/internal/apperr 0.182s
ok github.com/carlosframework/platform/internal/applog 0.256s
ok github.com/carlosframework/platform/internal/awsinv 0.008s
ok github.com/carlosframework/platform/internal/boxchoice 0.008s
ok github.com/carlosframework/platform/internal/boxid 0.784s
? 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.016s
ok github.com/carlosframework/platform/internal/caps 0.013s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.728s
ok github.com/carlosframework/platform/internal/changelog 0.473s
ok github.com/carlosframework/platform/internal/claimactivation 1.144s
ok github.com/carlosframework/platform/internal/clamd 0.058s
? 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.554s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.023s
ok github.com/carlosframework/platform/internal/dbquery 0.007s
ok github.com/carlosframework/platform/internal/dbreads 2.122s
? 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.003s
ok github.com/carlosframework/platform/internal/docsite 0.012s
ok github.com/carlosframework/platform/internal/domains 2.645s
ok github.com/carlosframework/platform/internal/doorkey 0.010s
ok github.com/carlosframework/platform/internal/economics 0.006s
ok github.com/carlosframework/platform/internal/edge 48.692s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.008s
ok github.com/carlosframework/platform/internal/fleetapi 0.136s
ok github.com/carlosframework/platform/internal/gate 1.655s
ok github.com/carlosframework/platform/internal/genselect 0.018s
ok github.com/carlosframework/platform/internal/geo 0.024s
ok github.com/carlosframework/platform/internal/geoip 0.030s
ok github.com/carlosframework/platform/internal/historyreadback 0.159s
ok github.com/carlosframework/platform/internal/host 150.303s
ok github.com/carlosframework/platform/internal/hostactors 5.955s
ok github.com/carlosframework/platform/internal/hostactors/awsmembership 0.011s
ok github.com/carlosframework/platform/internal/hostbox 3.594s
ok github.com/carlosframework/platform/internal/hostclaim 0.743s
ok github.com/carlosframework/platform/internal/hostenv 0.006s
ok github.com/carlosframework/platform/internal/hostevidence 0.009s
ok github.com/carlosframework/platform/internal/hostexec 5.605s
ok github.com/carlosframework/platform/internal/hostfence 0.069s
ok github.com/carlosframework/platform/internal/hostfiles 10.514s
ok github.com/carlosframework/platform/internal/hostfloor 0.025s
ok github.com/carlosframework/platform/internal/hostguard 1.033s
ok github.com/carlosframework/platform/internal/hostops 2.082s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.004s
ok github.com/carlosframework/platform/internal/hostquarantine 0.007s
ok github.com/carlosframework/platform/internal/hostrelocate 0.607s
ok github.com/carlosframework/platform/internal/hostresolve 0.182s
ok github.com/carlosframework/platform/internal/hostwrite 0.004s
ok github.com/carlosframework/platform/internal/infraview 0.003s
ok github.com/carlosframework/platform/internal/instancectl 5.256s
ok github.com/carlosframework/platform/internal/instanceexec 2.864s
ok github.com/carlosframework/platform/internal/instancefs 0.009s
ok github.com/carlosframework/platform/internal/instancekey 0.009s
ok github.com/carlosframework/platform/internal/instanceupdates 0.016s
ok github.com/carlosframework/platform/internal/layout 1.811s
ok github.com/carlosframework/platform/internal/lbhealth 0.004s
ok github.com/carlosframework/platform/internal/lease 10.149s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.534s
ok github.com/carlosframework/platform/internal/ledger 0.024s
ok github.com/carlosframework/platform/internal/mail 0.264s
ok github.com/carlosframework/platform/internal/maintenance 0.080s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.539s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.098s
ok github.com/carlosframework/platform/internal/metrics 0.008s
ok github.com/carlosframework/platform/internal/moves 29.126s
ok github.com/carlosframework/platform/internal/moves/archive 0.951s
ok github.com/carlosframework/platform/internal/objstore 0.238s
ok github.com/carlosframework/platform/internal/ops 0.234s
ok github.com/carlosframework/platform/internal/park 0.297s
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 52.440s
ok github.com/carlosframework/platform/internal/publicrelease 0.009s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 5.765s
ok github.com/carlosframework/platform/internal/relay 7.208s
ok github.com/carlosframework/platform/internal/release 29.527s
ok github.com/carlosframework/platform/internal/rollout 4.130s
ok github.com/carlosframework/platform/internal/rolloutwords 0.005s
ok github.com/carlosframework/platform/internal/rootaws 0.014s
ok github.com/carlosframework/platform/internal/rootparticipation 0.059s
ok github.com/carlosframework/platform/internal/rootstate 0.026s
ok github.com/carlosframework/platform/internal/runs 0.613s
ok github.com/carlosframework/platform/internal/s3compat 0.008s
ok github.com/carlosframework/platform/internal/safefile 0.005s
ok github.com/carlosframework/platform/internal/schedule 2.825s
ok github.com/carlosframework/platform/internal/seeds 0.010s
ok github.com/carlosframework/platform/internal/sim/engines 2.990s
ok github.com/carlosframework/platform/internal/sim/hostlife 10.553s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.202s
ok github.com/carlosframework/platform/internal/status 0.083s
ok github.com/carlosframework/platform/internal/steering 0.010s
ok github.com/carlosframework/platform/internal/stopevidence 0.937s
ok github.com/carlosframework/platform/internal/svcrole 0.003s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
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.005s
ok github.com/carlosframework/platform/internal/version 0.004s
ok github.com/carlosframework/platform/internal/vet 1.674s
=== internal_console@0 (85 s)
ok github.com/carlosframework/platform/internal/console 82.772s
=== internal_console@1 (50 s)
ok github.com/carlosframework/platform/internal/console 48.416s
=== internal_console@2 (43 s)
ok github.com/carlosframework/platform/internal/console 41.651s
=== internal_console@3 (47 s)
ok github.com/carlosframework/platform/internal/console 45.587s
=== internal_console@4 (47 s)
ok github.com/carlosframework/platform/internal/console 45.690s
=== internal_console@5 (48 s)
ok github.com/carlosframework/platform/internal/console 46.198s
=== internal_console@6 (44 s)
ok github.com/carlosframework/platform/internal/console 42.012s
=== internal_console@7 (50 s)
ok github.com/carlosframework/platform/internal/console 48.627s
=== root@0 (112 s)
ok github.com/carlosframework/platform 110.165s
=== root@1 (83 s)
ok github.com/carlosframework/platform 81.625s
=== root@2 (95 s)
ok github.com/carlosframework/platform 93.094s
=== 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.026s
=== RUN TestT4StorageProbeMinio
2026/10/24 18:51:12 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E18BD4DAAF9A1C, 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.033s
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:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 18:51:15 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000002
2026/10/24 18:51:15 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000002 maxTXID=0000000000000002 size=370
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000002
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000003
2026/10/24 18:51:15 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000003 maxTXID=0000000000000003 size=454
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000003 txid.db=0000000000000003
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 18:51:15 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 18:51:15 console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically
2026/10/24 18:51:15 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%3A46127: could not determine target mailbox
agent.go:135: dbreads: dbr_avvqr4ojfvh4mnf2cim7grzrhi: planned 1 files (720 bytes) and restored to 1 in 9ms
agent.go:135: dbreads: dbr_avvqr4ojfvh4mnf2cim7grzrhi: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:51:16 console: db read dbr_avvqr4ojfvh4mnf2cim7grzrhi: relayed 1 chunks (102 bytes) in 1.005s, ended done
agent.go:135: dbreads: dbr_ywpdwjmcdctofg47phibqe3cl4: planned 3 files (1544 bytes) and restored to 3 in 10ms
agent.go:135: dbreads: dbr_ywpdwjmcdctofg47phibqe3cl4: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:51:17 console: db read dbr_ywpdwjmcdctofg47phibqe3cl4: relayed 1 chunks (102 bytes) in 1.004s, ended done
agent.go:135: dbreads: dbr_eiyd6f6sj3c6qcsnrpjp467fty: planned 3 files (1544 bytes) and held through 3 in 10ms
agent.go:135: dbreads: dbr_eiyd6f6sj3c6qcsnrpjp467fty: output 1544 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:51:18 console: db read dbr_eiyd6f6sj3c6qcsnrpjp467fty: relayed 1 chunks (1641 bytes) in 1.004s, ended done
=== RUN TestDBForwardMinio/by_disconnect
=== NAME TestDBForwardMinio
agent.go:135: dbreads: dbr_lqr4klhdi7pcrh6qi4j4cfdngi: planned 3 files (1544 bytes) and restored to 3 in 10ms
2026/10/24 18:51:18 console: db read dbr_lqr4klhdi7pcrh6qi4j4cfdngi: relayed 0 chunks (0 bytes) in 16ms, ended client gone
agent.go:135: dbreads: dbr_lqr4klhdi7pcrh6qi4j4cfdngi: output 5 bytes in 1 confirmed chunks in 50ms
=== RUN TestDBForwardMinio/by_timeout
=== NAME TestDBForwardMinio
agent.go:135: dbreads: dbr_tknzaziaqlpnfiwbznhx4o7tse: planned 3 files (1544 bytes) and restored to 3 in 10ms
2026/10/24 18:51:38 console: db read dbr_tknzaziaqlpnfiwbznhx4o7tse: relayed 0 chunks (0 bytes) in 19.206s, ended no-result
agent.go:135: dbreads: dbr_tknzaziaqlpnfiwbznhx4o7tse: output 5 bytes in 1 confirmed chunks in 19.193s
--- PASS: TestDBForwardMinio (22.59s)
--- PASS: TestDBForwardMinio/by_disconnect (0.17s)
--- PASS: TestDBForwardMinio/by_timeout (19.32s)
PASS
ok github.com/carlosframework/platform/internal/console 22.615s
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 5ecb9c342..5efd47a00 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 |
| @@ -42,14 +42,14 @@ Every task's requirements include this section. |
| ## 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). |
| + - Changed frozen units keep their rows' form and are re-digested with class, task and basis unchanged: `internal/host/edgesfile.go` `edgesFileWriteClaims` stays a plain row of class `authority`, task `F,J1`, basis `root-route;kept=F;scope=protected-decision-only` (the `plan14-root-reads-region-ha-2a.tsv:26` precedent; the loader requires an authority row's task to name a kept family, `plan14_root_reads_test.go:507-515`); `internal/domains/aliases.go` `ReconcileAliases` and `deriveInstanceAliases` keep their `@legacy` rows (class `legacy`, re-fingerprinted by `rootReadLegacyFingerprint`). |
| + - New unit `internal/host/assetmanifests.go` `assetManifestCache.paths` (reads `release.GetManifest` for the same protected publication decision): a plain row of class `authority`, task `F,J1`, basis `root-route;kept=F;scope=protected-decision-only`, the writer's own disposition. |
| - 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): |
| +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): |
| | id | where | what it says | |
| |---|---|---| |
| @@ -63,6 +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 | |
| ## File structure |
| @@ -100,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-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 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 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 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 5:** Commit. |
| ### Task 1: The manifest's `assets` block and its grammar |
| @@ -144,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 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-23 and EAC-24 to `assetCacheCopyBuilt`. |
| - [ ] **Step 4:** Run both commands. Expected: PASS. |
| - [ ] **Step 5:** Commit. |
| @@ -158,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 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 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 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 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 4:** Run the same command. Expected: PASS. |
| - [ ] **Step 5:** Commit. |
| @@ -222,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 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. |
| +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. |
| -- [ ] **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 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 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. |
| @@ -239,7 +242,7 @@ An app is complete when: the listing was not `partial`, no record body under the |
| **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 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 SQLite triggers on the test registry's own database file: the test opens the file a second time through `database/sql` (the driver `internal/registry` uses) and creates `BEFORE INSERT`, `BEFORE UPDATE` or `BEFORE DELETE` triggers on `route_aliases`, `wildcard_domains` or `route_paths` that `SELECT RAISE(FAIL, 'injected')` for one named row, so reads succeed and only the named write fails (astra P1-5). A closed handle is not usable: `ListAliases` would fail first (`aliases.go:245`) and the pass would return an error. |
| - [ ] **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. |
| @@ -257,6 +260,8 @@ An app is complete when: the listing was not `partial`, no record body under the |
| - `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 |
| + - `func servingBuildsDigest(routes []registry.Route, sb servingBuild) string` (hash of every local route's serving build): `edgesFileLoop` computes it every 2 s tick and runs a pass when it changes, beside the `Fingerprint` check (`edgesfile.go:888`), so a generation switch republishes within seconds with the registry unchanged (astra P1-13) |
| + - `internal/edge/assettypes.go` (create, types only, so this task builds): `type HostAssets struct { Account, App string; Paths []string; Gen string; At time.Time }` and `type LocalAssets interface { For(host string) (HostAssets, bool); Declared(host, build, path string) bool }` (astra P1-1) |
| - `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"`) |
| - `type assetPolicy struct` (holds the registry, the authority, the manifest cache and `servingBuild`), with `func (p *assetPolicy) Publish(host string) (edgeAssets, bool)` (the publication rule below) and two methods plan 3's edge consumes through `edge.LocalAssets`: `func (p *assetPolicy) For(host string) (edge.HostAssets, bool)` (the same rule **without** the pooled exclusion, spec §3.3: a pooled route still caches on the boxes that serve it) and `func (p *assetPolicy) Declared(host, build, path string) bool` (true when `build`'s cached paths contain a prefix of `path`; false when the build's manifest is not cached) |
| @@ -273,6 +278,7 @@ Publication rule for a host, applied in `edgesFileWriteClaims` through `assetPol |
| - `TestAliasPublishesItsPrimarysBlock`; `TestCatchallAliasNeverPublishes`. |
| - `TestConsoleClassRouteDoesNotPublish` (Review Focus 4). |
| - `TestRewritesWhenAtAdvancesTwentyMinutes` and `TestDoesNotRewriteForASmallerAdvance` (`putCountingStore` counts PUTs). |
| + - `TestServingBuildSwitchRepublishesWithAnUnchangedFingerprint`. |
| - `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`. |
| @@ -286,10 +292,9 @@ Publication rule for a host, applied in `edgesFileWriteClaims` through `assetPol |
| - Test: `internal/edge/homemap_assets_test.go` (create), `internal/edgesfile/edgesfile_test.go` |
| **Interfaces:** |
| +- Consumes: `HostAssets`, `LocalAssets` (declared in Task 8's `internal/edge/assettypes.go`). |
| - 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)`: false until the home map's first successful hosts refresh after start (spec §7, "after a restart"); exact hosts only (no one-label wildcard match); owner present; `now.Sub(At) <= time.Hour` |
| - - `type LocalAssets interface { For(host string) (HostAssets, bool); Declared(host, build, path string) bool }` (implemented by `internal/host`'s `assetPolicy`, consumed by plan 3) |
| - a golden file `internal/edgesfile/testdata/assets-host.golden.json`: Task 8's writer test asserts its output entry equals it, and this task's parse test reads it, so writer and reader pin one shape |
| - `edgesfile.Host.Assets *edgesfile.HostAssets` with the same wire shape as `edgeAssets` |
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 a353ff9fc..22fa137ea 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 |
| @@ -4,7 +4,7 @@ |
| **Goal:** A self-contained package, `internal/assetcache`, that stores cached asset responses on a box's disk under a budget, with worst-case reservations, a free-space floor, per-box and per-app fill bounds, fair-share eviction, expiry and a sweep, and that nothing calls yet (plan 3 does). |
| -**Architecture:** One `Store` per process over one directory. Each entry is three files named by the hex sha256 of its key: `<id>.body` (identity), optional `<id>.gz`, and `<id>.meta` (JSON, written last, which is what makes an entry exist). An in-memory index is rebuilt from the `.meta` files at `Open`. A fill is a `Fill` holding a reservation; it writes temporaries in the same directory and renames them on `Commit`. Free space comes from an `FS` interface whose Linux implementation is `statfs`. The package knows nothing about HTTP semantics beyond storing headers it is given: admission, freshness rules and serving belong to plan 3. The design is spec §7, written so the media roadmap's stage 1 can reuse it with its own directory and budget (spec §11). |
| +**Architecture:** One `Store` per process over one directory. Each entry is a `<id>.meta` file (JSON) naming its versioned body files, `<id>.<v>.body` (identity) and optional `<id>.<v>.gz`, where `<id>` is the hex sha256 of the key and `<v>` a random version drawn per commit. A commit writes the new versioned bodies, then renames a temporary over `<id>.meta`, then deletes the previous version's bodies, so a crash at any point leaves either the old entry or the new one, never new bytes under old metadata (astra P1-9). An in-memory index is rebuilt from the `.meta` files at `Open`. A fill is a `Fill` holding a reservation; it writes temporaries in the same directory and renames them on `Commit`. Free space comes from an `FS` interface whose Linux implementation is `statfs`. The package knows nothing about HTTP semantics beyond storing headers it is given: admission, freshness rules and serving belong to plan 3. The design is spec §7, written so the media roadmap's stage 1 can reuse it with its own directory and budget (spec §11). |
| **Tech Stack:** Go 1.26 standard library (`os`, `compress/gzip`, `crypto/sha256`, `encoding/json`), `golang.org/x/sys/unix` for `Statfs` on Linux. |
| @@ -36,7 +36,7 @@ Every task's requirements include this section. |
| ## Census impact |
| -- New package `internal/assetcache`: its file-reading declarations (`Open`'s scan, `Store.Get`'s open of `.body`/`.gz`) are new `@legacy` rows of class `cache`, task `accepted-legacy`, with their envelope pinned. If the census has no `cache` class, use the class the static site cache's units carry (`internal/edge/static.go` `*`) and say so in the notes file. |
| +- New package `internal/assetcache`: its file-reading declarations (`Open`'s scan, `Entry.Open`) are new plain rows of class `legacy`, task `accepted-legacy`, basis `accepted-legacy;rev3=PROPOSED-edge-asset-cache-2;asset cache files under $CARLOS_DATA_DIR/asset-cache, written and read only by this package`, the form the loader requires (`plan14_root_reads_test.go:478-485`); there is no `cache` class (astra P1-3). Its envelope is pinned. |
| - Notes: `docs/superpowers/notes/census-proposal-edge-asset-cache-2.md`, ruling asked on #280. |
| ## File structure |
| @@ -56,17 +56,18 @@ Every task's requirements include this section. |
| ### Task 1: Keys, metadata and `Open` |
| **Files:** |
| -- Create: `internal/assetcache/key.go`, `meta.go`, `store.go`, `store_test.go` |
| +- Create: `internal/assetcache/key.go`, `meta.go`, `store.go`, `fs.go` (the interface only), `store_test.go` |
| **Interfaces:** |
| - Produces: |
| - `type Key struct { Account, App, Gen, Host, Path, RawQuery string }`; `func (k Key) ID() string` (hex sha256 over the six fields, each length-prefixed so no two keys collide); `func (k Key) AppScope() string` (`Account + "/" + App`) |
| - - `type Meta struct { Key Key; Header http.Header; Lifetime time.Duration; Expires time.Time; StoredAt time.Time; Size int64; GzipSize int64 }` (`GzipSize == 0` means no gzip copy) |
| + - `type Meta struct { Key Key; Version string; Header http.Header; Lifetime time.Duration; Expires time.Time; StoredAt time.Time; Size int64; GzipSize int64 }` (`GzipSize == 0` means no gzip copy; `Version` names the body files) |
| + - `type FS interface { Space(dir string) (total, free uint64, err error) }` in `fs.go` (declared here so this task builds; Task 2 adds the implementations) (astra P1-1) |
| - `type Config struct { Dir string; Budget int64; FS FS; Now func() time.Time }` |
| - - `func Open(cfg Config) (*Store, error)`: creates `Dir` (0700), deletes every `*.tmp-*` file, deletes `.body`/`.gz` with no `.meta`, deletes a `.meta` that does not parse together with its files, and indexes the rest. |
| + - `func Open(cfg Config) (*Store, error)`: creates `Dir` (0700), deletes every `*.tmp-*` file, deletes every `.body`/`.gz` file no parsed `.meta` names (orphans and superseded versions), deletes a `.meta` that does not parse together with its files, and indexes the rest. |
| - `type Stats struct { Entries int; Bytes int64; Reserved int64; Apps int }`; `func (s *Store) Stats() Stats` |
| -- [ ] **Step 1: Write the failing tests.** `TestKeyIDIsStableAndDistinct` (same key same id; moving one character between `Path` and `RawQuery` changes it); `TestOpenCreatesTheDirectory`; `TestOpenRemovesOrphanBodiesAndTemporaries`; `TestOpenKeepsMetaWithUnknownFields`; `TestOpenDropsUnparseableMeta`; `TestOpenIndexesExistingEntries` (write three entries' files by hand in the documented layout, `Open`, `Stats` counts them and their bytes). |
| +- [ ] **Step 1: Write the failing tests.** `TestKeyIDIsStableAndDistinct` (same key same id; moving one character between `Path` and `RawQuery` changes it); `TestOpenCreatesTheDirectory`; `TestOpenRemovesOrphanBodiesAndTemporaries`; `TestOpenKeepsMetaWithUnknownFields`; `TestOpenDropsUnparseableMeta`; `TestOpenIndexesExistingEntries` (write three entries' files by hand in the documented layout, `Open`, `Stats` counts them and their bytes); `TestOpenDeletesSupersededVersions` (a `.meta` naming version `b` beside bodies for `a` and `b`: `a` is removed). |
| - [ ] **Step 2:** Run `go test ./internal/assetcache/ -count=1`. Expected: FAIL (package missing). |
| - [ ] **Step 3:** Implement. |
| - [ ] **Step 4:** Run the same command. Expected: PASS. |
| @@ -75,11 +76,11 @@ Every task's requirements include this section. |
| ### Task 2: Free space |
| **Files:** |
| -- Create: `internal/assetcache/fs.go`, `statfs_linux.go`, `statfs_other.go`, `fs_test.go`, `fs_linux_test.go` |
| +- Create: `internal/assetcache/statfs_linux.go`, `statfs_other.go`, `fs_test.go`, `fs_linux_test.go` |
| **Interfaces:** |
| +- Consumes: `FS` (Task 1). |
| - Produces: |
| - - `type FS interface { Space(dir string) (total, free uint64, err error) }` |
| - `var Statfs FS` (Linux: `unix.Statfs`, `free` from `Bavail * Bsize`; other systems: returns `ErrUnsupported`) |
| - `var ErrUnsupported = errors.New("assetcache: free space unknown on this system")` |
| - `func DefaultBudget(total uint64) int64` (the smaller of 4 GiB and `total/10`) |
| @@ -123,12 +124,13 @@ Every task's requirements include this section. |
| **Interfaces:** |
| - Produces: |
| - - `func (f *Fill) Commit(m Meta, gzipCopy bool) error`: 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; fsyncs and renames `.body`, then `.gz`, then writes `.meta` last; sets `Size`, `GzipSize`, `StoredAt = Now()`; releases the reservation; on any error behaves as `Abort`. A `Commit` for a key that already has an entry replaces it. |
| + - `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)` |
| - `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); `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 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 3:** Implement. |
| - [ ] **Step 4:** Run the same command. Expected: PASS. |
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 6dd505e4a..9896fa90d 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 |
| @@ -34,7 +34,7 @@ Every task's requirements include this section. |
| ## Census impact |
| - Changed frozen units, re-fingerprinted `@legacy` with the same class: `internal/edge/edge.go` `Edge.ServeHTTP`, `Edge.backhaulTo` (if Task 6 shares its preamble), `RunWithActivatorReady` (arming), `internal/edge/usagemeter.go` `UsageMeter.Record` only if its body changes (it should not). |
| -- New units: `internal/edge/assetcatch.go` (file reads through `assetcache`), class `cache`, task `accepted-legacy`. |
| +- New units, if the census flags any (reads go through `assetcache`, so likely none): plain rows of class `legacy`, task `accepted-legacy`, basis `accepted-legacy;rev3=PROPOSED-edge-asset-cache-3;...` (astra P1-3). |
| - Envelopes re-pinned: `internal/edge`, `internal/host`. |
| - Notes: `docs/superpowers/notes/census-proposal-edge-asset-cache-3.md`, ruling asked on #280. |
| @@ -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-20 | `docs/site/cli.md`, under `carlos ship --assets` | files under an asset path are fetched without the visitor's cookies (spec §5.2) | |
| -| EAC-21 | 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-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 | |
| ## 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-20 and EAC-21 to the copy record as `pending` and extend the gate's inventory to EAC-1 to EAC-21. |
| +- [ ] **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 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. |
| @@ -80,11 +80,11 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0 |
| - `const assetFillHeader = "X-Carlos-Asset-Fill"`, `assetAckHeader = "X-Carlos-Asset-Fill-Ack"`, `assetExpiresHeader = "X-Carlos-Asset-Expires"`, `assetCacheHeader = "Carlos-Cache"` |
| - `type assetFillClaims struct { Host, PathQuery, Account, App, Gen string }` |
| - `func signAssetFill(key []byte, c assetFillClaims, now time.Time) string` (`"<unix>:<hex>"`, HMAC-SHA256 over `carlos-asset-fill-v1`, then the five fields and the timestamp, newline-separated, host canonicalized) |
| - - `func verifyAssetFill(current, next []byte, c assetFillClaims, header string, now time.Time) bool` (either key, `±hopSkew`, constant-time) |
| + - `func verifyAssetFill(current, next []byte, c assetFillClaims, header string, now time.Time) (matched []byte, ok bool)` (either key, `±hopSkew`, constant-time; returns the key that verified, and the home signs the acknowledgement with **that** key, so a home that already holds `.next` answers a far edge still signing with the current key in the far edge's key (astra P1-11)) |
| - `func signAssetAck(key []byte, assertion string, expires int64) string` (hex HMAC over `carlos-asset-fill-ack-v1`, the assertion and the decimal expiry) |
| - `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). |
| +- [ ] **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 3:** Implement. |
| - [ ] **Step 4:** Run the same command. Expected: PASS. |
| @@ -101,10 +101,11 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0 |
| - `type assetVerdict struct { Store bool; Reason string; Expires time.Time; Lifetime time.Duration; Gzip bool }` |
| - `func assetAdmit(status int, h http.Header, requestTime, responseTime time.Time, declared bool) assetVerdict`: the rules of spec §5.3 that can be decided from headers, in order: status 200; no `Set-Cookie`; `Cache-Control` present, not `private`, `no-store` or `no-cache`, positive `s-maxage` else `max-age`, capped at 365 days; `Vary` absent or exactly `Accept-Encoding` (case-insensitive, whitespace-tolerant); `Content-Encoding` absent or `identity`; `Content-Type` present and not `text/html` or `application/xhtml+xml`; `Access-Control-Allow-Origin` absent or `*`; `declared`; `Content-Length`, when present, at most 32 MiB; still fresh, with `Expires = responseTime + lifetime - correctedInitialAge`. `Gzip` is `compressible(h, 200)`. |
| - `func correctedInitialAge(h http.Header, requestTime, responseTime time.Time) time.Duration` (RFC 9111 §4.2.3: `max(apparent_age, Age + response_delay)`, `apparent_age = max(0, responseTime - Date)`) |
| + - `func assetTrailerVeto(trailer http.Header) bool` (true when the trailers carry `Set-Cookie`, announced in `Trailer` or sent unannounced with `http.TrailerPrefix`); every fill, home and far, checks it after the last body byte and before `Commit`, and the far edge reads the backhauled response's own trailers, which the transport passes through (astra P1-6) |
| - `func assetStoredHeader(h http.Header) http.Header`: a clone without hop-by-hop headers (`Connection` and the names it lists, `Keep-Alive`, `Transfer-Encoding`, `Upgrade`, `TE`, `Trailer`, `Proxy-*`), `Date`, `Age`, `Content-Length`, `Carlos-Purge`, `Carlos-Purge-Host`, `Carlos-Cache`, and every `X-Carlos-*` |
| -- [ ] **Step 1: Write the failing tests.** `TestAssetAdmitTable`, one row per rule above, each failing alone with its `Reason`; rows that store: `public, max-age=31536000, immutable` CSS; `s-maxage` beating `max-age`; `max-age` over a year capped at 365 days. `TestAssetAdmitRefusesALongLivedHTMLFallback` (`200 text/html`, `public, max-age=31536000`). `TestAssetAdmitRefusesNoContentType`. `TestAssetAdmitRefusesAnUndeclaringBuild`. `TestExpiryIsFromResponseTime` (request 0, response 20, `Age: 0`, lifetime 30: expires at 30). `TestAnAlreadyExpiredResponseIsNotStored` (`Age` larger than lifetime). `TestStoredHeaderKeepsCSPAndCORP` and `TestStoredHeaderDropsHopAndPlatformHeaders`. |
| -- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestAssetAdmit|TestExpiryIsFromResponseTime|TestAnAlreadyExpired|TestStoredHeader' -count=1`. Expected: FAIL. |
| +- [ ] **Step 1: Write the failing tests.** `TestAssetAdmitTable`, one row per rule above, each failing alone with its `Reason`; rows that store: `public, max-age=31536000, immutable` CSS; `s-maxage` beating `max-age`; `max-age` over a year capped at 365 days. `TestAssetAdmitRefusesALongLivedHTMLFallback` (`200 text/html`, `public, max-age=31536000`). `TestAssetAdmitRefusesNoContentType`. `TestAssetAdmitRefusesAnUndeclaringBuild`. `TestExpiryIsFromResponseTime` (request 0, response 20, `Age: 0`, lifetime 30: expires at 30). `TestAnAlreadyExpiredResponseIsNotStored` (`Age` larger than lifetime). `TestStoredHeaderKeepsCSPAndCORP` and `TestStoredHeaderDropsHopAndPlatformHeaders`; `TestTrailerVetoAnnouncedAndUnannounced`. |
| +- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestAssetAdmit|TestExpiryIsFromResponseTime|TestAnAlreadyExpired|TestStoredHeader|TestTrailerVeto' -count=1`. Expected: FAIL. |
| - [ ] **Step 3:** Implement. |
| - [ ] **Step 4:** Run the same command. Expected: PASS. |
| - [ ] **Step 5:** Commit. |
| @@ -154,7 +155,9 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0 |
| - Produces: |
| - `func (e *Edge) serveAssetLocal(w http.ResponseWriter, r *http.Request, host string, up upstream) bool` (true when it answered) |
| - `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 `a`'s `Declared`, calls `assetAdmit`, calls `onHeader` before the first body byte, tees the body to the client and to a `Fill` while the verdict says store, abandons the fill (keeping the client stream) at the 60 s deadline or on a short body, and commits on success. |
| + - `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`. |
| 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. |
| @@ -165,6 +168,9 @@ On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Car |
| - `TestLocalUndeclaringBuildIsNotStored` (`Declared` false for the answering `X-Carlos-Version`). |
| - `TestHibernatingRouteHitDoesNotWake` (the `genActivator` fixture from `genedge_test.go:16`; a hit leaves its wake count at zero). |
| - `TestHeadFillsWithGetAndAnswersHead`, `TestQueryIsPartOfTheKey`, `TestFillSurvivesTheInitiatorDisconnecting`, `TestShortBodyIsNotStored`. |
| + - `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). |
| - `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). |
| @@ -184,7 +190,7 @@ On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Car |
| **Interfaces:** |
| - Consumes: Tasks 1 to 5, `homeMap.AssetsFor`, `homeMap.Home`, `signingKey`, `backhaulAcquire`, `backhaulTransport`, `signHop`, `UsageMeter.Record`. |
| -- Produces: `func (e *Edge) serveAssetFar(w http.ResponseWriter, r *http.Request, host string) bool`: when `AssetsFor(host, now)` is true, the path is canonical and matches, and the method is `GET` or `HEAD`: answer a hit from the store (and meter it with `Record(registry.Route{Host: host, Account: a.Account, App: a.App}, 1, n, false)`); on a miss lead or join a flight; the leader sends the allow-listed request to the home through `backhaulAcquire` and `backhaulTransport`, with `X-Carlos-Hop`, `X-Carlos-Hop-Auth` and `X-Carlos-Asset-Fill`; with a valid ack it meters the response once and stores it when `X-Carlos-Asset-Expires` is non-zero and the plan 2 store admits it, using that expiry unchanged; without a valid ack it streams the response unmodified with `Carlos-Cache: bypass`, does not meter, does not store. `X-Carlos-Asset-Fill-Ack` and `X-Carlos-Asset-Expires` never reach the client. Returns false (so `backhaul` runs as today) when the host does not catch. |
| +- Produces: `func (e *Edge) serveAssetFar(w http.ResponseWriter, r *http.Request, host string) bool`: when `AssetsFor(host, now)` is true, the path is canonical and matches, the method is `GET` or `HEAD`, and `isUpgradeRequest(r)` is false (this hook runs before `ServeHTTP`'s upgrade branch, so it must exclude upgrades itself, astra P1-12): answer a hit from the store (and meter it with `Record(registry.Route{Host: host, Account: a.Account, App: a.App}, 1, n, false)`); on a miss lead or join a flight; the leader sends the allow-listed request to the home through `backhaulAcquire` and `backhaulTransport`, with `X-Carlos-Hop`, `X-Carlos-Hop-Auth` and `X-Carlos-Asset-Fill`; with a valid ack it meters the response once and stores it when `X-Carlos-Asset-Expires` is non-zero and the plan 2 store admits it, using that expiry unchanged, after `assetTrailerVeto` on the backhauled trailers and with the same fallback `ETag` rule and initiating-request rule as Task 5; without a valid ack it streams the response unmodified with `Carlos-Cache: bypass`, does not meter, does not store. `X-Carlos-Asset-Fill-Ack` and `X-Carlos-Asset-Expires` never reach the client. Returns false (so `backhaul` runs as today) when the host does not catch. |
| - [ ] **Step 1: Write the failing tests** (a far `Edge` and the `fakeHomeEdge` from `homemap_test.go:709` extended to answer with or without an ack; "not acceptance"). |
| - `TestFarMissFillsThenHits` and `TestFarHitDoesNotDialTheHome` (`e2eDialCounter`). |
| @@ -194,6 +200,9 @@ On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Car |
| - `TestFarStaleAtForwards` (`at` 61 minutes old: plain backhaul, nothing stored). |
| - `TestFarFillCarriesNoCookie` (asserted at the fake home). |
| - `TestClientSentFillHeaderIsStripped`. |
| + - `TestFarUpgradeUnderAPrefixIsBackhauledIntact` (a WebSocket `GET` under `/assets/`: not caught, upgrade headers reach the fake home). |
| + - `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 3:** Implement. |
| @@ -236,20 +245,26 @@ 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: |
| +- [ ] **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): |
| - `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. |
| - `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. |
| + - `TestAssetE2ECoalescedWaitersAndRefusals`: concurrent far requests for one key share an admitted entry; with a refused response, each waiter reaches the home on its own. |
| + - `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. |
| - [ ] **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-20 and EAC-21 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-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 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): `TestAssetPackagesImportNoAWSSDK` in `internal/edge` runs `go list -deps` over `internal/assetcache` and checks that no `github.com/aws/` package appears (`internal/edge` itself already imports none; assert it too). Task 9's e2e is the no-AWS-configuration run. |
| +- [ ] **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. |
| ### Task 11: Acceptance on dev (needs Paul's go-ahead to roll) |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-PLANS.md b/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-PLANS.md| new file mode 100644 |
| index 000000000..421e67935 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-PLANS.md |
| @@ -0,0 +1,33 @@ |
| +1. **P1 — Shared types are introduced after tasks that require them.** Plan 1 Task 8 defines `assetPolicy.For` returning `edge.HostAssets`, but Task 9 first introduces that type and `LocalAssets` ([plan 1:262](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:262), [plan 1:290](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:290)). Likewise, Plan 2 Task 1 defines `Config.FS FS`, while Task 2 introduces `FS` ([plan 2:65](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:65), [plan 2:82](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:82)). Neither earlier task can reach its required passing build. **Smallest fix:** move the shared type declarations before their first consumers; leave their implementations in the later tasks. |
| + |
| +2. **P1 — Plan 3 Task 5 calls a method on the wrong interface.** `fillFromApp` takes `a HostAssets` and asks “`a`’s `Declared`” ([plan 3:157](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:157)). `HostAssets` is a data struct; `Declared` belongs to `LocalAssets` ([plan 1:290](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:290)). **Smallest fix:** specify `e.assets.local.Declared(host, answeringBuild, path)`, using the request hostname. |
| + |
| +3. **P1 — The census instructions prescribe invalid dispositions.** Plan 1 Task 11’s inventory calls for an `@legacy` row retaining class `authority`, and a new authority row owned solely by `J1` ([plan 1:45](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:45)). Plan 3 Task 10 prescribes class `cache` ([plan 3:37](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:37)). The loader requires `@legacy` rows to have class `legacy`, has no `cache` class, and does not accept `J1` alone as an authority owner ([census:420](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/provision/plan14_root_reads_test.go:420), [census:480](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/provision/plan14_root_reads_test.go:480), [census:507](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/provision/plan14_root_reads_test.go:507)). **Smallest fix:** preserve existing authority rows as ordinary authority entries, assign valid kept-family ownership, and explicitly use the existing legacy format for applicable cache reads. |
| + |
| +4. **P1 — Plan 3 Task 10’s provider test necessarily fails on existing code.** It asserts that `go list -deps` for `internal/edge` contains no AWS packages, claiming the package already imports none ([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)). `route53dns.go` directly imports four AWS packages ([route53dns.go:25](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/route53dns.go:25)). **Smallest fix:** retain the dependency check for the new `assetcache` package and check the new asset implementation files for AWS imports; retain the credential-free `DirStore` integration test. |
| + |
| +5. **P1 — Plan 1 Task 7’s proposed write-failure fixtures cannot exercise the named branches.** The plan keeps a concrete `*registry.Registry` parameter but proposes an `aliasRegFailing` wrapper or closing the registry ([plan 1:240](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:240)). A wrapper cannot satisfy that concrete parameter. Closing the handle makes `ListAliases` fail before the write branches, so the pass returns an error instead of the required nil-with-incomplete result ([aliases.go:245](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/domains/aliases.go:245), [registry.go:2755](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/registry/registry.go:2755)). **Smallest fix:** prescribe selective SQLite write-failure triggers on the test database, or introduce a narrow registry interface. |
| + |
| +6. **P2 — Plan 3 Tasks 2, 5 and 6 omit the `Set-Cookie` trailer veto.** Admission checks headers, and the fill instructions commit after a successful body without a trailer check ([plan 3:102](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:102), [plan 3:157](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:157)). The spec explicitly prohibits `Set-Cookie` trailers ([spec:491](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:491)); the existing shared cache already checks them ([edge.go:1875](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/edge.go:1875)). **Smallest fix:** require a final trailer veto before committing on both edges, preserving the information through backhaul; test announced and unannounced cookie trailers. |
| + |
| +7. **P2 — Plan 1 Task 6 can confirm records that were never applied.** Its completeness predicate checks successful reads and successful attempted writes ([plan 1:225](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:225)). However, an unreadable trash marker sets `declineMint`, and the record loop skips the record before `convergeOwned`, without a failing route write ([reconcile.go:1137](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/provision/reconcile.go:1137), [reconcile.go:1174](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/provision/reconcile.go:1174)). A newly purged generation can therefore remain unapplied while authority time advances. **Smallest fix:** require positive application or verified equality for every relevant existing route; mark skipped application incomplete and test this case with a changed generation. |
| + |
| +8. **P2 — Plan 2 Tasks 3 and 4 do not bound metadata writes.** Reservations allocate 64 KiB for metadata, but the specified write limits cover only identity and gzip bodies; `Commit` serializes arbitrary `Meta.Header` and key contents ([plan 2:103](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:103), [plan 2:126](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:126)). This does not implement the spec’s prohibition on writes past the reservation ([spec:643](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:643)). **Smallest fix:** bound the encoded metadata before writing it, include its bytes in accounting, and test oversized headers and queries. |
| + |
| +9. **P2 — Plan 2 Task 4’s replacement sequence is not crash-safe.** `Commit` replaces `.body`, then `.gz`, then `.meta`, including for an existing key ([plan 2:126](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:126)). A crash after replacing the body leaves the previous valid metadata, so `Open` indexes new bytes with old headers, sizes or expiry. The proposed missing-meta crash test does not catch this. **Smallest fix:** invalidate the old metadata before replacing its files, with coordinated readers, or publish versioned body files through the metadata commit. Add a replacement-crash test. |
| + |
| +10. **P2 — Plan 3 Task 5 does not apply conditional or range semantics to the initiating miss.** It strips those request headers upstream and tees the resulting body directly to the client ([plan 3:157](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:157)). Task 3 tests those semantics only against an existing entry. The spec requires the edge to answer them itself ([spec:400](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:400)). **Smallest fix:** specify how an admitted cold fill answers its initiating conditional/range request through `serveAssetEntry`, and add cold local and far tests. |
| + |
| +11. **P2 — Plan 3 Tasks 1 and 5 leave acknowledgement key selection undefined during rotation.** `verifyAssetFill` accepts either key but returns only a boolean; `verifyAssetAck` accepts only the key the far edge used ([plan 3:83](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:83)). The existing `signingKey` prefers `.next` immediately ([mesh.go:167](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/mesh.go:167)). If the home has `.next` and the far edge still signs with current, signing the acknowledgement with the home’s preferred key makes both edges skip billing. **Smallest fix:** return the matched verification key and sign the acknowledgement with it; test asymmetric key availability. |
| + |
| +12. **P2 — Plan 3 Task 6 catches upgrade requests.** Its far-edge predicate accepts any matching canonical GET or HEAD, with no upgrade exclusion ([plan 3:187](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:187)). Unlike the local hook, this executes before the existing upgrade branch. A WebSocket GET under an asset prefix can receive a cached body or lose its upgrade headers during filling, contrary to [spec:385](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:385). **Smallest fix:** explicitly reject `isUpgradeRequest(r)` before far-cache lookup and test intact backhaul of the upgrade. |
| + |
| +13. **P2 — Plan 1 Task 8 lacks a publication trigger for serving-generation changes.** It adds the serving build to the path union and a trigger for authority-time advances, but no trigger for a serving build changing while registry fields stay unchanged ([plan 1:264](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:264)). The current loop skips the writer until the registry fingerprint changes or its timer expires ([edgesfile.go:888](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/host/edgesfile.go:888)). Thus a retiring build’s prefixes can remain published until the five-minute refresh, contrary to [spec:255](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:255). **Smallest fix:** include serving-build changes in the scheduling signal and test a generation switch with an unchanged registry fingerprint. |
| + |
| +14. **P2 — No task implements the fallback ETag.** Plan 2 Task 4 stores supplied headers, while Plan 3 Task 3 only preserves or weakens an existing ETag ([plan 2:126](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:126), [plan 3:119](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:119)). The spec requires a strong body-SHA256 ETag when the app supplies none ([spec:560](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:560)). **Smallest fix:** assign hashing and fallback-header creation to the fill/commit task, with tests for identical home/far validators and conditional hits. |
| + |
| +15. **P2 — Plan 3’s acceptance tasks omit required production-wired attacks.** Tasks 5 and 6 explicitly use fake `LocalAssets` and a fake home; Task 9’s real two-edge tests cover only five scenarios ([plan 3:161](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:161), [plan 3:189](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:189), [plan 3:239](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:239)). The spec requires production wiring for the attack matrix, including credential stripping, refused fills, coalescing, deadlines and answering-generation checks ([spec:717](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:717)). **Smallest fix:** assign those cases explicitly to the real two-edge suite, including the warm/sleep/restart generation lifecycle. |
| + |
| +16. **P2 — Plan 1’s fixed copy inventory omits newly required refusals.** Task 0 pins exactly EAC-1 through EAC-19 ([plan 1:104](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:104)), but Task 3 requires a new error for combining `--assets` and `--no-assets`, and Task 2 requires echo and changed-assets reship refusals, without corresponding rows ([plan 1:138](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:138), [plan 1:161](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:161)). **Smallest fix:** inventory these strings before Task 0’s review, update the exact-ID gate, and explicitly identify any existing approved wording reused. |
| + |
| +reject |
| \ No newline at end of file |