carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 819c8d0672c2b2ced68555e6aece670fe672df82 parent d090774
4 files changed, +72 −22
  • docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md +1 −1
  • docs/superpowers/plans/2026-10-06-moves-phase1-2-cut-flip-landing.md +1 −1
  • docs/superpowers/plans/2026-10-06-moves-phase1-copy.md +1 −1
  • docs/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