carlos / platform Public

docs: rehearsal runbook fix round 1 (layout check, roster and fence, evidence capture, return lane, run order, customer steps)

- The flat-layout pre-flight reads carlos status --layout (actual
  layout), apart from source_report.s1 after a request.
- The roster is decoded from its signed envelope; the termination fence
  is checked at control/box-fences/<enrollment>.json; Run 4's roster
  checks use both, and the readmit's retirement receipt is checked.
- Every run starts a capture loop before its cut, so the rollback
  journal (dirty, id, loss_ref) and evidence keys are read after close.
- Run 3 checks the return lane moves/<host>/<rb>/ is parked in the
  source bucket after its horizon (R-8).
- Run 5 needs dev Runs 1 to 4 (controller ruling; spec:6294).
- Runs 6 and 7 get their own customer steps, with <account>/<app>, an
  owner-approved health check and no probe-only calls; the detail
  helper and log prefix take ACCT and APP.
Sources updated for each changed line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev bc369e579ca0a014b0395e90e0dd33cebf875f27 parent 54f6933
1 file changed, +185 −55
  • docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md +185 −55
diff --git a/docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md b/docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md
index 4d12a9359..fd13f8294 100644
--- a/docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md
+++ b/docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md
@@ -27,7 +27,8 @@ The Sources section at the end gives file and line for each.
|---|---|
| `<console>` | the deployment's console origin, `https://...`, as `carlos auth login` stored it |
| `<ops>` | the ops account's sqid, as `--account` takes it |
-| `<host>` | the run's rehearsal host, `<label>.<instance domain>` |
+| `<account>`, `<app>` | a customer run's account sqid and app (Runs 6 and 7) |
+| `<host>` | the run's host, `<label>.<instance domain>` |
| `<deploy-bucket>` | the deployment bucket (`CARLOS_DEPLOYMENT_BUCKET` in a box's `/etc/carlos/host.env`) |
| `<data-bucket-R>` | region R's data bucket: `data_bucket` in that region's box edges file |
| `<profile>` | the deployment's named AWS profile. Always name one. There are no default credentials. |
@@ -44,34 +45,73 @@ Each check says where to read it.
recorded); 5 refused; 1 the console could not be reached or read; 2 a
usage error.
- **[detail]**: the operator's detail answer. No `carlos` command prints it
- yet, so read it with your own login token:
+ yet, so read it with your own login token. Set `ACCT` and `APP` for the
+ run: `<ops>` and `moveprobe` for Runs 1 to 5, `<account>` and `<app>`
+ for Runs 6 and 7.
```sh
CONSOLE=<console>
+ ACCT=<ops> APP=moveprobe
TOKEN=$(jq -r --arg c "$CONSOLE" '.credentials[] | select(.console == $c) | .token' ~/.carlos/credentials)
- detail() { curl -fsS -H "Authorization: Bearer $TOKEN" "$CONSOLE/api/cli/apps/<ops>/moveprobe/instances/$1/move?detail=1"; }
+ detail() { curl -fsS -H "Authorization: Bearer $TOKEN" "$CONSOLE/api/cli/apps/$ACCT/$APP/instances/$1/move?detail=1"; }
detail <host> | jq .
```
The flat fields (`state`, `phase`, `id`, `last`) sit at the top. The
- rest is under `detail`: `move` (the record's journal, absent once the
- move closes), `claim`, `target_report`, `source_report`, `evidence`,
- `log` (keys only), `timings` (after the close) and `region`.
+ rest is under `detail`: `move` (the record's journal), `claim`,
+ `target_report`, `source_report`, `evidence`, `log` (keys only),
+ `timings` (after the close) and `region`. **`detail.move` and
+ `detail.evidence` are gone once the move closes**, and a mutation waits
+ until it closes by default. So every run that needs them starts the
+ capture loop below before its cut and reads them from the capture file
+ afterwards.
+- **[capture]**: every detail answer of one move, saved as it happens.
+ Start it in a second terminal once the request has reached ready, before
+ the cut. It stops by itself once no move is in flight:
+
+ ```sh
+ capture() { # capture <host> <file>
+ while :; do
+ if d=$(detail "$1"); then
+ printf '%s\n' "$d" >> "$2"
+ [ "$(printf '%s' "$d" | jq -r .state)" = none ] && break
+ fi
+ sleep 2
+ done
+ }
+ capture <host> run-<n>-detail.jsonl
+ # later: the last rollback journal the move showed
+ jq -s '[.[] | .detail.move.rollback | select(. != null)] | last' run-<n>-detail.jsonl
+ ```
+
- **[deploy]**: the deployment bucket, read with `--profile <profile>`.
Each move keeps its reports, stop evidence and log under
- `moves/<ops>/moveprobe/<host>/<move>/`: `source.json`, `target.json`,
+ `moves/$ACCT/$APP/<host>/<move>/`: `source.json`, `target.json`,
`stopped-source.json`, `stopped-target.json` and `log/<identity>.json`.
- The claim is `control/hosts/<host>.json`, the roster
- `control/host-actors.json`, each box's edges file
+ The claim is `control/hosts/<host>.json`, each box's edges file
`control/edges/<box>.json`, each box's identity record
`control/boxes/<box>.json`, and the floor `control/moves-protocol.json`.
```sh
- P="moves/<ops>/moveprobe/<host>/<move>/"
+ P="moves/$ACCT/$APP/<host>/<move>/"
aws s3 ls --profile <profile> "s3://<deploy-bucket>/${P}log/"
aws s3 cp --profile <profile> "s3://<deploy-bucket>/${P}log/done.json" - | jq .
```
+ The roster, `control/host-actors.json`, is a signed envelope,
+ `{"payload": <base64 JSON>, "sig": ...}`. Decode the payload to read it
+ (this does not check the signature; the console does). A box's
+ termination fence is a separate object,
+ `control/box-fences/<enrollment>.json`, using the enrollment from the
+ roster. No object means no fence:
+
+ ```sh
+ roster() { aws s3 cp --profile <profile> s3://<deploy-bucket>/control/host-actors.json - | jq -r .payload | base64 -d; }
+ roster | jq '.boxes["<box>"] | {state, enrollment, loss_ids, quarantine_revision}'
+ aws s3api head-object --profile <profile> --bucket <deploy-bucket> \
+ --key "control/box-fences/$(roster | jq -r '.boxes["<box>"].enrollment').json" # 404: no fence
+ ```
+
- **[data]**: a region's data bucket, read with `--profile <profile>`. The
instance's replica is under `instances/<host>/` (the box's
`CARLOS_HIBERNATE_PREFIX`, `instances` in production), a move's staging
@@ -153,9 +193,13 @@ deployment before its first run.
`host.env` publishes no `placement` in its edges file: it is a home box.
- [ ] **Rehearsal boxes keep instances flat.** Moves refuse a nested
instance. Set `CARLOS_LAYOUT_DEFAULT=flat` in each rehearsal box's
- `host.env`. Check: `carlos system layout status --console <console>`
- shows no nested rehearsal host, and before each run [detail]
- `detail.source_report.s1.ok` is `true`.
+ `host.env`. Pre-flight check, before any request: `carlos status
+ --layout --console <console>` (the layout each host is actually on,
+ box by box) shows every rehearsal host's row as `flat`, and no
+ rehearsal box is marked "(nested by default)". `carlos system layout
+ status` is not this check: it shows only the recorded intent. Once a
+ run's request exists, S-2 also checks `detail.source_report.s1.ok`,
+ the source's own refusal of a nested instance.
- [ ] **Never set `CARLOS_MOVES_PHASE1=on` anywhere before P-0 is true.**
---
@@ -248,8 +292,9 @@ Paul confirms each of these per deployment before its first run.
the console's environment and every box's `host.env`. Check: [box] on
each box and the console. #250's activation blockers are closed.
- [ ] **Roster.** Each rehearsal box is an active, enrolled entry with no
- termination fence. Check: [deploy] `control/host-actors.json`
- `boxes.<box>.state` is `active` and `boxes.<box>.enrollment` is set.
+ termination fence. Check: [deploy] the decoded roster (`roster` above)
+ has `boxes.<box>.state` `active` and `boxes.<box>.enrollment` set, and
+ `control/box-fences/<enrollment>.json` does not exist.
- [ ] **Box identity records.** `CARLOS_REGION_HA=true` in each rehearsal
box's `host.env`. It takes `true`, `1` or `t`; **`on` reads as off**.
`CARLOS_AWS_ACCOUNT` is set in the console's environment. Check: [deploy]
@@ -330,13 +375,17 @@ Runs refer to these by number. `<from>` and `<to>` are the run's regions.
```
Record the answer's `name` and `due_at`.
-- **S-4 Cut.**
+- **S-4 Capture, then cut.** In a second terminal, start [capture] for
+ this run: `capture <host> run-<n>-detail.jsonl`. Then, in the first:
```sh
carlos move cut --app moveprobe --account <ops> --host <host>
```
- The expected sentences and exit code are in each run.
+ The cut waits until the move ends or needs an operator. The expected
+ sentences and exit code are in each run. Every run captures, so that
+ whatever the record held while the move was live (the evidence keys,
+ the rollback journal, a loss) can still be read after it closes.
### Checks after a commit
@@ -362,10 +411,9 @@ Runs refer to these by number. `<from>` and `<to>` are the run's regions.
`instances/<host>/` returns no versions and no delete markers, and
`parked/<inc>/<host>/` holds the retired replica objects with a
`manifest.json`. [deploy] the claim's `obligations` is empty.
-- **R-6 The source really stopped.** [deploy] `stopped-source.json` (also
- named by [detail] `detail.evidence.source` while the move is in flight)
- has `steps.tree.ok` and `steps.stray.ok` both `true` and no `stray`
- field. [box] on the source box, the journal has no "the instance is not
+- **R-6 The source really stopped.** [deploy] `stopped-source.json` (the
+ key [capture] shows as `detail.evidence.source`) has `steps.tree.ok`
+ and `steps.stray.ok` both `true` and no `stray` field. [box] on the source box, the journal has no "the instance is not
proven stopped yet" line for `<host>` and no M-C20 line ("... is running
a process for <host> that this move cannot account for. ..."), and
`/data/.moves/.page-outbox/<host>/<move>/stray.json` does not exist.
@@ -386,6 +434,20 @@ instance is back) in place of `<to>`. R-5 is replaced:
- **R-7 The target's writes came back** (dirty rollbacks only). [report]
on the source: `rows` has a `bg` row with `region: <to>`, written on the
target while it ran there.
+- **R-8 The return lane is parked** (dirty rollbacks only). A dirty
+ rollback returns the target's writes through its own lane,
+ `moves/<host>/<rb>/` in the **source's** data bucket, where `<rb>` is
+ the rollback's id (from [capture]: the last rollback journal's `id`).
+ The source queues its park when it has probed, with the horizon the
+ return seal's bound; the park runs only once that horizon plus the
+ signature age (15 minutes) and the request limit (2 minutes) have
+ passed. While it is owed, [deploy] the claim's `obligations` has an
+ entry with `park.location` `moves/<host>/<rb>` and `region` `<from>`;
+ record its `park.evidence_at`. After that time plus 17 minutes, and
+ once the obligation is gone: [data] `list-object-versions` on
+ `<data-bucket-from>` under `moves/<host>/<rb>/` returns no versions and
+ no delete markers, and `parked/<inc>/<host>/` holds a retirement
+ directory for it with a `manifest.json`.
A clean rollback needs two things: the stopped target's digests equal
what the landing installed, and `src_covered` (the source's own replica
@@ -465,12 +527,13 @@ nothing and the rollback should be clean.
The variable is app-wide. Any moveprobe instance that starts in that
region while it is set answers 503 and writes nothing. That is expected.
-- [ ] S-1, S-2 with `--to ie`, S-3, S-4. [CLI] M-C8 ("ie could not start
- <host>: <reason>. Moving it back to uk."), then M-C9 ("<host> is back in
- uk and serving. Nothing was lost."), exit 3.
-- [ ] While the move is rolling back and `detail.move.rollback.target_phase`
- is `released` or later: [detail] `detail.move.rollback.dirty` is absent
- or `false`. Record `detail.evidence.target`.
+- [ ] S-1, S-2 with `--to ie`, S-3, S-4 (capture first). [CLI] M-C8 ("ie
+ could not start <host>: <reason>. Moving it back to uk."), then M-C9
+ ("<host> is back in uk and serving. Nothing was lost."), exit 3.
+- [ ] From [capture], once the cut has returned and the capture has
+ stopped: the last rollback journal has `target_phase` `released` or
+ later, and `dirty` absent or `false`. Record its `id` as `<rb>`, and the
+ last `detail.evidence.target`.
- [ ] Digests: [deploy] `stopped-target.json`'s `db_sha256`, `files_plain`
and `timers_sha256` equal `log/rolling_back-target-released.json`'s
fields `installed_db_sha256`, `files_plain` and `timers_sha256`.
@@ -503,9 +566,11 @@ the rollback is dirty and the target's writes must come back.
carlos env set --app moveprobe --account <ops> MOVEPROBE_FAIL_READY_IN=ie,after-write
```
-- [ ] S-1, S-2 with `--to ie`, S-3, S-4. [CLI] M-C8, then M-C9, exit 3.
-- [ ] [detail] while rolling back, once `target_phase` is `released` or
- later: `detail.move.rollback.dirty` is `true`.
+- [ ] S-1, S-2 with `--to ie`, S-3, S-4 (capture first). [CLI] M-C8, then
+ M-C9, exit 3.
+- [ ] From [capture]: the last rollback journal has `target_phase`
+ `released` or later and `dirty` `true`. Record its `id` as `<rb>`
+ before anything else; R-8 needs it.
- [ ] Digests: [deploy] `stopped-target.json`'s `db_sha256` differs from
`log/rolling_back-target-released.json`'s `installed_db_sha256` (the
target wrote).
@@ -513,7 +578,8 @@ the rollback is dirty and the target's writes must come back.
- [ ] [deploy] `log/` has `rolling_back-target-returning` and
`rolling_back-target-returned`, then `rolling_back-target-cleaned` and
`rolled_back`.
-- [ ] R-1 to R-4 and R-6 on `uk`, R-5b on `ie`, and R-7.
+- [ ] R-1 to R-4 and R-6 on `uk`, R-5b on `ie`, R-7, and R-8 on `uk`
+ (the return lane `moves/<host>/<rb>/` in `<data-bucket-uk>`).
- [ ] Unset `MOVEPROBE_FAIL_READY_IN` once the checks are recorded.
### Run 4: dev, declare-lost
@@ -539,14 +605,16 @@ box cannot be readmitted.
Its probe then fails however long the stop takes. If the target finishes
its release before the box stops, the run is an ordinary clean rollback:
record it and repeat Run 4 on a spare host.
-- [ ] S-1, S-2 with `--to uk`, S-3, then
- `carlos move cut --app moveprobe --account <ops> --host <host> --no-wait`.
-- [ ] When [detail] `state` is `landing` (after `cutting-flipped`), stop
- `carlos-dev-uk` through the AWS console (dev only).
-- [ ] Wait for `carlos move status --app moveprobe --account <ops> --host
- <host> --wait` to say M-C10 ("Waiting for carlos-dev-uk in uk to confirm
- it has stopped <host>. <host> is offline."), exit 4. If it is still
- landing, the landing deadline has not passed yet. Keep waiting.
+- [ ] Before the run, record the box's roster entry: [deploy]
+ `roster | jq '.boxes["carlos-dev-uk"]'` shows `state` `active`; note
+ its `enrollment` and `quarantine_revision`.
+- [ ] S-1, S-2 with `--to uk`, S-3, S-4 (capture first). The capture runs
+ until the move closes, through the declaration below.
+- [ ] When [capture] shows `state` `landing` (after `cutting-flipped`),
+ stop `carlos-dev-uk` through the AWS console (dev only).
+- [ ] The cut's wait ends with M-C10 ("Waiting for carlos-dev-uk in uk to
+ confirm it has stopped <host>. <host> is offline."), exit 4. Until the
+ landing deadline passes, it keeps waiting.
- [ ] Declare it lost, with your own AWS credentials for dev in the shell
(the command reads EC2 with them and refuses to run as root):
@@ -556,20 +624,25 @@ box cannot be readmitted.
[CLI] M-C18 ("This declares carlos-dev-uk lost. Anything <host> wrote
on carlos-dev-uk after <time> is lost. Type the host name to
- confirm."). Type `<host>`. Expect M-C9 and exit 3.
+ confirm."). Type `<host>`. The command then waits for the outcome:
+ expect M-C9 and exit 3.
- [ ] Expected outcome: **rolled back, with the loss on the record**, not
- necessarily clean. [detail] while rolling back:
- `detail.move.rollback.loss_ref` and `lost_at` are set. [deploy]
+ necessarily clean. From [capture]: the last rollback journal has
+ `loss_ref` and `lost_at` set; record them and its `id`. [deploy]
`log/rolling_back-target-lost.json` (event `declared_lost`) and
- `log/rolled_back.json` exist. [deploy] `control/host-actors.json`:
- `boxes.carlos-dev-uk.state` is `quarantined` and its `loss_ids` holds
- the loss.
+ `log/rolled_back.json` exist. [deploy] the decoded roster:
+ `boxes.carlos-dev-uk.state` is `quarantined`, `quarantine_revision` is
+ one more than before, and `loss_ids` holds the loss.
- [ ] R-1 to R-4 and R-6 on `ie`.
- [ ] Unset `MOVEPROBE_FAIL_READY_IN`.
- [ ] Start `carlos-dev-uk` again. It stays quarantined: the deployment's
admission gate refuses it for every host until the readmit. Watch [box]
its journal from boot, and record anything it does for `<host>` before
the readmit. Doing anything for it there is a finding.
+- [ ] [deploy] the box retires its local state first and records it:
+ `control/host-retirement/carlos-dev-uk/<enrollment>/<quarantine_revision>.json`
+ exists with `"complete": true` and this boot's `boot_id`. Readmit
+ refuses without it.
- [ ] Readmit it. `carlos ops host` exists only with `CARLOS_HOST_CLAIM`
set in your shell:
@@ -578,8 +651,12 @@ box cannot be readmitted.
```
Answer `readmit` at its prompt. The box must be running and reporting:
- readmit refuses a box whose private report is older than 15 minutes.
-- [ ] [deploy] `boxes.carlos-dev-uk.state` is `active` again.
+ readmit refuses a box with a termination fence, and one whose private
+ report is older than 15 minutes. Before it, check [deploy]
+ `control/box-fences/<enrollment>.json` does not exist (a stopped box
+ has none; a terminated one does).
+- [ ] [deploy] the decoded roster: `boxes.carlos-dev-uk.state` is `active`
+ again, with the same `enrollment`.
---
@@ -589,8 +666,10 @@ box cannot be readmitted.
**Blockers:** P-0; P-1 to P-6 on the flagship; **P-3, the floor, published**
(§15 step 6: before any protected production rehearsal); the moves copy
-record fully approved (no `pending` row); dev Runs 1 to 3 passed; Paul's
-explicit go in the thread, for each of the three.
+record fully approved (no `pending` row); **dev Runs 1 to 4 passed**
+(each run gates the next, spec §15 step 7; claim Plan 8b is on main, so
+the old clause that let Run 5 go ahead with Run 4 skipped no longer
+applies); Paul's explicit go in the thread, for each of the three.
- **The switch** (Paul, N17): `CARLOS_MOVES_PHASE1=on` in the environment
of the console and of the two boxes in play, and nowhere else. The
@@ -615,6 +694,47 @@ The runbook names no tenant. The real instances, their owners and the
agreed windows live in the flagship's deployment repo and in the amadan
thread where Paul gives each go.
+These runs move a customer's app, not moveprobe. **Do not call `/ready`,
+`/arm-once` or `/report` on a customer host, and do not set
+`MOVEPROBE_FAIL_READY_IN`.** Those are moveprobe's own, and S-1, S-3, R-1
+to R-4 and R-7 do not apply. Set `ACCT=<account>` and `APP=<app>` for
+[detail], [capture] and the log prefix, and use `--app <app> --account
+<account>` on every command. `<from>` and `<to>` are the regions Paul
+approves for the run.
+
+### Customer steps
+
+- **C-1 Health check, before.** The owner names one health check for the
+ instance (a URL and the answer that means it is serving) and approves
+ it for the run. Record it in the deployment repo. Run it and record the
+ answer.
+- **C-2 Request.**
+
+ ```sh
+ carlos move --app <app> --account <account> --host <host> --to <to> --manual-cut
+ ```
+
+ [CLI] M-C1, M-C2, M-C3, then M-C12, exit 4. [detail] at ready, the same
+ checks as S-2: every target check `ok`, `attempts_admitted` at least 1,
+ `source_report.s1.ok` and `s2.ok` `true`. Record `<move>` and `<inc>`.
+- **C-3 Capture, then cut.** Start [capture] in a second terminal, then,
+ in the window agreed with the owner:
+
+ ```sh
+ carlos move cut --app <app> --account <account> --host <host>
+ ```
+
+ [CLI] M-C5, M-C6, M-C7, exit 0.
+- **C-4 Health check, after.** The owner's check from C-1 gives the
+ serving answer once M-C7 has printed. The owner confirms it.
+- **C-5 Platform checks.** R-5 as written. R-6 as written, except its
+ last two sentences, which are about moveprobe's worker. [deploy] `log/`
+ has `cutting-flipped`, `committed`, `committed-opened`,
+ `committed-source_parked` and `done`.
+
+If the move rolls back (M-C8 then M-C9, exit 3), the instance is back in
+`<from>`: run C-4 there, then R-5b and R-6, and stop the series.
+
### Run 6: a disposable customer-shaped instance
**Blockers:** P-0; **the home-region parks fix, both parts, merged and
@@ -627,9 +747,7 @@ A disposable instance of a customer app with the same shape as moveprobe
owner's agreement, before the flagship's claim went on (P-5).
- [ ] Instance used: recorded in the deployment repo.
-- [ ] Run 1's steps only (a commit), without the moveprobe-specific
- [report] checks. The owner checks the instance serves after the open.
-- [ ] R-5 and R-6 as written; [CLI] M-C7, exit 0.
+- [ ] C-1 to C-5.
### Run 7: the first production customer instance
@@ -641,7 +759,8 @@ with its owner.
- [ ] The instance was created before the flagship's claim went on, so it
is adopted at `instances/<host>` (P-5). One created later cannot move.
-- [ ] Run 6's steps. The cut window is the default 120 s.
+- [ ] C-1 to C-5, with C-3 inside the agreed window. The cut window is the
+ default 120 s.
- [ ] Report `offline_s` back to the owner: [detail] `last.offline_s`, or
`offline_s` in `log/done.json`'s fields.
@@ -690,6 +809,8 @@ copy is `docs/superpowers/plans/2026-10-06-moves-phase1-copy.md`.
| what | where |
|---|---|
| detail=1 only for operators, only with the switch on | `internal/console/apiinstancemove.go:312-327` |
+| `detail.move` is nil once no move is in flight, so the capture loop keeps it | `internal/console/apiinstancemove.go:1291-1292`, `:1416-1423` |
+| a mutation waits for the outcome unless `--no-wait` | `move.go:186-188` |
| flat fields `state`, `target`, `id`, `last`, `protocol`, `phase` | `internal/console/apiinstancemove.go:160-170` |
| `detail.move`, `claim`, `target_report`, `source_report`, `evidence`, `log`, `timings`, `from`, `region` | `internal/console/apiinstancemove.go:1290-1318` |
| `evidence.source` and `evidence.target` come from the record in flight | `internal/console/apiinstancemove.go:1367-1370`, `:1416-1423` |
@@ -717,6 +838,10 @@ copy is `docs/superpowers/plans/2026-10-06-moves-phase1-copy.md`.
| parked objects at `parked/<inc>/<host>/<retirement>/`, a `manifest.json`, the source versions deleted | `internal/park/remote.go:230-232`, `:473`, `:497` |
| stopped report `steps.tree`, `steps.stray`, `stray` (omitted when empty), digests | `internal/moves/evidence.go:11-20`, `:38-53` |
| a clean rollback: `src_covered` and unchanged digests; the "clean ..., src_covered ..." line; `dirty` and `evidence` written at released | `internal/host/movesrollback.go:394-398`, `:521-526` |
+| the rollback's `id` | `internal/provision/provision.go:1068` |
+| a dirty rollback's return lane is `moves/<host>/<rb>`, parked in the source's region, queued when the source has probed, horizon the return seal's bound | `internal/moves/parkscope.go:66-72`; `internal/host/movesrecover.go:658-675`; `internal/hostclaim/move.go:1093-1108` |
+| an obligation's `park.location`, `park.evidence_at`, `region` | `internal/hostclaim/claim.go:116-121`, `:137-147` |
+| a park runs only after `evidence_at` plus the signature age (15 min) and the request limit (2 min) | `internal/park/remote.go:21`, `:516-519`; `internal/moves/parkscope.go:23` |
**The door and the fleet**
@@ -738,11 +863,14 @@ copy is `docs/superpowers/plans/2026-10-06-moves-phase1-copy.md`.
| the claim at `control/hosts/<host>.json`; `holder.location`, `obligations`, `quiet_after` | `internal/hostclaim/claim.go:56`, `:166`, `:171`, `:182` |
| T12 reads `quiet_after`; H is 15 min plus 1 min skew | `internal/host/movesreport.go:756-762`; `internal/moves/lease.go:13`, `:22`; spec:2573 |
| the console's claim recovery pass adopts with the claim on | `internal/console/hostclaimbackfill.go:40-48` |
-| the roster at `control/host-actors.json`; `boxes`, `state`, `enrollment`, `loss_ids`; a declaration quarantines | `internal/hostactors/roster.go:21`, `:30`, `:38-40`, `:72`, `:265-279` |
+| the roster at `control/host-actors.json`; `boxes`, `state`, `enrollment`, `quarantine_revision`, `loss_ids`; a declaration quarantines and adds one to the revision | `internal/hostactors/roster.go:21`, `:30`, `:38-40`, `:72`, `:265-279` |
+| the roster is written as a root-signed envelope `{payload, sig}`, the payload base64 JSON | `internal/hostactors/roster.go:249`; `internal/release/rootenvelope.go:29-44`; `internal/release/release.go:377-379` |
+| a termination fence is `control/box-fences/<enrollment>.json`; no object, no fence | `internal/hostactors/fence_authority.go:14-34` |
| a quarantined box is refused admission | `internal/hostquarantine/quarantine.go:17-25` |
| `CARLOS_HOST_CLAIM` is any non-empty value | `cmd/console/main.go:1364-1366`; `hostclaim_cli.go:22` |
| `carlos ops host` exists only with `CARLOS_HOST_CLAIM` in the shell; `--readmit`, `--console`, the `readmit` prompt | `hostclaim_cli.go:35-39`, `:61-75`, `:143` |
| readmit refuses a fenced box and a private report older than 15 min | `internal/hostresolve/readmit.go:18-40` |
+| readmit needs the box's complete retirement receipt for this boot at `control/host-retirement/<box>/<enrollment>/<revision>.json`, then sets the box `active` | `internal/hostresolve/readmit.go:52-66`, `:134`; `internal/hostquarantine/quarantine.go:72-82`, `:91-96` |
**The floor, the switch and the boxes**
@@ -758,7 +886,8 @@ copy is `docs/superpowers/plans/2026-10-06-moves-phase1-copy.md`.
| the copy gate keeps the default off while a row is pending | `approved_moves_copy_test.go:811` |
| the default cut window, 120 s | `internal/moves/budget.go:17` |
| `CARLOS_LAYOUT_DEFAULT=flat` | `internal/host/layoutleg.go:68-70`; `system.go:714` |
-| `carlos system status`, `carlos system layout status` | `system.go:75-76`, `:611-641`; `README.md:272` |
+| `carlos system status` | `system.go:75-76`; `README.md:272` |
+| `carlos status --layout` shows each host's actual layout, box by box, and marks a box "(nested by default)"; `carlos system layout status` shows only the recorded intent | `cli.go:3528`; `status_instances.go:142-163`, `:224`; `system.go:709-716` |
| the agent is `carlos-edge.service`, reading `/etc/carlos/host.env`, with a delegated cgroup | `internal/host/units/carlos-edge.service:18-19`, `:101` |
| "exec-child accounting armed"; no manager, no stop evidence | `internal/cgroup/cgroup.go:181-184`; `internal/host/movespass.go:376-392` |
| "not proven stopped yet ... retrying until the cut's deadline" | `internal/host/movescut.go:422-427` |
@@ -800,4 +929,5 @@ copy is `docs/superpowers/plans/2026-10-06-moves-phase1-copy.md`.
|---|---|
| the runs and the pass rule | spec §14.4, spec:6263-6276 |
| the rollout order, the floor before any protected production rehearsal | spec §15, spec:6278-6294 |
+| each rehearsal gates the next, so Run 5 needs Run 4 | spec:6294; controller ruling, fix round 1 |
| the flagship's home is Ireland | spec:1032 |