carlos / platform Public
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/procor flocks is behind//go:build linuxwith a!linuxstub (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=1and 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.idandmove.incarnationand revalidates the claim's op id, kind and phase first (K5). - The required final TXID
Nis theMaxTXIDof the source's newest level-0 file after the final checkpoint andSyncAndWait(§5.1 step 1); the build must reach exactlyNand passPRAGMA integrity_check. - Snapshot and anchor are written as the pinned writers write theirs:
HeaderFlagNoChecksum, checksums zero, trailer CRC-64 present; the anchor'sWALOffsetislitestream.WALHeaderSize(facts R5-1c, R5-2). - A build is two durable states, intent then completed manifest;
generation.jsonis never written by the landing (§7.6published; R10-3). - One absolute deadline:
cut_deadline = cut_at + cut_budget − open_budget; every phase before the commit checks it (§7.4). promoteStagedReplicaand 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
- A source whose agent restarts between
stoppingandstoppedmust 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). - A final mirror round that defers an object three times must revert, never flip (§7.4
stagedfailure). Test in Task 5 (TestCutRevertsAfterThreeDeferredRounds). - A landing whose
buildingcrashed 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; witnesswBuildCrashes). Test in Task 8 (TestBuildReplayReusesIdenticalSnapshot). - A
configuredphase whose rendered bundle etags differ frommove.inputsmust mark the move for rollback with "configuration changed during the move", not install (§7.6; J2-R2-5). Test in Task 9 (TestLandingConfiguredPinsInputs). - Reconcile on the target must never mint the landing row
ready, and reconcile on the source must never remove themovingrow 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:
MoveGuardandMoveGuardedCutfrom 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;MoveGuardedCutis theready → cutting/requestedentry the source uses forcut == "auto"and the console's cut door uses too). - Produces:
MoveCutPhase(g MoveGuard, from, to string, stamp func(*MoveState)) func(*Record) error(state must becutting;CutPhase == from);MoveAdmit(g, now time.Time, budgetS int, deadline time.Time, inputs MoveInputs)(requested → admitted, stampsCutAt,CutBudgetS,CutDeadline,Inputs);MoveBackToReady(g)(cutting/requested|admitted → ready, clears the cut fields);MoveRecordSource(g, src MoveSource)(setsSource, legal instoppedonly);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))(statelanding);MoveRevert(g, reason string)(fromcuttingwithCutPhaseinstopping..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 underjournalKey);Flip(opID, targetBox string, sourceStamp uint64, srcGen string) func(*Claim) error(K4: requiresKindMoving, phasesealed,Exec == "",evidence_sourcejournalled; setsKind = KindLanding,Phase = "checking",Exec = targetBox,holder.stamp = c.Gen + 1, the gen this CAS produces:hostclaim.Updatesetsgen = observed + 1aftertrruns (store.go, andcreate.go:94writes the serving stamp the same way); journalssource_stampandsrc_gen);Revert(opID, sourceBox, targetRegion string, stagingScope *ParkScope, newID func() string) func(*Claim) error(fromKindMoving,Exec == "",evidence_sourcejournalled →KindReverting,Exec = sourceBox,rev_phase = "evidence"unless set, plus the lanepark-replicaobligation 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 ininternal/stopevidence, notinternal/activator:stopevidencealready importsactivator(evidence.go:10, for*activator.StopEvidence), so the activator cannot import it back;stopevidencemay importmoves(which imports neither). The cgroup manager is the exec backend'sExecBackend.Cgroups(backend_exec.go), handed in explicitly by the host package at arm time (eb.Cgroups);activator.Servicehas 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 idsStepTree = "tree",StepStray = "stray",StepFDs = "fds",StepLease = "lease",StepReplica = "replica";(r StoppedReport) Complete() bool(every step OK,StepReplicaincluded);(r StoppedReport) TreeStopped() bool(steps 1 to 4 OK: whatstoppingneeds before the checkpoint);lease.LockState(ctx, bucket, prefix, region string) (held bool, owner string, expires time.Time, err error)(readslock.jsonas 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 setsStepReplica); on!linux, returnsErrUnsupported.
- [ ] 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(spawnInCgrouppath at:210,Restoreat:290-320, deletepromoteStagedReplicaandMovesPrefix/MoveReceiptsKeyif unused after Plan 1),internal/activator/backend_exec_test.go,internal/host/agent.go:363-369(theeb.Mirrorarming becomes unnecessary; keepMirroronly if another caller uses it)
Interfaces:
- Produces:
ExecBackend.Strict func(host string) bool(when true, the spawn usesCgroups.PrepareStrict(host, limit)thenPrepared.Start(cmd), and a failure refuses the spawn withErrStrictRequired);(b *ExecBackend) NextSpawnStrict(host string) moves.Check(S2: OK whenStrict(host)would be honoured: cgroup v2 delegated andPrepareStrictsupported). - 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(withb.Phase1true, a staged object undermoves/<host>/and an empty own prefix →Restorereturnsfresh = trueand the staged object is untouched; withb.Phase1false 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 frommoves.Enabled);RestoreskipspromoteStagedReplicawhenPhase1; 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(MoveCuttingarm callsrunCut;moveCfggainsEvidence,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'sevidence_at, Plan 5).moveCfg.Evidence func(ctx, host string, dbPaths []string) (moves.StoppedReport, error)(wired tosvc.ReconstructStopEvidencewithleaseFree=lease.LockStateon the own prefix);moveCfg.TimersDrained func(host string) bool(the runner'sBusy, Task 6);moveCfg.NewestL0 func(ctx, bucket, prefix, region string) (uint64, error)(lists the own replica's level-0 files throughlease.VersionedStoreand returns the highestMaxTXID; the required final TXIDN);moveCfg.LocalPosition func(dbPath string) (uint64, error)(the local database's TXID from litestream's-txidsidecar or its WAL;src_coveredisN == LocalPositionwith no pending write).runCut(ctx, s, w, rec, tgt) error: the phase loop of §7.4: -requested: fresh target and source reports (moves.Readywith the last round's delta) andmove.inputsto 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.LoadMergedHostetag), overlay etag, release and launch report digest; mismatch →MoveBackToReady; elseMoveCutPhase(admitted → stopping). -stopping:SetHold(host, "moving", id); fence (cfg.Fence); waitTimersDrained;cfg.Evidence(steps 1 to 4);!TreeStopped()→ retry next pass untilcut_deadline, thenMoveRevert(g, "stop evidence budget")andhostclaim.Revert;TreeStopped()→ the four steps are written into the op journal (Journal.Steps["evidence"], local; nothing is published yet, because the report'sStepReplica,FinalTXIDand digests do not exist until the checkpoint) →MoveCutPhase(stopping → stopped). -stopped:Checkpoint(finalSyncAndWait);N = NewestL0;StepReplicaOK with the database's sha256;Replication,NextDuefrom the row; timers snapshot →move-snapshots/<host>/<id>/timers.json(own bucket, create-only) andTimersPrefix(target,PutVersioned);files.plainviaarchive.PlainDigest; now the complete report:stopped-source.jsoncreate-only (K13:release.Store.Put(ctx, key, body, "")is create-only on both the S3 and the dir store,store.go:38, and loses withrelease.ErrCASConflict; on a conflict read the existing object, and equalHost,OpID,RoleandFinalTXIDis a replay and fine);MoveRecordSource; thenhostclaim.Update(ExecToNone(id, box, stoppedKey, JournalEvidenceSource))andSetJournal(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 phasestoppedrepeats the mirror until it does: - the final round:MirrorPrefixintoreplica/under the lease;Deferred > 0→ count in the journal (moveLedger.DeferredRounds), three →MoveRevert; else seal =round.Versionsfor every current key plus the files and timers versions,SrcCovered = (N == LocalPosition),FromDB = truewhen the stopped database file exists;SetJournal(seal)andSetMovePhase(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+Sealinto<JournalDir>/files.sealedandfiles.json(the manifest with the sealed key and both digests), then the identity (sealeddigest,plaindigest, 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 keyfiles-<sealed>.sealed(create-only;ErrExistsis its own earlier PUT); a replay that finds the entry but not the temp file checks the object by key (ListVersionsof 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:PutIfAbsenttomove-snapshots/<host>/<id>/files-<sealed>.sealedand the manifest beside it; copy both intoFilesPrefixwithPutVersioned;SetJournal(seal)withFilesfilled (sameN); only thenMoveCutPhase(staged → archived, stamp MoveSource.Files digests). The phase never saysarchivedwithout the seal naming the archive, and the record's digests are the journaled identity's. -archived: precondition, re-read from the claim:seal.Filesnon-empty andseal.N == Source.FinalTXID(else the phase is a replay ofstaged's tail: redo the copy and the journal, never flip);MoveFlip(g, now, now + 15 min); thenhostclaim.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(phasesstopping..archived) andhostclaim.Revert(id, cfg.Box, rec.Move.To, &stagingScope, newID)once exec is none (Task 2's five arguments;stagingScopeas Plan 4 Task 6 builds it from the receipts ledger'sLeaseUntil).- Each phase's local progress is in the op journal F12 (
moves.Journal, Task 7's shape, written here first asinternal/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 forrecisUpserted withStatus = status, HoldOp = opand neverready; 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'sTicktakesclaim(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):SetHoldthen "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 advanceattempts(runner.go:350). The route read beforeclaimatrunner.go:143stays as a cheap pre-filter; the decision is the second read.schedule.Converger.Frozen func(host string) bool:convergeHostreturns(false, nil)for a frozen host, and the prune'saliveset includes frozen hosts soPruneTimerskeeps their rows. One gate for all three writers (round-2 finding 21):schedule.HostGate(internal/schedule/hostgate.go, a per-hostsync.RWMutexmap) is shared by theRunner(itsclaim/releasetake the host's read lock for the delivery), theConverger(convergeHostholds the read lock across its check ofFrozenand its writes) and the cut's drain (moveCfg.TimersDrainedtakes the write lock afterSetHold, 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 oneHostGatethe schedule wiring creates.movesHoldViewin the host package: built from each moves pass's record list plushostclaim.Readper host with a move in flight; exposeshold(rec) (status, op, ok)(target after the flip:landing; source inreverting/restoring:restoring; target in a rollback's release:releasing; source duringmovingat or afterstopping:moving),keep(host) bool(this box inop.participantsof 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)withHeldRender{Path, SHA256 string; BundleETags map[string]string; OverlayETag string}: loadsLoadMergedHost(ctx, s, acct, app, rt.Environment, rt.Host), renders through the same writersyncExecHostuses (extract the file-writing half intorenderExecConfig(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 writesConfigAck{Box, Host, Schema, Shape, At, InstanceConfigETag, MoveOp: op, FileSHA256, BundleETags}throughWriteConfigAck(two additive fields onConfigAck:MoveOp string \json:"moveop,omitempty"\`,FileSHA256 string \`json:"filesha256,omitempty"\`,BundleETags map[string]string \json:"bundle_etags,omitempty"\).SyncExeckeeps 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 pointsLiveLink(optDir, rt.Host)atVersionPath(...)the wayadoptHostdoes (extract that symlink step intopointLive(optDir, host, target string) errorif 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.NewEncoderwithHeader{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), whileCommitkeeps the full page count;Close; the CRC-64 is the trailer'sFileChecksum(read it back withltx.NewDecoder(bytes.NewReader(data)).Verify()thenTrailer().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-boxbf63e250: claim Plan 6 defines this exact shape ininternal/moves/install_contract.go, besidemoves.ErrAttemptOver, with these snakecase json tags (final, from platform-1e):v,op,host, `snapshottxid(the Go field staysN),snapshotkey,snapshotversion,snapshotcrc64,anchorkey,anchorversion,anchorcrc64,integritycheck,dbsha256,builtat; spec rev 7.22 §7.6 names the same keys.installcontract.go**is onmainat d649d8d4** (claim Plan 6 merged 2026-10-06): **this task importsmoves.InstallManifestand does not redefine it**;install.jsontherefore carries those snake_case keys;TestInstallManifestKeysdecodes a builtinstall.jsonintomap[string]anyand 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) *SealedClientimplementinglitestream.ReplicaClient:Type() "sealed",Initno-op,LTXFiles(ctx, level, seek, _)returnsltx.NewFileInfoSliceIteratorover the sealed objects at that level parsed from their keys (ltx.ParseFilename),OpenLTXFileopens exactly the sealed version byOpenVersion(ctx, bucket, key, versionID)(ranged throughVersionRangeOpenerwhen offset/size are set),WriteLTXFile,DeleteLTXFiles,DeleteAllreturnErrSealedReadOnly,SetLoggerstores it.moves.BuildGeneration(ctx, in BuildInputs) (InstallManifest, error)withBuildInputs{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 agenselectplan or a current listing pins, L16); N uint64; GenerationPrefix string; Op, Host string; WorkDir string; Now func() time.Time; Verify func(dbPath string) error}(SealedClientis built overSource, so nothing in the routine names a staging lane): (1) read or write the intent (PutIfAbsent; onlease.ErrExistsread it and use its timestamp, page size, commit); (2) restore into a fresh scratch file: remove<WorkDir>/<host>.db,-wal,-shmand-txidfirst (litestream'sRestorerefuses an existing output path,replica.go:601; the file is reproducible from the sealed versions, so a replay after any crash starts clean), thenlitestream.NewReplicaWithClient(nil, sealedClient).Restore(ctx, opt)withopt.OutputPath = <WorkDir>/<host>.db,opt.TXID = ltx.TXID(seal.N); refuse unless the restored position isN(litestream writes<db>-txid? Only in follow mode; instead compare the plan's finalMaxTXIDthroughlitestream.CalcRestorePlan(ctx, client, ltx.TXID(N), time.Time{}, logger)whose last file'sMaxTXIDmust beN, and verify every planned file withltx.NewDecoder(...).Verify()before the restore);Verify(dbPath)=lease.CheckDB(integrity_check); (3)EncodeFullsnapshot[1, N]andPutIfAbsent(SnapshotKey); onErrExists,OpenVersionthe current one and require equal bytes (identical by construction from the intent; differing bytes refuse withErrBuildDiverged); (4) the anchor[N, N]withWALOffset = litestream.WALHeaderSize, same rule; (5) sha256 of the database file; (6)InstallManifestviaPutIfAbsent(on exists, read and compareN, 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.Parkedonly),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-replicascoped tomoves/<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 atcleaning) is apark-replicascoped toinstances/<host>/<inc>; a landing's earlier-stay residue in the target region is onepark-generationper residue generation (hostclaim.RootGenerationfor the origin).Claim.Validate(claim.go:266-288) accepts all three unchanged; the archive lands underparked/<incarnation>/<host>/<retirement>/; F3 reads the manifest'sOp. (e Engine) Parked(ctx, w Work) (bool, error): the manifest exists and every version in its deletion set is gone (what the landing'sparkingphase waits for, §7.6; acknowledgement still waits for the horizon inRun).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, whenbox != "", apark-localfor the box; scope validated withValidate(host); idempotent per(opID, region)and(opID, box));hostclaim.QueueStagingPark(opID, region string, scope ParkScope, newID func() string) func(*Claim) error(apark-replicawhose scope location ismoves/<host>/<op>);hostclaim.QueueLocalPark(opID, box string, newID func() string) func(*Claim) error(apark-localalone, for the landing'slocalphase:park.Local.RetireArtifactsruns only against a persistedpark-localchore 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(onepark-generationper listed generation, each{Kind: "park-generation", Region, Generation: g, Owner: "region:"+region, Park: &scope}, deduplicated by(opID, region, g); the landing'sparkingphase feeds itpark.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 runslandingPassfor records whoseMove.To == cfg.RegionandMove.State == landing),internal/host/agent.go(wiring:hostguard.Guard{DataDir, LockDir, Claims: hostguard.StoreReader{Store: store}},park.Enginefor the own bucket,park.Local,RenderHeld,MaterializeHeld,dbverify)
Interfaces:
moveCfggains:Guard hostguard.Guard,Park park.Engine(own bucket and region;View,Authorizefrom 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(loadsdbverify.Load, deletesHosts[host],Publish),OwnLease func(ctx, prefix string) (*lease.Held, error),SecretsPriv *ecdh.PrivateKey.landingPass(ctx, s, w, rec) errorper phase (§7.6), each phase firstMoveLandingPhase(g, from, to)then its side effects, journaled inmoves.Journal: - every landing phase begins withsyncClaimPhase(ctx, rec, obs): the record'slanding_phaseis the decision and the claim'sop.phasefollows it (§7.5); when the claim is behind (a crash after the record CAS and before the claim CAS), the function walkslandingOrderfrom the claim's phase to the record's, oneSetMovePhase(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.AdmitWriterchecks the phase and never advances it (generation.go:35), so the probe's admission observesprobingonly through this mirroring. -checking: readstopped-source.json(must beComplete()); 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);ErrNoRecordmeans no earlier stay) and the claim'sInventory[cfg.Box]: an earlier stay's paths and their stampS'are whatlocalwill retire, so they are noted in the op journal (Journal.Steps["previous"] = S') beforecfg.Guard.Activate(ctx, host, cfg.Box, nil)setsactive_stamp = holder.stamp(the flip'sF);hostclaim.Update(QueueLocalPark(id, cfg.Box, newID))solocalhas a persisted chore. No lease is taken here (round-2 finding 9):park.Engine.Runacquires the location's lease itself (remote.go:404), so the landing acquiresOwnLeaseonholder.locationonly afterparking, at the start ofbuilding, and keeps it in the worker's per-host map throughpublished. -parking: listholder.locationin the own bucket (Store.ListVersions); nothing there → done; else group the residue by generation aspark.Decidedoes (every generation other than the one the landing will build; the origin as@root) andhostclaim.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), thencfg.Park.Parkedmust 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 meetremote.go:346's live-set refusal forever); the acknowledgement half is C4's runner. -building:moves.BuildGenerationwith the seal from the claim journal (DecodeSeal(obs.Claim.Op.Journal[JournalSeal])),ReplicaPrefixin the own bucket,GenerationPrefix(holder.location, id),WorkDir = JournalDir. -published: journal only (nogeneration.json, R10-3). -local, in this order (round-2 finding 8;park/local.go:202requires paths relative toDataDir, tags them with the captured stamp and refuses a path already tagged with another stamp): (1)captured, err := hostguard.Capture(dataDir, host)(nowF); (2) when the journal'spreviousnamesS'andInventory[cfg.Box]has paths taggedS':cfg.Local.RetireArtifacts(ctx, host, captured, <the park-local chore id>, id, S')parks every artifact taggedS'(the old<host>.db,<host>.db.gen,<host>/,<host>.env) into.parked/<inc>/<host>/<retirement>/artifacts/, which clears their inventory entries; when there is noS',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+Extractthe newest archive fromFilesPrefix(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 underF; (5)ReplaceTimers(host, snapshot.Rows)from the sealed timers version (throughSelectedTimers.ReplaceTimersonce C3 lands;registry.ReplaceTimersdirectly 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.execwrites the installed files,<host>.envand 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}, wherePis the installation's prepared destination generation,"g-" + src.GenerationOp(the move id here; the rollback id for Plan 4's dirty return,installSource.GenerationOp), neverholder.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);TestLocalBindsThePreparedManifestToTheNewGenerationasserts the manifest key isfiles/g-<op>/manifest.jsonand its binding namesg-<op>while the claim'sholder.generationis 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 withgeneration = 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'sBundleETagsmust equalrec.Move.Inputs.BundleETagsand its overlay etagInputs.OverlayETag, elseMoveRollBack(Plan 4 Task 1's transition; until it exists, writeReasonand stop:markForRollback(rec, "configuration changed during the move")is the one call site Plan 4 replaces). -restored: copy the built database to the row'sDBPath(flat), sha256 == manifest's,lease.CheckDB, write<dbPath>.gen{"op": id, "incarnation": inc, "location": holder.location},MoveLandingPhase(configured → restored, stamp Landing.InstalledDBSHA256). -landing_deadlinepassed beforerestored→ the rollback marker as above.armMovesPasswiring inagent.gofor 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.gopaths). - [ ] Add this plan's strings to
docs/superpowers/plans/2026-10-06-moves-phase1-copy.mdunder "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:
restoredis the last phase this plan writes;probingbegins withProbeWake.
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).