carlos / platform Public

zdd-6 task 17: roll notes for the zero-downtime deploys (gates A to G, probe, owed list)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 0e3d31cfdfae434c9891be25cd13739c6fdc2637 parent 77d77b3
1 file changed, +515 −0
  • docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md +515 −0
diff --git a/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md b/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md
new file mode 100644
index 000000000..595ccf46b
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md
@@ -0,0 +1,515 @@
+# Zero-downtime deploys: roll notes (plan 6, task 17)
+
+These are the notes for putting zero-downtime deploys into production, gate
+by gate. They are written, not run. Nothing in this file has been executed
+against a box, a bucket, a console or an AWS account, and every command
+below is quoted for Paul to run or approve.
+
+Spec: `2026-10-07-zero-downtime-deploys-design.md` (rev 17), section 15 for
+the order. The plans are `2026-10-07-zero-downtime-deploys-0-index.md` and
+plans 1 to 6, in `platform-design-zero-downtime-deploys`.
+
+## Ground rules
+
+- **Approval.** Every gate that touches dev or the flagship is production
+ infrastructure (AGENTS.md, "Production infrastructure needs scoped
+ approval"). Before running one, get Paul's explicit approval for that gate,
+ naming the boxes. Approval for one gate is not approval for the next.
+- **AWS profiles.** Assumed-role profiles need the Bash sandbox off for that
+ command. Dev is profile `kass`. The flagship is profile `keymail`.
+- **Order.** Boxes first, then the console's swap-policy settings, then the
+ flag: dev, then `bab/adopt-probe` on the flagship, then the rest of the
+ flagship. The deploy docs and release notes carry the migrations rule
+ before the flag goes on across the fleet. Plan 5 keeps the flag off
+ outside dev until this plan has merged.
+- **The flag.** `CARLOS_SWAP=overlap` in a box's `/etc/carlos/host.env`. Only
+ the exact word `overlap` arms it. Anything else, or nothing, leaves the box
+ on today's stop-then-start paths.
+- **Scratch.** The probe's scratch lives under `$TMPDIR`, never `/tmp`.
+
+## Before Gate E: owed
+
+This is the flag-on precondition. It is copied from amadan discussion #272
+("Zero-downtime deploys: owed before CARLOS_SWAP can go on", opened by
+claude~paul@keymail.dev), as read on 2026-10-11, including its replies.
+Nothing on this list is closed. **The flag stays off on any box until every
+item is closed or Paul waives it in writing.** Gates A to D do not turn the
+flag on and can go ahead while this list is open.
+
+The discussion's opening text says zero-downtime deploys are merged
+dormant and `CARLOS_SWAP` must stay unset until everything below is closed.
+
+### Correctness, from Astra's plan 5 review (`CODEX-FINDINGS-ZDD-IMPL-5*.md`)
+
+- N5 to N7: gaps in the hold-ran marker. N7 is a disagreement over how it
+ treats an existing failure record.
+- F5: failure-record durability across a failed write followed by a restart.
+- Boxes that declined to arm: worker restarts; refusals checked only at plan
+ time; records read only at start.
+- The 15-item list in round 3, section 5.
+
+### Carried from plans 2 and 3
+
+- With the flag off, a retained wake is proven by its leader's exit only;
+ forked descendants are not checked (F6, pre-existing).
+- Leftover executable files are not swept from `/run`.
+- Claims-backed swaps are refused (P2-9). Main's claimed launch is closed, so
+ a launch protocol and the strict-parent lifecycle are needed before claims
+ boxes get swaps.
+- Strict hosts get no swaps until a claim path exists (P2-8).
+
+### Dev-box checks (need Paul's approval to run)
+
+- D1: `daemon-reload` cost at titogo-scale unit counts. Above about 1 s per
+ swap means transient units instead of drop-ins. (Gate E below runs this
+ measurement.)
+- The plan 2 kernel cgroup suite on arm64 with `User=carlos`.
+- Real-unit sidecar and control checks: the registry read under the control
+ unit, and StopGen(kill) on a never-loaded unit.
+- A guard against legacy cgroup parents that still hold processes.
+
+### Surfaces (plan 6)
+
+- Copy review of all event and refusal wording. That includes the
+ `WakePlan.Refuse` texts, "restart refused: rolling restarts are unavailable
+ on this box", the refused-wake texts, and main's "sidecar could not be
+ restarted" retry line, which has an em dash and now fires more often.
+- A member-safe wake reason for `ErrBuildNotPublished` (plan 4 R22).
+- `carlos sidecars ls` shows `ServingVersion`.
+- Roll notes: boxes first, then console policy, then dev, then
+ `bab/adopt-probe` under load with zero failed requests, then the fleet.
+ (This file.)
+
+### Reply: four claims-on gaps
+
+Plan 5 was merged dormant as `d905a67`, after merging main d6f66ef3 (#268
+lease margin, region-HA). Astra's merge review
+(`CODEX-FINDINGS-ZDD-IMPL-5-MERGE.md`) said ACCEPT WITH FIXES for a dormant
+merge, with no flag-off defect.
+
+Main's `RoleLaunch.Root` is wired in production, so claims-on launches can
+execute. **Before the flag goes on, either decline arming on boxes with host
+claims on, or close all four:**
+
+1. Recovery selection and fallback cannot be honoured for claimed planned
+ wakes.
+2. A claimed hold launch can lose its execution evidence and outcome fence.
+3. Retained claim processes are missing from the generations file.
+4. Successful claimed wakes cannot record their good build or serving
+ version.
+
+I did not find a claims check in `swapArming`
+(`internal/host/rollout.go:145-158`), which is where the arming decision is
+made. I looked only there, so this is not proof that nothing declines
+elsewhere. Treat "decline arming on claims boxes" as unbuilt until someone
+shows the code.
+
+### Reply: wiring caution (settings plan 2, merged as `70f8119`)
+
+Held staging checks authorization inside `stageHeld`, so every entry point is
+covered, including main's claim-path `MaterializeHeldUnder`.
+
+Whoever wires zero-downtime plan 5 Task 9: when `movesProbeOwners.Release`
+is connected to `MaterializeHeldUnder`, it must pass the parsed
+`CARLOS_RELEASE_PUBKEY` and the agent's digest ratchet. A missing key or
+ratchet refuses every stage (fail closed). That is not a bug to "fix" by
+passing nil.
+
+### Where plan 6 stands against that list (for information)
+
+The discussion text predates plan 6 ("not started"). On this branch the
+`carlos sidecars ls` change is built (task 13), drafts for the refusal and
+wake wording exist behind `TODO-COPY` markers (task 14b), and this file is
+task 17. None of those closes its item: the copy items wait for Paul's
+review, and only Paul closes the discussion's items.
+
+## What plan 6 changed that the roll should check
+
+- **Copy is still pending.** No approved copy exists yet. Every new
+ user-facing string is the plan's draft text with a `TODO-COPY zdd.<id>`
+ marker beside it (Go: a `// TODO-COPY` comment; templates:
+ `{{/* TODO-COPY ... */}}`; docs: `<!-- TODO-COPY ... -->`). The strings sit
+ in `copy-review/` for review. Task 16's pin of the approved copy
+ (`approved_copy_zdd_test.go`, reading `6-copy.json`) is deferred until Paul
+ approves. **Do not turn the flag on until the approved copy is applied and
+ pinned.** Where this file says "the approved line", it means the line after
+ that review. Until then the draft is what prints.
+- **`carlos sidecars ls` changes with the flag off.** It reads
+ `ServingVersion` and prints "unknown" for a sidecar that has never started
+ anywhere. Plan 4 already changed what the registry `Version` means for a
+ sidecar, so the old column would lie. Look at it on dev after Gate B and
+ expect different output from the last release. The code is in
+ `internal/console/apisidecars.go:121-130` and `sidecars_cli.go:368-378`.
+- **The swap-policy door is person-only.** `PUT
+ /api/cli/apps/{account}/{app}/swap` is registered with the zero role set
+ (`internal/console/authcli.go:858`, reasoning at
+ `internal/console/apiswap.go:1-8`). A signed-in member can run
+ `carlos apps set`. A CI token or any credential the app itself holds
+ cannot. If CI needs to set the policy, that is a one-line role change and
+ Paul's call.
+- **The first non-default swap policy creates `config.json` and causes one
+ config delivery (plan Q3).** Setting the defaults (`overlap`, 1) on an app
+ with no `config.json` writes nothing (`internal/appconfig/swap.go:47-56`).
+ The first `hold` or concurrency above 1 creates the file, and boxes then
+ see a config change once. After that, config delivery restarts an instance
+ only when the rendered bytes change. In the probe this means the
+ `--swap hold` control may restart `adopt-probe` once, a few seconds after
+ the command and before the `hold` load phase has had its 20 s head start.
+ Do not read that restart as part of the control's result. Q3 stays open with
+ Paul.
+- **A host retry rides the unsigned restart-scope log (plan Q4).**
+ `carlos instances retry` asks the box to retry through the restart-scope
+ log. That log is not signed, and the box's `RetryHost`
+ (`internal/rollout/manager.go:501-515`) trusts any request that names a host
+ on that box. Only the app's record and the rollout report gate it, at the
+ console, not at the box. Q4 stays open with Paul.
+- **`FailureBook` is not rendered anywhere.** The box publishes it
+ (`internal/status/snapshot.go:268-275`) but no `carlos` surface or console
+ page shows it. It is parked in #272. What `carlos` shows for a failed or
+ held build comes from the per-host rollout report instead.
+- **The deploy watch polls the instance list every 5 s.** It does not poll
+ every 2 s. It falls back to polling the URL (today's `watchVersion`) only
+ after a complete list with no reports for the targeted instances, or after
+ 3 failed first lists. A truncated list never triggers the fallback, and a
+ box that refuses or declines is named, not URL-polled (declined boxes fall
+ back to today's URL watch). The constants are `deployWatchListEvery`,
+ `firstListTries` and `deployWatchPollEvery`
+ (`deploywatch.go:85-88`). A deploy against a flaky console can therefore
+ wait up to about 15 s longer before it falls back.
+- **A dash under SERVING means the build is not reported, for either of two
+ reasons.** No rollout report (flag off, or an older console), or a box that
+ set the flag and declined (`Unavailable`). `servingCell` is at
+ `instances_cli.go:450-455`. After Gate E, a dev instance still showing a
+ dash means the box did not arm. Look for the box's reason code and the
+ agent log line `REFUSING CARLOS_SWAP=overlap`
+ (`internal/host/rollout.go:151`).
+- **The migrations rule.** The new "Two builds at once" section of the public
+ CLI page (`docs/site/cli.md:330`, marked `TODO-COPY zdd.docs.migrations`)
+ and the release notes carry it: add in one release and remove in a later
+ one, and keep the names of scheduled work the same across the two builds.
+ `--swap hold` and `carlos apps set --swap hold` are the way out for a
+ release that cannot follow it.
+
+## Gate A: release
+
+Needs Paul's approval, naming the release. This gate touches no box.
+
+Plans 1 to 6 are merged to amadan `main` and `main`'s CI is green. Cut a
+platform release from that commit, following README "Releasing the platform"
+(steps 1 to 9). Its release notes (step 5, copy-reviewed) lead with the
+migrations rule (spec section 9.5) and say the flag ships off. Call the
+release `vX.Y.Z` below.
+
+README's "Releasing the platform" section ends with a place for release
+specific operator notes. Add one for this release naming the flag-on
+preconditions above, so the person rolling it reads them.
+
+## Gate B: boxes, flag off
+
+Needs Paul's approval, naming the boxes: dev (`platform.dev.carlosframework.com`,
+channel `canary`) and the flagship (`console.carloku.com`, channel `stable`),
+which is `ie`, `us`, `uk` and `au`.
+
+```bash
+carlos system update --console https://platform.dev.carlosframework.com --channel canary --version vX.Y.Z --yes
+carlos system status --console https://platform.dev.carlosframework.com
+carlos system update --console https://console.carloku.com --channel stable --version vX.Y.Z --yes
+carlos system status --console https://console.carloku.com
+```
+
+Expected: every box reports `vX.Y.Z`. Sydney is slow, so give it the full
+gate. With `CARLOS_SWAP` unset nothing changes: a `carlos deploy` still
+prints `live https://...` lines, and `carlos instances list` shows a dash
+under SERVING.
+
+## Gate C: consoles
+
+Needs Paul's approval, naming the consoles and the app.
+
+Roll each console after its boxes. The script is platform-infrastructure's
+`roll-console.sh`; it is not in this repo (not checked here). Or use direct
+mode:
+
+```bash
+carlos ship --app console --kind binary --version <sha> console-linux-arm64
+carlos promote --app console <sha> canary/console
+```
+
+with `CARLOS_DEPLOYMENT_BUCKET` set for both. Then check the door:
+
+```bash
+carlos apps set adopt-probe --account bab --swap overlap --console https://console.carloku.com
+```
+
+Expected: the approved `zdd.apps-set.done` line for `adopt-probe`, overlap,
+1. Until the copy is approved the draft prints:
+`adopt-probe swaps in overlap mode, 1 at a time on each box`, then a muted
+note about backward-compatible migrations. Setting the default writes
+nothing (see the Q3 note above).
+
+A console that predates the door answers that the app is not in this account
+or that the console predates `carlos apps set`. Roll the console and run it
+again. In direct bucket mode (`CARLOS_DEPLOYMENT_BUCKET` or
+`CARLOS_DEPLOYMENT_DIR` set in the shell) `carlos apps set` refuses and says
+so, so run the door check from a shell that is logged in to the console and
+has neither variable set.
+
+## Gate D: docs and changelog
+
+Needs Paul's approval to publish. This gate touches no box.
+
+Run README step 10: `go run ./cmd/changelog` in a full checkout of `main`
+(after `git fetch origin --tags '+refs/amadan/closed/*:refs/amadan/closed/*'`,
+with `gh` and `amadan` signed in), map the release's bullets in
+`docs/site/changelog-links.json`, and land `docs/site/changelog.json` and the
+links file on a branch. Then run `node hack/sync-docs.mjs <checkout>` in
+`carlosframework/website` and ship the site the way its AGENTS.md describes.
+`hack/sync-docs.mjs` and that AGENTS.md live in the website repo (not checked
+here).
+
+Check that carlosframework.com/platform/changelog lists the release and the
+public CLI page shows "Two builds at once". The page's source is
+`docs/site/cli.md:330` here; where the live page sits in the website is not
+checked here. Both must be live before Gate G.
+
+## Gate E: dev, the daemon-reload check, then the flag
+
+Needs Paul's approval, naming the boxes: dev box `i-05498eada6f0afd23` (the
+dev console box, eu-west-1) and dev box `i-046c7fe43620944bc` (eu-west-1),
+profile `kass`. Both ids come from the brief; only the first appears in
+another repo document (`2026-10-06-console-release-defences-plan-4-d4.md`),
+and the second is not checked here. **Do not start this gate while the
+"Before Gate E: owed" list has an open item that Paul has not waived.**
+
+First the index's D1 guard: measure `systemctl daemon-reload` on a dev box
+carrying titogo-scale unit counts.
+
+```bash
+aws --profile kass --region eu-west-1 ssm send-command --instance-ids i-046c7fe43620944bc \
+ --document-name AWS-RunShellScript --query Command.CommandId --output text \
+ --parameters 'commands=["systemctl list-units --all --no-legend | wc -l","for i in 1 2 3; do s=$(date +%s.%N); systemctl daemon-reload; e=$(date +%s.%N); echo \"$e - $s\" | bc; done"]'
+aws --profile kass --region eu-west-1 ssm get-command-invocation --instance-id i-046c7fe43620944bc --command-id <the id printed> --query StandardOutputContent --output text
+```
+
+Expected: a unit count, then three times. If any reload takes more than about
+1 s, stop: plan 2's D1 says nested generation units move to transient units in
+a follow-up before the flag goes on. Otherwise set the flag on both dev
+boxes:
+
+```bash
+aws --profile kass --region eu-west-1 ssm send-command \
+ --instance-ids i-05498eada6f0afd23 i-046c7fe43620944bc --document-name AWS-RunShellScript \
+ --parameters 'commands=["grep -q ^CARLOS_SWAP= /etc/carlos/host.env || echo CARLOS_SWAP=overlap >> /etc/carlos/host.env","systemctl restart carlos-edge"]'
+```
+
+If `host.env` already has a `CARLOS_SWAP=` line, even an empty or different
+one, the command leaves it alone and the box does not arm. Read the file
+first. The unit is `carlos-edge` and it loads `/etc/carlos/host.env`
+(`internal/host/units/carlos-edge.service:18`).
+
+Restarting `carlos-edge` stops every instance on the box (spec section 10).
+Each wakes on its next request.
+
+Then run the probe below against a dev app. Create one with:
+
+```bash
+carlos apps create --app zdd-probe --console https://platform.dev.carlosframework.com
+```
+
+then an operator's `carlos instances enable --app zdd-probe`, and:
+
+```bash
+carlos instances create --app zdd-probe --host <label>.<the account's platform domain>
+```
+
+Use the host it prints as `HOST`. For this gate: `APP=zdd-probe`,
+`CONSOLE=https://platform.dev.carlosframework.com`, and `ACCOUNT` is the dev
+account (an empty `--account` uses your only account).
+
+Check the flag took before trusting the probe: `carlos instances list --app
+zdd-probe` should show a build under SERVING once the box has reported. A
+dash means the box did not arm (see the dash note above).
+
+## Gate F: the flagship probe box
+
+Needs Paul's approval, naming the box and its region.
+
+Find the probe's region with:
+
+```bash
+carlos instances list --app adopt-probe --account bab --console https://console.carloku.com
+```
+
+(REGION column; the instance is `probe1.bab.oncarlos.com`.) The flagship
+boxes, profile `keymail`:
+
+| box | instance id | region |
+| --- | --- | --- |
+| `ie` | i-092c0c1eea75723cb | eu-west-1 |
+| `us` | i-0ed0641d8e476f1b8 | us-east-1 |
+| `uk` | i-03c310ceaf50a9bab | eu-west-2 |
+| `au` | i-022288023f5e7496d | ap-southeast-2 |
+
+`ie`'s id matches the flagship console box id in
+`2026-10-05-scanner-host-rollout.md`. The other three are not checked here.
+
+Run Gate E's flag command against that one box with `--profile keymail` and
+its region. Then run the probe with:
+
+```bash
+APP=adopt-probe ACCOUNT=bab CONSOLE=https://console.carloku.com HOST=probe1.bab.oncarlos.com
+```
+
+## Gate G: the rest of the flagship
+
+Needs Paul's approval, naming each box before it is done.
+
+One box at a time, with Gate E's flag command. After each box, do one
+ordinary deploy of `adopt-probe` and run `carlos instances list --app
+adopt-probe --account bab`, which must show INSTALLED equal to SERVING. Tito
+is Tito's operator's call; do not touch Tito's boxes from here.
+
+## The probe
+
+Spec section 12: zero failed requests under continuous load, and no request
+held longer than a normal response. Run it at Gate E (dev) and Gate F
+(`adopt-probe`).
+
+### 1. Build a probe app that drains on SIGTERM, in scratch
+
+```bash
+mkdir -p "$TMPDIR/zdd-probe" && cd "$TMPDIR/zdd-probe"
+cat > main.go <<'EOF'
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+)
+
+var version = "dev"
+
+func main() {
+ sock := flag.String("socket", "", "unix socket to serve on")
+ flag.Parse()
+ _ = os.Remove(*sock)
+ l, err := net.Listen("unix", *sock)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(50 * time.Millisecond) // keep requests in flight across the switch
+ fmt.Fprintf(w, "ok %s\n", version)
+ })
+ srv := &http.Server{Handler: mux}
+ go func() {
+ ch := make(chan os.Signal, 1)
+ signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
+ <-ch
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ defer cancel()
+ srv.Shutdown(ctx)
+ }()
+ if err := srv.Serve(l); err != nil && err != http.ErrServerClosed {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+EOF
+go mod init zddprobe
+for v in zdd-1 zdd-2 zdd-3 zdd-4; do
+ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-X main.version=$v" -o "probe-$v" .
+done
+```
+
+### 2. A load phase
+
+Four request loops for the given seconds, cache-busted, logging status and
+time:
+
+```bash
+load() { # usage: load <phase> <secs>
+ mkdir -p "$TMPDIR/zdd-probe/$1"
+ for i in 1 2 3 4; do
+ ( end=$((SECONDS+$2)); n=0
+ while [ $SECONDS -lt $end ]; do n=$((n+1))
+ curl -s -o /dev/null -m 35 -w '%{http_code} %{time_total}\n' "https://$HOST/?zdd=$1-$i-$n" >> "$TMPDIR/zdd-probe/$1/load-$i.log"
+ done ) &
+ done
+ wait
+}
+summary() { cat "$TMPDIR/zdd-probe/$1"/load-*.log | awk -v p="$1" '$1 !~ /^2/ {bad++} {if ($2>max) max=$2; n++} END {printf "%s: %d requests, %d failed, slowest %.2fs\n", p, n, bad+0, max}'; }
+```
+
+### 3. Run it
+
+```bash
+cd "$TMPDIR/zdd-probe"
+carlos deploy --app "$APP" --account "$ACCOUNT" --console "$CONSOLE" --version zdd-1 probe-zdd-1
+load baseline 60; summary baseline
+( load overlap 240 ) & sleep 20
+carlos deploy --app "$APP" --account "$ACCOUNT" --console "$CONSOLE" --version zdd-2 probe-zdd-2
+sleep 40
+carlos deploy --app "$APP" --account "$ACCOUNT" --console "$CONSOLE" --version zdd-3 probe-zdd-3
+wait; summary overlap
+# Control: hold mode must show the pause, or the probe proves nothing.
+carlos apps set "$APP" --account "$ACCOUNT" --console "$CONSOLE" --swap hold
+( load hold 120 ) & sleep 20
+carlos deploy --app "$APP" --account "$ACCOUNT" --console "$CONSOLE" --version zdd-4 probe-zdd-4
+wait; summary hold
+carlos apps set "$APP" --account "$ACCOUNT" --console "$CONSOLE" --swap overlap
+```
+
+**Pass.** `overlap` has 0 failed and its slowest is at most the larger of 1 s
+and twice the baseline's slowest. Each deploy's watch ends with the approved
+`swapped https://$HOST` line and a box line, exit 0. The `hold` control's
+slowest is clearly above the baseline. If it is not, the probe cannot see a
+pause and the overlap result means nothing.
+
+The watch prints `swapped https://<host>` (draft word `swapped`,
+`deploywatch.go:459`) and `box <label>: ...` (`deploywatch.go:43`). With the
+flag off it prints `live https://<host>` instead (`deploywatch.go:454`), so a
+`live` line during the overlap phase means that box is not armed.
+
+One caution on the control: the first `--swap hold` on an app creates
+`config.json` and causes one config delivery (the Q3 note above). If the
+hold phase's slowest request is high because of that restart rather than the
+zdd-4 swap, the control is contaminated. If the numbers look odd, rerun the
+hold phase once with the policy already set.
+
+### 4. Clean up
+
+```bash
+rm -rf "$TMPDIR/zdd-probe"
+```
+
+On dev, run `carlos apps delete zdd-probe` once Paul is done with it.
+
+## Names checked against this branch
+
+Checked in the code on `zdd-6-surfaces` (HEAD `77d77b3a` plus this file):
+the verbs and flags `carlos system update|status`, `carlos apps set|create|delete`,
+`carlos instances list|create|enable|retry`, `carlos deploy`, `carlos ship`,
+`carlos promote`, `carlos promote --retry`, `carlos rollback`; the flag
+`CARLOS_SWAP` and its exact value `overlap`; the unit `carlos-edge` and
+`/etc/carlos/host.env`; the SERVING, INSTALLED and REGION columns; the
+`swapped`, `live` and `box` watch lines; `cmd/changelog`;
+`docs/site/changelog.json`, `docs/site/changelog-links.json` and the "Two
+builds at once" section.
+
+Not checked here (not in this repo, or not verifiable without running
+something): `roll-console.sh` and `update-platform-binary.sh` (platform-infrastructure);
+`hack/sync-docs.mjs` and the website's AGENTS.md (carlosframework/website);
+the live URLs; the AWS instance ids except `i-092c0c1eea75723cb` and
+`i-05498eada6f0afd23`; the SSM `AWS-RunShellScript` documents; that `bc`
+exists on the boxes; the dev account and the flagship's channel names; the
+behaviour of the running boxes.