carlos / platform Public

plan: console release defences, plan 4 of 4 (D4 and rollout)

The release recipe builds and rolls its console dev first. roll-console.sh
in carloku-infrastructure gains a dev target, one artifact for every
target, a binding check and the dev-proof carry. platform-infrastructure's
copy is retired. A rollout runbook, and the operational run, gated on
Paul at each production step. The flagship console's binding was
confirmed wrong on 2026-10-06.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 136ce440565d4a7fcb8d9578b39183de294ccab1 parent 9d64bbc
1 file changed, +439 −0
  • docs/superpowers/plans/2026-10-06-console-release-defences-plan-4-d4.md +439 −0

CI cancelled — superseded by a newer push — run details

diff --git a/docs/superpowers/plans/2026-10-06-console-release-defences-plan-4-d4.md b/docs/superpowers/plans/2026-10-06-console-release-defences-plan-4-d4.md
new file mode 100644
index 000000000..53cd5be7c
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-06-console-release-defences-plan-4-d4.md
@@ -0,0 +1,439 @@
+# Console release defences, Plan 4 of 4: D4 (the release carries a console; the roll goes dev first) and the rollout
+
+> **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.
+
+**Goal:** Each public release publishes a signed console artifact. The flagship
+console is rolled with exactly those bytes, through dev first, carrying dev's
+proof. The four defences are switched on in the order spec §6 requires.
+
+**Architecture:** Three repos. The platform README's release recipe gains the
+console build and a console roll step. `carloku-infrastructure/scripts/roll-console.sh`
+gains:
+- a `dev` target;
+- `--artifact` and a single `-trimpath` build;
+- a binding check;
+- the dev-proof carry, with a one-time `--bootstrap`.
+
+The rollout is a runbook section in carloku-infrastructure's README, run by an
+operator over SSM. It is not code.
+
+**Tech Stack:** bash, the `carlos` CLI (with Plans 1–3 built), AWS CLI with
+SSM, `jq`.
+
+**Spec:** `docs/superpowers/specs/2026-10-06-console-release-defences-design.md`
+(rev 5): §5 (D4), §6 (rollout). **Depends on Plans 1–3 being merged and in a
+release.** The script calls `carlos proof get|put` (Plan 3) and relies on the
+box's settle check (Plan 1).
+
+## Global Constraints
+
+- The release builds `console-linux-arm64` and `console-linux-amd64` with `go build -trimpath -ldflags "-s -w -X github.com/carlosframework/platform/internal/version.Build=vX.Y.Z" ./cmd/console` and lists them in the signed `SHA256SUMS.txt` (spec §5.1).
+- Dev coordinates: profile `kass`, bucket `carlos-dev-640012953448`, console box `i-05498eada6f0afd23`, origin `platform.dev.carlosframework.com`, region eu-west-1 (spec §5.2).
+- Flagship coordinates: profile `keymail`, bucket `carlos-flagship-271376211898`, console box `i-092c0c1eea75723cb`, origin `platform.carlosframework.com`.
+- The script refuses a flagship roll without a dev proof, except with `--bootstrap`, which prints that it is skipping (spec §5.2).
+- The script keeps its final `systemctl restart` on any box whose binding is wrong (spec §5.2, §2.2).
+- The public `carlos-linux-*` binary is never a console artifact, and the recipe says so (spec §5.2).
+- README prose in both repos goes through the copy-review skill before it is committed (Paul's standing rule for READMEs). Plans 1 and 2's README notes need the same gate. The copy rule: no em dashes in new prose (`[[copy-voice-no-em-dashes]]`).
+- Tito is a third party: the script's `tito` target is left as it is and never run by this plan.
+- **Plan decision:** the console routes' readiness uses the TCP-connect fallback (spec §2.4 rule 2) on `127.0.0.1:8090`, not a configured probe. The console's `/healthz` answers as soon as its listener is up, so a probe would check nothing more than the connect does. No box-local CLI sets `registry.Route.Health` on a console row today; adding one is a follow-up, needed only if that equivalence ever stops holding.
+
+## Review Focus
+
+1. **The public release's `SHA256SUMS.txt` lacks a console line** (an older release, or a recipe step skipped). Expect: `--artifact` refuses an unverifiable file, never ships it. Covered in Task 2 (`verify_artifact`).
+2. **Dev has the build but its proof isn't minted yet** (inside the 15 minutes). Expect: the flagship target exits non-zero naming how long dev has had it, with nothing shipped to flagship. Task 2's dry-run check.
+3. **The binding on a box is wrong.** Expect: the script says so, keeps its final restart, and still verifies `/proc/<pid>/exe`. Task 2's dry-run check against the flagship box today (stamped on `console.carloku.com`, while `ExecStart` names `platform.carlosframework.com`; confirm on the box).
+4. **An operator rolls a hotfix sha, not a release.** Expect: one `-trimpath` build reused for dev and flagship, so the proof's hash matches. Task 2's build-once rule.
+5. **Someone runs the old two-repo habit** (platform-infrastructure's copy of the script for flagship). Expect: that copy says it is retired and points here. Task 3.
+
+---
+
+### Task 1: The release recipe publishes a console and rolls it dev first
+
+**Files:**
+- Modify: `README.md` (platform repo): "Releasing the platform" steps 2, 4 and 6, and a new console step after "Last, roll it"
+
+- [ ] **Step 1: Draft the recipe changes**
+
+Step 2, append:
+
+```markdown
+ Build the console twice, from the same clean checkout:
+ `CGO_ENABLED=0 GOOS=linux GOARCH=<arch> go build -trimpath -ldflags "-s -w -X github.com/carlosframework/platform/internal/version.Build=vX.Y.Z" -o console-linux-<arch> ./cmd/console`
+ for arm64 and amd64. The console is `./cmd/console`. The `carlos` binary is
+ never a console artifact; shipping it as one took the flagship console down
+ on 2026-10-06.
+```
+
+Step 4: "over the eight files" becomes "over the ten files". Step 6: "with all
+ten files" becomes "with all twelve files".
+
+After the "Last, roll it" paragraph, add:
+
+```markdown
+**Roll the console.** After the boxes are current, roll the release's own
+console bytes, dev first:
+
+ curl -sSLO $rel/console-linux-arm64 # verified against SHA256SUMS.txt like the CLI above
+ carloku-infrastructure/scripts/roll-console.sh --artifact console-linux-arm64 --version vX.Y.Z - dev
+ # wait for dev's proof: 15 minutes of the build running healthily
+ carloku-infrastructure/scripts/roll-console.sh --artifact console-linux-arm64 --version vX.Y.Z - flagship
+
+The flagship roll refuses until dev has proven those exact bytes.
+```
+
+- [ ] **Step 2: Put the new prose through copy review**
+
+Invoke the `copy-review` skill on the three new passages above. Apply Paul's
+edits verbatim.
+
+- [ ] **Step 3: Commit (platform repo, implementation branch)**
+
+```bash
+git add README.md
+git commit -m "README: the release builds and rolls its console, dev first (console defences D4)"
+```
+
+---
+
+### Task 2: `roll-console.sh`: dev target, one artifact, binding check, proof carry
+
+**Files:**
+- Modify: `~/github.com/carlosframework/carloku-infrastructure/scripts/roll-console.sh` (whole file below)
+- Create the work on a branch in that repo, `console-release-defences`. It is a GitHub repo: commit on a branch and open a PR for Paul. Do not merge.
+
+**Interfaces:**
+- Consumes: `ship-app.sh <profile> <bucket> <app> <ver> <channel> <kind> <artifact>` (same directory, unchanged); `carlos proof get --app console --sha256 <S>` and `carlos proof put --app console <file>` (Plan 3); `carlos version` (prints `carlos <build>`).
+- Produces: `roll-console.sh [--artifact FILE --version V] [--bootstrap] <platform-checkout|-> dev|flagship|tito`.
+
+- [ ] **Step 1: Replace the script**
+
+```bash
+#!/bin/bash
+# 🤖 Roll the console: build (or take) ONE artifact, ship + promote it,
+# verify the running binary. Console release defences D4 (platform spec
+# 2026-10-06-console-release-defences-design.md §5.2).
+#
+# roll-console.sh [--artifact FILE --version V] [--bootstrap] <platform-checkout|-> <dev|flagship|tito>
+#
+# --artifact rolls exactly FILE (a release's public console-linux-arm64,
+# checked against that release's signed SHA256SUMS.txt first), so dev and
+# flagship run the bytes the public got. Without it the script builds
+# ./cmd/console ONCE at the checkout's HEAD with -trimpath and keeps the file
+# in $CONSOLE_BUILD_DIR (default ~/.cache/roll-console/<sha>) for the next
+# target: a second build would be different bytes, and flagship's dev proof
+# names bytes.
+#
+# flagship refuses unless dev has proven those bytes (`carlos proof get`
+# against dev). --bootstrap skips that once, for the roll that installs D3.
+#
+# The box adopts and restarts the console itself; its settle check rolls back
+# a build that does not come up. The old adopt + restart SSM steps run only on
+# a box whose console.service ExecStart does not name the unit-stamped route's
+# live link, where the settle check cannot judge the restart.
+set -euo pipefail
+
+artifact= version= bootstrap=0
+while [ $# -gt 2 ]; do
+ case $1 in
+ --artifact) artifact=$2; shift 2;;
+ --version) version=$2; shift 2;;
+ --bootstrap) bootstrap=1; shift;;
+ *) echo "unknown flag $1" >&2; exit 2;;
+ esac
+done
+repo=${1:?usage: roll-console.sh [--artifact FILE --version V] [--bootstrap] <platform-checkout|-> <dev|flagship|tito>}
+target=${2:?target: dev, flagship or tito}
+scripts=$(cd "$(dirname "$0")" && pwd)
+
+# name -> "aws-profile bucket instance-id console-origin"
+coords() {
+ case $1 in
+ dev) echo "kass carlos-dev-640012953448 i-05498eada6f0afd23 platform.dev.carlosframework.com";;
+ flagship) echo "keymail carlos-flagship-271376211898 i-092c0c1eea75723cb platform.carlosframework.com";;
+ tito) echo "titogo carlos-tito-platform-665106592142 i-025a0b70f3d93304d carlos.tito.io";;
+ *) echo "unknown deployment $1: dev, flagship or tito" >&2; exit 2;;
+ esac
+}
+read -r profile bucket instance origin <<<"$(coords "$target")"
+
+# A native carlos to drive ship/promote/proof. From the checkout when one is
+# given, else the one on PATH (it must have `proof`, i.e. Plans 1-3).
+tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
+if [ "$repo" != - ]; then
+ if [ -n "$(git -C "$repo" status --porcelain)" ]; then
+ echo "refusing to build: $repo has uncommitted changes" >&2; exit 1
+ fi
+ (cd "$repo" && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X github.com/carlosframework/platform/internal/version.Build=$(git -C "$repo" rev-parse --short HEAD)" -o "$tmp/carlos" .)
+ carlos=$tmp/carlos
+else
+ carlos=$(command -v carlos)
+fi
+"$carlos" proof -h >/dev/null 2>&1 || { echo "$carlos has no 'proof' verb: build from a checkout with the console release defences" >&2; exit 1; }
+
+if [ -n "$artifact" ]; then
+ [ -n "$version" ] || { echo "--artifact needs --version (the release tag)" >&2; exit 2; }
+ sums=$(dirname "$artifact")/SHA256SUMS.txt
+ [ -f "$sums" ] || { echo "no $sums next to $artifact: download the release's SHA256SUMS.txt and .sig and verify them first (README, Releasing the platform)" >&2; exit 1; }
+ want=$(awk -v f="$(basename "$artifact")" '$2==f {print $1}' "$sums")
+ [ -n "$want" ] || { echo "$sums has no line for $(basename "$artifact"): this release did not publish a console" >&2; exit 1; }
+ got=$(sha256sum "$artifact" | cut -d' ' -f1)
+ [ "$got" = "$want" ] || { echo "$artifact sha256 $got does not match $sums ($want)" >&2; exit 1; }
+ file=$artifact
+else
+ [ "$repo" != - ] || { echo "no --artifact and no checkout to build from" >&2; exit 2; }
+ version=$(git -C "$repo" rev-parse --short HEAD)
+ dir=${CONSOLE_BUILD_DIR:-$HOME/.cache/roll-console/$version}
+ file=$dir/console-linux-arm64
+ if [ ! -f "$file" ]; then
+ mkdir -p "$dir"
+ (cd "$repo" && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath \
+ -ldflags "-s -w -X github.com/carlosframework/platform/internal/version.Build=$version" -o "$file" ./cmd/console)
+ echo "built $file (kept for the next target)"
+ fi
+fi
+sha=$(sha256sum "$file" | cut -d' ' -f1)
+echo "== console $version sha256 $sha -> $target"
+
+# ssm_run <command>: one command on this target's console box (jq-built
+# parameters; see the 2026-08-09 note in git history for why).
+ssm_run() {
+ local params id status
+ params=$(jq -n --arg c "$1" '{commands: [$c]}')
+ id=$(aws --profile "$profile" --region eu-west-1 ssm send-command --instance-ids "$instance" \
+ --document-name AWS-RunShellScript --parameters "$params" --query Command.CommandId --output text)
+ aws --profile "$profile" --region eu-west-1 ssm wait command-executed --command-id "$id" --instance-id "$instance" || true
+ status=$(aws --profile "$profile" --region eu-west-1 ssm list-command-invocations --command-id "$id" --details \
+ --query 'CommandInvocations[0].CommandPlugins[0].Status' --output text)
+ aws --profile "$profile" --region eu-west-1 ssm list-command-invocations --command-id "$id" --details \
+ --query 'CommandInvocations[0].CommandPlugins[0].Output' --output text
+ [ "$status" = Success ]
+}
+
+# The binding: which live link console.service runs, and which route carries
+# the unit stamp. Equal means the box's settle check judges the restart.
+binding=$(ssm_run 'exec=$(systemctl show -p ExecStart console.service | sed -n "s/.*path=\([^ ;]*\).*/\1/p"); \
+ stamped=$(sqlite3 -readonly /data/registry.db "select host from routes where unit=\"console.service\"" | tr "\n" " "); \
+ echo "$exec|$stamped"')
+exec_link=${binding%%|*}; stamped=${binding#*|}
+bound=0
+[ "$exec_link" = "/opt/carlos/live/${stamped% }" ] && bound=1
+if [ $bound = 1 ]; then
+ echo " binding ok: console.service runs $exec_link, stamped on ${stamped% }"
+else
+ echo " binding WRONG: console.service runs ${exec_link:-?}, unit stamp on '${stamped% }' — keeping the final restart (fix: platform spec §6 step 1)"
+fi
+
+if [ "$target" = flagship ]; then
+ proof=$tmp/proof.json
+ if AWS_PROFILE=kass AWS_REGION=eu-west-1 CARLOS_DEPLOYMENT_BUCKET=carlos-dev-640012953448 \
+ "$carlos" proof get --app console --sha256 "$sha" >"$proof"; then
+ echo " dev proof found"
+ elif [ $bootstrap = 1 ]; then
+ echo " --bootstrap: NOT checking for a dev proof (only for the roll that installs D3)"
+ proof=
+ else
+ echo "flagship: dev has not proven $sha (above); roll it to dev and wait, or --bootstrap once" >&2
+ exit 1
+ fi
+fi
+
+CARLOS_BIN=$carlos "$scripts/ship-app.sh" "$profile" "$bucket" console "$version" canary/console binary "$file"
+if [ "$target" = flagship ] && [ -n "${proof:-}" ]; then
+ AWS_PROFILE=keymail AWS_REGION=eu-west-1 CARLOS_DEPLOYMENT_BUCKET=carlos-flagship-271376211898 \
+ "$carlos" proof put --app console "$proof"
+fi
+
+if [ $bound = 0 ]; then
+ ssm_run "bash -c 'set -a; source /etc/carlos/host.env; /opt/carlos/carlos adopt' 2>&1 | tail -8"
+ ssm_run "systemctl restart console && sleep 2 && systemctl is-active console"
+fi
+
+echo "== $target: waiting for the running binary to be this build"
+for i in $(seq 1 24); do
+ exe=$(ssm_run 'readlink /proc/$(systemctl show -p MainPID --value console)/exe' || true)
+ case $exe in */versions/$version/console*) break;; esac
+ sleep 5
+done
+case $exe in
+ */versions/$version/console*) ;;
+ *) echo "$target: console is running ${exe:-nothing}, not versions/$version/console — check the box's adopt journal for a settle rollback" >&2; exit 1;;
+esac
+curl -fsS -o /dev/null "https://$origin/"
+echo "== $target: console $version live at https://$origin"
+```
+
+Checked on the flagship console box on 2026-10-06: `/usr/bin/sqlite3` is
+present, and `ExecStart` is `/opt/carlos/live/platform.carlosframework.com`,
+while `carlos instances` shows the `console.service` stamp on
+`console.carloku.com`. That is the wrong binding Review Focus 3 expects. Run
+`ssm_run 'command -v sqlite3'` against dev once as well. If dev lacks it,
+install it there (`apt-get install -y sqlite3`) rather than branch the
+script.
+
+- [ ] **Step 2: Static checks**
+
+Run: `bash -n scripts/roll-console.sh && shellcheck -S warning scripts/roll-console.sh`
+Expected: no output. Fix every warning except SC2016 on the single-quoted
+SSM command strings, which are expanded on the box on purpose.
+
+- [ ] **Step 3: Read-only check against dev and flagship**
+
+Temporarily add `[ "${ROLL_DRY:-0}" = 1 ] && exit 0` immediately after the
+binding report. Run
+`ROLL_DRY=1 scripts/roll-console.sh - dev` and
+`ROLL_DRY=1 scripts/roll-console.sh - flagship` with the sandbox off
+(assumed-role profiles: `[[aws-from-the-sandbox]]`).
+
+Expected:
+- dev prints its binding line;
+- flagship prints `binding WRONG`, unless someone already restamped it, which rollout step 1 does;
+- nothing is shipped.
+
+Then remove the line.
+
+- [ ] **Step 4: Commit and open the PR (carloku-infrastructure)**
+
+```bash
+cd ~/github.com/carlosframework/carloku-infrastructure
+git checkout -b console-release-defences
+git add scripts/roll-console.sh
+git commit -m "roll-console: dev target, one artifact for every target, binding check, dev proof before flagship"
+git push -u origin console-release-defences
+gh pr create --title "roll-console: dev first, one artifact, dev proof (console release defences D4)" \
+ --body "Implements platform spec 2026-10-06-console-release-defences-design.md §5.2. Not to be run until the platform release carrying Plans 1-3 is on the boxes.
+
+🤖 Generated with [Claude Code](https://claude.com/claude-code)"
+```
+
+This repo lives on GitHub, not amadan, so a PR is the right door here.
+Leave it for Paul to merge.
+
+---
+
+### Task 3: Retire platform-infrastructure's copy of the script
+
+**Files:**
+- Modify: `~/amadan.net/carlos/platform-infrastructure/scripts/roll-console.sh`: replace the body with a pointer
+
+- [ ] **Step 1: Replace the body**
+
+```bash
+#!/bin/bash
+# Retired 2026-10: the console roll lives in carloku-infrastructure
+# (scripts/roll-console.sh), which rolls dev first and carries dev's proof
+# to flagship. Platform spec 2026-10-06-console-release-defences-design.md §5.2.
+echo "roll-console.sh moved to carloku-infrastructure/scripts/roll-console.sh (dev, flagship, tito)" >&2
+exit 2
+```
+
+- [ ] **Step 2: Commit on a branch in that repo**
+
+Read that repo's own instructions first (its README or AGENTS.md) for where
+and how changes land. Then commit:
+`git commit -m "roll-console: retired, see carloku-infrastructure"`.
+
+---
+
+### Task 4: The rollout runbook
+
+**Files:**
+- Modify: `~/github.com/carlosframework/carloku-infrastructure/README.md`: add a section "Console release defences: rollout", on the Task 2 branch
+
+- [ ] **Step 1: Write the section**
+
+The commands below are the spec §6 steps, made concrete. Each SSM command is
+run with `.claude/ssm.sh <profile> <region> <instance> '<cmd>'` or the
+script's `ssm_run` pattern.
+
+````markdown
+## Console release defences: rollout
+
+Run once, after the platform release that carries the defences is published.
+In order:
+
+1. **Fix the bindings.** On each console box (dev `i-05498eada6f0afd23`,
+ flagship `i-092c0c1eea75723cb`), the one route stamped with
+ `console.service` must be the host its `ExecStart` names:
+
+ systemctl show -p ExecStart console.service # path=/opt/carlos/live/<host>
+ /opt/carlos/carlos route --host <that host> --unit console.service
+ /opt/carlos/carlos route --host <any other stamped host> --unit ""
+
+ On flagship today that means stamping `platform.carlosframework.com` and
+ clearing `console.carloku.com`.
+2. **Roll the boxes** with `carlos system update --console <origin> --version vX.Y.Z`
+ on dev and flagship, then `carlos system status`.
+3. **Dev proving.** On dev's console box, as root:
+
+ /opt/carlos/carlos proof keygen --out /etc/carlos/proving.key # prints the public key
+ cat > /etc/carlos/proof-policy.json <<'EOF'
+ {"schema":1,"prove":{"apps":["console"],"prover":"dev","after":"15m","key_file":"/etc/carlos/proving.key"}}
+ EOF
+ chmod 0644 /etc/carlos/proof-policy.json
+
+ Record the public key here: `dev proving key: <base64>`.
+4. **Roll the release's console to dev:**
+ `scripts/roll-console.sh --artifact console-linux-arm64 --version vX.Y.Z - dev`.
+ Watch the box journal for `settled on`.
+5. **Rehearse on dev**, then delete each rehearsal release from dev's bucket
+ (`apps/<acct>/console/releases/<ver>/`) and its staged copy on the box:
+ - ship the public `carlos-linux-arm64` as console `rehearse-cli` with a
+ **v0.24.1** `carlos`, direct-bucket mode (`CARLOS_DEPLOYMENT_BUCKET`
+ set). Promote it, and expect the box journal to show `not pointed live`
+ with no restart;
+ - ship a console built from `./cmd/console` with `os.Exit(2)` as the first
+ line of `main`, as `rehearse-exit`. Expect a rollback within 30 s, a
+ refusal, and an alert email. A plain re-promote stays refused, and
+ `carlos promote --retry rehearse-exit canary/console` retries;
+ - ship a console whose `serve` sleeps forever before listening, as
+ `rehearse-hang`. Expect a deadline rollback that replaces the process.
+6. **Wait for dev's proof** of step 4's build: `carlos proof get --app console --sha256 <sha>`
+ against dev.
+7. **Roll it to flagship:** `scripts/roll-console.sh --artifact console-linux-arm64 --version vX.Y.Z - flagship`.
+8. **Switch flagship's requirement on.** On flagship's console box, as root:
+
+ cat > /etc/carlos/proof-policy.json <<'EOF'
+ {"schema":1,"require":[{"app":"console","prover":"dev","key":"<dev proving key>"}]}
+ EOF
+ chmod 0644 /etc/carlos/proof-policy.json
+
+ The live build is unaffected. It enters the box's healthy history after
+ 15 minutes.
+
+**Break glass:** on flagship, `rm /etc/carlos/proof-policy.json`, then
+`/opt/carlos/carlos adopt --stage-only`. The box logs hourly that the
+requirement is off until the file is back.
+
+The policy files are not in tofu `user_data` on purpose: a whole-root apply
+restarts every box.
+````
+
+The `carlos proof keygen --out` flag name must match what Plan 3 defines.
+Check Plan 3's `proof keygen` task and use its exact flags here before
+committing.
+
+- [ ] **Step 2: Put the section through copy review**
+
+Invoke the `copy-review` skill on the section's prose (not the commands).
+Apply Paul's edits verbatim.
+
+- [ ] **Step 3: Commit on the Task 2 branch and push**
+
+```bash
+git add README.md
+git commit -m "README: console release defences rollout runbook"
+git push
+```
+
+---
+
+### Task 5: Run the rollout
+
+This task is operational and outward-facing: it changes production boxes.
+**Confirm with Paul before each of steps 1, 7 and 8** of the runbook.
+Steps 2–6 touch only dev or are read-only on flagship, so they need his
+go-ahead once, at the start.
+
+- [ ] **Step 1:** Run runbook steps 1–8 in order, pasting each command's output into the branch's amadan discussion (`amadan discuss new carlos/platform -branch <impl branch> -title "Console release defences rollout"`).
+- [ ] **Step 2:** After step 8, check `carlos status --live` against flagship shows no refusals and `curl -sI https://console.carloku.com/ | grep -i x-carlos-version` names vX.Y.Z.
+- [ ] **Step 3:** Update the `[[console-release-defences]]` memory with what was rolled and when.