carlos / platform Public

spec 2 rev 15: H27 clarified (choose before registering; the four choosing doors; standalone vs attached sidecars; alert limit per console process); plan 2a Astra round 1 committed unedited

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev aa5b6b079bccc596990dfaa160f2bc664d27d62a parent ec91ac1
2 files changed, +53 −2
  • docs/superpowers/plans/CODEX-REVIEW-REGION-HA-2A-1.md +51 −0
  • docs/superpowers/specs/2026-10-15-region-ha-two-box-regions-design.md +2 −2
diff --git a/docs/superpowers/plans/CODEX-REVIEW-REGION-HA-2A-1.md b/docs/superpowers/plans/CODEX-REVIEW-REGION-HA-2A-1.md
new file mode 100644
index 000000000..a2a7c8e4e
--- /dev/null
+++ b/docs/superpowers/plans/CODEX-REVIEW-REGION-HA-2A-1.md
@@ -0,0 +1,51 @@
+Found **seven P2 issues** at `ec91ac11`. This was a static, read-only review against the specified HEAD and locally available amadan `origin/main` (`e963691d`). No builds or tests ran.
+
+1. **P2 — The no-capacity path leaves a registered operation behind.**
+ [Task 11’s “writes nothing” test](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-region-ha-two-box/docs/superpowers/plans/2026-10-16-region-ha-2a-core.md:4886) calls `StartCreate` directly. The real `beginClaimCreate` path first calls `RegisterStart` and `Bind`; on chooser failure it only stops renewal. That registration blocks app mutations until settlement. Recovery’s `judgeRegistration` can subsequently create a free claim to invalidate the saved observation. Thus the HTTP refusal still leaves recovery work and can cause a later claim write. Test the complete door through recovery, and resolve this against H27 while preserving the claim’s fencing guarantees.
+
+2. **P2 — H27 is missing from other chooser doors.**
+ [Task 11’s sole integration point](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-region-ha-two-box/docs/superpowers/plans/2026-10-16-region-ha-2a-core.md:5033) covers only `handleAPIInstanceCreate`. Other callers are:
+ - `handleAPIDomainAttach` and browser `handleAppDomainAttach`, through `attachClaimFirstRoute → beginClaimCreate`.
+ - `handleAPISidecarCreate`, for standalone sidecar records, through `beginClaimCreate`.
+ - `handleAPIInstanceMoveRequest`, through `selectMoveBoxes → chooseBox`.
+
+ First-route and standalone-sidecar chooser failures already produce a **generic 503**, rather than the plan’s claimed 409, but lack the capacity explanation and alert. An empty move destination produces **400**, without an alert. The standalone-sidecar path also contradicts the spec’s blanket statement that sidecars choose nothing. Cover these doors and distinguish standalone sidecars from instance-attached sidecars.
+
+3. **P2 — Earlier instance validation bypasses the new placement/refusal policy.**
+ [Task 11](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-region-ha-two-box/docs/superpowers/plans/2026-10-16-region-ha-2a-core.md:5026) leaves `apiinstances.go:1239–1266` unchanged. For an app-derived region, stale status on every box produces **400 before the chooser**, without an alert. Moreover, `regionCandidates` requires every potentially serving fresh sibling to qualify: one misconfigured sibling can reject the region despite another eligible box. Use the HA eligible set for these decisions. Add HTTP tests for both scenarios; the proposed helper tests cannot catch them.
+
+4. **P2 — Move role filtering remains active with region HA off.**
+ [Task 10, line 4459](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-region-ha-two-box/docs/superpowers/plans/2026-10-16-region-ha-2a-core.md:4459) gates the roster read on `HostClaim`, without checking `RegionHA`. After disabling HA, persisted roles still exclude destinations, and the new roster read can independently refuse a move. This violates the explicit dormant-behavior constraint. Test `HostClaim=true, RegionHA=false`; the proposed test only checks claims off.
+
+5. **P2 — Private forwarding also ignores the HA switch.**
+ [Task 8’s `dialAddress`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-region-ha-two-box/docs/superpowers/plans/2026-10-16-region-ha-2a-core.md:3057) uses private addresses whenever the published regions match. An HA-disabled reader will therefore switch to private dialing when a peer publishes `private_ip`, including during mixed configuration or rollback. That can break forwarding where private access has not been enabled. Carry the reader’s HA setting into address selection and test both settings.
+
+6. **P2 — The hourly alert limit does not survive console restart.**
+ [Task 11’s limiter](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-region-ha-two-box/docs/superpowers/plans/2026-10-16-region-ha-2a-core.md:4962) stores timestamps only in `Server` memory. A refusal, restart, and retry sends two alerts within an hour. The plan acknowledges this exception, but H27 grants none. Persist the limit or obtain an explicit ruling relaxing it; test across server reconstruction.
+
+7. **P2 — Pending copy is visible despite the claimed feature gate.**
+ [Task 1’s gate](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-region-ha-two-box/docs/superpowers/plans/2026-10-16-region-ha-2a-core.md:407) changes pending rows to “approved” for checking. Task 10 then adds unconditional CLI help and documentation. Running `carlos system help` exposes pending copy even with HA off. Require actual approval before wiring these surfaces; disabling HA does not hide them.
+
+The census instructions correctly identify Tasks 7 and 10 and require recalculation after the newer overlays. I found no additional census omission in the proposed changes by inspection. However, fixing the findings above may also change frozen units including `Server.beginClaimCreate`, `Server.attachClaimFirstRoute`, and `Server.handleAPIInstanceMoveRequest`; those need explicit census treatment.
+
+---
+
+**FIRST PROMPT:**
+Review plan 2a at `ec91ac11`, read-only, without heavy checks.
+
+**WHAT:**
+Review implementation correctness, spec coverage, census handling, and tests.
+
+**STATUS:**
+Review complete: seven P2 findings. Worktree unchanged.
+
+**NEEDED:**
+Nothing for this review.
+
+**RECOMMENDED:**
+Revise the plan and add complete request-path tests before implementation.
+
+**SESSION:**
+Safe to close; nothing started remains running.
+
+**Verdict: not ready to build.**
\ No newline at end of file
diff --git a/docs/superpowers/specs/2026-10-15-region-ha-two-box-regions-design.md b/docs/superpowers/specs/2026-10-15-region-ha-two-box-regions-design.md
index e694e0514..c5fe8d7a0 100644
--- a/docs/superpowers/specs/2026-10-15-region-ha-two-box-regions-design.md
+++ b/docs/superpowers/specs/2026-10-15-region-ha-two-box-regions-design.md
@@ -73,7 +73,7 @@ from 2026-10-15:
| H24 | **IE's current box becomes the platform box.** It's fixed, outside the group, and keeps the console, keymail, woodstar, the relay and the scanner grants. It takes no new customer instances. Its existing ones leave with spec 4's drain (H25). |
| H25 | **(2026-10-16) Spec 2 doesn't empty the old boxes.** Each region's old fixed box keeps the instances it has and takes no new ones. Those instances keep today's single-box behaviour until spec 4's drain moves them, or until spec 3's failover moves them if AWS terminates the box. Each region runs three boxes until then. |
| H26 | **(2026-10-16) Multi-box regions stay AWS-only until a provider-neutral box identity exists** (#299). Box identity, enrolment, roster validation and placement eligibility rest on AWS's signed instance document on main today. Spec 2 doesn't make that worse: its own new AWS parts are adapters (§25). |
-| H27 | **(2026-10-16) No pending creates; this supersedes H4's "waits as unplaced".** When no box qualifies, every door that chooses a box refuses with HTTP 503 and a clear message: no capacity in that region right now, try again shortly. The operator is alerted, at most once per region per hour. No create without an executor is ever written to the claim. Sidecars run on their instance's box and choose nothing, so they're unaffected. |
+| H27 | **(2026-10-16) No pending creates; this supersedes H4's "waits as unplaced".** When no box qualifies, every door that chooses a box refuses with HTTP 503 and a clear message: no capacity in that region right now, try again shortly. The operator is alerted, at most once per region per hour per console process. A console restart may send one extra alert, which is accepted rather than persisting the limit. The door chooses before it registers anything, so a refusal leaves no claim write and no recovery work. The doors that choose a box are: instance create, domain attach (first route), standalone sidecar create, and the move door's destination. A sidecar attached to an instance runs on that instance's box and chooses nothing. |
Design choices Paul approved in section review on the same day, recorded
here so they aren't reopened:
@@ -291,7 +291,7 @@ The choice is made once and recorded in the create intent, as now (H12). Recover
**Accepted limit.** `mem_free_mib` is memory free now (MemAvailable), so a box full of sleeping instances looks roomy until they wake. Counting each instance's memory limit would be more accurate, but limits aren't set everywhere. Spec 2 uses live memory, and the memory alarm and spec 4's scale-out cover overcommitment.
-**No capacity** (H27). When no box is eligible, the door refuses the request with HTTP 503 and a clear message that there's no capacity in that region right now, so the caller should try again shortly. The message goes through Paul's copy review. An operator alert fires, at most once per region per hour, so a retry loop doesn't flood it. Nothing is written to the claim, so there is nothing to recover or cancel. The eligible-capacity metric (§4) is published by a small console pass every minute.
+**No capacity** (H27). When no box is eligible, the door refuses the request with HTTP 503 and a clear message that there's no capacity in that region right now, so the caller should try again shortly. The message goes through Paul's copy review. An operator alert fires, at most once per region per hour per console process, so a retry loop doesn't flood it. The door chooses the box **before** it registers the operation, so a refusal leaves nothing registered, nothing written to the claim, and nothing for recovery to settle. This applies to every door that chooses a box (H27). The eligible-capacity metric (§4) is published by a small console pass every minute.
**Old boxes** (H25). Each region's old fixed box takes the roster role **`legacy`**; IE's takes **`platform`**. Both roles are never eligible for a new placement, a relocation target or a move target. That's a static exclusion, read from the roster by every chooser (`boxchoice.Eligible`), and it needs no settlement:
- the box's existing instances stay where they are;