carlos / platform Public

design-cohort-activation: scope the backfill, done and freeze to one activation cohort; empty first activation instead of Plan 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev a30388fd5a4af009a8a4818d20be20b5dba524f4 parent 96db0ef
1 file changed, +331 −0
  • docs/superpowers/specs/2026-10-19-cohort-activation-design.md +331 −0

CI passed — run details

CI log
=== gate ===
go: downloading github.com/aws/aws-sdk-go-v2/service/autoscaling v1.74.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	303.273s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.697s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.390s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.707s
ok  	github.com/carlosframework/platform/internal/activator	250.713s
ok  	github.com/carlosframework/platform/internal/agentsock	0.015s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.018s
ok  	github.com/carlosframework/platform/internal/appconfig	2.474s
ok  	github.com/carlosframework/platform/internal/apperr	0.081s
ok  	github.com/carlosframework/platform/internal/applog	0.133s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.627s
?   	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.010s
ok  	github.com/carlosframework/platform/internal/caps	0.013s
ok  	github.com/carlosframework/platform/internal/certwarm	0.004s
ok  	github.com/carlosframework/platform/internal/cgroup	0.698s
ok  	github.com/carlosframework/platform/internal/changelog	0.331s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.783s
ok  	github.com/carlosframework/platform/internal/clamd	0.056s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	404.351s
?   	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	5.780s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.005s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.387s
ok  	github.com/carlosframework/platform/internal/doorkey	0.005s
ok  	github.com/carlosframework/platform/internal/economics	0.003s
ok  	github.com/carlosframework/platform/internal/edge	45.185s
?   	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.135s
ok  	github.com/carlosframework/platform/internal/gate	1.656s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
ok  	github.com/carlosframework/platform/internal/geo	0.010s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.152s
ok  	github.com/carlosframework/platform/internal/host	181.404s
ok  	github.com/carlosframework/platform/internal/hostactors	6.117s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.014s
ok  	github.com/carlosframework/platform/internal/hostbox	2.720s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.587s
ok  	github.com/carlosframework/platform/internal/hostenv	0.013s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.013s
ok  	github.com/carlosframework/platform/internal/hostexec	5.211s
ok  	github.com/carlosframework/platform/internal/hostfence	0.041s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.602s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.015s
ok  	github.com/carlosframework/platform/internal/hostguard	1.028s
ok  	github.com/carlosframework/platform/internal/hostops	1.599s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.014s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.013s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.464s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.140s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.013s
ok  	github.com/carlosframework/platform/internal/infraview	0.015s
ok  	github.com/carlosframework/platform/internal/instancectl	5.252s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.748s
ok  	github.com/carlosframework/platform/internal/instancefs	0.019s
ok  	github.com/carlosframework/platform/internal/instancekey	0.015s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.020s
ok  	github.com/carlosframework/platform/internal/layout	1.536s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.002s
ok  	github.com/carlosframework/platform/internal/lease	9.824s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.008s
ok  	github.com/carlosframework/platform/internal/ledger	0.013s
ok  	github.com/carlosframework/platform/internal/mail	0.165s
ok  	github.com/carlosframework/platform/internal/maintenance	0.082s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.498s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.077s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.304s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.797s
ok  	github.com/carlosframework/platform/internal/objstore	0.195s
ok  	github.com/carlosframework/platform/internal/ops	0.186s
ok  	github.com/carlosframework/platform/internal/park	0.210s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	37.372s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.012s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.560s
ok  	github.com/carlosframework/platform/internal/relay	7.225s
ok  	github.com/carlosframework/platform/internal/release	24.301s
ok  	github.com/carlosframework/platform/internal/rollout	3.926s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.057s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/runs	0.574s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.003s
ok  	github.com/carlosframework/platform/internal/schedule	2.501s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.165s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.833s
ok  	github.com/carlosframework/platform/internal/status	0.062s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.240s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
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.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.580s
=== 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: 49414306 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: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
  minio: downloaded, digest verified
  mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
  mc: downloaded, 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.022s
=== RUN   TestT4StorageProbeMinio
2026/10/19 22:26:21 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E00EAB8B8DD4F3, 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.020s
minio contract: PASS
diff --git a/docs/superpowers/specs/2026-10-19-cohort-activation-design.md b/docs/superpowers/specs/2026-10-19-cohort-activation-design.md
new file mode 100644
index 000000000..df976e88d
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-19-cohort-activation-design.md
@@ -0,0 +1,331 @@
+# Cohort activation: one claims-on box inside a populated deployment
+
+Status: draft for platform-1e and Paul, 2026-10-19. Nothing here is built.
+Code citations are to main at `96db0efe` unless they name branch
+`claim-step8-readiness` (`232baff2`, parked).
+
+The dev trial is paused at the end of step 7. Activation as built assumes the
+whole deployment turns on at once. The trial turns on ONE box (leo, roster pool
+`cohort1`) while sophie (roster pool `console`, claims off, serving pool-less
+records and the console) and dennis (outside the roster) keep running main's
+code. This note scopes the backfill, the adopt step, `done` and the freeze to
+that cohort, and recommends an empty first activation instead of Plan 5.
+
+## Assumptions
+
+- A1. The only cohort-pool record in the dev bucket is the legacy `claimtrial`.
+ wakecheck and rollcheck-0261 are pool-less (runbook P5 and step 4).
+- A2. Neither sophie nor dennis sets `CARLOS_POOLS` (step 4's record for
+ sophie; dennis "serves nothing"). This is read again before the window
+ (section 1).
+- A3. Turning sophie's console claim runtime off and on again is safe while no
+ claim and no backfill plan exist. Not exercised.
+- A4. `apps create`, `ship`, `promote` and store-mode `instances enable` work
+ for a new app on a claim-mode console. Not exercised on dev.
+- A5. A claims-on box acts only on hosts it holds a claim for:
+ `WorkSource.Select` refuses a host with no claim (`internal/hostbox/work.go:69-71`).
+- A6. A claims-off box ignores claims and mints whatever its own host.env scope
+ matches: `inScope` and `poolServed` (`internal/provision/reconcile.go:401-414`,
+ `:2155-2160`). With no `CARLOS_POOLS` it serves pool `""` only.
+
+## 1. Scoping
+
+**Recommendation.** The backfill plan (`control/hosts-backfill.json`) gains an
+additive `scope`: `{namespace, regions, pools}`, the same three fields as
+`hostactors.Scope` (`internal/hostactors/census.go:44-54`). One predicate
+decides membership everywhere:
+
+- a **box** is in scope when the create census would require it: roster
+ namespace, region and pool all match (`census.go:99-101`);
+- a **record** is in scope when its pool is a cohort pool and its region,
+ resolved for home the way `StartCreate` does (`production_create.go:222-225`),
+ is a cohort region.
+
+Every consumer then reads only the in-scope part:
+
+| Consumer | Today | Scoped |
+|---|---|---|
+| `backfill-collect` (branch, `claimactivation/backfill.go:136-177`) | freezes every record in the bucket | freezes in-scope records only |
+| Migration census (`rootmigration_census.go:65-95`) | every record must be frozen, by etag | in-scope records must be frozen; `s.Records` holds only them, so out-of-scope writes cannot move the snapshot fingerprint (`rootmigration.go:82-92`) |
+| `census`, `Publish`, `VerifyAdopting` (`backfill.go:217-264`) | a report from every roster box | reports from in-scope boxes only |
+| `ReconcileClaims` (`backfill_controller.go:89-113`) | tries to adopt every unclaimed record | skips out-of-scope records |
+| `CheckCreateFreeze` (`create_freeze.go:12-28`) | refuses every write until `done` | see section 2 |
+
+A plan without `scope` keeps today's deployment-wide behaviour byte for byte.
+An older reader of a scoped plan fails closed: it freezes the whole deployment
+until `done`, and `done` still arrives.
+
+`collect` refuses a cohort-pool record that is everywhere-class or has
+replicas. Such a record crosses regions on claims-off boxes, and `Adoption`
+already refuses it (`backfill.go:370-376`). The cohort pool must be a named
+pool. It is never `""`: pool `""` is what every unpooled claims-off box serves
+(A6), which is the two-writer hazard H1.
+
+**What must be proven about an out-of-scope box.** The single-writer property
+for a cohort host is this: the only boxes that can serve it are claims-on
+boxes holding its claim. Claims-on boxes are covered by A5. For claims-off
+boxes, there are three parts:
+
+1. **Record placement (mechanical).** The host's record carries a cohort pool,
+ and its placement is not everywhere-class, as above.
+2. **Signed roster (mechanical).** Every roster box whose roster pool is a
+ cohort pool is a cohort box, and its signed report says
+ `HostClaimEnabled` (`census.go:149`). `backfill-done` checks this.
+3. **Signed placement statement (attested).** A claims-off box's roster pool
+ says nothing about what it serves. Sophie is roster pool `console` but mints
+ every pool-less record. Its real scope is host.env, and nothing signs that
+ today. So the operator reads `CARLOS_POOLS`, `CARLOS_REGION` and
+ `CARLOS_HOST_CLAIM` on every box whose role can read the deployment bucket
+ (for dev: sophie, dennis and leo). The read is over SSM and read-only. The
+ readings go into the plan as `outside: {box: {region, pools, read_at}}`, so
+ the root signature covers them. `collect` refuses if a roster box outside
+ the scope has no entry, or if any entry lists a cohort pool. This is an
+ operator statement, not box evidence. Production replaces it (section 5).
+
+**Sophie and its pool-less records.** Sophie serves pool `""`; the cohort
+serves `cohort1`. `poolServed` keeps them disjoint, in both directions. So
+sophie's records are never frozen, never adopted and never need its report,
+and sophie never mints a `cohort1` record. Sophie stays the console's compute
+host. The create census still verifies that binding through sophie's box
+report (`census.go:171-174`, `:385-421`), which is the only thing the cohort
+needs from it.
+
+**Cost.** One additive plan field, one predicate and five call sites. The
+attestation is trusted for the trial: if someone later adds `cohort1` to a
+claims-off box's host.env, that box becomes a second writer, which main allows
+today too.
+
+**Rejected.**
+- A census key and the migration census on sophie (S3 option a). Sophie's
+ named-unit console route trips the activation preflight
+ (`claimactivation_preflight.go:25-42`). It would also turn on root census
+ work on a box the trial keeps claims-off.
+- Taking sophie out of the roster (option c). The console's compute binding
+ must name a roster box (`census.go:395-399`).
+- A "holds nothing" report signed by sophie. That needs a census key and new
+ code on a claims-off box (ruling 6).
+- Using roster pools as the proof for claims-off boxes. They are not what the
+ reconciler reads.
+
+## 2. The `done` path and the freeze
+
+**Who writes `done`, and when.** A new signer phase, `claims run
+backfill-done`, run on the trusted signer like `collect` and `adopt`. It
+writes If-Match on the adopting plan's etag. The plan is root-signed
+(`create_freeze.go:21`), so neither the console nor a box can write it. In the
+trial it runs straight after `backfill-adopt` (new 8.2c) and before step 9.
+
+**What `done` requires.**
+- The plan reads `adopting`, with the named id, at the current roster revision.
+- `VerifyAdopting` still passes on the in-scope reports (branch,
+ `backfill_publish.go`).
+- Every roster box in scope is active and reports `HostClaimEnabled`.
+- The in-scope records listed now are exactly `plan.Hosts`, so no cohort record
+ appeared or moved.
+- Every host in `plan.Hosts` holds a serving claim whose holder matches the
+ frozen key and acquisition, executed by the box the plan names.
+
+An in-scope host that no in-scope box reported is a refusal. It is a cohort
+record that only an outside box could be serving, which breaks part 3 above.
+Out-of-scope hosts are never in `plan.Hosts`, so they cannot block `done`.
+That removes the "unadopted hosts block it forever" trap. In the empty trial
+`plan.Hosts` is empty, so the last condition holds trivially.
+
+**What the freeze covers.** `CheckCreateFreeze` takes the record being written
+and refuses (`ErrPending`) only when the plan is not `done` and either has no
+scope or the record is in scope. Two callers get the record: create
+(`production_create.go:38`) and record writes (`record_writes.go:90`). The
+callers that have no record keep the deployment-wide check until `done`: the
+effect runner (`effect_runner.go:59`) and the move grants. That is bounded by
+`done`. Moves are off. On dev, no legacy app has an admission, so no effects
+run.
+
+**For how long.** In the trial, from `collect` to `done`. Because the cohort is
+empty, `collect` can run in the signing session before the window. The freeze
+then covers `cohort1` only, and no `cohort1` create can happen anyway until
+leo is claims-on (the create census needs leo's `HostClaimEnabled`,
+`census.go:149`). A plan that sticks at `collecting` or `adopting` blocks
+nothing outside the cohort, so no abandon phase is needed for the trial.
+
+**What an operator sees.** An in-scope write gets the claim door's 409 and its
+busy message, with `Retry-After: 2` (`claim_response.go:23-24`, `:29-33`).
+Out-of-scope writes behave as now. The phase can be read from `claims run`'s
+summary, or from the plan object's head (runbook P-B1).
+
+**Rejected.**
+- A deployment-wide freeze with a quick `done`. It still freezes sophie's
+ tenants, and a stop between phases freezes dev until someone acts.
+- No freeze. A cohort record could then appear between `collect` and
+ `adopt`, past the census.
+- The console writing `done` when adoption settles. The console has no release
+ key.
+
+## 3. S4: an empty cohort box is the first activation
+
+**Recommendation.** Yes. Activate leo empty and create the acceptance host
+under claims. That removes Plan 5 from the trial, and the binding scope
+supports it: "Historical-record conversion for records that cannot exist at
+first activation: FREEZE (explicit refusal is enough)"
+(`activation-scope-2026-10-16.md`). So does the approved seed copy: "A proven
+new empty host needs no files seed" (runbook line 2397).
+
+**What "empty" means, checked by machine:**
+- E1. There is no in-scope record and no claim. `collect` in empty mode
+ refuses unless `plan.Hosts` is empty and `control/hosts/` is empty.
+- E2. Leo's signed migration report lists zero installations, and the census
+ completes. That means no retained installation, no launch, no move journal
+ and no unfinished reservation (`rootmigration_census.go:146-215`).
+- E3. Leo's registry has no row. The branch's routes reader is reused, but any
+ row that no claim owns refuses the census. It is no longer reported as a
+ legacy installation.
+- E4. Leo's managed roots hold only shared classes: the window masks (a), the
+ certificate cache (b) and the box files (c), all from the branch. There is no
+ retained-unowned class (e). This matters after activation as well. The new
+ host's first install runs the per-host census, which refuses every
+ unclassified file of any non-claimed installation
+ (`empty_census.go:233-241`; only claimed siblings are excused,
+ `rootinventory_sibling.go:22-85`).
+
+**Getting leo empty (before the window, claims still off on leo):**
+1. Delete the legacy `claimtrial` instance. The claim-mode console cannot do
+ it: `claimtrial` has no admission, and a record with no claim "waits for
+ backfill" (`claim_retire.go:33-71`). The delete verb is console-only
+ (`instances_cli.go:620`). So sophie's console runs with its claim runtime
+ off for this one delete: `console-config` without
+ `--enable-console-runtime`, then a restart, the delete, the runtime back on,
+ and `console-report` (A3; Paul's go). Leo's claims-off removal phase then
+ retires the row.
+2. Move claimtrial's and wakecheck's leftovers aside on leo, with the
+ delta's move-aside recipe applied to both hosts. Certificates stay: the
+ `acme` tree is a shared class.
+3. Read-only checks: P4 shows 0 rows, P6 passes, no `cohort1` record exists,
+ and the `outside` readings are taken (section 1).
+
+**The acceptance host under claims (after step 9).** The legacy app
+`claimtrial` cannot get an admission. A legacy grant needs an adopting plan
+(`backfill.go:166-175`, `backfill_publish.go:126-168`), and its prefix is not
+free for a new enrollment (`enroll.go:94-99`). So the acceptance runs on a new
+app, `claimtrial2`, whose admission comes from
+`createClaimAppRow`/`EnrollNewApp` (`claim_app_enroll.go:16-43`):
+1. `apps create`, `ship` wc4, `promote` to `edge`, and store-mode
+ `instances enable --pool cohort1`, as step 4 does.
+2. `console-report` on sophie, immediately before the create. The create
+ census reads the console process from sophie's box report, which the agent
+ overwrites within about 6 minutes (#312, `census.go:400-418`).
+3. `instances create`. The claim path (`apiinstances.go:1623-1630`) selects
+ leo, the only active `cohort1` box (`production_create.go:136-152`), takes
+ the scoped census (`:226`) and starts a create with one participant and no
+ park obligations (`hostclaim/create_selection.go:8-23`). Leo's first
+ install passes the per-host census through its new-host branch
+ (`empty_census.go:130-132`).
+4. Only after leo reports the install is the certificate hello sent. A mirrored
+ certificate naming the host before its first install would refuse that
+ install (branch `claimcensus.go`, `claimBoxDataCensus` comment).
+
+Step 10's gate, "a real launch and two cold wakes with real origin evidence",
+holds unchanged. Only the expected counts change: `hits=` starts after the
+first wake's own rows, not at 4. Step 8.3 (files seed) is skipped, so H6 goes
+away.
+
+**Cost.** One console-runtime off/on on sophie. The files seed's KEEP items
+(the reboot race, retained parks) go unexercised until the first populated
+cohort.
+
+**Rejected.**
+- Building Plan 5 now. The per-host census would have to accept legacy rows
+ and files, and the legacy-row trust (G,J4) is unruled. Adoption also leaves
+ `park-local` chores on sophie, which a claims-off box never discharges
+ (`backfill.go:410-414`, `hostclaim/adopt.go:23-28`).
+- Keeping legacy `claimtrial` but out of scope. Its files refuse every new
+ install on leo.
+- A raw bucket delete of the record. It goes around the door and leaves the
+ domain claim behind.
+
+## 4. Smallest build that unblocks step 8
+
+From `claim-step8-readiness`:
+- **Reused:** both signer phases (extended below); `Publish`'s owners from the
+ census, and `VerifyAdopting`; `journal-migrate --window` and the mask class
+ (a); the box classes (b) and (c) in `claimGlobalSharedPaths`; the R3 umask
+ fixes; the S5 absent-journal fix; the witness and the leo snapshot loader.
+- **Moved to Plan 5:** reporting a legacy route row as a route installation
+ (`legacyRouteRow`). Only the routes reader stays.
+- **Thrown away:** the wakecheck retained-unowned class (e), replaced by the
+ move-aside; and the witness variant with sophie's stand-in report.
+
+Tasks:
+1. `hostops`: add `scope` and `outside` to `BackfillPlan`, and one exported
+ in-scope predicate for boxes and records.
+2. `backfill-collect --scope … --empty`: freeze in-scope records only; refuse
+ everywhere-class or replica cohort records, missing or cohort-pool
+ `outside` entries, and in empty mode any in-scope record or claim.
+3. `census`/`Publish`/`VerifyAdopting`: in-scope roster boxes only.
+4. Migration census: in-scope records only, and no unowned registry row.
+ Needs a census overlay ruling from platform-1e.
+5. `backfill-done` phase, as in section 2.
+6. Scoped `CheckCreateFreeze` at the two record call sites.
+7. `ReconcileClaims` skips out-of-scope records.
+8. Tests:
+ - unit tests for each refusal, mutation-checked;
+ - an unscoped-plan test proving today's behaviour unchanged;
+ - the ruling 6 pin test extended to the new entry points;
+ - one root-runner witness on leo's emptied tree: masks, `collect`,
+ `journal-migrate --window`, `adopt`, `done`, claims-on root, then a
+ production-constructor create and first install.
+9. Runbook delta: the new prechecks and attestation, the console-runtime
+ window, the move-aside, 8.1b moved before the window, 8.2b and 8.2c, no
+ 8.3, the acceptance create, and the new step 10 counts.
+10. Copy review for the new refusal strings.
+
+## 5. Deferred to production cohorts
+
+- Plan 5 populated backfill: installing adopted legacy hosts, the
+ legacy-row trust ruling, the files seed over adopted hosts, and scoping
+ Adoption's park chores to in-scope boxes.
+- Plan 15's independent writer, key rotation, and the rollback export (already
+ deferred for the trial).
+- Box-signed placement. Each box's signed report carries its reconcile scope,
+ and IAM exclusivity keeps non-roster boxes out of the deployment bucket.
+ Together they replace the `outside` attestation.
+- Plan lifecycle across cohorts: replacing a `done` plan, per-app scoping of
+ the effect-runner and move-grant freeze, and an abandon phase.
+- Deployment-wide lifecycle censuses. Delete, purge and app lifecycle require
+ every namespace pool (`production_delete.go:23-42`,
+ `production_held_start.go:54-75`), so with sophie claims-off a claimed host
+ cannot be deleted. In the trial, `claimtrial2` lives until a teardown design
+ exists (runbook open question 2).
+- #312, so the console process evidence lasts.
+- 4a: the named-unit console box, which brings sophie in.
+
+## 6. Ruling 6
+
+Every change sits on a claim-mode path:
+- the signer phases exist only in `claims run`;
+- `journal-migrate` refuses unless `hostwrite.Enabled`;
+- `CheckCreateFreeze`'s record callers are the claim create door and the
+ conditional record store, which only the console's claim runtime builds
+ (`internal/console/claim_runtime.go:42`);
+- `ReconcileClaims` runs only with `HostClaim` (`hostclaimbackfill.go:40-48`).
+
+Nothing touches `provision`, the edge, status reports or edges files. The
+`outside` readings are SSM reads by the operator, not box code. A claims-off
+box (sophie, dennis, and leo until 8.2) and a no-census box run main's code
+unchanged. A plan without `scope` behaves exactly as today.
+
+## Questions for Paul
+
+1. The empty-cohort route: delete legacy `claimtrial` through a claims-off
+ console window on sophie, move its leftovers and wakecheck's aside, skip
+ the files seed, and accept on a new app `claimtrial2`. The seed's KEEP items
+ then go unexercised in this trial.
+2. Is an operator-read placement, signed into the plan, enough proof for the
+ trial that sophie and dennis cannot serve `cohort1`? Box-signed scope and
+ IAM exclusivity would be owed before production.
+3. With sophie claims-off, the acceptance host cannot be deleted under
+ claims. Is that acceptable for the trial?
+4. #312: run `console-report` right before the create (the recommendation), or
+ fix #312 first?
+5. Noted while reading, not exercised: dev's claim-mode console has had no
+ executor for pool-less creates since step 7. No roster box is in pool
+ `default` (`production_create.go:123-152`). Does that affect platform-d8's
+ work on dev?