carlos / platform Public

plan: claim Plan 12 rev 1, moves integration completion (M2 loss-bound admission, M12/J5 target fence declaration, M3 recovery cross-box handover, M5 RunHeld and M6 lease-set contract, M7 retired-location census, M9 local authorizer, spec rev 6.32)

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codex agent, operated by paul@keymail.dev pushed by claude agent, operated by paul@keymail.dev 7facdb61d548a8ec742a06fe84a00bb9e0634657 parent b577afb
1 file changed, +2252 −0
  • docs/superpowers/plans/2026-10-10-host-lifecycle-claim-12-moves-integration.md +2252 −0

CI passed — run details

diff --git a/docs/superpowers/plans/2026-10-10-host-lifecycle-claim-12-moves-integration.md b/docs/superpowers/plans/2026-10-10-host-lifecycle-claim-12-moves-integration.md
new file mode 100644
index 000000000..fb8e1ce8a
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-10-host-lifecycle-claim-12-moves-integration.md
@@ -0,0 +1,2252 @@
+# Host lifecycle claim, Plan 12: moves integration completion
+
+Implementation plan, 2026-10-10, rev 1. This supplies the claim side of the controller's accepted moves delta answers M2, M3, M5, M6, M7, M9 and M12. Apply Tasks A–H in order. The amended claim spec is rev 6.32. This plan authorizes no production changes.
+
+Requirements: `/home/paulca/.cache/tmp/claude-1001/own/after236/plan12-brief.md`, and sections 2 (M1–M21) and 5 of `/home/paulca/amadan.net/carlos/platform-moves-p5/.superpowers/sdd/2026-10-06-moves-phase1-5-cleanup-and-parks/delta-after-plan10.md`. The controller's accepted answers override the delta's suggested rulings. In particular, the claim owns the instance-residue census; moves cannot be asked to invent that history.
+
+## Global Constraints
+
+- Every code hunk is anchored to `44c1b6d77ed16f92e46e892b8254835edbd567d8`, the required `origin/main` base carrying claim Plans 1–11, then the earlier tasks in this document. Never replace that input with the moving remote-tracking ref. It advanced during authoring; none of its subsequent changes are included.
+- Everything remains behind `CARLOS_HOST_CLAIM`. Group D's existing moves gate remains in place. No new initializer, registration, flag, service or production activation is added. With the flag off, existing bytes and behavior stay identical to the pinned base.
+- Keep #267: no by-name root file effect, no root SQLite open, and all AWS configuration loads through `rootaws`. This plan adds no AWS client and no privileged file effect. `Guard.Activate` and `Guard.With` retain their descriptor-based implementation. A source never manufactures a root receipt for a remote target session.
+- Read and preserve the underlying admission, stop, publication, park and loss checks. A cross-box predecessor proof is not a coverage certificate. A lease does not replace authorization. The new local authorizer does not weaken the regional one.
+- Existing wire data still parses. The new `retired_locations` map is optional. Because old claim readers are strict, deploy the new reader to every claim consumer before enabling writes of the new field; the release floor must reflect that requirement. Missing historical mappings remain a typed refusal.
+- Change census rows only with the classifications in Task G. No console route, record writer or moves copy sink is added here. Moves' later callers add their own classified rows when they land.
+- Author without git writes, commits, subagents or production activity. Export the pinned commit under `$TMPDIR`. Never create a private Go cache, never put one under `/tmp`, and remove only scratch paths this session created.
+- The two new diagnostic strings are inventory drafts, not approved product copy. The implementation owner must obtain copy approval before shipping them. This document and the report are implementation instructions for the controller.
+
+## Review Focus
+
+1. M2 reaches the real `ClaimWakeAssembly.Build` writer through `ClaimAssembly.Build`. The root public key comes from the verified production authority, while moves supplies only its loss policy and baseline validator. Omitting `Admission.LossID` must not admit clean-return coverage for an operation that journals a loss.
+2. J2(b) uses the actual predecessor admission when present. Clean return may already have changed `holder.generation` back to `src_gen`; the target's session and its admission generation remain the predecessor. Require the target stop's authenticated session, a matching committed loss source, or a freshly verified target fence. Never treat `StoppedReport.ClaimGen` as its session.
+3. `RunHeld` borrows an acquisition-bound exact bucket/location lease through the configured `AggregateLeases`. It must retain the caller's lease after success, pending, authorization failure and partial acquisition failure. All other constituents retain their normal lifecycle and cancellation.
+4. M6 uses data-bucket lock names only. The staging signature horizon remains the original persisted `evidence_at`. Instance scope and replica lease names currently coincide, so deduplication yields one lock.
+5. M7 distinguishes identity from evidence. The 32 retained IDs do not identify which one occupied the bare legacy location. Retain the location at future retirement, refuse unprovable old residue, and keep the legacy label separate from the moves proof's label.
+6. J5 writes a verified reference by CAS and has no other effect. Consuming transitions must queue their chores in their own CAS. A fence declaration cannot be passed off as a completed local or regional park, or as a signed stopped report.
+7. Preserve the unwrapped owner for `park.Local`; keep `NewClaimRegionalWorker`'s stronger wrapper on the returned regional engine. Recheck enrollment when the local callback runs, not just when the capability is obtained.
+8. Main's current unsigned `moves.StoppedReport` cannot satisfy the signed target-stop contract. Moves must supply that verifier and session-bearing evidence before enabling normal recovery. Test all three evidence forms here, then run moves' acceptance test with its real registered owners before rollout.
+
+## Published interfaces
+
+The patches below contain the complete definitions. Moves integrates with:
+
+```go
+// internal/hostbox
+// The record passed to Sources has already passed root-signature verification.
+type LossAdmissionSources func(context.Context, hostclaim.Observed,
+ hostclaim.LossRecord, hostclaim.Admission, *genselect.Plan) ([]hostclaim.ValidatedWriteSource, error)
+type LossAdmission struct {
+ PublicKey ed25519.PublicKey
+ Owner hostclaim.LossRecoveryOwner
+ Sources LossAdmissionSources
+}
+func ReplicaParkLeaseSet(context.Context, string, string) ([]string, error)
+func (AggregateLeases) AcquireHeld(context.Context, string, string, park.HeldLease) (park.HeldLease, error)
+
+// internal/host: new fields on the existing ClaimMovesOwners.
+// LossRecovery hostclaim.LossRecoveryOwner
+// LossSources hostbox.LossAdmissionSources
+// TargetStop ClaimTargetStopVerifier
+type ClaimTargetStop struct {
+ Host, OpID, Box, Incarnation, Generation string
+ Session uint64
+}
+type ClaimTargetStopVerifier func(context.Context, hostclaim.Observed, string) (ClaimTargetStop, error)
+type ClaimLocalParkAuthorizer func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error
+func (ClaimProviders) MoveParks(context.Context) (park.Engine, hostbox.LeaseSet, ClaimLocalParkAuthorizer, error)
+func ClaimRetirementCensus(hostclaim.Observed, []lease.ObjectVersion) (map[string]string, error)
+
+// internal/park
+func (Engine) RunHeld(context.Context, Work, HeldLease) error
+
+// internal/hostclaim
+func OperationLossID(*Claim) (string, error)
+func DeclareMoveTargetFence(context.Context, release.Store, Observed,
+ string, FenceRef, FenceAuthority) (Observed, error)
+```
+
+`TargetStop` is a trusted evidence adapter, just as `LossRecoveryOwner` is a trusted policy adapter. It must verify the evidence's signature against the target's enrolled signer, immutable object identity, complete stop checks and tuple before returning `ClaimTargetStop`. The claim checks the returned tuple again. A JSON decoder alone is not an implementation. The claim does not supply a signer or accept a caller's `Verified: true` field.
+
+The existing J4 `hostresolve.OperationOwner` and `AdmitWriterWithLoss` signatures remain unchanged. `MoveParks` gains a third value before its error; its only current callers on this base are the tests patched in Task F. Moves Task 9a must consume all four results.
+
+## Replay without git writes
+
+The Go tree comes from the pinned main commit. The design spec is intentionally held on this worktree's design branch, not in that archive. Task H therefore has one separately identified document input: this worktree's rev 6.31 spec, SHA-256 `1e86430c434e54c17c54f5ebfcd603cc45aa4803620d4a97649f7750be29540d`. Do not silently substitute another revision.
+
+```bash
+CLAIM12_REPO=/home/paulca/amadan.net/carlos/platform/.claude/worktrees/replica-ownership
+CLAIM12_BASE=44c1b6d77ed16f92e46e892b8254835edbd567d8
+CLAIM12_PLAN="$CLAIM12_REPO/docs/superpowers/plans/2026-10-10-host-lifecycle-claim-12-moves-integration.md"
+CLAIM12_SCRATCH=$(mktemp -d "$TMPDIR/claim12.XXXXXX")
+export CLAIM12_REPO CLAIM12_BASE CLAIM12_PLAN CLAIM12_SCRATCH
+mkdir "$CLAIM12_SCRATCH/tree"
+git -C "$CLAIM12_REPO" archive "$CLAIM12_BASE" | tar -x -C "$CLAIM12_SCRATCH/tree"
+mkdir -p "$CLAIM12_SCRATCH/tree/docs/superpowers/specs"
+cp "$CLAIM12_REPO/docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md" "$CLAIM12_SCRATCH/tree/docs/superpowers/specs/"
+cd "$CLAIM12_SCRATCH/tree"
+# Use the default shared Go cache. No per-task cache or downloads.
+export GOFLAGS=-mod=mod GOPROXY=off
+```
+
+Save the following as `$CLAIM12_SCRATCH/apply.py`. It rejects absent stages and any fuzz, offset or already-applied result. It only touches the exported files, never the git directory.
+
+```python
+import os, pathlib, re, subprocess, sys
+plan, task, phase = sys.argv[1:]
+assert phase in ('tests', 'implementation')
+text = pathlib.Path(plan).read_text()
+blocks = [body for name, stage, body in re.findall(
+ r'<!-- apply: (\w+) (tests|implementation) -->\n```diff\n(.*?)\n```',
+ text, re.S) if name == task and stage == phase]
+if not blocks:
+ raise SystemExit('no patch for this task/stage; G and H use existing tests')
+for body in blocks:
+ result = subprocess.run(['patch', '--batch', '--forward', '--fuzz=0', '-p1'],
+ input=body + '\n', text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ print(result.stdout, end='')
+ if result.returncode or re.search(r'offset|fuzz|previously applied|Reversed', result.stdout):
+ raise SystemExit('patch did not apply exactly')
+```
+
+For each task, apply its tests before its implementation and run its focused command. New names that do not exist yet are a valid first compile failure. A cache, socket, permission or dependency failure is not evidence that the regression caught the defect. Apply the implementation and rerun that command before proceeding. Tasks G and H test existing inventories and spec assertions, so they do not have a `tests` patch.
+
+Example for Task A:
+
+```bash
+python3 "$CLAIM12_SCRATCH/apply.py" "$CLAIM12_PLAN" A tests
+go test ./internal/hostbox -run TestPlan12LossAdmission -count=1
+python3 "$CLAIM12_SCRATCH/apply.py" "$CLAIM12_PLAN" A implementation
+go test ./internal/hostbox ./internal/hostclaim -run 'TestPlan12|TestAdmitWriterWithLoss' -count=1
+```
+
+## Task A: Route declared recovery through the loss admission primitive (M2)
+
+The current Group D writer always calls the pure `AdmitWriter` transition. Add the typed loss configuration to the writer, pass it through production construction, and freeze the choice from the operation's journal. The primitive itself also rejects a loss-bearing operation's attempt to bypass verification. Stale CAS is returned once; evidence must be selected again by the caller.
+
+Tests cover a signed committed declaration, missing owner and source adapters, wrong source identity, wrong operation, wrong signing key, stale observation, both invalid coverage forms and direct primitive bypass. Existing loss-library tests continue to cover every frozen additional source and duplicate identities.
+
+Focused command: `go test ./internal/hostbox ./internal/hostclaim -run 'TestPlan12|TestAdmitWriterWithLoss' -count=1`.
+
+Expected first failure: the writer has no `Loss` field or `LossAdmission` type. After implementation, the successful admission carries `LossID`, uncovered cold coverage and the target's exact write loss. Moves' callbacks remain unset until its recovery owners register them; there is no clean-return fallback.
+
+### Failing tests
+
+<!-- apply: A tests -->
+```diff
+--- /dev/null
++++ b/internal/hostbox/plan12_loss_test.go
+@@ -0,0 +1,114 @@
++//go:build unix
++
++package hostbox
++
++import (
++ "context"
++ "crypto/ed25519"
++ "encoding/json"
++ "errors"
++ "github.com/carlosframework/platform/internal/genselect"
++ "github.com/carlosframework/platform/internal/hostclaim"
++ "github.com/carlosframework/platform/internal/release"
++ "testing"
++ "time"
++)
++
++type plan12LossOwner struct{ calls int }
++
++func (p *plan12LossOwner) ValidateLossRecovery(c hostclaim.Claim, l hostclaim.LossRecord, a hostclaim.Admission) error {
++ p.calls++
++ if c.Op.Kind != hostclaim.KindRestoring || l.OpID != c.Op.ID || a.Box != "box" {
++ return hostclaim.ErrLossRecord
++ }
++ return nil
++}
++func TestPlan12LossAdmission(t *testing.T) {
++ for _, mode := range []string{"ok", "clean-return", "sealed", "missing-owner", "missing-source", "wrong-source", "wrong-op", "wrong-signature", "stale", "bypass"} {
++ t.Run(mode, func(t *testing.T) {
++ _, store, o := workFixture(t)
++ var err error
++ o, err = hostclaim.Update(t.Context(), store, o, func(c *hostclaim.Claim) error {
++ c.Op = hostclaim.Op{Kind: hostclaim.KindRestoring, ID: "op", Phase: hostclaim.PhaseProbing, Exec: "box", Participants: []string{"box"}, Journal: map[string]string{"loss.loss_1": "{}"}}
++ c.Holder.Session, c.Holder.Generation, c.Holder.WriterBox = 2, "target-gen", "target"
++ c.LossRefs = []string{"loss_1"}
++ return nil
++ })
++ if err != nil {
++ t.Fatal(err)
++ }
++ pub, priv, err := ed25519.GenerateKey(nil)
++ if err != nil {
++ t.Fatal(err)
++ }
++ now := time.Now().UTC()
++ r := hostclaim.LossRecord{V: 1, ID: "loss_1", State: "prepared", Actor: "operator", AcceptedAt: now, Host: o.Host, HolderIncarnation: o.Claim.Holder.Incarnation, HolderStamp: o.Claim.Holder.Stamp, HolderSession: 2, ExpectedClaimGen: o.Claim.Gen, ExpectedClaimETag: o.ETag, OpID: "op", OpKind: hostclaim.KindLanding, OpPhase: "probing", Box: "target", Enrollment: "enroll", QuarantineRevision: 1,
++ CloudObservation: hostclaim.LossCloudObservation{ObservedAt: now, Account: "123", Region: "eu-west-1", InstanceID: "i-1", State: "stopped", RequestID: "request"}, OperatorAttestation: hostclaim.LossOperatorAttestation{MaintainComputeFence: true, ColdQuarantineStartOnly: true}, FencedAt: now,
++ ConfirmedConsequences: hostclaim.LossConsequences{RevokeExecution: true, RemoveParticipation: true, RecoveryIntent: json.RawMessage(`{"kind":"move-target-lost"}`)}, Timers: hostclaim.TimerLoss{Knowledge: hostclaim.TimersUnknown}, WriteSources: []hostclaim.WriteSource{{Reason: "target", Box: "target", Session: 2, Generation: "target-gen", Region: "uk", Baseline: "target-snapshot"}}}
++ if mode == "wrong-op" {
++ r.OpID = "different"
++ }
++ created, err := hostclaim.CreateLossRecord(t.Context(), store, release.RootSigner(priv), r)
++ if err != nil {
++ t.Fatal(err)
++ }
++ r.State, r.ResultClaimGen = "committed", o.Claim.Gen
++ if _, err = hostclaim.ReplaceLossRecord(t.Context(), store, release.RootSigner(priv), created, r); err != nil {
++ t.Fatal(err)
++ }
++ owner := &plan12LossOwner{}
++ w := Writer{Claims: store, Box: "box", Region: "ie", AdmissionEvidence: func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error { return nil }, Loss: &LossAdmission{PublicKey: pub, Owner: owner}}
++ w.Loss.Sources = func(_ context.Context, _ hostclaim.Observed, r hostclaim.LossRecord, _ hostclaim.Admission, _ *genselect.Plan) ([]hostclaim.ValidatedWriteSource, error) {
++ if len(r.WriteSources) != 1 || r.WriteSources[0].Baseline != "target-snapshot" {
++ t.Fatal("not frozen sources")
++ }
++ v := hostclaim.ValidatedWriteSource{Reason: "target", Box: "target", Session: 2, Generation: "target-gen", Region: "uk", Baseline: "target-snapshot", RemoteMaxTXID: 7}
++ if mode == "wrong-source" {
++ v.Session++
++ }
++ return []hostclaim.ValidatedWriteSource{v}, nil
++ }
++ if mode == "missing-owner" {
++ w.Loss.Owner = nil
++ }
++ if mode == "missing-source" {
++ w.Loss.Sources = nil
++ }
++ if mode == "wrong-signature" {
++ w.Loss.PublicKey, _, _ = ed25519.GenerateKey(nil)
++ }
++ in := hostclaim.Admission{ObservedOpID: "op", ObservedPhase: hostclaim.PhaseProbing, ObservedSession: 2, ObservedGeneration: "target-gen", Box: "box", Mode: "cold", NewGeneration: "returned", Region: "ie", PredecessorRegion: "uk", RemoteMaxTXID: 7}
++ if mode == "clean-return" || mode == "sealed" {
++ in.Coverage = mode
++ }
++ if mode == "stale" {
++ _, err = hostclaim.Update(t.Context(), store, o, func(c *hostclaim.Claim) error { return nil })
++ if err != nil {
++ t.Fatal(err)
++ }
++ }
++ var admitted hostclaim.Observed
++ if mode == "bypass" {
++ admitted, err = hostclaim.Update(t.Context(), store, o, hostclaim.AdmitWriter(in))
++ } else {
++ admitted, err = w.Admit(t.Context(), o, in, nil)
++ }
++ if mode != "ok" {
++ if err == nil {
++ t.Fatal("accepted", mode)
++ }
++ if mode == "stale" && !errors.Is(err, hostclaim.ErrConflict) {
++ t.Fatal(err)
++ }
++ return
++ }
++ if err != nil {
++ t.Fatal(err)
++ }
++ a := admitted.Claim.Holder.Admissions[0]
++ if a.LossID != "loss_1" || a.Coverage != "" || a.WritesLost == nil || a.WritesLost.Session != 2 || owner.calls != 1 {
++ t.Fatalf("%+v owner=%d", a, owner.calls)
++ }
++ })
++ }
++}
+```
+
+### Implementation
+
+<!-- apply: A implementation -->
+```diff
+--- /dev/null
++++ b/internal/hostbox/writer_loss.go
+@@ -0,0 +1,40 @@
++package hostbox
++
++import (
++ "context"
++ "crypto/ed25519"
++ "github.com/carlosframework/platform/internal/genselect"
++ "github.com/carlosframework/platform/internal/hostclaim"
++)
++
++// LossAdmissionSources validates every frozen source against its remote baseline.
++// The record is verified by the writer before this callback receives it.
++type LossAdmissionSources func(context.Context, hostclaim.Observed, hostclaim.LossRecord, hostclaim.Admission, *genselect.Plan) ([]hostclaim.ValidatedWriteSource, error)
++
++type LossAdmission struct {
++ PublicKey ed25519.PublicKey
++ Owner hostclaim.LossRecoveryOwner
++ Sources LossAdmissionSources
++}
++
++func (w Writer) admitLoss(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, baseline *genselect.Plan, id string) (hostclaim.Observed, error) {
++ if w.Loss == nil || w.Loss.Owner == nil || w.Loss.Sources == nil || len(w.Loss.PublicKey) != ed25519.PublicKeySize {
++ return hostclaim.Observed{}, hostclaim.ErrLossOwnerRequired
++ }
++ if o.Claim.Op.Kind != hostclaim.KindRestoring || o.Claim.Op.Phase != hostclaim.PhaseProbing || in.Coverage != "" || in.KeepStamp || (in.LossID != "" && in.LossID != id) {
++ return hostclaim.Observed{}, hostclaim.ErrLossRecord
++ }
++ loss, err := hostclaim.ReadLossRecord(ctx, w.Claims, w.Loss.PublicKey, id)
++ if err != nil {
++ return hostclaim.Observed{}, err
++ }
++ r := loss.Record
++ if r.State != "committed" || r.Host != o.Host || r.OpID != o.Claim.Op.ID || r.HolderIncarnation != o.Claim.Holder.Incarnation {
++ return hostclaim.Observed{}, hostclaim.ErrLossRecord
++ }
++ sources, err := w.Loss.Sources(ctx, o, r, in, baseline)
++ if err != nil {
++ return hostclaim.Observed{}, err
++ }
++ return hostclaim.AdmitWriterWithLoss(ctx, w.Claims, w.Loss.PublicKey, o, in, id, sources, w.Loss.Owner)
++}
+--- /dev/null
++++ b/internal/hostclaim/operation_loss.go
+@@ -0,0 +1,23 @@
++package hostclaim
++
++import "strings"
++
++// OperationLossID selects only a declaration journalled under this operation.
++// Old claim-level LossRefs alone do not turn a later move into a loss recovery.
++func OperationLossID(c *Claim) (string, error) {
++ if c == nil {
++ return "", ErrLossRecord
++ }
++ id := ""
++ for key := range c.Op.Journal {
++ if !strings.HasPrefix(key, "loss.") {
++ continue
++ }
++ next := strings.TrimPrefix(key, "loss.")
++ if c.Op.ID == "" || !safeLossID(next) || !lossRefPresent(c, next) || id != "" {
++ return "", ErrLossRecord
++ }
++ id = next
++ }
++ return id, nil
++}
+--- a/internal/hostbox/writer.go
++++ b/internal/hostbox/writer.go
+@@ -12,6 +12,7 @@
+
+ // Writer supplies transition adapters, not stop, certificate or marker evidence.
+ type Writer struct {
++ Loss *LossAdmission
+ Claims release.Store
+ Data genselect.Reader
+ Bucket, Region, Box string
+@@ -40,6 +41,16 @@
+ if err := w.AdmissionEvidence(ctx, observed, in, baseline); err != nil {
+ return hostclaim.Observed{}, err
+ }
++ lossID, err := hostclaim.OperationLossID(observed.Claim)
++ if err != nil {
++ return hostclaim.Observed{}, err
++ }
++ if lossID != "" {
++ return w.admitLoss(ctx, observed, in, baseline, lossID)
++ }
++ if in.LossID != "" {
++ return hostclaim.Observed{}, hostclaim.ErrLossRecord
++ }
+ // Exactly one attempt. A conflict requires new evidence, not an internal retry.
+ return hostclaim.Update(ctx, w.Claims, observed, hostclaim.AdmitWriter(in))
+ }
+--- a/internal/hostclaim/generation.go
++++ b/internal/hostclaim/generation.go
+@@ -48,6 +48,13 @@
+ return err
+ }
+ }
++ id, err := OperationLossID(c)
++ if err != nil {
++ return err
++ }
++ if id != "" && (!in.lossVerified || in.LossID != id || in.Coverage != "") {
++ return ErrLossRecord
++ }
+ h := c.Holder
+ if in.LossID != "" && !in.lossVerified {
+ return fmt.Errorf("%w: declaration-bound admission requires verified durable declaration", ErrWrongState)
+--- a/internal/host/claimowners.go
++++ b/internal/host/claimowners.go
+@@ -39,13 +39,15 @@
+ // with CARLOS_MOVES_PHASE1 on. ProbeReady replaces the claim's probe
+ // readiness check when set.
+ type ClaimMovesOwners struct {
++ LossRecovery hostclaim.LossRecoveryOwner
++ LossSources hostbox.LossAdmissionSources
+ ClaimKindOwners
+ Operations OperationAttempts
+ ProbeReady ClaimProbeReadiness
+ }
+
+ func (m ClaimMovesOwners) zero() bool {
+- return m.Inputs == nil && m.AdmissionEvidence == nil && m.PublicationEvidence == nil && m.Operations == nil && m.ProbeReady == nil
++ return m.LossRecovery == nil && m.LossSources == nil && m.Inputs == nil && m.AdmissionEvidence == nil && m.PublicationEvidence == nil && m.Operations == nil && m.ProbeReady == nil
+ }
+
+ // ClaimRelocationOwners is region HA's owner set for the relocating kind
+--- a/internal/host/claimassembly.go
++++ b/internal/host/claimassembly.go
+@@ -91,6 +91,7 @@
+ // ClaimAssembly owns the concrete wiring. Dependencies retain their contract
+ // names so a missing implementation cannot silently become permissive policy.
+ type ClaimAssembly struct {
++ LossAdmission *hostbox.LossAdmission
+ moveRead *claimMoveReadAccess
+ wakeUnavailable error
+ Journals *hostbox.JournalService
+@@ -176,7 +177,7 @@
+ a.Provision = a.Reconcile.Configure
+ check := claimBoundIdentity(a.Identity.Verify, actor)
+ data := hostbox.RequestStore{Store: a.Data, Check: check}
+- writer := hostbox.Writer{Claims: a.Claims, Data: data, Bucket: actor.Bucket, Region: actor.Region, Box: actor.Box, AdmissionEvidence: a.AdmissionEvidence, PublicationEvidence: a.PublicationEvidence, NewID: a.NewID}
++ writer := hostbox.Writer{Loss: a.LossAdmission, Claims: a.Claims, Data: data, Bucket: actor.Bucket, Region: actor.Region, Box: actor.Box, AdmissionEvidence: a.AdmissionEvidence, PublicationEvidence: a.PublicationEvidence, NewID: a.NewID}
+ a.bindClaimWakeStop()
+ if local, ok := a.Local.(*productionClaimLocal); ok {
+ if err := local.bind(ctx, &a); err != nil {
+--- a/internal/host/claimconstruction.go
++++ b/internal/host/claimconstruction.go
+@@ -331,6 +331,9 @@
+ stop.Inventory = inventory
+ stop.NewID = newID
+ assembly := &ClaimAssembly{moveRead: owners.moveRead, wakeUnavailable: owners.wakeUnavailable, Journals: journals, cleanupEnrollment: owners.cleanupEnrollment, RetirementAuthority: owners.RetirementAuthority, SharedPaths: claimGlobalSharedPaths(r), Mode: r.Mode, Producers: producers, Launches: launches, ManagedRoots: roots, Reconcile: reconcile, RootJournal: launches, Controllers: owners.Controllers, Claims: store, Guard: guard, Identity: owners.Identity, Local: owners.Local, Policy: hostbox.Policy{Inputs: owners.InputPolicy, Moves: owners.MovesPolicy, Relocation: groupD.Relocation, Held: hostbox.RegistryHoldPolicy(r.Registry)}.WithRefusals(owners.PolicyRefusals...), Data: owners.Data, Wake: wake, Stop: stop, Evidence: owners.Evidence, LeaseSet: owners.LeaseSet, View: owners.View, Authorize: owners.Authorize, AdmissionEvidence: admissionEvidence, PublicationEvidence: publicationEvidence, NewID: newID}
++ if movesC1 && owners.RetirementAuthority != nil {
++ assembly.LossAdmission = &hostbox.LossAdmission{PublicKey: owners.RetirementAuthority.Directory.PublicKey, Owner: owners.Moves.LossRecovery, Sources: owners.Moves.LossSources}
++ }
+ var closeMu sync.Mutex
+ closed := false
+ assembly.Close = func(ctx context.Context) error {
+```
+
+## Task B: Declare a target fence without inventing release effects (M12, J5)
+
+Add a typed evidence declaration for the post-flip target. It binds the observed operation, target enrollment and region to `VerifyFence`, then records the reference in one CAS. Identical replay re-verifies evidence and the live ETag without another write. A differing reference refuses.
+
+This function is deliberately an evidence declaration. It changes no record, phase, executor, holder, participant or chore. Therefore it introduces no unfinished effect requiring a new obligation kind. The moves recovery owner must persist its record decision and queue every park in the consuming transition; it must not use this call as a release or park acknowledgment. This preserves the same evidence-recording shape as `ConvertMovingToRelocating` without borrowing that function's pre-flip authority transfer.
+
+Focused command: `go test ./internal/hostclaim -run 'TestPlan12TargetFenceDeclaration|TestC7' -count=1`.
+
+Expected first failure: `DeclareMoveTargetFence` is undefined. Test both permitted kinds, wrong operation/box/enrollment/region, missing verification, a committed move, stale CAS, replay, and preservation of holder and obligations.
+
+### Failing tests
+
+<!-- apply: B tests -->
+```diff
+--- /dev/null
++++ b/internal/hostclaim/plan12_fence_test.go
+@@ -0,0 +1,72 @@
++package hostclaim
++
++import (
++ "errors"
++ "reflect"
++ "testing"
++)
++
++func TestPlan12TargetFenceDeclaration(t *testing.T) {
++ for _, kind := range []string{KindLanding, KindRestoring} {
++ for _, mode := range []string{"ok", "wrong-op", "wrong-target", "wrong-enrollment", "wrong-region", "unverified", "stale", "committed"} {
++ t.Run(kind+"/"+mode, func(t *testing.T) {
++ s, o, ref, a := c7Fixture(t)
++ var err error
++ o, err = Update(t.Context(), s, o, func(c *Claim) error {
++ c.Op.Kind, c.Op.Phase = kind, PhaseProbing
++ c.Op.Intent["target"], c.Op.Intent["source"], c.Op.Intent["to"], c.Op.Intent["target_enrollment"] = "adi-knitting", "bri-knitting", "ie", ref.Enrollment
++ if mode == "wrong-target" {
++ c.Op.Intent["target"] = "elsewhere"
++ }
++ if mode == "wrong-enrollment" {
++ c.Op.Intent["target_enrollment"] = "bad"
++ }
++ if mode == "wrong-region" {
++ c.Op.Intent["to"] = "uk"
++ }
++ if mode == "committed" {
++ c.Op.Phase = PhaseCommitted
++ }
++ return nil
++ })
++ if err != nil {
++ t.Fatal(err)
++ }
++ op := o.Claim.Op.ID
++ if mode == "wrong-op" {
++ op = "different"
++ }
++ if mode == "unverified" {
++ a.err = ErrFence
++ }
++ if mode == "stale" {
++ _, err = Update(t.Context(), s, o, func(*Claim) error { return nil })
++ if err != nil {
++ t.Fatal(err)
++ }
++ }
++ s.puts = 0
++ got, err := DeclareMoveTargetFence(t.Context(), s, o, op, ref, a)
++ if mode != "ok" {
++ if err == nil {
++ t.Fatal("accepted", mode)
++ }
++ if mode == "stale" && !errors.Is(err, ErrConflict) {
++ t.Fatal(err)
++ }
++ return
++ }
++ if err != nil {
++ t.Fatal(err)
++ }
++ if s.puts != 1 || got.Claim.Op.Journal["fence.adi-knitting"] == "" || !reflect.DeepEqual(o.Claim.Holder, got.Claim.Holder) || !reflect.DeepEqual(o.Claim.Obligations, got.Claim.Obligations) || got.Claim.Op.Exec != o.Claim.Op.Exec || got.Claim.Op.Phase != o.Claim.Op.Phase {
++ t.Fatal("unexpected effects")
++ }
++ again, err := DeclareMoveTargetFence(t.Context(), s, got, op, ref, a)
++ if err != nil || again.ETag != got.ETag || s.puts != 1 {
++ t.Fatal("replay", err)
++ }
++ })
++ }
++ }
++}
+```
+
+### Implementation
+
+<!-- apply: B implementation -->
+```diff
+--- /dev/null
++++ b/internal/hostclaim/move_target_fence.go
+@@ -0,0 +1,47 @@
++package hostclaim
++
++import (
++ "context"
++ "encoding/json"
++ "github.com/carlosframework/platform/internal/release"
++)
++
++// DeclareMoveTargetFence records evidence only. It does not acknowledge a
++// park, transfer execution or advance the move record. ReturnToSource and
++// the recovery owner still have to queue their retirement work atomically.
++func DeclareMoveTargetFence(ctx context.Context, s release.Store, o Observed, opID string, ref FenceRef, authority FenceAuthority) (Observed, error) {
++ if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.ETag == "" || opID == "" || o.Claim.Op.ID != opID {
++ return Observed{}, ErrStaleOp
++ }
++ c := o.Claim
++ if (c.Op.Kind != KindLanding && c.Op.Kind != KindRestoring) || c.Op.Phase == PhaseCommitted || c.Op.Intent["target"] == "" || c.Op.Intent["target"] == c.Op.Intent["source"] || c.Op.Intent["target_enrollment"] != ref.Enrollment {
++ return Observed{}, ErrWrongState
++ }
++ fence, err := VerifyFence(ctx, s, authority, ref)
++ if err != nil {
++ return Observed{}, err
++ }
++ if fence.Box() != c.Op.Intent["target"] || fence.Region() != c.Op.Intent["to"] {
++ return Observed{}, ErrFence
++ }
++ raw, err := json.Marshal(ref)
++ if err != nil {
++ return Observed{}, err
++ }
++ key := "fence." + fence.Box()
++ if old, ok := c.Op.Journal[key]; ok {
++ if old != string(raw) {
++ return Observed{}, ErrFence
++ }
++ // Replay still re-reads the signed roster, immutable fence and live claim.
++ fresh, err := Read(ctx, s, o.Host)
++ if err != nil {
++ return Observed{}, err
++ }
++ if fresh.ETag != o.ETag {
++ return Observed{}, ErrConflict
++ }
++ return fresh, nil
++ }
++ return Update(ctx, s, o, func(next *Claim) error { setJournal(next, key, string(raw)); return nil })
++}
+```
+
+## Task C: Hand a remote target predecessor back to recovery (M3, J2(b))
+
+Replace the Plan 10 landing-only regression. Recovery kinds dispatch to cross-box evidence before lease acquisition and again after admission. The normal stop form is an explicit moves-supplied signature verifier because the current `StoppedReport` has neither a signature nor a writer-session field. Loss and fence forms are read and verified by the claim code itself.
+
+The real wake-assembly admission regression exercises `stopPredecessor`, `wireClaimAdmission` and `runtime.Admit`. It stands in for the S3 lease with the existing fixture, while using the real guard and launch journal. It does not prove moves' owner implementation, C8 files, privileged stops or production activation; those remain moves acceptance gates.
+
+Focused command: `go test ./internal/host -run 'TestPlan12Recovery|TestPlan9D2CrossBox|TestPlan10J2|TestPlan11.*Relocation' -count=1`.
+
+Expected first failure: the new target-stop verifier type is absent; after its API exists, the old routing and fresh-stamp check would still refuse recovery. Evidence cases include signed stop, signed loss and verified fence, plus wrong session, wrong operation and unverified fence. Keep the separate landing artifact-retirement test: J2(d)'s S-tagged local database is moves' responsibility and is not silently retired here.
+
+### Failing tests
+
+<!-- apply: C tests -->
+```diff
+--- a/internal/host/plan10_j2_test.go
++++ b/internal/host/plan10_j2_test.go
+@@ -320,36 +320,6 @@
+ }
+ }
+
+-// Cross-box handover and predecessor evidence stay landing-only: a recovery
+-// op whose previous writer ran on another box goes to the local path, which
+-// needs this box's own completed fence receipt.
+-func TestPlan10J2CrossBoxCodeStaysLandingOnly(t *testing.T) {
+- var calls []string
+- local := func(context.Context, hostclaim.Observed, hostclaim.Observed) error {
+- calls = append(calls, "local")
+- return nil
+- }
+- cross := func(context.Context, hostclaim.Observed, hostclaim.Observed) error {
+- calls = append(calls, "cross")
+- return nil
+- }
+- hook := claimAdmissionHandover("box-1", local, cross)
+- for _, kind := range []string{hostclaim.KindRestoring, hostclaim.KindReverting} {
+- calls = nil
+- prior := plan10J2Observed(kind, "probing", "box-1")
+- prior.Claim.Holder.WriterBox = "box-2"
+- if err := hook(t.Context(), prior, hostclaim.Observed{}); err != nil || len(calls) != 1 || calls[0] != "local" {
+- t.Fatalf("%s admission went to %v (%v)", kind, calls, err)
+- }
+- localStops, remote := 0, 0
+- w := &ClaimWakeAssembly{Stop: func(context.Context, hostclaim.Observed) error { localStops++; return nil }}
+- w.BeforeAdmission = claimPredecessorStop(w, "box-1", func(context.Context, hostclaim.Observed) error { remote++; return nil })
+- if err := w.stopPredecessor(t.Context(), prior); err != nil || localStops != 1 || remote != 0 {
+- t.Fatalf("%s predecessor stop: local=%d remote=%d %v", kind, localStops, remote, err)
+- }
+- }
+-}
+-
+ // A clean return records its current inputs at probing before the held
+ // render and launch. No configured phase is needed. The adapter must call
+ // recoveryProbeRequest.Record first; movesrecoveryprobe.go supplies the adapter.
+--- /dev/null
++++ b/internal/host/plan12_handover_test.go
+@@ -0,0 +1,160 @@
++package host
++
++import (
++ "context"
++ "crypto/ed25519"
++ "crypto/sha256"
++ "encoding/hex"
++ "encoding/json"
++ "github.com/carlosframework/platform/internal/activator"
++ "github.com/carlosframework/platform/internal/hostclaim"
++ "github.com/carlosframework/platform/internal/registry"
++ "github.com/carlosframework/platform/internal/release"
++ "github.com/carlosframework/platform/internal/termfence"
++ "os/exec"
++ "strings"
++ "testing"
++ "time"
++)
++
++type plan12FenceAuthority struct{ reject bool }
++
++func (a plan12FenceAuthority) ValidateFence(context.Context, termfence.Record) (hostclaim.FenceBinding, error) {
++ if a.reject {
++ return hostclaim.FenceBinding{}, hostclaim.ErrFence
++ }
++ return hostclaim.FenceBinding{Region: "uk", RosterRevision: 1, RosterETag: "roster"}, nil
++}
++func plan12Handover(t *testing.T, kind, mode string) (*recoveryFixture, hostclaim.Observed, claimCrossBoxHandover) {
++ t.Helper()
++ f := newRecoveryFixture(t)
++ o := f.read()
++ key := "moves/bab/jam/" + o.Host + "/m1/stopped-target.json"
++ journal := map[string]string{hostclaim.JournalEvidenceTarget: key}
++ pub := f.signer.Public().(ed25519.PublicKey)
++ h := claimCrossBoxHandover{Guard: f.guard, Evidence: f.claims, Box: "box-1", PublicKey: pub, FenceAuthority: plan12FenceAuthority{reject: mode == "unverified-fence"}}
++ if mode == "fence" || mode == "unverified-fence" {
++ record := termfence.Record{V: 1, Box: "box-2", Enrollment: strings.Repeat("a", 32), State: "terminated"}
++ raw, err := json.Marshal(record)
++ if err != nil {
++ t.Fatal(err)
++ }
++ sum := sha256.Sum256(raw)
++ ref := hostclaim.FenceRef{Enrollment: record.Enrollment, Key: "control/box-fences/" + record.Enrollment + ".json", SHA256: hex.EncodeToString(sum[:])}
++ if _, err = f.claims.Put(t.Context(), ref.Key, raw, ""); err != nil {
++ t.Fatal(err)
++ }
++ refRaw, _ := json.Marshal(ref)
++ journal = map[string]string{"fence.box-2": string(refRaw)}
++ }
++ if mode == "loss" || mode == "wrong-loss-op" {
++ journal = map[string]string{"loss.loss_1": "{}"}
++ }
++ var err error
++ o, err = hostclaim.Update(t.Context(), f.claims, o, func(c *hostclaim.Claim) error {
++ c.Op = plan10J2Op(kind, hostclaim.PhaseProbing)
++ c.Op.Journal = journal
++ c.Op.Intent["target_enrollment"] = strings.Repeat("a", 32)
++ c.Holder.Session, c.Holder.Generation, c.Holder.WriterBox = 2, "target-gen", "box-2"
++ if mode == "loss" || mode == "wrong-loss-op" {
++ c.LossRefs = []string{"loss_1"}
++ }
++ return nil
++ })
++ if err != nil {
++ t.Fatal(err)
++ }
++ if err = f.guard.Activate(t.Context(), o.Host, "box-1", nil); err != nil {
++ t.Fatal(err)
++ }
++ proof := ClaimTargetStop{Host: o.Host, OpID: "m1", Box: "box-2", Incarnation: o.Claim.Holder.Incarnation, Generation: "target-gen", Session: 2}
++ if mode == "wrong-session" {
++ proof.Session++
++ }
++ if mode == "wrong-op" {
++ proof.OpID = "another"
++ }
++ raw, err := release.SignRootEnvelope(release.RootSigner(f.signer), proof)
++ if err != nil {
++ t.Fatal(err)
++ }
++ if _, err = f.claims.Put(t.Context(), key, raw, ""); err != nil {
++ t.Fatal(err)
++ }
++ h.TargetStop = func(ctx context.Context, _ hostclaim.Observed, key string) (ClaimTargetStop, error) {
++ raw, _, err := f.claims.Get(ctx, key)
++ if err != nil {
++ return ClaimTargetStop{}, err
++ }
++ var p ClaimTargetStop
++ err = release.VerifyRootEnvelope(pub, raw, &p)
++ return p, err
++ }
++ if mode == "loss" || mode == "wrong-loss-op" {
++ now := time.Now().UTC()
++ r := hostclaim.LossRecord{V: 1, ID: "loss_1", State: "prepared", Actor: "operator", AcceptedAt: now, Host: o.Host, HolderIncarnation: o.Claim.Holder.Incarnation, HolderStamp: o.Claim.Holder.Stamp, HolderSession: 2, ExpectedClaimGen: o.Claim.Gen, ExpectedClaimETag: o.ETag, OpID: "m1", OpKind: hostclaim.KindLanding, OpPhase: "probing", Box: "box-2", Enrollment: "enroll", QuarantineRevision: 1,
++ CloudObservation: hostclaim.LossCloudObservation{ObservedAt: now, Account: "123", Region: "eu-west-1", InstanceID: "i-1", State: "stopped", RequestID: "request"}, OperatorAttestation: hostclaim.LossOperatorAttestation{MaintainComputeFence: true, ColdQuarantineStartOnly: true}, FencedAt: now,
++ ConfirmedConsequences: hostclaim.LossConsequences{RevokeExecution: true, RemoveParticipation: true, RecoveryIntent: json.RawMessage(`{"kind":"move-target-lost"}`)}, Timers: hostclaim.TimerLoss{Knowledge: hostclaim.TimersUnknown}, WriteSources: []hostclaim.WriteSource{{Reason: "target", Box: "box-2", Session: 2, Generation: "target-gen", Region: "uk", Baseline: "target-snapshot"}}}
++ if mode == "wrong-loss-op" {
++ r.OpID = "other"
++ }
++ made, err := hostclaim.CreateLossRecord(t.Context(), f.claims, release.RootSigner(f.signer), r)
++ if err != nil {
++ t.Fatal(err)
++ }
++ r.State, r.ResultClaimGen = "committed", o.Claim.Gen
++ if _, err = hostclaim.ReplaceLossRecord(t.Context(), f.claims, release.RootSigner(f.signer), made, r); err != nil {
++ t.Fatal(err)
++ }
++ }
++ return f, o, h
++}
++func TestPlan12RecoveryPredecessorEvidence(t *testing.T) {
++ for _, kind := range []string{hostclaim.KindRestoring, hostclaim.KindReverting} {
++ for _, mode := range []string{"stop", "loss", "fence", "wrong-session", "wrong-op", "wrong-loss-op", "unverified-fence"} {
++ t.Run(kind+"/"+mode, func(t *testing.T) {
++ _, o, h := plan12Handover(t, kind, mode)
++ if !claimRemotePredecessor(o, "box-1") {
++ t.Fatal("local path selected")
++ }
++ err := h.Before(t.Context(), o)
++ valid := mode == "stop" || mode == "loss" || mode == "fence"
++ if valid && err != nil || !valid && err == nil {
++ t.Fatalf("%s: %v", mode, err)
++ }
++ })
++ }
++ }
++}
++func TestPlan12RecoveryAdmissionAfterTargetSession(t *testing.T) {
++ for _, kind := range []string{hostclaim.KindRestoring, hostclaim.KindReverting} {
++ t.Run(kind, func(t *testing.T) {
++ f, o, h := plan12Handover(t, kind, "stop")
++ c := f.controller("boot-1", "controller-1", 4101, "start-1")
++ c.wake.Inputs = plan9D2Inputs{recoveryInputs{f}}
++ c.wake.Operations = &plan9D2Attempts{}
++ c.wake.ProbeReady = func(context.Context, *exec.Cmd, registry.Route, ClaimLaunchInputs, string) error { return nil }
++ wireClaimAdmission(c.wake, "box-1", func(context.Context, hostclaim.Observed, hostclaim.Observed) error {
++ t.Fatal("local predecessor handover")
++ return nil
++ }, h, nil)
++ if err := c.wake.stopPredecessor(t.Context(), o); err != nil {
++ t.Fatal(err)
++ }
++ live, cancel := context.WithCancel(t.Context())
++ defer cancel()
++ done := make(chan struct{})
++ go func() { <-live.Done(); close(done) }()
++ c.wake.mu.Lock()
++ c.wake.pending[o.Host] = &claimWakeAttempt{observed: o, held: recoveryHeld{}, ctx: live, cancel: cancel, done: done}
++ c.wake.mu.Unlock()
++ got, err := c.runtime.Admit(t.Context(), registry.Route{Host: o.Host}, o, activator.StartPolicy{Probe: true})
++ if err != nil {
++ t.Fatal(err)
++ }
++ if got.Claim.Holder.Session <= 2 || got.Claim.Holder.Stamp != got.Claim.Holder.Session || got.Claim.Holder.WriterBox != "box-1" {
++ t.Fatal(got)
++ }
++ })
++ }
++}
+```
+
+### Implementation
+
+<!-- apply: C implementation -->
+```diff
+--- a/internal/host/claimowners.go
++++ b/internal/host/claimowners.go
+@@ -39,6 +39,7 @@
+ // with CARLOS_MOVES_PHASE1 on. ProbeReady replaces the claim's probe
+ // readiness check when set.
+ type ClaimMovesOwners struct {
++ TargetStop ClaimTargetStopVerifier
+ LossRecovery hostclaim.LossRecoveryOwner
+ LossSources hostbox.LossAdmissionSources
+ ClaimKindOwners
+@@ -47,7 +48,7 @@
+ }
+
+ func (m ClaimMovesOwners) zero() bool {
+- return m.LossRecovery == nil && m.LossSources == nil && m.Inputs == nil && m.AdmissionEvidence == nil && m.PublicationEvidence == nil && m.Operations == nil && m.ProbeReady == nil
++ return m.TargetStop == nil && m.LossRecovery == nil && m.LossSources == nil && m.Inputs == nil && m.AdmissionEvidence == nil && m.PublicationEvidence == nil && m.Operations == nil && m.ProbeReady == nil
+ }
+
+ // ClaimRelocationOwners is region HA's owner set for the relocating kind
+--- a/internal/host/claimhandover_crossbox.go
++++ b/internal/host/claimhandover_crossbox.go
+@@ -2,6 +2,7 @@
+
+ import (
+ "context"
++ "crypto/ed25519"
+ "strconv"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+@@ -39,12 +40,18 @@
+ // installation stamp (item 4), so the local record this box initialized for
+ // the landing installation is the one the session runs under.
+ type claimCrossBoxHandover struct {
+- Guard hostguard.Guard
+- Evidence release.Store
+- Box string
++ PublicKey ed25519.PublicKey
++ FenceAuthority hostclaim.FenceAuthority
++ TargetStop ClaimTargetStopVerifier
++ Guard hostguard.Guard
++ Evidence release.Store
++ Box string
+ }
+
+ func (h claimCrossBoxHandover) After(ctx context.Context, prior, admitted hostclaim.Observed) error {
++ if claimRecoveryKind(prior) {
++ return h.afterRecovery(ctx, prior, admitted)
++ }
+ if h.Evidence == nil || h.Box == "" || prior.Claim == nil || prior.Claim.Holder == nil || admitted.Claim == nil || admitted.Claim.Holder == nil || prior.Host != admitted.Host || prior.Absent || admitted.Absent {
+ return hostbox.ErrChanged
+ }
+@@ -115,6 +122,9 @@
+ }
+
+ func (h claimCrossBoxHandover) Before(ctx context.Context, prior hostclaim.Observed) error {
++ if claimRecoveryKind(prior) {
++ return h.beforeRecovery(ctx, prior)
++ }
+ if _, err := h.sourceEvidence(ctx, prior); err != nil {
+ return err
+ }
+@@ -171,5 +181,5 @@
+ if prior.Claim == nil || prior.Claim.Holder == nil || prior.Claim.Holder.WriterBox == box {
+ return false
+ }
+- return prior.Claim.Op.Kind == hostclaim.KindRelocating || (prior.Claim.Op.Kind == hostclaim.KindLanding && prior.Claim.Holder.WriterBox != "")
++ return claimRecoveryKind(prior) || prior.Claim.Op.Kind == hostclaim.KindRelocating || (prior.Claim.Op.Kind == hostclaim.KindLanding && prior.Claim.Holder.WriterBox != "")
+ }
+--- a/internal/host/claimconstruction.go
++++ b/internal/host/claimconstruction.go
+@@ -22,6 +22,7 @@
+ "github.com/carlosframework/platform/internal/controlsock"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
++ "github.com/carlosframework/platform/internal/hostfence"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/hostops"
+ "github.com/carlosframework/platform/internal/hostwrite"
+@@ -282,7 +283,11 @@
+ wake.AfterAdmission = coldAdmissionHook(guard, owners.Identity, controlsock.Path)
+ wake.RetireArtifacts = func(context.Context, hostclaim.Observed, ClaimLaunchInputs, string) error { return hostbox.ErrChanged }
+ if movesC1 {
+- handover := claimCrossBoxHandover{Guard: guard, Evidence: store, Box: actor.Box}
++ handover := claimCrossBoxHandover{Guard: guard, Evidence: store, Box: actor.Box, TargetStop: owners.Moves.TargetStop}
++ if owners.RetirementAuthority != nil {
++ handover.PublicKey = owners.RetirementAuthority.Directory.PublicKey
++ handover.FenceAuthority = hostfence.ClaimFenceAuthority{Directory: owners.RetirementAuthority.Directory, Now: time.Now}
++ }
+ var relocation *ClaimRelocationOwners
+ if groupD.Dispatch.Relocation != nil {
+ relocation = &owners.Relocation
+--- /dev/null
++++ b/internal/host/claimhandover_recovery.go
+@@ -0,0 +1,145 @@
++package host
++
++import (
++ "context"
++ "encoding/json"
++ "github.com/carlosframework/platform/internal/hostbox"
++ "github.com/carlosframework/platform/internal/hostclaim"
++ "github.com/carlosframework/platform/internal/hostguard"
++)
++
++// ClaimTargetStop is the identity authenticated by moves' signed stop reader.
++// Session is a writer session, never the report's claim generation.
++type ClaimTargetStop struct {
++ Host, OpID, Box, Incarnation, Generation string
++ Session uint64
++}
++type ClaimTargetStopVerifier func(context.Context, hostclaim.Observed, string) (ClaimTargetStop, error)
++
++func claimRecoveryKind(o hostclaim.Observed) bool {
++ return o.Claim != nil && (o.Claim.Op.Kind == hostclaim.KindRestoring || o.Claim.Op.Kind == hostclaim.KindReverting)
++}
++func recoveryPredecessor(o hostclaim.Observed) hostclaim.AdmissionRecord {
++ h := o.Claim.Holder
++ p := hostclaim.AdmissionRecord{Box: h.WriterBox, Session: h.Session, Generation: h.Generation}
++ if len(h.Admissions) > 0 {
++ p = h.Admissions[len(h.Admissions)-1]
++ }
++ return p
++}
++func (h claimCrossBoxHandover) recoveryEvidence(ctx context.Context, o hostclaim.Observed) error {
++ if !claimRecoveryKind(o) || o.Absent || o.ETag == "" || o.Claim.Holder == nil || h.Evidence == nil || h.Box == "" {
++ return hostbox.ErrChanged
++ }
++ c := o.Claim
++ p := recoveryPredecessor(o)
++ op := c.Op
++ if op.ID == "" || op.Phase != hostclaim.PhaseProbing || op.Exec != h.Box || op.Intent["source"] != h.Box || p.Box == "" || p.Box == h.Box || p.Box != op.Intent["target"] || p.Box != c.Holder.WriterBox || p.Session != c.Holder.Session {
++ return hostbox.ErrChanged
++ }
++ lossID, err := hostclaim.OperationLossID(c)
++ if err != nil {
++ return err
++ }
++ if lossID != "" {
++ l, err := hostclaim.ReadLossRecord(ctx, h.Evidence, h.PublicKey, lossID)
++ if err != nil {
++ return err
++ }
++ r := l.Record
++ if r.State != "committed" || r.Host != o.Host || r.OpID != op.ID || r.HolderIncarnation != c.Holder.Incarnation || r.Box != p.Box {
++ return hostclaim.ErrLossRecord
++ }
++ for _, source := range r.WriteSources {
++ if source.Reason == "target" && source.Box == p.Box && source.Session == p.Session && source.Generation == p.Generation {
++ return nil
++ }
++ }
++ return hostclaim.ErrLossRecord
++ }
++ if op.Journal["fence."+p.Box] != "" {
++ var ref hostclaim.FenceRef
++ if json.Unmarshal([]byte(op.Journal["fence."+p.Box]), &ref) != nil || op.Intent["target_enrollment"] == "" || ref.Enrollment != op.Intent["target_enrollment"] {
++ return hostclaim.ErrFence
++ }
++ verified, err := hostclaim.VerifyFence(ctx, h.Evidence, h.FenceAuthority, ref)
++ if err != nil {
++ return err
++ }
++ if verified.Box() != p.Box || verified.Region() != op.Intent["to"] {
++ return hostclaim.ErrFence
++ }
++ return nil
++ }
++ key := op.Journal[hostclaim.JournalEvidenceTarget]
++ expected := "moves/" + c.Holder.Account + "/" + c.Holder.App + "/" + o.Host + "/" + op.ID + "/stopped-target.json"
++ if key != expected || h.TargetStop == nil {
++ return hostbox.ErrChanged
++ }
++ proof, err := h.TargetStop(ctx, o, key)
++ if err != nil {
++ return err
++ }
++ if proof != (ClaimTargetStop{Host: o.Host, OpID: op.ID, Box: p.Box, Incarnation: c.Holder.Incarnation, Generation: p.Generation, Session: p.Session}) {
++ return hostbox.ErrChanged
++ }
++ return nil
++}
++func (h claimCrossBoxHandover) beforeRecovery(ctx context.Context, o hostclaim.Observed) error {
++ if err := h.recoveryEvidence(ctx, o); err != nil {
++ return err
++ }
++ old := o.Claim.Holder
++ return h.Guard.With(ctx, o.Host, hostguard.Captured{Incarnation: old.Incarnation, Stamp: old.Stamp}, func(c *hostclaim.Claim) error {
++ if c.Gen != o.Claim.Gen || c.Op.ID != o.Claim.Op.ID || c.Op.Kind != o.Claim.Op.Kind || c.Op.Phase != o.Claim.Op.Phase || c.Op.Exec != h.Box || c.Holder.Session != old.Session {
++ return hostbox.ErrChanged
++ }
++ return nil
++ }, func() error { return nil })
++}
++func (h claimCrossBoxHandover) afterRecovery(ctx context.Context, prior, admitted hostclaim.Observed) error {
++ if err := h.recoveryEvidence(ctx, prior); err != nil {
++ return err
++ }
++ if admitted.Absent || admitted.Claim == nil || admitted.Claim.Holder == nil || admitted.Host != prior.Host {
++ return hostbox.ErrChanged
++ }
++ old, next := prior.Claim.Holder, admitted.Claim.Holder
++ op, ac := prior.Claim.Op, admitted.Claim
++ if ac.Op.ID != op.ID || ac.Op.Kind != op.Kind || ac.Op.Phase != op.Phase || ac.Op.Exec != h.Box || next.Incarnation != old.Incarnation || next.Session <= old.Session || next.Stamp != next.Session || next.WriterBox != h.Box || len(next.Admissions) == 0 {
++ return hostbox.ErrChanged
++ }
++ p := recoveryPredecessor(prior)
++ a := next.Admissions[len(next.Admissions)-1]
++ if a.Session != next.Session || a.Box != h.Box || a.Mode != "cold" || a.Generation != next.Generation || a.Predecessor != p.Generation {
++ return hostbox.ErrChanged
++ }
++ lossID, err := hostclaim.OperationLossID(prior.Claim)
++ if err != nil {
++ return err
++ }
++ if lossID != "" && (a.LossID != lossID || a.Coverage != "" || a.WritesLost == nil || a.WritesLost.Box != p.Box || a.WritesLost.Session != p.Session) {
++ return hostclaim.ErrLossRecord
++ }
++ if lossID == "" && a.LossID != "" {
++ return hostclaim.ErrLossRecord
++ }
++ current, err := hostclaim.Read(ctx, h.Evidence, admitted.Host)
++ if err != nil {
++ return err
++ }
++ if current.ETag != admitted.ETag {
++ return hostbox.ErrChanged
++ }
++ // Activate retires the prior local stamp through the descriptor-based guard.
++ // It neither clears a fence nor retires files; moves owns S-tagged artifacts.
++ if err = h.Guard.Activate(ctx, admitted.Host, h.Box, nil); err != nil {
++ return err
++ }
++ return h.Guard.With(ctx, admitted.Host, hostguard.Captured{Incarnation: next.Incarnation, Stamp: next.Stamp}, func(c *hostclaim.Claim) error {
++ if c.Gen != ac.Gen || c.Holder.Session != next.Session || c.Holder.Generation != next.Generation || c.Holder.WriterBox != h.Box || c.Op.ID != op.ID || c.Op.Kind != op.Kind || c.Op.Phase != op.Phase || c.Op.Exec != h.Box {
++ return hostbox.ErrChanged
++ }
++ return nil
++ }, func() error { return nil })
++}
+```
+
+## Task D: Run parks under the caller’s location lease (M5, M6)
+
+Factor the existing engine body into `run`; `Run` preserves the fresh-acquisition behavior. `RunHeld` asks the same configured aggregate resolver to borrow the caller's exact scope lease. The adapter wraps only the borrowed constituent's release and close; its checks and cancellation continue to flow into requests. Extra locks, if any, are still owned and released by the aggregate.
+
+`RenewingLeases.Acquire` and `AggregateLeases.Acquire` stamp their private handles with the acquired bucket and scope. Moves keeps its own lease across passes and gives that handle to `RunHeld`. It must not invent a wrapper that labels an unrelated lease as the requested scope.
+
+`ReplicaParkLeaseSet` is the canonical M6 resolver. An instance location's replica and scope lock are the same name. A moves scope uses its own lock, while `evidence_at` bounds staging writers in the other bucket. Whole-host moves scopes remain legal only where the existing sweep/delete checks permit them.
+
+Focused command: `go test ./internal/hostbox ./internal/park -run 'TestPlan12|TestAggregateLeases|TestRegionalWorker|Test.*Sweep' -count=1`.
+
+Expected first failure: `RunHeld` and `ReplicaParkLeaseSet` are undefined. The test uses the real engine, real `AggregateLeases`, real `RenewingLeases` handle and the in-memory versioned store. Its renewable transport rejects a second acquisition. It checks actual residue removal, authorizer use, lease retention and refusals for another host, location or bucket and a lost lease. This is an isolated transport test; it contacts no AWS service.
+
+### Failing tests
+
+<!-- apply: D tests -->
+```diff
+--- /dev/null
++++ b/internal/hostbox/plan12_held_test.go
+@@ -0,0 +1,116 @@
++package hostbox
++
++import (
++ "context"
++ "errors"
++ "github.com/carlosframework/platform/internal/hostclaim"
++ "github.com/carlosframework/platform/internal/lease/leasetest"
++ "github.com/carlosframework/platform/internal/park"
++ "github.com/carlosframework/platform/internal/release"
++ "strings"
++ "testing"
++ "time"
++)
++
++type plan12Renewable struct {
++ lost chan struct{}
++ err error
++ releases int
++}
++
++func (r *plan12Renewable) Err() error { return r.err }
++func (r *plan12Renewable) Lost() <-chan struct{} { return r.lost }
++func (r *plan12Renewable) Release(context.Context) error { r.releases++; return nil }
++func TestPlan12LandingResidueParkUnderItsOwnLease(t *testing.T) {
++ for _, mode := range []string{"ok", "wrong-host", "wrong-location", "wrong-bucket", "lost", "authorization"} {
++ t.Run(mode, func(t *testing.T) {
++ ctx := t.Context()
++ host := "app.example"
++ inc := strings.Repeat("a", 64)
++ loc := "instances/" + host + "/" + inc
++ now := time.Date(2026, 10, 10, 12, 0, 0, 0, time.UTC)
++ claims := release.NewDirStore(t.TempDir())
++ _, err := hostclaim.Update(ctx, claims, hostclaim.Observed{Host: host, Absent: true}, func(c *hostclaim.Claim) error {
++ c.Op = hostclaim.Op{Kind: hostclaim.KindParking, ID: "op", Intent: map[string]string{"park_sweep": "v1"}}
++ c.Obligations = []hostclaim.Obligation{{ID: "chore", Op: "op", Kind: "park-replica", Region: "ie", Owner: "region:ie", Retirement: "ret", Park: &hostclaim.ParkScope{Location: loc, Incarnation: inc, EvidenceAt: now.Add(-time.Hour), RequestLimit: RequestLimit}}}
++ return nil
++ })
++ if err != nil {
++ t.Fatal(err)
++ }
++ data := leasetest.NewVersionedBucket()
++ data.AddBucket("data")
++ data.Put("data", loc+"/db", []byte("residue"))
++ r := &plan12Renewable{lost: make(chan struct{})}
++ acquires := 0
++ leases := RenewingLeases{Region: "eu-west-1", Open: func(context.Context, string, string, string) (Renewable, error) {
++ acquires++
++ if acquires > 1 {
++ return nil, errors.New("already held")
++ }
++ return r, nil
++ }}
++ bucket, scope := "data", loc
++ switch mode {
++ case "wrong-host":
++ scope = "instances/other.example/" + inc
++ case "wrong-location":
++ scope = "instances/" + host
++ case "wrong-bucket":
++ bucket = "other"
++ }
++ own, err := leases.Acquire(ctx, bucket, scope)
++ if err != nil {
++ t.Fatal(err)
++ }
++ defer own.Close()
++ if mode == "lost" {
++ r.err = errors.New("lost")
++ }
++ authorized := 0
++ engine := park.Engine{Claims: claims, Store: data, Bucket: "data", Region: "ie", Leases: AggregateLeases{Leases: leases, Resolve: ReplicaParkLeaseSet}, Now: func() time.Time { return now }, NewID: func() string { return "next" }, View: func(_ context.Context, o hostclaim.Observed) (park.View, error) {
++ return park.View{ClaimETag: o.ETag, Region: "ie"}, nil
++ }, Authorize: func(context.Context, hostclaim.Observed, hostclaim.Obligation) error {
++ authorized++
++ if mode == "authorization" {
++ return park.ErrUnsafe
++ }
++ return nil
++ }}
++ err = engine.RunHeld(ctx, park.Work{Host: host, ID: "chore", Op: "op"}, own)
++ if mode == "ok" {
++ if err != nil && !errors.Is(err, park.ErrPending) {
++ t.Fatal(err)
++ }
++ if authorized == 0 {
++ t.Fatal("authorizer bypassed")
++ }
++ versions, e := data.ListVersions(ctx, "data", loc+"/")
++ if e != nil || len(versions) != 0 {
++ t.Fatal("residue not parked", e, versions)
++ }
++ } else if err == nil {
++ t.Fatal("accepted", mode)
++ }
++ if acquires != 1 || r.releases != 0 {
++ t.Fatalf("acquires=%d releases=%d", acquires, r.releases)
++ }
++ if mode != "lost" && own.Check(ctx) != nil {
++ t.Fatal("caller lease lost")
++ }
++ })
++ }
++}
++func TestPlan12LeaseSetSemantics(t *testing.T) {
++ for _, scope := range []string{"moves/app.example/op", "moves/app.example", "instances/app.example", "instances/app.example/" + strings.Repeat("a", 64)} {
++ got, err := ReplicaParkLeaseSet(t.Context(), "data", scope)
++ if err != nil || len(got) != 1 || got[0] != scope {
++ t.Fatalf("%s: %v %v", scope, got, err)
++ }
++ }
++ for _, scope := range []string{"control/staging-leases/ie/app.example", "s3://deployment/moves/app.example/op", "instances/app.example/not-an-incarnation", "moves/other/../op"} {
++ if _, err := ReplicaParkLeaseSet(t.Context(), "data", scope); err == nil {
++ t.Fatal(scope)
++ }
++ }
++}
+```
+
+### Implementation
+
+<!-- apply: D implementation -->
+```diff
+--- a/internal/park/remote.go
++++ b/internal/park/remote.go
+@@ -385,7 +385,18 @@
+
+ // Run performs one resumable event. ErrPending is a scheduler retry, never an
+ // acknowledgement. A newly rotated event is persisted before returning it.
+-func (e Engine) Run(ctx context.Context, w Work) error {
++func (e Engine) Run(ctx context.Context, w Work) error { return e.run(ctx, w, nil) }
++
++// RunHeld borrows an exact location lease. The caller retains its renewal and
++// release duties through publication. Only the configured lease resolver may
++// borrow it; the same authorizer, census and engine effects still run.
++func (e Engine) RunHeld(ctx context.Context, w Work, held HeldLease) error {
++ if held == nil {
++ return ErrUnsafe
++ }
++ return e.run(ctx, w, held)
++}
++func (e Engine) run(ctx context.Context, w Work, own HeldLease) error {
+ if e.Store == nil || e.Claims == nil || e.Leases == nil || e.Now == nil || e.NewID == nil || e.View == nil || e.Authorize == nil || e.Bucket == "" || e.Region == "" {
+ return ErrUnsafe
+ }
+@@ -406,9 +417,23 @@
+ if o.Region != e.Region {
+ return ErrUnsafe
+ }
+- held, err := e.Leases.Acquire(ctx, e.Bucket, o.Park.Location)
+- if err != nil {
+- return err
++ var held HeldLease
++ if own == nil {
++ held, err = e.Leases.Acquire(ctx, e.Bucket, o.Park.Location)
++ } else {
++ resolver, ok := e.Leases.(interface {
++ AcquireHeld(context.Context, string, string, HeldLease) (HeldLease, error)
++ })
++ if !ok {
++ return ErrUnsafe
++ }
++ held, err = resolver.AcquireHeld(ctx, e.Bucket, o.Park.Location, own)
++ }
++ if err != nil {
++ return err
++ }
++ if held == nil {
++ return ErrUnsafe
+ }
+ defer held.Close()
+ if bound, ok := held.(interface {
+--- a/internal/hostbox/renewing.go
++++ b/internal/hostbox/renewing.go
+@@ -37,7 +37,7 @@
+ return nil, ErrUnsupported
+ }
+ cctx, stop := context.WithCancel(ctx)
+- held := &renewingHeld{h: h, ctx: cctx, cancel: stop, done: make(chan struct{})}
++ held := &renewingHeld{bucket: bucket, scope: scope, h: h, ctx: cctx, cancel: stop, done: make(chan struct{})}
+ go func() {
+ defer close(held.done)
+ select {
+@@ -54,12 +54,13 @@
+ }
+
+ type renewingHeld struct {
+- h Renewable
+- ctx context.Context
+- cancel context.CancelFunc
+- done chan struct{}
+- once sync.Once
+- releaseErr error
++ bucket, scope string
++ h Renewable
++ ctx context.Context
++ cancel context.CancelFunc
++ done chan struct{}
++ once sync.Once
++ releaseErr error
+ }
+
+ func (h *renewingHeld) Check(ctx context.Context) error {
+@@ -108,3 +109,5 @@
+ }
+ return errors.Join(ctx.Err(), health(), authority(ctx))
+ }
++
++func (h *renewingHeld) LeaseScope() (string, string) { return h.bucket, h.scope }
+--- a/internal/hostbox/aggregateleases.go
++++ b/internal/hostbox/aggregateleases.go
+@@ -8,9 +8,11 @@
+ "github.com/carlosframework/platform/internal/park"
+ )
+
+-// LeaseSet resolves every lock which can write a retirement scope. Production
+-// must include the replica location and any moves staging location able to
+-// write it; returning an empty set is a refusal, not permission.
++// LeaseSet names locks in the engine's data bucket. A moves/<host>[/<op>]
++// scope needs its own lock; staging writers are bounded by Park.EvidenceAt.
++// An instances/<host>[/<inc>] scope needs its replica lease and scope lock
++// (the same location, deduplicated). Deployment-bucket staging leases must
++// never be expressed as data-bucket names. An empty set refuses.
+ type LeaseSet func(context.Context, string, string) ([]string, error)
+
+ // AggregateLeases adapts a renewing lease implementation to park.Leases.
+@@ -22,6 +24,23 @@
+ }
+
+ func (a AggregateLeases) Acquire(ctx context.Context, bucket, scope string) (park.HeldLease, error) {
++ return a.acquire(ctx, bucket, scope, nil)
++}
++func (a AggregateLeases) AcquireHeld(ctx context.Context, bucket, scope string, own park.HeldLease) (park.HeldLease, error) {
++ bound, ok := own.(interface{ LeaseScope() (string, string) })
++ if !ok || own == nil {
++ return nil, ErrUnsupported
++ }
++ b, s := bound.LeaseScope()
++ if b != bucket || s != scope {
++ return nil, ErrChanged
++ }
++ if err := own.Check(ctx); err != nil {
++ return nil, err
++ }
++ return a.acquire(ctx, bucket, scope, own)
++}
++func (a AggregateLeases) acquire(ctx context.Context, bucket, scope string, own park.HeldLease) (park.HeldLease, error) {
+ if ctx == nil || a.Leases == nil || a.Resolve == nil || bucket == "" || scope == "" {
+ return nil, ErrUnsupported
+ }
+@@ -29,6 +48,7 @@
+ if err != nil {
+ return nil, err
+ }
++ names = append([]string(nil), names...)
+ sort.Strings(names)
+ uniq := names[:0]
+ hasScope := false
+@@ -46,9 +66,15 @@
+ if len(uniq) == 0 || !hasScope {
+ return nil, ErrUnsupported
+ }
+- held := &aggregateHeld{}
++ held := &aggregateHeld{bucket: bucket, scope: scope}
+ for _, name := range uniq {
+- h, acquireErr := a.Leases.Acquire(ctx, bucket, name)
++ var h park.HeldLease
++ var acquireErr error
++ if own != nil && name == scope {
++ h = borrowedLease{own}
++ } else {
++ h, acquireErr = a.Leases.Acquire(ctx, bucket, name)
++ }
+ if acquireErr != nil {
+ return nil, errors.Join(acquireErr, held.releaseAndClose(ctx))
+ }
+@@ -61,8 +87,9 @@
+ }
+
+ type aggregateHeld struct {
+- leases []park.HeldLease
+- closed bool
++ bucket, scope string
++ leases []park.HeldLease
++ closed bool
+ }
+
+ func (h *aggregateHeld) Check(ctx context.Context) error {
+@@ -106,3 +133,18 @@
+ h.closed = true
+ return errors.Join(errs...)
+ }
++
++func (h *aggregateHeld) LeaseScope() (string, string) { return h.bucket, h.scope }
++
++type borrowedLease struct{ park.HeldLease }
++
++func (b borrowedLease) Release(ctx context.Context) error { return b.Check(ctx) }
++func (b borrowedLease) Close() {}
++func (b borrowedLease) Context(ctx context.Context) context.Context {
++ if bound, ok := b.HeldLease.(interface {
++ Context(context.Context) context.Context
++ }); ok {
++ return bound.Context(ctx)
++ }
++ return ctx
++}
+--- /dev/null
++++ b/internal/hostbox/parkleases.go
+@@ -0,0 +1,42 @@
++package hostbox
++
++import (
++ "context"
++ "encoding/hex"
++ "github.com/carlosframework/platform/internal/controlname"
++ "strings"
++)
++
++// ReplicaParkLeaseSet implements the data-bucket locks in claim section 6.2a.
++// Generation parks also use their location's replica lock; the generation
++// selector is in the chore, not an extra lock namespace.
++func ReplicaParkLeaseSet(ctx context.Context, bucket, scope string) ([]string, error) {
++ if ctx == nil || bucket == "" {
++ return nil, ErrUnsupported
++ }
++ if err := ctx.Err(); err != nil {
++ return nil, err
++ }
++ p := strings.Split(scope, "/")
++ if len(p) < 2 || len(p) > 3 || !controlname.ValidHostShape(p[1]) {
++ return nil, ErrUnsupported
++ }
++ switch p[0] {
++ case "instances":
++ if len(p) == 3 {
++ if len(p[2]) != 64 || strings.ToLower(p[2]) != p[2] {
++ return nil, ErrUnsupported
++ }
++ if _, err := hex.DecodeString(p[2]); err != nil {
++ return nil, ErrUnsupported
++ }
++ }
++ case "moves":
++ if len(p) == 3 && (p[2] == "" || p[2] == "." || p[2] == ".." || strings.ContainsAny(p[2], "\\\x00:")) {
++ return nil, ErrUnsupported
++ }
++ default:
++ return nil, ErrUnsupported
++ }
++ return []string{scope}, nil
++}
+```
+
+## Task E: Provide the archived-instance census (M7)
+
+Main retains `RetiredIncarnations` (32 IDs), `QuietAfter` by region, and loss references. It retains no general retired holder/location archive. `retire` currently throws away the mapping when a holder is later removed. A claim object's historical S3 versions are not exposed by `release.Store`; do not assume that a version-history reader or retention policy exists.
+
+Add an optional incarnation-to-location map in the same retirement CAS and trim it with the existing IDs. `ClaimRetirementCensus` proves each non-live instance location from retained history. Legacy objects require a unique explicit mapping; a path-qualified incarnation requires its retired ID. Missing or ambiguous history returns `ClaimDependencyError`. This intentionally cannot recover already discarded legacy history.
+
+The production regional adapter composes that census with moves' provider. When moves residue exists, moves receives only its namespace; the full listing still reaches its `VerifyMovesSweep` precondition. `View.ArchivedIncarnations` keeps the legacy label independent from the move's proof label. Without moves residue, the claim can answer from its own proven instance history. It establishes a conservative nonzero observation-time bound; the queued scope persists it and the engine waits the full horizon. It never replaces an already persisted bound on retry.
+
+Focused command: `go test ./internal/host ./internal/hostclaim ./internal/park -run 'TestPlan12Archived|TestPlan12Legacy|Test.*Retir|Test.*Sweep|Test.*Park' -count=1`.
+
+Expected first failure: the census and retained mapping fields are undefined. Tests drive the real retirement transitions and cover legacy and old-incarnation residue, absent history, unknown incarnation, ambiguous history, mixed namespace labels and a nonzero evidence bound. The existing wire round-trip and retirement tests remain required.
+
+### Failing tests
+
+<!-- apply: E tests -->
+```diff
+--- /dev/null
++++ b/internal/host/plan12_census_test.go
+@@ -0,0 +1,98 @@
++package host
++
++import (
++ "errors"
++ "github.com/carlosframework/platform/internal/hostclaim"
++ "github.com/carlosframework/platform/internal/lease"
++ "github.com/carlosframework/platform/internal/park"
++ "strings"
++ "testing"
++ "time"
++)
++
++func plan12Retired(t *testing.T) hostclaim.Observed {
++ t.Helper()
++ f := newRecoveryFixture(t)
++ o := f.read()
++ var err error
++ o, err = hostclaim.Update(t.Context(), f.claims, o, func(c *hostclaim.Claim) error {
++ c.Holder.Location = "instances/" + o.Host
++ c.Touched = hostclaim.Touched{Complete: true}
++ return hostclaim.BeginDelete("delete", "console", "record", f.now, nil, nil)(c)
++ })
++ if err != nil {
++ t.Fatal(err)
++ }
++ o, err = hostclaim.Update(t.Context(), f.claims, o, hostclaim.ToParking("delete", nil, nil, f.newID))
++ if err != nil {
++ t.Fatal(err)
++ }
++ o, err = hostclaim.Update(t.Context(), f.claims, o, hostclaim.FreeAfterPark("delete"))
++ if err != nil {
++ t.Fatal(err)
++ }
++ return o
++}
++func TestPlan12ArchivedIncarnationCensus(t *testing.T) {
++ for _, mode := range []string{"legacy", "old-incarnation", "missing-history", "unknown-incarnation", "ambiguous-legacy"} {
++ t.Run(mode, func(t *testing.T) {
++ o := plan12Retired(t)
++ inc := o.Claim.RetiredIncarnations[0]
++ loc := "instances/" + o.Host
++ if mode == "old-incarnation" {
++ loc += "/" + inc
++ }
++ if mode == "missing-history" {
++ o.Claim.RetiredLocations = nil
++ }
++ if mode == "unknown-incarnation" {
++ loc += "/" + strings.Repeat("b", 64)
++ }
++ if mode == "ambiguous-legacy" {
++ other := strings.Repeat("b", 64)
++ o.Claim.RetiredIncarnations = append(o.Claim.RetiredIncarnations, other)
++ o.Claim.RetiredLocations[other] = loc
++ }
++ versions := []lease.ObjectVersion{{Key: loc + "/0000/db", VersionID: "v1"}}
++ labels, err := ClaimRetirementCensus(o, versions)
++ if mode == "legacy" || mode == "old-incarnation" {
++ if err != nil || labels[loc] != inc {
++ t.Fatal(labels, err)
++ }
++ } else {
++ var dependency *ClaimDependencyError
++ if !errors.As(err, &dependency) {
++ t.Fatal("not a typed dependency", err)
++ }
++ }
++ })
++ }
++}
++func TestPlan12LegacyAndMovesHaveIndependentLabels(t *testing.T) {
++ o := plan12Retired(t)
++ legacy := o.Claim.RetiredIncarnations[0]
++ move := strings.Repeat("c", 64)
++ versions := []lease.ObjectVersion{{Key: "instances/" + o.Host + "/0000/db", VersionID: "v1"}, {Key: "moves/" + o.Host + "/op/replica/db", VersionID: "v2"}}
++ view := park.View{ClaimETag: o.ETag, Region: "ie", ArchivedIncarnations: claimArchivedLocations(o.Claim)}
++ d, err := park.Decide(o, view, versions, move)
++ if err != nil {
++ t.Fatal(err)
++ }
++ if len(d.Candidates) != 2 {
++ t.Fatal(d)
++ }
++ for _, c := range d.Candidates {
++ want := move
++ if strings.HasPrefix(c.Location, "instances/") {
++ want = legacy
++ }
++ if c.Incarnation != want {
++ t.Fatal(c)
++ }
++ }
++ p := productionClaimRegional{Now: func() time.Time { return time.Unix(100, 0) }}
++ label, at, err := p.retirement(t.Context(), o, versions[:1])
++ if err != nil || label != legacy || at.IsZero() {
++ t.Fatal(label, at, err)
++ }
++}
+```
+
+### Implementation
+
+<!-- apply: E implementation -->
+```diff
+--- a/internal/hostclaim/claim.go
++++ b/internal/hostclaim/claim.go
+@@ -155,6 +155,7 @@
+ }
+
+ type Claim struct {
++ RetiredLocations map[string]string `json:"retired_locations,omitempty"`
+ EvidenceRefs []AutomaticLoss `json:"evidence_refs,omitempty"`
+ V int `json:"v"`
+ Host string `json:"host"`
+@@ -297,6 +298,11 @@
+ if err := validateAutomaticLosses(c, seenLossRefs); err != nil {
+ return err
+ }
++ for inc, location := range c.RetiredLocations {
++ if !isHex64(inc) || !stringPresent(c.RetiredIncarnations, inc) || (location != "instances/"+c.Host && location != "instances/"+c.Host+"/"+inc) {
++ return ErrMalformed
++ }
++ }
+ if len(c.RetiredIncarnations) > retiredCap {
+ return fmt.Errorf("%w: retired_incarnations over cap", ErrMalformed)
+ }
+--- a/internal/hostclaim/create.go
++++ b/internal/hostclaim/create.go
+@@ -42,9 +42,20 @@
+ }
+
+ func retire(c *Claim, inc string) {
++ if c.Holder != nil && c.Holder.Incarnation == inc && isHex64(inc) {
++ if c.RetiredLocations == nil {
++ c.RetiredLocations = map[string]string{}
++ }
++ c.RetiredLocations[inc] = c.Holder.Location
++ }
+ c.RetiredIncarnations = append([]string{inc}, c.RetiredIncarnations...)
+ if len(c.RetiredIncarnations) > retiredCap {
+ c.RetiredIncarnations = c.RetiredIncarnations[:retiredCap]
++ for retired := range c.RetiredLocations {
++ if !stringPresent(c.RetiredIncarnations, retired) {
++ delete(c.RetiredLocations, retired)
++ }
++ }
+ }
+ }
+
+--- a/internal/park/sweep.go
++++ b/internal/park/sweep.go
+@@ -18,6 +18,9 @@
+ // Pinned includes the exact noncurrent versions needed by the installed root.
+ // MarkerKnown means its read succeeded; an absent marker may denote @root.
+ type View struct {
++ // ArchivedIncarnations labels proven retired locations independently of
++ // the moves-sweep proof's incarnation. Missing entries never invent labels.
++ ArchivedIncarnations map[string]string
+ ClaimETag, Region, AuthoritativeRegion string
+ MarkerKnown bool
+ MarkerGeneration string
+@@ -147,6 +150,9 @@
+ candidate.Incarnation = tail
+ } else {
+ candidate.Incarnation = legacyIncarnation
++ if loc == "instances/"+c.Host && v.ArchivedIncarnations[loc] != "" {
++ candidate.Incarnation = v.ArchivedIncarnations[loc]
++ }
+ }
+ if c.Holder != nil && loc == c.Holder.Location && v.Region == v.AuthoritativeRegion {
+ if liveErr != nil {
+@@ -403,3 +409,23 @@
+ }
+ return d, nil
+ }
++
++// ResidueLocations uses the same namespace and lock classifier as Decide.
++func ResidueLocations(host string, versions []lease.ObjectVersion) ([]string, error) {
++ seen := map[string]bool{}
++ for _, v := range versions {
++ loc, err := locationOf(host, v.Key)
++ if err != nil || v.VersionID == "" {
++ return nil, ErrUnsafe
++ }
++ if !isLock(host, loc, v.Key) {
++ seen[loc] = true
++ }
++ }
++ out := make([]string, 0, len(seen))
++ for loc := range seen {
++ out = append(out, loc)
++ }
++ sort.Strings(out)
++ return out, nil
++}
+--- /dev/null
++++ b/internal/host/claimretirementcensus.go
+@@ -0,0 +1,124 @@
++package host
++
++import (
++ "context"
++ "github.com/carlosframework/platform/internal/hostbox"
++ "github.com/carlosframework/platform/internal/hostclaim"
++ "github.com/carlosframework/platform/internal/lease"
++ "github.com/carlosframework/platform/internal/park"
++ "strings"
++ "time"
++)
++
++// ClaimRetirementCensus proves labels from retained claim history. Bare legacy
++// objects contain no incarnation identity. A retired ID alone cannot label
++// them: the retirement must have retained its holder location as well.
++func ClaimRetirementCensus(o hostclaim.Observed, versions []lease.ObjectVersion) (map[string]string, error) {
++ if o.Absent || o.Claim == nil || o.ETag == "" {
++ return nil, hostbox.ErrChanged
++ }
++ c := o.Claim
++ if err := c.Validate(); err != nil {
++ return nil, err
++ }
++ locations, err := park.ResidueLocations(o.Host, versions)
++ if err != nil {
++ return nil, err
++ }
++ labels := map[string]string{}
++ retired := map[string]bool{}
++ for _, inc := range c.RetiredIncarnations {
++ retired[inc] = true
++ }
++ for _, loc := range locations {
++ if loc == "moves/"+o.Host || c.Holder != nil && loc == c.Holder.Location {
++ continue
++ }
++ var inc string
++ if loc == "instances/"+o.Host {
++ for candidate, where := range c.RetiredLocations {
++ if where != loc {
++ continue
++ }
++ if inc != "" && inc != candidate {
++ return nil, claimRetirementDependency()
++ }
++ inc = candidate
++ }
++ } else {
++ inc = strings.TrimPrefix(loc, "instances/"+o.Host+"/")
++ }
++ if inc == "" || !retired[inc] {
++ return nil, claimRetirementDependency()
++ }
++ labels[loc] = inc
++ }
++ return labels, nil
++}
++func claimRetirementDependency() error {
++ return &ClaimDependencyError{Contracts: []string{"claim archived incarnation and location evidence"}}
++}
++func claimArchivedLocations(c *hostclaim.Claim) map[string]string {
++ labels := map[string]string{}
++ ambiguous := map[string]bool{}
++ if c == nil {
++ return labels
++ }
++ for inc, loc := range c.RetiredLocations {
++ if prior := labels[loc]; prior != "" && prior != inc {
++ ambiguous[loc] = true
++ }
++ labels[loc] = inc
++ }
++ for loc := range ambiguous {
++ delete(labels, loc)
++ }
++ return labels
++}
++
++func (p productionClaimRegional) retirement(ctx context.Context, o hostclaim.Observed, v []lease.ObjectVersion) (string, time.Time, error) {
++ labels, err := ClaimRetirementCensus(o, v)
++ if err != nil {
++ return "", time.Time{}, err
++ }
++ now := time.Now().UTC()
++ if p.Now != nil {
++ now = p.Now().UTC()
++ }
++ if now.IsZero() {
++ return "", time.Time{}, hostbox.ErrChanged
++ }
++ locations, err := park.ResidueLocations(o.Host, v)
++ if err != nil {
++ return "", time.Time{}, err
++ }
++ hasMoves := false
++ for _, loc := range locations {
++ if loc == "moves/"+o.Host {
++ hasMoves = true
++ }
++ }
++ if !hasMoves {
++ return labels["instances/"+o.Host], now, nil
++ }
++ if p.Parks == nil || p.Parks.Evidence == nil {
++ return "", time.Time{}, &ClaimDependencyError{Contracts: []string{"moves Plan 5 newest retired move fence or release evidence"}}
++ }
++ // Moves proves its own namespace. It cannot be asked to infer legacy labels.
++ filtered := make([]lease.ObjectVersion, 0, len(v))
++ for _, x := range v {
++ if strings.HasPrefix(x.Key, "moves/"+o.Host+"/") {
++ filtered = append(filtered, x)
++ }
++ }
++ inc, at, err := p.Parks.Evidence.VerifyRetirement(ctx, o, filtered)
++ if err != nil {
++ return "", time.Time{}, err
++ }
++ if at.IsZero() || at.After(now) {
++ return "", time.Time{}, hostbox.ErrChanged
++ }
++ // A listing-time bound is conservative for already retired instance data;
++ // moves has its own earlier retirement proof, checked again by Sweep.
++ return inc, now, nil
++}
+--- a/internal/host/claimproduction.go
++++ b/internal/host/claimproduction.go
+@@ -297,6 +297,7 @@
+ }
+
+ type productionClaimRegional struct {
++ Now func() time.Time
+ Identity hostbox.IdentityProvider
+ Parks *claimMoveParkSources
+ Local *productionClaimLocal
+@@ -309,7 +310,12 @@
+ if _, err := p.Identity.Verify(ctx); err != nil {
+ return park.View{}, err
+ }
+- return p.Parks.View(ctx, o)
++ v, err := p.Parks.View(ctx, o)
++ if err != nil {
++ return park.View{}, err
++ }
++ v.ArchivedIncarnations = claimArchivedLocations(o.Claim)
++ return v, nil
+ }
+ func (p productionClaimRegional) LeaseSet(ctx context.Context, bucket, scope string) ([]string, error) {
+ if p.Parks == nil || p.Parks.Leases == nil {
+@@ -325,13 +331,10 @@
+ return p.Parks.Leases(ctx, bucket, scope)
+ }
+ func (p productionClaimRegional) VerifyRetirement(ctx context.Context, o hostclaim.Observed, v []lease.ObjectVersion) (string, time.Time, error) {
+- if p.Parks == nil || p.Parks.Evidence == nil {
+- return "", time.Time{}, &ClaimDependencyError{Contracts: []string{"moves Plan 5 archived-incarnation regional evidence"}}
+- }
+ if _, err := p.Identity.Verify(ctx); err != nil {
+ return "", time.Time{}, err
+ }
+- return p.Parks.Evidence.VerifyRetirement(ctx, o, v)
++ return p.retirement(ctx, o, v)
+ }
+ func (p productionClaimRegional) Authorize(ctx context.Context, o hostclaim.Observed, ob hostclaim.Obligation) error {
+ a, err := p.Identity.Verify(ctx)
+```
+
+## Task F: Expose the local park authorizer in MoveParks (M9)
+
+Return the owner's local authorizer as the third capability value, followed by the error. `ClaimAssembly.Build` wraps the unmodified owner callback with the assembly's exact identity check. The callback accepts only a local chore and repeats the check at use time. It does not call `RetirementEvidence.VerifyBound`.
+
+Keep the returned regional engine after `NewClaimRegionalWorker` wrapped it. The existing Build test now asserts both facts: local authorization succeeds through the owner, while the regional engine still refuses an unbound chore. All current `MoveParks` callers are updated explicitly.
+
+Focused command: `go test ./internal/host -run 'TestMoveParks|TestPlan12Local' -count=1`.
+
+Expected first failure: the new four-value accessor does not exist. Also verify that a later enrollment failure reaches the caller before the local owner runs.
+
+### Failing tests
+
+<!-- apply: F tests -->
+```diff
+--- a/internal/host/claimmoveparks_test.go
++++ b/internal/host/claimmoveparks_test.go
+@@ -115,8 +115,8 @@
+ sentinel := errors.New("real view")
+ calls := 0
+ engine := park.Engine{Bucket: actor.Bucket, Region: actor.Region, View: func(context.Context, hostclaim.Observed) (park.View, error) { calls++; return park.View{}, sentinel }}
+- providers := ClaimProviders{moveParks: &claimMoveParks{engine: engine, check: check, leases: func(context.Context, string, string) ([]string, error) { return []string{"real lease"}, nil }}}
+- got, leases, err := providers.MoveParks(t.Context())
++ providers := ClaimProviders{moveParks: &claimMoveParks{local: func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error { return nil }, engine: engine, check: check, leases: func(context.Context, string, string) ([]string, error) { return []string{"real lease"}, nil }}}
++ got, leases, _, err := providers.MoveParks(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+@@ -132,12 +132,12 @@
+ changed := actor
+ mutate(&changed)
+ providers.moveParks.check = claimBoundIdentity(func(context.Context) (hostbox.Actor, error) { return changed, nil }, actor)
+- if _, _, err := providers.MoveParks(t.Context()); !errors.Is(err, hostbox.ErrChanged) {
++ if _, _, _, err := providers.MoveParks(t.Context()); !errors.Is(err, hostbox.ErrChanged) {
+ t.Fatalf("identity change accepted: %v", err)
+ }
+ })
+ }
+- if _, _, err := (ClaimProviders{}).MoveParks(t.Context()); !errors.Is(err, hostwrite.ErrUnavailable) {
++ if _, _, _, err := (ClaimProviders{}).MoveParks(t.Context()); !errors.Is(err, hostwrite.ErrUnavailable) {
+ t.Fatal("unbuilt accessor accepted")
+ }
+ }
+@@ -187,7 +187,13 @@
+ if err != nil {
+ t.Fatal(err)
+ }
+- engine, leases, err := providers.MoveParks(ctx)
++ engine, leases, local, err := providers.MoveParks(ctx)
++ if local == nil {
++ t.Fatal("missing local authorizer")
++ }
++ if err := local(ctx, &hostclaim.Claim{Host: "app.example"}, hostclaim.Obligation{Kind: "park-local"}); err != nil {
++ t.Fatal("local authorizer went through VerifyBound", err)
++ }
+ if err != nil {
+ t.Fatal(err)
+ }
+--- /dev/null
++++ b/internal/host/plan12_local_test.go
+@@ -0,0 +1,30 @@
++package host
++
++import (
++ "context"
++ "errors"
++ "github.com/carlosframework/platform/internal/hostclaim"
++ "testing"
++)
++
++func TestPlan12LocalParkUsesOwnerAndRechecksIdentity(t *testing.T) {
++ identityErr := error(nil)
++ calls := 0
++ sentinel := errors.New("owner")
++ local := claimMoveLocalAuthorizer(func(context.Context) error { return identityErr }, func(_ context.Context, o hostclaim.Observed, ob hostclaim.Obligation) error {
++ calls++
++ if o.Host != "app.example" || ob.Park != nil || ob.Kind != "park-local" {
++ t.Fatal("wrong owner input")
++ }
++ return sentinel
++ })
++ c := &hostclaim.Claim{Host: "app.example"}
++ ob := hostclaim.Obligation{Kind: "park-local"}
++ if err := local(t.Context(), c, ob); !errors.Is(err, sentinel) || calls != 1 {
++ t.Fatal(err, calls)
++ }
++ identityErr = errors.New("enrollment changed")
++ if err := local(t.Context(), c, ob); !errors.Is(err, identityErr) || calls != 1 {
++ t.Fatal(err, calls)
++ }
++}
+```
+
+### Implementation
+
+<!-- apply: F implementation -->
+```diff
+--- a/internal/host/claimmoveparks.go
++++ b/internal/host/claimmoveparks.go
+@@ -27,7 +27,7 @@
+ missing = append(missing, "moves Plan 5 exact-version generation and files-manifest view")
+ }
+ if retirementEvidence == nil {
+- missing = append(missing, "moves Plan 5 archived-incarnation regional evidence")
++ missing = append(missing, "moves Plan 5 retired-move regional evidence")
+ }
+ if leases == nil {
+ missing = append(missing, "moves Plan 5 complete replica and staging lease inventory")
+@@ -87,22 +87,29 @@
+ }
+ }
+
++// ClaimLocalParkAuthorizer is the owner's local check before park.Local effects.
++type ClaimLocalParkAuthorizer func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error
++
+ type claimMoveParks struct {
++ local ClaimLocalParkAuthorizer
+ engine park.Engine
+ leases hostbox.LeaseSet
+ check func(context.Context) error
+ }
+
+-// MoveParks returns the assembly's regional engine and complete lease inventory.
++// MoveParks returns the regional engine, lease inventory and local authorizer.
+ // Only Build can populate this capability, after Identity.Verify succeeds.
+-func (p ClaimProviders) MoveParks(ctx context.Context) (park.Engine, hostbox.LeaseSet, error) {
++func (p ClaimProviders) MoveParks(ctx context.Context) (park.Engine, hostbox.LeaseSet, ClaimLocalParkAuthorizer, error) {
+ if p.moveParks == nil || p.moveParks.check == nil || p.moveParks.leases == nil {
+- return park.Engine{}, nil, hostwrite.ErrUnavailable
++ return park.Engine{}, nil, nil, hostwrite.ErrUnavailable
+ }
+ if err := p.moveParks.check(ctx); err != nil {
+- return park.Engine{}, nil, err
++ return park.Engine{}, nil, nil, err
+ }
+- return p.moveParks.engine, p.moveParks.leases, nil
++ if p.moveParks.local == nil {
++ return park.Engine{}, nil, nil, hostwrite.ErrUnavailable
++ }
++ return p.moveParks.engine, p.moveParks.leases, p.moveParks.local, nil
+ }
+
+ func (p productionClaimRegional) VerifyMovesSweep(ctx context.Context, o hostclaim.Observed, versions []lease.ObjectVersion) (park.MoveSweepProof, error) {
+--- a/internal/host/claimassembly.go
++++ b/internal/host/claimassembly.go
+@@ -251,7 +251,7 @@
+ if err := a.startWakeResetWorker(ctx); err != nil {
+ return ClaimProviders{}, err
+ }
+- return ClaimProviders{moveParks: &claimMoveParks{engine: regional.Regional.Engine, leases: leases, check: check}, Delivery: ClaimTimerDelivery(a.Producers, a.NewID), Selector: source, Wake: wake, Regional: regional, Stop: a.Stop, Provision: a.Provision, Close: a.Close}, nil
++ return ClaimProviders{moveParks: &claimMoveParks{local: claimMoveLocalAuthorizer(check, a.Authorize), engine: regional.Regional.Engine, leases: leases, check: check}, Delivery: ClaimTimerDelivery(a.Producers, a.NewID), Selector: source, Wake: wake, Regional: regional, Stop: a.Stop, Provision: a.Provision, Close: a.Close}, nil
+ }
+
+ type claimRegionalEnrollment struct {
+@@ -348,3 +348,16 @@
+ reconcile.Box, reconcile.Enrollment = actor.Box, actor.Enrollment
+ return ClaimRootHTTP{Journals: assembly.Journals, RequireNewWork: func(ctx context.Context) error { _, err := assembly.Identity.Verify(ctx); return err }, Launches: assembly.Launches, Stop: stop, Reconcile: reconcile, Roots: assembly.ManagedRoots}, nil
+ }
++
++func claimMoveLocalAuthorizer(check func(context.Context) error, owner func(context.Context, hostclaim.Observed, hostclaim.Obligation) error) ClaimLocalParkAuthorizer {
++ return func(ctx context.Context, c *hostclaim.Claim, ob hostclaim.Obligation) error {
++ if check == nil || owner == nil || c == nil || ob.Kind != "park-local" {
++ return hostwrite.ErrUnavailable
++ }
++ if err := check(ctx); err != nil {
++ return err
++ }
++ // Preserve the unwrapped owner used by the assembly's own park.Local.
++ return owner(ctx, hostclaim.Observed{Host: c.Host, Claim: c}, ob)
++ }
++}
+```
+
+## Task G: Classify the changed census units
+
+Run the existing record-write census before applying this patch: `go test ./internal/provision -run 'TestPlan9B8' -count=1`. Its expected defect is stale hashes and unlisted units, not an infrastructure failure. Review these exact classifications before applying the inventory patch:
+
+| Unit | Classification | Reason |
+|---|---|---|
+| `hostclaim.Claim.Validate` | `key-site`, `lit:instances` | Validates retained location identity; writes no record. |
+| `park.Engine.run` (formerly `Engine.Run`) | `versioned-transport`, `sel:CopyVersion,sel:PutIfAbsent` | Existing exact-version archive effects move into the common engine body; no new storage authority. |
+| `park.Decide` | `key-site`, `lit:instances` | Chooses an independently proven legacy archive label. |
+| `hostbox.ReplicaParkLeaseSet` | `key-site`, `lit:instances` | Parses relative data-bucket location names. |
+| `host.ClaimRetirementCensus` | `key-site`, `lit:instances` | Classifies instance residue from claim history. |
+| `host.productionClaimRegional.retirement` | `key-site`, `lit:instances` | Separates instance evidence from the moves namespace. |
+
+No production top-level declaration touched by this task writes an instance record. The console authority census scans console, hostops, mail and cmd/console; none of those production files changes. The console route census has no new route. The moves copy scan has no new moves sink. Leave those inventories unchanged, and run them after this patch. Later moves callers of the fence declaration and the four-value capability belong to that integration's classified changes.
+
+After applying: `go test ./internal/provision -run 'TestPlan9B8' -count=1`, `go test ./internal/console -run 'TestClaim.*(Census|Route)' -count=1`, and `go test . -run 'TestMoves(StringsAreRecorded|SinksAreSentences)' -count=1`.
+
+### Implementation
+
+<!-- apply: G implementation -->
+```diff
+--- a/internal/provision/plan9_b8_census_test.go
++++ b/internal/provision/plan9_b8_census_test.go
+@@ -472,6 +472,9 @@
+
+ // BEGIN REVIEWED INVENTORY
+ var plan9B8CensusInventory = []censusEntry{
++ {"internal/host/claimretirementcensus.go", "productionClaimRegional.retirement", "key-site", "lit:instances", "af5e4800aca07da1f414063b5f38325713bb1463b5be1887beb6fc060b7ab2a5"},
++ {"internal/host/claimretirementcensus.go", "ClaimRetirementCensus", "key-site", "lit:instances", "217ae8f21e4e682571f30b0921a854a3f51b38a43b11d4124bb1e0eb2c24a55e"},
++ {"internal/hostbox/parkleases.go", "ReplicaParkLeaseSet", "key-site", "lit:instances", "0ca8d9b743402fda9d15cc215464423b35e21c1c2b47f91294d76188b7d43fb6"},
+ {"internal/hostops/move_recovery_grant.go", "MoveRecoveryGrant.Convert", "record-writer", "key:WriteRecord", "794963a24a0a50d980505f83ac912973a10267b21f74bd41c56584ab9b0b25a1"},
+ {"internal/hostops/move_recovery_grant.go", "MoveRecoveryGrant.Record", "record-writer", "key:WriteRecord", "6b8bd4131842d8f22a6ab55aaae736329df40e34abb348d3d68cbd0a713503ad"},
+ // Plan 10: reviewed typed console move and loss writers.
+@@ -686,7 +689,7 @@
+ {"internal/hostclaim/adopt.go", "Adopt", "key-site", "lit:instances", "24c06b99cce6c638acb19826985cfadd3c736fd0ace7289bcf7ac3dfe784f05a"},
+ {"internal/hostclaim/claim.go", "ParkScope.Validate", "key-site", "lit:instances", "80b798b0cb679d8c533ff562c0c4804c06f53e7dba126250a3c8182f7ff0c08c"},
+ {"internal/hostclaim/claim.go", "holderKeyMatches", "key-site", "lit:instances,lit:sidecars", "d15a6841ea6f8cbfa9c8407fd3af020ddeacdfa0743b9e17de412f113faf7979"},
+- {"internal/hostclaim/claim.go", "Claim.Validate", "key-site", "lit:instances", "ddcde93d00c6fe420c60a30f561928580338617061a0bdd911de2db386e00fa9"},
++ {"internal/hostclaim/claim.go", "Claim.Validate", "key-site", "lit:instances", "3278f2b4b24b6627b9dcd0863c3bef58c6694a65c8a19d05ce280ae34801bdfc"},
+ {"internal/hostclaim/create.go", "BeginCreate", "key-site", "lit:instances", "45ee1e2b1fc9fa8ffdef4f27cf983dccfa3acf7218dccd06f0ee6d12572f4a44"},
+ {"internal/hostclaim/lossrecord.go", "LossRecordKey", "key-site", "key:LossRecordKey", "1b109b49a8fcdb09f01dbf908a173394072f01afb0c9e324023fb9b3f71f98b3"},
+ {"internal/hostclaim/lossrecord.go", "ReadLossRecord", "key-site", "key:LossRecordKey", "1237dd3fa3e79a81e6c35f371bf545c9a04841a6297c078d96852daf25b7b483"},
+@@ -808,8 +811,8 @@
+ {"internal/objstore/sidecar.go", "claimRawPutSidecar", "nonrecord", "sel:Put", "233e3d808dd462ff83aecdac5176b180556b948c552aa26f6e0b778df91a24c7"},
+ {"internal/ops/litestream.go", "FleetDBsWithSkipped", "key-site", "lit:instances", "f62291df0685db15529c4946dca358a29aba66fc31e81aab299de579e37f60f1"},
+ {"internal/park/remote.go", "locationOf", "key-site", "lit:instances", "83b3efca31b3faadcd5997dc181d81b7bb8fc866cc4a448683bf8423b1bea866"},
+- {"internal/park/remote.go", "Engine.Run", "versioned-transport", "sel:CopyVersion,sel:PutIfAbsent", "dd4b60ef9b3f5e186b9866cee818f2684ed93bd4e1c934cd66d1af9ef376d81e"},
+- {"internal/park/sweep.go", "Decide", "key-site", "lit:instances", "c4ec278026ad3b99ed088471bf32a2b2787b19b948587b34f784e97a31a29f1d"},
++ {"internal/park/remote.go", "Engine.run", "versioned-transport", "sel:CopyVersion,sel:PutIfAbsent", "3de5b215e4b7aba67768243891b3f6d8ff8c77a465aefe5168c789cb012b1ea9"},
++ {"internal/park/sweep.go", "Decide", "key-site", "lit:instances", "6f8660313ec724b5a390a8c00aadb3412579849bda89e68ef4b55cc52d05dc39"},
+ {"internal/park/sweep.go", "Engine.Discover", "key-site", "lit:instances", "69b83e6e859c1c9809ba76d46cb0192888df1ecdcd161b4f2b6435733eecdfa3"},
+ {"internal/provision/claim_app_writes.go", "UpdateRecord", "record-writer", "key:UpdateRecord,key:claimRawUpdateRecord", "caf714cb78e1280d489c7ad5b3cefb1420e812d541108b1fe13d733abed97229"},
+ {"internal/provision/provision.go", "ManifestKey", "key-site", "lit:instances", "b8ee0e77bdfade19af79a4012c411c1eb0b6ddca16037e5536fa8597f7cde270"},
+```
+
+## Task H: Amend the claim spec to rev 6.32
+
+Use the worktree spec identified in Replay. Before applying this patch, the assertion below fails on rev 6.31. Run it again afterward. The amendment adds the accepted J2(b) evidence forms, J5 declaration semantics, loss-bound admission, `RunHeld`, the exact M6 lease set, M7 history/refusals and M9 local authorization. It also replaces P6-D5's older all-bucket staging-lease wording, so there are not two conflicting instructions.
+
+```bash
+python3 - <<'CHECK'
+from pathlib import Path
+import hashlib
+p = Path('docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md')
+s = p.read_text()
+assert 'Design, 2026-10-10, **rev 6.32**.' in s
+for term in ('RunHeld', 'retired_locations', 'DeclareMoveTargetFence',
+ 'ClaimMovesOwners.TargetStop', 'LossAdmissionSources',
+ 'Deployment-bucket staging leases are never', 'MoveParks'):
+ assert term in s, term
+assert 'Acquire all required replica-location and staging-work leases' not in s
+print('claim spec amendment checks passed')
+CHECK
+```
+
+The code archive has no copy of this spec, so copying the checked document input during bootstrap is required. This task is a future amendment; authoring Plan 12 does not change the worktree's existing spec itself.
+
+### Implementation
+
+<!-- apply: H implementation -->
+```diff
+--- a/docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md
++++ b/docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md
+@@ -1,8 +1,10 @@
+ # The host lifecycle claim
+
++**Rev 6.32 (2026-10-10):** completes the claim interfaces for moves M2, M3, M5, M6, M7, M9 and M12. Claim Plan 12 supplies loss-bound Group D admission, recovery predecessor evidence, typed target fence declaration, held-lease parks, local park authority and retained retirement-location evidence.
++
+ **Rev 6.31 (2026-10-09):** records the accepted region HA contracts in amadan #236: `AutomaticLoss` in claim-level `evidence_refs`, the stop-only `relocation-release` obligation, and immutable signed `enrolled_at`. Section 6.2a binds moves' push, landing-install and rollback-return parks by name and region; R6 refuses automatic empty failover after any writer session. Claim Plan 11 supplies the integration.
+
+-Design, 2026-10-09, **rev 6.31**. Joint rounds 6 and 7 judged the claim ready for primitive planning. Rev
++Design, 2026-10-10, **rev 6.32**. Joint rounds 6 and 7 judged the claim ready for primitive planning. Rev
+ 6.5 added the R6-4 rule. Rev 6.6 added writer epochs (R7-1, amadan #225).
+ Rev 6.7 answered the moves session's notes on §6.4a. Rev 6.8 rewrote §6.4a
+ around writer sessions, for joint round 8. Rev 6.10 answers joint round 10
+@@ -1605,6 +1607,55 @@
+ `s3:DeleteObjectVersion` on its own data bucket. Today it has only
+ `Get`, `Put`, `Delete` and `List` (region-host main.tf:166, :174).
+
++#### Held location leases and the lease set (moves M5, M6)
++
++`park.Engine.RunHeld(ctx, work, held)` runs the same authorizer, version
++census, archive, delete and completion path as `Run`. Its configured
++`AggregateLeases` borrows only a still-valid lease whose acquisition-bound
++bucket and location equal the persisted scope exactly. The location includes
++the host. A lease for another host, bucket, incarnation or staging lane is
++refused before data effects. The engine may acquire other required locks,
++but never releases or closes the borrowed lease. The landing keeps renewing
++that lease through `published`, as moves section 7.6 requires.
++
++For `moves/<host>/<op>` scopes, the lease set contains the scope's own
++lock. Staging signatures are bounded by the persisted `evidence_at`: the
++seal, abort, revert or return-seal bound. The same own-lock rule applies
++to an eligible whole-host moves sweep. For `instances/<host>[/<inc>]`,
++the set contains the location's replica lease and the scope lock. These
++are the same location name and are deduplicated. Every name is relative
++to the engine's data bucket. Deployment-bucket staging leases are never
++passed as bucket-qualified names. `ReplicaParkLeaseSet` states this contract.
++
++#### Archived-incarnation census (moves M7)
++
++A bare legacy object key does not identify its incarnation. The existing
++`retired_incarnations` list proves retirement, but does not say which
++incarnation occupied `instances/<host>`. Neither its ordering nor a move's
++`LastMove` is sufficient to infer that mapping.
++
++Retirement now retains `retired_locations`, a map from incarnation to the
++holder's location, in the same claim CAS that appends the retired ID. Its
++entries are retained only while the corresponding retired ID remains.
++The map is optional; older claims remain readable without it. There is no
++backfill by guessing. A legacy census requires a unique retained mapping;
++missing, trimmed or ambiguous history returns a typed dependency error.
++An old incarnation-qualified location additionally requires that incarnation
++in `retired_incarnations`. The current holder's live location remains under
++the ordinary live-set rules.
++
++The claim census labels instance residue; moves proves its own staging
++namespace from its retired-move evidence. `View.ArchivedIncarnations`
++keeps these labels separate when both namespaces occur in one listing.
++The claim's census supplies a nonzero observation-time bound after proving
++retirement, so the park still waits the complete late-completion horizon.
++Every retry uses the scope's persisted bound. A missing instance proof
++refuses the sweep; it never falls back to the moves proof's incarnation.
++
++The field is additive, but older strict claim readers reject unknown fields.
++Upgrade every claim reader before enabling writers of `retired_locations`;
++include that compatibility requirement in the release floor.
++
+ ### 6.3 Local retirement (J-I-7, 199-13, 199-14)
+
+ Retirement covers both layouts.
+@@ -2295,6 +2346,60 @@
+ the old session certified that every one of its requests had completed.
+ Dead generations are parked by the at-rest sweep (§6.2a).
+
++### 6.4b Moves recovery admission completion (J2(b), J5)
++
++When a `restoring/probing` operation journals `loss.<id>`, Group D's
++`hostbox.Writer.Admit` uses `AdmitWriterWithLoss`. The loss must be the
++operation's committed signed record. Moves supplies `LossRecoveryOwner`
++and `LossAdmissionSources`; the latter validates every remote baseline
++named in the record's frozen `WriteSources`. The claim library checks exact
++source membership, unique box/session identities and the remote maxima.
++The owner checks the legal recovery kind, executor, stamp and input policy.
++A loss-bearing operation cannot use `clean-return` or sealed coverage and
++cannot bypass the route by omitting `Admission.LossID`. The accepted
++admission is cold, uncovered, and records the signed declaration ID.
++Old claim-level loss references without this operation's journal do not
++turn a later operation into a loss recovery. Multiple current declarations
++are refused until an explicit combination protocol is defined.
++
++A `restoring` or `reverting` source whose predecessor writer ran on the
++move's target uses cross-box handover at `probing`. It needs one of:
++
++- Authenticated target stop evidence at the operation's exact
++ `evidence_target` / `stopped-target.json` key, naming the host,
++ incarnation, operation, target box, generation and holder's writer
++ session. `claim_gen` is not a session. Moves supplies the signature
++ verifier through `ClaimMovesOwners.TargetStop`; unsigned legacy reports
++ do not qualify.
++- This operation's `loss.<id>`, re-read from the committed signed loss
++ record and naming that exact target predecessor in its frozen sources.
++- `fence.<target>`, re-verified against the immutable termination evidence
++ and roster, and bound to the target enrollment and region in the move.
++
++The claim checks the source executor, operation and predecessor before
++admission and the resulting cold session and fresh stamp afterward. It
++never asks the source root to manufacture a local stop receipt for the
++target's session. Existing local fences still refuse access; this evidence
++does not clear them or grant file effects. Moves remains responsible for
++retiring the source's S-tagged database under the existing guarded local
++retirement protocol before using the recovered installation (J2(d)).
++
++`DeclareMoveTargetFence` records a verified `fence.<target>` through one
++ETag-bound claim CAS for an uncommitted `landing` or `restoring` operation.
++It binds the operation, target box, enrollment and region, rejects a
++conflicting reference, and verifies the fence again on retry. This is an
++evidence declaration: it changes no holder, executor, phase, participant,
++record or obligation. It creates no unjournalled follow-up obligation.
++A recovery owner that consumes the fence must atomically queue any parks
++or other chores its transition owes, before acknowledging release; the
++reference itself never discharges one. Moves must retain record-first
++close ordering and verify a fresh `ReturnFence` when returning execution.
++
++`MoveParks` also returns the enrollment-bound local authorizer used by the
++claim's own `park.Local`. Moves uses it for local retirement, with its
++operation-specific checks in addition. The regional engine keeps its
++stronger `VerifyBound` wrapper. Neither authorizer substitutes for the other.
++
+ ### 6.5 Door reads pinned to versions (J-N10)
+
+ The pull door's listing:
+@@ -2595,7 +2700,7 @@
+
+ 1. Validate its enrollment, effective region, bucket and retirement authority.
+ 2. Resolve immutable evidence references covering every producer that could write the scope.
+-3. Acquire all required replica-location and staging-work leases in a deterministic order.
++3. Acquire the data-bucket lease set defined in section 6.2a in a deterministic order, or borrow the exact caller-held location lease through RunHeld. Staging writers are bounded by the persisted evidence_at.
+ 4. Re-read the claim and validate the exact obligation, original operation, phase and immutable scope.
+ 5. Obtain P6-D3’s view under that observation.
+
+@@ -2618,8 +2723,8 @@
+
+ Rejected alternatives:
+
+-- Holding only the replica lock: staging can still write into the retirement scope.
+-- Replacing evidence time with `time.Now()`: it invents evidence.
++- Holding a scope lock without the staging signature bound: staging can still write into the retirement scope.
++- Replacing a persisted evidence time with a retry clock, or using the clock without retirement proof. The archived-incarnation census in section 6.2a first proves retirement, then establishes the initial conservative bound; retries retain it.
+ - Treating publication as proof that every old writer stopped: publication establishes coverage, not termination.
+
+ #### P6-D6. Cross-process denial and returning-box cleanup
+```
+
+## Integration and rollout
+
+Nothing here arms moves or raises a production floor. Implement the consumer work in the delta's order, with these interfaces:
+
+| Delta / moves task | What moves must build |
+|---|---|
+| M2; Plan 4 Task 10 | A declared return kind in `waiting`/`claimed`; uncovered cold probe with the loss ID; `recoveryProbeMatches` refusal of clean-return whenever `LossRef` exists. Supply `ClaimMovesOwners.LossRecovery` and `LossSources` from the signed record's exact frozen `WriteSources`, including every target session and uncovered source-cut session once. Validate each baseline and its maximum; do not synthesize zeroes. Run `TestDeclaredLossNeverAdmitsCleanReturn`, `TestLostTargetRecoveryRunsAgainstARealDeclaration` and `TestLostTargetReturnDeclaresEverySessionOnce`. |
+| M3; Task 9b / Plan 4 Task 9 | Implement and register `TargetStop`, including authenticated writer session and generation. The current unsigned stopped-report reader does not qualify. Keep the dirty/clean return input policies and fresh stamp. Run `TestRecoveryAdmissionAfterTargetSession` against the real registered `ClaimWakeAssembly` for stop, loss and fence evidence, not just the fixture in this plan. |
+| M5; Task 9a | Keep the existing own-location handle from `checking` through `published`; call the returned engine's `RunHeld` for that exact location. Use `Run` for other scopes. Acquire through the production `RenewingLeases` adapter so the handle retains its identity; no release/reacquire shortcut or fake production lease resolver. Run moves' `TestLandingResidueParkUnderItsOwnLease` with real `AggregateLeases`. |
+| M6; Task 9a | Use `ReplicaParkLeaseSet` or an equivalent exact resolver. Bind staging parks to the seal/abort/revert/return-seal `evidence_at`. Never put the deployment bucket's `control/staging-leases` name into the data bucket. Run `TestMovesLeaseSetCoversTheScope`. |
+| M7/M8; Task 5 | Keep `VerifyRetirement` for the moves namespace and implement `VerifyMovesSweep` from no live move, matching `LastMove` / `released_move` (or a free claim), and every lane's operation or exact-version modification time before retirement. Claim production composes its instance census with this provider. Verify mixed labels, free-host sweeps, unknown lanes, and refusals for history the claim cannot prove. `LastMove.At` remains the moves proof's retirement time. |
+| M9; Task 9a | Consume `(engine, leases, localAuthorize, err)` from `MoveParks`. Bind `park.Local.Authorize` to `localAuthorize`, retaining each arm's additional move checks. Keep the regional engine intact. Refuse a bucket/region mismatch before arming. |
+| M12; Plan 4 Task 10 fence form | Call `DeclareMoveTargetFence` from typed, claim-enabled recovery authority after observing a real terminated target. Keep the exact operation and target enrollment. The declaration only records evidence. The consuming record/claim recovery transition must queue the target-location, staging and any return-lane parks before release, retain inventory-bound local work, and use a freshly verified `ReturnFence`. It cannot use an arbitrary `SetJournal` as verification or acknowledge parks on the strength of the fence. |
+| M1/M10 | Resolve and implement C8's origin-qualified files manifest with the files owner. Keep source-region versions pinned until commit. Confirm whether a target may still reference a source-region object after commit; if so, implement the required copy/pin policy before source retirement. This plan does not invent a files producer. |
+| M4/M11/M13–M21 | Moves retains record-first box closes plus its recovery hook, console alerting and durable rate limits, recorded revert-input checks, lost-box gate/quarantine, install ownership fixes, release-time stuck reason, wiring and censuses. Keep J4's existing declare-lost sequence. Prove `TestCleanReturnAdmissionRetiresTheSTaggedDatabase` before recovery activation. |
+
+Land the claim implementation through a reviewed amadan branch; do not commit or land from this authoring session. Integrate moves providers on their reviewed branches. Run the complete gate on the combined tip and require CI green on that exact tip before merge. Check main's CI after merge. No production flag change is included in this task.
+
+Reader rollout precedes the first new `retired_locations` write. Existing claims without the optional field remain valid. Old legacy residue with missing history continues to refuse; a clock, newest move ID or first retained incarnation does not repair that absence. Any future evidence-import/backfill needs its own reviewed design. Production moves remains unarmed until C8, recovery owners, the signed stop adapter, S-tagged retirement and the named integration tests pass.
+
+## Copy review inventory
+
+No UI, HTTP response, CLI help, alert subject, page or move phase sentence is introduced here. Reused typed errors retain their existing text. The only new diagnostics are assembled by the existing `ClaimDependencyError.Error` formatter:
+
+| Draft ID | Full sentence | Site and classification |
+|---|---|---|
+| HC12-D1 | `host claims: missing provider: claim archived incarnation and location evidence` | `claimRetirementDependency`; internal scheduler refusal for missing, trimmed or ambiguous incarnation evidence. It may reach operator logs. |
+| HC12-D2 | `host claims: missing provider: moves Plan 5 retired-move regional evidence` | `RegisterProductionClaimMoveParks`; internal startup/registration refusal. Replaces the inaccurate request for moves-owned archived-instance evidence. |
+
+These drafts are inventoried for approval before implementation shipment. They are not added to `approved_moves_copy_test.go`, whose operator sinks are unchanged. Test-only failure strings are assertions, not shipped copy. Future moves pages, timers, declared-return messages and the release-time stuck sentence belong in moves' approved MV-N inventory with their actual sinks.
+
+## Verification and authoring results
+
+Authoring used one source export from the required main commit. A second fresh export pinned to `44c1b6d77ed16f92e46e892b8254835edbd567d8` accepted every A–H patch in tests-first order with `patch --fuzz=0`, no offsets, and no rejects. Its Go files matched the authored tree byte for byte. `gofmt -l .` was clean over that replay. The spec assertion above passed after its patch and failed before it.
+
+The Go tests were attempted with the required default cache at `/home/paulca/.cache/go-build`; the sandbox refused cache writes before compilation. Consequently no Go compilation, red/green regression, census execution, full gate or cross-build is claimed by this authoring run. The inventory hashes were computed from the exact formatted declaration slices after reviewing the classifications in Task G. They still require the real census test.
+
+Before landing, observe the failing tests first, then the passing focused tests for each task, followed by:
+
+```bash
+go build ./...
+make test
+# Match all targets from .amadan/ci.d/20-cross@5 once on the final tip.
+```
+
+Also retain the repository's flag-off tests, #267 authority checks and claim identity/retirement tests. There is no exemption for a known failure. A sandbox bind or cache error is reported separately from a failing assertion and must not be called a passing gate. No production test or AWS operation is required to author or locally replay this plan.
+
+## Cleanup
+
+After all replay commands and processes finish, leave the scratch directory and remove only the path returned by this task's `mktemp`. The default shared Go cache is not task scratch and must remain untouched.
+
+```bash
+cd "$CLAIM12_REPO"
+python3 - <<'CLEAN'
+import os, pathlib, shutil
+p = pathlib.Path(os.environ['CLAIM12_SCRATCH'])
+assert p.is_absolute() and p.name.startswith('claim12.')
+shutil.rmtree(p)
+CLEAN
+```