carlos / platform Public

spec rev 7.22 (joint round 19, R19-1): the prepared files manifest is placed under and bound to the installation's prepared destination generation (g-<move id>, or g-<rollback id> for a dirty return), never holder.generation, which stays the source's until admission; install.json keys named (snapshot_txid for N); Plan 2 Task 10 local step (6) and Task 8 match; index C8

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 e1d7c3f1b5552948a1293d4e1c3df40299465bad parent 0a4cadd
3 files changed, +43 −10
  • docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md +2 −2
  • docs/superpowers/plans/2026-10-06-moves-phase1-2-cut-flip-landing.md +2 −2
  • docs/superpowers/specs/2026-10-02-move-any-app-design.md +39 −6

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 801e8e26c..776a8aec5 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 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) |
+| C8 | Region HA's files publisher API in two forms (spec rev 7.22 §7.6, L17, R18-1, R19-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}` where `generation` is the installation's **prepared destination generation** (`g-<move id>`, or `g-<rollback id>` for a dirty return), never `holder.generation`, which stays the source's until the probe's admission (R19-1); 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) |
### Shared contract types (coordination with claim Plan 6 Task 6)
@@ -144,7 +144,7 @@ Claim Plan 6 Task 6 on `feat/hostclaim-box` (bf63e250) implements C1 to C4 and d
| type | claim Plan 6 file | our definition | shape check |
|---|---|---|---|
-| `moves.InstallManifest` | `internal/moves/install_contract.go` | Plan 2 Task 8 | identical: `V int; Op, Host string; N uint64; SnapshotKey, SnapshotVersion string; SnapshotCRC64 uint64; AnchorKey, AnchorVersion string; AnchorCRC64 uint64; IntegrityCheck string; DBSHA256 string; BuiltAt time.Time`; platform-1e is adding snake_case json tags before merging (`v`, `op`, `host`, `n` or `snapshot_txid`, `snapshot_key`, `snapshot_version`, `snapshot_crc64`, `anchor_key`, `anchor_version`, `anchor_crc64`, `integrity_check`, `db_sha256`, `built_at`); Plan 2 Task 8 expects those keys and tests them; spec §7.6 names them at its next revision (round 19) |
+| `moves.InstallManifest` | `internal/moves/install_contract.go` | Plan 2 Task 8 | identical: `V int; Op, Host string; N uint64; SnapshotKey, SnapshotVersion string; SnapshotCRC64 uint64; AnchorKey, AnchorVersion string; AnchorCRC64 uint64; IntegrityCheck string; DBSHA256 string; BuiltAt time.Time`; snake_case json tags (final): `v`, `op`, `host`, `snapshot_txid` (Go field `N`), `snapshot_key`, `snapshot_version`, `snapshot_crc64`, `anchor_key`, `anchor_version`, `anchor_crc64`, `integrity_check`, `db_sha256`, `built_at`; Plan 2 Task 8 expects and tests them; spec rev 7.22 §7.6 names them |
| `moves.ErrAttemptOver` | `internal/moves/install_contract.go` | Plan 3 Task 1 | identity only (`errors.Is`); Plan 3 now uses claim Plan 6's message text |
| `provision.MoveInputs` | `internal/provision/moveinputs_contract.go` | **Plan 1 Task 2** (the record journal; it lands with the first task of the series, so our side may define it first) | identical fields and json tags: `bundle_etags`, `overlay_etag`, `release`, `files_plain`, `env_report`, all `omitempty` |
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 060884d1b..2112daa44 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
@@ -750,7 +750,7 @@ Tests: `RenderHeld` writes the file for a `landing` row that `SyncExec` would sk
**Interfaces:**
- `moves.ReadDBHeader(path string) (pageSize uint32, pageCount uint32, err error)` (SQLite header bytes 16-17 and 28-31).
- `moves.EncodeFull(db io.ReaderAt, pageSize, pageCount uint32, min, max ltx.TXID, timestamp int64, walOffset int64) (data []byte, crc64 uint64, err error)`: `ltx.NewEncoder` with `Header{Version: ltx.Version, Flags: ltx.HeaderFlagNoChecksum, PageSize, Commit: pageCount, MinTXID: min, MaxTXID: max, Timestamp: timestamp, WALOffset: walOffset}`, every page in order **except SQLite's lock page**, `ltx.LockPgno(pageSize)` (`ltx.go:494`; the encoder refuses it, `encoder.go:219`, and litestream skips it), while `Commit` keeps the full page count; `Close`; the CRC-64 is the trailer's `FileChecksum` (read it back with `ltx.NewDecoder(bytes.NewReader(data)).Verify()` then `Trailer().FileChecksum`).
-- `moves.BuildIntent{V int; Op, Host string; N uint64; PageSize, Commit uint32; Timestamp int64}`; `moves.InstallManifest{V int; Op, Host string; N uint64; SnapshotKey, SnapshotVersion string; SnapshotCRC64 uint64; AnchorKey, AnchorVersion string; AnchorCRC64 uint64; IntegrityCheck string; DBSHA256 string; BuiltAt time.Time}` (**shared contract type**, coordination with claim Plan 6 Task 6, `feat/hostclaim-box` bf63e250: claim Plan 6 defines this exact shape in `internal/moves/install_contract.go`, beside `moves.ErrAttemptOver`, and is adding snake_case json tags before it merges: `v`, `op`, `host`, `n`, `snapshot_key`, `snapshot_version`, `snapshot_crc64`, `anchor_key`, `anchor_version`, `anchor_crc64`, `integrity_check`, `db_sha256`, `built_at` (platform-1e may rename `n` to `snapshot_txid` to match `generation.json`: follow the contract file as merged). Whichever lands on `main` first owns the type and the other imports it (the coordinator's rule). **If `internal/moves/install_contract.go` is on `main` when this task is built, import the existing `InstallManifest` and do not redefine it.** Either way `install.json` carries those snake_case keys, never Go's default field names: if this task defines the type, it adds exactly those tags; `TestInstallManifestKeys` decodes a built `install.json` into `map[string]any` and asserts the key set, so a drift from the contract fails here. Spec §7.6's `install.json` description names the same keys at its next revision); `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.BuildIntent{V int; Op, Host string; N uint64; PageSize, Commit uint32; Timestamp int64}`; `moves.InstallManifest{V int; Op, Host string; N uint64; SnapshotKey, SnapshotVersion string; SnapshotCRC64 uint64; AnchorKey, AnchorVersion string; AnchorCRC64 uint64; IntegrityCheck string; DBSHA256 string; BuiltAt time.Time}` (**shared contract type**, coordination with claim Plan 6 Task 6, `feat/hostclaim-box` bf63e250: claim Plan 6 defines this exact shape in `internal/moves/install_contract.go`, beside `moves.ErrAttemptOver`, with these snake_case json tags (final, from platform-1e): `v`, `op`, `host`, `snapshot_txid` (the Go field stays `N`), `snapshot_key`, `snapshot_version`, `snapshot_crc64`, `anchor_key`, `anchor_version`, `anchor_crc64`, `integrity_check`, `db_sha256`, `built_at`; spec rev 7.22 §7.6 names the same keys. Whichever lands on `main` first owns the type and the other imports it (the coordinator's rule). **If `internal/moves/install_contract.go` is on `main` when this task is built, import the existing `InstallManifest` and do not redefine it.** Either way `install.json` carries those snake_case keys, never Go's default field names: if this task defines the type, it adds exactly those tags; `TestInstallManifestKeys` decodes a built `install.json` into `map[string]any` and asserts exactly that key set, so a drift from the contract fails here); `moves.GenerationPrefix(location, op string) string` (`<location>g-<op>/`, location with its trailing slash); `moves.SnapshotKey(prefix string, n uint64)` = `prefix + "0009/" + ltx.FormatFilename(1, n)`; `moves.AnchorKey(prefix, n)` = `prefix + "0000/" + ltx.FormatFilename(n, n)`; `BuildIntentKey(prefix) = prefix+"build-intent.json"`, `InstallManifestKey(prefix) = prefix+"install.json"`.
- `moves.NewSealedClient(store lease.VersionedStore, bucket, replicaPrefix string, seal Seal) *SealedClient` implementing `litestream.ReplicaClient`: `Type() "sealed"`, `Init` no-op, `LTXFiles(ctx, level, seek, _)` returns `ltx.NewFileInfoSliceIterator` over the sealed objects at that level parsed from their keys (`ltx.ParseFilename`), `OpenLTXFile` opens exactly the sealed version by `OpenVersion(ctx, bucket, key, versionID)` (ranged through `VersionRangeOpener` when offset/size are set), `WriteLTXFile`, `DeleteLTXFiles`, `DeleteAll` return `ErrSealedReadOnly`, `SetLogger` stores it.
- `moves.BuildGeneration(ctx, in BuildInputs) (InstallManifest, error)` with `BuildInputs{Store lease.VersionedStore; Bucket string; Source []SealedObject (the pinned version set: for a move the seal's replica/ objects, for region HA's relocation the versions a `genselect` plan or a current listing pins, L16); N uint64; GenerationPrefix string; Op, Host string; WorkDir string; Now func() time.Time; Verify func(dbPath string) error}` (`SealedClient` is built over `Source`, so nothing in the routine names a staging lane): (1) read or write the intent (`PutIfAbsent`; on `lease.ErrExists` read it and use its timestamp, page size, commit); (2) restore into a fresh scratch file: remove `<WorkDir>/<host>.db`, `-wal`, `-shm` and `-txid` first (litestream's `Restore` refuses an existing output path, `replica.go:601`; the file is reproducible from the sealed versions, so a replay after any crash starts clean), then `litestream.NewReplicaWithClient(nil, sealedClient).Restore(ctx, opt)` with `opt.OutputPath = <WorkDir>/<host>.db`, `opt.TXID = ltx.TXID(seal.N)`; refuse unless the restored position is `N` (litestream writes `<db>-txid`? Only in follow mode; instead compare the plan's final `MaxTXID` through `litestream.CalcRestorePlan(ctx, client, ltx.TXID(N), time.Time{}, logger)` whose last file's `MaxTXID` must be `N`, and verify every planned file with `ltx.NewDecoder(...).Verify()` before the restore); `Verify(dbPath)` = `lease.CheckDB` (integrity_check); (3) `EncodeFull` snapshot `[1, N]` and `PutIfAbsent(SnapshotKey)`; on `ErrExists`, `OpenVersion` the current one and require equal bytes (identical by construction from the intent; differing bytes refuse with `ErrBuildDiverged`); (4) the anchor `[N, N]` with `WALOffset = litestream.WALHeaderSize`, same rule; (5) sha256 of the database file; (6) `InstallManifest` via `PutIfAbsent` (on exists, read and compare `N`, keys, versions).
@@ -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 **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.
+ - `local`, in this order (round-2 finding 8; `park/local.go:202` requires paths **relative to `DataDir`**, tags them with the captured stamp and refuses a path already tagged with another stamp): (1) `captured, err := hostguard.Capture(dataDir, host)` (now `F`); (2) when the journal's `previous` names `S'` and `Inventory[cfg.Box]` has paths tagged `S'`: `cfg.Local.RetireArtifacts(ctx, host, captured, <the park-local chore id>, id, S')` parks every artifact tagged `S'` (the old `<host>.db`, `<host>.db.gen`, `<host>/`, `<host>.env`) into `.parked/<inc>/<host>/<retirement>/artifacts/`, which clears their inventory entries; when there is no `S'`, `AcknowledgeEmpty`; the test asserts on the filesystem that the old files are gone from the live paths and present under `.parked/`, not on the call returning nil; (3) `archive.Open` + `Extract` the newest archive from `FilesPrefix` (by the seal's files version) into `<DataDir>/<host>/` and `<DataDir>/<host>.env`; (4) `cfg.Local.RecordInventory(ctx, host, captured, "flat", []string{host + ".db", host + ".db.gen", host, host + ".env"}, check)` registers the new installation under `F`; (5) `ReplaceTimers(host, snapshot.Rows)` from the sealed timers version (through `SelectedTimers.ReplaceTimers` once C3 lands; `registry.ReplaceTimers` directly behind the switch until then); `SetReplication(host, Source.Replication)`, `SetNextDue(host, Source.NextDue)`; `RemoveVerifyEntry`; (6) the **prepared files manifest** of spec rev 7.21 §7.6 (L17, R18-1; claim rev 6.30 73b1b9dc) under the installation's authority, never the session-bound publisher (no target session exists yet): `op.exec` writes the installed files, `<host>.env` and the wake state as create-only content objects under `<location>files/<P>/objects/<seq>-<sha256>` and then the one create-only manifest `<location>files/<P>/manifest.json`, bound to `{location, P, op id, installation stamp}`, where **`P` is the installation's prepared destination generation, `"g-" + src.GenerationOp`** (the move id here; the rollback id for Plan 4's dirty return, `installSource.GenerationOp`), **never `holder.generation`**, which still names the source's pre-move generation until the probe's admission (spec rev 7.22 §7.6, R19-1: a manifest bound to or filed under the source's generation could never be the new generation's baseline, and the first landing admission would block); `TestLocalBindsThePreparedManifestToTheNewGeneration` asserts the manifest key is `files/g-<op>/manifest.json` and its binding names `g-<op>` while the claim's `holder.generation` is still the source's; naming exact versions, at sequence 0, never replaced and retired with its generation, through C8's prepared-manifest build (region HA :809; `files.BuildPrepared(ctx, store, location, generation, opID string, stamp uint64, dir, envPath string, wake WakeState) (manifestVersion string, err error)` or its real name, called with `generation = P`), journalled like the install manifest; a replay reuses the objects by digest and the manifest by key; the session-bound publisher starts at the probe's admission (Plan 3 Task 5), takes this manifest by exact version as its baseline and publishes from sequence 1 (a journal step, skipped with a log line until C8 exists); (7) fsync the directory; write `.files-gen` `{"op": id, "incarnation": inc}` last.
- `configured`: `RenderHeld`; `MaterializeHeld(rt, rec.Move.Inputs.Release)`; the ack's `BundleETags` must equal `rec.Move.Inputs.BundleETags` and its overlay etag `Inputs.OverlayETag`, else `MoveRollBack` (Plan 4 Task 1's transition; until it exists, write `Reason` and stop: `markForRollback(rec, "configuration changed during the move")` is the one call site Plan 4 replaces).
- `restored`: copy the built database to the row's `DBPath` (flat), sha256 == manifest's, `lease.CheckDB`, write `<dbPath>.gen` `{"op": id, "incarnation": inc, "location": holder.location}`, `MoveLandingPhase(configured → restored, stamp Landing.InstalledDBSHA256)`.
- `landing_deadline` passed before `restored` → the rollback marker as above.
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 30f53fcfd..c194bf885 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,7 +1,25 @@
# Move any app or instance to another region: Phase 1, a safe single-instance move
**Date:** 2026-10-06
-**Revision:** 7.21.
+**Revision:** 7.22.
+
+- **Rev 7.22** answers **joint round 19** (`CODEX-FINDINGS-JOINT-R19.md`,
+ "R19-1", the round's one finding, P2) and fixes the install
+ manifest's keys. **R19-1:** rev 7.21 bound the prepared files manifest
+ to "the holder location and generation" during `local`, but
+ `holder.generation` still names the source's pre-move generation `S`
+ until the probe's admission (§7.14, §9.4), and claim rev 6.30 binds the
+ manifest to the **prepared** generation `P`; a manifest bound to `S`
+ could never be `P`'s baseline and an ordinary cold landing would block.
+ §7.6 now binds the manifest, and places it, under the installation's
+ **prepared destination generation**: `g-<move id>` for a forward
+ landing, the prepared return generation `g-<rollback id>` for a dirty
+ rollback; `holder.generation` is unchanged until admission. Plan 2
+ Task 10's `local` step matches. **`install.json` keys:** §7.6 names the
+ snake_case keys claim Plan 6's `install_contract.go` tags the shared
+ `moves.InstallManifest` with (`snapshot_txid` for the Go field `N`).
+ The model encodes no manifest binding (§14.2.6); no rule changed, gate
+ only.
- **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):**
@@ -1207,7 +1225,12 @@ fails there first (§14.2.6).
manifest**, `<location>g-<move id>/install.json`, with the artifacts'
acknowledged identities (keys, the `versionId` each PUT returned, `N`,
each file's CRC-64) and the verification results (`integrity_check`
- passed, the sha256 of the database file the build produced). The
+ passed, the sha256 of the database file the build produced). Its
+ keys are the shared `moves.InstallManifest`'s json tags (claim Plan 6's
+ `internal/moves/install_contract.go`; rev 7.22): `v`, `op`, `host`,
+ `snapshot_txid` (the Go field `N`), `snapshot_key`,
+ `snapshot_version`, `snapshot_crc64`, `anchor_key`, `anchor_version`,
+ `anchor_crc64`, `integrity_check`, `db_sha256`, `built_at`. The
completion predicate is the manifest's existence, never the intent's:
rev 7.7 said "the install manifest records the inputs before the PUTs"
and "`building` is done when the install manifest exists", so a crash
@@ -2686,10 +2709,20 @@ 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
+objects under `<location>files/<P>/objects/` and then the one
+manifest `<location>files/<P>/manifest.json`, **bound to
+`{location, P, op id, installation stamp}`** where **`P` is the
+installation's prepared destination generation, never
+`holder.generation`** (rev 7.22, R19-1): `g-<move id>` for a forward
+landing, the prepared return generation `g-<rollback id>` for a dirty
+rollback (§7.12), the same name the install manifest and the generation
+prefix of §5.1 use. `holder.generation` still names the source's
+pre-move generation `S` until the probe's cold admission moves it
+(§7.14, §9.4) and is **not** changed by `local`; a manifest bound to or
+filed under `S` could never be `P`'s baseline, so the first landing
+admission would block (claim rev 6.30 requires the prepared generation).
+The binding's other members are the move id (or rollback id), the holder
+location and the flip's stamp. The manifest is published **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