spec rev 7.21 (joint round 18): the prepared files manifest mirrors claim rev 6.30 (73b1b9dc; R18-1), "before its flip" is before the record enters landing through the flip CAS (R18-2), M-C22 promises retained data and pending recovery, not zero loss (R18-4); Plan 2 Task 10 local step (6); index C8 names the prepared-manifest build
Gate: gofmt, vet and the hostlife model tests green; no rule changed, no sweep.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
4 files changed,
+72
−22
docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md+1 −1docs/superpowers/plans/2026-10-06-moves-phase1-2-cut-flip-landing.md+1 −1docs/superpowers/plans/2026-10-06-moves-phase1-copy.md+1 −1docs/superpowers/specs/2026-10-02-move-any-app-design.md+69 −19
CI passed — run details
diff --git a/docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md b/docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md| index b304e7b88..f5d50ba3e 100644 |
| --- a/docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md |
| +++ b/docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md |
| @@ -136,7 +136,7 @@ source conversion, L13, the one-box scope). |
| | C5 | Production callers for `hostops.Controller.Adopt` (`adoptHostRecord`) on the reconcile pass and for `(s *Server) StartClaimRecoveryPass`: a legacy host gets its claim on the next reconcile pass. | claim Plan 5 integration (work order 3) | the rehearsal (Plan 7 P-5); the request door's "no host claim yet" refusal is correct meanwhile | |
| | C6 | The loss sequence, end to end: `CreateLossRecord` of a `prepared` record with `WriteSources` composed as **the latest target session as reason `target` and every earlier target session since the cut and, when `src_covered` is false, the source's cut session as further sources** (`lost.go:149-191` requires the frozen set to equal the admission's primary `WritesLost` plus `AdditionalWritesLost`, unique by `(box, generation, session)`, each with a validated baseline); the transition `prepared → committed` with `ResultClaimGen` after `DeclareLost` (`lost.go:119` admits only a `committed` record); `DestroyedStorage` attestations for `DischargeLocal`; `Enrollment` and `QuarantineRevision` as the roster supplies them (Plan 8b), and a fence form of the declaration with neither (region HA L2 to L4). Moves supplies `moves.LossOwner` (`ValidateDeclareLost`, `ValidateLossRecovery`) and the record-side `lost` phase; the claim owns the record's lifecycle and the exact `LossRecord` composition. | claim Plan 8 + 8b (work order 2) | Plan 4 Task 10; the lost-target branch of Plan 4 Task 5; Plan 6 Task 1 `--declare-lost`; Plan 7 Run 4 | |
| | C7 | **Settled by claim rev 6.28 §7.3 (spec rev 7.20 §7.10):** `op.journal["fence.<box>"] = {"enrollment","key","sha256"}` names `control/box-fences/<enrollment>.json` and the digest of the bytes verified; a `loss.<id>` intent cites the same reference; consumers accept a fence only through it, re-verifying the record; an automatic declaration needs no quarantine revision and no operator. Still owed by Plans 8/8b: the fence record's writer and the verification helper (`hostclaim.VerifyFence(ctx, s, ref) error` or its real name) and the one-CAS conversion of a pre-flip `moving` op to `relocating` at `pinning` (`hostclaim.ConvertMovingToRelocating(opID, fence FenceRef) func(*Claim) error` or its real name: exec `"none"`, stamp and generation preserved, obligations untouched, `relocating`/`pinning` in the source region; refused once the record is `landing`). Moves' `ReturnToSource` (Plan 4 Task 2) and the console pass (Plan 5 Task 7) consume both. | claim Plans 8 and 8b | Plan 4 Task 2's fence form; Plan 5 Task 7's conversion call; Plan 6 Task 2's `waiting for a box in <region>` state | |
| -| C8 | Region HA's files publisher API, `files.Publish(ctx, store lease.VersionedStore, location, generation, dir, envPath string) (manifestVersion string, err error)` or its real name, for `local`'s files manifest (spec rev 7.20 §7.6, L17), bound to the session (claim C3), writing origin-qualified references (P6-D3), with the reader a cold admission uses to establish its files manifest from the admitted predecessor chain by exact version. | region HA plan 1a | Plan 2 Task 10 `local`'s manifest step (`.files-gen` alone until then); Plan 3 Task 5's admitted start (files from the manifest once C8 exists) | |
| +| C8 | Region HA's files publisher API in two forms (spec rev 7.21 §7.6, L17, R18-1; claim rev 6.30 73b1b9dc). The **prepared-manifest build**, `files.BuildPrepared(ctx, store lease.VersionedStore, location, generation, opID string, stamp uint64, dir, envPath string, wake WakeState) (manifestVersion string, err error)` or its real name: before any session exists, under `op.exec`'s installation authority, create-only content objects plus one create-only manifest bound to `{location, generation, op id, installation stamp}`, exact versions, sequence 0, never replaced, retired with its generation. The **session-bound publisher**, `files.Publish(...)` or its real name (claim C3), which starts after the first admission, takes the prepared manifest by exact version as its baseline and publishes from sequence 1, writing origin-qualified references (P6-D3); and the reader a cold admission uses to establish its files manifest from the admitted predecessor chain by exact version. | region HA plan 1a | Plan 2 Task 10 `local`'s manifest step (6) (`.files-gen` alone until then); Plan 3 Task 5's admitted start (files from the manifest once C8 exists) | |
| ## Decisions locked across the series |
diff --git a/docs/superpowers/plans/2026-10-06-moves-phase1-2-cut-flip-landing.md b/docs/superpowers/plans/2026-10-06-moves-phase1-2-cut-flip-landing.md| index f61305819..2136b37d2 100644 |
| --- a/docs/superpowers/plans/2026-10-06-moves-phase1-2-cut-flip-landing.md |
| +++ b/docs/superpowers/plans/2026-10-06-moves-phase1-2-cut-flip-landing.md |
| @@ -793,7 +793,7 @@ Tests: `TestLocationParkIsAParkReplicaTheStoreAccepts` (a real `hostclaim.Update |
| - `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 files manifest of spec rev 7.19 §7.6 (L17) through C8's publisher when it exists (a journal step, skipped with a log line until then); (7) fsync the directory; write `.files-gen` `{"op": id, "incarnation": inc}` last. |
| + - `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/<generation>/objects/<seq>-<sha256>` and then the one create-only manifest `<location>files/<generation>/manifest.json`, bound to `{location, generation, op id, installation stamp}`, 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), 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. |
diff --git a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md| index cf8cf81c2..64fd8fe9f 100644 |
| --- a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md |
| +++ b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md |
| @@ -34,7 +34,7 @@ Columns: id, file, text, status. |
| | M-C19 | internal/console/apiinstances.go | <host> is being moved. Delete it after the move finishes, or abort the move first. | pending | |
| | M-C20 | internal/host/movespaging.go | <box> is running a process for <host> that this move cannot account for. An operator has been paged. | pending | |
| | M-C21 | internal/console/apiinstancemove.go | <region> has more than one box. Moves between regions with several boxes are not supported yet. | pending | |
| -| M-C22 | move.go | <host> is waiting for a box in <region>. Its last box was terminated before the move finished. Nothing is lost, and it will start when a box joins the region. | pending | |
| +| M-C22 | move.go | <host> is waiting for a box in <region>. Its last box was terminated before the move finished. Its data up to the last completed upload is kept in <region>. It will start when a box joins the region, and any writes after that upload will be reported then. | pending | |
| ## Plan 1 |
diff --git a/docs/superpowers/specs/2026-10-02-move-any-app-design.md b/docs/superpowers/specs/2026-10-02-move-any-app-design.md| index 1950fbd41..30f53fcfd 100644 |
| --- a/docs/superpowers/specs/2026-10-02-move-any-app-design.md |
| +++ b/docs/superpowers/specs/2026-10-02-move-any-app-design.md |
| @@ -1,8 +1,30 @@ |
| # Move any app or instance to another region: Phase 1, a safe single-instance move |
| **Date:** 2026-10-06 |
| -**Revision:** 7.20. |
| - |
| +**Revision:** 7.21. |
| + |
| +- **Rev 7.21** answers **joint round 18** (`CODEX-FINDINGS-JOINT-R18.md`, |
| + cited "R18-n"; not clean: one P2, three P3s, no P1). **R18-1 (P2):** |
| + the first files manifest of an installation is not the session-bound |
| + publisher's, since no target session exists until `probing`; §7.6 now |
| + has the landing build the initial immutable files objects and a |
| + create-only **prepared manifest**, mirroring claim rev 6.30 |
| + (73b1b9dc) exactly: published by `op.exec` before any session exists, |
| + in a phase that permits installation; bound to `{location, |
| + generation, op id, installation stamp}`, naming exact versions, at |
| + sequence 0, never replaced, retired with its generation; the |
| + session-bound publisher starts only after the first admission, takes |
| + that manifest by exact version as its baseline and publishes from |
| + sequence 1 (region HA spec 1 §5.8's prepared-manifest build, `:809`, |
| + is the same rule). **R18-2 (P3):** §7.10's "before the flip" is stated |
| + as "the record has not entered `landing`", the flip's record CAS, |
| + distinct from the final commit. **R18-4 (P3):** M-C22 no longer |
| + promises that nothing is lost; it describes the retained remote data, |
| + the pending recovery and the declaration a restart may make (§20). |
| + R18-3 is the claim's (§11 there). The model encodes no publication |
| + timing beyond installing the files at the installation CAS, which is |
| + the prepared manifest's moment (§14.2.6); no rule changed and no sweep |
| + was run. |
| - **Rev 7.20** aligns with claim rev 6.28 (4ffe8a0d), which adopts region |
| HA's L1 to L20 at its sections (`CODEX-ANALYSIS-REGION-HA-L1-L20.md` |
| beside it). **The fence reference** (§7.10, moves C7): every transition |
| @@ -2651,17 +2673,36 @@ versions `genselect.SelectPrepared` or the generation's current listing |
| pins and that listing's maximum TXID. The anchor rule, the two durable |
| states, the replay rule and the marker rule are the routine's, so both |
| callers get them once. **Every installation writes a files manifest in |
| -its new generation** (L17, §8.3; claim rev 6.28 C3, §6.2a, P6-D3): after |
| -`local` has extracted the archive, the installed files directory is |
| -published by the **session-bound files publisher** (claim C3's second |
| -ordinary-replication exception, bound to `{location, generation, |
| -session}`) as region HA §5.6's immutable objects and one manifest under |
| -`<location>files/<generation>/`, before `.files-gen` is written. Manifest |
| -entries carry **origin-qualified references** (claim P6-D3): an object is |
| -named by its own incarnation location and region, so copying a manifest |
| -into a new generation never reinterprets its references, and a cross-region |
| -landing publishes a manifest naming destination-local objects it |
| -materialized and verified from the sealed archive. **Files reachability is |
| +its new generation** (L17, §8.3; claim rev 6.28 C3, §6.2a, P6-D3; rev |
| +7.21 after R18-1): the first manifest is **not** the session-bound |
| +publisher's, because no target writer session exists until `probing` |
| +(claim C3 binds that publisher to an admitted `{location, generation, |
| +session}`). It is the **prepared manifest**, as claim rev 6.30 (73b1b9dc) states the |
| +rule and region HA spec 1 §5.8's remote build (`:809`) uses it: |
| +**before any session exists, `op.exec` may publish an installation's |
| +initial files state in a phase that permits installation**: a move's |
| +`local`, a relocation's `installing`, or region HA's prepared build. That |
| +state is **create-only content objects plus one create-only prepared |
| +manifest**: in `local`, after the archive is extracted and verified, the |
| +target writes the installed files, `<host>.env` and the wake state (the |
| +timers snapshot, `NextDue`, `Replication`) as region HA §5.6's immutable |
| +objects under `<location>files/<generation>/objects/` and then the one |
| +manifest `<location>files/<generation>/manifest.json`, **bound to |
| +`{location, generation, op id, installation stamp}`** (the move id, the |
| +holder location and generation, the flip's stamp), **naming exact |
| +versions, with sequence 0, never replaced, and retired with its |
| +generation**; it is journalled in the op journal like the install |
| +manifest, before `.files-gen` is written, and a replay reuses the objects |
| +it finds by digest and the manifest by key (create-only, so a late second |
| +write fails). The **session-bound publisher starts only after the first |
| +admission** (the probe's, §7.14): it **takes that manifest by exact |
| +version as its baseline and publishes from sequence 1**; until then |
| +nothing but the install writes under `files/<generation>/`. Manifest entries carry **origin-qualified |
| +references** (claim P6-D3): an object is named by its own incarnation |
| +location and region, so copying a manifest into a new generation never |
| +reinterprets its references, and a cross-region landing's prepared |
| +manifest names destination-local objects it materialized and verified |
| +from the sealed archive. **Files reachability is |
| independent of the database baseline:** a cold admission establishes its |
| files manifest from the newest valid manifest in the admitted predecessor |
| chain, read by exact version, before the app starts; missing or malformed |
| @@ -2857,9 +2898,15 @@ and the fence record as `rollback.evidence`; after the record's commit |
| the target is authoritative and the move is not what recovers the |
| instance: the move finishes its own close (its remaining steps are parks |
| run by regional workers), and the instance's recovery is region HA's |
| -relocation, outside this spec. **A source terminated before the flip (rev 7.20; claim rev 6.28 §4).** |
| -The fence converts the pre-flip `moving` operation to `relocating` in |
| -**one CAS**: it records the fence reference (below), sets `op.exec = |
| +relocation, outside this spec. **A source terminated before the flip (rev 7.20; claim rev 6.28 §4; |
| +R18-2; claim rev 6.30).** "Before its flip" means **before the |
| +authoritative record enters `landing` through the flip CAS** (`archived |
| +→ flipped`, which sets `state = landing`, §7.4). That is distinct from, |
| +and earlier than, the final commit CAS (`probed → committed`, §7.9): a |
| +record at `landing` whose claim half is still `moving/sealed` is already |
| +past the flip, and the conversion is refused there as everywhere after |
| +it. The fence converts |
| +the pre-flip `moving` operation to `relocating` in **one CAS**: it records the fence reference (below), sets `op.exec = |
| "none"`, preserves `holder.stamp` and `holder.generation`, starts |
| `relocating` at `pinning` in the source's region, and leaves every |
| obligation untouched; the move's staging lane, `park-replica` scoped |
| @@ -6018,8 +6065,11 @@ baselines, this sweep and the witnesses are the evidence for rev 7.10. |
| then completing on it. The model collapses the relocation's install |
| into the cold admission's restore of the pinned generation from the |
| region's remote, and stands in for region HA's files publisher by |
| - taking the fenced source's files directory as published (L17); it |
| - does not model `choose()`, two replacements, or `stuck` after them. |
| + taking the fenced source's files directory as published (L17) and |
| + installing it at the installation CAS, before any admission, which is |
| + the prepared manifest's moment (§7.6, R18-1); it encodes no publisher |
| + timing beyond that and does not model `choose()`, two replacements, or |
| + `stuck` after them. |
| The two-box extension belongs to a joint round with the region HA |
| session, on its own branch, with its own sweeps. |
| - **It is a model.** It checks the protocol, not the code that will |
| @@ -6784,7 +6834,7 @@ copy-review gate. No em dashes. |
| | M-C19 | <host> is being moved. Delete it after the move finishes, or abort the move first. | |
| | M-C20 | <box> is running a process for <host> that this move cannot account for. An operator has been paged. | |
| | M-C21 | <region> has more than one box. Moves between regions with several boxes are not supported yet. | |
| -| M-C22 | <host> is waiting for a box in <region>. Its last box was terminated before the move finished. Nothing is lost, and it will start when a box joins the region. | |
| +| M-C22 | <host> is waiting for a box in <region>. Its last box was terminated before the move finished. Its data up to the last completed upload is kept in <region>. It will start when a box joins the region, and any writes after that upload will be reported then. | |
| ## 21. Out of scope for Phase 1, restated |