carlos / platform Public

Region HA 1b-next stage B plan rev 1 (re-planned against claim Plan 11 on main) + Astra round 1 brief

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev bb95591601ea3425a9e4e0d71dead0d900d99b72 parent b250a1f
2 files changed, +2020 −0
  • docs/superpowers/plans/2026-10-10-region-ha-1b-next-stage-b.md +1995 −0
  • docs/superpowers/plans/CODEX-PLAN-REGION-HA-STAGE-B-BRIEF-1.md +25 −0

CI failed — run details

diff --git a/docs/superpowers/plans/2026-10-10-region-ha-1b-next-stage-b.md b/docs/superpowers/plans/2026-10-10-region-ha-1b-next-stage-b.md
new file mode 100644
index 000000000..9308ab565
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-10-region-ha-1b-next-stage-b.md
@@ -0,0 +1,1995 @@
+# Region HA 1b-next Stage B: Relocation Wired Into the Claim Assembly Implementation Plan (rev 1)
+
+> **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:** Bind stage A's relocation (the claim transitions, the worker, the participant, the release effects) to the box assembly as main now has it after claim Plans 5, 6b, 8/8b, 9 and 11 and moves Plans 1 to 5, so that with `CARLOS_HOST_CLAIM`, `CARLOS_MOVES_PHASE1` and `CARLOS_REGION_HA` all on a box releases, builds, installs, probes and reconciles a relocation; and keep every flag off, so nothing changes until the rollout checklist is walked.
+
+**Architecture:** Stage B replaces the gated Tasks 8a, 8b, 8c, 9 and 10 of `2026-10-07-region-ha-1b-next-relocation.md` (rev 19, lines 11793 to 13557) and absorbs its Task 11. Every task builds on main's types: `ClaimOwnerDispatch.Relocation`, `ClaimRelocationOwners`, `RegisterProductionClaimRelocationSource`, `StopWriters`, `ClaimAssembly.WritersStopped`, `CollectWritersStopped`, `RelocationReleaseReceipts`, `QueueRelocationRelease`, `Policy.WithRefusals` and `PolicyRefusal`. R8's `Extend` hook is gone: the policy owner is `ClaimRelocationOwners.Policy`, refusals go through `WithRefusals`, and the probe runs through main's `OperationAttempts` dispatch and the activator's `ProbeWake`. Seven tasks: 8a the release providers, 8b the remote build of P, 8c the local install with its held row and the probe's inputs, 8d the relocation owner set and its registration, 9 reconcile under a relocating claim, 10 the passes behind the flags with the rollout checklist, 11 the merge gate. Every open ask on amadan #236 is a dependency with a named fallback, never an assumption.
+
+**Tech Stack:** Go 1.26, module `github.com/carlosframework/platform`; `release.Store`; `hostclaim`, `hostactors`, `hostfence`, `hostguard`, `hostexec`, `hostbox`, `hostrelocate`, `hostfiles`, `genselect`, `moves`, `lease`, `activator`, `registry`, `appconfig`, `release`.
+
+**Spec:** `docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md` (rev 5.4 with §24): §5.3, §5.8, §6, §8. The 1b-next plan's rulings 1 to 27 stand; this plan's rulings continue the numbering at 28. Claim Plan 11 (`docs/superpowers/plans/2026-10-09-host-lifecycle-claim-11-region-ha-integration.md` on `origin/design-replica-ownership`, "Region HA integration and rollout", items 1 to 7) and the #236 answers (R1 to R10, C1 to C5, M1 to M3, J2, J3) are the contracts. Every signature below was read on this branch at b250a1fc (main merged at dc333fb5); file:line cites are to this branch.
+
+---
+
+## What this plan replaces, and what it keeps
+
+| 1b-next section | Here |
+|---|---|
+| Stage B re-plan table (L11793) | "Obligations" below: each met, re-homed or retired |
+| Task 8a | Task 8a (rewritten: main's `WritersStopped`, `ReleaseObligation`, `CollectClaimStop`) |
+| Task 8b | Task 8b (rewritten against `moves.BuildGeneration` as merged) |
+| Task 8c | Tasks 8c and 8d (the local install now mints the held row; the probe goes through `OperationAttempts` and `ProbeWake`, not a bespoke `Probe` method) |
+| Task 9 | Task 9 (reconcile never activates under a relocating claim; hold-only and the completion release) |
+| Task 10 | Task 10 (the source registration, the passes, the console chooser, the rollout checklist) |
+| Task 11 | Task 11, unchanged in substance |
+
+Stage A (Tasks 1 to 7, 9a), the merge of main (dc333fb5 and its fix round), R9 (ccce3672) and J3 (2ced0c5d, b250a1fc) are done and are not reopened.
+
+## Obligations from the 1b-next re-plan table
+
+| Obligation (round) | Disposition |
+|---|---|
+| Release providers bound to the real attempt: identity, producers, collect, writers-only stop, descriptors recorded at open (1, 2) | **Met, Task 8a.** `localStopIdentity` (claimwakestop.go:79), `ClaimWakeAssembly.Producers` (claimwakeassembly.go:98), `CollectClaimStop` (claimstop_transport.go:121), `ClaimAssembly.WritersStopped` (claimstop_writers.go:458) which runs `StopWriters` (claimwakewriters.go:118) and root's observation with the hints `recordReplicatorHints` took at open (claimwakewriters.go:228). |
+| Every stage A provider a test stubs is tested with its production value (10) | **Met across 8a to 10.** Each task names the production value and the test over real evidence; the pinner's `SelectDB` over a versioned fixture is Task 10's `TestRelocationPinnerSelectsOverTheVersionedBucket`. |
+| A production probe launches only through the reservation-bearing isolated root launch; refused at `Preflight` without it; passes off until then (12) | **Met by main, re-homed to Task 10's checklist.** R10 is built (Plan 11 Task D, `isolated-unit-v2`; `ClaimRoleAssembly.Preflight`, claimroleassembly.go:60, refuses with `activator.ErrClaimIsolationUnavailable`). Every probe goes through `ClaimWakeAssembly.Build`'s `Prerequisites` (claimwakeassembly.go:211), which calls `Preflight` first. Task 10 never starts a pass on a box whose preflight refuses: `TestRelocatePassStaysOffWhileThePreflightRefuses`. |
+| P's prepared files manifest create-only at sequence 0, bound to {location, P, op, stamp}; the first admission takes it by digest (14) | **Met, Tasks 8b and 8c.** `CopyPinnedManifest` (pin.go:355) writes sequence 0; `PreparedFromInstallManifest` reports it; `RelocationWakeInputs.AdmissionEvidence` (8d) refuses a baseline whose files manifest digest is not `prepared_authority.files_manifest_sha256`. |
+| `EnsureLocal`'s production body: verify under `install_stamp`, restore a retired install, never reinstall an opened P (14) | **Met, Task 8c.** `VerifyLocal` and `Install` providers; `EnsureLocal` is main's (relocationeffects.go:867). |
+| A source's ordinary cold wake in `starting`/`releasing` dispatches while its fence is open (14) | **Retired.** Claim Plan 11 R1 as accepted: `WakePhase` admits no ordinary relocating wake (hostbox/wakepolicy.go:36-42), and the merge aligned `RelocationRow` to it. A source in `starting` keeps serving on the session it already has; nothing new is admitted. |
+| `Receipt`'s production body; a missing receipt after P opened marks stuck (15) | **Met, Task 8c.** `relocationReceipt` reads `<DataDir>/relocate/<host>/<op>/install.json`; `VerifyReceipt` is main's (relocationeffects.go:900). |
+| Every cold admission writes its files manifest before the app starts (17) | **Re-homed to the claim: main's C8 provider.** `ClaimFilesManifest.Establish` (claimowners.go, `Materialize`) runs before the app executes, on every kind through the dispatcher. Region HA adds no second copy; Task 8d's `Materialize` delegates to it. |
+| The producers' drain is idempotent (7) | **Met by main; pinned in Task 8a** through the resumed drain (`releaseDrain.resumed`). |
+| The complete writers-stopped scan is root's, authenticated (2) | **Met by main.** `ClaimStopCoordinator.WritersStopped` (claimstop_writers.go:338) and `LinuxStopKernel.VerifyWritersStopped` (claimstop_writers_linux.go:54). |
+| `Region` on every stop request; `Recover` after a restart (2, 3, 4) | **Met, Task 8a.** `releaseStopRequest` already sets `Region` (relocationeffects.go:741); `Collect` sets `StopRequest.Recover` (claimstop.go:53) for a `RecoveryReleaseDrain`. |
+| The controller's cleanup scoped; `Clear` is `ClearStale` (3, 4) | **Met, Task 8a** (`relocationCleanup`, `execctl.ClearStale` as relocationeffects.go:932 already does). |
+| Clearing rests on a cleanup that succeeded now; the journal under `<DataDir>/relocate/participations` (5) | **Met by main** (`RelocationControllers.Finish`, relocationeffects.go:217-302); Task 8a binds `Cleanup` and `Clear` and tests the restart. |
+| The env file restored exactly; `ENOENT` means none (5) | **Met, Task 8c** (`ReadPlatform` on `hostfiles.EnvItem`; `StagePlatformDelete` on the flush is main's, relocationflush.go:221). |
+| P built under the location lease, authority re-checked at every write (1, 3, 4) | **Met, Task 8b.** |
+| The local install under `install_stamp`; destination inventoried first; held staging through root's relay; never remade after P's admission (1, 4) | **Met, Task 8c**, with M3 as a dependency (below). |
+| The probe: P opened once, later attempts ordinary, `"probe"` work, one attempt per `BeginProbeAttempt` (1, 2, 3, 4) | **Met, Tasks 8c and 8d.** The action is main's `claimSpawnAction` (claimoperations.go, `"probe"` for relocating); the attempt count is the claim's (`BeginProbeAttempt`, relocation.go:941) and the start count the activator's (`probeMaxStarts`). |
+| Reconcile through 6b's adapter: conditional holds, the completed hold cleared, nothing minted for a box with no role (1, 2, 3, 4) | **Met, Task 9**, with ruling 31: reconcile never activates under a relocating claim; the target's row is minted by its install (Task 8c). |
+| Production wiring flag-gated; the passes with the sweep; create's chooser only with HA on (2, 3, 4) | **Met, Task 10.** `applyRegionHA` and R8 are gone; the owners are registered through `RegisterProductionClaimRelocationSource` (claimrelocation.go:32) and opened only with `CARLOS_REGION_HA` on (claimrelocation.go:53). |
+
+## Ledger notes for stage B
+
+| Note | Disposition |
+|---|---|
+| Task 10 wires `ConfigureClaimRuntime` and the `RegionChooser` | Task 10 steps 5 and 3. |
+| 8a sets `Recover`; `ReleasePositions` is `Record`/`Recorded` | Task 8a; the interface is relocationeffects.go:470-473. |
+| 6b's adapter limits `RelocationRow` to relocating claims | **Retired: done.** `Reconciler.relocates` (hostbox/reconcile.go:53) scopes every row decision to `KindRelocating`. |
+| IAM allows writes to the evidence prefix | **Rollout checklist (Task 10).** Root writes `control/hosts/<host>/evidence/<op>/<box>/<id>.json` and `.sig` (claimstop.go:490-494). Not checkable in this repository. |
+| Root's scan needs the host PID namespace and `CAP_SYS_PTRACE` | **Rollout checklist.** `carlos-instance-control.service` runs as root with no `CapabilityBoundingSet`, no `PrivatePIDs`, no `ProtectProc` (internal/host/units/carlos-instance-control.service), so it has both today; the dev-box check proves it. |
+| `PinAfterRelease` and roster-removed refusals have no time bound | **Retired with reason (ruling 36):** no bound; the pass alerts once per host and cause. A refusal is visible and safe; a bound would move a live host's operation to `stuck` on a transient read error. |
+| The replicator descriptor is matched by PID, number and path | **Retired: main's shape.** Root matches the authenticated peer's descriptors by `(fd, device, inode)` (`stopevidence.DescriptorHint`, holders.go:26) against its own `/proc` scan. The residual, the replicator closing a descriptor and other agent code reopening the same file at the same number, stands as accepted: no agent code but the replicator opens a tenant database. Raised if the live witness (dependency W1) shows otherwise. |
+
+## Review findings carried in
+
+| Finding | Disposition |
+|---|---|
+| Merge review 5: an owed `relocation-release` has no way out and wedges the host | Dependency C1 with its fallback; ruling 33. |
+| Merge review 8: release order versus Plan 11 item 4 | Ruling 28: Plan 11's order. Task 8a restructures `Release`. |
+| Merge review 8: Litestream's lazily opened descriptors | Dependency W1 (the live witness); ruling 29. |
+| R9-2: `Route.Version` versus `GoodVersion`; the pinner's principal | Ruling 30: the channel pointer, read from the control bucket; Task 10 wires `StorePinner.Release`. |
+| J3-3: a conversion waiting for a target, unbounded | Ruling 35; Task 10 step 6 (alert; bound into stuck after `ReleaseDeadline` from the fence). |
+| `CommittedLandingGuard`'s production implementation | Task 10 step 6 (`CommittedLandingRecords` over `provision.GetRecord`). |
+| Task 8c reads `install_release` and fills `HeldStageRequest.Stamp` | Task 8c; `Stamp` is dependency M3. |
+
+## Open dependencies
+
+Each is modelled as a dependency with a fallback. None is assumed.
+
+| # | To | Ask | Fallback until answered | Where |
+|---|---|---|---|---|
+| C1 | claim | A discharge for a fenced or stuck source's `relocation-release`, an op-scoped ID or a fence rule, and release authority for a `probe-failed` target | **Drains stay off** (spec §8 item 4 already holds them). Failover never queues the obligation (`Release` runs only for a live executor in `releasing`/`probe-failed`; `QueueRelocationRelease`, relocationrelease.go:7, refuses `probe-failed`), so a failover-only region cannot wedge. A failed probe on a failover's target goes `stuck` at the deadline for an operator. Task 10's checklist marks C1 as the gate for `reason: drain`. | ruling 33, Task 10 |
+| C2 | claim | Verify-only `RelocationReleaseReceipts.Verify` | `Complete`'s replay form (claimrelocationrelease.go:75-82) writes nothing; `ReleaseObligation.Verify` (relocationreleases.go:143) keeps using it; its no-write test stands. | none |
+| C3 | claim | Four receipt checks in `Complete` | None needed: parity only. | none |
+| C4 | claim | `PredecessorCertified` as the relocation release's certification | Kept as built (`relocationProbeOpening`, generation.go:307). If the claim adds a `released` coverage value instead, Task 8d's `Select` sets `Coverage` from the same rule and `PredecessorCertified` false: one function changes. | Task 8d |
+| C5 | claim | The claim's kind in `AdmitWriter`'s op match | `RelocationWakeInputs.Select` refuses any kind but relocating, and the dispatcher routes by kind (claimowners.go:208). | none |
+| M1 | moves | Close the move record after a converted landing (`released_move`) | Region HA's half is built here (Task 9 step 5): `CompleteRelocation` journals `released_move` when `conversion == committed-landing`. Until moves relaxes `closeMove`, a converted host cannot move again: operator path. | Task 9 |
+| M2 | moves | `sourceOf` accepting a converted claim | Nothing to build here. The source's `park-local` waits; the row is held (`RowRetiring`). | none |
+| M3 | moves | The held-stage relay's relocating branch, with `Stamp` on `HeldStageRequest` | Task 8c writes the request with `Version` = `op.journal.install_release` and today's fields; `Stamp` is added the day the field lands. Until the branch exists, `heldStageClaim` refuses the request as stale and removes it; the install re-requests on every pass and waits in `installed`. **Activation gate:** the dev-box check must show the relay serving a relocating request. | Task 8c, Task 10 |
+| J2 | moves | Plan 5 Task 9's park arming with the files live set | Not stage B's. Task 11's joint round waits for it. | Task 11 |
+| W1 | this plan | The live Litestream witness for lazily opened descriptors (Plan 11 item 4) | If root's observation refuses a hint that misses a descriptor the replicator opened after `Replicate`, the drain fails closed and the operation goes `stuck` at `ReleaseDeadline`; failover is unaffected. Then a claim ask: widen the hint to the agent's own descriptors on the database paths at `StopWriters` time (ruling 29). | Task 10 checklist |
+| W2 | this plan | Root's writers-stopped scan on the deployed unit (PID namespace, ptrace) | Fails closed; same cost as W1. | Task 10 checklist |
+| W3 | infrastructure | The box role may `PutObject` create-only under `control/hosts/*/evidence/` and `control/relocations/` | Root's receipt write fails, the release fails, the operation goes `stuck`; a pin write fails, the worker retries. | Task 10 checklist |
+
+## Rulings
+
+28. **The release runs in Plan 11's order** (merge review 8; Plan 11 item 4): writers stopped and observed, the database and files flushed and the positions recorded at the claim as observed; then the obligation queued (`ReleaseObligation.Queue`); then root's full stop with the producers' own drain alone; then `Complete`. Root's `WritersStopped` needs no obligation (`StopAuthorityMatches`, hostguard/fence.go:266-278: a relocating target with an empty obligation id needs only `op.exec == box`), so nothing is asked of root inside its own fence, and the drain callback never makes a root request. The writers are stopped before the claim owes the stop. *Cost if wrong:* if `Queue` then refuses (the operation moved), the source's writers are stopped for an operation that is `releasing` anyway; the next pass retries `Queue` with `StopWriters` answering its cached witness. A stop whose operation goes `stuck` leaves the host down on the source until the operator resolves it, as before.
+29. **The replicator hint is main's; the live witness decides whether it is complete** (W1). This plan changes no hint. If the witness shows descriptors opened after `Replicate`, the ask is to re-take the hint from `/proc/self` at `StopWriters` time, once the containment is empty: every descriptor the agent then holds on the database paths is the replicator's, because no other agent code opens a tenant database. *Cost if wrong:* a drain fails closed until the hint is widened.
+30. **The pinned release is the channel pointer's current version** (R9-2): `release.Channel(ctx, store, pub, holder.Account, holder.App, record.Channel).Version` (release.go:1220), the deployment's authority for what a host runs, read from the control bucket by the worker. No box's registry row is read: `Route.Version` and `GoodVersion` (registry.go:77-87) are box-local, and a failover's source is dead. Bake holds pace boxes; they are not the authority. *Cost if wrong:* a relocation during a bad deploy installs the bad build; the probe's three attempts catch it, the operation goes `probe-failed`, then nested or `stuck`, never serving a failing build.
+31. **Reconcile never activates under a relocating claim.** Plan 11's policy admits only `"probe"` for relocating (hostbox/wakepolicy.go:187-203) and `checkOn` refuses before any write (reconcile.go:147-156). So the target's held row is minted by the target's local install under its own guard at `install_stamp` (Task 8c, `ReconcileAdapter.MintHeld`), every `RowHeld` decision is hold-only, and `RowReady` for the source in `starting`/`releasing` is "leave the row as it is". *Cost if wrong:* the row's authority is the install's stamp rather than a reconcile activation; both are the same stamp.
+32. **A relocation ends with one cold restart on the new box.** `CompleteRelocation` (relocation.go:1003) goes straight to kind `serving`; Plan 11 item 2 says the kind change ends the probe's session and requires a separate serving wake. The target releases its row's hold once the claim serves on it (Task 9 step 4); the activator retires the probe entry whose row is no longer held, main's stop ends the session with a receipt, and the next request wakes the host cold. The `relocating/serving` phase (hostclaim/sessionstep.go:39) is not used here. *Cost if wrong:* a few seconds' cut per relocation, which a drain already pays; the zero-downtime variant (phase `serving` with the probe's session kept, the kind change at the next idle) is a follow-up after Task 11.
+33. **Drains wait for C1; failovers do not.** With C1 open, `reason: drain` and `rebalance` stay off (spec §8 item 4). A failover never queues `relocation-release`. *Cost if wrong:* none for failover; a drain started before C1 is answered can wedge its host's claim (merge review 5c).
+34. **The local install's writes run under `Guard.With` at `install_stamp` with the install's own claim check, not through a `WorkSource` selection.** Plan 11's policy gives relocating no `install` or `held-config` action, so `RenderHeldUnder` (appconfig/renderheld.go:67) is given a pass-through `hostwrite.Work` inside that `With`. *Cost if wrong:* the claim session may want a typed `install` action for relocating; then the pass-through is replaced by `SelectAt(host, "install", captured)`, one line.
+35. **A conversion with no target is bounded like a dead target with no spare** (J3-3): `ReleaseDeadline` after the fence's `observed_at`, the worker marks the operation `stuck` with the chooser's reason, and the pass alerts once per host before that. *Cost if wrong:* a region with no spare for ten minutes gets an operator instead of a later automatic start.
+36. **Refusals in `pinning`, `installing` and `probing` stay unbounded; the pass alerts once.** The worker's `refused` bounds only `releasing` and `probe-failed` (worker.go). A roster-removed source elsewhere refuses visibly until an operator acts. *Cost if wrong:* an operator instead of a timer.
+37. **The owner set is complete or nothing.** `claimRelocationC1` (claimowners.go:87-95) refuses a partial set with `hostwrite.ErrUnavailable`, which refuses the whole assembly. Task 8d's `Open` returns every field or an error; Task 10 registers nothing unless `CARLOS_REGION_HA` is on. *Cost if wrong:* a box with the flag on and a bad source refuses every claim wake at startup, which is loud and safe.
+38. **The target's held row has status `relocating`.** `registry.HoldStatuses` (registry.go:1164) gains the entry; the edge, ticks and `ProbeWake`'s `probeHeld` (probewake.go:238) treat it as any hold. *Cost if wrong:* one map entry; `"landing"` would have lied about the operation.
+39. **Refusals registered through `WithRefusals` are two:** a relocating selection whose captured stamp is not the holder's (a stale guard record), and any selection on a host whose claim lists this box in `lost`. They can only refuse. *Cost if wrong:* a redundant check.
+40. **L21's platform side is its own plan, not stage B** (below).
+41. **The relocation's attempt journal lives under `<DataDir>/relocate/<host>/<op>/attempts.json`**, written whole and renamed, one file per operation, modelled on `movesProbeAttempts` (movesprobeattempts.go:57) without the record. The claim's `probe_attempts.<box>` is the record attempt `k`; the journal binds each admitted session to one `k`. *Cost if wrong:* a second attempt journal shape beside moves'; both answer the same `OperationAttempts` contract.
+
+## L21: where the platform-provisioned `CARLOS_INSTANCE_KEY` belongs
+
+**Recommendation: its own plan, after rastrillo/rastrillo #66 agrees the contract.** Reasons: it touches create (`appconfig.ApplyInstance`, instances.go:371, the overlay `apps/<acct>/<app>/instances/<host>.json`), the render path (`<host>.config.env`, syncexec.go), a forced publish for every existing host and a rollout note of its own; none of its code is a relocation contract; stage B is correct without it because the pinner's refusal (ruling 25, `ErrFilesUnevidenced`, pin.go:209) is the backstop that keeps a key-less nonempty database from being relocated empty; and the app side is not agreed, so the platform side cannot be tested end to end. Stage B's cost of leaving it out: a host whose app minted `instance.key` after its files manifest and before the next publish goes `stuck` on relocation instead of relocating. That is the spec's ruling 25 behaviour. The L21 plan should land before region HA is enabled on a region with such apps (Task 10's checklist names it).
+
+## Global Constraints
+
+- **Dormant.** Nothing changes on a box or console where `CARLOS_REGION_HA` is unset or not true (`regionHAEnabled`, boxrecord.go:25). The relocation owners open only with it on (claimrelocation.go:53); the passes start only with it and `CARLOS_HOST_CLAIM` on; the console's chooser only with `Config.RegionHA`.
+- **Activation gate (claim §1.1, L18).** Relocation does not merge until Task 11's joint round passes on the two-box model.
+- **One implementation per concept; main's wins.** No second scanner, receipt verifier, roster reader, fence writer, inventory, retirement or marker publisher. The probe is main's attempt path; the attempt journal is region HA's only because `OperationAttempts` is per kind.
+- **Absence is never evidence.** A fence, receipt, pin, manifest or inventory that cannot be read is an error.
+- **Exact values** as the 1b-next plan: `FailoverPinDelay` 30 s, `ProbeDeadline` and `ReleaseDeadline` 10 min, `MaxTargetReplacements` 2, `MaxProbeAttempts` 3, obligation `relocation-release`, pin key `control/relocations/<host>/<op>/pin-<gen>.json`, journal `<DataDir>/relocate/`. New here: hold status `relocating`; the attempt journal `attempts.json`; the install journal `install.json`.
+- **No new module dependencies. Do not add a method to `release.Store`.** Linux-only code in `*_linux.go` with a `!linux` stub; CI cross-compiles darwin, windows and linux/arm64.
+- **Copy.** No user-facing copy. Log lines are operator-facing in the surrounding style.
+- **Go environment.** `GOFLAGS=-buildvcs=false`; `GOCACHE=$TMPDIR/gocache` only if the sandbox refuses the default; socket tests `TMPDIR=/tmp/k`; tests niced and targeted (`nice -n 19 go test ./pkg -run X -count=1`); the `internal/host` package only outside the sandbox and once per task. No sweeps.
+- **Gate before each commit:** `go build ./... && go vet ./<pkgs>/ && gofmt -l <dirs>` (empty), the task's tests, then `GOOS=darwin go build ./... && GOOS=windows go build ./...`.
+- Commit messages: lowercase `area:` prefix, a sentence stating the behaviour, a blank line, `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`. Branch `region-ha-1b-next-build`. Merging is Paul's, through `amadan branch merge`, after Task 11.
+
+## Review Focus
+
+1. **A probe admission certifying the wrong predecessor.** `PredecessorCertified` must be set only on P's opening after this operation's verified release. Task 8d `TestRelocationSelectCertifiesOnlyPsOpeningAfterItsRelease`.
+2. **The target's row minted or kept for an installation that is not this box's.** The row's ownership digest must be the install stamp's. Task 8c `TestMintHeldBindsTheRowToTheInstallStamp`; Task 9 `TestReconcileLeavesARelocatingTargetsRowToItsInstall`.
+3. **A release returned from a writers-stopped witness alone.** Only root's receipt discharges. Task 8a `TestReleaseNeverReturnsBeforeRootsReceipt`.
+4. **A later probe attempt reopening P.** Task 8d `TestRelocationSelectNeverReopensP`.
+5. **The post-relocation serving reconcile on the target at a later stamp.** After the probe's session ends and a cold serving wake takes a new stamp, reconcile's adapter must find a coherent inventory. Task 9 `TestReconcileAdapterClearsTheHoldAfterCompletion` runs the real adapter and census through it; if the census refuses, that is a claim ask (the admission carries the install's entries forward), not a region HA workaround.
+
+## File Structure
+
+| Path | Responsibility | Task |
+|---|---|---|
+| `internal/host/relocationassembly.go` | `RelocationAssemblyConfig`; `(ClaimAssembly) RelocationEffects`; the release providers' production bodies; `relocationCleanup` | 8a |
+| `internal/host/relocationeffects.go` | `Release` in ruling 28's order; the drain is the producers' alone | 8a |
+| `internal/host/relocationbuild.go` | P's build under the lease; `PreparedFromInstallManifest` | 8b |
+| `internal/host/relocationinstall.go` | The local install under `install_stamp`; the held row; `VerifyLocal`; `Receipt` | 8c |
+| `internal/hostbox/reconcile_adapter.go` | `ownedRow` extracted; `MintHeld` | 8c |
+| `internal/provision/reconcile.go` | `RouteFor` exported | 8c |
+| `internal/registry/registry.go` | `HoldStatuses["relocating"]` | 8c |
+| `internal/host/relocationwake.go` | `RelocationWakeInputs`: `Select`, `Materialize`, `EmptyAuthority`, `Publish`, `PublishFenced`, `AdmissionEvidence`, `PublicationEvidence` | 8d |
+| `internal/host/relocationattempts.go` | `relocationProbeAttempts`, region HA's `OperationAttempts` | 8d |
+| `internal/host/relocationowners.go` | `RelocationSource` (`ClaimProductionRelocationSource`): hooks, policy, refusals, `relocationAdmit` over `ProbeWake` | 8d |
+| `internal/hostrelocate/participant.go` | `CertifiedFor` exported | 8d |
+| `internal/hostbox/reconcile.go` | Every `RowHeld` hold-only under relocating | 9 |
+| `internal/host/relocationeffects.go` | `Finish` releases the completed operation's hold on the serving box | 9 |
+| `internal/hostclaim/relocation.go` | `released_move` on a converted landing's completion (M1) | 9 |
+| `internal/host/relocatepass.go` | The passes; the worker's and participant's production providers; the source registration | 10 |
+| `internal/host/relocationlanding.go` | `CommittedLandingRecords`; the fenced-landing conversion in the pass | 10 |
+| `internal/hostrelocate/worker.go` | The no-target bound (ruling 35) | 10 |
+| `internal/edgesfile/snapshot.go` | `LiveSnapshot` (moved from console) | 10 |
+| `internal/console/claim_runtime.go`, `boxchoice.go` | `Executors` on the create door; console reads `edgesfile.LiveSnapshot` | 10 |
+| `internal/host/agent.go` | Start the pass; register the source | 10 |
+
+---
+
+### Task 8a: The release providers on the claim assembly
+
+**Files:**
+- Create: `internal/host/relocationassembly.go`, `internal/host/relocationassembly_test.go`
+- Modify: `internal/host/relocationeffects.go:643-833` (`Release`, `releaseDrain`)
+
+**Interfaces:**
+- Consumes: `ClaimAssembly` (claimassembly.go:93-126: `Claims`, `Guard`, `Wake`, `Reconcile`, `RetirementAuthority`); `localStopIdentity` (claimwakestop.go:79); `ClaimWakeAssembly.Producers` (claimwakeassembly.go:98) and `get` (667); `claimWakeAttempt.rep` (137); `ClaimAssembly.WritersStopped` (claimstop_writers.go:458); `CollectClaimStop` (claimstop_transport.go:121); `StopRequest.Recover` (claimstop.go:53); `ReleaseObligation` (relocationreleases.go:94); `ReleaseFlushFiles` (relocationflush.go:176); `ReleasePositionsFile` (252); `RelocationControllers` (relocationeffects.go:61); `OpenRelocationController` (938); `execctl.Config` (hostexec/controller.go:24-43), `execctl.ClearStale`; `hostactors.Directory.Lookup` (roster.go:175) as `hostquarantine.ActorDirectory`; `genselect.Select` (select.go:202), `Plan.MaxTXID`/`MaxLevel0TXID` (86-95); `lease.Replicator.SyncAndWait` (lease.go:391).
+- Produces: `RelocationAssemblyConfig`; `(ClaimAssembly) RelocationEffects(cfg) *RelocationEffects` with `BuildP`, `Install`, `VerifyLocal`, `Receipt`, `Admit` nil until Tasks 8b to 8d set them (the methods refuse nil providers: relocationeffects.go:849-920).
+
+- [ ] **Step 1: Write the failing tests**
+
+```go
+package host
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+)
+
+// The flush refuses a host whose admitted attempt has no replicator:
+// there is nothing to sync, and N is never guessed.
+func TestRelocationFlushRefusesWithoutTheAdmittedReplicator(t *testing.T) {
+ a := ClaimAssembly{Wake: &ClaimWakeAssembly{}}
+ _, err := a.relocationFlushDB(RelocationAssemblyConfig{})(context.Background(),
+ hostclaim.Observed{Host: "shop.example", Claim: &hostclaim.Claim{Host: "shop.example", Holder: &hostclaim.Holder{Session: 3}}})
+ if !errors.Is(err, ErrClaimStopUnavailable) {
+ t.Fatalf("got %v", err)
+ }
+}
+
+// Every provider Task 8a owns is bound under the agent's lifetime.
+func TestRelocationEffectsAreBoundToTheAssembly(t *testing.T) {
+ a := ClaimAssembly{Wake: &ClaimWakeAssembly{}}
+ e := a.RelocationEffects(RelocationAssemblyConfig{Lifetime: context.Background(), ReleaseDir: t.TempDir(), DataDir: t.TempDir()})
+ if e.Lifetime == nil || e.Controllers == nil || e.Identity == nil || e.Controller == nil || e.Producers == nil || e.Collect == nil ||
+ e.WritersStopped == nil || e.Releases == nil || e.FlushDB == nil || e.FlushFiles == nil || e.Positions == nil {
+ t.Fatalf("an unbound provider: %+v", e)
+ }
+ if e.Controllers.Journal == "" || e.Controllers.Cleanup == nil || e.Controllers.Clear == nil {
+ t.Fatal("the controllers are not journaled or cannot be cleaned")
+ }
+}
+
+// A recovery drain is submitted as a recovery request (6b's Recover),
+// under the recorded owner's identity, never as a live stop.
+func TestRelocationCollectMarksARecoveryDrain(t *testing.T) {
+ var seen StopRequest
+ a := ClaimAssembly{Wake: &ClaimWakeAssembly{}, collectStop: func(_ context.Context, r StopRequest, _ StopDrainer) (StopResult, error) { seen = r; return StopResult{}, nil }}
+ e := a.RelocationEffects(RelocationAssemblyConfig{Lifetime: context.Background()})
+ if _, err := e.Collect(context.Background(), StopRequest{Host: "shop.example"}, RecoveryReleaseDrain{}); err != nil {
+ t.Fatal(err)
+ }
+ if !seen.Recover {
+ t.Fatal("a RecoveryReleaseDrain was submitted as a live stop")
+ }
+ if _, err := e.Collect(context.Background(), StopRequest{Host: "shop.example"}, &releaseDrain{}); err != nil || seen.Recover {
+ t.Fatalf("a live drain was marked recovery: %v", err)
+ }
+}
+```
+
+Three more tests go in `relocationeffects_test.go`, against the restructured `Release` (step 3), using the file's existing fixture (`rootCollect`, main's `ReleaseObligation` over a dir store, a recording `WritersStopped`, a `ReleasePositionsFile`):
+
+- `TestReleaseStopsWritersAndFlushesBeforeItQueues`: the recorded call order is `writers-stopped, flush-db, flush-files, record, queue, collect(drain), complete`; the claim read by `WritersStopped` has the pre-queue ETag; `rootCollect` sees the post-queue ETag and the obligation owed; the drain it is handed is the resumed form (`DrainClaim` calls only the producers' drain).
+- `TestReleaseNeverReturnsBeforeRootsReceipt` (Review Focus 3): a `rootCollect` that refuses leaves the release unreturned and the positions recorded; the next call takes the resumed path and asks root again, flushing nothing twice.
+- `TestReleaseAfterARestartRecoversTheRecordedOwner`: the existing "same ids, new process" case is kept; the recovery request still names the recorded boot and controller.
+
+- [ ] **Step 2: Run them to see them fail**
+
+Run: `nice -n 19 go test ./internal/host/ -run 'RelocationFlush|RelocationEffectsAreBound|RelocationCollect|TestRelease' -count=1`
+Expected: FAIL to compile: `undefined: RelocationAssemblyConfig`.
+
+- [ ] **Step 3: `Release` in ruling 28's order**
+
+In `internal/host/relocationeffects.go`, replace the body of `Release` from the `Queue` call to the end, and `releaseDrain.DrainClaim`:
+
+```go
+ id, err := e.Identity(ctx)
+ if err != nil {
+ return hostclaim.Release{}, err
+ }
+ if id.PID <= 0 || id.ProcessStart == "" {
+ return hostclaim.Release{}, ErrClaimStopUnavailable
+ }
+ if key, digest := obs.Claim.Op.Journal["release_stop_receipt"], obs.Claim.Op.Journal["release_stop_sha256"]; key != "" || digest != "" {
+ return e.replayDischarged(ctx, obs, id, key, digest)
+ }
+ h := obs.Claim.Holder
+ req := releaseStopRequest(obs, id)
+ recorded, flushed, err := e.Positions.Recorded(ctx, req)
+ if err != nil {
+ return hostclaim.Release{}, err
+ }
+ if !flushed {
+ // Steps 1 to 3 at the claim as observed, before it owes anything
+ // (ruling 28, Plan 11 item 4): writers stopped and observed by
+ // root, the database at N, the files at M, the positions durable.
+ drained, seen, err := e.WritersStopped(ctx, obs)
+ if err != nil {
+ return hostclaim.Release{}, err
+ }
+ if err := drained.For(obs); err != nil {
+ return hostclaim.Release{}, err
+ }
+ if seen.Host != obs.Host || seen.Incarnation != h.Incarnation || seen.InstallationStamp != h.Stamp || seen.WriterSession != h.Session ||
+ seen.Box != id.Actor.Box || seen.Enrollment != id.Actor.Enrollment || seen.CheckedAt.IsZero() {
+ return hostclaim.Release{}, fmt.Errorf("%w: writers-stopped evidence names another session", ErrClaimStopIdentity)
+ }
+ txid, err := e.FlushDB(ctx, obs)
+ if err != nil {
+ return hostclaim.Release{}, err
+ }
+ seq, err := e.FlushFiles(ctx, obs)
+ if err != nil {
+ return hostclaim.Release{}, err
+ }
+ recorded = ReleasePosition{Request: req, PID: id.PID, ProcessStart: id.ProcessStart, TXID: txid, Seq: seq}
+ if err := e.Positions.Record(ctx, recorded); err != nil {
+ return hostclaim.Release{}, err
+ }
+ }
+ // Root fences a relocation-release stop only while the claim owes it
+ // (hostguard.StopAuthorityMatches): queued now, at the flushed claim.
+ if obs, err = e.Releases.Queue(ctx, obs); err != nil {
+ return hostclaim.Release{}, err
+ }
+ req = releaseStopRequest(obs, id)
+ req.ExpectedDirectoryRevision = recorded.Request.ExpectedDirectoryRevision
+ var d StopDrainer = RecoveryReleaseDrain{}
+ if recorded.owner().local(id) {
+ inner, err := e.Producers(ctx, obs)
+ if err != nil {
+ return hostclaim.Release{}, err
+ }
+ d = &releaseDrain{inner: inner}
+ } else {
+ req.BootID, req.ControllerID = recorded.Request.BootID, recorded.Request.ControllerID
+ }
+ result, err := e.Collect(ctx, req, d)
+ if err != nil {
+ return hostclaim.Release{}, err
+ }
+ if _, err := e.Releases.Complete(ctx, obs, result.Key, result.Digest); err != nil {
+ return hostclaim.Release{}, err
+ }
+ return hostclaim.Release{ReleasedBy: id.Actor.Box, Enrollment: id.Actor.Enrollment, Generation: h.Generation, Session: h.Session,
+ FinalTXID: recorded.TXID, ManifestSeq: recorded.Seq, Receipt: result.Key, ReceiptSHA256: result.Digest}, nil
+}
+
+// releaseDrain is the producers' own drain: steps 1 to 3 ran before the
+// obligation was queued (ruling 28), so root's collection closes the
+// producers and acknowledges, nothing more.
+type releaseDrain struct{ inner StopDrainer }
+
+func (d *releaseDrain) DrainClaim(ctx context.Context, r StopRequest, fence string) (DrainAcknowledgement, error) {
+ if d.inner == nil {
+ return DrainAcknowledgement{}, ErrClaimStopUnavailable
+ }
+ return d.inner.DrainClaim(ctx, r, fence)
+}
+```
+
+Delete the old `releaseDrain` fields (`e`, `obs`, `id`, `txid`, `seq`, `done`, `resumed`). The `flushed && !local` branch keeps its meaning: a restarted process asks root for the recorded owner's durable receipt with a refusing `RecoveryReleaseDrain`, and the `Collect` provider marks it `Recover`.
+
+- [ ] **Step 4: The bindings**
+
+`internal/host/relocationassembly.go`:
+
+```go
+package host
+
+import (
+ "context"
+ "crypto/ecdh"
+ "errors"
+ "fmt"
+ "path/filepath"
+ "time"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/controlsock"
+ "github.com/carlosframework/platform/internal/genselect"
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ execctl "github.com/carlosframework/platform/internal/hostexec"
+ "github.com/carlosframework/platform/internal/hostrelocate"
+ "github.com/carlosframework/platform/internal/lease"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// RelocationAssemblyConfig is what the relocation providers need beyond
+// the assembly itself. Task 10 fills it once per agent.
+type RelocationAssemblyConfig struct {
+ Lifetime context.Context // the agent's context: controllers live under it
+ // Exec is hostexec's config without Binding and Cleanup, which are set
+ // per participation: Store, Guard, Directory (hostactors.Directory,
+ // roster.go:175), Clock, CloseAdmission, CancelWork. LockDir empty
+ // takes hostexec's production defaults (controller.go:71-76).
+ Exec execctl.Config
+ Directory hostactors.Directory // the roster: release receipts verify under the releasing box's census key
+ Data release.Store // the region's data bucket (files manifests)
+ Versions lease.VersionedStore // the same bucket, versioned (LTX files, install manifests)
+ Bucket string
+ Region string // the lease region (lease.Acquire)
+ Registry *registry.Registry
+ FilesDir func(host string) string
+ EnvPath func(host string) string
+ DataDir string // hostguard.Guard.DataDir
+ WorkDir string // <DataDir>/relocate/build
+ ReleaseDir string // <DataDir>/relocate/release
+ SecretsKey *ecdh.PrivateKey // CARLOS_SECRETS_KEY, for RenderHeldUnder (Task 8c)
+ // Probe is the activator's ProbeWake (Task 8d).
+ Probe func(context.Context, activator.ProbeConfig) (activator.ProbeResult, error)
+ Now func() time.Time
+}
+
+// RelocationEffects binds every provider to this assembly: one value per
+// agent, its controllers outliving every pass. Tasks 8b to 8d set BuildP,
+// Install, VerifyLocal, Receipt and Admit.
+func (a ClaimAssembly) RelocationEffects(cfg RelocationAssemblyConfig) *RelocationEffects {
+ if cfg.Now == nil {
+ cfg.Now = time.Now
+ }
+ receipts := RelocationReleaseReceipts{Store: a.Claims, Directory: cfg.Directory}
+ e := &RelocationEffects{
+ Lifetime: cfg.Lifetime,
+ Controllers: &RelocationControllers{Journal: filepath.Join(cfg.DataDir, "relocate", "participations"), DataDir: cfg.DataDir,
+ Cleanup: func(ctx context.Context, b execctl.Binding) error { return a.relocationCleanup(ctx, cfg, b) },
+ Clear: func(ctx context.Context, b execctl.Binding) error { c := cfg.Exec; c.Binding = b; return execctl.ClearStale(ctx, c) }},
+ Identity: func(ctx context.Context) (RelocationIdentity, error) { return a.localStopIdentity(ctx) },
+ Controller: func(ctx context.Context, b execctl.Binding) (RelocationController, error) {
+ c := cfg.Exec
+ c.Binding = b
+ c.Cleanup = func(ctx context.Context) error { return a.relocationCleanup(ctx, cfg, b) } // ruling 20: the relocation's own partial work
+ return OpenRelocationController(ctx, c)
+ },
+ Producers: func(ctx context.Context, o hostclaim.Observed) (StopDrainer, error) {
+ if a.Wake == nil {
+ return nil, ErrClaimStopUnavailable
+ }
+ p, err := a.Wake.Producers(ctx, o)
+ if err != nil {
+ return nil, err
+ }
+ return p, nil
+ },
+ Collect: func(ctx context.Context, r StopRequest, d StopDrainer) (StopResult, error) {
+ if _, ok := d.(RecoveryReleaseDrain); ok {
+ r.Recover = true // 6b's authenticated recovery path: root replays the durable receipt
+ }
+ if a.collectStop != nil {
+ return a.collectStop(ctx, r, d)
+ }
+ return CollectClaimStop(ctx, controlsock.Path, r, d)
+ },
+ WritersStopped: func(ctx context.Context, o hostclaim.Observed) (ClaimWritersDrained, WritersStoppedEvidence, error) {
+ return a.WritersStopped(ctx, o.Host)
+ },
+ Releases: ReleaseObligation{Receipts: receipts},
+ FlushDB: a.relocationFlushDB(cfg),
+ FlushFiles: ReleaseFlushFiles{Data: cfg.Data, Registry: cfg.Registry, FilesDir: cfg.FilesDir, EnvPath: cfg.EnvPath}.Flush,
+ Positions: ReleasePositionsFile{Dir: cfg.ReleaseDir},
+ }
+ return e
+}
+
+// relocationFlushDB is release step 2 (ruling 8): SyncAndWait with every
+// writer stopped, then N is the newest level-0 MaxTXID, which must equal
+// the plan's MaxTXID. A listing with no LTX file is the stopped writer
+// vouching that it never committed (claim §6.4a "Empty bootstrap"): N is
+// 0, which lets the pin be empty (ruling 15).
+func (a ClaimAssembly) relocationFlushDB(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed) (uint64, error) {
+ return func(ctx context.Context, o hostclaim.Observed) (uint64, error) {
+ if a.Wake == nil || o.Claim == nil || o.Claim.Holder == nil {
+ return 0, ErrClaimStopUnavailable
+ }
+ att := a.Wake.get(o.Host)
+ if att == nil || att.rep == nil || cfg.Versions == nil || cfg.Bucket == "" {
+ return 0, ErrClaimStopUnavailable
+ }
+ if err := att.rep.SyncAndWait(ctx); err != nil {
+ return 0, err
+ }
+ plan, err := genselect.Select(ctx, cfg.Versions, genselect.Request{Bucket: cfg.Bucket, Region: o.Claim.Op.Intent["region"], Host: o.Host, Holder: *o.Claim.Holder})
+ if errors.Is(err, genselect.ErrEmpty) {
+ return 0, nil
+ }
+ if err != nil {
+ return 0, err
+ }
+ if plan.MaxLevel0TXID != plan.MaxTXID {
+ return 0, fmt.Errorf("%w: level 0 at %d, the database at %d", ErrClaimStopUnavailable, plan.MaxLevel0TXID, plan.MaxTXID)
+ }
+ return plan.MaxTXID, nil
+ }
+}
+
+// relocationCleanup is the relocation controller's scoped cleanup (ruling
+// 20): it checks the binding is this box's, and retires only this
+// operation's unopened local installation of the host through main's
+// typed retirement (ReconcileAdapter.Retire over the held row the install
+// minted, Task 8c), which asks root for the installation's session-zero
+// witness (R4) and parks what the inventory names. It does nothing once
+// this box admitted a writer under the operation or serves the host.
+func (a ClaimAssembly) relocationCleanup(ctx context.Context, cfg RelocationAssemblyConfig, b execctl.Binding) error {
+ local, err := a.localStopIdentity(ctx)
+ if err != nil {
+ return err
+ }
+ if b.Box != local.Actor.Box || b.Enrollment != local.Actor.Enrollment {
+ return fmt.Errorf("%w: participation %s/%s is not this box's", hostrelocate.ErrRefused, b.Host, b.Op)
+ }
+ obs, err := hostclaim.Read(ctx, a.Claims, b.Host)
+ if err != nil {
+ return err
+ }
+ c := obs.Claim
+ if obs.Absent || c.Holder == nil || c.Holder.Incarnation != b.Incarnation {
+ return nil
+ }
+ if c.Holder.WriterBox == local.Actor.Box || (c.Op.Kind == hostclaim.KindServing && c.Op.Exec == local.Actor.Box) {
+ return nil
+ }
+ for _, adm := range c.Holder.Admissions {
+ if adm.Box == local.Actor.Box {
+ return nil
+ }
+ }
+ if c.Op.Kind != hostclaim.KindRelocating || c.Op.ID != b.Op || c.Op.Exec != local.Actor.Box || c.Op.Journal["install_stamp"] == "" || hostclaim.PreparedOpened(c) {
+ return nil
+ }
+ if a.Reconcile == nil || cfg.Registry == nil {
+ return ErrClaimStopUnavailable
+ }
+ row, err := cfg.Registry.Get(b.Host)
+ if err != nil {
+ return err
+ }
+ if row == nil || row.HoldOp != c.Op.ID || !row.ClaimOwnership.Valid() {
+ return nil // nothing of this operation's is installed here yet
+ }
+ return a.Reconcile.Retire(ctx, *row) // hostbox/reconcile_adapter.go:160
+}
+```
+
+`RelocationIdentity` is `claimStopLocal` (relocationeffects.go:25), so `localStopIdentity` returns it directly. `ReleaseObligation`, `ReleaseFlushFiles`, `ReleasePositionsFile`, `OpenRelocationController` and `RelocationControllers` are stage A's as merged.
+
+- [ ] **Step 5: Run the tests**
+
+Run: `nice -n 19 go test ./internal/host/ -run 'RelocationFlush|RelocationEffectsAreBound|RelocationCollect|TestRelease|TestADischargedRelease|TestStopReceiptReleases' -count=1` (outside the sandbox, `TMPDIR=/tmp/k`)
+Expected: PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add internal/host/relocationassembly.go internal/host/relocationassembly_test.go internal/host/relocationeffects.go internal/host/relocationeffects_test.go
+git commit -m "host: the release stops and flushes at the observed claim, then queues the obligation and asks root; the providers bind to the claim assembly
+
+Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
+```
+
+---
+
+### Task 8b: Build P and verify it
+
+**Files:**
+- Create: `internal/host/relocationbuild.go`, `internal/host/relocationbuild_test.go`
+- Modify: `internal/host/relocationassembly.go` (set `BuildP`)
+
+**Interfaces:**
+- Consumes: `hostrelocate.ReadPin` (pin.go:326), `CopyPinnedManifest` (355), `PinObject` (28), `FilesPin` (46); `moves.BuildGeneration(ctx, BuildInputs) (InstallManifest, error)` (build.go:162), `BuildInputs{Store lease.VersionedStore; Bucket string; Source []SealedObject; N uint64; GenerationPrefix, Op, Host, WorkDir string; Now func() time.Time; Verify func(string) error}` (105-116), `SealedObject{Key, VersionID string; Size int64}` (sealedclient.go:22), `InstallManifestKey`, `SnapshotKey`, `AnchorKey` (build.go:93, 80, 85), `InstallManifest` (install_contract.go:11-25); `lease.Acquire` (lease.go:153), `Held.Lost`/`Release` (222, 270), `lease.CheckDB` (check.go:35); `hostguard.Capture` (record.go:103); `hostfiles.ManifestKey`, `Manifest` (manifest.go:57-64), `ManifestVersion`.
+- Produces: `(ClaimAssembly) relocationBuild(cfg)`; `PreparedFromInstallManifest{Claims, Files release.Store; Data lease.VersionedStore; Bucket string}` implementing `hostrelocate.Prepared`.
+
+- [ ] **Step 1: Write the failing tests**
+
+The tests are the 1b-next plan's (L12206-12250), unchanged in intent: `TestPreparedFromInstallManifestNeedsBothManifests`, `TestPreparedRefusesAPartialBuild` (files manifest only; install manifest only) and `TestRelocationBuildStopsWhenAuthorityChanges` (after the files manifest; after the snapshot PUT; after the stamp is retired). `buildFixture` uses `release.NewDirStore` for files and pins, the in-memory `lease.VersionedStore` fake `internal/moves` tests use (`moves/build_test.go`), a claim at `installing` with a journaled pin and `install_stamp`, a guard record activated at that stamp, and a hook after each durable write. Add `TestRelocationBuildReplaysToTheSameBytes`: a second run after a crash between the two steps reuses the intent, snapshot and anchor (`BuildGeneration`'s `putOrReuse`) and the files manifest copy succeeds on its own earlier copy.
+
+- [ ] **Step 2: Run them to see them fail**
+
+Run: `nice -n 19 go test ./internal/host/ -run 'PreparedFromInstallManifest|PreparedRefuses|RelocationBuild' -count=1`
+Expected: FAIL to compile: `undefined: PreparedFromInstallManifest`.
+
+- [ ] **Step 3: The build and the verifier**
+
+`internal/host/relocationbuild.go` is the 1b-next plan's L12264-12518 with these bindings, every name verified on this branch:
+
+- `relocationBuild` reads the pin with `hostrelocate.ReadPin(ctx, a.Claims, ref)` (the journaled `RecoveryPin` carries `Release`; `ReadPin` refuses a mismatch); the authority closure re-reads the claim (`op.id`, `PhaseInstalling`, `op.exec` == this box, `prepared`, `install_stamp`, `pin`, no `stuck`) and `hostguard.Capture(a.Guard.DataDir, host)` (`Incarnation`, `Stamp`); `lease.Acquire(ctx, cfg.Bucket, holder.Location, cfg.Region)`; `hostrelocate.CopyPinnedManifest(alive, a.Claims, cfg.Data, pin.Files, location+"/files/"+p+"/")` (`hostfiles.ErrManifestExists` on a replay is success when the bytes match: compare with a `Get` of the manifest and `sameEntries`); `moves.BuildGeneration(alive, moves.BuildInputs{Store: authorityStore{...}, Bucket, Source, N: pin.DB.MaxTXID, GenerationPrefix: location+"/"+p+"/", Op, Host, WorkDir: filepath.Join(cfg.WorkDir, op), Now: cfg.Now, Verify: lease.CheckDB})`. `authorityStore` wraps `PutIfAbsent` and `CopyVersion` as before and refuses `DeleteVersion`; check `lease.VersionedStore`'s method set (versioned.go:45-55) and wrap every write it declares.
+- `PreparedFromInstallManifest.Verify` as L12415-12503: P's files manifest at sequence 0 (`m.Seq != 0` refuses; `SourceFilesSeq` is `pin.Files.Seq`), the retained copy's digest and entries, then the install manifest at `moves.InstallManifestKey(prefix)` by its current version, its `Op`, `Host`, `N`, keys, versions and `IntegrityCheck == "ok"`, and both named versions present. It fills `hostclaim.PreparedAuthority{Generation, InstallManifest, SHA256, SnapshotTXID, FilesSeq: 0, SourceFilesSeq: pin.Files.Seq, FilesManifestSHA256}` (relocation.go:131-152; `InstallStamp` is `ToInstalled`'s).
+- Leftover `.restore-*.db-wal`/`.db-shm` beside the restored database: main's fix (0d338c14) is in `moves`; `relocationBuild` removes nothing itself and the test asserts `WorkDir/<op>` holds only `<host>.db` after a build.
+
+In `RelocationEffects` (Task 8a) add `BuildP: a.relocationBuild(cfg)`.
+
+- [ ] **Step 4: Run the tests**
+
+Run: `nice -n 19 go test ./internal/host/ -run 'PreparedFromInstallManifest|PreparedRefuses|RelocationBuild' -count=1`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add internal/host/relocationbuild.go internal/host/relocationbuild_test.go internal/host/relocationassembly.go
+git commit -m "host: P is built under the replica lease from the pinned versions with moves' install routine, files manifest first, and verified by reading both back
+
+Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
+```
+
+---
+
+### Task 8c: The local install, its held row and the probe's inputs
+
+**Files:**
+- Create: `internal/host/relocationinstall.go`, `internal/host/relocationinstall_test.go`
+- Modify: `internal/hostbox/reconcile_adapter.go:40-158` (extract `ownedRow`; add `MintHeld`), `internal/provision/reconcile.go:425` (`RouteFor`), `internal/registry/registry.go:1164` (`HoldStatuses["relocating"]`), `internal/host/relocationassembly.go` (set `Install`, `VerifyLocal`, `Receipt`)
+- Test: `internal/hostbox/reconcile_adapter_mint_test.go`, `internal/registry/registry_test.go`
+
+**Interfaces:**
+- Consumes: `hostguard.Guard.Activate` (guard.go:183), `With` (116), `Capture`; `hostfiles.Restore` (restore.go:59), `ReadPlatform` (203), `LoadWake` (wake.go:125), `ImportWake` (75), `EnvItem` (platform.go:13); `appconfig.RenderHeldUnder` (renderheld.go:67), `HeldRender` (33); `release.MaterializeHeld` (materializeheld.go:137), `ErrStageBlocked` (26), `HeldStageRequest` (451-458), `LiveVersion` (versions.go:308), `OptDir` (37), `NewProofGate`, `ProofPolicyPath`; `writeHeldStageRequest` (movesstage.go:106); `ReconcileAdapter` (reconcile_adapter.go:23-33), `EmptyInstallationCensus.Initialize`; `registry.Upsert` (registry.go:1071), `SetHold` (1176), `IsHold` (1167); `provision.Record` (provision.go:38), `NestedPaths`, `RecordKey`, `ClaimRecord` (claimassembly.go:12); `hostops.ReadRecord` (record.go:43).
+- Produces: `(ClaimAssembly) relocationInstall(cfg)`, `relocationVerifyLocal(cfg)`, `relocationReceipt(cfg)`; `(ReconcileAdapter) MintHeld(ctx, rec provision.ClaimRecord, captured hostguard.Captured, status, op string) error`; `provision.RouteFor`; `relocationInstallJournal`; `relocationInstallPath(cfg, host, op)`.
+
+- [ ] **Step 1: Write the failing tests**
+
+```go
+package host
+
+// The install runs only under the stamp the installing CAS issued, and
+// never once any admission has named P.
+func TestRelocationInstallIsBoundToTheInstallStamp(t *testing.T) { /* guard record at another stamp: hostguard.ErrStampMismatch, nothing written; P opened (prepared_opened journaled): hostrelocate.ErrRefused; happy path: receipt.InstallStamp == journaled, JournalSHA256 == sha256 of install.json */ }
+
+// A fresh spare: the files directory and the env file are restored from
+// P's manifest, the wake state imported, and the row minted held by the
+// operation with status relocating, owned by the install stamp.
+func TestRelocationInstallRestoresAndMintsTheHeldRow(t *testing.T) { /* dir store with P's manifest (two files, host.env, wake state); after Install: files bytes equal, env equal, timers in the registry, row {Status: "relocating", HoldOp: op, ClaimOwnership.ClaimStamp == install_stamp}; a second Install returns the same receipt and writes nothing new (inventory entry count unchanged) */ }
+
+// Under the edge unit's restrictions MaterializeHeld is blocked: the
+// install leaves a held-stage request naming install_release, never the
+// route's version, and reports ErrNotYet until live/<host> names it.
+func TestRelocationInstallRequestsRootsHeldStageWithThePinnedRelease(t *testing.T) { /* read-only optDir; journal install_release "v1.5.0" and a route Version "v1.4.0"; after Install: HeldStageDir/<host>.json has Version "v1.5.0", Op, Box; err is ErrNotYet; then write live/<host> -> v1.5.0 (the test's own symlink) and Install returns the receipt */ }
+
+// VerifyLocal is true only when the files, env and wake state are what P's
+// manifest says under the receipt's stamp; a retired tree is false.
+func TestRelocationVerifyLocalReadsTheInstallBack(t *testing.T) { /* true after Install; false after removing one file; false after the guard record moves to another stamp */ }
+
+// Receipt reads the durable journal; a missing or damaged one is (false, nil).
+func TestRelocationReceiptIsTheDurableJournal(t *testing.T) {}
+
+// MintHeld binds the row to the install stamp (Review Focus 2).
+func TestMintHeldBindsTheRowToTheInstallStamp(t *testing.T) { /* in hostbox: a captured stamp that is not the claim holder's: ErrChanged; the route minted equals provision.RouteFor's for the record; ClaimOwner is sha256(host\x00inc\x00stamp\x00box); a second call is idempotent */ }
+
+// "relocating" is a hold.
+func TestRelocatingIsAHoldStatus(t *testing.T) { /* registry.IsHold("relocating") */ }
+```
+
+- [ ] **Step 2: Run them to see them fail**
+
+Run: `nice -n 19 go test ./internal/host/ ./internal/hostbox/ ./internal/registry/ -run 'RelocationInstall|RelocationVerifyLocal|RelocationReceipt|MintHeld|RelocatingIsAHold' -count=1`
+Expected: FAIL to compile.
+
+- [ ] **Step 3: `MintHeld`, `RouteFor` and the hold status**
+
+In `internal/registry/registry.go:1164` add `"relocating": true` to `HoldStatuses`. In `internal/provision/reconcile.go:425` rename `routeFor` to `RouteFor` (and its callers). In `internal/hostbox/reconcile_adapter.go` move the body of `Install`'s closure (lines 83-157: capture check, `ReadInstallation`, `Census.Initialize`, the route entry, `write(owner)`, materialized) into:
+
+```go
+// ownedRow is the typed-inventory half of a row write under the host
+// guard at captured: the installation's inventory (initialised by the
+// census when none exists), one route entry owned by captured's digest,
+// write(owner), and the entry materialized. Install calls it under
+// Reconciler.Install; MintHeld under a relocation install's own With.
+func (a ReconcileAdapter) ownedRow(ctx context.Context, rec provision.ClaimRecord, inc string, selected RecordSelection, captured hostguard.Captured, write func(registry.ClaimOwnership) error) error {
+ /* lines 83-157 as they are, with captured and inc as parameters */
+}
+
+// MintHeld mints or keeps the host's row held by op with status under the
+// installation captured, for a relocation target's local install (region
+// HA ruling 31): the row reconcile would mint for rec (RouteFor), with its
+// ClaimOwnership, status and hold_op set in the same Upsert; an existing
+// row must already be this installation's and held by op.
+func (a ReconcileAdapter) MintHeld(ctx context.Context, rec provision.ClaimRecord, captured hostguard.Captured, status, op string) error {
+ if !registry.IsHold(status) || op == "" || a.Registry == nil || a.Inventory == nil {
+ return ErrUnsupported
+ }
+ inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
+ if err != nil {
+ return err
+ }
+ if captured.Incarnation != inc {
+ return ErrChanged
+ }
+ layout := "flat"
+ if rec.Family != "sidecar" && provision.NestedPaths(a.Reconciler.Guard.DataDir, rec.Route) {
+ layout = "nested"
+ }
+ selected := RecordSelection{Host: rec.Host, Key: rec.Key, Incarnation: inc, Family: rec.Family, Layout: layout}
+ return a.ownedRow(ctx, rec, inc, selected, captured, func(owner registry.ClaimOwnership) error {
+ row, err := a.Registry.Get(rec.Host)
+ if err != nil {
+ return err
+ }
+ if row != nil {
+ if row.ClaimOwnership != owner || row.HoldOp != op || !registry.IsHold(row.Status) {
+ return registry.ErrClaimOwnership
+ }
+ return nil
+ }
+ rt := rec.Route
+ rt.ClaimOwnership, rt.Status, rt.HoldOp = owner, status, op
+ return a.Registry.Upsert(rt)
+ })
+}
+```
+
+`ownedRow`'s `ReadInstallation` branch calls `a.Census.Initialize(ctx, observed, selected)` with the claim read fresh, exactly as today; `Install` passes its own `selected` and the captured stamp it captured.
+
+- [ ] **Step 4: The local install**
+
+`internal/host/relocationinstall.go`:
+
+```go
+package host
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+
+ "github.com/carlosframework/platform/internal/appconfig"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfiles"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/hostops"
+ "github.com/carlosframework/platform/internal/hostrelocate"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// relocationInstallJournal is the box's durable record of its install for
+// one operation (<DataDir>/relocate/<host>/<op>/install.json); its digest
+// is the receipt's journal_sha256.
+type relocationInstallJournal struct {
+ V int `json:"v"`
+ Host string `json:"host"`
+ Op string `json:"op"`
+ Box string `json:"box"`
+ Enrollment string `json:"enrollment"`
+ InstallStamp uint64 `json:"install_stamp"`
+ Prepared string `json:"prepared"`
+ Steps map[string]string `json:"steps"` // files, wake, env, row, render, materialize
+}
+
+func relocationInstallPath(cfg RelocationAssemblyConfig, host, op string) string {
+ return filepath.Join(cfg.DataDir, "relocate", host, op, "install.json")
+}
+
+// relocationRecord reads the host's authoritative record and the route
+// reconcile would mint for it on this box.
+func relocationRecord(ctx context.Context, cfg RelocationAssemblyConfig, claims release.Store, o hostclaim.Observed) (provision.ClaimRecord, error) {
+ rec, err := hostops.ReadRecord(ctx, claims, o.Claim.Holder.Key)
+ if err != nil {
+ return provision.ClaimRecord{}, err
+ }
+ var record provision.Record
+ if err := json.Unmarshal(rec.Body, &record); err != nil {
+ return provision.ClaimRecord{}, err
+ }
+ if record.Host != o.Host || record.Kind != "" && record.Kind != "instance" || record.Addr != "" || record.Unit != "" {
+ return provision.ClaimRecord{}, fmt.Errorf("%w: %s is not a relocatable instance record", hostrelocate.ErrRefused, o.Host)
+ }
+ nested := provision.NestedPaths(cfg.DataDir, registry.Route{Host: o.Host})
+ rt := provision.RouteFor(record, cfg.DataDir, "", nested)
+ return provision.ClaimRecord{Host: o.Host, Key: o.Claim.Holder.Key, Family: "instance", Account: record.Account, App: record.App, CreatedAt: record.CreatedAt, Value: record, Route: rt}, nil
+}
+
+// guardedWork is the pass-through hostwrite.Work for writes already under
+// the install's Guard.With (ruling 34).
+type guardedWork struct{}
+
+func (guardedWork) With(_ context.Context, _ hostwrite.Identity, fn func() error) error { return fn() }
+
+// relocationInstall is §5.8's local install (B1-2), under the host guard
+// with the stamp the installing CAS issued: files, wake state and env from
+// P's manifest; the row minted held by the operation (ruling 31); the held
+// config rendered and the pinned release staged through root's relay (R9,
+// M3); every step journaled; then the receipt. Idempotent: a pass that
+// ends in ErrNotYet (the stage not live yet) is repeated whole.
+func (a ClaimAssembly) relocationInstall(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed) (hostclaim.LocalInstall, error) {
+ return func(ctx context.Context, o hostclaim.Observed) (hostclaim.LocalInstall, error) {
+ if cfg.Data == nil || cfg.Registry == nil || a.Reconcile == nil || o.Claim == nil || o.Claim.Holder == nil {
+ return hostclaim.LocalInstall{}, ErrClaimStopUnavailable
+ }
+ c := o.Claim
+ local, err := a.localStopIdentity(ctx)
+ if err != nil {
+ return hostclaim.LocalInstall{}, err
+ }
+ stamp, err := strconv.ParseUint(c.Op.Journal["install_stamp"], 10, 64)
+ if err != nil || stamp == 0 {
+ return hostclaim.LocalInstall{}, fmt.Errorf("%w: no install stamp", hostrelocate.ErrRefused)
+ }
+ if hostclaim.PreparedOpened(c) {
+ return hostclaim.LocalInstall{}, fmt.Errorf("%w: P was admitted; the local install is never remade", hostrelocate.ErrRefused) // B2-5
+ }
+ var ref hostclaim.RecoveryPin
+ if err := json.Unmarshal([]byte(c.Op.Journal["pin"]), &ref); err != nil {
+ return hostclaim.LocalInstall{}, err
+ }
+ pin, err := hostrelocate.ReadPin(ctx, a.Claims, ref)
+ if err != nil {
+ return hostclaim.LocalInstall{}, err
+ }
+ p := c.Op.Journal["prepared"]
+ prefix := c.Holder.Location + "/files/" + p + "/"
+ origins := append(append([]string(nil), pin.Files.Origins...), prefix)
+ rec, err := relocationRecord(ctx, cfg, a.Claims, o)
+ if err != nil {
+ return hostclaim.LocalInstall{}, err
+ }
+ if err := a.Guard.Activate(ctx, o.Host, local.Actor.Box, nil); err != nil { // idempotent on the same stamp
+ return hostclaim.LocalInstall{}, err
+ }
+ captured, err := hostguard.Capture(a.Guard.DataDir, o.Host)
+ if err != nil {
+ return hostclaim.LocalInstall{}, err
+ }
+ if captured.Stamp != stamp || captured.Incarnation != c.Holder.Incarnation {
+ return hostclaim.LocalInstall{}, hostguard.ErrStampMismatch
+ }
+ check := func(fresh *hostclaim.Claim) error {
+ if fresh.Op.ID != c.Op.ID || fresh.Op.Phase != hostclaim.PhaseInstalled || fresh.Op.Exec != local.Actor.Box || fresh.Op.Journal["prepared"] != p ||
+ fresh.Op.Journal["install_stamp"] != c.Op.Journal["install_stamp"] || fresh.Op.Journal["stuck"] != "" || hostclaim.PreparedOpened(fresh) {
+ return fmt.Errorf("%w: the install's authority changed", hostrelocate.ErrRefused)
+ }
+ return nil
+ }
+ journal := relocationInstallJournal{V: 1, Host: o.Host, Op: c.Op.ID, Box: local.Actor.Box, Enrollment: local.Actor.Enrollment, InstallStamp: stamp, Prepared: p, Steps: map[string]string{}}
+ var receipt hostclaim.LocalInstall
+ err = a.Guard.With(ctx, o.Host, captured, check, func() error {
+ if err := os.MkdirAll(cfg.FilesDir(o.Host), 0o700); err != nil {
+ return err
+ }
+ m, _, err := hostfiles.Restore(ctx, cfg.Data, prefix, cfg.FilesDir(o.Host), origins)
+ if err != nil {
+ return err
+ }
+ journal.Steps["files"] = strconv.FormatInt(m.Seq, 10)
+ if ws, ok, err := hostfiles.LoadWake(ctx, cfg.Data, prefix, m, origins); err != nil {
+ return err
+ } else if ok {
+ if err := hostfiles.ImportWake(cfg.Registry, o.Host, ws); err != nil {
+ return err
+ }
+ journal.Steps["wake"] = "imported"
+ } else {
+ journal.Steps["wake"] = "none"
+ }
+ if _, ok := m.Platform[hostfiles.EnvItem]; ok {
+ env, err := hostfiles.ReadPlatform(ctx, cfg.Data, prefix, m, hostfiles.EnvItem, origins)
+ if err != nil {
+ return err
+ }
+ if err := writeFileSynced(cfg.EnvPath(o.Host), env, 0o600); err != nil {
+ return err
+ }
+ journal.Steps["env"] = "written"
+ } else if err := os.Remove(cfg.EnvPath(o.Host)); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err // ENOENT in P's manifest means the host has none: any env file here is not the host's
+ }
+ // The row, held by the operation, owned by this installation
+ // (ruling 31, 38); the typed inventory sees the restored tree.
+ if err := a.Reconcile.MintHeld(ctx, rec, captured, "relocating", c.Op.ID); err != nil {
+ return err
+ }
+ journal.Steps["row"] = c.Op.ID
+ rt := rec.Route
+ rt.Status, rt.HoldOp = "relocating", c.Op.ID
+ // CC13: the held config with current inputs, recorded as the
+ // operation's (moves Plan 1 Task 7b), under this With (ruling 34).
+ rendered, err := appconfig.RenderHeldUnder(ctx, a.Claims, cfg.SecretsKey, cfg.DataDir, rt, c.Op.ID, local.Actor.Box,
+ func(appconfig.HeldRender) (hostwrite.Work, error) { return guardedWork{}, nil })
+ if err != nil {
+ return err
+ }
+ journal.Steps["render"] = rendered.SHA256
+ // The pinned release (R9): install_release, never the route's
+ // version. The edge unit cannot write the version store, so
+ // MaterializeHeld returns ErrStageBlocked and root's relay stages
+ // it (M3); the receipt waits until live/<host> names it.
+ version := c.Op.Journal["install_release"]
+ if version == "" {
+ return fmt.Errorf("%w: no install_release journaled", hostrelocate.ErrRefused)
+ }
+ proof := release.NewProofGate(release.ProofPolicyPath, cfg.DataDir)
+ if _, err := release.MaterializeHeld(ctx, a.Claims, release.OptDir(), rt, version, proof, func(publish func() error) error { return publish() }); err != nil && !errors.Is(err, release.ErrStageBlocked) {
+ return err
+ } else if errors.Is(err, release.ErrStageBlocked) {
+ req := release.HeldStageRequest{Account: rec.Account, App: rec.App, Host: o.Host, Version: version, Op: c.Op.ID, Box: local.Actor.Box}
+ if err := writeHeldStageRequest(cfg.DataDir, req); err != nil {
+ return err
+ }
+ }
+ if live, err := release.LiveVersion(release.OptDir(), o.Host); err != nil || live != version {
+ return fmt.Errorf("%w: the pinned release %s is not staged yet (live %q)", hostrelocate.ErrNotYet, version, live)
+ }
+ journal.Steps["materialize"] = version
+ raw, err := json.Marshal(journal)
+ if err != nil {
+ return err
+ }
+ if err := os.MkdirAll(filepath.Dir(relocationInstallPath(cfg, o.Host, c.Op.ID)), 0o700); err != nil {
+ return err
+ }
+ if err := writeFileSynced(relocationInstallPath(cfg, o.Host, c.Op.ID), raw, 0o600); err != nil {
+ return err
+ }
+ sum := sha256.Sum256(raw)
+ receipt = hostclaim.LocalInstall{Box: local.Actor.Box, Enrollment: local.Actor.Enrollment, InstallStamp: stamp, FilesManifestVersion: m.Seq, JournalSHA256: hex.EncodeToString(sum[:])}
+ return nil
+ })
+ return receipt, err
+ }
+}
+
+// relocationVerifyLocal reports whether the receipt's install is whole on
+// this box now: the journal at the receipt's digest, the guard record at
+// its stamp, every manifest entry present at its size and digest, the env
+// file as P's manifest says, and the row held by the operation.
+func (a ClaimAssembly) relocationVerifyLocal(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed, hostclaim.LocalInstall) (bool, error) {
+ return func(ctx context.Context, o hostclaim.Observed, l hostclaim.LocalInstall) (bool, error) {
+ got, ok, err := a.relocationReceipt(cfg)(ctx, o)
+ if err != nil || !ok || got != l {
+ return false, err
+ }
+ captured, err := hostguard.Capture(a.Guard.DataDir, o.Host)
+ if errors.Is(err, hostguard.ErrNoRecord) {
+ return false, nil
+ }
+ if err != nil {
+ return false, err
+ }
+ if captured.Stamp != l.InstallStamp {
+ return false, nil
+ }
+ row, err := cfg.Registry.Get(o.Host)
+ if err != nil {
+ return false, err
+ }
+ if row == nil || row.HoldOp != o.Claim.Op.ID || !registry.IsHold(row.Status) {
+ return false, nil
+ }
+ p := o.Claim.Op.Journal["prepared"]
+ raw, _, err := cfg.Data.Get(ctx, hostfiles.ManifestKey(o.Claim.Holder.Location+"/files/"+p+"/"))
+ if err != nil {
+ return false, err
+ }
+ var m hostfiles.Manifest
+ if err := json.Unmarshal(raw, &m); err != nil {
+ return false, err
+ }
+ return hostfiles.Present(cfg.FilesDir(o.Host), cfg.EnvPath(o.Host), m) // Task 8c adds Present: every live entry at its size and digest, the env present iff the manifest has it
+ }
+}
+
+// relocationReceipt reads this box's durable install journal for the
+// claim's operation; false when there is none or it is not this operation's.
+func (a ClaimAssembly) relocationReceipt(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed) (hostclaim.LocalInstall, bool, error) {
+ return func(_ context.Context, o hostclaim.Observed) (hostclaim.LocalInstall, bool, error) {
+ raw, err := os.ReadFile(relocationInstallPath(cfg, o.Host, o.Claim.Op.ID))
+ if errors.Is(err, os.ErrNotExist) {
+ return hostclaim.LocalInstall{}, false, nil
+ }
+ if err != nil {
+ return hostclaim.LocalInstall{}, false, err
+ }
+ var j relocationInstallJournal
+ if err := json.Unmarshal(raw, &j); err != nil || j.V != 1 || j.Host != o.Host || j.Op != o.Claim.Op.ID || j.Steps["materialize"] == "" {
+ return hostclaim.LocalInstall{}, false, nil
+ }
+ seq, _ := strconv.ParseInt(j.Steps["files"], 10, 64)
+ sum := sha256.Sum256(raw)
+ return hostclaim.LocalInstall{Box: j.Box, Enrollment: j.Enrollment, InstallStamp: j.InstallStamp, FilesManifestVersion: seq, JournalSHA256: hex.EncodeToString(sum[:])}, true, nil
+ }
+}
+
+var _ = hostbox.ErrChanged
+```
+
+`writeFileSynced` is relocationeffects.go:399. `hostfiles.Present(dir, envPath string, m Manifest) (bool, error)` is new in `internal/hostfiles/restore.go` (a read-only walk: each non-deleted entry's size and sha256, the env file's presence equal to `m.Platform[EnvItem]`'s). Add `HeldStageRequest.Stamp: stamp` the day M3 lands.
+
+In `RelocationEffects` (Task 8a) set `Install: a.relocationInstall(cfg)`, `VerifyLocal: a.relocationVerifyLocal(cfg)`, `Receipt: a.relocationReceipt(cfg)`.
+
+- [ ] **Step 5: Run the tests**
+
+Run: `nice -n 19 go test ./internal/host/ ./internal/hostbox/ ./internal/registry/ ./internal/hostfiles/ ./internal/provision/ -run 'RelocationInstall|RelocationVerifyLocal|RelocationReceipt|MintHeld|RelocatingIsAHold|Present|Reconcile' -count=1`
+Expected: PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add internal/host/relocationinstall.go internal/host/relocationinstall_test.go internal/host/relocationassembly.go internal/hostbox/reconcile_adapter.go internal/hostbox/reconcile_adapter_mint_test.go internal/provision/reconcile.go internal/registry/registry.go internal/registry/registry_test.go internal/hostfiles/restore.go
+git commit -m "host: a relocation target installs from P under its install stamp, mints its row held by the operation and stages the pinned release through root's relay
+
+Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
+```
+
+---
+
+### Task 8d: The relocation owner set and its source
+
+**Files:**
+- Create: `internal/host/relocationwake.go`, `internal/host/relocationattempts.go`, `internal/host/relocationowners.go`, tests `relocationwake_test.go`, `relocationattempts_test.go`, `relocationowners_test.go`
+- Modify: `internal/hostrelocate/participant.go` (export `CertifiedFor`), `internal/host/relocationassembly.go` (set `Admit`)
+
+**Interfaces:**
+- Consumes: `ClaimRelocationOwners` (claimowners.go:61-69), `ClaimKindOwners` (26-30), `claimRelocationC1` (87-95); `ClaimProductionRelocationSource` (claimrelocation.go:19-22), `RegisterProductionClaimRelocationSource` (32); `ClaimWakeInputs` (claimwakeassembly.go:45-53), `ClaimLaunchInputs` (28-43), `ClaimFencedPublication` (81-83), `ClaimPublication` (57-66), `ClaimPublicationReceipt` (70-73), `ErrClaimRootPending`; `OperationAttempts`, `OperationStart` (claimoperations.go), `ClaimLaunchDigest`, `ClaimProbeReadiness`; `hostclaim.Admission` (generation.go:10-26), `relocatingAdmission` (284), `relocationProbeOpening` (307), `WakeRetryAttempt` (wakeretry.go:21-30), `BeginProbeAttempt` (relocation.go:941); `genselect.Select`, `SelectPrepared` (select.go:278), `PreparedPin` (64), `Marker`; `activator.ProbeWake` (probewake.go:185), `ProbeConfig` (55-68), `ProbeResult` (72-76), `ErrProbeRefused`, `ErrProbeFailed`, `ErrAttemptOver`; `hostbox.Policy`, `PolicyRefusal` (wakepolicy.go:53-64), `Selection` (work.go:17-22); `hostrelocate.ProbeDeadline`; the stage A verifier `StopReceiptReleases.VerifyRelease` (relocationreleases.go:42).
+- Produces: `RelocationWakeInputs`; `relocationProbeAttempts`; `RelocationSource` and `NewRelocationSource(cfg RelocationAssemblyConfig, assembly func() *ClaimAssembly) *RelocationSource`; `(ClaimAssembly) relocationAdmit(cfg)`; `hostrelocate.CertifiedFor`.
+
+- [ ] **Step 1: Write the failing tests**
+
+```go
+package host
+
+// Select: the first attempt opens P from its prepared pin with KeepStamp
+// and PredecessorCertified only after this operation's verified release;
+// every later attempt is cold into a fresh generation from the
+// predecessor, no KeepStamp, never certified, never P (Review Focus 1, 4).
+func TestRelocationSelectCertifiesOnlyPsOpeningAfterItsRelease(t *testing.T) { /* table: drain with a verified release -> certified; failover -> not; release present but the receipt unverifiable -> Select refuses; later attempt -> not certified */ }
+func TestRelocationSelectNeverReopensP(t *testing.T) { /* prepared_opened journaled -> NewGeneration != P, Prepared false, KeepStamp false */ }
+func TestRelocationSelectRefusesEverythingButThisBoxsProbe(t *testing.T) { /* kind serving; phase installed; exec another box; p.Probe false; stuck: hostrelocate.ErrRefused each */ }
+
+// AdmissionEvidence binds the admission to a fresh baseline and, on P's
+// opening, to the prepared manifest by digest.
+func TestRelocationAdmissionEvidenceBindsTheBaselineAndThePreparedManifest(t *testing.T) {}
+
+// PublishFenced writes P's marker on the opening and a fresh generation's
+// marker on a later attempt, after Sync, only with the root present.
+func TestRelocationPublishFencedWritesTheAdmittedGenerationsMarker(t *testing.T) {}
+
+// The attempt journal: OpenProbe writes the window once per record
+// attempt k; Reserve(0) binds a session to k; Healthy settles; a session
+// bound to an earlier k refuses once the claim's count moved.
+func TestRelocationProbeAttemptsBindOneSessionPerRecordAttempt(t *testing.T) {}
+
+// The owner set is complete, or Open refuses (ruling 37); the policy and
+// the refusals only refuse.
+func TestRelocationSourceOpensACompleteOwnerSet(t *testing.T) { /* Open returns owners with every field set; claimRelocationC1(env on, true, owners) == (true, nil); Policy refuses a captured stamp != holder.stamp and a lost box, admits op.exec's probe */ }
+
+// BeforeAdmission verifies the source's release or its fence; AfterAdmission
+// verifies the target's installation (the receipt, the stamp on P's opening).
+func TestRelocationAdmissionHooksVerifyReleaseAndInstall(t *testing.T) {}
+
+// relocationAdmit drives one attempt through ProbeWake with P's install
+// manifest and the probe deadline, and reports the session and health.
+func TestRelocationAdmitRunsOneProbeAttempt(t *testing.T) { /* a fake Probe records ProbeConfig{Host, Op, HoldOp: op, Deadline: probing_since+ProbeDeadline, Prepared: &im}; success -> (session, true, nil); ErrProbeFailed -> (0, false, nil); ErrProbeRefused -> (0, false, err) */ }
+```
+
+Plus, in `internal/hostrelocate`, `TestCertifiedForIsExported` is unnecessary: the existing `certifiedFor` tests are renamed.
+
+- [ ] **Step 2: Run them to see them fail**
+
+Run: `nice -n 19 go test ./internal/host/ -run 'RelocationSelect|RelocationAdmissionEvidence|RelocationPublishFenced|RelocationProbeAttempts|RelocationSource|RelocationAdmissionHooks|RelocationAdmit' -count=1`
+Expected: FAIL to compile.
+
+- [ ] **Step 3: The wake inputs**
+
+`internal/host/relocationwake.go`:
+
+```go
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "reflect"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/genselect"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostrelocate"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/moves"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// RelocationWakeInputs is the relocating kind's ClaimWakeInputs (claim
+// Plan 11 R1): the probe's launch inputs are the serving launch source's
+// (executable, args, env, identity, limit) and the admission is §5.8's.
+type RelocationWakeInputs struct {
+ Launch func(context.Context, registry.Route) (ClaimServingLaunch, error) // the provider's serving launch source
+ Files ClaimFilesManifest // C8, main's provider
+ Markers ClaimMarkerStore
+ Claims release.Store
+ Data genselect.Reader
+ Bucket string
+ Box string
+ NewID func() string
+ Now func() time.Time
+ // Releases verifies a journaled release at the pin (stage A's verifier
+ // over main's Complete, relocationreleases.go:42).
+ Releases hostclaim.ReleaseAuthority
+ mu sync.Mutex
+ published map[string]ClaimPublicationReceipt
+}
+
+var (
+ _ ClaimWakeInputs = (*RelocationWakeInputs)(nil)
+ _ ClaimFencedPublication = (*RelocationWakeInputs)(nil)
+)
+
+func (r *RelocationWakeInputs) probe(o hostclaim.Observed, p activator.StartPolicy) error {
+ if r == nil || r.Launch == nil || r.Files == nil || r.Markers == nil || r.Claims == nil || r.Data == nil || r.Bucket == "" || r.Box == "" || r.NewID == nil || r.Now == nil || r.Releases == nil {
+ return hostwrite.ErrUnavailable
+ }
+ c := o.Claim
+ if o.Absent || c == nil || c.Holder == nil || c.Op.Kind != hostclaim.KindRelocating || c.Op.Phase != hostclaim.PhaseRelocatingProbing || c.Op.Exec != r.Box || !p.Probe || c.Op.Journal["stuck"] != "" {
+ return fmt.Errorf("%w: not this box's relocation probe", hostrelocate.ErrRefused)
+ }
+ return nil
+}
+
+func (r *RelocationWakeInputs) request(o hostclaim.Observed) genselect.Request {
+ return genselect.Request{Bucket: r.Bucket, Region: o.Claim.Op.Intent["region"], Host: o.Host, Holder: *o.Claim.Holder}
+}
+
+// Select (§5.8 "The probe's admission"): the first attempt opens P from
+// its prepared pin with KeepStamp, certified only by this operation's
+// verified release (C4: PredecessorCertified); every later attempt is
+// cold into a fresh generation from the predecessor.
+func (r *RelocationWakeInputs) Select(ctx context.Context, rt registry.Route, o hostclaim.Observed, p activator.StartPolicy) (ClaimLaunchInputs, error) {
+ if err := r.probe(o, p); err != nil {
+ return ClaimLaunchInputs{}, err
+ }
+ c, h := o.Claim, o.Claim.Holder
+ if rt.Host != o.Host || !strings.HasPrefix(rt.DBPath, "/") {
+ return ClaimLaunchInputs{}, hostbox.ErrUnsupported
+ }
+ l, err := r.Launch(ctx, rt)
+ if err != nil {
+ return ClaimLaunchInputs{}, err
+ }
+ if l.Identity.Config == "" || l.Identity.Release == "" || l.Executable == "" || len(l.ExecutableSHA256) != 64 || l.Version == "" {
+ return ClaimLaunchInputs{}, hostwrite.ErrUnavailable
+ }
+ if l.Version != c.Op.Journal["install_release"] {
+ return ClaimLaunchInputs{}, fmt.Errorf("%w: the staged release %s is not the pinned %s", hostrelocate.ErrRefused, l.Version, c.Op.Journal["install_release"])
+ }
+ var ref hostclaim.RecoveryPin
+ if err := json.Unmarshal([]byte(c.Op.Journal["pin"]), &ref); err != nil {
+ return ClaimLaunchInputs{}, err
+ }
+ pin, err := hostrelocate.ReadPin(ctx, r.Claims, ref)
+ if err != nil {
+ return ClaimLaunchInputs{}, err
+ }
+ prepared := c.Op.Journal["prepared"]
+ region := c.Op.Intent["region"]
+ in := ClaimLaunchInputs{Roles: []string{"main"}, Identity: l.Identity, Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: append([]string(nil), l.Env...),
+ DBPath: rt.DBPath, Version: l.Version, Limit: l.Limit, ReadyPath: "/"}
+ adm := hostclaim.Admission{ObservedOpID: c.Op.ID, ObservedPhase: hostclaim.PhaseRelocatingProbing, ObservedSession: h.Session, ObservedGeneration: h.Generation,
+ Box: r.Box, Mode: "cold", Region: region, PredecessorRegion: region}
+ if n := len(h.Admissions); n > 0 {
+ adm.PredecessorRegion = h.Admissions[n-1].Region
+ }
+ switch {
+ case !hostclaim.PreparedOpened(c):
+ certified := hostrelocate.CertifiedFor(c)
+ if certified {
+ var rel hostclaim.Release
+ if json.Unmarshal([]byte(c.Op.Journal["release"]), &rel) != nil {
+ return ClaimLaunchInputs{}, hostbox.ErrChanged
+ }
+ if err := r.Releases.VerifyRelease(ctx, *c, rel); err != nil { // the receipt, root's signature, the obligation discharged
+ return ClaimLaunchInputs{}, err
+ }
+ }
+ adm.NewGeneration, adm.KeepStamp, adm.PredecessorCertified = prepared, true, certified
+ in.Prepared = true
+ if pin.DB == nil {
+ in.Empty = true // an evidenced empty bootstrap opened as P
+ break
+ }
+ var im moves.InstallManifest
+ if err := readInstallManifest(ctx, r.Data, r.Bucket, h.Location+"/"+prepared+"/", &im); err != nil {
+ return ClaimLaunchInputs{}, err
+ }
+ plan, err := genselect.SelectPrepared(ctx, r.Data, r.request(o), genselect.PreparedPin{Generation: prepared, SnapshotKey: im.SnapshotKey, SnapshotVersion: im.SnapshotVersion, SnapshotTXID: im.N, SnapshotCRC64: im.SnapshotCRC64})
+ if err != nil {
+ return ClaimLaunchInputs{}, err
+ }
+ adm.RemoteMaxTXID, in.Baseline = pin.DB.MaxTXID, plan
+ default:
+ adm.NewGeneration = r.NewID()
+ if !claimFreshGeneration(h, adm.NewGeneration) {
+ return ClaimLaunchInputs{}, hostwrite.ErrUnavailable
+ }
+ plan, err := genselect.Select(ctx, r.Data, r.request(o))
+ switch {
+ case errors.Is(err, genselect.ErrEmpty):
+ in.Empty = true
+ case err != nil:
+ return ClaimLaunchInputs{}, err
+ default:
+ adm.RemoteMaxTXID, in.Baseline = plan.MaxTXID, plan
+ }
+ }
+ in.Admission = adm
+ return in, nil
+}
+
+// Materialize proves the held identities and has C8 establish the files
+// manifest, as the serving owners do; RenderHeld and MaterializeHeld ran
+// at the local install.
+func (r *RelocationWakeInputs) Materialize(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs, work hostwrite.Work) error {
+ if err := r.probe(o, activator.StartPolicy{Probe: true}); err != nil {
+ return err
+ }
+ if work == nil || in.Identity.Config == "" || in.Identity.Release == "" {
+ return hostwrite.ErrUnavailable
+ }
+ if err := work.With(ctx, in.Identity, func() error { return nil }); err != nil {
+ return err
+ }
+ return r.Files.Establish(ctx, o, in, work)
+}
+
+// EmptyAuthority: P's opening is empty only with an evidenced empty pin;
+// a later attempt over an empty predecessor is an ordinary empty cold
+// admission (a complete listing of the location holds nothing).
+func (r *RelocationWakeInputs) EmptyAuthority(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs) error {
+ if err := r.probe(o, activator.StartPolicy{Probe: true}); err != nil {
+ return err
+ }
+ if !in.Empty || in.Baseline != nil {
+ return hostbox.ErrChanged
+ }
+ if in.Prepared {
+ var ref hostclaim.RecoveryPin
+ if json.Unmarshal([]byte(o.Claim.Op.Journal["pin"]), &ref) != nil || !ref.Empty {
+ return fmt.Errorf("%w: no evidenced empty pin", hostrelocate.ErrRefused)
+ }
+ return nil
+ }
+ versions, err := r.Data.ListVersions(ctx, r.Bucket, strings.TrimSuffix(o.Claim.Holder.Location, "/")+"/")
+ if err != nil {
+ return err
+ }
+ if len(versions) != 0 {
+ return hostbox.ErrChanged
+ }
+ return nil
+}
+
+// Publish is unfenced and refused; PublishFenced is the marker writer.
+func (r *RelocationWakeInputs) Publish(context.Context, hostclaim.Observed, ClaimLaunchInputs) error {
+ return hostwrite.ErrUnavailable
+}
+
+// PublishFenced writes generation.json for the admitted generation (moves
+// §7.14; spec §5.8 "generation.json is not written here: the probe's
+// admission publishes it"): P's marker on the opening, a fresh generation's
+// on a later attempt. It is claimServingOwners.PublishFenced's rule for a
+// relocating holder: Sync, the generation's own root (none yet is
+// ErrClaimRootPending), supersede the current marker by its exact ETag.
+func (r *RelocationWakeInputs) PublishFenced(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs, pub ClaimPublication) (ClaimPublicationReceipt, error) {
+ if err := r.probe(o, activator.StartPolicy{Probe: true}); err != nil {
+ return ClaimPublicationReceipt{}, err
+ }
+ if pub.Sync == nil || pub.Check == nil {
+ return ClaimPublicationReceipt{}, hostwrite.ErrUnavailable
+ }
+ h := o.Claim.Holder
+ if h.Generation == "" || h.Generation != in.Admission.NewGeneration || h.WriterBox != r.Box {
+ return ClaimPublicationReceipt{}, hostbox.ErrChanged
+ }
+ return publishMarker(ctx, r.Data, r.Markers, r.Bucket, r.Box, r.Now, o, in.Empty, pub, &r.mu, &r.published)
+}
+
+// AdmissionEvidence binds a relocation probe admission to its observation
+// and to the certified baseline, read again: on P's opening the exact
+// prepared pin and P's files manifest by digest (prepared_authority); on a
+// later attempt the predecessor's fresh baseline.
+func (r *RelocationWakeInputs) AdmissionEvidence(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, baseline *genselect.Plan) error {
+ if err := r.probe(o, activator.StartPolicy{Probe: true}); err != nil {
+ return err
+ }
+ c, h := o.Claim, o.Claim.Holder
+ if in.ObservedOpID != c.Op.ID || in.ObservedPhase != hostclaim.PhaseRelocatingProbing || in.ObservedSession != h.Session || in.ObservedGeneration != h.Generation || in.Box != r.Box || in.Mode != "cold" || in.Coverage != "" || len(in.AdditionalWritesLost) != 0 {
+ return hostbox.ErrChanged
+ }
+ prepared := c.Op.Journal["prepared"]
+ if in.NewGeneration == prepared {
+ if !in.KeepStamp || hostclaim.PreparedOpened(c) {
+ return hostbox.ErrChanged
+ }
+ var auth hostclaim.PreparedAuthority
+ if json.Unmarshal([]byte(c.Op.Journal["prepared_authority"]), &auth) != nil || auth.Generation != prepared {
+ return hostbox.ErrChanged
+ }
+ raw, _, err := r.Claims.Get(ctx, hostclaim.RecoveryPinKey(o.Host, c.Op.ID, 0)) // placeholder: see below
+ _ = raw
+ _ = err
+ if baseline == nil {
+ var ref hostclaim.RecoveryPin
+ if json.Unmarshal([]byte(c.Op.Journal["pin"]), &ref) != nil || !ref.Empty || in.RemoteMaxTXID != 0 {
+ return hostbox.ErrChanged
+ }
+ return nil
+ }
+ if baseline.Generation != prepared || baseline.MaxTXID != in.RemoteMaxTXID || baseline.MaxTXID != auth.SnapshotTXID {
+ return hostbox.ErrChanged
+ }
+ return nil
+ }
+ if in.KeepStamp || in.PredecessorCertified || !claimFreshGeneration(h, in.NewGeneration) {
+ return hostbox.ErrChanged
+ }
+ if baseline == nil {
+ return nil // an empty predecessor, proved by EmptyAuthority
+ }
+ fresh, err := genselect.Select(ctx, r.Data, r.request(o))
+ if err != nil {
+ return err
+ }
+ if fresh.Generation != baseline.Generation || fresh.MaxTXID != baseline.MaxTXID || in.RemoteMaxTXID != fresh.MaxTXID || !reflect.DeepEqual(fresh.Files, baseline.Files) {
+ return hostbox.ErrChanged
+ }
+ return nil
+}
+
+// PublicationEvidence is claimServingOwners.publicationEvidence's rule for
+// a relocating holder: this process's receipt for the admitted session,
+// the marker at exactly that version naming the holder, and the root still
+// there.
+func (r *RelocationWakeInputs) PublicationEvidence(ctx context.Context, o hostclaim.Observed) error {
+ if err := r.probe(o, activator.StartPolicy{Probe: true}); err != nil {
+ return err
+ }
+ return publicationEvidence(ctx, r.Data, r.Bucket, r.Box, o, &r.mu, r.published)
+}
+```
+
+Replace the `placeholder` lines: the prepared manifest check on P's opening reads P's files manifest (`r.Data` is the data bucket; use a `release.Store` field `Files` for `hostfiles.ManifestKey(location+"/files/"+prepared+"/")`) and compares its sha256 with `auth.FilesManifestSHA256`; the field is added to the struct as `Files release.Store` beside `Data`. `publishMarker` and `publicationEvidence` are the serving owners' bodies (claimowners.go `PublishFenced` and `publicationEvidence`) extracted into package functions taking the reader, markers, bucket, box, clock, observation and the receipt map, so one implementation serves both kinds; `claimServingOwners` calls them with its own fields. `readInstallManifest` is Task 8b's read of the current install manifest version, factored out of `PreparedFromInstallManifest.Verify`. In `internal/hostrelocate/participant.go` rename `certifiedFor` to `CertifiedFor` (exported; the participant and the owners use the same rule).
+
+- [ ] **Step 4: The attempt journal**
+
+`internal/host/relocationattempts.go`, modelled on `movesProbeAttempts` (movesprobeattempts.go:57-100) without the record:
+
+```go
+// relocationProbeAttempts is region HA's OperationAttempts for a probe
+// admitted under relocating/probing (claim Plan 11 R1, D2). The record
+// attempt k is the claim's probe_attempts.<box>, counted by the
+// participant before each admission (BeginProbeAttempt); the journal at
+// <DataDir>/relocate/<host>/<op>/attempts.json binds each admitted session
+// to one k and holds the attempt's opened time and each start's
+// reservation and state. Every method refuses (hostbox.ErrChanged) once
+// the claim no longer names the admitted writer (op, kind, session,
+// stamp, generation; phase probing, not stuck).
+type relocationProbeAttempts struct {
+ Claims release.Store
+ DataDir string
+ Box string
+ NewID func() string
+ Now func() time.Time
+ Settle, Ready time.Duration
+ mu sync.Mutex
+ current map[string]boundAttempt // host\x00session -> {k, attempt}
+}
+
+type relocationAttemptJournal struct {
+ V int `json:"v"`
+ Host, Op string `json:"host","op"`
+ Opened map[string]time.Time `json:"opened"` // "k" -> first app start
+ Sessions map[string]int `json:"sessions"` // "session" -> k
+ Starts map[string]probeStart `json:"starts"` // "k/n" -> start
+}
+```
+
+`bound(ctx, s)` reads the claim fresh, checks `claimSameWriter(s.Admitted, fresh, hostclaim.SessionContinues)`, phase `probing`, no `stuck`, `s.Owner` equal to the holder's tuple, and takes `k` from `probe_attempts.<box>` (`journalUint`, 1 to `MaxProbeAttempts`); a session already bound to another `k` refuses. `OpenProbe` writes `Opened[k]` once. `Reserve(0)` validates the journaled start; `Reserve(n)` writes start `n` with a fresh `WakeRetryAttempt{ID, InvocationID, Session, Box, Enrollment, BootID, ControllerID, LaunchInputs: ClaimLaunchDigest(s.Inputs)}`. `CurrentAttempt` returns the last reserved. `Failed` and `Healthy` settle the start; `Healthy` runs `again` after `Settle` (default `moves.SettleSeconds`) bounded by `Ready` (default `moves.ProbeSeconds`), as `movesProbeAttempts.Healthy` does. Writes go through `writeFileSynced`.
+
+- [ ] **Step 5: The source, the hooks, the policy and the probe driver**
+
+`internal/host/relocationowners.go`:
+
+```go
+// RelocationSource is region HA's ClaimProductionRelocationSource (claim
+// Plan 11 R1, R8): registered once at agent startup (Task 10), opened by
+// the production provider only with CARLOS_REGION_HA on. Open returns the
+// complete owner set (ruling 37) and two refusals (ruling 39).
+type RelocationSource struct {
+ cfg RelocationAssemblyConfig
+ launch func(context.Context, registry.Route) (ClaimServingLaunch, error)
+ files ClaimFilesManifest
+ markers ClaimMarkerStore
+ pub ed25519.PublicKey
+ mu sync.Mutex
+ actor hostbox.Actor
+ owners *ClaimRelocationOwners
+}
+
+func (s *RelocationSource) Open(ctx context.Context, store release.Store, data lease.VersionedStore, actor hostbox.Actor) (ClaimRelocationOwners, []hostbox.PolicyRefusal, error) {
+ if s == nil || store == nil || data == nil || actor.Box == "" || s.launch == nil || s.files == nil || s.markers == nil {
+ return ClaimRelocationOwners{}, nil, hostwrite.ErrUnavailable
+ }
+ directory := s.cfg.Directory
+ releases := StopReceiptReleases{Receipts: RelocationReleaseReceipts{Store: store, Directory: directory}}
+ inputs := &RelocationWakeInputs{Launch: s.launch, Files: s.files, Markers: s.markers, Claims: store, Files: s.cfg.Data, Data: hostbox.RequestStore{Store: data, Check: func(context.Context) error { return nil }},
+ Bucket: actor.Bucket, Box: actor.Box, NewID: newClaimID, Now: time.Now, Releases: releases}
+ attempts := &relocationProbeAttempts{Claims: store, DataDir: s.cfg.DataDir, Box: actor.Box, NewID: newClaimID, Now: time.Now}
+ owners := ClaimRelocationOwners{
+ BeforeAdmission: relocationBeforeAdmission(store, actor.Box, releases, directory, time.Now),
+ AfterAdmission: relocationAfterAdmission(s.cfg, actor.Box),
+ ClaimKindOwners: ClaimKindOwners{Inputs: inputs, AdmissionEvidence: inputs.AdmissionEvidence, PublicationEvidence: inputs.PublicationEvidence},
+ Operations: attempts,
+ ProbeReady: nil, // claimProbeSocketReady (claimconstruction.go:385)
+ Policy: relocationPolicy(actor.Box),
+ }
+ s.mu.Lock()
+ s.actor, s.owners = actor, &owners
+ s.mu.Unlock()
+ return owners, []hostbox.PolicyRefusal{refuseStaleStamp, refuseLostBox}, nil
+}
+
+func (s *RelocationSource) Close(context.Context) error { return nil }
+
+// relocationPolicy is the typed Policy.Relocation owner: it runs after
+// relocationProbe (wakepolicy.go:187) passed and only refuses: the captured
+// installation must be the holder's, this box a participant, not lost.
+func relocationPolicy(box string) func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error {
+ return func(_ context.Context, c *hostclaim.Claim, s hostbox.Selection, _ hostwrite.Identity) error {
+ if c == nil || c.Holder == nil || s.Box != box || !slices.Contains(c.Op.Participants, box) || slices.Contains(c.Lost, box) {
+ return hostbox.ErrChanged
+ }
+ if s.Captured.Incarnation != c.Holder.Incarnation || s.Captured.Stamp != c.Holder.Stamp {
+ return hostbox.ErrChanged
+ }
+ return nil
+ }
+}
+
+func refuseStaleStamp(_ context.Context, c *hostclaim.Claim, s hostbox.Selection, _ hostwrite.Identity) error {
+ if c.Op.Kind == hostclaim.KindRelocating && (s.Captured.Incarnation != c.Holder.Incarnation || s.Captured.Stamp != c.Holder.Stamp) {
+ return hostbox.ErrChanged
+ }
+ return nil
+}
+
+func refuseLostBox(_ context.Context, c *hostclaim.Claim, s hostbox.Selection, _ hostwrite.Identity) error {
+ if slices.Contains(c.Lost, s.Box) {
+ return hostbox.ErrChanged
+ }
+ return nil
+}
+
+// relocationBeforeAdmission verifies the source's release before the
+// target's first admission: a drain's journaled release re-verified at
+// its receipt (root's signature, the obligation discharged), or a
+// failover's fence consumed (the source on lost, its fence journaled).
+// A later attempt (P opened) needs neither: its predecessor is this box's
+// own previous session, which main's local stop ended.
+func relocationBeforeAdmission(store release.Store, box string, releases StopReceiptReleases, d hostactors.Directory, now func() time.Time) func(context.Context, hostclaim.Observed) error {
+ return func(ctx context.Context, prior hostclaim.Observed) error {
+ c := prior.Claim
+ if c == nil || c.Holder == nil || c.Op.Kind != hostclaim.KindRelocating || c.Op.Exec != box {
+ return hostbox.ErrChanged
+ }
+ if hostclaim.PreparedOpened(c) {
+ return nil
+ }
+ var rel hostclaim.Release
+ if json.Unmarshal([]byte(c.Op.Journal["release"]), &rel) == nil && rel.ReleasedBy != "" {
+ return releases.VerifyRelease(ctx, *c, rel)
+ }
+ src := c.Op.Intent["source_box"]
+ if slices.Contains(c.Lost, src) && c.Op.Journal["fence."+src] != "" {
+ return nil
+ }
+ return fmt.Errorf("%w: no release and no fence for %s", hostrelocate.ErrRefused, src)
+ }
+}
+
+// relocationAfterAdmission verifies the target's installation after the
+// admission: on P's opening the admitted stamp is install_stamp
+// (KeepStamp) and the local install receipt is this box's, as journaled.
+func relocationAfterAdmission(cfg RelocationAssemblyConfig, box string) func(context.Context, hostclaim.Observed, hostclaim.Observed) error {
+ return func(_ context.Context, prior, admitted hostclaim.Observed) error {
+ c, n := prior.Claim, admitted.Claim
+ if c == nil || n == nil || n.Holder == nil || n.Op.ID != c.Op.ID || n.Op.Exec != box || n.Holder.WriterBox != box {
+ return hostbox.ErrChanged
+ }
+ var l hostclaim.LocalInstall
+ if json.Unmarshal([]byte(n.Op.Journal["local_install"]), &l) != nil || l.Box != box || l.InstallStamp == 0 {
+ return hostbox.ErrChanged
+ }
+ raw, err := os.ReadFile(relocationInstallPath(cfg, admitted.Host, n.Op.ID))
+ if err != nil {
+ return err
+ }
+ if sum := sha256.Sum256(raw); hex.EncodeToString(sum[:]) != l.JournalSHA256 {
+ return hostbox.ErrChanged
+ }
+ if n.Holder.Generation == n.Op.Journal["prepared"] && n.Holder.Stamp != l.InstallStamp {
+ return hostbox.ErrChanged // P's opening keeps the install stamp
+ }
+ return nil
+ }
+}
+
+// relocationAdmit is one probe attempt (hostrelocate.Effects.Probe): the
+// activator's ProbeWake with the probe deadline and P's install manifest,
+// through main's claim runtime, the dispatcher and this box's owners. It
+// returns the admitted session and the attempt's exact health; a refused
+// probe is an error, a failed one is (0, false, nil).
+func (a ClaimAssembly) relocationAdmit(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed, bool) (uint64, bool, error) {
+ return func(ctx context.Context, o hostclaim.Observed, certified bool) (uint64, bool, error) {
+ if cfg.Probe == nil || o.Claim == nil || o.Claim.Holder == nil {
+ return 0, false, ErrClaimStopUnavailable
+ }
+ c := o.Claim
+ if certified != hostrelocate.CertifiedFor(c) {
+ return 0, false, fmt.Errorf("%w: the participant and the owners disagree on certification", hostrelocate.ErrRefused)
+ }
+ since, err := time.Parse(time.RFC3339Nano, c.Op.Journal["probing_since"])
+ if err != nil {
+ return 0, false, err
+ }
+ pc := activator.ProbeConfig{Host: o.Host, Op: c.Op.ID, HoldOp: c.Op.ID, Deadline: since.Add(hostrelocate.ProbeDeadline)}
+ if !hostclaim.PreparedOpened(c) {
+ var im moves.InstallManifest
+ if err := readInstallManifest(ctx, cfg.Versions, cfg.Bucket, c.Holder.Location+"/"+c.Op.Journal["prepared"]+"/", &im); err == nil {
+ pc.Prepared = &im
+ } else if !errors.Is(err, os.ErrNotExist) {
+ return 0, false, err
+ }
+ }
+ res, err := cfg.Probe(ctx, pc)
+ switch {
+ case err == nil:
+ return res.Session, true, nil
+ case errors.Is(err, activator.ErrProbeFailed), errors.Is(err, activator.ErrAttemptOver):
+ return 0, false, nil
+ default:
+ return 0, false, err
+ }
+ }
+}
+```
+
+`newClaimID` is the 16-byte hex generator claimconstruction.go:242 uses, factored into a package function. `hostbox.RequestStore` is the `genselect.Reader` wrapper the serving owners use (claimconstruction.go:257). `NewRelocationSource(cfg, launch, files, markers)` sets the fields; Task 10 supplies `launch` from `claimServingLaunchSource` (claimproduction.go:139) and `files`/`markers` from the production provider: the provider's `OpenClaimOwners` opens the relocation source after the moves source (claimproduction.go:151), so Task 10 extends `openClaimRelocationOwners`'s caller to hand the source `out.Serving.Launch`, `out.Serving.Files` and `out.Serving.Markers` before `Open` (a setter `SetServingSources` on `RelocationSource`, called at claimproduction.go:150, inside the same `regionHAEnabled` gate). In `RelocationEffects` set `Admit: a.relocationAdmit(cfg)`.
+
+- [ ] **Step 6: Run the tests**
+
+Run: `nice -n 19 go test ./internal/host/ ./internal/hostrelocate/ -run 'RelocationSelect|RelocationAdmissionEvidence|RelocationPublishFenced|RelocationProbeAttempts|RelocationSource|RelocationAdmissionHooks|RelocationAdmit|Plan11|CertifiedFor|Participant' -count=1`
+Expected: PASS, including main's `TestPlan11*` dispatch tests with the real owner set substituted for `plan9D5Owner` in a new case.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add internal/host/relocationwake.go internal/host/relocationattempts.go internal/host/relocationowners.go internal/host/relocation*_test.go internal/host/relocationassembly.go internal/host/claimowners.go internal/host/claimconstruction.go internal/host/claimproduction.go internal/hostrelocate/participant.go internal/hostrelocate/participant_test.go
+git commit -m "host: region HA's relocation owners (inputs, evidence, attempts, policy, hooks) register as the claim's production relocation source; the probe runs through the activator
+
+Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
+```
+
+---
+
+### Task 9: Reconcile under a relocating claim, and the completion
+
+**Files:**
+- Modify: `internal/hostbox/reconcile.go:192-205` (every `RowHeld` hold-only), `internal/host/relocationeffects.go:842` (`Finish` releases the hold), `internal/hostclaim/relocation.go:1003` (`released_move`, M1)
+- Test: `internal/hostbox/reconcile_relocation_test.go`, `internal/host/relocationeffects_test.go`, `internal/hostclaim/relocation_test.go`
+
+**Interfaces:**
+- Consumes: `Reconciler.Install` (reconcile.go:166), `holdOnly` (253), `RelocationRow` (relocationrows.go:47); `registry.ReleaseHold(host, op) (bool, error)` (registry.go:1263); `ReconcileAdapter` fixture from Task 8c's tests.
+- Produces: `RelocationEffects.Registry *registry.Registry` (set by Task 8a's config); `CompleteRelocation` journals `released_move`.
+
+- [ ] **Step 1: Write the failing tests**
+
+```go
+// Every relocating row decision is hold-only (ruling 31): the target's
+// existing row is kept with its hold; a source after its fence closes is
+// held; the source in starting is left alone; a box with no role and a row
+// keeps it held; nothing is activated and no inventory is written.
+func TestReconcileLeavesARelocatingTargetsRowToItsInstall(t *testing.T) { /* adapter fixture (Task 8c); row minted by MintHeld; Install over phases installing..probing: err nil or ErrChanged, row unchanged, guard dir unchanged, claim ETag unchanged */ }
+
+// After the serving CAS the target releases its own hold once (Finish), the
+// row is ready, and the next reconcile pass on the serving executor finds a
+// coherent inventory (Review Focus 5).
+func TestReconcileAdapterClearsTheHoldAfterCompletion(t *testing.T) { /* MintHeld at install_stamp; CompleteRelocation; effects.Finish(host, op) -> ReleaseHold; adapter.Install succeeds through the real census */ }
+
+// A converted landing's completion journals released_move for moves (M1).
+func TestCompleteRelocationJournalsReleasedMoveForAConvertedLanding(t *testing.T) { /* hostclaim: conversion = committed-landing -> Op.Journal["released_move"] == op id on the serving op; a plain relocation journals none */ }
+```
+
+- [ ] **Step 2: Run them to see them fail**
+
+Run: `nice -n 19 go test ./internal/hostbox/ ./internal/hostclaim/ ./internal/host/ -run 'ReconcileLeavesARelocating|ReconcileAdapterClearsTheHold|CompleteRelocationJournalsReleasedMove' -count=1`
+Expected: FAIL.
+
+- [ ] **Step 3: Hold-only for every held row**
+
+In `internal/hostbox/reconcile.go` `Install`, line 204, replace the decision with:
+
+```go
+ // Under a relocating claim reconcile never activates (ruling 31):
+ // Plan 11's policy admits only the probe, and the target's row is
+ // its install's. Every held decision is a kept row's hold write;
+ // a ready decision (the source before its fence closes) leaves
+ // the row as it is.
+ dec = &decided{row: d, holdOnly: d.Case == RowHeld}
+```
+
+and in `checkOn`, the `RowReady` case under `relocates` returns `ErrChanged` as today (the row is left alone). `holdOnly` is unchanged: `HoldRow` is a no-op when the hold is already the operation's.
+
+- [ ] **Step 4: The completion's hold release**
+
+In `internal/host/relocationeffects.go`, `Finish`:
+
+```go
+func (e *RelocationEffects) Finish(ctx context.Context, host, op string) error {
+ if e.Controllers == nil || e.Identity == nil {
+ return ErrClaimStopUnavailable
+ }
+ if e.Registry != nil && e.Claims != nil {
+ // The serving executor releases its own row's hold once the
+ // operation is over (ruling 32): the activator then retires the
+ // probe entry and the next request wakes the host as serving.
+ obs, err := hostclaim.Read(ctx, e.Claims, host)
+ if err != nil {
+ return err
+ }
+ id, err := e.Identity(ctx)
+ if err != nil {
+ return err
+ }
+ if !obs.Absent && obs.Claim.Op.Kind == hostclaim.KindServing && obs.Claim.Op.Exec == id.Actor.Box {
+ if _, err := e.Registry.ReleaseHold(host, op); err != nil {
+ return err
+ }
+ }
+ }
+ return e.Controllers.Finish(ctx, host, op)
+}
+```
+
+`RelocationEffects` gains `Registry *registry.Registry` and `Claims release.Store`, set in Task 8a's `RelocationEffects` from `cfg.Registry` and `a.Claims`. `ReleaseHold` reports `false` with no error when the row is not held by `op` (registry.go:1263); that is the replay.
+
+- [ ] **Step 5: `released_move` (M1)**
+
+In `internal/hostclaim/relocation.go` `CompleteRelocation`, before `c.Op = Op{Kind: KindServing, ...}`:
+
+```go
+ next := Op{Kind: KindServing, Exec: target, Participants: []string{target}}
+ if c.Op.Journal["conversion"] == "committed-landing" {
+ // M1 on amadan #236: the move that this relocation finished is
+ // named on the serving op, so moves' close can find it.
+ next.Journal = map[string]string{"released_move": opID}
+ }
+ c.Op = next
+```
+
+- [ ] **Step 6: Run the tests**
+
+Run: `nice -n 19 go test ./internal/hostbox/ ./internal/hostclaim/ ./internal/host/ -run 'Reconcile|CompleteRelocation|Finish|Sweep' -count=1`
+Expected: PASS.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add internal/hostbox/reconcile.go internal/hostbox/reconcile_relocation_test.go internal/host/relocationeffects.go internal/host/relocationeffects_test.go internal/host/relocationassembly.go internal/hostclaim/relocation.go internal/hostclaim/relocation_test.go
+git commit -m "hostbox: under a relocating claim reconcile only holds; the serving target releases its hold at completion; a converted landing's completion names its move
+
+Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
+```
+
+---
+
+### Task 10: The passes behind `CARLOS_REGION_HA`, and the rollout checklist
+
+**Files:**
+- Create: `internal/host/relocatepass.go`, `internal/host/relocatepass_test.go`, `internal/host/relocationlanding.go`, `internal/edgesfile/snapshot.go`
+- Modify: `internal/host/agent.go:214-278, 450` (register the source; start the pass), `internal/host/claimproduction.go:150` (hand the serving sources to the source), `internal/hostrelocate/worker.go` (the no-target bound), `internal/console/boxchoice.go`, `instancesplacement.go:114`, `regioncandidates.go:203` (read through `edgesfile.LiveSnapshot`), `internal/console/claim_runtime.go:71` (`Executors`)
+
+**Interfaces:**
+- Consumes: `hostrelocate.Worker` (worker.go:102-141), `Participant` (participant.go:92-98), `RegionChooser` (chooser.go:22-30), `StorePinner` (pin.go:75-91); `hostfence.ClaimFenceAuthority` (claim_fence.go:14); `hostactors.Directory.CensusBoxRelocation` (census.go:425), `CheckBoxStart` (296), `ExactEvidence` (326), `Box.EnrolledAt` (roster.go:28); `hostfloor.Read` (floor.go:72), `ReadEnrollment` (report.go:89); `boxvolumes.List`, `Discharge(records, Entry, enrolledAt, terminatedAt)` (volumes.go:250, 327); `termfence.Record` (fence.go:73); `hostops.ExecutorChooser` (executor.go:21-40), `ClaimLoads` (loads.go:18), `ProductionAuthority.Executors` (backfill.go:98); `release.Channel` (release.go:1220); `lease.Acquire`, `NewVersionedS3`; `genselect.Select`; `activator.Service.ProbeWake`; `status.LoadBoxesComplete` (status/fleet.go:234); `console.readLiveEdgesSnapshot` (instancesplacement.go:114), `boxSnapshot` (boxchoice.go:26); `hostclaim.ConvertCommittedLanding` (relocating.go:221), `CommittedLandingGuard` (187), `CommittedLanding` (175); `provision.GetRecord` (provision.go:2530), `MoveState` (937), `MoveCommitted` (1093); `moves.CommitScopes` (parkscope.go:87); `hostops.AppAdmission` registration (`hostops/ingest.go` `registerStart`) for the worker's `Admission`.
+- Produces: `relocatePassEnabled`, `runRelocatePass`, `NewRelocationRuntime` (the worker, participant and source for an agent); `edgesfile.LiveSnapshot(ctx, s, now) (boxchoice.Snapshot, error)`; `CommittedLandingRecords`; the rollout checklist.
+
+- [ ] **Step 1: Write the failing tests**
+
+```go
+func TestRelocatePassIsOffUnlessBothFlagsAreOn(t *testing.T) { /* the 1b-next table (L13395) */ }
+func TestRelocatePassStaysOffWhileThePreflightRefuses(t *testing.T) { /* a runtime whose Roles.Preflight returns ErrClaimIsolationUnavailable: the pass logs once and steps nothing */ }
+func TestRelocatePassStepsEveryRelocationInItsRegionAndSweeps(t *testing.T) { /* two relocating claims in ie, one in uk, one serving: Worker.Step and Participant.Step called for the two; Sweep once */ }
+func TestRelocatePassAlertsOnceWhileAConversionWaitsForATarget(t *testing.T) { /* ruling 35: the chooser refuses; one log line per host; after ReleaseDeadline from the fence the op is stuck */ }
+func TestRelocatePassConvertsAFencedCommittedLanding(t *testing.T) { /* landing claim at committed, record MoveCommitted, target fenced: ConvertCommittedLanding called with CommittedLandingRecords; an uncommitted record: nothing written */ }
+func TestRelocationPinnerSelectsOverTheVersionedBucket(t *testing.T) { /* genselect's versioned fixture: the pin names the plan's versions and the channel's Version */ }
+func TestRelocationReleaseIsTheChannelPointer(t *testing.T) { /* ruling 30: a signed pointer at v1.5.0 and a route Version v1.4.0 -> "v1.5.0"; an unverifiable pointer refuses */ }
+func TestDischargeIsUnqualifiedWithoutEnrolledAt(t *testing.T) { /* ruling 3 as main now allows: EnrolledAt zero -> Qualified false, reason; set -> boxvolumes.Discharge's verdict */ }
+func TestEdgesLiveSnapshotIsTheConsolesRead(t *testing.T) { /* console's existing eligibility tests pass through edgesfile.LiveSnapshot */ }
+func TestCreateDoorUsesTheChooserOnlyWithRegionHAOn(t *testing.T) { /* console: ConfigureClaimRuntime with RegionHA off -> Executors nil; on -> set */ }
+```
+
+- [ ] **Step 2: Run them to see them fail**
+
+Run: `nice -n 19 go test ./internal/host/ ./internal/edgesfile/ ./internal/console/ -run 'RelocatePass|RelocationPinner|RelocationRelease|DischargeIsUnqualified|EdgesLiveSnapshot|CreateDoorUsesTheChooser' -count=1`
+Expected: FAIL to compile.
+
+- [ ] **Step 3: The edges snapshot, shared**
+
+Move `readEdgesSnapshot`, `readLiveEdgesSnapshot` and `boxSnapshot` (console/instancesplacement.go, boxchoice.go) into `internal/edgesfile/snapshot.go` as `LiveSnapshot(ctx, s release.Store, now time.Time) (boxchoice.Snapshot, error)`, keeping the console's `edgesSnapshot` type for its displays by having the console build it from the same read (`edgesfile` already imports `status`, fold.go:10; `boxchoice` imports only `boxid`, so no cycle). The console's three callers read through it.
+
+- [ ] **Step 4: The runtime**
+
+`internal/host/relocatepass.go`:
+
+```go
+// relocatePassEnabled: the claim and region HA both on (spec §8 step 2).
+func relocatePassEnabled(getenv func(string) string) bool {
+ return hostwrite.Enabled(getenv) && regionHAEnabled(getenv)
+}
+
+// RelocationRuntime is one agent's relocation: its worker, participant,
+// effects and owner source, built once the claim assembly exists.
+type RelocationRuntime struct {
+ Worker hostrelocate.Worker
+ Participant hostrelocate.Participant
+ Effects *RelocationEffects
+ alerted sync.Map // host\x00cause -> struct{}: one alert per host and cause (rulings 35, 36)
+}
+
+// NewRelocationRuntime binds the production providers. It returns
+// (nil, nil) with either flag off.
+func NewRelocationRuntime(ctx context.Context, lifetime context.Context, getenv func(string) string, store release.Store, a *ClaimAssembly, reg *registry.Registry, svc *activator.Service, dd string) (*RelocationRuntime, error) {
+ if !relocatePassEnabled(getenv) || a == nil || a.RetirementAuthority == nil || svc == nil {
+ return nil, nil
+ }
+ pub, err := release.ParsePublicKey(getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY")
+ if err != nil {
+ return nil, err
+ }
+ enrollment, err := hostfloor.ReadEnrollment("/etc/carlos/host-actor.json")
+ if err != nil {
+ return nil, err
+ }
+ d := a.RetirementAuthority.Directory
+ roster, err := d.Read(ctx)
+ if err != nil {
+ return nil, err
+ }
+ self, ok := roster.Roster.Boxes[enrollment.Box]
+ if !ok {
+ return nil, hostbox.ErrChanged
+ }
+ region, dataBucket := self.Region, getenv("CARLOS_HIBERNATE_BUCKET")
+ versioned, err := lease.NewVersionedS3(ctx, map[string]string{dataBucket: self.Cloud.Region})
+ if err != nil {
+ return nil, err
+ }
+ var secrets *ecdh.PrivateKey
+ if raw := getenv("CARLOS_SECRETS_KEY"); raw != "" {
+ if secrets, err = appconfig.ParsePrivateKey(raw, "CARLOS_SECRETS_KEY"); err != nil {
+ return nil, err
+ }
+ }
+ cfg := RelocationAssemblyConfig{Lifetime: lifetime,
+ Exec: execctl.Config{Store: store, Guard: a.Guard, Directory: d, Clock: execctl.SystemClock{}, CloseAdmission: func() {}, CancelWork: func() {}},
+ Directory: d, Data: a.Data.(release.Store), Versions: versioned, Bucket: dataBucket, Region: region, Registry: reg,
+ FilesDir: func(host string) string { return filepath.Join(dd, host) }, EnvPath: func(host string) string { return filepath.Join(dd, host+".env") },
+ DataDir: dd, WorkDir: filepath.Join(dd, "relocate", "build"), ReleaseDir: filepath.Join(dd, "relocate", "release"), SecretsKey: secrets,
+ Probe: svc.ProbeWake, Now: time.Now}
+ effects := a.RelocationEffects(cfg)
+ chooser := hostrelocate.RegionChooser{Records: store, Namespace: self.Namespace, Region: region,
+ Executors: hostops.ExecutorChooser{Directory: d, Edges: func(ctx context.Context) (boxchoice.Snapshot, error) { return edgesfile.LiveSnapshot(ctx, store, time.Now()) },
+ Loads: func(ctx context.Context) (map[string]boxchoice.Load, error) { return hostops.ClaimLoads(ctx, store) }, SecretsKeyFP: expectedSecretsFP(secrets), HomeName: region, Now: time.Now}}
+ worker := hostrelocate.Worker{Claims: store, Directory: d,
+ Fences: hostfence.ClaimFenceAuthority{Directory: d, Now: time.Now},
+ Releases: StopReceiptReleases{Receipts: RelocationReleaseReceipts{Store: store, Directory: d}},
+ Admission: relocationAppAdmission{Store: store, Owner: hostops.RegistrationOwner{...}}, // claim Plan 5's app gate: hostops' registration under a box owner tuple (ruling 4)
+ Census: func(ctx context.Context, starter string, parts []string) error {
+ floor, err := hostfloor.Read(ctx, store, pub, d.Bucket)
+ if err != nil {
+ return err
+ }
+ _, err = d.CensusBoxRelocation(ctx, hostactors.ExactEvidence{PublicKey: pub, Store: store, Boxes: d.PrivateBoxReport, Self: localBoxReport(store)}, floor,
+ hostactors.Scope{StarterBox: starter, Participants: parts, Namespace: self.Namespace, Regions: []string{region}, Pools: []string{self.Pool}}, time.Now())
+ return err
+ },
+ Choose: chooser.Choose, Eligible: chooser.Eligible,
+ Relocatable: func(ctx context.Context, obs hostclaim.Observed) error { _, err := relocationRecord(ctx, cfg, store, obs); return err },
+ Leases: func(ctx context.Context, location string) (hostrelocate.Lease, error) { return lease.Acquire(ctx, dataBucket, location, region) },
+ Pins: hostrelocate.StorePinner{Claims: store, Data: cfg.Data, Bucket: dataBucket, Region: region,
+ SelectDB: func(ctx context.Context, r genselect.Request) (*genselect.Plan, error) { return genselect.Select(ctx, versioned, r) },
+ Release: channelRelease(store, pub)}, // ruling 30
+ Prepared: PreparedFromInstallManifest{Claims: store, Files: cfg.Data, Data: versioned, Bucket: dataBucket},
+ Discharge: volumeDischarge(store),
+ Notify: func(ctx context.Context) { _ = release.BumpSerial(ctx, store, time.Now()) },
+ Self: termfence.WrittenBy{Box: enrollment.Box, Enrollment: enrollment.Enrollment},
+ Region: region, Controller: newClaimID(), NewID: newClaimID, Now: time.Now}
+ participant := hostrelocate.Participant{Claims: store, Box: enrollment.Box, Enrollment: enrollment.Enrollment, Effects: effects, Now: time.Now}
+ return &RelocationRuntime{Worker: worker, Participant: participant, Effects: effects}, nil
+}
+
+// channelRelease is ruling 30: the host's channel pointer in the control
+// bucket, by the holder's account and app and the record's channel.
+func channelRelease(store release.Store, pub ed25519.PublicKey) func(context.Context, string) (string, error) {
+ return func(ctx context.Context, host string) (string, error) {
+ obs, err := hostclaim.Read(ctx, store, host)
+ if err != nil || obs.Absent || obs.Claim.Holder == nil {
+ return "", errors.Join(err, hostrelocate.ErrRefused)
+ }
+ rec, err := provision.GetRecord(ctx, store, obs.Claim.Holder.Account, obs.Claim.Holder.App, host)
+ if err != nil {
+ return "", err
+ }
+ p, err := release.Channel(ctx, store, pub, obs.Claim.Holder.Account, obs.Claim.Holder.App, rec.Channel)
+ if err != nil {
+ return "", err
+ }
+ return p.Version, nil
+ }
+}
+
+// volumeDischarge is ruling 3 with enrolled_at now in the roster
+// (claim Plan 11 Task A): unqualified without it.
+func volumeDischarge(store release.Store) func(context.Context, hostactors.Box, termfence.Record) *hostclaim.LocalDischarge {
+ return func(ctx context.Context, b hostactors.Box, f termfence.Record) *hostclaim.LocalDischarge {
+ if b.EnrolledAt.IsZero() {
+ return &hostclaim.LocalDischarge{Reason: "the roster records no enrollment time"}
+ }
+ at, err := time.Parse(time.RFC3339Nano, f.ObservedAt)
+ if err != nil {
+ return &hostclaim.LocalDischarge{Reason: err.Error()}
+ }
+ records, err := boxvolumes.List(ctx, store, b.Enrollment)
+ if err != nil {
+ return &hostclaim.LocalDischarge{Reason: err.Error()}
+ }
+ v := boxvolumes.Discharge(records, boxvolumes.Entry{Box: b.BoxID, Enrollment: b.Enrollment, Cloud: b.Cloud}, b.EnrolledAt, at)
+ return &hostclaim.LocalDischarge{Qualified: v.Discharge, Reason: v.Reason}
+ }
+}
+
+// runRelocatePass replays every relocation in this box's region and this
+// box's participations, converts a fenced committed landing (J3), and
+// sweeps. It starts no relocation: carlos ops host relocate and spec 3's
+// placer call Worker.Start. One pass every 30 seconds and on a control
+// serial change.
+func (r *RelocationRuntime) Run(ctx context.Context, store release.Store, serial <-chan struct{}) {
+ tick := time.NewTicker(30 * time.Second)
+ defer tick.Stop()
+ for {
+ pass, cancel := context.WithTimeout(ctx, 2*time.Minute)
+ r.once(pass, store)
+ cancel()
+ select {
+ case <-ctx.Done():
+ return
+ case <-tick.C:
+ case <-serial:
+ }
+ }
+}
+
+func (r *RelocationRuntime) once(ctx context.Context, store release.Store) {
+ keys, err := store.List(ctx, "control/hosts/")
+ if err != nil {
+ log.Printf("relocate: list claims: %v", err)
+ return
+ }
+ for _, key := range keys {
+ host, ok := strings.CutSuffix(strings.TrimPrefix(key, "control/hosts/"), ".json")
+ if !ok || strings.Contains(host, "/") {
+ continue
+ }
+ obs, err := hostclaim.Read(ctx, store, host)
+ if err != nil || obs.Absent {
+ continue
+ }
+ switch obs.Claim.Op.Kind {
+ case hostclaim.KindLanding:
+ r.convertFencedLanding(ctx, store, obs) // step 6
+ continue
+ case hostclaim.KindRelocating:
+ default:
+ continue
+ }
+ if _, err := r.Worker.Step(ctx, host); err != nil && !errors.Is(err, hostrelocate.ErrNotYet) {
+ r.alert(host, "worker", err)
+ }
+ if _, err := r.Participant.Step(ctx, host); err != nil && !errors.Is(err, hostrelocate.ErrNotYet) {
+ r.alert(host, r.Participant.Box, err)
+ }
+ }
+ if err := r.Participant.Sweep(ctx); err != nil {
+ log.Printf("relocate: sweep controllers: %v", err)
+ }
+}
+
+// alert logs one line per host and cause (rulings 35, 36): a refusal that
+// never clears is visible, never silent, and never a storm.
+func (r *RelocationRuntime) alert(host, who string, err error) {
+ if _, said := r.alerted.LoadOrStore(host+"\x00"+err.Error(), struct{}{}); !said {
+ log.Printf("relocate %s: %s: %v", host, who, err)
+ }
+}
+```
+
+`relocationAppAdmission` is claim Plan 5's app-admission registration over `hostops`' `AppAdmission` (`apps/<acct>/<app>/admission.json`, production_create.go:24; `registerStart`, ingest.go:32): `Register` writes a registration under the box owner tuple and returns its id, `Recheck` re-reads it open; the exact helper is `hostops`' and this task calls it, adding an exported `hostops.RegisterBoxOperation`/`RecheckBoxOperation` pair if none is exported (the implementer checks `internal/hostops/enroll.go` and `ingest.go` first and lists the deviation). `localBoxReport` is the box's own census report as `hostfencepass.go`'s `hostFenceOnce` builds it. `expectedSecretsFP` is the fingerprint the edges file publishes (edgesfile.go). `relocationRecord` is Task 8c's.
+
+- [ ] **Step 5: Registration, the agent and the console**
+
+In `internal/host/agent.go`, before `claimEntryContext` (214): `if regionHAEnabled(os.Getenv) { if err := RegisterProductionClaimRelocationSource(NewRelocationSource(...)); err != nil { log.Fatal(err) } }` with the source's config filled from the same values `NewRelocationRuntime` uses (`dd`, `release.OptDir()`); and after `armMovesPass` (450): build the runtime from `entry.Assembly` (via `productionClaimFactory`'s entry) and `go runtime.Run(agentCtx, store, serialChanges)`. In `claimproduction.go:150`, inside the `regionHAEnabled` gate, hand the source `out.Serving.Launch`, `out.Serving.Files` and `out.Serving.Markers` (`source.SetServingSources(...)`) before `openClaimRelocationOwners`. In `internal/console/claim_runtime.go:71` set `a.Executors = s.executorChooser(d)` (nil with `Config.RegionHA` off: hostclaimexecutor.go).
+
+- [ ] **Step 6: The fenced committed landing (J3) and the no-target bound (J3-3)**
+
+`internal/host/relocationlanding.go`:
+
+```go
+// CommittedLandingRecords is hostclaim.CommittedLandingGuard over the
+// authoritative instance record: the move named by the claim's op has
+// committed for this incarnation (provision.MoveCommitted, no rollback),
+// and the regions and park scopes are the commit's (moves.CommitScopes).
+type CommittedLandingRecords struct{ Records MovesRecordReader }
+
+func (g CommittedLandingRecords) CheckLandingCommitted(ctx context.Context, o hostclaim.Observed, opID string) (hostclaim.CommittedLanding, error) {
+ c := o.Claim
+ if g.Records == nil || c == nil || c.Holder == nil || c.Op.ID != opID {
+ return hostclaim.CommittedLanding{}, hostclaim.ErrWrongState
+ }
+ rec, err := g.Records(ctx, c.Holder.Account, c.Holder.App, o.Host)
+ if err != nil {
+ return hostclaim.CommittedLanding{}, err
+ }
+ m := rec.Move
+ if rec == nil || m == nil || m.ID != opID || m.Incarnation != c.Holder.Incarnation || m.State != provision.MoveCommitted || m.Rollback != nil || m.CommittedAt == nil {
+ return hostclaim.CommittedLanding{}, hostclaim.ErrWrongState
+ }
+ source, staging, err := moves.CommitScopes(c, m.FlippedAt)
+ if err != nil {
+ return hostclaim.CommittedLanding{}, err
+ }
+ return hostclaim.CommittedLanding{FromRegion: m.From, ToRegion: m.To, SourceScope: source, StagingScope: staging}, nil
+}
+```
+
+`convertFencedLanding` (in relocatepass.go): for a landing claim whose `op.exec` box has a termination fence (`Worker.fence`, exported as `Fence(ctx, box) (*hostclaim.FenceRef, termfence.Record, error)`), call `hostclaim.ConvertCommittedLanding(ctx, store, obs, c.Op.ID, c.Op.Exec, *ref, r.Worker.Fences, CommittedLandingRecords{MovesRecordsFrom(store)}, r.Worker.Discharge(ctx, box, record), newClaimID)`; `ErrWrongState` (not committed) is moves' case and is not logged; any other error is alerted once.
+
+In `internal/hostrelocate/worker.go`, the `PhaseStarting` branch with no target: on a chooser error, parse the newest `fence.*.observed_at` and, `ReleaseDeadline` after it, `MarkStuck(id, "no eligible box for the converted landing after ...")`; before that return the chooser's error (the pass alerts once). Test `TestWorkerBoundsAConversionWaitingForATarget` beside `TestWorkerNamesAConvertedLandingsTargetBeforeItsPin`.
+
+- [ ] **Step 7: Run the tests**
+
+Run: `nice -n 19 go test ./internal/host/ ./internal/hostrelocate/ ./internal/edgesfile/ ./internal/console/ -run 'RelocatePass|RelocationPinner|RelocationRelease|DischargeIsUnqualified|EdgesLiveSnapshot|CreateDoorUsesTheChooser|WorkerBoundsAConversion|Eligib|Placement' -count=1`
+Expected: PASS.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add internal/host/relocatepass.go internal/host/relocatepass_test.go internal/host/relocationlanding.go internal/host/agent.go internal/host/claimproduction.go internal/hostrelocate/worker.go internal/hostrelocate/worker_test.go internal/edgesfile/snapshot.go internal/edgesfile/snapshot_test.go internal/console/
+git commit -m "host: with CARLOS_HOST_CLAIM and CARLOS_REGION_HA on, every box registers its relocation owners, replays its region's relocations and its own participations, and converts a fenced committed landing
+
+Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
+```
+
+- [ ] **Step 9: The rollout checklist** (not code; recorded in the plan and in the handoff)
+
+Before `CARLOS_REGION_HA` is set on any box with a second box in its region, on dev with two fixed boxes:
+
+1. **Floor and flags.** The release floor includes claim Plan 11's additions (its rollout section); `CARLOS_HOST_CLAIM` and `CARLOS_MOVES_PHASE1` are on; claim Plan 9's activation census and floor guard pass (`#250`); every box's root runs `isolated-unit-v3`.
+2. **Preflight.** On each box, a serving wake passes `ClaimRoleAssembly.Preflight` (the isolated launch): `TestRelocatePassStaysOffWhileThePreflightRefuses`'s production counterpart is the agent's log line at startup.
+3. **IAM (W3).** The box role may create-only `PutObject` under `control/hosts/*/evidence/*`, `control/relocations/*` and the data bucket's `instances/*/files/*/` prefixes; `DeleteObject` denies do not cover them (the lease-release outage rule).
+4. **Root's scan (W2).** Terminate a drain on dev: root's writers-stopped observation passes under the deployed `carlos-instance-control.service` (host PID namespace, ptrace), and the full stop's receipt lands with its `.sig`.
+5. **The live witness (W1).** With the real Litestream replicator, a drain's `WritersStopped` passes; if root refuses on a descriptor outside the hint, file the claim ask of ruling 29 and keep drains off.
+6. **M3.** Root's held-stage relay serves a relocating request (the target's `live/<host>` names `install_release`). Until then relocation waits in `installed`.
+7. **C1.** Drains stay off until the claim answers C1 (ruling 33). Test failover first: terminate the executor of one host; watch `starting -> pinning -> installing -> installed -> probing -> serving`, one cold restart on the target, the source's `park-local` left open (no `enrolled_at` discharge yet unless the roster has it).
+8. **J3.** A committed landing whose target is terminated converts; moves' M1/M2 close it.
+9. **L21.** The instance-key plan has landed, or every app in the region writes `instance.key` before its first manifest.
+10. **Capabilities.** Every box and edge publishes `region-ha-1` (spec §8 step 3).
+
+---
+
+### Task 11: The merge gate (L18)
+
+Unchanged from the 1b-next plan's Task 11 (L13558-13586), with the model at `origin/region-ha-two-box-build` (e814ece3 keeps the landing's op id) and these additions to the joint round's list: the committed-landing conversion on both sides of the commit; the one-cold-restart completion (ruling 32); a drain under ruling 28's order; a probe-failed target with C1 open going `stuck` at the deadline and never queuing an obligation.
+
+- [ ] **Step 1: Confirm the joint round has passed** (as L13562-13566; stop otherwise and record the gap on amadan #236).
+- [ ] **Step 2: Run the branch gate** once, outside the sandbox: `mkdir -p /tmp/k && go build ./... && TMPDIR=/tmp/k nice -n 19 make test`, then the three cross-builds.
+- [ ] **Step 3: Push and read CI**: `git push origin region-ha-1b-next-build && amadan ci status carlos/platform -branch region-ha-1b-next-build`. Merging is Paul's, through `amadan branch merge`, after re-merging main and waiting for CI on the merged tip.
+
+---
+
+## After this plan
+
+| What | Waits for |
+|---|---|
+| Drains (`reason: drain`, `rebalance`) | C1 |
+| The zero-downtime completion through `relocating/serving` | Task 11's round; a follow-up plan |
+| The L21 instance key | rastrillo #66, its own plan |
+| Automatic discharge of a fenced box's chores | roster entries with `enrolled_at` (now written by Plan 11 with the claim flag on) |
+| `carlos ops host relocate`, the console's view, spec 3's placer | Paul's copy review; spec 3 |
+| `control/relocations/` create-only policy | a tofu change (ruling 5) |
+
+## What cannot be planned yet
+
+- **The serving reconcile after a later stamp** (Review Focus 5): whether main's census accepts the target's tree at a new stamp is answered by Task 9's test over the real adapter; a refusal there is a claim ask, not a region HA change.
+- **The app-admission registration's exact helper** (ruling 4): `hostops` has the registry; whether an exported box-owner registration exists is checked at Task 10 and listed as a deviation if one is added.
+- **W1 and W2** are observable only on a box.
+
+## Self-review
+
+- **Spec coverage.** §5.8 release sequence (8a), pinned set and build (8b), local install and receipt (8c), probe admission with `KeepStamp`, coverage via `PredecessorCertified`, P opened once (8d), who acts and the passes (10), serving (9); §5.3 row cases under ruling 31 (9); §6 and §8 as the checklist (10); L18 (11).
+- **Placeholders.** Task 8b and Task 10 step 4 name functions by their 1b-next or main bodies rather than repeating them in full; each is cited by file and line and exists on this branch. Task 8d's `AdmissionEvidence` marks two lines to replace with the manifest-digest check it describes; the implementer removes the marker.
+- **Type consistency.** `RelocationEffects`' provider signatures are relocationeffects.go:478-536's; `RelocationAssemblyConfig` is shared by 8a to 10; `relocationInstallPath` is used by 8c and 8d; `CertifiedFor` by 8d and the participant; `MintHeld`'s `(rec, captured, status, op)` by 8c and 9's tests; `CommittedLandingRecords` by 10.
+- **Review Focus.** Each line names its test and task.
+- **Dependencies.** Every open ask on #236 is in the dependency table with a fallback; the plan builds nothing that assumes an answer.
diff --git a/docs/superpowers/plans/CODEX-PLAN-REGION-HA-STAGE-B-BRIEF-1.md b/docs/superpowers/plans/CODEX-PLAN-REGION-HA-STAGE-B-BRIEF-1.md
new file mode 100644
index 000000000..cb7610ff4
--- /dev/null
+++ b/docs/superpowers/plans/CODEX-PLAN-REGION-HA-STAGE-B-BRIEF-1.md
@@ -0,0 +1,25 @@
+# Astra round 1 brief: region HA 1b-next stage B plan (rev 1)
+
+**Review:** `docs/superpowers/plans/2026-10-10-region-ha-1b-next-stage-b.md`, rev 1, on branch `region-ha-1b-next-build`. Every signature in it is cited file:line against this branch at b250a1fc. Main is merged in at dc333fb5, including claim Plans 5, 6b, 8/8b, 9 and 11 and moves Plans 1–5.
+
+**Context:**
+- Stage A is built: Tasks 1–7 and 9a of `2026-10-07-region-ha-1b-next-relocation.md`, plus R9 and J3.
+- This plan rewrites the gated stage B (8a–c, 9, 10) against main's real claim assembly, and absorbs Task 11, the L18 merge gate.
+- The contracts are on amadan carlos/platform #236 (R1–R10, C1–C5, M1–M3, J2, J3, M10) and in claim Plan 11 (`docs/superpowers/plans/2026-10-09-host-lifecycle-claim-11-region-ha-integration.md`).
+- The spec is `docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md`.
+- Rulings 1–27 are the 1b-next plan's; this plan continues at 28.
+
+**Judge:**
+1. **Code against the branch.** Does every task's code compile against the real code on this branch? Are the cited signatures right? Does anything duplicate a concept main already implements? The rule is one implementation per concept, and main's wins.
+2. **Obligations.** Is every stage B obligation from the 1b-next plan's re-plan list, the ledger and the reviews met, re-homed or retired with a sound reason?
+3. **Open dependencies.** Is each fallback safe while its ask is unanswered (C1, M1–M3, W1–W3)? Above all: ruling 33 (drains stay off until C1) and ruling 32 (one cold restart at the end).
+4. **Authority.** Does any path let a box or a request supply authority that root or the region controller must own? Check:
+ - the pinned release (ruling 30, the channel pointer);
+ - `install_release`;
+ - the held-stage relay;
+ - the `WithRefusals` refusals;
+ - the pass-through `hostwrite.Work` in ruling 34.
+5. **Tests.** Are they real? Would each fail if its guard were removed?
+6. **Task 10.** Does the activation checklist cover everything needed before `CARLOS_REGION_HA` is turned on?
+
+Give findings P1/P2/P3 with file:line, then a verdict: ready to build, or not.