carlos / platform Public
Moves Phase 1, Plan 7: Rehearsal Runbook Implementation Plan
> For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking. Nothing in this plan runs against production without Paul's go, and every rehearsal gates the next (§15 step 7).
Goal: Prove Phase 1 on a probe app that exercises every carried state, then on a disposable real instance, then on kpmg, in M12's order, with a written runbook an operator follows and a pass criterion the run must meet.
Architecture: moveprobe is a small Go app in cmd/moveprobe shaped like ips (§14.4): instance.key written at first boot and verified every boot; a required store binding and a sealed secret; a declared schedule and a once-timer; a background loop writing rows; a forked worker that outlives the main process; a switch to fail its readiness path in a named region before or after writing. The runbook is docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md, a checklist per run with the commands, the expected log events, and the checks. Cross-version tests pin the door and the floor against pre-floor binaries.
Tech Stack: Go, net/http, database/sql with modernc sqlite, the platform's carlos CLI, tofu for nothing (no infra change in this plan beyond §15 step 5, which is applied as scoped work by Paul).
Spec: rev 7.23 §14.3, §14.4, §15. Index: 2026-10-06-moves-phase1-0-index.md. Plans 1 to 6 are merged, not rolled, and not complete (preflight N1): no run in this plan executes on main until claim Plan 12 and its moves dependants are merged, released and rolled.
Global Constraints
- Gate, cross-compile, the default Go build cache; tests serial and niced.
- Rollout order (§15): the dormant release first (consoles, then every box), M17's
sibling_staging_arnsand the versioned-object grants (§15 step 5) applied by Paul, thencontrol/moves-protocol.jsonwritten create-only before any protected production rehearsal (§11, joint N12), then the rehearsals in order: dev IE to uk done; dev forced clean rollback; dev forced dirty rollback; dev declare-lost; the same on the flagship with scoped approval; a disposable ips instance inbdn; kpmg with Paul's approval. - A run passes only if: the key verifies, the object reads back, the secret is present, every timer ran exactly once around the cut, no background row is missing, and no copy remains in a live namespace of the region that lost (§14.4).
moveprobe's readiness-failure switch is an environment variable (MOVEPROBE_FAIL_READY_IN=<region>[,after-write]) delivered through the config bundle, so a rehearsal flips it withcarlos env set, never a rebuild.
Review Focus
- The forked worker must survive the main process's exit and still be killed by
ReconstructStopEvidence; a run where the worker survives the cut must fail the run with the stray-process page, never pass silently. Task 1 (TestMoveprobeWorkerOutlivesMain) and runbook check R-6. - A once-timer registered seconds before the cut must fire exactly once, on the target, with its
due_atpreserved; firing twice or never fails the run. Task 1's timer registration and runbook check R-4. - The disposable
bdninstance must be created with a fresh host name no previous rehearsal used, or a staleinstances/<host>/ormoves/<host>/lane makes the sweep's work part of the run. Runbook step S-1. - The dirty-rollback run must prove the target's writes came back: the probe app writes a row with the target's
CARLOS_REGIONwhile it runs on the target; after the rollback the source must hold that row. Runbook check R-7. - The declare-lost run must use a box the operator actually stopped through the cloud API (dev only, never the flagship's shared boxes without Paul's go), and the loss record must name the declared sessions. Runbook step S-9.
---
File structure
| path | responsibility | |---|---| | cmd/moveprobe/main.go, cmd/moveprobe/main_test.go | the probe app | | cmd/moveprobe/README.md | how to deploy it as a carlos app (carlos ship) | | internal/console/apiinstancemove_test.go, internal/host/systemapply_test.go | cross-version tests | | docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md | the runbook |
---
Task 1: moveprobe
Files: create cmd/moveprobe/main.go, cmd/moveprobe/main_test.go, cmd/moveprobe/README.md.
Interfaces (the app's behaviour):
- The exec backend's launch contract is flags, not variables (
internal/activator/backend_exec.go:1541, inspawnChildFrom:exec.Command(bin, "--socket", sock, "--db", dbPath)):moveprobe --socket <unix path> --db <sqlite path>listens on the socket and opens the database; the files directory isstrings.TrimSuffix(dbPath, ".db")(F4, the flat layout). The agent-owned environment is composed byagentOwnedVars(backend_exec.go:1603-1626):CARLOS_CONTROL_SOCKETonly when the control listener bound,CARLOS_PURGE,CARLOS_REGIONonly when the box has a region, andCARLOS_HOST.CARLOS_ADMIN_TOKENandCARLOS_INSTANCE_KEYcome from<host>.env, not the composer. The app reads those by name and treats a missingCARLOS_REGIONas regionhome(preflight N7).TestMoveprobeLaunchContractstarts the binary exactly as the backend does (--socket,--db, the agent variables) and asserts/readyanswers on the socket. - First boot: writes
files/instance.key(32 random bytes, hex) beside the database; every boot: verifies the file exists and matches the rowmeta(key='instance_key')in the database; mismatch → the readiness path answers 500 withkey mismatch. - Requires
MOVEPROBE_BUCKET,MOVEPROBE_REGION,MOVEPROBE_ENDPOINT,MOVEPROBE_ACCESS_KEY,MOVEPROBE_SECRET_KEY(anobjstorebinding with prefixMOVEPROBE) and writesprobe/<boot id>.txtat boot, reading it back on/ready; requiresMOVEPROBE_SECRET(a sealed secret) and answers 500 on/readywhen absent. - Registers, through the instance control socket (
CARLOS_CONTROL_SOCKET), a declared scheduleevery-minute→/tickand, onPOST /arm-once?in=<seconds>, a once-timeronce-<n>→/once;/tickand/onceinsert rows(kind, name, due_at, fired_at, region). - A background loop inserts a row
(kind='bg', seq, region)every 500 ms; a forked worker (exec.Command(os.Args[0], "worker")withSetsid) inserts(kind='worker', seq, region)every second and ignores SIGTERM for 30 s (Review Focus 1). MOVEPROBE_FAIL_READY_IN=<region>[,after-write]: in the named region, the before-write mode (no suffix) suppresses every mutation on this box from the first instruction: the background loop, the forked worker, the/tickand/oncehandlers and the control-socket registrations all return without writing, and/readyanswers 500, so the target's database, files and timers digests stay exactly what was installed and the rollback is clean (the readiness checks after a spawn, the wake'swaitHealthyCtxatbackend_exec.go:1335(GET/, any status below 500,waitHealthyat:2344) and the claim probe'sclaimProbeSocketReadyatinternal/host/claimready_linux.go:23-41(2xx only), would otherwise see rows written before the refusal); the after-write mode writes onebgrow (and lets the worker write) and then fails/ready, so the rollback is dirty with a known row to find on the source./ready: 200 when the key verifies, the object reads back and the secret is present, and the failure switch does not apply./report: JSON with the row counts per kind and region, the once-timers fired (name, count, dueat), the key fingerprint, the object body, `hassecret`.
Tests: TestMoveprobeWorkerOutlivesMain (start the binary in a temp cgroup-less dir, kill the main process, the worker's pid is alive and still writing; the test then kills it); TestReadyFailsWhereTold (both modes: before-write leaves the database's row count and the timers table unchanged after two seconds of running; after-write adds exactly the rows the mode promises); TestReportCountsRowsByRegion; TestMoveprobeLaunchContract.
- [ ] Steps 1 to 5. Commit:
git commit -m "cmd/moveprobe: the rehearsal probe app (§14.4)" ...
---
Task 2: Cross-version tests
Files: modify internal/console/apiinstancemove_test.go, internal/host/systemapply_test.go, internal/provision/move_test.go.
TestDoorRefusesBelowTheFloor: an edges file withmoves_protocol: 0(a pre-floor box) in either region → 409 naming the floor; with the field absent → the same.TestSystemApplyRefusesAPreFloorBinaryOnAParticipant: a box whose registry holds a held row (hold_opset) refuses anycarlos system applyor rollback (§11 "any downgrade of a box that is a participant in a held operation is refused"); the applier reads the registry at/data/registry.dbthrough the same path the agent uses.TestOldConsoleAbortAllowlist:MoveAbortfrom a v0.22 console (the currentprovision.go:1027-1035allowlist, reproduced as a test double) refuses every new state (landing,committed,rolling_back,reverting,stuck), per P0's rule.
- [ ] Commit:
git commit -m "moves: cross-version tests: the floor at the door and at activation, the old abort allowlist (§14.3)" ...
---
Task 3: The runbook
Files: create docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md.
The runbook has one section per run in §14.4's order; each section is a checklist an operator ticks:
Preconditions (once):
- [ ] P-1 The Phase 1 release is rolled dormant: consoles first, then every box (
carlos statusshows the build;control/edges/*.jsonall carry"moves_protocol":1). - [ ] P-2 §15 step 5 applied:
sibling_staging_arnsand the versioned-object grants (Paul, scopedtofu apply -target); verify with T10's probe throughcarlos move status --host <h>after the first request (T10 passes). - [ ] P-3
control/moves-protocol.jsonwritten create-only:{"min":1,"min_release":"<the rolled version>"}(anaws s3api put-object --if-none-match '*'from an operator shell with the deployment profile);carlos system applyof an older build on dev now refuses. - [ ] P-4
moveprobeshipped as appmoveprobeunder the ops account with a store bindingMOVEPROBE, a sealed secretMOVEPROBE_SECRET, a declared schedule, andmoves.ready_pathunset (/serves/ready's answer). - [ ] P-5 Every rehearsal host is adopted (claim Plan 5's adoption has run:
control/hosts/<host>.jsonexists withop.kind: serving). This needs C5 merged and the #232 ownership backfill run for every rehearsal host: adoption happens only inside that backfill, never from the move door or the recovery pass, and merging the claim branches alone does not adopt anything. - [ ] P-6 The switch is on for this deployment:
CARLOS_MOVES_PHASE1=onon the console and every box (index D13), with the copy record approved and the default flipped in one commit before any run on the flagship. Run 4 (declare-lost) needs claim Plan 8b merged and rolled (the roster, census, release identity and guard, the termination fence, the real declare-lost and readmit) andCARLOS_MOVES_DECLARE_LOSTis never set outside tests; until 8b is rolled, Run 4 is skipped and the series continues with Run 5, and the loss run is repeated on dev once 8b lands.
Run 1, dev IE → uk, done:
- [ ] S-1
carlos instances create --app moveprobe --host mp1-<date>.<domain> --region ie(a fresh name; Review Focus 3); wake it (curl https://<host>/),POST /arm-once?in=600, note/report. - [ ] S-2
carlos move --host <host> --to uk --manual-cut; wait for M-C12;carlos move status --host <host>shows every T check passing andattempts_admitted ≥ 1. - [ ] S-3
carlos move cut --host <host>; expect M-C5, M-C6, M-C7; exit 0. - [ ] R-1
/reporton uk: the key fingerprint equals S-1's; the object reads back;has_secrettrue. - [ ] R-2 the
bgandworkerrow counts are at least S-1's and everyseqis contiguous (no missing row). - [ ] R-3 the
every-minuterows continue on uk with regionukfrom the first tick after the open. - [ ] R-4 the once-timer fired exactly once, on uk, with S-1's
due_at(Review Focus 2). - [ ] R-5
aws s3api list-object-versions --bucket <ie data bucket> --prefix instances/<host>/is empty after the source park acknowledged (carlos move statusdetail shows no obligation; the log hassource_parkedanddone);parked/<inc>/<host>/<ret>/holds the archive. - [ ] R-6 the move log has no
straypage; the ie box's journal showsReconstructStopEvidencekilled the worker (thetreestep OK) (Review Focus 1). - [ ] R-7 (not for this run).
Run 2, dev forced clean rollback: carlos env set --app moveprobe MOVEPROBE_FAIL_READY_IN=ie (the before-write mode on the target, moving uk → ie: nothing is written there, so the digests equal the install's); request and cut; expect M-C8 then M-C9, exit 3; checks R-1 to R-6 on uk (the source), plus: the uk row is ready again, the ie bucket's instances/<host>/ and moves/<host>/<mv>/ are empty after cleaned, rollback.dirty == false in the log's released event, and the released event's three digests equal landing.installed_db_sha256, source.files.plain and source.timers_sha256.
Run 3, dev forced dirty rollback: MOVEPROBE_FAIL_READY_IN=ie,after-write; expect exit 3; R-7: the source's /report holds a bg row with region ie written on the target during its probe (Review Focus 4); the log's released event has dirty: true, returned names the lane snapshot; moves/<host>/<rb>/ in the uk bucket is parked after the horizon.
Run 4, dev declare-lost (needs P-6's Plan 8b clause): request a move to a dev box Paul designates as disposable; after the flip, stop that box through the AWS console (dev only); carlos move resolve --host <host> --declare-lost <box> --instance-id <id>; type the host name at M-C18; expect the clean rollback to complete on the source, exit 3; the loss record names the box, fenced_at, the declared timers; restart the box and watch it quarantine the host's local state before anything else (S-9, Review Focus 5).
Run 5, flagship, scoped approval: runs 1 to 3 again on the flagship with a moveprobe instance, each with Paul's explicit go in the discussion thread.
Run 6, a disposable ips instance in bdn: carlos instances create of an ips instance Tony designates, run 1 only, with Tony's check that the instance serves after the open.
Run 7, kpmg: run 1 only, with Paul's approval, in a window agreed with the customer; the cut budget stays 120 s; the runbook's offline_s from the done event is the number reported back.
After every run: attach the done/rolled_back log event's timings, /report, and the bucket listings to the discussion; a failed check stops the series until its cause is fixed and the run repeated.
- [ ] Commit:
git commit -m "docs: the Phase 1 rehearsal runbook in M12's order (§14.4, §15)" ...
---
Task 4: Gate
- [ ] Full gate, cross-compile (
cmd/moveprobebuilds on darwin and windows; theSetsidattribute is behind//go:build !windows).
Self-review notes
Coverage: §14.3 (Task 2), §14.4 (Tasks 1, 3), §15 steps 2 to 7 (the runbook's preconditions and order; step 1 and step 8 are done per the spec). The infra change of step 5 is Paul's scoped work and is a precondition, not a task.