carlos / platform Public

moves-offline: copy approved, copy-gate helper extraction, census re-pin

Copy: all 151 MV-O rows of the moves copy record are approved
unchanged (Paul, 2026-10-20), flipped pending -> approved.

Copy gate: the five record-transition refusals MV-O36, 38, 40, 41 and
43 move into fmt.Sprintf helpers (offlineWrongState, offlineSealedBy,
offlineAfterOpen, offlineFinishState, offlineNotFrom), wrapped as
fmt.Errorf("%w: %s", ErrMoveConflict, ...), because the gate's applied
check cannot read %w formats. The unused refusedOfflineAfterOpen const
goes. Text only: every sentence is byte-identical.
TestOfflineHelperRefusalsMatchTheCopyRecord drives each refusal through
its real transition and pins it to the approved copy-record text, with
errors.Is(err, ErrMoveConflict) still holding.

Census (census-ruling-moves-offline, re-pin accepted with conditions by
platform-1c): OfflineStart is re-frozen at its disposition with its new
digest (site file:Open@184 unchanged), the internal/provision envelope
is re-pinned, and the seal moves to 393e7f45...65bec29. Recorded in the
overlay header and the census proposal note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev e240df0e7305433bed097cb2e6548d18cfb92071 parent d65c2c6
6 files changed, +280 −163
  • docs/superpowers/notes/census-proposal-moves-offline.md +1 −0
  • docs/superpowers/plans/2026-10-06-moves-phase1-copy.md +151 −151
  • internal/provision/offline_move.go +30 −8
  • internal/provision/offline_move_copy_test.go +91 −0
  • internal/provision/plan14_root_reads_test.go +3 −1
  • internal/provision/testdata/plan14-root-reads-moves-offline.tsv +4 −3
diff --git a/docs/superpowers/notes/census-proposal-moves-offline.md b/docs/superpowers/notes/census-proposal-moves-offline.md
index 8f5131acc..9328456d6 100644
--- a/docs/superpowers/notes/census-proposal-moves-offline.md
+++ b/docs/superpowers/notes/census-proposal-moves-offline.md
@@ -146,3 +146,4 @@ regenerated the same way.
- **R2 (9B8).** Accepted as classified.
- **R3 (agentRunsAsRoot).** Accepted with a condition. The offline move must never run on a box whose agent actually runs as root. The runbook prechecks, read-only on every source-region box and the target box, that `systemctl show carlos-edge -p User` prints `User=carlos`. Any other value is a STOP.
- **Extension (platform-1c, 2026-10-20, for 8dc2a544's release item).** `landRun.release` is re-frozen at its disposition (whole:Route@368, digest 492cffa9… to d589aea5…). `landRun.liveNotSealed` is accepted legacy under the same tag, on the condition that it compares the live link's Readlink target string with the sealed build's path and follows nothing beyond the link. It used `release.ResolveLiveLink`, which walks the chain, so it now does Lstat, Readlink and a string comparison; its sites are file:Lstat@172, file:Readlink@404, registry:App@537, whole:Route@516, whole:Route@539. The internal/host envelope is re-pinned, and the seal moves to b14f1723…570637.
+- **Re-pin (platform-1c, 2026-10-20, accepted with conditions).** Re-pinned for a text-only helper extraction (copy gate), 2026-10-20; no read, class or behaviour change. The five transition refusals MV-O36, 38, 40, 41 and 43 moved into fmt.Sprintf helpers so the copy gate can read them; OfflineStart's diff is the MV-O43 refusal alone. OfflineStart is re-frozen at its disposition (file:Open@184 unchanged, digest 2d3cd134… to a5097e64…), the internal/provision envelope is re-pinned, and the seal moves to 393e7f45…65bec29. `TestOfflineHelperRefusalsMatchTheCopyRecord` (internal/provision/offline_move_copy_test.go) drives each of the five through its real transition and pins it byte for byte to the approved copy-record text, still wrapping ErrMoveConflict.
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 0d2e8c798..504dcc7a4 100644
--- a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
+++ b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
@@ -491,194 +491,194 @@ Plan `docs/superpowers/plans/2026-10-20-offline-operator-move.md`. The rows take
| id | file | text | status |
|---|---|---|---|
-| MV-O1 | move.go | <host> is offline from now until the move opens or is aborted. | pending |
-| MV-O2 | move.go | Every box in <from> has stopped <host>. Run carlos move offline seal --host <host> to copy it. | pending |
-| MV-O3 | move.go | Copying <host> to <to>. | pending |
-| MV-O4 | move.go | Copied and checked. Run carlos move offline flip --host <host> to start it in <to>. | pending |
-| MV-O5 | move.go | <to> holds everything <host> needs. Nothing is served yet. Run carlos move offline open --host <host> to serve it from <to>. | pending |
-| MV-O6 | move.go | <to> could not take <host>: <reason>. Run carlos move offline abort --host <host> to move it back. | pending |
-| MV-O7 | move.go | <host> is served from <to>. Run carlos move offline finish --host <host> once <from> has cleaned up. | pending |
-| MV-O8 | move.go | Moving <host> back to <from>. | pending |
-| MV-O9 | move.go | The move could not finish a step: <reason>. Run carlos move offline status --host <host> for each item. | pending |
-| MV-O10 | move.go | Waiting for <box> to clean up <host>. | pending |
-| MV-O20 | internal/console/apiinstancemove_offline.go | offline moves are not available while host claims or Phase 1 moves are on | pending |
-| MV-O21 | internal/console/apiinstancemove_offline.go | <box> cannot take part in an offline move. it runs an older release or has host claims on | pending |
-| MV-O22 | internal/console/apiinstancemove_offline.go | <region> has <n> boxes. an offline move needs exactly one box in the region it moves to | pending |
-| MV-O23 | internal/console/apiinstancemove_offline.go | <box> does not hold this deployment's secrets key | pending |
-| MV-O24 | internal/console/apiinstancemove_offline.go | <host> has no offline move in flight | pending |
-| MV-O25 | internal/console/apiinstancemove_offline.go | this move is <state>. run carlos move offline status --host <host> to see what it waits for | pending |
-| MV-O26 | internal/console/apiinstancemove_offline.go | <host> keeps local data on more than one box: <boxes>. remove the stale copy first | pending |
-| MV-O27 | internal/console/apiinstancemove_offline.go | <host> is already served from <to>. move it back with a new move | pending |
-| MV-O28 | internal/console/apiinstancemove_offline.go | <box> keeps a sidecar file for <host>. instances with sidecars cannot move yet | pending |
-| MV-O29 | internal/console/apiinstancemove_offline.go | <host> has a host claim. offline moves are only for hosts without one | pending |
-| MV-O30 | internal/console/apiinstancemove_offline.go | <other> is moving offline. move one instance at a time | pending |
-| MV-O31 | internal/console/apiinstancemove_offline.go | <box> still runs a process for <host>. wait for it to stop, then flip | pending |
+| MV-O1 | move.go | <host> is offline from now until the move opens or is aborted. | approved |
+| MV-O2 | move.go | Every box in <from> has stopped <host>. Run carlos move offline seal --host <host> to copy it. | approved |
+| MV-O3 | move.go | Copying <host> to <to>. | approved |
+| MV-O4 | move.go | Copied and checked. Run carlos move offline flip --host <host> to start it in <to>. | approved |
+| MV-O5 | move.go | <to> holds everything <host> needs. Nothing is served yet. Run carlos move offline open --host <host> to serve it from <to>. | approved |
+| MV-O6 | move.go | <to> could not take <host>: <reason>. Run carlos move offline abort --host <host> to move it back. | approved |
+| MV-O7 | move.go | <host> is served from <to>. Run carlos move offline finish --host <host> once <from> has cleaned up. | approved |
+| MV-O8 | move.go | Moving <host> back to <from>. | approved |
+| MV-O9 | move.go | The move could not finish a step: <reason>. Run carlos move offline status --host <host> for each item. | approved |
+| MV-O10 | move.go | Waiting for <box> to clean up <host>. | approved |
+| MV-O20 | internal/console/apiinstancemove_offline.go | offline moves are not available while host claims or Phase 1 moves are on | approved |
+| MV-O21 | internal/console/apiinstancemove_offline.go | <box> cannot take part in an offline move. it runs an older release or has host claims on | approved |
+| MV-O22 | internal/console/apiinstancemove_offline.go | <region> has <n> boxes. an offline move needs exactly one box in the region it moves to | approved |
+| MV-O23 | internal/console/apiinstancemove_offline.go | <box> does not hold this deployment's secrets key | approved |
+| MV-O24 | internal/console/apiinstancemove_offline.go | <host> has no offline move in flight | approved |
+| MV-O25 | internal/console/apiinstancemove_offline.go | this move is <state>. run carlos move offline status --host <host> to see what it waits for | approved |
+| MV-O26 | internal/console/apiinstancemove_offline.go | <host> keeps local data on more than one box: <boxes>. remove the stale copy first | approved |
+| MV-O27 | internal/console/apiinstancemove_offline.go | <host> is already served from <to>. move it back with a new move | approved |
+| MV-O28 | internal/console/apiinstancemove_offline.go | <box> keeps a sidecar file for <host>. instances with sidecars cannot move yet | approved |
+| MV-O29 | internal/console/apiinstancemove_offline.go | <host> has a host claim. offline moves are only for hosts without one | approved |
+| MV-O30 | internal/console/apiinstancemove_offline.go | <other> is moving offline. move one instance at a time | approved |
+| MV-O31 | internal/console/apiinstancemove_offline.go | <box> still runs a process for <host>. wait for it to stop, then flip | approved |
The docs page (`docs/site/cli.md`, under `### carlos move`) gets two paragraphs and one block of commands. The block is commands, not prose, and is not a row. The docsite gate reads it.
| id | file | text | status |
|---|---|---|---|
-| MV-O32 | docs/site/cli.md | `carlos move offline` moves one instance with planned downtime, one step at a time. It is for operators, works with host claims and Phase 1 moves off, and never acts on its own: each verb does one step and says which verb comes next. | pending |
-| MV-O33 | docs/site/cli.md | The instance is offline from `start` until `open`. Until `open`, `abort` puts it back where it was with nothing lost. After `open`, moving it back is a new move. | pending |
+| MV-O32 | docs/site/cli.md | `carlos move offline` moves one instance with planned downtime, one step at a time. It is for operators, works with host claims and Phase 1 moves off, and never acts on its own: each verb does one step and says which verb comes next. | approved |
+| MV-O33 | docs/site/cli.md | The instance is offline from `start` until `open`. Until `open`, `abort` puts it back where it was with nothing lost. After `open`, moving it back is a new move. | approved |
The offline transitions' refusals (fix round 1, review M4). A refusal that names no value is a whole sentence; the ones that name a value are one format. The no-move, same-region, malformed-start, replica and in-flight refusals reuse the existing rows (MV-N45, MV-N16, MV-N14, MV-N44, MV-N43).
| id | file | text | status |
|---|---|---|---|
-| MV-O34 | internal/provision/offline_move.go | this is not an offline move | pending |
-| MV-O35 | internal/provision/offline_move.go | the record carries a different offline move | pending |
-| MV-O36 | internal/provision/offline_move.go | this offline move is <state>. this step follows <state> | pending |
-| MV-O37 | internal/provision/offline_move.go | a seal names the box that seals and the boxes that stopped the instance. the box that seals is one of them | pending |
-| MV-O38 | internal/provision/offline_move.go | this offline move is sealed by <box> | pending |
-| MV-O39 | internal/provision/offline_move.go | the record's region is <region>, which this step does not expect. the instance changed under the move | pending |
-| MV-O40 | internal/provision/offline_move.go | this offline move is <state>. it cannot be aborted once it is open | pending |
-| MV-O41 | internal/provision/offline_move.go | this offline move is <state>. it finishes from finishing or reverting | pending |
-| MV-O42 | internal/provision/offline_move.go | this is an offline move. use carlos move offline | pending |
-| MV-O43 | internal/provision/offline_move.go | the instance is in <region>, not <from> | pending |
+| MV-O34 | internal/provision/offline_move.go | this is not an offline move | approved |
+| MV-O35 | internal/provision/offline_move.go | the record carries a different offline move | approved |
+| MV-O36 | internal/provision/offline_move.go | this offline move is <state>. this step follows <state> | approved |
+| MV-O37 | internal/provision/offline_move.go | a seal names the box that seals and the boxes that stopped the instance. the box that seals is one of them | approved |
+| MV-O38 | internal/provision/offline_move.go | this offline move is sealed by <box> | approved |
+| MV-O39 | internal/provision/offline_move.go | the record's region is <region>, which this step does not expect. the instance changed under the move | approved |
+| MV-O40 | internal/provision/offline_move.go | this offline move is <state>. it cannot be aborted once it is open | approved |
+| MV-O41 | internal/provision/offline_move.go | this offline move is <state>. it finishes from finishing or reverting | approved |
+| MV-O42 | internal/provision/offline_move.go | this is an offline move. use carlos move offline | approved |
+| MV-O43 | internal/provision/offline_move.go | the instance is in <region>, not <from> | approved |
Two more refusals of the console doors (Task 6), for cases MV-O20 to MV-O31 do not cover: the start door's class rail for a steered or unit-backed instance, and its rule that the region the instance moves to must not already list the host.
| id | file | text | status |
|---|---|---|---|
-| MV-O50 | internal/console/apiinstancemove_offline.go | <host> is steered or runs under a unit. offline moves cannot carry either yet | pending |
-| MV-O51 | internal/console/apiinstancemove_offline.go | <box> already serves <host>. an offline move needs the region it moves to to be empty of it | pending |
+| MV-O50 | internal/console/apiinstancemove_offline.go | <host> is steered or runs under a unit. offline moves cannot carry either yet | approved |
+| MV-O51 | internal/console/apiinstancemove_offline.go | <box> already serves <host>. an offline move needs the region it moves to to be empty of it | approved |
Seven more lines of `carlos move offline` (`move.go`, Task 7), for places MV-O1 to MV-O10 do not reach: what `abort` says once every box has put the instance back (the plan's runbook has the operator run `finish` next, and no row said so), the lines `status` prints for each box and for each manifest item (`<status>` is ok, waiting, failed or unchecked; `<detail>` and `<phase>` are box diagnostics, not copy; an item with no detail uses the shorter line), the usage line, and the line `status` prints for a path a source box could not park and left where it was (MV-O58; `<reason>` is the box's own reason, not copy). A box whose report is not current says so: `flip` and `open` refuse on a stale report, so `status` has to show it. MV-O56 follows MV-O57 in the table only by number.
| id | file | text | status |
|---|---|---|---|
-| MV-O52 | move.go | Every box has put <host> back in <from>. Run carlos move offline finish --host <host> to end the move. | pending |
-| MV-O53 | move.go | <box> is <phase> as the <role>. | pending |
-| MV-O54 | move.go | <box> is <phase> as the <role>. Its report is not current. | pending |
-| MV-O55 | move.go | Item <id> is <status>: <detail>. | pending |
-| MV-O57 | move.go | Item <id> is <status>. | pending |
-| MV-O56 | move.go | carlos move offline needs start, seal, flip, open, finish, abort or status. | pending |
-| MV-O58 | move.go | <box> left <path> in place: <reason>. | pending |
-| MV-O65 | move.go | <box> could not clean up <host>: <reason>. Run carlos move offline status --host <host> for each item. | pending |
+| MV-O52 | move.go | Every box has put <host> back in <from>. Run carlos move offline finish --host <host> to end the move. | approved |
+| MV-O53 | move.go | <box> is <phase> as the <role>. | approved |
+| MV-O54 | move.go | <box> is <phase> as the <role>. Its report is not current. | approved |
+| MV-O55 | move.go | Item <id> is <status>: <detail>. | approved |
+| MV-O57 | move.go | Item <id> is <status>. | approved |
+| MV-O56 | move.go | carlos move offline needs start, seal, flip, open, finish, abort or status. | approved |
+| MV-O58 | move.go | <box> left <path> in place: <reason>. | approved |
+| MV-O65 | move.go | <box> could not clean up <host>: <reason>. Run carlos move offline status --host <host> for each item. | approved |
The source box's reasons for a path it left in place (`internal/host/movesoffline.go`, `retire`), which `carlos move offline status` shows as MV-O58's `<reason>`. They are human text the box writes into its parked report. The two that end in the box's own OS error (`<error>`, a diagnostic, not copy) are recorded for their fixed beginning.
| id | file | text | status |
|---|---|---|---|
-| MV-O59 | internal/host/movesoffline.go | root-owned: listed for the census, not renamed | pending |
-| MV-O60 | internal/host/movesoffline.go | the mount view of the instance directory: it stays, empty | pending |
-| MV-O61 | internal/host/movesoffline.go | the instance directory stays, empty | pending |
-| MV-O62 | internal/host/movesoffline.go | not renamed yet: the stop could not be confirmed this pass | pending |
-| MV-O63 | internal/host/movesoffline.go | cannot be renamed: <error> | pending |
-| MV-O64 | internal/host/movesoffline.go | not renamed yet: <error> | pending |
+| MV-O59 | internal/host/movesoffline.go | root-owned: listed for the census, not renamed | approved |
+| MV-O60 | internal/host/movesoffline.go | the mount view of the instance directory: it stays, empty | approved |
+| MV-O61 | internal/host/movesoffline.go | the instance directory stays, empty | approved |
+| MV-O62 | internal/host/movesoffline.go | not renamed yet: the stop could not be confirmed this pass | approved |
+| MV-O63 | internal/host/movesoffline.go | cannot be renamed: <error> | approved |
+| MV-O64 | internal/host/movesoffline.go | not renamed yet: <error> | approved |
The sealer's lost-stop page (`internal/host/movesoffline_seal.go`, Task 4 fix round 1, review I2 and M2). After the seal the stop is kept, never taken again; when it no longer holds, the sealer pages an operator once per move. The body splits by state: before `open`, `abort` puts the instance back with nothing lost; from `open` on, `abort` is refused (MV-O40) and the move is fixed forward by hand. `<state>` is the record's offline state (`offline-sealed`, `offline-open` and so on). `<cause>` is one of the two causes below, and the failed item's reason (MV-O71) carries the same cause. `<lease>` is one of the four lease states MV-O72 to MV-O75; `<owner>` is a lease owner (`moved:<id>` or a box's lease id) and `<time>` an RFC 3339 time.
| id | file | text | status |
|---|---|---|---|
-| MV-O66 | internal/host/movesoffline_seal.go | offline move of <host> lost its stop | pending |
-| MV-O67 | internal/host/movesoffline_seal.go | The offline move <id> of <host> is <state>, and its stop no longer holds: <cause>. The move cannot go on. Read carlos move offline status --host <host> and abort the move. | pending |
-| MV-O68 | internal/host/movesoffline_seal.go | The offline move <id> of <host> is <state>, and its stop no longer holds: <cause>. It cannot be aborted once it is open. Read carlos move offline status --host <host> and follow the offline move runbook. | pending |
-| MV-O69 | internal/host/movesoffline_seal.go | the source's lease is <lease>, not this move's tombstone | pending |
-| MV-O70 | internal/host/movesoffline_seal.go | <box> reports TXID <n>, past the sealed position <final> | pending |
-| MV-O71 | internal/host/movesoffline_seal.go | lost stop: <cause> | pending |
-| MV-O72 | internal/host/movesoffline_seal.go | absent | pending |
-| MV-O73 | internal/host/movesoffline_seal.go | lifted (<owner>) | pending |
-| MV-O74 | internal/host/movesoffline_seal.go | expired (<owner> at <time>) | pending |
-| MV-O75 | internal/host/movesoffline_seal.go | held by <owner> until <time> | pending |
+| MV-O66 | internal/host/movesoffline_seal.go | offline move of <host> lost its stop | approved |
+| MV-O67 | internal/host/movesoffline_seal.go | The offline move <id> of <host> is <state>, and its stop no longer holds: <cause>. The move cannot go on. Read carlos move offline status --host <host> and abort the move. | approved |
+| MV-O68 | internal/host/movesoffline_seal.go | The offline move <id> of <host> is <state>, and its stop no longer holds: <cause>. It cannot be aborted once it is open. Read carlos move offline status --host <host> and follow the offline move runbook. | approved |
+| MV-O69 | internal/host/movesoffline_seal.go | the source's lease is <lease>, not this move's tombstone | approved |
+| MV-O70 | internal/host/movesoffline_seal.go | <box> reports TXID <n>, past the sealed position <final> | approved |
+| MV-O71 | internal/host/movesoffline_seal.go | lost stop: <cause> | approved |
+| MV-O72 | internal/host/movesoffline_seal.go | absent | approved |
+| MV-O73 | internal/host/movesoffline_seal.go | lifted (<owner>) | approved |
+| MV-O74 | internal/host/movesoffline_seal.go | expired (<owner> at <time>) | approved |
+| MV-O75 | internal/host/movesoffline_seal.go | held by <owner> until <time> | approved |
The sealer's item reasons, which `carlos move offline status` shows as MV-O55's `<detail>`. The copy gate reads every Check reason, so each has a row. MV-O76 is the `files` refusal for an entry the app keeps beside its files directory (review I3): the move cannot carry it, and moving it into the files directory or removing it would lose the app's data, so the row offers neither and points to the runbook; `<paths>` are the entries' paths on the box. MV-O77 to MV-O84 are the other refusals. Two refusals carry an error's own text and are box diagnostics, not rows (`movesDiagnosticWrites`): the archive's own refusal (its sentinels are MV-N77 to MV-N81 and MV-N86) and a secrets key that does not decode. MV-O85 to MV-O98 are the reasons of the items that passed; `<rung>` is the replication rung, quoted.
| id | file | text | status |
|---|---|---|---|
-| MV-O76 | internal/host/movesoffline_seal.go | <host> keeps <paths> beside its files directory, and this move can carry only the files directory. see the offline move runbook | pending |
-| MV-O77 | internal/host/movesoffline_seal.go | <box> keeps a sidecar file for <host> | pending |
-| MV-O78 | internal/host/movesoffline_seal.go | this box's local position <n> is past the replica's <replica> and there is no database at <path> to checkpoint | pending |
-| MV-O79 | internal/host/movesoffline_seal.go | the replica at <bucket>/<prefix> holds no database | pending |
-| MV-O80 | internal/host/movesoffline_seal.go | <box>'s local position <n> is past the replica's final position <final> | pending |
-| MV-O81 | internal/host/movesoffline_seal.go | this box has no exec backend to compose the launch environment | pending |
-| MV-O82 | internal/host/movesoffline_seal.go | this box holds no CARLOS_SECRETS_KEY to key the env report | pending |
-| MV-O83 | internal/host/movesoffline_seal.go | no build is live for <host> on this box | pending |
-| MV-O84 | internal/host/movesoffline_seal.go | this box holds no CARLOS_SECRETS_KEY to seal the archive under | pending |
-| MV-O85 | internal/host/movesoffline_seal.go | no source box keeps a sidecar file | pending |
-| MV-O86 | internal/host/movesoffline_seal.go | every source box is at or below TXID <final> | pending |
-| MV-O87 | internal/host/movesoffline_seal.go | <owner> holds <location>/lock.json | pending |
-| MV-O88 | internal/host/movesoffline_seal.go | the request record is in the build state the replica prefix carries | pending |
-| MV-O89 | internal/host/movesoffline_seal.go | this box keeps no request records; the build state travels as the replica prefix holds it | pending |
-| MV-O90 | internal/host/movesoffline_seal.go | <n> objects staged at <bucket>/<prefix>, newest TXID <final> | pending |
-| MV-O91 | internal/host/movesoffline_seal.go | <n> files, <bytes> bytes, sealed at <prefix><object> | pending |
-| MV-O92 | internal/host/movesoffline_seal.go | <n> active timers at <prefix><object> | pending |
-| MV-O93 | internal/host/movesoffline_seal.go | absent on the source, so none is carried | pending |
-| MV-O94 | internal/host/movesoffline_seal.go | carried in the archive | pending |
-| MV-O95 | internal/host/movesoffline_seal.go | launch environment digest <digest> | pending |
-| MV-O96 | internal/host/movesoffline_seal.go | live build <version> | pending |
-| MV-O97 | internal/host/movesoffline_seal.go | replication "<rung>" | pending |
-| MV-O98 | internal/host/movesoffline_seal.go | next due <unix time> | pending |
+| MV-O76 | internal/host/movesoffline_seal.go | <host> keeps <paths> beside its files directory, and this move can carry only the files directory. see the offline move runbook | approved |
+| MV-O77 | internal/host/movesoffline_seal.go | <box> keeps a sidecar file for <host> | approved |
+| MV-O78 | internal/host/movesoffline_seal.go | this box's local position <n> is past the replica's <replica> and there is no database at <path> to checkpoint | approved |
+| MV-O79 | internal/host/movesoffline_seal.go | the replica at <bucket>/<prefix> holds no database | approved |
+| MV-O80 | internal/host/movesoffline_seal.go | <box>'s local position <n> is past the replica's final position <final> | approved |
+| MV-O81 | internal/host/movesoffline_seal.go | this box has no exec backend to compose the launch environment | approved |
+| MV-O82 | internal/host/movesoffline_seal.go | this box holds no CARLOS_SECRETS_KEY to key the env report | approved |
+| MV-O83 | internal/host/movesoffline_seal.go | no build is live for <host> on this box | approved |
+| MV-O84 | internal/host/movesoffline_seal.go | this box holds no CARLOS_SECRETS_KEY to seal the archive under | approved |
+| MV-O85 | internal/host/movesoffline_seal.go | no source box keeps a sidecar file | approved |
+| MV-O86 | internal/host/movesoffline_seal.go | every source box is at or below TXID <final> | approved |
+| MV-O87 | internal/host/movesoffline_seal.go | <owner> holds <location>/lock.json | approved |
+| MV-O88 | internal/host/movesoffline_seal.go | the request record is in the build state the replica prefix carries | approved |
+| MV-O89 | internal/host/movesoffline_seal.go | this box keeps no request records; the build state travels as the replica prefix holds it | approved |
+| MV-O90 | internal/host/movesoffline_seal.go | <n> objects staged at <bucket>/<prefix>, newest TXID <final> | approved |
+| MV-O91 | internal/host/movesoffline_seal.go | <n> files, <bytes> bytes, sealed at <prefix><object> | approved |
+| MV-O92 | internal/host/movesoffline_seal.go | <n> active timers at <prefix><object> | approved |
+| MV-O93 | internal/host/movesoffline_seal.go | absent on the source, so none is carried | approved |
+| MV-O94 | internal/host/movesoffline_seal.go | carried in the archive | approved |
+| MV-O95 | internal/host/movesoffline_seal.go | launch environment digest <digest> | approved |
+| MV-O96 | internal/host/movesoffline_seal.go | live build <version> | approved |
+| MV-O97 | internal/host/movesoffline_seal.go | replication "<rung>" | approved |
+| MV-O98 | internal/host/movesoffline_seal.go | next due <unix time> | approved |
The landing's item reasons (`internal/host/movesoffline_land.go`, Task 5 fix round 1), which `carlos move offline status` shows as MV-O55's `<detail>` for the target's items. The copy gate reads every Check reason, so each has a row. A reason never carries an error's text: the error behind a reason that waits or fails is the box's log line. `<names>` is the list of variables a failed env pin finds different, each as `<name> (<kind>)` from `moves.CompareEnv`, never a value. MV-O110 to MV-O177. MV-O177 is the `release` item's failure when the target box keeps a live link for the host that names another build than the sealed one (final review, return moves).
| id | file | text | status |
|---|---|---|---|
-| MV-O110 | internal/host/movesoffline_land.go | a stale tombstone in the target's bucket could not be lifted | pending |
-| MV-O111 | internal/host/movesoffline_land.go | a writer holds the target's lock | pending |
-| MV-O112 | internal/host/movesoffline_land.go | after extraction the files are not the seal's | pending |
-| MV-O113 | internal/host/movesoffline_land.go | next due could not be set | pending |
-| MV-O114 | internal/host/movesoffline_land.go | next due reads back differently from the seal's | pending |
-| MV-O115 | internal/host/movesoffline_land.go | no registry on this box | pending |
-| MV-O116 | internal/host/movesoffline_land.go | no row yet: reconcile mints it held | pending |
-| MV-O117 | internal/host/movesoffline_land.go | the archive could not be extracted | pending |
-| MV-O118 | internal/host/movesoffline_land.go | the archive could not be read | pending |
-| MV-O119 | internal/host/movesoffline_land.go | the archive does not verify | pending |
-| MV-O120 | internal/host/movesoffline_land.go | the archive is not the one the seal names | pending |
-| MV-O121 | internal/host/movesoffline_land.go | the channel could not be resolved | pending |
-| MV-O122 | internal/host/movesoffline_land.go | the channel points at another build than the sealed one | pending |
-| MV-O123 | internal/host/movesoffline_land.go | the config ack could not be read | pending |
-| MV-O124 | internal/host/movesoffline_land.go | the config ack does not name this move and the rendered file | pending |
-| MV-O125 | internal/host/movesoffline_land.go | the config could not be rendered | pending |
-| MV-O126 | internal/host/movesoffline_land.go | the config file could not be read | pending |
-| MV-O127 | internal/host/movesoffline_land.go | the config file does not hold the rendered bytes | pending |
-| MV-O128 | internal/host/movesoffline_land.go | the config was rendered at another path | pending |
-| MV-O129 | internal/host/movesoffline_land.go | the env file could not be read | pending |
-| MV-O130 | internal/host/movesoffline_land.go | the env file is missing | pending |
-| MV-O131 | internal/host/movesoffline_land.go | the env file is not the source's | pending |
-| MV-O132 | internal/host/movesoffline_land.go | the env file the target held could not be parked | pending |
-| MV-O133 | internal/host/movesoffline_land.go | the files directory the target held could not be parked | pending |
-| MV-O134 | internal/host/movesoffline_land.go | the instance key is not the source's | pending |
-| MV-O135 | internal/host/movesoffline_land.go | the launch environment could not be composed | pending |
-| MV-O136 | internal/host/movesoffline_land.go | the launch environment differs from the source's | pending |
-| MV-O137 | internal/host/movesoffline_land.go | the launch environment differs from the source's in <names> | pending |
-| MV-O138 | internal/host/movesoffline_land.go | the record and the seal disagree on this item | pending |
-| MV-O139 | internal/host/movesoffline_land.go | the record carries no seal evidence | pending |
-| MV-O140 | internal/host/movesoffline_land.go | the replication rung could not be set | pending |
-| MV-O141 | internal/host/movesoffline_land.go | the replication rung reads back differently from the seal's | pending |
-| MV-O142 | internal/host/movesoffline_land.go | the row is held by another operation | pending |
-| MV-O143 | internal/host/movesoffline_land.go | the row is not held landing by this move yet | pending |
-| MV-O144 | internal/host/movesoffline_land.go | the row's paths are nested, and an offline move lands flat rows only | pending |
-| MV-O145 | internal/host/movesoffline_land.go | the seal could not be read | pending |
-| MV-O146 | internal/host/movesoffline_land.go | the seal does not decode | pending |
-| MV-O147 | internal/host/movesoffline_land.go | the seal is missing | pending |
-| MV-O148 | internal/host/movesoffline_land.go | the seal is not a valid seal of this move | pending |
-| MV-O149 | internal/host/movesoffline_land.go | the seal is not the one the record names | pending |
-| MV-O150 | internal/host/movesoffline_land.go | the seal staged nothing | pending |
-| MV-O151 | internal/host/movesoffline_land.go | the staged replica could not be listed | pending |
-| MV-O152 | internal/host/movesoffline_land.go | the staged replica could not be verified this pass | pending |
-| MV-O153 | internal/host/movesoffline_land.go | the staged replica does not end at the sealed position | pending |
-| MV-O154 | internal/host/movesoffline_land.go | the staged replica does not restore clean | pending |
-| MV-O155 | internal/host/movesoffline_land.go | the staged replica is not the one the seal staged | pending |
-| MV-O156 | internal/host/movesoffline_land.go | the target's lock could not be read | pending |
-| MV-O157 | internal/host/movesoffline_land.go | the target's lock is still held after the lift | pending |
-| MV-O158 | internal/host/movesoffline_land.go | the target's lock is this move's own tombstone | pending |
-| MV-O159 | internal/host/movesoffline_land.go | the timers could not be read | pending |
-| MV-O160 | internal/host/movesoffline_land.go | the timers could not be replaced | pending |
-| MV-O161 | internal/host/movesoffline_land.go | the timers read back differently from the seal's | pending |
-| MV-O162 | internal/host/movesoffline_land.go | the timers snapshot could not be read | pending |
-| MV-O163 | internal/host/movesoffline_land.go | the timers snapshot is not the one the seal names | pending |
-| MV-O164 | internal/host/movesoffline_land.go | this box cannot resolve channels | pending |
-| MV-O165 | internal/host/movesoffline_land.go | this box has no exec backend to compose the launch environment | pending |
-| MV-O166 | internal/host/movesoffline_land.go | this box has no held render | pending |
-| MV-O167 | internal/host/movesoffline_land.go | this box holds no CARLOS_SECRETS_KEY to key the env report | pending |
-| MV-O168 | internal/host/movesoffline_land.go | this box holds no CARLOS_SECRETS_KEY to open the archive with | pending |
-| MV-O169 | internal/host/movesoffline_land.go | waits for the config item: the instance key is read from both files | pending |
-| MV-O170 | internal/host/movesoffline_land.go | waits for the files and config items | pending |
-| MV-O171 | internal/host/movesoffline_land.go | waits for the files item | pending |
-| MV-O172 | internal/host/movesoffline_land.go | a path the target held for the host could not be parked | pending |
-| MV-O173 | internal/host/movesoffline_land.go | a path the target held for the host is still there | pending |
-| MV-O174 | internal/host/movesoffline_land.go | not checked: the row is not held for this move | pending |
-| MV-O175 | internal/host/movesoffline_land.go | not checked: the seal could not be used | pending |
-| MV-O176 | internal/host/movesoffline_land.go | the row could not be read back | pending |
-| MV-O177 | internal/host/movesoffline_land.go | the target box still holds an older build of this instance. see the offline move runbook | pending |
+| MV-O110 | internal/host/movesoffline_land.go | a stale tombstone in the target's bucket could not be lifted | approved |
+| MV-O111 | internal/host/movesoffline_land.go | a writer holds the target's lock | approved |
+| MV-O112 | internal/host/movesoffline_land.go | after extraction the files are not the seal's | approved |
+| MV-O113 | internal/host/movesoffline_land.go | next due could not be set | approved |
+| MV-O114 | internal/host/movesoffline_land.go | next due reads back differently from the seal's | approved |
+| MV-O115 | internal/host/movesoffline_land.go | no registry on this box | approved |
+| MV-O116 | internal/host/movesoffline_land.go | no row yet: reconcile mints it held | approved |
+| MV-O117 | internal/host/movesoffline_land.go | the archive could not be extracted | approved |
+| MV-O118 | internal/host/movesoffline_land.go | the archive could not be read | approved |
+| MV-O119 | internal/host/movesoffline_land.go | the archive does not verify | approved |
+| MV-O120 | internal/host/movesoffline_land.go | the archive is not the one the seal names | approved |
+| MV-O121 | internal/host/movesoffline_land.go | the channel could not be resolved | approved |
+| MV-O122 | internal/host/movesoffline_land.go | the channel points at another build than the sealed one | approved |
+| MV-O123 | internal/host/movesoffline_land.go | the config ack could not be read | approved |
+| MV-O124 | internal/host/movesoffline_land.go | the config ack does not name this move and the rendered file | approved |
+| MV-O125 | internal/host/movesoffline_land.go | the config could not be rendered | approved |
+| MV-O126 | internal/host/movesoffline_land.go | the config file could not be read | approved |
+| MV-O127 | internal/host/movesoffline_land.go | the config file does not hold the rendered bytes | approved |
+| MV-O128 | internal/host/movesoffline_land.go | the config was rendered at another path | approved |
+| MV-O129 | internal/host/movesoffline_land.go | the env file could not be read | approved |
+| MV-O130 | internal/host/movesoffline_land.go | the env file is missing | approved |
+| MV-O131 | internal/host/movesoffline_land.go | the env file is not the source's | approved |
+| MV-O132 | internal/host/movesoffline_land.go | the env file the target held could not be parked | approved |
+| MV-O133 | internal/host/movesoffline_land.go | the files directory the target held could not be parked | approved |
+| MV-O134 | internal/host/movesoffline_land.go | the instance key is not the source's | approved |
+| MV-O135 | internal/host/movesoffline_land.go | the launch environment could not be composed | approved |
+| MV-O136 | internal/host/movesoffline_land.go | the launch environment differs from the source's | approved |
+| MV-O137 | internal/host/movesoffline_land.go | the launch environment differs from the source's in <names> | approved |
+| MV-O138 | internal/host/movesoffline_land.go | the record and the seal disagree on this item | approved |
+| MV-O139 | internal/host/movesoffline_land.go | the record carries no seal evidence | approved |
+| MV-O140 | internal/host/movesoffline_land.go | the replication rung could not be set | approved |
+| MV-O141 | internal/host/movesoffline_land.go | the replication rung reads back differently from the seal's | approved |
+| MV-O142 | internal/host/movesoffline_land.go | the row is held by another operation | approved |
+| MV-O143 | internal/host/movesoffline_land.go | the row is not held landing by this move yet | approved |
+| MV-O144 | internal/host/movesoffline_land.go | the row's paths are nested, and an offline move lands flat rows only | approved |
+| MV-O145 | internal/host/movesoffline_land.go | the seal could not be read | approved |
+| MV-O146 | internal/host/movesoffline_land.go | the seal does not decode | approved |
+| MV-O147 | internal/host/movesoffline_land.go | the seal is missing | approved |
+| MV-O148 | internal/host/movesoffline_land.go | the seal is not a valid seal of this move | approved |
+| MV-O149 | internal/host/movesoffline_land.go | the seal is not the one the record names | approved |
+| MV-O150 | internal/host/movesoffline_land.go | the seal staged nothing | approved |
+| MV-O151 | internal/host/movesoffline_land.go | the staged replica could not be listed | approved |
+| MV-O152 | internal/host/movesoffline_land.go | the staged replica could not be verified this pass | approved |
+| MV-O153 | internal/host/movesoffline_land.go | the staged replica does not end at the sealed position | approved |
+| MV-O154 | internal/host/movesoffline_land.go | the staged replica does not restore clean | approved |
+| MV-O155 | internal/host/movesoffline_land.go | the staged replica is not the one the seal staged | approved |
+| MV-O156 | internal/host/movesoffline_land.go | the target's lock could not be read | approved |
+| MV-O157 | internal/host/movesoffline_land.go | the target's lock is still held after the lift | approved |
+| MV-O158 | internal/host/movesoffline_land.go | the target's lock is this move's own tombstone | approved |
+| MV-O159 | internal/host/movesoffline_land.go | the timers could not be read | approved |
+| MV-O160 | internal/host/movesoffline_land.go | the timers could not be replaced | approved |
+| MV-O161 | internal/host/movesoffline_land.go | the timers read back differently from the seal's | approved |
+| MV-O162 | internal/host/movesoffline_land.go | the timers snapshot could not be read | approved |
+| MV-O163 | internal/host/movesoffline_land.go | the timers snapshot is not the one the seal names | approved |
+| MV-O164 | internal/host/movesoffline_land.go | this box cannot resolve channels | approved |
+| MV-O165 | internal/host/movesoffline_land.go | this box has no exec backend to compose the launch environment | approved |
+| MV-O166 | internal/host/movesoffline_land.go | this box has no held render | approved |
+| MV-O167 | internal/host/movesoffline_land.go | this box holds no CARLOS_SECRETS_KEY to key the env report | approved |
+| MV-O168 | internal/host/movesoffline_land.go | this box holds no CARLOS_SECRETS_KEY to open the archive with | approved |
+| MV-O169 | internal/host/movesoffline_land.go | waits for the config item: the instance key is read from both files | approved |
+| MV-O170 | internal/host/movesoffline_land.go | waits for the files and config items | approved |
+| MV-O171 | internal/host/movesoffline_land.go | waits for the files item | approved |
+| MV-O172 | internal/host/movesoffline_land.go | a path the target held for the host could not be parked | approved |
+| MV-O173 | internal/host/movesoffline_land.go | a path the target held for the host is still there | approved |
+| MV-O174 | internal/host/movesoffline_land.go | not checked: the row is not held for this move | approved |
+| MV-O175 | internal/host/movesoffline_land.go | not checked: the seal could not be used | approved |
+| MV-O176 | internal/host/movesoffline_land.go | the row could not be read back | approved |
+| MV-O177 | internal/host/movesoffline_land.go | the target box still holds an older build of this instance. see the offline move runbook | approved |
diff --git a/internal/provision/offline_move.go b/internal/provision/offline_move.go
index 94b257b27..2af9aa355 100644
--- a/internal/provision/offline_move.go
+++ b/internal/provision/offline_move.go
@@ -70,9 +70,6 @@ const (
// refusedOfflineSeal: a seal that names no sealer, no boxes, or a sealer
// that is not one of them (MV-O37).
refusedOfflineSeal = "a seal names the box that seals and the boxes that stopped the instance. the box that seals is one of them"
- // refusedOfflineAfterOpen is OfflineAbort past the point where the target
- // serves (MV-O40 is the format: this offline move is <state>. ...).
- refusedOfflineAfterOpen = "it cannot be aborted once it is open"
)
// errRecordUnchanged is what a transition returns to say "the record already
@@ -95,7 +92,7 @@ func offlineAt(rec *Record, id, from string) error {
return err
}
if rec.Move.State != from {
- return fmt.Errorf("%w: this offline move is %s. this step follows %s", ErrMoveConflict, rec.Move.State, from)
+ return fmt.Errorf("%w: %s", ErrMoveConflict, offlineWrongState(rec.Move.State, from))
}
return nil
}
@@ -138,7 +135,7 @@ func OfflineStart(id, from, to, by string, now time.Time) func(*Record) error {
return fmt.Errorf("%w: the instance is already in %s", ErrMoveConflict, regionWord(rec.Region))
}
if from != rec.Region {
- return fmt.Errorf("%w: the instance is in %s, not %s", ErrMoveConflict, regionWord(rec.Region), regionWord(from))
+ return fmt.Errorf("%w: %s", ErrMoveConflict, offlineNotFrom(rec.Region, from))
}
if rec.Role != "" {
return fmt.Errorf("%w: a %s does not move. delete it and create one in the new region", ErrMoveConflict, rec.Role)
@@ -192,7 +189,7 @@ func OfflineSealedBy(id, sealer string, src MoveSource) func(*Record) error {
return err
}
if sealer == "" || rec.Move.Sealer != sealer {
- return fmt.Errorf("%w: this offline move is sealed by %s", ErrMoveConflict, rec.Move.Sealer)
+ return fmt.Errorf("%w: %s", ErrMoveConflict, offlineSealedBy(rec.Move.Sealer))
}
s := src
if src.Files != nil {
@@ -292,7 +289,7 @@ func OfflineAbort(id string) func(*Record) error {
case OfflineLanding, OfflineLanded:
rec.Region = rec.Move.From
default:
- return fmt.Errorf("%w: this offline move is %s. %s", ErrMoveConflict, rec.Move.State, refusedOfflineAfterOpen)
+ return fmt.Errorf("%w: %s", ErrMoveConflict, offlineAfterOpen(rec.Move.State))
}
rec.Move.State = OfflineReverting
return nil
@@ -327,7 +324,7 @@ func OfflineFinish(id string, now time.Time) func(*Record) error {
}
out.Outcome = OfflineOutcomeAborted
default:
- return fmt.Errorf("%w: this offline move is %s. it finishes from finishing or reverting", ErrMoveConflict, m.State)
+ return fmt.Errorf("%w: %s", ErrMoveConflict, offlineFinishState(m.State))
}
if s := end.Sub(m.RequestedAt).Seconds(); s > 0 {
out.OfflineS = s
@@ -337,6 +334,31 @@ func OfflineFinish(id string, now time.Time) func(*Record) error {
}
}
+// offlineWrongState is MV-O36.
+func offlineWrongState(state, from string) string {
+ return fmt.Sprintf("this offline move is %s. this step follows %s", state, from)
+}
+
+// offlineSealedBy is MV-O38.
+func offlineSealedBy(box string) string {
+ return fmt.Sprintf("this offline move is sealed by %s", box)
+}
+
+// offlineAfterOpen is MV-O40.
+func offlineAfterOpen(state string) string {
+ return fmt.Sprintf("this offline move is %s. it cannot be aborted once it is open", state)
+}
+
+// offlineFinishState is MV-O41.
+func offlineFinishState(state string) string {
+ return fmt.Sprintf("this offline move is %s. it finishes from finishing or reverting", state)
+}
+
+// offlineNotFrom is MV-O43.
+func offlineNotFrom(region, from string) string {
+ return fmt.Sprintf("the instance is in %s, not %s", regionWord(region), regionWord(from))
+}
+
// offlineRegionMoved is the refusal for a record whose region is not where
// the step expects it (MV-O39): something wrote the region under the move.
func offlineRegionMoved(region string) string {
diff --git a/internal/provision/offline_move_copy_test.go b/internal/provision/offline_move_copy_test.go
new file mode 100644
index 000000000..a277457ee
--- /dev/null
+++ b/internal/provision/offline_move_copy_test.go
@@ -0,0 +1,91 @@
+package provision
+
+import (
+ "context"
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// TestOfflineHelperRefusalsMatchTheCopyRecord pins the five transition
+// refusals that live in fmt.Sprintf helpers (MV-O36, MV-O38, MV-O40, MV-O41,
+// MV-O43) to the approved text in the copy record, byte for byte, with each
+// placeholder filled with the value the transition supplies. Each refusal is
+// driven through its real transition and still wraps ErrMoveConflict.
+// (census-ruling-moves-offline, condition 2.)
+func TestOfflineHelperRefusalsMatchTheCopyRecord(t *testing.T) {
+ approved := offlineApprovedCopy(t)
+ now := time.Date(2026, 10, 21, 9, 0, 0, 0, time.UTC)
+ steps := offlineSteps(now)
+ ctx := context.Background()
+
+ for _, c := range []struct {
+ id string
+ walked int
+ mut func(*Record) error
+ fills []string // in the order the placeholders appear
+ }{
+ {"MV-O36", 1, OfflineFlip(offID), []string{OfflineStopping, OfflineSealed}},
+ {"MV-O38", 2, OfflineSealedBy(offID, "box-b", MoveSource{}), []string{offSealer}},
+ {"MV-O40", 6, OfflineAbort(offID), []string{OfflineOpen}},
+ {"MV-O41", 1, OfflineFinish(offID, now), []string{OfflineStopping}},
+ {"MV-O43", 0, OfflineStart(offID, "us", "uk", "ops@example.com", now), []string{"home", "us"}},
+ } {
+ t.Run(c.id, func(t *testing.T) {
+ text, ok := approved[c.id]
+ if !ok {
+ t.Fatalf("%s is not an approved row of the copy record", c.id)
+ }
+ want := text
+ for _, v := range c.fills {
+ i := strings.Index(want, "<")
+ j := strings.Index(want, ">")
+ if i < 0 || j < i {
+ t.Fatalf("%s: more fills than placeholders in %q", c.id, text)
+ }
+ want = want[:i] + v + want[j+1:]
+ }
+ if strings.ContainsAny(want, "<>") {
+ t.Fatalf("%s: an unfilled placeholder in %q", c.id, want)
+ }
+
+ s := release.NewDirStore(t.TempDir())
+ rec := offlineRecord(t, s)
+ offlineWalk(t, s, rec, steps[:c.walked])
+ _, err := SetRecordMove(ctx, s, rec.Account, rec.App, rec.Host, c.mut)
+ if !errors.Is(err, ErrMoveConflict) {
+ t.Fatalf("err %v, want ErrMoveConflict", err)
+ }
+ if got, full := err.Error(), ErrMoveConflict.Error()+": "+want; got != full {
+ t.Fatalf("refusal\n got %q\nwant %q", got, full)
+ }
+ })
+ }
+}
+
+// offlineApprovedCopy reads the MV-O rows of the copy record whose status is
+// approved, keyed by id.
+func offlineApprovedCopy(t *testing.T) map[string]string {
+ t.Helper()
+ raw, err := os.ReadFile(filepath.Join(censusRoot(t), "docs/superpowers/plans/2026-10-06-moves-phase1-copy.md"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ rows := map[string]string{}
+ for _, line := range strings.Split(string(raw), "\n") {
+ if !strings.HasPrefix(line, "| MV-O") {
+ continue
+ }
+ cells := strings.Split(strings.Trim(line, "|"), " | ")
+ if len(cells) != 4 || strings.TrimSpace(cells[3]) != "approved" {
+ continue
+ }
+ rows[strings.TrimSpace(cells[0])] = cells[2]
+ }
+ return rows
+}
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go
index b873d2714..9ff2cb52b 100644
--- a/internal/provision/plan14_root_reads_test.go
+++ b/internal/provision/plan14_root_reads_test.go
@@ -966,10 +966,12 @@ func TestPlan14RootReadCensus(t *testing.T) {
// disposition and accepts landRun.liveNotSealed as legacy, on the
// condition that it compares the live link's Readlink target string and
// follows nothing beyond the link; 44 frozen declarations in all.
+ // OfflineStart re-pinned for a text-only helper extraction (copy gate),
+ // 2026-10-20; no read, class or behaviour change.
if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-moves-offline.tsv"), &inv, true); err != nil {
t.Fatal(err)
}
- seal = "b14f1723a516d4c95d68b044773b0774f74e7369d22f2d09e8e1c667e7570637"
+ seal = "393e7f456d8f02b2eee0c683880e5f7bf696b6ee0c87f20e5c113434065bec29"
errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...)
errs = append(errs, checkRootReadDeclarations(got, inv)...)
if len(errs) > 0 {
diff --git a/internal/provision/testdata/plan14-root-reads-moves-offline.tsv b/internal/provision/testdata/plan14-root-reads-moves-offline.tsv
index ddfc6dc42..34235884a 100644
--- a/internal/provision/testdata/plan14-root-reads-moves-offline.tsv
+++ b/internal/provision/testdata/plan14-root-reads-moves-offline.tsv
@@ -2,6 +2,7 @@
# Every new root-read unit runs as the agent (carlos-edge.service, user carlos), the console (user carlos-console) or the carlos CLI on a person's machine, and makes no root decision, so each is proposed accepted legacy on census-ruling-carlos-run's footing, at the disposition main gives its moves siblings (J1 root-route for registry and route reads, J4 root-input for file reads). In a root process the armed moves worker is nil, so reconcileSerializedWith adds no offline hook there.
# Four changed legacy units are re-frozen at main's disposition with the change named in their basis (edgesFileLoop, NewS3Server, archive.walk, mover.roundTrip). Two authority units (fastConvergeLoop B3, armMovesPass B,C) are re-pinned at main's class and task. The accepted-legacy rows add 43 frozen declarations, so the seal moves. Ten source envelopes are re-pinned.
# Extension of the ruling (platform-1c, for 8dc2a544's release item): landRun.release is re-frozen at its disposition with its new digest; landRun.liveNotSealed is accepted legacy on the condition that it compares the live link's Readlink target string and follows nothing beyond the link (44 frozen declarations); the internal/host envelope is re-pinned.
+# OfflineStart and the internal/provision envelope: re-pinned for a text-only helper extraction (copy gate), 2026-10-20; no read, class or behaviour change. OfflineStart is re-frozen at its disposition with its new digest; its site (file:Open@184) is unchanged.
@coverage . 1d5649fef5f08e01c5b974b164e2aae3c5178f281601f6ffcb2df89fb1ab5fc2
@coverage internal/cgroup 59eaf7077de51aad79f5370fd21972e2b8782ffc76f7223f95e3efab4e47c0ed
@coverage internal/console f500b73a74639435526caee7592de07ce79936a3b09870203c379dc70255c999
@@ -11,7 +12,7 @@
@coverage internal/lease/leasetest 36f174cae2871a12582d804ac8887ca43ae51ad4421bd645eaabbbac4c5387fd
@coverage internal/moves 3c7de98d4a1783d542039d050b0ebee0c159f416b89cb3cae2461d7560b7867b
@coverage internal/moves/archive 5ca2e60aedefb3ef43fb7a334e2e8c659e3ef504ede415a93521e32540e00014
-@coverage internal/provision 560826e175f37f4a7b96b9b6a4871360fec5063cb11a783347b721db7dd49b8a
+@coverage internal/provision 1d6ac220427a88a68786ae5fc59fe5d053394ffa4e3918db7075ace230340374
@legacy 2412dae990f2a0de699c37ceab1fbb8c25f23d88d4eb024975e1df6b1dc17b55 internal/host/edgesfile.go edgesFileLoop legacy accepted-legacy accepted-legacy;rev3=census-ruling-moves-offline;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's body (census-ruling-seeded-instances) with offline-move-1 joining the published capabilities each pass while the armed moves worker is capable (withOfflineCapability), and headroom asked of region HA's capability alone; read site unchanged, offset moved; a report, not a decision file:Read@2741 a764d80b75c3cf046ba499363f278c3411c5cb0c2b528322525e4dce292fb8b5
internal/host/edgesfile.go edgesFileLoop legacy accepted-legacy accepted-legacy;rev3=census-ruling-moves-offline;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's body (census-ruling-seeded-instances) with offline-move-1 joining the published capabilities each pass while the armed moves worker is capable (withOfflineCapability), and headroom asked of region HA's capability alone; read site unchanged, offset moved; a report, not a decision file:Read@2741 a764d80b75c3cf046ba499363f278c3411c5cb0c2b528322525e4dce292fb8b5
@legacy 7d09db1a70c36e9efdd523a370152ab8f938b00f668d312becc634245ed824db internal/lease/leasetest/s3server.go NewS3Server legacy accepted-legacy accepted-legacy;rev3=census-ruling-moves-offline;basis=tests only (internal/lease/leasetest, imported by no production package): main's fixture (census-ruling-FS-R10) with ListObjectsV2 and If-Match on DeleteObject added, as S3 answers them; simulated ETags and conditional writes grant no production root authority file:Query@479 8a28a1d7654bd91cfa9af1396293ec4c872b0f549852240cb3f4a1a4c0a336f0
@@ -98,7 +99,7 @@ internal/host/movespass.go channelVersion legacy accepted-legacy accepted-legacy
internal/layout/plan.go NestedPlatformNames legacy accepted-legacy accepted-legacy;rev3=census-ruling-moves-offline;basis=root-input (J4, as main's moves units); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): new, called only from the agent's offline sealer and source retire (IsNestedPlatformName): a fixed list of names, reads nothing data-literal@84 79a4b08c26c1fa95f6bb6b5a177a9ff50059eb0e68fbe605c0a663047b0557af
@legacy f0bcde3a56332265adccfcb5c693748c6e1c5cb532bfbbe39c945f37127ec1c0 internal/lease/tombstone.go readLock legacy accepted-legacy accepted-legacy;rev3=census-ruling-moves-offline;basis=root-input (J4, as main's moves units); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): new, through lease.Tombstone, Reassert and LiftTombstone (the offline sealer and landing): reads the litestream lock object with its ETag; a garbled or expiry-less lock is an error, never free file:ReadAll@417 19def7e4df0da9f3a14f687447397e4c211e1182a0a76995fef5439181f82a35
internal/lease/tombstone.go readLock legacy accepted-legacy accepted-legacy;rev3=census-ruling-moves-offline;basis=root-input (J4, as main's moves units); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): new, through lease.Tombstone, Reassert and LiftTombstone (the offline sealer and landing): reads the litestream lock object with its ETag; a garbled or expiry-less lock is an error, never free file:ReadAll@417 19def7e4df0da9f3a14f687447397e4c211e1182a0a76995fef5439181f82a35
-@legacy eebee666de3a77f3ed83ece4777057d0220baf8dd6d09e2ac94a3eeb01ef3540 internal/provision/offline_move.go OfflineStart legacy accepted-legacy accepted-legacy;rev3=census-ruling-moves-offline;basis=root-input (J4, as main's moves units); the console (cmd/console serve under console.service, user carlos-console): new, the record transition the start door applies through SetRecordMove; its one site is rec.ActiveRun.Open(), a method, not a file read file:Open@184 2d3cd134dab7d467aad300e58a2b8c5f95fe7c706f3c31d4b55ad2b271a5f0fd
-internal/provision/offline_move.go OfflineStart legacy accepted-legacy accepted-legacy;rev3=census-ruling-moves-offline;basis=root-input (J4, as main's moves units); the console (cmd/console serve under console.service, user carlos-console): new, the record transition the start door applies through SetRecordMove; its one site is rec.ActiveRun.Open(), a method, not a file read file:Open@184 2d3cd134dab7d467aad300e58a2b8c5f95fe7c706f3c31d4b55ad2b271a5f0fd
+@legacy ff49265904909d14a2b264fd7d2b328bed2c151338eb299a4af09f04c5462e0a internal/provision/offline_move.go OfflineStart legacy accepted-legacy accepted-legacy;rev3=census-ruling-moves-offline;basis=root-input (J4, as main's moves units); the console (cmd/console serve under console.service, user carlos-console): new, the record transition the start door applies through SetRecordMove; its one site is rec.ActiveRun.Open(), a method, not a file read file:Open@184 a5097e64c5393c6d1ccea7affbc09b35c1e1c8b084f839658b379dc184aef6e0
+internal/provision/offline_move.go OfflineStart legacy accepted-legacy accepted-legacy;rev3=census-ruling-moves-offline;basis=root-input (J4, as main's moves units); the console (cmd/console serve under console.service, user carlos-console): new, the record transition the start door applies through SetRecordMove; its one site is rec.ActiveRun.Open(), a method, not a file read file:Open@184 a5097e64c5393c6d1ccea7affbc09b35c1e1c8b084f839658b379dc184aef6e0
internal/host/agent.go fastConvergeLoop authority B3 rev3=census-ruling-moves-offline;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's body (census-ruling-carlos-run) with the offline keep view read before the first reconcile pass (refreshOfflineView); read sites unchanged, offsets moved; main's B3 authority retained whole:Route@8896,whole:Route@9459 dac141dfbb9e82bf99cb8e9334418db792af872d72e954e4e43c2dbc7958bbb7
internal/host/movespass.go armMovesPass authority B,C rev3=census-ruling-moves-offline;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's construction with newMovesWorker, the box label and version store set whatever the switch says, and armOffline called after armPhase1; read sites unchanged, offsets moved; main's B,C authority retained file:Read@2973,registry:Provisioned@1749,whole:Route@1736 40c56169ae583a8bb24ffff424090cab82a214c2fbb34520a45284267fc19523