docs: nearest-region plan rev 7, after Astra's plan round 6, and a review summary
Task 12 logs spec §6.5's one region line for every successful creation, in every switch state, with a test across assignment on, off and an unnamed home (failed before, passes after, proved in scratch). Adds a one-page summary for Paul: what ships per release, the tasks, the 121-string copy gate, the rollout steps and who approves them, and the risks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+173
−6
docs/superpowers/plans/2026-10-02-nearest-region-default-SUMMARY.md+103 −0docs/superpowers/plans/2026-10-02-nearest-region-default.md+70 −6
CI passed — run details
diff --git a/docs/superpowers/plans/2026-10-02-nearest-region-default-SUMMARY.md b/docs/superpowers/plans/2026-10-02-nearest-region-default-SUMMARY.md| new file mode 100644 |
| index 000000000..247e67e95 |
| --- /dev/null |
| +++ b/docs/superpowers/plans/2026-10-02-nearest-region-default-SUMMARY.md |
| @@ -0,0 +1,103 @@ |
| +# Nearest region by default: plan summary for review |
| + |
| +Plan: `2026-10-02-nearest-region-default.md` (rev 7). Spec: |
| +`../specs/2026-10-01-nearest-region-default-design.md` (rev 6.2). Scope is |
| +placement only (R8): no DNS writes, no steering, no latency routing. |
| + |
| +## What ships, by release |
| + |
| +| Release | Tasks | What changes for people | |
| +| --- | --- | --- | |
| +| Box release | 1, 1b | Nothing visible. Each box's edges file also says whether it takes ordinary instances (`serves_shared_pool`, `accepts_inbound`) and which secrets key it holds (`secrets_key_fp`). | |
| +| Console release, switch off | 2 to 17, 23 | Apps gain a stored region (the home for now). `GET …/regions` gains place names and `app_default` (with `region_known`, `placement_known`, `instance_domain`). A new guess endpoint. An explicit create into a region whose boxes provably lack the secrets key is refused (C31). Nothing non-home is written. | |
| +| Switch on (flagship) | none (config) | New apps get the region nearest the person (timezone, then IP, then home), shown in a picker when two or more regions are eligible. New instances default to their app's region. | |
| +| CLI release | 18 to 22 | `--region` on `apps create`, `deploy` (new apps only) and `canary`; the CLI sends a timezone hint and prints the place. | |
| +| Docs | 24 | `docs/site/cli.md` flags; README release checklist, floor version and "Turning app regions on". | |
| +| Later, own repo | none here | titogo adopts the guess endpoint and place names. | |
| + |
| +## Tasks (one line each) |
| + |
| +0. Copy inventory and review; write the approved copy record. Nothing else starts before it. |
| +1. Boxes publish shared-pool and inbound eligibility, from the reconciler's own predicates. |
| +1b. Boxes publish their secrets-key fingerprint (R9). |
| +2. `internal/geo`: points, distance, nearest pick. |
| +3. `internal/geo`: embedded tz zone table and country points. |
| +4. AWS region table and its 34 place names. |
| +5. Optional GeoIP locator (MaxMind format, pinned test fixtures). |
| +6. Console page copy. |
| +7. Console API messages, including the empty-list variants. |
| +8. Store: `apps.region` and `apps.region_source`, carried through every rebuild. |
| +9. Console config: the switch, the named home, the expected secrets fingerprint, test seams. |
| +10. Strict edges read, region candidates, one-box eligibility (incl. R9), app policy. |
| +11. The guess, with its invariant property test. |
| +12. Region resolution inside `claimApp`; the API create door takes region and timezone. |
| +13. The new-app page picker and browser guess route; no-picker form byte-identical. |
| +14. The instance create door: fill from the app region, eligibility, the R9 refusal, place names. |
| +15. `app_default`, place names and `POST …/regions/guess`. |
| +16. The Instances tab region lines. |
| +17. The roll-forward discrepancy log. |
| +18. CLI copy. |
| +19. Timezone detection on the person's machine. |
| +20. Create-app wire, `apps create --region`, the onboarding line. |
| +21. `deploy --region` on an existing app, checked before anything ships. |
| +22. `canary --region`, read-only checks before any write; instance output names the place. |
| +23. The region service-gate script, proved against a real backhaul. |
| +24. Docs and the release checklist. |
| +25. Whole-branch gate (build, vet, gofmt, tests, five cross-compile targets, copy checks). |
| + |
| +## The copy-review gate (Task 0) |
| + |
| +Every string the plan writes is reviewed with the copy-review skill before |
| +any product file holds it, and Task 25 proves the approved text landed byte |
| +for byte. The inventory is **121 strings**: |
| + |
| +- 44 user-facing messages and labels (C and R entries, including C16b to |
| + C19b and C30c added during review); |
| +- 34 place names (C23); |
| +- 10 doc passages (D1 to D10); |
| +- 20 operator log lines (O1 to O20); |
| +- 13 script, usage and fixture strings (S1 to S3, G1 to G6, U1 to U3, F1). |
| + |
| +Two notes for the review: C25's draft says `carlos canary remove`, but the |
| +verb is `carlos canary rm`; and C24 needs DB-IP's required attribution |
| +wording confirmed. |
| + |
| +## Rollout (plan "Rollout"; spec §17) |
| + |
| +| # | Step | Needs Paul | |
| +| --- | --- | --- | |
| +| 1 | Audit manifests' `regions`; boxes publish their AWS region; set `CONSOLE_HOME_REGION` and `CARLOS_SECRETS_PUBKEY` on the console; name the floor version in README, release notes and the roll recipe. | approve the console config | |
| +| 2 | Stage `CARLOS_SECRETS_KEY` on every box that should take placed instances. **Done for the flagship (2026-10-03).** | only for other deployments | |
| +| 3 | Box release everywhere; check every `secrets_key_fp` equals the console's. | approve the production roll | |
| +| 4 | Console release with the switch unset. | approve the production roll | |
| +| 5 | Dev: switch on, browser and API creates, `/regions` diff, service gate. | no | |
| +| 6 | Production service gate with the switch off: probe, deploy, gate from outside and through every edge. | no (report to Paul) | |
| +| 7 | Flagship: switch on. The R7 rollback floor applies from here. | **yes: his call** | |
| +| 8 | CLI release. | approve the release | |
| +| 9 | titogo adopts the guess and names (its own repo). | yes | |
| + |
| +## Risks |
| + |
| +- **A placed instance on a box without the secrets key runs with no config, |
| + secrets or store** (R9, the kpmg outage). Automatic choices need the key on |
| + one box and explicit creates are refused when it is provably absent. |
| + Rollout step 3 checks the fingerprints. The flagship now has the key |
| + everywhere. |
| +- **Rolling the console back below the floor** makes non-home apps create |
| + instances at home again. The rule is a runbook floor (R7), and the |
| + discrepancy log (Task 17) catches what slips through. |
| +- **No latency routing yet** (R8). A non-home instance is reached through the |
| + home's DNS and the backhaul until the follow-on spec. The service gate |
| + (Task 23) proves reachability, not speed. |
| +- **Mixed versions.** Every wire change is additive. An old CLI is treated as |
| + sending no hints. A new CLI against an old console skips deploy's check, |
| + and refuses `canary --region` rather than guess. |
| +- **Guess quality.** Timezone first, then a GeoIP database if configured, then |
| + home. The tz table is a release-checklist refresh; a stale one only misses |
| + new zone names. |
| +- **Local test environment.** On this machine the root package's full suite |
| + is blocked by the operator's `~/.carlos/config`, and a few `internal/edge` |
| + and `internal/activator` tests fail identically on the unmodified base. CI |
| + is the real gate (Task 25). |
| +- **Review size.** 121 strings is a long copy review, and it gates every |
| + task after Task 0. |
diff --git a/docs/superpowers/plans/2026-10-02-nearest-region-default.md b/docs/superpowers/plans/2026-10-02-nearest-region-default.md| index 893277964..7b569e9e9 100644 |
| --- a/docs/superpowers/plans/2026-10-02-nearest-region-default.md |
| +++ b/docs/superpowers/plans/2026-10-02-nearest-region-default.md |
| @@ -2,7 +2,9 @@ |
| > **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. |
| -**Revision:** 6. |
| +**Revision:** 7. A one-page summary for review (what ships per release, |
| +the task list, the copy-review gate, the rollout and the risks) is in |
| +`2026-10-02-nearest-region-default-SUMMARY.md`, beside this file. |
| - Rev 1 (9513d52c) was reviewed by Astra |
| (`CODEX-PLAN-NEAREST-REGION-R1.md`, beside this file): not ready, with |
| @@ -16,10 +18,11 @@ |
| carries spec rev 6.2 (`instance_domain`, approved by Paul 2026-10-03). |
| - Rev 6 answers Astra's round 5 (`CODEX-PLAN-NEAREST-REGION-R5.md`), and |
| marks the flagship's secrets-key staging done (Rollout step 2). |
| +- Rev 7 answers Astra's round 6 (`CODEX-PLAN-NEAREST-REGION-R6.md`). |
| - The code blocks the findings touched were **proved**: extracted verbatim |
| into a throwaway worktree at the branch base, built and vetted, and each |
| prescribed test was run before and after its implementation step. |
| -- The five dispositions tables, with the proof results, are at the end. |
| +- The six dispositions tables, with the proof results, are at the end. |
| **Goal:** Give every new app a region at creation, preselected from a timezone-then-IP guess, and have new instances default to that region. Never place an instance automatically on a box that cannot open the deployment's secrets. Existing apps, older CLIs and DNS stay unchanged. |
| @@ -4022,6 +4025,38 @@ func TestClaimAdoptionDiscardsTheGuess(t *testing.T) { |
| } |
| } |
| +// codex plan-R6 1, spec §6.5: every successful creation logs exactly one |
| +// region line, naming what was stored, whatever the switch says: on, off, |
| +// and on with an unnamed home (which keeps assignment off). |
| +func TestClaimLogsOneRegionLineInEverySwitchState(t *testing.T) { |
| + for _, c := range []struct { |
| + name, app, home string |
| + on bool |
| + stored, source string |
| + line string |
| + }{ |
| + {"assignment on", "on1", "ie", true, "uk", "chosen", "console: app region bab/on1: uk (chosen)"}, |
| + {"assignment off", "off1", "ie", false, "", "default", "console: app region bab/off1: ie (default)"}, |
| + {"on, home unnamed", "unnamed1", "", true, "", "default", "console: app region bab/unnamed1: (default)"}, |
| + } { |
| + fx, token := regionFixture(t) |
| + fx.srv.cfg.AppRegions, fx.srv.cfg.HomeRegion = c.on, c.home |
| + var logs bytes.Buffer |
| + restore := captureLog(&logs) |
| + status, res := postJSON(t, newAppURL(fx), map[string]string{"name": c.app, "region": "uk"}, token) |
| + restore() |
| + if status != http.StatusOK { |
| + t.Fatalf("%s: create = %d %v", c.name, status, res) |
| + } |
| + if r, src, _ := fx.st.AppRegion(OpsAccountID, c.app); r != c.stored || src != c.source { |
| + t.Fatalf("%s: stored %q %q, want %q %q", c.name, r, src, c.stored, c.source) |
| + } |
| + if n := strings.Count(logs.String(), "console: app region bab/"+c.app+":"); n != 1 || !strings.Contains(logs.String(), c.line+"\n") { |
| + t.Errorf("%s: %d region line(s) in %q, want exactly %q", c.name, n, logs.String(), c.line) |
| + } |
| + } |
| +} |
| + |
| // Review Focus 5: a competing insert that lands AFTER the region was |
| // resolved loses the claim: no row of ours, no region log line. |
| func TestClaimLostAfterResolutionWritesNothing(t *testing.T) { |
| @@ -4243,12 +4278,18 @@ func (s *Server) claimApp(ctx context.Context, acct *Account, name, addr, placem |
| if !created { // lost a claim race after the checks |
| return claimTaken, "", nil |
| } |
| - if s.appRegionsOn() { |
| - log.Printf("console: app region %s/%s: %s (%s)", acct.Sqid, name, s.homeRegionOut(region), source) |
| - } |
| + // Spec §6.5: one line per creation, in every switch state (codex |
| + // plan-R6 1). Only reached once the row is ours. |
| + log.Printf("console: app region %s/%s: %s (%s)", acct.Sqid, name, s.homeRegionOut(region), source) |
| ``` |
| -The log text is O8. Add `, regionReq{}` to every existing call: |
| +The log text is O8. It is logged for every successful creation, with the |
| +switch on or off and with the home named or not, exactly as spec §6.5 |
| +says: one line per creation, naming what was stored. With an unnamed home |
| +the code is the record's `""`, so the line reads `bab/app: (default)`. |
| +A claim that loses its race or is refused logs nothing, because the line |
| +comes after both checks. `TestClaimLogsOneRegionLineInEverySwitchState` |
| +pins all three states. Add `, regionReq{}` to every existing call: |
| `server.go:3626`, `apps_claim_test.go:118,134,150,165` and |
| `apicli_test.go:2504`. |
| @@ -4303,6 +4344,12 @@ Add to the response map: |
| Run: `go test ./internal/console -count=1` |
| Expected: PASS, including every existing claim test. |
| +Then the log check (codex plan-R6 1): wrap the `log.Printf` back in |
| +`if s.appRegionsOn() { … }`, and run |
| +`go test ./internal/console -run TestClaimLogsOneRegionLineInEverySwitchState -count=1`. |
| +It must FAIL on `assignment off` and `on, home unnamed` (0 lines). Revert, |
| +and re-run: PASS. |
| + |
| Then a mutation check (codex plan-R4 2): in `explicitChoices`, also accept |
| codes whose refusal is `no-key`. Run |
| `go test ./internal/console -run TestAPICreateAppRefusesBadExplicitRegions -count=1`. |
| @@ -8476,3 +8523,20 @@ the rev 5 and rev 6 trees: 17 PASS on each. |
| Rollout: `CARLOS_SECRETS_KEY` is on all four flagship boxes as of |
| 2026-10-03 (SSM `/carlos/secrets-key` in every region; config sync and |
| generation armed everywhere), so step 2 is marked done for the flagship. |
| + |
| +## Review dispositions (Astra, plan round 6, on 4df028e2 with spec 56756f09) |
| + |
| +Astra found every earlier finding resolved. One new P2. It was proved as |
| +before: a throwaway worktree at the branch base (e8406ce6), rev 6's blocks |
| +replayed and committed there as the baseline, the new test run failing |
| +against rev 6 and passing after the fix, and the changed blocks compared |
| +with the scratch files byte for byte (both matched). The scratch worktree |
| +and branch were deleted afterwards. |
| + |
| +| # | sev | finding | disposition | proof | |
| +| --- | --- | --- | --- | --- | |
| +| R6-1 | P2 | Task 12 logged spec §6.5's creation line only with assignment on | **accepted.** O8 is logged after every successful insert and lost-race check, in every switch state, naming what was stored. The failed-claim silence tests stay. | `TestClaimLogsOneRegionLineInEverySwitchState`: against rev 6, `assignment off` and `on, home unnamed` FAIL with 0 region lines; after, all three PASS with exactly one line each (`bab/on1: uk (chosen)`, `bab/off1: ie (default)`, `bab/unnamed1: (default)`) and the stored region and source as named. Wrapping the log in `if s.appRegionsOn()` again FAILs it. `TestClaimLostAfterResolutionWritesNothing` and `TestClaimHeldApexWritesNothing` still PASS | |
| + |
| +Whole-tree results on the rev 7 scratch tree: `go build ./...`, |
| +`go vet ./...` and `gofmt -l` clean; all five cross-compile targets built; |
| +`go test ./internal/console` `ok` (147.2s) and `cmd/console` `ok`. |