plan: plans 3a, creation caps
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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. |