carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

Moves Phase 1, Plan 2: Cut, Flip and Landing Implementation Plan

> For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Cut a prepared move over: stop the source with durable evidence, seal the staged copy by version, publish the archive, flip the record and the claim, and install the generation on the target through restored, with every phase journaled and replayable and no process yet started on the target.

Architecture: The source's cut phases live in internal/host/movescut.go beside movespass.go, the target's landing phases in internal/host/movesland.go; both drive provision.MoveState CASes guarded by move id and incarnation and then the claim CASes internal/hostclaim/move.go defines (Flip, ExecToNone, SetMovePhase, SetJournal). Stop evidence is activator.ReconstructStopEvidence, built on cgroup.Cleanup and stopevidence.Collect. The generation build is internal/moves/build.go: litestream's own planner over moves.SealedClient (exactly the sealed versions), then the snapshot and anchor encoded with ltx.NewEncoder as litestreamfacts does. Holds are registry rows with hold_op; reconcile mints and keeps participant rows through two hooks the host package supplies. appconfig.RenderHeld and release.MaterializeHeld render and stage for a held row. internal/park gains Engine.Parked; the existing kinds and the parked/ destination carry move retirements (index D6).

Tech Stack: Go, litestream v0.5.14 (NewReplicaWithClient, RestoreOptions.TXID), superfly/ltx v0.5.1 (NewEncoder, NewDecoder, Verify), modernc sqlite via lease.CheckDB, cgroup v2 (internal/cgroup), internal/stopevidence, internal/hostguard, internal/park, internal/genselect.

Spec: docs/superpowers/specs/2026-10-02-move-any-app-design.md rev 7.18: §5.1 (steps 1 to 8, markers, the start check), §5.2, §5.3, §6.1, §6.2, §6.4, §7.4, §7.5, §7.6, §7.13, §9.1, §9.2, §9.3, §9.4, §9.6, §19.2 K3, K4, K5, K9, K13. Index: 2026-10-06-moves-phase1-0-index.md. Plan 1 must be merged.

Global Constraints

  • Gate before every commit: go build ./..., go vet ./..., go test ./... -count=1, gofmt -l . clean, make test; cross-compile darwin, windows, linux-arm64. Anything that opens a cgroup, scans /proc or flocks is behind //go:build linux with a !linux stub (AGENTS.md cross-compile gate). Sandboxed Go: export GOCACHE=$TMPDIR/gocache GOFLAGS=-buildvcs=false.
  • Tests serial and niced; the cgroup test (Task 3) skips unless CARLOS_CGROUP_TEST=1 and a delegated cgroup v2 root is present.
  • Every phase is written to the record before its side effects (§7.4, §7.6); every box transition is a CAS guarded by move.id and move.incarnation and revalidates the claim's op id, kind and phase first (K5).
  • The required final TXID N is the MaxTXID of the source's newest level-0 file after the final checkpoint and SyncAndWait (§5.1 step 1); the build must reach exactly N and pass PRAGMA integrity_check.
  • Snapshot and anchor are written as the pinned writers write theirs: HeaderFlagNoChecksum, checksums zero, trailer CRC-64 present; the anchor's WALOffset is litestream.WALHeaderSize (facts R5-1c, R5-2).
  • A build is two durable states, intent then completed manifest; generation.json is never written by the landing (§7.6 published; R10-3).
  • One absolute deadline: cut_deadline = cut_at + cut_budget − open_budget; every phase before the commit checks it (§7.4).
  • promoteStagedReplica and the staging-wins restore are deleted (§5.1, M15).
  • No em dashes in any string.
  • Cold only (P7, index D14); the switch (D13) gates the cut and the landing like every other entry point.
  • The install routine (Task 8) is shared with region HA's relocation (spec rev 7.19 §7.6, L16): its input is a pinned version set and N, never "a staged replica" by name.

Review Focus

  1. A source whose agent restarts between stopping and stopped must produce the same evidence from cgroup and unit state with no memory, and must not spawn anything meanwhile (INV-3, codex R2-2). Test in Task 5 (TestCutResumesStoppingFromEvidenceAlone).
  2. A final mirror round that defers an object three times must revert, never flip (§7.4 staged failure). Test in Task 5 (TestCutRevertsAfterThreeDeferredRounds).
  3. A landing whose building crashed after the snapshot PUT and before the manifest must rebuild identical bytes and reuse the existing snapshot version, never write a second differing object (R6-5, R7-4; witness wBuildCrashes). Test in Task 8 (TestBuildReplayReusesIdenticalSnapshot).
  4. A configured phase whose rendered bundle etags differ from move.inputs must mark the move for rollback with "configuration changed during the move", not install (§7.6; J2-R2-5). Test in Task 9 (TestLandingConfiguredPinsInputs).
  5. Reconcile on the target must never mint the landing row ready, and reconcile on the source must never remove the moving row while the claim lists the box as a participant, whatever the record's region says (§9.6, K9, codex R2-4). Test in Task 6 (TestReconcileHoldsParticipantRows).

---

File structure

| path | responsibility | |---|---| | internal/provision/provision.go | cut, landing and reverting phase transitions on the record | | internal/hostclaim/move.go | SetMovePhase, SetJournal, ExecToNone, Flip, Revert, journal keys | | internal/activator/stopreconstruct_linux.go, _other.go | ReconstructStopEvidence | | internal/activator/backend_exec.go | strict spawns under a move; the staging-wins fallback deleted | | internal/moves/seal.go | Seal and its journal form (data shape from Plan 1's index decision D1) | | internal/moves/evidence.go | StoppedReport | | internal/moves/journal.go | the box-local operation journal F12 | | internal/moves/ltxbuild.go | full-file LTX encoding, BuildIntent, InstallManifest | | internal/moves/sealedclient.go | SealedClient, a litestream.ReplicaClient over the sealed versions | | internal/moves/build.go | BuildGeneration | | internal/park/remote.go | Engine.Parked (the existing kinds and parked/ destination carry move retirements, index D6) | | internal/appconfig/renderheld.go | RenderHeld and the move-bound ack | | internal/release/materializeheld.go | MaterializeHeld | | internal/provision/reconcile.go | Hold, KeepParticipant | | internal/schedule/runner.go, converge.go | held rows skipped; Busy, Frozen | | internal/host/movescut.go | the source's cut phases | | internal/host/movesland.go | the target's landing phases through restored | | internal/host/moveshold.go | the hold view the host package gives reconcile and schedule |

---

Task 1: Record transitions for the cut, the flip and the landing

Files:

  • Modify: internal/provision/provision.go
  • Test: internal/provision/move_test.go

Interfaces:

  • Consumes: MoveGuard and MoveGuardedCut from Plan 1 Task 2 (the guard is defined there, with the guarded promote, cut, abort and abort-complete; this task adds the cut phases on top; MoveGuardedCut is the ready → cutting/requested entry the source uses for cut == "auto" and the console's cut door uses too).
  • Produces: MoveCutPhase(g MoveGuard, from, to string, stamp func(*MoveState)) func(*Record) error (state must be cutting; CutPhase == from); MoveAdmit(g, now time.Time, budgetS int, deadline time.Time, inputs MoveInputs) (requested → admitted, stamps CutAt, CutBudgetS, CutDeadline, Inputs); MoveBackToReady(g) (cutting/requested|admitted → ready, clears the cut fields); MoveRecordSource(g, src MoveSource) (sets Source, legal in stopped only); MoveFlip(g, now time.Time, landingDeadline time.Time) (cutting/archived → landing, Region = To, CutPhase = flipped, FlippedAt, LandingDeadline, LandingPhase = checking); MoveLandingPhase(g, from, to string, stamp func(*MoveState)) (state landing); MoveRevert(g, reason string) (from cutting with CutPhase in stopping..archived → reverting, Reason).
  • [ ] Step 1: Write the failing test
func TestCutPhasesAdvanceInOrderUnderTheGuard(t *testing.T) {
	g := MoveGuard{ID: "mv_abcdefghijklmnopqrstuvwxyz", Incarnation: strings.Repeat("ab", 32)}
	now := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC)
	rec := &Record{Region: "ie", Move: &MoveState{ID: g.ID, Incarnation: g.Incarnation, From: "ie", To: "uk", State: MoveReady}}
	if err := MoveGuardedCut(g)(rec); err != nil || rec.Move.State != MoveCutting || rec.Move.CutPhase != CutRequested {
		t.Fatalf("auto cut: %v %+v", err, rec.Move)
	}
	deadline := now.Add(115 * time.Second)
	if err := MoveAdmit(g, now, 120, deadline, MoveInputs{Release: "v1"})(rec); err != nil || rec.Move.CutPhase != CutAdmitted || !rec.Move.CutDeadline.Equal(deadline) || rec.Move.Inputs.Release != "v1" {
		t.Fatalf("admit: %v %+v", err, rec.Move)
	}
	// Wrong incarnation: refused.
	if err := MoveCutPhase(MoveGuard{ID: g.ID, Incarnation: strings.Repeat("cd", 32)}, CutAdmitted, CutStopping, nil)(rec); !errors.Is(err, ErrMoveConflict) {
		t.Fatalf("wrong guard: %v", err)
	}
	// Skipping a phase: refused.
	if err := MoveCutPhase(g, CutStopping, CutStopped, nil)(rec); !errors.Is(err, ErrMoveConflict) {
		t.Fatalf("skip: %v", err)
	}
	for _, step := range [][2]string{{CutAdmitted, CutStopping}, {CutStopping, CutStopped}} {
		if err := MoveCutPhase(g, step[0], step[1], nil)(rec); err != nil {
			t.Fatal(err)
		}
	}
	if err := MoveRecordSource(g, MoveSource{FinalTXID: 42, TimersSHA256: "sha256:t"})(rec); err != nil || rec.Move.Source.FinalTXID != 42 {
		t.Fatalf("source: %v", err)
	}
	for _, step := range [][2]string{{CutStopped, CutStaged}, {CutStaged, CutArchived}} {
		if err := MoveCutPhase(g, step[0], step[1], nil)(rec); err != nil {
			t.Fatal(err)
		}
	}
	// Abort is refused past requested.
	if err := MoveAbort()(rec); !errors.Is(err, ErrMoveConflict) {
		t.Fatal("abort past admitted")
	}
	ld := now.Add(10 * time.Minute)
	if err := MoveFlip(g, now, ld)(rec); err != nil || rec.Region != "uk" || rec.Move.State != MoveLanding || rec.Move.CutPhase != CutFlipped || rec.Move.LandingPhase != LandChecking || !rec.Move.LandingDeadline.Equal(ld) {
		t.Fatalf("flip: %v %+v", err, rec)
	}
	if err := MoveFlip(g, now, ld)(rec); !errors.Is(err, ErrMoveConflict) {
		t.Fatal("a second flip")
	}
	if err := MoveLandingPhase(g, LandChecking, LandParking, nil)(rec); err != nil || rec.Move.LandingPhase != LandParking {
		t.Fatalf("landing phase: %v", err)
	}
}

func TestBackToReadyAndRevert(t *testing.T) {
	g := MoveGuard{ID: "mv_abcdefghijklmnopqrstuvwxyz", Incarnation: strings.Repeat("ab", 32)}
	now := time.Now()
	rec := &Record{Region: "ie", Move: &MoveState{ID: g.ID, Incarnation: g.Incarnation, From: "ie", To: "uk", State: MoveCutting, CutPhase: CutAdmitted, CutAt: &now}}
	if err := MoveBackToReady(g)(rec); err != nil || rec.Move.State != MoveReady || rec.Move.CutPhase != "" || rec.Move.CutAt != nil {
		t.Fatalf("back to ready: %v %+v", err, rec.Move)
	}
	rec.Move.State, rec.Move.CutPhase = MoveCutting, CutStopped
	if err := MoveRevert(g, "stop evidence budget")(rec); err != nil || rec.Move.State != MoveReverting || rec.Move.Reason == "" {
		t.Fatalf("revert: %v %+v", err, rec.Move)
	}
	// Not from requested or admitted: nothing ran yet, so back-to-ready is the exit there.
	rec2 := &Record{Region: "ie", Move: &MoveState{ID: g.ID, Incarnation: g.Incarnation, State: MoveCutting, CutPhase: CutRequested}}
	if err := MoveRevert(g, "x")(rec2); !errors.Is(err, ErrMoveConflict) {
		t.Fatal("revert from requested")
	}
}
  • [ ] Step 2: Run to verify it fails: nice -n 10 go test ./internal/provision -run 'TestCutPhases|TestBackToReady' -count=1 → FAIL, undefined.
  • [ ] Step 3: Implement
// MoveGuard and its check are Plan 1 Task 2's; the cut phases below build on them.

var cutOrder = []string{CutRequested, CutAdmitted, CutStopping, CutStopped, CutStaged, CutArchived, CutFlipped}

func nextIn(order []string, from, to string) bool {
	for i := 0; i+1 < len(order); i++ {
		if order[i] == from && order[i+1] == to {
			return true
		}
	}
	return false
}

// MoveCutPhase advances cut_phase one step, in order, under the guard.
// stamp may fill the fields the phase records; it runs after the check.
func MoveCutPhase(g MoveGuard, from, to string, stamp func(*MoveState)) func(*Record) error {
	return func(rec *Record) error {
		if err := g.check(rec); err != nil {
			return err
		}
		if rec.Move.State != MoveCutting || rec.Move.CutPhase != from || !nextIn(cutOrder, from, to) {
			return fmt.Errorf("%w: cut is %s/%s, step expects %s → %s", ErrMoveConflict, rec.Move.State, rec.Move.CutPhase, from, to)
		}
		rec.Move.CutPhase = to
		if stamp != nil {
			stamp(rec.Move)
		}
		return nil
	}
}

// MoveAdmit is §7.4 admitted: the budget, the absolute deadline and the
// pinned inputs, in one write.
func MoveAdmit(g MoveGuard, now time.Time, budgetS int, deadline time.Time, inputs MoveInputs) func(*Record) error {
	return MoveCutPhase(g, CutRequested, CutAdmitted, func(m *MoveState) {
		t := now.UTC()
		d := deadline.UTC()
		in := inputs
		m.CutAt, m.CutBudgetS, m.CutDeadline, m.Inputs = &t, budgetS, &d, &in
	})
}

// MoveBackToReady undoes an admission whose checks are no longer fresh or
// whose inputs moved (§7.4 admitted, codex R2-8). Legal from requested or
// admitted only: nothing has stopped yet.
func MoveBackToReady(g MoveGuard) func(*Record) error {
	return func(rec *Record) error {
		if err := g.check(rec); err != nil {
			return err
		}
		if rec.Move.State != MoveCutting || (rec.Move.CutPhase != CutRequested && rec.Move.CutPhase != CutAdmitted) {
			return fmt.Errorf("%w: back to ready is legal from requested or admitted only", ErrMoveConflict)
		}
		rec.Move.State, rec.Move.CutPhase, rec.Move.CutAt, rec.Move.CutBudgetS, rec.Move.CutDeadline, rec.Move.Inputs = MoveReady, "", nil, 0, nil, nil
		return nil
	}
}

// MoveRecordSource writes the stopped source's baselines (§7.4 stopped).
func MoveRecordSource(g MoveGuard, src MoveSource) func(*Record) error {
	return func(rec *Record) error {
		if err := g.check(rec); err != nil {
			return err
		}
		if rec.Move.State != MoveCutting || rec.Move.CutPhase != CutStopped {
			return fmt.Errorf("%w: source digests are recorded at stopped", ErrMoveConflict)
		}
		s := src
		rec.Move.Source = &s
		return nil
	}
}

// MoveFlip is the flip (§7.4, §7.5): region = to, state = landing, the
// landing deadline, landing_phase = checking, in one record write. The
// claim follows (hostclaim.Flip).
func MoveFlip(g MoveGuard, now, landingDeadline time.Time) func(*Record) error {
	return func(rec *Record) error {
		if err := g.check(rec); err != nil {
			return err
		}
		if rec.Move.State != MoveCutting || rec.Move.CutPhase != CutArchived {
			return fmt.Errorf("%w: the flip follows archived", ErrMoveConflict)
		}
		t, d := now.UTC(), landingDeadline.UTC()
		rec.Region = rec.Move.To
		rec.Move.State, rec.Move.CutPhase, rec.Move.FlippedAt, rec.Move.LandingDeadline, rec.Move.LandingPhase = MoveLanding, CutFlipped, &t, &d, LandChecking
		return nil
	}
}

var landingOrder = []string{LandChecking, LandParking, LandBuilding, LandPublished, LandLocal, LandConfigured, LandRestored, LandProbing, LandProbed}

// MoveLandingPhase advances landing_phase one step (§7.6).
func MoveLandingPhase(g MoveGuard, from, to string, stamp func(*MoveState)) func(*Record) error {
	return func(rec *Record) error {
		if err := g.check(rec); err != nil {
			return err
		}
		if rec.Move.State != MoveLanding || rec.Move.LandingPhase != from || !nextIn(landingOrder, from, to) {
			return fmt.Errorf("%w: landing is %s/%s, step expects %s → %s", ErrMoveConflict, rec.Move.State, rec.Move.LandingPhase, from, to)
		}
		rec.Move.LandingPhase = to
		if stamp != nil {
			stamp(rec.Move)
		}
		return nil
	}
}

// MoveRevert starts the pre-flip recovery (§7.4 reverting): from any cut
// phase that has stopped the source. Before stopping, back-to-ready is
// the exit.
func MoveRevert(g MoveGuard, reason string) func(*Record) error {
	return func(rec *Record) error {
		if err := g.check(rec); err != nil {
			return err
		}
		switch {
		case rec.Move.State != MoveCutting:
			return fmt.Errorf("%w: revert is a cut's recovery", ErrMoveConflict)
		case rec.Move.CutPhase == CutRequested || rec.Move.CutPhase == CutAdmitted:
			return fmt.Errorf("%w: nothing has stopped yet. back to ready instead", ErrMoveConflict)
		case rec.Move.CutPhase == CutFlipped:
			return fmt.Errorf("%w: flipped. a rollback, not a revert", ErrMoveConflict)
		}
		rec.Move.State, rec.Move.Reason = MoveReverting, reason
		return nil
	}
}
  • [ ] Step 4: Run: nice -n 10 go test ./internal/provision -count=1 → PASS.
  • [ ] Step 5: Commit: git commit -m "provision: cut, flip and landing transitions under the move guard (§7.4 to §7.6)" ...

---

Task 2: Claim transitions for the cut and the flip

Files:

  • Modify: internal/hostclaim/move.go, internal/hostclaim/move_test.go

Interfaces:

  • Produces: journal keys JournalSeal = "seal", JournalSrcGen = "src_gen", JournalSourceStamp = "source_stamp", JournalCutSession = "cut_session", JournalRevPhase = "rev_phase", JournalEvidenceSource = "evidence_source", JournalEvidenceTarget = "evidence_target"; SetMovePhase(opID, kind, from, to string) func(*Claim) error; SetJournal(opID string, kv map[string]string) func(*Claim) error (op id only; any kind); ExecToNone(opID, box, evidenceKey, journalKey string) func(*Claim) error (K3: c.Op.Exec == box → "", records the evidence key under journalKey); Flip(opID, targetBox string, sourceStamp uint64, srcGen string) func(*Claim) error (K4: requires KindMoving, phase sealed, Exec == "", evidence_source journalled; sets Kind = KindLanding, Phase = "checking", Exec = targetBox, holder.stamp = c.Gen + 1, the gen this CAS produces: hostclaim.Update sets gen = observed + 1 after tr runs (store.go, and create.go:94 writes the serving stamp the same way); journals source_stamp and src_gen); Revert(opID, sourceBox, targetRegion string, stagingScope *ParkScope, newID func() string) func(*Claim) error (from KindMoving, Exec == "", evidence_source journalled → KindReverting, Exec = sourceBox, rev_phase = "evidence" unless set, plus the lane park-replica obligation for the target region's lane with the writers' bound as its scope, which §7.11's "opened waits for the staging obligation" refers to; Plan 4 Task 6 waits on it and Plan 5 Task 1 dispatches it).
  • [ ] Step 1: Write the failing test
func TestCutClaimCASesFollowK3AndK4(t *testing.T) {
	c := movingTestClaim()
	op := "mv_abcdefghijklmnopqrstuvwxyz"
	_ = BeginMove(op, "ops", "ie-box", "uk-box", "uk", time.Now())(c)
	// Only the executor gives execution up, and only citing evidence.
	if err := ExecToNone(op, "uk-box", "moves/bab/app/h/mv/stopped-source.json", JournalEvidenceSource)(c); !errors.Is(err, ErrWrongState) {
		t.Fatalf("non-exec to none: %v", err)
	}
	if err := ExecToNone(op, "ie-box", "", JournalEvidenceSource)(c); !errors.Is(err, ErrMalformed) {
		t.Fatalf("no evidence: %v", err)
	}
	// The flip needs the seal phase and exec none.
	if err := Flip(op, "uk-box", 3, "")(c); !errors.Is(err, ErrWrongState) {
		t.Fatalf("flip before sealed: %v", err)
	}
	if err := ExecToNone(op, "ie-box", "moves/bab/app/h/mv/stopped-source.json", JournalEvidenceSource)(c); err != nil || c.Op.Exec != "" {
		t.Fatalf("to none: %v %+v", err, c.Op)
	}
	if err := SetJournal(op, map[string]string{JournalSeal: `{"n":7}`, JournalCutSession: "5"})(c); err != nil || c.Op.Journal[JournalSeal] != `{"n":7}` {
		t.Fatalf("journal: %v", err)
	}
	if err := SetMovePhase(op, KindMoving, PhaseMoving, PhaseSealed)(c); err != nil || c.Op.Phase != PhaseSealed {
		t.Fatalf("phase: %v", err)
	}
	if err := SetMovePhase(op, KindMoving, PhaseMoving, PhaseSealed)(c); !errors.Is(err, ErrWrongState) {
		t.Fatal("phase replay")
	}
	before := c.Holder.Stamp
	c.Gen = 9 // what Update will have set
	if err := Flip(op, "uk-box", before, "")(c); err != nil {
		t.Fatal(err)
	}
	if c.Op.Kind != KindLanding || c.Op.Phase != "checking" || c.Op.Exec != "uk-box" || c.Op.Journal[JournalSourceStamp] != "3" || c.Op.Journal[JournalSrcGen] != "" || c.Holder.Stamp == before {
		t.Fatalf("after flip: %+v stamp %d", c.Op, c.Holder.Stamp)
	}
	// holder.generation is untouched by the flip (§7.5).
	if c.Holder.Generation != "" {
		t.Fatal("flip moved the generation")
	}
}

func TestRevertNeedsEvidenceAndReturnsExecToSource(t *testing.T) {
	c := movingTestClaim()
	op := "mv_abcdefghijklmnopqrstuvwxyz"
	_ = BeginMove(op, "ops", "ie-box", "uk-box", "uk", time.Now())(c)
	if err := Revert(op, "ie-box")(c); !errors.Is(err, ErrWrongState) {
		t.Fatalf("revert while exec held: %v", err)
	}
	_ = ExecToNone(op, "ie-box", "moves/bab/app/h/mv/stopped-source.json", JournalEvidenceSource)(c)
	scope := ParkScope{Location: "moves/a.go.example.com/" + op, Incarnation: c.Holder.Incarnation, EvidenceAt: time.Now(), RequestLimit: 120 * time.Second}
	ids := 0
	newID := func() string { ids++; return fmt.Sprintf("id%d", ids) }
	if err := Revert(op, "uk-box", "uk", &scope, newID)(c); !errors.Is(err, ErrWrongState) {
		t.Fatalf("revert to the target: %v", err)
	}
	if err := Revert(op, "ie-box", "uk", &scope, newID)(c); err != nil || c.Op.Kind != KindReverting || c.Op.Exec != "ie-box" || c.Op.Journal[JournalRevPhase] != "evidence" {
		t.Fatalf("revert: %v %+v", err, c.Op)
	}
	if len(c.Obligations) != 1 || c.Obligations[0].Kind != "park-replica" || c.Obligations[0].Region != "uk" || c.Obligations[0].Park == nil {
		t.Fatalf("revert queued no target staging park: %+v", c.Obligations)
	}
}
  • [ ] Step 2: Run to verify it fails, Step 3: implement
const (
	JournalSeal           = "seal"
	JournalSrcGen         = "src_gen"
	JournalSourceStamp    = "source_stamp"
	JournalCutSession     = "cut_session"
	JournalRevPhase       = "rev_phase"
	JournalEvidenceSource = "evidence_source"
	JournalEvidenceTarget = "evidence_target"
)

func setJournal(c *Claim, k, v string) {
	if c.Op.Journal == nil {
		c.Op.Journal = map[string]string{}
	}
	c.Op.Journal[k] = v
}

// SetJournal records move journal entries under the op (D1): what the
// claim half must read with the same fresh read as an admission.
func SetJournal(opID string, kv map[string]string) func(*Claim) error {
	return func(c *Claim) error {
		if err := RequireOp(opID)(c); err != nil {
			return err
		}
		for k, v := range kv {
			setJournal(c, k, v)
		}
		return nil
	}
}

// SetMovePhase moves the op's phase one step, under its kind.
func SetMovePhase(opID, kind, from, to string) func(*Claim) error {
	return func(c *Claim) error {
		if err := RequireMoveOp(opID, kind, from)(c); err != nil {
			return err
		}
		c.Op.Phase = to
		return nil
	}
}

// ExecToNone is K3: a participant that has written stop evidence for this
// op gives execution up. Nobody moves exec from one box to another. A
// replay after a crash finds exec already empty and the same evidence
// journalled: that is idempotent, not a conflict.
func ExecToNone(opID, box, evidenceKey, journalKey string) func(*Claim) error {
	return func(c *Claim) error {
		if err := RequireOp(opID)(c); err != nil {
			return err
		}
		if evidenceKey == "" || (journalKey != JournalEvidenceSource && journalKey != JournalEvidenceTarget) {
			return fmt.Errorf("%w: execution is given up only with evidence", ErrMalformed)
		}
		if c.Op.Exec == "" && c.Op.Journal[journalKey] == evidenceKey {
			return nil
		}
		if c.Op.Exec != box {
			return fmt.Errorf("%w: exec is %q, not %q", ErrWrongState, c.Op.Exec, box)
		}
		c.Op.Exec = ""
		setJournal(c, journalKey, evidenceKey)
		return nil
	}
}

// Flip is the claim half of the record's flip (§7.5, K4): moving/sealed →
// landing/checking, exec none → target, citing the source's evidence;
// holder.stamp = the gen this CAS produces (the target's installation
// stamp F, claim rev 6.5 §6.4), with the source's stamp S and generation
// journalled for a rollback's return. holder.generation is not touched.
func Flip(opID, targetBox string, sourceStamp uint64, srcGen string) func(*Claim) error {
	return func(c *Claim) error {
		if err := RequireMoveOp(opID, KindMoving, PhaseSealed)(c); err != nil {
			return err
		}
		if c.Op.Exec != "" || c.Op.Journal[JournalEvidenceSource] == "" {
			return fmt.Errorf("%w: the flip needs exec none and the source's evidence", ErrWrongState)
		}
		if len(c.Op.Participants) != 2 || c.Op.Participants[1] != targetBox {
			return fmt.Errorf("%w: %q is not the move's target", ErrWrongState, targetBox)
		}
		c.Op.Kind, c.Op.Phase, c.Op.Exec = KindLanding, "checking", targetBox
		setJournal(c, JournalSourceStamp, strconv.FormatUint(sourceStamp, 10))
		setJournal(c, JournalSrcGen, srcGen)
		c.Holder.Stamp = c.Gen + 1
		return nil
	}
}

// Revert is §7.4 reverting: the source takes execution back with its own
// evidence, before the flip. The target region's staging park is queued in
// the same write (§7.11: the revert's `opened` waits for it), owned by the
// target region's box, scoped to the lane with the writers' horizon.
func Revert(opID, sourceBox, targetRegion string, stagingScope *ParkScope, newID func() string) func(*Claim) error {
	return func(c *Claim) error {
		if err := RequireOp(opID)(c); err != nil {
			return err
		}
		if c.Op.Kind != KindMoving || c.Op.Exec != "" || c.Op.Journal[JournalEvidenceSource] == "" {
			return fmt.Errorf("%w: revert needs a moving op with exec none and the source's evidence", ErrWrongState)
		}
		if len(c.Op.Participants) == 0 || c.Op.Participants[0] != sourceBox {
			return fmt.Errorf("%w: %q is not the move's source", ErrWrongState, sourceBox)
		}
		if stagingScope == nil || targetRegion == "" {
			return fmt.Errorf("%w: a revert names the staging lane it leaves behind", ErrMalformed)
		}
		if err := stagingScope.Validate(c.Host); err != nil {
			return err
		}
		c.Op.Kind, c.Op.Exec = KindReverting, sourceBox
		if c.Op.Journal[JournalRevPhase] == "" {
			setJournal(c, JournalRevPhase, "evidence")
		}
		for _, o := range c.Obligations {
			if o.Op == opID && o.Kind == "park-replica" && o.Region == targetRegion {
				return nil
			}
		}
		s := *stagingScope
		c.Obligations = append(c.Obligations, Obligation{ID: newID(), Op: opID, Kind: "park-replica", Region: targetRegion, Retirement: newID(), Owner: "region:" + targetRegion, Park: &s})
		return nil
	}
}

c.Gen + 1 presumes Update increments Gen after the transition; check store.go's Update and create.go's CompleteCreate (which writes the serving stamp) and use the same expression they use. If Update increments before calling tr, write c.Gen. The test sets c.Gen explicitly, so adjust its expectation to the real convention.

  • [ ] Step 4: Run: nice -n 10 go test ./internal/hostclaim -count=1 → PASS.
  • [ ] Step 5: Commit: git commit -m "hostclaim: ExecToNone, Flip, Revert, SetMovePhase, SetJournal (moves K3, K4, §7.5)" ...

---

Task 3: ReconstructStopEvidence

Files:

  • Create: internal/stopevidence/reconstruct_linux.go, internal/stopevidence/reconstruct_other.go, internal/stopevidence/reconstruct_linux_test.go
  • Create: internal/moves/evidence.go, internal/lease/lockstate.go, internal/lease/lockstate_test.go

Interfaces:

  • Consumes: cgroup.Manager.OpenCleanup(host) (*cgroup.Cleanup, error), (*Cleanup).KillAndEmpty(ctx), Populated(), Members(), Close(); stopevidence.Collect, stopevidence.FDHolders(procRoot, paths), stopevidence.Cleanup; cgroup.ParseSelfCgroup. The function lives in internal/stopevidence, not internal/activator: stopevidence already imports activator (evidence.go:10, for *activator.StopEvidence), so the activator cannot import it back; stopevidence may import moves (which imports neither). The cgroup manager is the exec backend's ExecBackend.Cgroups (backend_exec.go), handed in explicitly by the host package at arm time (eb.Cgroups); activator.Service has no manager field.
  • Produces: moves.StoppedReport{V int; Role, Box, Incarnation string; ClaimGen uint64; Host, OpID string; Steps map[string]moves.Check; Stray []int; FinalTXID uint64; DBSHA256, FilesPlain, TimersSHA256 string; CheckedAt time.Time} with step ids StepTree = "tree", StepStray = "stray", StepFDs = "fds", StepLease = "lease", StepReplica = "replica"; (r StoppedReport) Complete() bool (every step OK, StepReplica included); (r StoppedReport) TreeStopped() bool (steps 1 to 4 OK: what stopping needs before the checkpoint); lease.LockState(ctx, bucket, prefix, region string) (held bool, owner string, expires time.Time, err error) (reads lock.json as the leaser writes it; absent → held=false); stopevidence.ReconstructInputs{Host string; Cgroups *cgroup.Manager; DBPaths []string; ProcRoot string; LeaseFree func(context.Context) (bool, error)}; stopevidence.Reconstruct(ctx, in ReconstructInputs) (moves.StoppedReport, error) (steps 1 to 4 of §5.3; step 5, the final checkpoint and digests, is the caller's, which then sets StepReplica); on !linux, returns ErrUnsupported.
  • [ ] Step 1: Write the failing tests

internal/lease/lockstate_test.go: with the package's leasetest local store (see internal/lease/leasetest), acquire a lease then LockState → held with an expiry in the future; release → not held; an absent prefix → not held, nil error.

internal/activator/stopreconstruct_linux_test.go:

//go:build linux

// §14.1: ReconstructStopEvidence against a real cgroup v2 hierarchy with a
// forked descendant that closes its descriptors and outlives its parent
// (witness wEvidenceWholeTree). Needs a delegated cgroup root:
// CARLOS_CGROUP_TEST=1 and CARLOS_CGROUP_ROOT=<writable cgroup2 dir>.
func TestReconstructStopEvidenceKillsTheWholeTree(t *testing.T) {
	root := os.Getenv("CARLOS_CGROUP_ROOT")
	if os.Getenv("CARLOS_CGROUP_TEST") != "1" || root == "" {
		t.Skip("needs CARLOS_CGROUP_TEST=1 and a delegated cgroup v2 root")
	}
	m, err := cgroup.SetupAt(root, t.Logf)
	if err != nil {
		t.Skip(err)
	}
	host := "ev.go.example.com"
	// A parent that forks a child which detaches and sleeps, then exits.
	cmd, err := m.Start(host, cgroup.Limit{}, func() *exec.Cmd {
		return exec.Command("sh", "-c", "(sleep 300 </dev/null >/dev/null 2>&1 &) ; exit 0")
	})
	if err != nil {
		t.Fatal(err)
	}
	cmd.Wait()
	rep, err := Reconstruct(context.Background(), ReconstructInputs{Host: host, Cgroups: m, ProcRoot: "/proc", LeaseFree: func(context.Context) (bool, error) { return true, nil }})
	if err != nil {
		t.Fatal(err)
	}
	if !rep.Steps[moves.StepTree].OK || !rep.TreeStopped() || rep.Complete() {
		t.Fatalf("tree evidence after the parent exited: %+v (Complete needs the caller's replica step)", rep)
	}
	c, _ := m.OpenCleanup(host)
	pop, _ := c.Populated()
	if pop {
		t.Fatal("the descendant survived")
	}
}

func TestReconstructStopEvidenceRefusesAStrayProcess(t *testing.T) {
	// A fake self-cgroup listing with a second pid refuses with StepStray.
	rep := strayCheck([]int{os.Getpid(), 99999}, os.Getpid())
	if rep.OK {
		t.Fatal("a stray pid passed")
	}
}
  • [ ] Step 2: Run to verify they fail, Step 3: implement

moves/evidence.go declares the report and steps (as above, with Complete() and TreeStopped()). lease/lockstate.go reads prefix + "/lock.json" through the same S3 client constructor acquireTTL uses and decodes the leaser's lock body (owner, expires); if the leaser library (lss3.Leaser) exposes a read, use it. reconstruct_linux.go:

//go:build linux

package stopevidence

// Reconstruct is §5.3's evidence from durable, observable state: it reads
// nothing from the agent's memory, so a restarted agent gives the same
// answer. Steps 1 to 4; the caller does step 5 (the final checkpoint and
// the digests), since only it knows the paths and the replica it closes.
func Reconstruct(ctx context.Context, in ReconstructInputs) (moves.StoppedReport, error) {
	rep := moves.StoppedReport{V: 1, Host: in.Host, Steps: map[string]moves.Check{}, CheckedAt: time.Now().UTC()}
	if in.Cgroups == nil || in.LeaseFree == nil || !controlname.ValidHostShape(in.Host) {
		return rep, ErrInvalidInput
	}
	procRoot := in.ProcRoot
	if procRoot == "" {
		procRoot = "/proc"
	}
	// 1. Kill and drain the tree.
	c, err := in.Cgroups.OpenCleanup(in.Host)
	switch {
	case errors.Is(err, cgroup.ErrNoCgroup): // the group never existed or was removed: an empty tree
		rep.Steps[moves.StepTree] = moves.Check{OK: true, Reason: "no cgroup"}
	case err != nil:
		return rep, err
	default:
		defer c.Close()
		if err := Cleanup(ctx, c); err != nil {
			rep.Steps[moves.StepTree] = moves.Check{Reason: err.Error()}
		} else {
			rep.Steps[moves.StepTree] = moves.Check{OK: true}
		}
	}
	// 2. No stray process in the agent's own leaf.
	self, err := os.ReadFile(filepath.Join(procRoot, "self", "cgroup"))
	if err != nil {
		return rep, err
	}
	leaf, ok := cgroup.ParseSelfCgroup(string(self))
	if !ok {
		return rep, fmt.Errorf("stopevidence: own cgroup unreadable")
	}
	pids, err := readProcs(filepath.Join("/sys/fs/cgroup", leaf, "cgroup.procs"))
	if err != nil {
		return rep, err
	}
	rep.Steps[moves.StepStray] = strayCheck(pids, os.Getpid())
	for _, p := range pids {
		if p != os.Getpid() {
			rep.Stray = append(rep.Stray, p)
		}
	}
	// 3. No open database.
	holders, err := FDHolders(procRoot, dbPathsWithSidecars(in.DBPaths))
	if err != nil {
		return rep, err
	}
	if len(holders) == 0 {
		rep.Steps[moves.StepFDs] = moves.Check{OK: true}
	} else {
		rep.Steps[moves.StepFDs] = moves.Check{Reason: fmt.Sprintf("%d process(es) hold the database open", len(holders))}
	}
	// 4. Lease released.
	free, err := in.LeaseFree(ctx)
	if err != nil {
		return rep, err
	}
	if free {
		rep.Steps[moves.StepLease] = moves.Check{OK: true}
	} else {
		rep.Steps[moves.StepLease] = moves.Check{Reason: "lock.json still held"}
	}
	return rep, nil
}

func strayCheck(pids []int, self int) moves.Check {
	for _, p := range pids {
		if p != self {
			return moves.Check{Reason: fmt.Sprintf("pid %d in the agent's cgroup is not the agent", p)}
		}
	}
	return moves.Check{OK: true}
}

func dbPathsWithSidecars(paths []string) []string {
	var out []string
	for _, p := range paths {
		out = append(out, p, p+"-wal", p+"-shm")
	}
	return out
}

(readProcs parses one pid per line; cgroup.ErrNoCgroup is cleanup.go:6's sentinel for a missing host cgroup; the agent's own leaf is the directory ParseSelfCgroup names under /sys/fs/cgroup, tested with a fixture procRoot.) reconstruct_other.go returns ErrUnsupported. The cut's step 5 (Task 5) adds StepReplica after the checkpoint. The host package wires moveCfg.Evidence to stopevidence.Reconstruct with Cgroups: eb.Cgroups at armMovesPass (which gains the eb *activator.ExecBackend argument; agent.go:371 has eb in hand).

  • [ ] Step 4: Run: nice -n 10 go test ./internal/stopevidence ./internal/lease ./internal/moves -count=1 -p 1; GOOS=darwin go build ./... → PASS.
  • [ ] Step 5: Commit: git commit -m "stopevidence: Reconstruct, stop evidence from cgroup, proc and lease state (§5.3)" ...

---

Task 4: The staging-wins restore under the switch (strict spawns moved to Plan 1 Task 7b)

Files:

  • Modify: internal/activator/backend_exec.go (spawnInCgroup path at :210, Restore at :290-320, delete promoteStagedReplica and MovesPrefix/MoveReceiptsKey if unused after Plan 1), internal/activator/backend_exec_test.go, internal/host/agent.go:363-369 (the eb.Mirror arming becomes unnecessary; keep Mirror only if another caller uses it)

Interfaces:

  • Produces: ExecBackend.Strict func(host string) bool (when true, the spawn uses Cgroups.PrepareStrict(host, limit) then Prepared.Start(cmd), and a failure refuses the spawn with ErrStrictRequired); (b *ExecBackend) NextSpawnStrict(host string) moves.Check (S2: OK when Strict(host) would be honoured: cgroup v2 delegated and PrepareStrict supported).
  • The host package sets eb.Strict = movesStrict (Task 10's hold view: true for a host whose claim op is a move kind).
  • [ ] Step 1: Write the failing test: TestRestoreDoesNotPromoteStagingUnderPhase1 (with b.Phase1 true, a staged object under moves/<host>/ and an empty own prefix → Restore returns fresh = true and the staged object is untouched; with b.Phase1 false the legacy promotion runs as today and its existing tests pass unchanged).
  • [ ] Step 2: Run, Step 3: implement: ExecBackend.Phase1 bool (set by the agent from moves.Enabled); Restore skips promoteStagedReplica when Phase1; the legacy code stays until Plan 6 Task 8 retires it (index D13). Strict spawns are Plan 1 Task 7b's.
  • [ ] Step 4: Run: nice -n 10 go test ./internal/activator ./internal/host -count=1 -p 1 → PASS.
  • [ ] Step 5: Commit: git commit -m "activator: strict spawns under a move; the staging-wins restore is gone (§5.3, §9.3, M15)" ...

---

Task 5: The cut on the source

Files:

  • Create: internal/host/movescut.go, internal/host/movescut_test.go
  • Create: internal/moves/seal.go, internal/moves/seal_test.go
  • Modify: internal/host/movespass.go (MoveCutting arm calls runCut; moveCfg gains Evidence, TimersDrained, NewestL0, OwnReplicaPosition, SealLeaseTTL)

Interfaces:

  • moves.Seal{V int; N uint64; Replica []SealedObject; Files []SealedObject; Timers []SealedObject; SrcCovered, FromDB bool; SealedAt time.Time; LeaseUntil time.Time}, SealedObject{Key, VersionID string; Size int64}, (s Seal) Encode() string, moves.DecodeSeal(string) (Seal, error), (s Seal) Bound() time.Time (= LeaseUntil: the time after which no staging writer that held the lease can still write; the staging park's evidence_at, Plan 5).
  • moveCfg.Evidence func(ctx, host string, dbPaths []string) (moves.StoppedReport, error) (wired to svc.ReconstructStopEvidence with leaseFree = lease.LockState on the own prefix); moveCfg.TimersDrained func(host string) bool (the runner's Busy, Task 6); moveCfg.NewestL0 func(ctx, bucket, prefix, region string) (uint64, error) (lists the own replica's level-0 files through lease.VersionedStore and returns the highest MaxTXID; the required final TXID N); moveCfg.LocalPosition func(dbPath string) (uint64, error) (the local database's TXID from litestream's -txid sidecar or its WAL; src_covered is N == LocalPosition with no pending write).
  • runCut(ctx, s, w, rec, tgt) error: the phase loop of §7.4: - requested: fresh target and source reports (moves.Ready with the last round's delta) and move.inputs to pin from the target report → MoveAdmit(g, now, budget, moves.CutDeadline(now, budget), inputs); stale → MoveBackToReady. - admitted: re-check inputs against current bundle etags (appconfig.LoadMergedHost etag), overlay etag, release and launch report digest; mismatch → MoveBackToReady; else MoveCutPhase(admitted → stopping). - stopping: SetHold(host, "moving", id); fence (cfg.Fence); wait TimersDrained; cfg.Evidence (steps 1 to 4); !TreeStopped() → retry next pass until cut_deadline, then MoveRevert(g, "stop evidence budget") and hostclaim.Revert; TreeStopped() → the four steps are written into the op journal (Journal.Steps["evidence"], local; nothing is published yet, because the report's StepReplica, FinalTXID and digests do not exist until the checkpoint) → MoveCutPhase(stopping → stopped). - stopped: Checkpoint (final SyncAndWait); N = NewestL0; StepReplica OK with the database's sha256; Replication, NextDue from the row; timers snapshot → move-snapshots/<host>/<id>/timers.json (own bucket, create-only) and TimersPrefix (target, PutVersioned); files.plain via archive.PlainDigest; now the complete report: stopped-source.json create-only (K13: release.Store.Put(ctx, key, body, "") is create-only on both the S3 and the dir store, store.go:38, and loses with release.ErrCASConflict; on a conflict read the existing object, and equal Host, OpID, Role and FinalTXID is a replay and fine); MoveRecordSource; then hostclaim.Update(ExecToNone(id, box, stoppedKey, JournalEvidenceSource)) and SetJournal(cut_session = holder.session) in the same transition (a crash between any two of these replays the phase: the checkpoint is idempotent on a stopped database, the report is create-only, the CAS is conditional); MoveCutPhase(stopped → staged) is written only after the final round below succeeds, so the phase stopped repeats the mirror until it does: - the final round: MirrorPrefix into replica/ under the lease; Deferred > 0 → count in the journal (moveLedger.DeferredRounds), three → MoveRevert; else seal = round.Versions for every current key plus the files and timers versions, SrcCovered = (N == LocalPosition), FromDB = true when the stopped database file exists; SetJournal(seal) and SetMovePhase(moving, "" → sealed) in one claim CAS; MoveCutPhase(stopped → staged). - staged: the final archive from the stopped directory, every step replayable and all of them before the phase advances. Sealing is randomized (§8.2), so the archive's identity is fixed before any PUT: archive.Build + Seal into <JournalDir>/files.sealed and files.json (the manifest with the sealed key and both digests), then the identity (sealed digest, plain digest, sealed key, byte count) into the op journal (Journal.Steps["archive"]); a replay that finds the journal entry and the temp file re-puts those bytes under the same key files-<sealed>.sealed (create-only; ErrExists is its own earlier PUT); a replay that finds the entry but not the temp file checks the object by key (ListVersions of the exact key in the own bucket): present → reuse; absent → rebuild, reseal and overwrite the journal entry (nothing was published under the old identity). Then: PutIfAbsent to move-snapshots/<host>/<id>/files-<sealed>.sealed and the manifest beside it; copy both into FilesPrefix with PutVersioned; SetJournal(seal) with Files filled (same N); only then MoveCutPhase(staged → archived, stamp MoveSource.Files digests). The phase never says archived without the seal naming the archive, and the record's digests are the journaled identity's. - archived: precondition, re-read from the claim: seal.Files non-empty and seal.N == Source.FinalTXID (else the phase is a replay of staged's tail: redo the copy and the journal, never flip); MoveFlip(g, now, now + 15 min); then hostclaim.Update(Flip(id, targetBox, holder.stamp, holder.generation)); nudgeSerial. The claim half may be written by anyone who sees the record ahead (§7.5); the source tries once here and the console pass (Plan 5) finishes it. - every phase: now > cut_deadline → MoveRevert (phases stopping..archived) and hostclaim.Revert(id, cfg.Box, rec.Move.To, &stagingScope, newID) once exec is none (Task 2's five arguments; stagingScope as Plan 4 Task 6 builds it from the receipts ledger's LeaseUntil).
  • Each phase's local progress is in the op journal F12 (moves.Journal, Task 7's shape, written here first as internal/moves/journal.go).

Tests (movescut_test.go, harness from Plan 1 Task 14 extended with a fakeEvidence returning a configurable report, a fake versioned store with a failAfter(n) hook, and a claim written into the harness store): TestCutAdmitsOnlyFreshPassingReports, TestCutGoesBackToReadyWhenInputsMove, TestCutResumesStoppingFromEvidenceAlone (Review Focus 1: a fresh worker on the same store with the record at stopping calls Evidence and advances, with no in-memory state), TestStoppedReportIsCompleteBeforeExecToNone (the report object exists with StepReplica and FinalTXID before the claim's exec is ""; a crash injected after the checkpoint and before the report leaves exec with the source; the replay publishes one report), TestCutRevertsAfterThreeDeferredRounds (Review Focus 2), TestSealNamesEveryCurrentVersionAndN (witness wSealByVersion: a stale copy landing after the seal creates a version the seal does not name), TestCutArchiveIsCreateOnlyAndReused (codex 19, with the real randomized sealer: crashes after the journal entry, after the create-only PUT, after the first copy; every replay publishes the same files-<sealed> key; a replay with the temp file deleted and no object yet reseals under a new key and the record names the new one; a replay with the temp file deleted and the object present reuses it), TestArchivedNeverPrecedesTheSealedArchive (crashes injected after the create-only put, after the first PutVersioned, and after the second: in every case the record is still staged; the replay completes and the seal names both files/ versions before archived; a record hand-edited to archived with an empty seal.Files does not flip), TestFlipWritesRecordThenClaim (record landing with region uk; claim landing/checking, exec uk-box, source_stamp journalled; holder.generation unchanged), TestCutDeadlinePassedReverts.

  • [ ] Step 1 to 4 as above. Run: nice -n 10 go test ./internal/host ./internal/moves -run 'TestCut|TestSeal|TestFlip' -count=1 -p 1 → PASS.
  • [ ] Step 5: Commit: git commit -m "host: the journaled cut, requested to flipped, with stop evidence and the seal by version (§7.4, §7.5)" ...

---

Task 6: Holds everywhere else: reconcile, schedule, the hold view

Files:

  • Create: internal/host/moveshold.go, internal/host/moveshold_test.go
  • Modify: internal/provision/reconcile.go (ReconcileConfig.Hold, KeepParticipant; mint and removal), internal/provision/reconcile_test.go
  • Modify: internal/schedule/runner.go (skip held rows; Busy), internal/schedule/converge.go (Frozen), their tests
  • Modify: internal/host/schedule_wiring.go, internal/host/agent.go (wire the hooks)

Interfaces:

  • provision.ReconcileConfig.Hold func(rec Record) (status, op string, ok bool): when ok, a row minted for rec is Upserted with Status = status, HoldOp = op and never ready; an existing row under that op is left as is. KeepParticipant func(host string) bool: when true, the removal phase skips the row ("participant of a running operation"), and the orphan guard does not act on it.
  • schedule.Runner.Busy(host string) bool (the per-host claim, runner.go:276); the runner's Tick takes claim(host) first and then re-reads the route (r.Reg.Get) inside the claim: a held row (registry.IsHold(rt.Status)) releases the claim and continues with no bookkeeping. The order matters (the cut's drain): SetHold then "wait until !Busy(host)" is correct only if every tick that observes the row after the hold does so while holding the claim, so a tick paused between its route read and its claim cannot slip past the drain and advance attempts (runner.go:350). The route read before claim at runner.go:143 stays as a cheap pre-filter; the decision is the second read. schedule.Converger.Frozen func(host string) bool: convergeHost returns (false, nil) for a frozen host, and the prune's alive set includes frozen hosts so PruneTimers keeps their rows. One gate for all three writers (round-2 finding 21): schedule.HostGate (internal/schedule/hostgate.go, a per-host sync.RWMutex map) is shared by the Runner (its claim/release take the host's read lock for the delivery), the Converger (convergeHost holds the read lock across its check of Frozen and its writes) and the cut's drain (moveCfg.TimersDrained takes the write lock after SetHold, which waits for in-flight deliveries and convergences, then releases it; from then on the hold check inside the lock refuses both). The agent's overlapping converge loops (agent.go:1991, :2555) share the one HostGate the schedule wiring creates.
  • movesHoldView in the host package: built from each moves pass's record list plus hostclaim.Read per host with a move in flight; exposes hold(rec) (status, op, ok) (target after the flip: landing; source in reverting/restoring: restoring; target in a rollback's release: releasing; source during moving at or after stopping: moving), keep(host) bool (this box in op.participants of a move-kind op), frozen(host) bool (a move-kind op holds the claim), strict(host) bool (same).

Test TestReconcileHoldsParticipantRows (Review Focus 5): a record with region uk and a Hold returning ("landing", id, true) mints the row held on the uk box and never ready; the ie box with KeepParticipant(host) == true keeps its moving row though the record's region is uk; with KeepParticipant == false the row is removed as today. Test TestHeldRowsFreezeTimers (witness wTimerFreeze): a held row's due once-timer is not delivered and its attempts does not advance; a frozen host's declared schedule is neither upserted nor pruned by Converger.Pass; after the hold lifts both resume. Test TestDrainRaceCannotAdvanceBookkeeping: a tick reads the route (ready), pauses on a test hook before claim; the cut sets the hold and observes Busy == false; the tick resumes, claims, re-reads, sees the hold and releases; attempts is unchanged and nothing was delivered. Test TestConvergerCannotWriteAfterTheBarrier: a convergeHost paused after its Frozen check (inside the gate's read lock) blocks the cut's drain until it finishes; a converger that starts after the drain sees Frozen and writes nothing; the timers snapshot taken after the drain equals the table.

  • [ ] Steps 1 to 4. Run: nice -n 10 go test ./internal/provision ./internal/schedule ./internal/host -count=1 -p 1 → PASS.
  • [ ] Step 5: Commit: git commit -m "reconcile and schedule honour move holds: participant rows kept, held rows never ready, timers frozen and drained (§5.2, §9.4, §9.6, K9)" ...

---

Task 7: The operation journal (RenderHeld and MaterializeHeld moved to Plan 1 Task 7b)

Files:

  • Create: internal/appconfig/renderheld.go, internal/appconfig/renderheld_test.go, internal/release/materializeheld.go, internal/release/materializeheld_test.go, internal/moves/journal.go, internal/moves/journal_test.go

Interfaces:

  • appconfig.RenderHeld(ctx, s release.Store, priv *ecdh.PrivateKey, dataDir string, rt registry.Route, op string, box string) (HeldRender, error) with HeldRender{Path, SHA256 string; BundleETags map[string]string; OverlayETag string}: loads LoadMergedHost(ctx, s, acct, app, rt.Environment, rt.Host), renders through the same writer syncExecHost uses (extract the file-writing half into renderExecConfig(rt, m, key, priv, dataDir) (path string, changed bool, err error) and call it from both), writes the file whether or not it changed, and writes ConfigAck{Box, Host, Schema, Shape, At, InstanceConfigETag, MoveOp: op, FileSHA256, BundleETags} through WriteConfigAck (two additive fields on ConfigAck: MoveOp string \json:"moveop,omitempty"\`, FileSHA256 string \`json:"filesha256,omitempty"\`, BundleETags map[string]string \json:"bundle_etags,omitempty"\). SyncExec keeps skipping non-ready rows (execRouteBase`), so the two never race.
  • release.MaterializeHeld(ctx, s Store, optDir string, rt registry.Route, version string) (*Manifest, error): ensureStaged(ctx, s, optDir, RouteAccount(rt), rt.App, version, rt.Host) then points LiveLink(optDir, rt.Host) at VersionPath(...) the way adoptHost does (extract that symlink step into pointLive(optDir, host, target string) error if it is inline), with no restart and no bake-hold check: a held row runs exactly the pinned release.
  • moves.Journal{V int; Host, Op, Phase string; Steps map[string]string; Attempt int; UpdatedAt time.Time}; moves.JournalDir(dataDir, host, op) = <dataDir>/.moves/<host>/<op>/; moves.ReadJournal(dataDir, host, op) (*Journal, error) (absent → empty journal); (j *Journal) Write(dataDir string) error (temp file, fsync, rename, dir fsync); (j *Journal) Done(step string) bool; (j *Journal) Mark(step, value string).

Tests: RenderHeld writes the file for a landing row that SyncExec would skip and the ack carries move_op, the sha256 and the bundle etags; a second call with no change still writes an ack. MaterializeHeld stages the pinned version into a temp optDir and points live/<host> at it, never at the channel head. Journal round-trips and Done reflects Mark.

  • [ ] Steps 1 to 4. Run: nice -n 10 go test ./internal/appconfig ./internal/release ./internal/moves -count=1 -p 1 → PASS.
  • [ ] Step 5: Commit: git commit -m "appconfig.RenderHeld, release.MaterializeHeld, the box-local operation journal (§9.1, §9.2, F12)" ...

---

Task 8: Build the generation from the sealed replica

Files:

  • Create: internal/moves/ltxbuild.go, internal/moves/sealedclient.go, internal/moves/build.go, internal/moves/build_test.go

Interfaces:

  • moves.ReadDBHeader(path string) (pageSize uint32, pageCount uint32, err error) (SQLite header bytes 16-17 and 28-31).
  • moves.EncodeFull(db io.ReaderAt, pageSize, pageCount uint32, min, max ltx.TXID, timestamp int64, walOffset int64) (data []byte, crc64 uint64, err error): ltx.NewEncoder with Header{Version: ltx.Version, Flags: ltx.HeaderFlagNoChecksum, PageSize, Commit: pageCount, MinTXID: min, MaxTXID: max, Timestamp: timestamp, WALOffset: walOffset}, every page in order except SQLite's lock page, ltx.LockPgno(pageSize) (ltx.go:494; the encoder refuses it, encoder.go:219, and litestream skips it), while Commit keeps the full page count; Close; the CRC-64 is the trailer's FileChecksum (read it back with ltx.NewDecoder(bytes.NewReader(data)).Verify() then Trailer().FileChecksum).
  • moves.BuildIntent{V int; Op, Host string; N uint64; PageSize, Commit uint32; Timestamp int64}; moves.InstallManifest{V int; Op, Host string; N uint64; SnapshotKey, SnapshotVersion string; SnapshotCRC64 uint64; AnchorKey, AnchorVersion string; AnchorCRC64 uint64; IntegrityCheck string; DBSHA256 string; BuiltAt time.Time} (shared contract type, coordination with claim Plan 6 Task 6, feat/hostclaim-box bf63e250: claim Plan 6 defines this exact shape in internal/moves/install_contract.go, beside moves.ErrAttemptOver, with these snakecase json tags (final, from platform-1e): v, op, host, `snapshottxid (the Go field stays N), snapshotkey, snapshotversion, snapshotcrc64, anchorkey, anchorversion, anchorcrc64, integritycheck, dbsha256, builtat; spec rev 7.22 §7.6 names the same keys. installcontract.go **is on main at d649d8d4** (claim Plan 6 merged 2026-10-06): **this task imports moves.InstallManifest and does not redefine it**; install.json therefore carries those snake_case keys; TestInstallManifestKeys decodes a built install.json into map[string]any and asserts exactly that key set, so a drift between the contract and this plan fails here); moves.GenerationPrefix(location, op string) string (<location>g-<op>/, location with its trailing slash); moves.SnapshotKey(prefix string, n uint64) = prefix + "0009/" + ltx.FormatFilename(1, n); moves.AnchorKey(prefix, n) = prefix + "0000/" + ltx.FormatFilename(n, n); BuildIntentKey(prefix) = prefix+"build-intent.json", InstallManifestKey(prefix) = prefix+"install.json"`.
  • moves.NewSealedClient(store lease.VersionedStore, bucket, replicaPrefix string, seal Seal) *SealedClient implementing litestream.ReplicaClient: Type() "sealed", Init no-op, LTXFiles(ctx, level, seek, _) returns ltx.NewFileInfoSliceIterator over the sealed objects at that level parsed from their keys (ltx.ParseFilename), OpenLTXFile opens exactly the sealed version by OpenVersion(ctx, bucket, key, versionID) (ranged through VersionRangeOpener when offset/size are set), WriteLTXFile, DeleteLTXFiles, DeleteAll return ErrSealedReadOnly, SetLogger stores it.
  • moves.BuildGeneration(ctx, in BuildInputs) (InstallManifest, error) with BuildInputs{Store lease.VersionedStore; Bucket string; Source []SealedObject (the pinned version set: for a move the seal's replica/ objects, for region HA's relocation the versions a genselect plan or a current listing pins, L16); N uint64; GenerationPrefix string; Op, Host string; WorkDir string; Now func() time.Time; Verify func(dbPath string) error} (SealedClient is built over Source, so nothing in the routine names a staging lane): (1) read or write the intent (PutIfAbsent; on lease.ErrExists read it and use its timestamp, page size, commit); (2) restore into a fresh scratch file: remove <WorkDir>/<host>.db, -wal, -shm and -txid first (litestream's Restore refuses an existing output path, replica.go:601; the file is reproducible from the sealed versions, so a replay after any crash starts clean), then litestream.NewReplicaWithClient(nil, sealedClient).Restore(ctx, opt) with opt.OutputPath = <WorkDir>/<host>.db, opt.TXID = ltx.TXID(seal.N); refuse unless the restored position is N (litestream writes <db>-txid? Only in follow mode; instead compare the plan's final MaxTXID through litestream.CalcRestorePlan(ctx, client, ltx.TXID(N), time.Time{}, logger) whose last file's MaxTXID must be N, and verify every planned file with ltx.NewDecoder(...).Verify() before the restore); Verify(dbPath) = lease.CheckDB (integrity_check); (3) EncodeFull snapshot [1, N] and PutIfAbsent(SnapshotKey); on ErrExists, OpenVersion the current one and require equal bytes (identical by construction from the intent; differing bytes refuse with ErrBuildDiverged); (4) the anchor [N, N] with WALOffset = litestream.WALHeaderSize, same rule; (5) sha256 of the database file; (6) InstallManifest via PutIfAbsent (on exists, read and compare N, keys, versions).

Tests (build_test.go, using internal/lease/leasetest's local versioned store if it offers one, else a small in-memory lease.VersionedStore fake in moves tests): TestSealedClientServesOnlySealedVersions (a newer version of a sealed key exists; OpenLTXFile returns the sealed bytes; an unsealed key is absent); TestBuildGenerationReachesNAndWritesSnapshotAndAnchor (drive litestream over a real sqlite file through the file replica client as litestreamfacts does to produce a replica, seal its current versions, build, then assert: snapshot header MinTXID 1, MaxTXID N, Flags NoChecksum, anchor [N, N] with WALOffset == WALHeaderSize, both Verify(), manifest's DBSHA256 equals the file's, IntegrityCheck == "ok"); TestBuildReplayReusesIdenticalSnapshot (Review Focus 3, witness wBuildCrashes: run the build with a Store that fails after the snapshot PUT, in the same WorkDir, with the half-restored database file left on disk; run again on the same store and directory; the snapshot key has one version, the manifest exists, bytes identical; a second variant fails during the restore itself, leaving a partial output file, and the replay still completes); TestBuildRefusesAGapOrAShortRestore (drop a sealed middle file → ErrBuildIncomplete, no snapshot PUT); TestEncodeFullSkipsTheLockPage (a sparse database file extended past 1 GiB with Truncate, page size 4096: the encoder succeeds, the file has pageCount - 1 pages, Verify() passes, and litestream's own restore of it produces a database whose PRAGMA page_count equals the original).

  • [ ] Steps 1 to 4. Run: nice -n 10 go test ./internal/moves -run 'TestSealedClient|TestBuild' -count=1 → PASS.
  • [ ] Step 5: Commit: git commit -m "moves: build the generation from the sealed replica: planner restore, snapshot, anchor, intent and manifest (§5.1 steps 2 and 7, §7.6 building)" ...

---

Task 9: Park queue helpers for move retirements (no new kinds)

Files:

  • Modify: internal/park/remote.go (Engine.Parked only), internal/park/remote_test.go
  • Modify: internal/hostclaim/move.go (QueueLocationPark, QueueStagingPark, QueueLocalPark, QueueResidueParks)

Interfaces (index D6: the engine's existing kinds and destination):

  • A staging lane is a park-replica scoped to moves/<host>/<op>; a whole location to retire in a region where it is no longer authoritative (the source after the commit, a rolled-back target at cleaning) is a park-replica scoped to instances/<host>/<inc>; a landing's earlier-stay residue in the target region is one park-generation per residue generation (hostclaim.RootGeneration for the origin). Claim.Validate (claim.go:266-288) accepts all three unchanged; the archive lands under parked/<incarnation>/<host>/<retirement>/; F3 reads the manifest's Op.
  • (e Engine) Parked(ctx, w Work) (bool, error): the manifest exists and every version in its deletion set is gone (what the landing's parking phase waits for, §7.6; acknowledgement still waits for the horizon in Run).
  • hostclaim.QueueLocationPark(opID, region, box string, scope ParkScope, newID func() string) func(*Claim) error (appends {Kind: "park-replica", Region, Owner: "region:"+region, Park: &scope, Retirement: newID(), ID: newID()} and, when box != "", a park-local for the box; scope validated with Validate(host); idempotent per (opID, region) and (opID, box)); hostclaim.QueueStagingPark(opID, region string, scope ParkScope, newID func() string) func(*Claim) error (a park-replica whose scope location is moves/<host>/<op>); hostclaim.QueueLocalPark(opID, box string, newID func() string) func(*Claim) error (a park-local alone, for the landing's local phase: park.Local.RetireArtifacts runs only against a persisted park-local chore of the claim, local.go:62, and treats an unknown id as already completed, so the chore must exist in the claim before the call); hostclaim.QueueResidueParks(opID, region string, location ParkScope, generations []string, newID func() string) func(*Claim) error (one park-generation per listed generation, each {Kind: "park-generation", Region, Generation: g, Owner: "region:"+region, Park: &scope}, deduplicated by (opID, region, g); the landing's parking phase feeds it park.Decide's candidates grouped by generation).

Tests: TestLocationParkIsAParkReplicaTheStoreAccepts (a real hostclaim.Update round trip through release.NewDirStore: Validate passes, the obligation reads back); TestStagingParkScopeIsTheLane; TestResidueParksArePerGeneration (two residue generations and the root → three chores, @root named); TestParkedReportsManifestAndDeletions; TestEngineArchivesAMoveRetirementUnderParked (the destination is parked/<inc>/h/<ret>/instances/h/<inc>/0000/x.ltx@v1 and the manifest's Op is the move id).

  • [ ] Steps 1 to 4. Run: nice -n 10 go test ./internal/park ./internal/hostclaim -count=1 -p 1 → PASS.
  • [ ] Step 5: Commit: git commit -m "park: Parked; hostclaim: park queue helpers for move retirements on the existing kinds (§6.4, D6)" ...

---

Task 10: The landing, checking to restored

Files:

  • Create: internal/host/movesland.go, internal/host/movesland_test.go
  • Modify: internal/host/movespass.go (the pass also runs landingPass for records whose Move.To == cfg.Region and Move.State == landing), internal/host/agent.go (wiring: hostguard.Guard{DataDir, LockDir, Claims: hostguard.StoreReader{Store: store}}, park.Engine for the own bucket, park.Local, RenderHeld, MaterializeHeld, dbverify)

Interfaces:

  • moveCfg gains: Guard hostguard.Guard, Park park.Engine (own bucket and region; View, Authorize from Plan 5's providers until then a view that lists the location and authorizes only the landing's own op), Local park.Local, RenderHeld func(ctx, rt registry.Route, op string) (appconfig.HeldRender, error), MaterializeHeld func(ctx, rt registry.Route, version string) error, ReplaceTimers func(host string, rows []registry.Timer) error, SetReplication func(host, class string) error, SetNextDue func(host string, due int64) error, RemoveVerifyEntry func(ctx, host string) error (loads dbverify.Load, deletes Hosts[host], Publish), OwnLease func(ctx, prefix string) (*lease.Held, error), SecretsPriv *ecdh.PrivateKey.
  • landingPass(ctx, s, w, rec) error per phase (§7.6), each phase first MoveLandingPhase(g, from, to) then its side effects, journaled in moves.Journal: - every landing phase begins with syncClaimPhase(ctx, rec, obs): the record's landing_phase is the decision and the claim's op.phase follows it (§7.5); when the claim is behind (a crash after the record CAS and before the claim CAS), the function walks landingOrder from the claim's phase to the record's, one SetMovePhase(id, KindLanding, from, to) per step, before the phase's work; when the claim is ahead (a crash after the claim CAS of a later step, impossible by construction since the claim never leads, but checked) it is left alone and logged; equal is the common case. Then the phase does its work, writes the record CAS (MoveLandingPhase) and mirrors it (SetMovePhase). The console pass (Plan 5 Task 7) runs the same catch-up for a landing it finds behind. AdmitWriter checks the phase and never advances it (generation.go:35), so the probe's admission observes probing only through this mirroring. - checking: read stopped-source.json (must be Complete()); claim: RequireMoveOp(id, KindLanding, "checking"), Exec == cfg.Box; hostclaim.Update(Touch(id, nil, []string{cfg.Box})) if absent (Plan 1 Task 14 added it at the first staging write; this is the idempotent K12 re-add); move.json (own bucket, readable here) incarnation == rec.Move.Incarnation == holder.incarnation; before activating anything, read the guard record (hostguard.Capture(dataDir, host) returns (Captured, error); ErrNoRecord means no earlier stay) and the claim's Inventory[cfg.Box]: an earlier stay's paths and their stamp S' are what local will retire, so they are noted in the op journal (Journal.Steps["previous"] = S') before cfg.Guard.Activate(ctx, host, cfg.Box, nil) sets active_stamp = holder.stamp (the flip's F); hostclaim.Update(QueueLocalPark(id, cfg.Box, newID)) so local has a persisted chore. No lease is taken here (round-2 finding 9): park.Engine.Run acquires the location's lease itself (remote.go:404), so the landing acquires OwnLease on holder.location only after parking, at the start of building, and keeps it in the worker's per-host map through published. - parking: list holder.location in the own bucket (Store.ListVersions); nothing there → done; else group the residue by generation as park.Decide does (every generation other than the one the landing will build; the origin as @root) and hostclaim.Update(QueueResidueParks(id, cfg.Region, scope{Location: TrimSuffix(location, "/"), Incarnation, EvidenceAt: rec.Move.FlippedAt, RequestLimit: 120 s}, generations, newID)) once (journal step); cfg.Park.Run(ctx, Work{Host, ID, Op}) per chore (ErrPending expected), then cfg.Park.Parked must be true for each before advancing. Per-generation chores are what let the acknowledgement finish after the probe makes the new generation live in this location (D6; a whole-location park would meet remote.go:346's live-set refusal forever); the acknowledgement half is C4's runner. - building: moves.BuildGeneration with the seal from the claim journal (DecodeSeal(obs.Claim.Op.Journal[JournalSeal])), ReplicaPrefix in the own bucket, GenerationPrefix(holder.location, id), WorkDir = JournalDir. - published: journal only (no generation.json, R10-3). - local, in this order (round-2 finding 8; park/local.go:202 requires paths relative to DataDir, tags them with the captured stamp and refuses a path already tagged with another stamp): (1) captured, err := hostguard.Capture(dataDir, host) (now F); (2) when the journal's previous names S' and Inventory[cfg.Box] has paths tagged S': cfg.Local.RetireArtifacts(ctx, host, captured, <the park-local chore id>, id, S') parks every artifact tagged S' (the old <host>.db, <host>.db.gen, <host>/, <host>.env) into .parked/<inc>/<host>/<retirement>/artifacts/, which clears their inventory entries; when there is no S', AcknowledgeEmpty; the test asserts on the filesystem that the old files are gone from the live paths and present under .parked/, not on the call returning nil; (3) archive.Open + Extract the newest archive from FilesPrefix (by the seal's files version) into <DataDir>/<host>/ and <DataDir>/<host>.env; (4) cfg.Local.RecordInventory(ctx, host, captured, "flat", []string{host + ".db", host + ".db.gen", host, host + ".env"}, check) registers the new installation under F; (5) ReplaceTimers(host, snapshot.Rows) from the sealed timers version (through SelectedTimers.ReplaceTimers once C3 lands; registry.ReplaceTimers directly behind the switch until then); SetReplication(host, Source.Replication), SetNextDue(host, Source.NextDue); RemoveVerifyEntry; (6) the prepared files manifest of spec rev 7.21 §7.6 (L17, R18-1; claim rev 6.30 73b1b9dc) under the installation's authority, never the session-bound publisher (no target session exists yet): op.exec writes the installed files, <host>.env and the wake state as create-only content objects under <location>files/<P>/objects/<seq>-<sha256> and then the one create-only manifest <location>files/<P>/manifest.json, bound to {location, P, op id, installation stamp}, where P is the installation's prepared destination generation, "g-" + src.GenerationOp (the move id here; the rollback id for Plan 4's dirty return, installSource.GenerationOp), never holder.generation, which still names the source's pre-move generation until the probe's admission (spec rev 7.22 §7.6, R19-1: a manifest bound to or filed under the source's generation could never be the new generation's baseline, and the first landing admission would block); TestLocalBindsThePreparedManifestToTheNewGeneration asserts the manifest key is files/g-<op>/manifest.json and its binding names g-<op> while the claim's holder.generation is still the source's; naming exact versions, at sequence 0, never replaced and retired with its generation, through C8's prepared-manifest build (region HA :809; files.BuildPrepared(ctx, store, location, generation, opID string, stamp uint64, dir, envPath string, wake WakeState) (manifestVersion string, err error) or its real name, called with generation = P), journalled like the install manifest; a replay reuses the objects by digest and the manifest by key; the session-bound publisher starts at the probe's admission (Plan 3 Task 5), takes this manifest by exact version as its baseline and publishes from sequence 1 (a journal step, skipped with a log line until C8 exists); (7) fsync the directory; write .files-gen {"op": id, "incarnation": inc} last. - configured: RenderHeld; MaterializeHeld(rt, rec.Move.Inputs.Release); the ack's BundleETags must equal rec.Move.Inputs.BundleETags and its overlay etag Inputs.OverlayETag, else MoveRollBack (Plan 4 Task 1's transition; until it exists, write Reason and stop: markForRollback(rec, "configuration changed during the move") is the one call site Plan 4 replaces). - restored: copy the built database to the row's DBPath (flat), sha256 == manifest's, lease.CheckDB, write <dbPath>.gen {"op": id, "incarnation": inc, "location": holder.location}, MoveLandingPhase(configured → restored, stamp Landing.InstalledDBSHA256). - landing_deadline passed before restored → the rollback marker as above.
  • armMovesPass wiring in agent.go for the new hooks; the hold view (Task 6) is updated from both passes.

Tests (movesland_test.go, the harness with cfg.Region = "uk", a flipped record and a landing/checking claim with exec = uk-box, the sealed replica staged in the harness's versioned store from a real small sqlite replica built as in Task 8's test, and a fake park.Leases that refuses a second acquisition of a held location, so a lease the landing still held would fail the test): TestLandingCheckingRequiresClaimEvidenceAndTouch, TestLandingParkingWaitsForTheManifest (an earlier stay's objects under holder.location in two generations are archived to parked/<inc>/<host>/<ret>/... before building, by two park-generation chores; after the probe publishes the new generation, Engine.Run on those chores past the horizon acknowledges rather than refusing), TestLandingPhaseCatchUp (round-2 finding 11: a crash after the record's parking and before the claim's; the restarted worker advances the claim to parking and continues; a claim left at checking with the record at restored is walked forward step by step), TestLandingInventoryOrder (an earlier stay's inventory tagged S': local parks it, then registers the new paths under F; registering first is refused by the real RecordInventory), TestLandingBuildsInstallsAndRestores (end to end through restored: .files-gen last, timers replaced with registrations preserved, <dbPath>.gen written, installed_db_sha256 recorded), TestLandingMarkersAreWrittenLast (witness wMarkersLast: a crash injected before the final sub-step of local leaves no .files-gen; the replay finishes it), TestLandingConfiguredPinsInputs (Review Focus 4), TestLandingDeadlineMarksRollback, TestLandingReplaysArePhaseIdempotent (every phase run twice yields one object per key).

  • [ ] Steps 1 to 4. Run: nice -n 10 go test ./internal/host -run 'TestLanding' -count=1 → PASS.
  • [ ] Step 5: Commit: git commit -m "host: the landing from checking to restored: park, build, local install, held render and materialization (§7.6)" ...

---

Task 11: Gate, copy list and hand-off

  • [ ] Full gate serial and niced; cross-compile darwin, windows, linux-arm64 (stopreconstruct_other.go, fds_other.go paths).
  • [ ] Add this plan's strings to docs/superpowers/plans/2026-10-06-moves-phase1-copy.md under "Plan 2": "configuration changed during the move" (reason), "stop evidence budget" (reason), M-C20's text for a stray process alert.
  • [ ] Hand-off note for Plan 3 in the commit message: restored is the last phase this plan writes; probing begins with ProbeWake.

Self-review notes

Spec coverage: §7.4 every phase and its recovery exit (Task 5), §7.5 record then claim (Tasks 1, 2, 5), §5.3 steps 1 to 4 (Task 3) and step 5 (Task 5), §5.1 steps 1, 2, 7 and the build's two durable states (Task 8), §5.2 holds (Plan 1 Task 13, Task 6), §9.6 (Task 6), §9.4 drained, frozen, replaced (Tasks 6, 10), §9.1, §9.2 (Task 7), §9.3 strict spawns and the deleted fallback (Task 4), §6.4 destinations (Task 9), §7.6 through restored (Task 10), §7.13 rows for cutting, flipped, landing: checking to restored (Tasks 5, 10 replay tests). The start check of §5.1 and ProbeWake are Plan 3's; reverting's phases are Plan 4's (this plan writes the reverting record and claim transitions only).