carlos / platform Public

plan: plans 3a, creation caps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 8fd9d4ebcc9454263495666d465d8f18d78fc094 parent 5f341f3
1 file changed, +95 −0
  • docs/superpowers/plans/2026-10-22-plans-3a-creation-caps.md +95 −0

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (0 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.015s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (2 s)
=== gofmt (1 s)
gofmt -l .: clean
=== rest (149 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	7.339s
ok  	github.com/carlosframework/platform/cmd/moveprobe	13.137s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.983s
ok  	github.com/carlosframework/platform/internal/activator	61.752s
ok  	github.com/carlosframework/platform/internal/agentsock	0.036s
ok  	github.com/carlosframework/platform/internal/alerting	0.011s
ok  	github.com/carlosframework/platform/internal/analytics	0.102s
ok  	github.com/carlosframework/platform/internal/appconfig	6.053s
ok  	github.com/carlosframework/platform/internal/apperr	0.497s
ok  	github.com/carlosframework/platform/internal/applog	0.322s
ok  	github.com/carlosframework/platform/internal/awsinv	0.011s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.013s
ok  	github.com/carlosframework/platform/internal/boxid	2.444s
?   	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.013s
ok  	github.com/carlosframework/platform/internal/caps	0.017s
ok  	github.com/carlosframework/platform/internal/certwarm	0.005s
ok  	github.com/carlosframework/platform/internal/cgroup	0.997s
ok  	github.com/carlosframework/platform/internal/changelog	1.088s
ok  	github.com/carlosframework/platform/internal/claimactivation	2.259s
ok  	github.com/carlosframework/platform/internal/clamd	0.057s
?   	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	6.882s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.040s
ok  	github.com/carlosframework/platform/internal/dbverify	0.009s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.006s
ok  	github.com/carlosframework/platform/internal/docsite	0.029s
ok  	github.com/carlosframework/platform/internal/domains	5.131s
ok  	github.com/carlosframework/platform/internal/doorkey	0.011s
ok  	github.com/carlosframework/platform/internal/economics	0.028s
ok  	github.com/carlosframework/platform/internal/edge	48.189s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.025s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.237s
ok  	github.com/carlosframework/platform/internal/gate	2.543s
ok  	github.com/carlosframework/platform/internal/genselect	0.048s
ok  	github.com/carlosframework/platform/internal/geo	0.025s
ok  	github.com/carlosframework/platform/internal/geoip	0.272s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.454s
ok  	github.com/carlosframework/platform/internal/host	145.091s
ok  	github.com/carlosframework/platform/internal/hostactors	9.356s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.013s
ok  	github.com/carlosframework/platform/internal/hostbox	5.556s
ok  	github.com/carlosframework/platform/internal/hostclaim	1.680s
ok  	github.com/carlosframework/platform/internal/hostenv	0.025s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.010s
ok  	github.com/carlosframework/platform/internal/hostexec	9.835s
ok  	github.com/carlosframework/platform/internal/hostfence	0.078s
ok  	github.com/carlosframework/platform/internal/hostfiles	12.898s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.162s
ok  	github.com/carlosframework/platform/internal/hostguard	1.530s
ok  	github.com/carlosframework/platform/internal/hostops	4.774s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.129s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.007s
ok  	github.com/carlosframework/platform/internal/hostrelocate	1.297s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.345s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.005s
ok  	github.com/carlosframework/platform/internal/infraview	0.004s
ok  	github.com/carlosframework/platform/internal/instancectl	5.406s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.921s
ok  	github.com/carlosframework/platform/internal/instancefs	0.009s
ok  	github.com/carlosframework/platform/internal/instancekey	0.007s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.021s
ok  	github.com/carlosframework/platform/internal/layout	3.673s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.017s
ok  	github.com/carlosframework/platform/internal/lease	10.684s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	1.229s
ok  	github.com/carlosframework/platform/internal/ledger	0.202s
ok  	github.com/carlosframework/platform/internal/mail	0.665s
ok  	github.com/carlosframework/platform/internal/maintenance	0.112s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.986s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.137s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	26.264s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.988s
ok  	github.com/carlosframework/platform/internal/objstore	0.867s
ok  	github.com/carlosframework/platform/internal/ops	0.543s
ok  	github.com/carlosframework/platform/internal/park	0.758s
ok  	github.com/carlosframework/platform/internal/plans	0.014s
ok  	github.com/carlosframework/platform/internal/privatefile	0.008s
ok  	github.com/carlosframework/platform/internal/provision	20.508s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.014s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	7.758s
ok  	github.com/carlosframework/platform/internal/relay	7.324s
ok  	github.com/carlosframework/platform/internal/release	31.102s
ok  	github.com/carlosframework/platform/internal/rollout	5.580s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.007s
ok  	github.com/carlosframework/platform/internal/rootaws	0.015s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.078s
ok  	github.com/carlosframework/platform/internal/rootstate	0.028s
ok  	github.com/carlosframework/platform/internal/runs	0.662s
ok  	github.com/carlosframework/platform/internal/s3compat	0.014s
ok  	github.com/carlosframework/platform/internal/safefile	0.009s
ok  	github.com/carlosframework/platform/internal/schedule	3.561s
ok  	github.com/carlosframework/platform/internal/seeds	0.025s
ok  	github.com/carlosframework/platform/internal/sim/engines	6.996s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	23.282s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.277s
ok  	github.com/carlosframework/platform/internal/status	0.088s
ok  	github.com/carlosframework/platform/internal/steering	0.008s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.534s
ok  	github.com/carlosframework/platform/internal/svcrole	0.003s
ok  	github.com/carlosframework/platform/internal/telemetry	0.020s
ok  	github.com/carlosframework/platform/internal/termfence	0.010s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.006s
ok  	github.com/carlosframework/platform/internal/unixdial	0.004s
ok  	github.com/carlosframework/platform/internal/updatewords	0.005s
ok  	github.com/carlosframework/platform/internal/usage	0.008s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	2.170s
=== internal_console@0 (85 s)
ok  	github.com/carlosframework/platform/internal/console	81.381s
=== internal_console@1 (40 s)
ok  	github.com/carlosframework/platform/internal/console	36.492s
=== internal_console@2 (47 s)
ok  	github.com/carlosframework/platform/internal/console	43.762s
=== internal_console@3 (43 s)
ok  	github.com/carlosframework/platform/internal/console	38.859s
=== internal_console@4 (43 s)
ok  	github.com/carlosframework/platform/internal/console	40.475s
=== internal_console@5 (45 s)
ok  	github.com/carlosframework/platform/internal/console	41.267s
=== internal_console@6 (48 s)
ok  	github.com/carlosframework/platform/internal/console	44.625s
=== internal_console@7 (48 s)
ok  	github.com/carlosframework/platform/internal/console	44.607s
=== root@0 (40 s)
ok  	github.com/carlosframework/platform	37.829s
=== root@1 (48 s)
ok  	github.com/carlosframework/platform	45.583s
=== root@2 (48 s)
ok  	github.com/carlosframework/platform	44.462s
=== 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: 50069666 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.025s
=== RUN   TestT4StorageProbeMinio
2026/10/22 10:15:55 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E0D28D3B8E746F, 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
minio contract: PASS
diff --git a/docs/superpowers/plans/2026-10-22-plans-3a-creation-caps.md b/docs/superpowers/plans/2026-10-22-plans-3a-creation-caps.md
new file mode 100644
index 000000000..b10cae7d6
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-22-plans-3a-creation-caps.md
@@ -0,0 +1,95 @@
+# Plans 3a: Creation Caps Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** When plans are on, the console refuses to create what an account's plan does not allow: more running things (apps and instances) than the cap, more workers per app than the cap, a second free account for the same person, fleets and custom domains on plans without them.
+
+**Architecture:** One file, `internal/console/plancaps.go`, holds the checks. Each takes the account's effective limits (Plan 1's `EffectiveLimits`; nil means plans are off and every check passes) and returns a `*planCapError` whose message is approved copy. Each capped door takes the account lock (`lockAccount`) around its count and its write so two concurrent creates cannot both pass. JSON doors answer 409 with `"code":"cap"`; browser doors flash the message, as they already do for their other refusals.
+
+**Tech Stack:** Go, `internal/console`, `internal/provision` (record listing).
+
+**Spec:** `docs/superpowers/specs/2026-10-20-plans-caps-packs-design.md` §5.1 (running things, sidecars, creation has no grace), §8.1 (one free account per person, creation only), §8.4 (paid-only custom domains), table 4.2 (fleets). Builds on Plans 1 and 2 (merged f550afa, 0235046).
+
+## Global Constraints
+
+- **No catalog, no change.** Every check returns nil when `EffectiveLimits` is nil. Each task pins this with a test.
+- Creation refusals have no grace (spec §5.1): refuse at the cap, touch nothing that already runs.
+- **Running things** = live apps (including temporary apps and apps being restored: `deleted_at = 0 OR restoring = 1`) plus customer instance records of those live apps. Trashed apps and their instances do not count.
+- Lock order is account before app (`applocks.go:50`). A door takes `lockAccount` first, before any app lock.
+- Direct-mode creations never reach the console (`apps.go:546`) and are out of scope; adopting such an app through `ownerClaim` is capped like any create.
+- All refusal text is approved copy (copy review before the strings go into files). Browser copy is sentence case; CLI and JSON copy is lower case. No em dashes.
+- Tests: console tests run serially in practice; touched packages only, with `-run`; CI runs the full suite. New routes: none. Claim fixtures: none expected (no new doors or workers). The #280 root-read overlay is re-pinned in the last task.
+
+## Review Focus
+
+1. **Plans off.** With no catalog, every door behaves exactly as today, including under concurrency. Pinned per task (`...WithoutACatalog` tests).
+2. **Concurrent creates at the cap.** Two creates racing for the last slot: exactly one succeeds. Pinned in Task 2 (`TestRunningThingsCapIsExactUnderConcurrentCreates`).
+3. **Restore over the cap.** Restoring a trashed app when the account is at its cap is refused, with nothing changed. Pinned in Task 2 (`TestRestoreRefusedAtTheRunningThingsCap`).
+4. **A sponsored account above the catalog ceiling.** Its recorded limit, not the plan's, is the cap. Pinned in Task 1 (`TestRunningThingsCapUsesEffectiveLimits`).
+5. **Platform hostnames.** Attaching a platform hostname stays allowed on free plans; only custom domains are paid-only. Pinned in Task 5 (`TestPlatformHostnamesStayFreeOnFreePlans`).
+
+---
+
+### Task 1: The checks
+
+**Files:** Create `internal/console/plancaps.go`, `internal/console/plancaps_test.go`, `internal/console/plancaps_copy.go` (generated from approved copy); Modify `internal/console/accountplans.go` (one store query).
+
+**Interfaces — produces:**
+```go
+type planCapError struct{ msg string }
+func (e *planCapError) Error() string
+func (s *Store) LiveAppCount(accountID int64) (int, error) // deleted_at = 0 OR restoring = 1
+func (s *Server) runningThings(ctx context.Context, acct *Account) (int, error) // live apps + their instance records (key-only bucket list of apps/<sqid>/)
+func (s *Server) checkRunningThings(ctx context.Context, acct *Account, adding int) error
+func (s *Server) checkSidecars(ctx context.Context, acct *Account, app string) error
+func (s *Server) checkNewFreeAccount(addr string) error
+func (s *Server) checkFleets(acct *Account) error
+func (s *Server) checkCustomDomains(acct *Account) error
+```
+Each returns nil when plans are off, `*planCapError` at the cap, or an ordinary error when counting failed (doors answer that as they answer other internal errors).
+
+- [ ] **Step 1: Failing tests** in `plancaps_test.go`:
+ `TestPlanChecksPassWithoutACatalog` (every check nil with no catalog, even at absurd counts); `TestLiveAppCountCountsRestoringNotTrashed`; `TestRunningThingsCountsAppsAndInstances` (two apps, three instance records, one trashed app with an instance: 5); `TestRunningThingsCapAtAndBelow` (free cap 3: at 2 → nil, at 3 → planCapError); `TestRunningThingsCapUsesEffectiveLimits` (sponsored running_things 300 above the 200 ceiling: 250 → nil); `TestSidecarCap` (free: second sidecar refused; personal: sixth refused); `TestNewFreeAccountCap` (owner of one free account: refused; owner of one paid account: allowed; member, not owner, of a free account: allowed); `TestFleetsAndCustomDomainsFollowThePlan` (free refused, personal allowed).
+- [ ] **Step 2:** Run `go test ./internal/console/ -run 'PlanChecks|LiveAppCount|RunningThings|SidecarCap|NewFreeAccountCap|FleetsAndCustomDomains'` → FAIL.
+- [ ] **Step 3:** Implement. `runningThings` lists keys under `apps/<sqid>/` once and counts `provision.ParseRecordKey` hits whose app is in the live set from `AppsIn`. `checkNewFreeAccount` uses `AccountsFor(addr)`, owner role only, each account's plan from `AccountPlan` (nil → `DefaultPlan`), counting accounts whose plan is not `Paid`, refusing at `FreeAccountsPerPerson` (0 means no cap).
+- [ ] **Step 4:** Run → PASS. **Step 5:** Commit `console: plan creation checks`.
+
+### Task 2: App creation and restore
+
+**Files:** Modify `server.go` (`handleNewApp`), `apicli.go` (`handleAPICreateApp`, `handleAPIRestoreApp`), `apitemporary.go`, `restore.go`, `claim_restore.go`, `ownership.go` (`handleRestoreApp`); tests beside each (`apicli_test.go`, `restore_test.go`, `apitemporary_test.go`).
+
+- [ ] **Step 1: Failing tests:** `TestCreateAppRefusedAtTheRunningThingsCap` (CLI: 409 `"code":"cap"`, no row; browser: flash with the approved message, no row); `TestTemporaryAppCountsTowardRunningThings`; `TestRestoreRefusedAtTheRunningThingsCap` (both restore doors, both claim modes; nothing changed); `TestRunningThingsCapIsExactUnderConcurrentCreates` (cap 3, two live, 8 concurrent CLI creates: exactly one 201); `TestCreateAppWithoutACatalogUnchanged`.
+- [ ] **Step 2:** Run → FAIL.
+- [ ] **Step 3:** In `handleNewApp` and `handleAPICreateApp`, take `defer s.lockAccount(acct.Sqid)()` before `claimApp` and call `checkRunningThings(ctx, acct, 1)`; refuse with the door's existing shape. In `/new-temporary`, call it after `temporaryCapCheck` (the lock is already held). In `restoreApp` and `restoreClaimApp`, call it for every row, beside the temporary check, returning the `*planCapError`; both restore doors map it with `errors.As` (CLI 409 `"code":"cap"`; browser `fail`).
+- [ ] **Step 4:** Run → PASS. **Step 5:** Commit `console: the running-things cap on app creation and restore`.
+
+### Task 3: Instances, first-route attach and sidecars
+
+**Files:** Modify `apiinstances.go` (`handleAPIInstanceCreate`, before the seed branch at ~1580), `domainattach.go` (first-route record write ~237 and `attachClaimFirstRoute`), `apisidecars.go` (`handleAPISidecarCreate`); tests in `apiinstancescanary_test.go`, `apiinstanceseed_test.go`, `apidomains_firstroute_test.go`, `apisidecars_test.go`.
+
+- [ ] **Step 1: Failing tests:** `TestInstanceCreateRefusedAtTheRunningThingsCap` (plain, seeded and imported: 409 `"code":"cap"`, no record); `TestFirstRouteAttachRefusedAtTheRunningThingsCap`; `TestSidecarCreateRefusedAtTheSidecarCap`; `TestInstanceAndSidecarCreateWithoutACatalogUnchanged`.
+- [ ] **Step 2:** Run → FAIL.
+- [ ] **Step 3:** Each door takes `lockAccount` before its checks and calls the matching check; the instance door does so once, before the seed branch, so all three variants are covered.
+- [ ] **Step 4:** Run → PASS. **Step 5:** Commit `console: caps on instances, first routes and sidecars`.
+
+### Task 4: One free account per person
+
+**Files:** Modify `server.go` (`handleNewAccount`), `authcli.go` (`handleCLINewAccount`); tests in `server_account_test.go`, `authcli_test.go`.
+
+- [ ] **Step 1: Failing tests:** `TestNewAccountRefusedForASecondFreeAccount` (browser flash; CLI 409 `"code":"cap"`; no account minted, no bucket manifest); `TestFirstAccountIsNeverRefused` (auto-mint on first sign-in with a catalog configured); `TestNewAccountAllowedWhenTheOtherIsPaid`; `TestNewAccountWithoutACatalogUnchanged`.
+- [ ] **Step 2:** Run → FAIL. **Step 3:** Call `checkNewFreeAccount(addr)` in both doors before `mintAccount`. `currentAccount`'s auto-mint is untouched (spec §1: landing never refuses). **Step 4:** Run → PASS. **Step 5:** Commit `console: one free account per person`.
+
+### Task 5: Fleets and custom domains
+
+**Files:** Modify `fleets.go` (`handleAPIFleetNew`), `domainattach.go` (`attachDomainAs`, after the platform-hostname check and above `heldElsewhere`, per the file's ordering rule), `apidomains.go` (alias branch above `heldElsewhere` ~525; map `*planCapError` to 409 `"code":"cap"`), `appdomains.go` (`attachInstanceCatchall` and the refusal render); tests in `fleets_test.go`, `apidomains_test.go`, `appdomains_test.go`.
+
+- [ ] **Step 1: Failing tests:** `TestFleetCreateRefusedOnAPlanWithoutFleets`; `TestCustomDomainRefusedOnAFreePlan` (CLI static, CLI alias, browser alias, browser catch-all: refused, nothing claimed); `TestPlatformHostnamesStayFreeOnFreePlans`; `TestFleetsAndDomainsWithoutACatalogUnchanged`.
+- [ ] **Step 2:** Run → FAIL. **Step 3:** Implement the gates. **Step 4:** Run → PASS. **Step 5:** Commit `console: fleets and custom domains follow the plan`.
+
+### Task 6: Census and gates
+
+- [ ] Re-pin the #280 root-read overlay for the changed console units (generator recipe from Plans 1–2), loaded last, `rev3=PROPOSED-plans-3a-creation-caps`; check the record-write census; run the full claim invariant once; merge main; push; CI green; ask for the ruling on #280.
+
+## Done when
+
+Every task's tests pass, Review Focus 1–5 are pinned, CI is green after merging current main, and Astra's whole-branch review has no open Critical or Important finding.