carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 96cd7df577f46d7f0c451885e0325308f7e471c3 parent d723301
2 files changed, +173 −6
  • docs/superpowers/plans/2026-10-02-nearest-region-default-SUMMARY.md +103 −0
  • docs/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`.