plan: implementation plan for the platform-dev environment
Task breakdown for docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md: dev/ tofu root (buckets+host), DNS in tofu (unlike flagship, this stack's whole DNS footprint fits one DNSimple account so it doesn't need the out-of-state special case), bootstrap+harden over SSM, real ship/promote/ route of rastrillo's hello world, docs+memory wrap-up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 file changed,
+533
−0
docs/superpowers/plans/2026-08-02-platform-dev-environment.md+533 −0
diff --git a/docs/superpowers/plans/2026-08-02-platform-dev-environment.md b/docs/superpowers/plans/2026-08-02-platform-dev-environment.md| new file mode 100644 |
| index 000000000..8b9177084 |
| --- /dev/null |
| +++ b/docs/superpowers/plans/2026-08-02-platform-dev-environment.md |
| @@ -0,0 +1,533 @@ |
| +# Platform Dev Environment Implementation Plan |
| + |
| +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
| + |
| +**Goal:** Stand up `platform.dev.carlosframework.com`, a full-stack CARLOS |
| +platform dev deployment in the repurposed kass AWS account, and prove it |
| +real by getting rastrillo's `examples/helloworld` genuinely live on it |
| +through the real `carlos` binary (not a local-directory demo). |
| + |
| +**Architecture:** A new `dev/` tofu root in `carlosframework/platform-infrastructure`, |
| +built from the exact same reusable modules (`deployment-buckets`, |
| +`region-host`) and operator scripts already used for the flagship and |
| +Tito deployments — same shape, new account, no mail/console/extra |
| +hardening beyond the baseline. DNS lives entirely in the |
| +`carlosframework.com` zone (DNSimple account 285), which this stack can |
| +manage directly via tofu (unlike flagship, which has to special-case it |
| +because flagship's default dnsimple provider is pinned to a different |
| +account). |
| + |
| +**Tech Stack:** OpenTofu (`tofu`) `>= 1.10`, AWS provider `~> 6.0`, |
| +dnsimple provider `~> 1.5`, AWS CLI, SSM Session Manager (no SSH), Go |
| +1.22+ (rastrillo/carlos build), the real `carlos` binary. |
| + |
| +## Global Constraints |
| + |
| +- AWS account: 640012953448 (profile `kass`), region `eu-west-1` — copied |
| + verbatim from `docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md` §2. |
| +- Deployment bucket `carlos-dev-640012953448`, data bucket |
| + `carlos-dev-ie-640012953448` (spec §3). |
| +- Host: one `t4g.micro`, 20 GiB root, `carlos-dev-ie` (spec §3). |
| +- DNS: `platform.dev.carlosframework.com` and `*.dev.carlosframework.com` |
| + → the dev EIP, DNSimple account 285, zone `carlosframework.com` (spec §4). |
| +- No mail.tf, no console, no extra hardening beyond |
| + `scripts/harden-host.sh`, no apps besides rastrillo's hello world (spec §6). |
| +- Nothing account-shaped (tenant hostnames, routes) enters tofu state — |
| + only buckets, the host, and the two static DNS records above. |
| +- Old frozen kass resources (3 stopped `t4g.micro` instances, the |
| + `kass-litestream` S3 bucket) are never touched by any step here. |
| + |
| +--- |
| + |
| +## Task 1: `dev/` tofu root — state, buckets, host |
| + |
| +**Files:** |
| +- Create: `platform-infrastructure/dev/main.tf` |
| +- Create: `platform-infrastructure/dev/region-ie.tf` |
| +- Create: `platform-infrastructure/dev/host-init.sh` |
| +- Create: `platform-infrastructure/dev/outputs.tf` |
| +- Create: `platform-infrastructure/dev/.gitignore` |
| + |
| +**Interfaces:** |
| +- Produces: tofu outputs `ie_eip`, `deployment_bucket`, `data_bucket_ie`, |
| + `host_role_arn`, `host_instance_id` — Task 2 (DNS) and Task 3 (bootstrap) |
| + read these via `tofu output`. |
| + |
| +- [ ] **Step 1: Create the tfstate bucket** (must exist before `tofu init` |
| + with an S3 backend — this is how the flagship/tito state buckets were |
| + bootstrapped too; the bucket itself isn't tofu-managed). |
| + |
| +```bash |
| +cd /tmp/rastrillo-dev-infra/platform-infrastructure # your clone |
| +AWS_PROFILE=kass aws s3api create-bucket \ |
| + --bucket carlos-tfstate-640012953448 --region eu-west-1 \ |
| + --create-bucket-configuration LocationConstraint=eu-west-1 |
| +AWS_PROFILE=kass aws s3api put-bucket-versioning \ |
| + --bucket carlos-tfstate-640012953448 \ |
| + --versioning-configuration Status=Enabled |
| +AWS_PROFILE=kass aws s3api put-public-access-block \ |
| + --bucket carlos-tfstate-640012953448 \ |
| + --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true |
| +``` |
| + |
| +- [ ] **Step 2: Verify the bucket** |
| + |
| +Run: `AWS_PROFILE=kass aws s3api get-bucket-versioning --bucket carlos-tfstate-640012953448` |
| +Expected: `{"Status": "Enabled"}` |
| + |
| +- [ ] **Step 3: Write `dev/main.tf`** |
| + |
| +```hcl |
| +# CARLOS platform dev deployment — AWS account 640012953448 (the kass |
| +# account, repurposed 2026-08-02; see |
| +# docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md |
| +# in carlosframework/platform). Tofu owns boxes, buckets, IAM, and static |
| +# DNS; the platform owns apps, releases, instances, and dynamic DNS. |
| +# Nothing account-shaped may ever enter this state — `tofu apply` must |
| +# always be safe. |
| +# |
| +# The reusable shapes live in ../modules (self-hosters consume them by |
| +# git source URL; see infra/modules/README.md). This root is the thin |
| +# dev-specific instantiation: names, sizes, DNS. No mail, no console, |
| +# no extra apps beyond rastrillo's hello world. |
| + |
| +terraform { |
| + required_version = ">= 1.10" |
| + backend "s3" { |
| + bucket = "carlos-tfstate-640012953448" |
| + key = "dev/terraform.tfstate" |
| + region = "eu-west-1" |
| + profile = "kass" |
| + use_lockfile = true |
| + } |
| + required_providers { |
| + aws = { |
| + source = "hashicorp/aws" |
| + version = "~> 6.0" |
| + } |
| + dnsimple = { |
| + source = "dnsimple/dnsimple" |
| + version = "~> 1.5" |
| + } |
| + } |
| +} |
| + |
| +provider "aws" { |
| + region = "eu-west-1" |
| + profile = "kass" |
| +} |
| + |
| +locals { |
| + account = "640012953448" |
| +} |
| + |
| +# The deployment bucket + the IE regional data bucket (the residency |
| +# promise). Versioning, SSE, public-access block, prevent_destroy — all |
| +# inside the module. |
| +module "buckets" { |
| + source = "git::https://github.com/carlosframework/platform.git//infra/modules/deployment-buckets?ref=main" |
| + deployment_bucket_name = "carlos-dev-${local.account}" |
| + data_bucket_name = "carlos-dev-ie-${local.account}" |
| +} |
| +``` |
| + |
| +- [ ] **Step 4: Write `dev/region-ie.tf`** |
| + |
| +```hcl |
| +# The IE region of the dev deployment: one Graviton box running `carlos |
| +# agent`, behind an Elastic IP. Disposable by construction — all durable |
| +# state is in the buckets. |
| + |
| +module "host_ie" { |
| + source = "git::https://github.com/carlosframework/platform.git//infra/modules/region-host?ref=main" |
| + name = "carlos-dev-ie" |
| + sg_description = "carlos dev IE host" |
| + iam_name = "carlos-dev-host" |
| + instance_type = "t4g.micro" |
| + root_volume_size = 20 |
| + user_data = file("${path.module}/host-init.sh") |
| + deployment_bucket_arn = module.buckets.deployment_bucket_arn |
| + data_bucket_arn = module.buckets.data_bucket_arn |
| + ssm_parameter_arn = "arn:aws:ssm:eu-west-1:${local.account}:parameter/carlos/*" |
| +} |
| +``` |
| + |
| +- [ ] **Step 5: Copy `host-init.sh` from flagship verbatim** |
| + |
| +```bash |
| +cp ../flagship/host-init.sh dev/host-init.sh |
| +``` |
| + |
| +- [ ] **Step 6: Write `dev/outputs.tf`** |
| + |
| +```hcl |
| +output "ie_eip" { |
| + value = module.host_ie.eip_public_ip |
| +} |
| + |
| +output "deployment_bucket" { |
| + value = module.buckets.deployment_bucket |
| +} |
| + |
| +output "data_bucket_ie" { |
| + value = module.buckets.data_bucket |
| +} |
| + |
| +output "host_role_arn" { |
| + value = module.host_ie.role_arn |
| +} |
| + |
| +output "host_instance_id" { |
| + value = module.host_ie.instance_id |
| +} |
| +``` |
| + |
| +(`instance_id` confirmed against `infra/modules/region-host/outputs.tf` |
| +in the platform repo — it exposes `aws_instance.host.id` under that |
| +exact name.) |
| + |
| +- [ ] **Step 7: Write `dev/.gitignore`** (copy flagship's — local `.terraform/` and plan files only, never state) |
| + |
| +```bash |
| +cp ../flagship/.gitignore dev/.gitignore |
| +``` |
| + |
| +- [ ] **Step 8: Init, plan, review** |
| + |
| +```bash |
| +cd dev && tofu init |
| +tofu plan -out dev.plan |
| +``` |
| + |
| +Expected: plan shows resources to add only (buckets, host role/SG/EIP/instance) |
| +— **0 to change, 0 to destroy**. If anything shows as a change or destroy, |
| +stop and re-read the plan before continuing; this root should only ever add. |
| + |
| +- [ ] **Step 9: Apply** |
| + |
| +```bash |
| +tofu apply dev.plan |
| +``` |
| + |
| +Expected: apply completes; `tofu output` prints a real EIP for `ie_eip` |
| +and the two bucket names. |
| + |
| +- [ ] **Step 10: Verify against AWS directly** |
| + |
| +Run: `AWS_PROFILE=kass aws ec2 describe-instances --region eu-west-1 --filters Name=tag:Name,Values=carlos-dev-ie --query 'Reservations[].Instances[].State.Name' --output text` |
| +Expected: `running` |
| + |
| +Run: `AWS_PROFILE=kass aws s3 ls | grep carlos-dev` |
| +Expected: both `carlos-dev-640012953448` and `carlos-dev-ie-640012953448` listed. |
| + |
| +- [ ] **Step 11: Commit** |
| + |
| +```bash |
| +git add dev/ |
| +git commit -m "infra: add the dev deployment root (buckets + IE host)" |
| +``` |
| + |
| +--- |
| + |
| +## Task 2: DNS |
| + |
| +**Files:** |
| +- Create: `platform-infrastructure/dev/dns.tf` |
| + |
| +**Interfaces:** |
| +- Consumes: `module.host_ie.eip_public_ip` from Task 1. |
| +- Produces: `platform.dev.carlosframework.com` and `*.dev.carlosframework.com` |
| + resolving to the dev EIP — Task 5 (hello world) routes through the |
| + wildcard. |
| + |
| +- [ ] **Step 1: Write `dev/dns.tf`** |
| + |
| +```hcl |
| +# Dev addressing (design spec §4): |
| +# platform.dev.carlosframework.com -> the dev edge (control-plane surface) |
| +# *.dev.carlosframework.com -> dev apps (rastrillo's hello world |
| +# lands at helloworld.dev.carlosframework.com) |
| +# |
| +# Unlike flagship, this stack's ENTIRE DNS footprint lives in one zone |
| +# (carlosframework.com, DNSimple account 285) — so, unlike flagship's |
| +# platform.carlosframework.com record, these ARE managed in tofu state. |
| +# |
| +# Reads DNSIMPLE_TOKEN from the environment; never written to a file here. |
| +# export DNSIMPLE_TOKEN=$(cat ~/.dnsimple/carlos.token) before apply. |
| +provider "dnsimple" { |
| + account = "285" |
| +} |
| + |
| +resource "dnsimple_zone_record" "dev_platform" { |
| + zone_name = "carlosframework.com" |
| + name = "platform.dev" |
| + type = "A" |
| + value = module.host_ie.eip_public_ip |
| + ttl = 300 |
| +} |
| + |
| +resource "dnsimple_zone_record" "dev_apps_wildcard" { |
| + zone_name = "carlosframework.com" |
| + name = "*.dev" |
| + type = "A" |
| + value = module.host_ie.eip_public_ip |
| + ttl = 300 |
| +} |
| +``` |
| + |
| +- [ ] **Step 2: Plan and apply** |
| + |
| +```bash |
| +export DNSIMPLE_TOKEN=$(cat ~/.dnsimple/carlos.token) |
| +tofu plan -out dns.plan |
| +``` |
| + |
| +Expected: 2 to add, 0 to change, 0 to destroy. |
| + |
| +```bash |
| +tofu apply dns.plan |
| +``` |
| + |
| +- [ ] **Step 3: Verify resolution** (allow a minute or two for propagation) |
| + |
| +Run: `dig +short platform.dev.carlosframework.com` |
| +Expected: the `ie_eip` value from Task 1. |
| + |
| +Run: `dig +short helloworld.dev.carlosframework.com` |
| +Expected: the same EIP (proves the wildcard). |
| + |
| +- [ ] **Step 4: Commit** |
| + |
| +```bash |
| +git add dev/dns.tf |
| +git commit -m "infra: point platform.dev.carlosframework.com + wildcard at the dev EIP" |
| +``` |
| + |
| +--- |
| + |
| +## Task 3: Bootstrap and harden the box |
| + |
| +**Files:** |
| +- No new files — reuses `scripts/bootstrap-box.sh`, `scripts/harden-host.sh`, |
| + `scripts/provision-keys.sh` from `platform-infrastructure` as-is (all |
| + already parameterized by AWS profile; no code changes needed). |
| + |
| +**Interfaces:** |
| +- Consumes: `host_instance_id`, `deployment_bucket`, `data_bucket_ie` outputs |
| + from Task 1. |
| +- Produces: a running `carlos-edge` service on the box, and a release |
| + keypair whose pubkey Task 5 needs for `host.env` (already written by |
| + bootstrap) and whose seed Task 5's `ship-app.sh` reads from SSM. |
| + |
| +- [ ] **Step 1: Build the `carlos` binary for arm64 and upload it as the bootstrap artifact** |
| + |
| +```bash |
| +cd /path/to/carlosframework/platform |
| +GOOS=linux GOARCH=arm64 go build -o /tmp/carlos-arm64 . |
| +AWS_PROFILE=kass aws s3 cp /tmp/carlos-arm64 s3://carlos-dev-640012953448/bootstrap/carlos |
| +``` |
| + |
| +- [ ] **Step 2: Generate and stage this deployment's release key** |
| + |
| +```bash |
| +cd /path/to/platform-infrastructure |
| +CARLOS_BIN=/tmp/carlos-arm64-host-build-not-needed # see note below |
| +``` |
| + |
| +Note: `provision-keys.sh` only needs a *local* `carlos` binary to run |
| +`release-keygen` (pure Go, no host access) — the host's arch doesn't |
| +matter for this step. Build a local-arch binary instead: |
| + |
| +```bash |
| +go build -o /tmp/carlos-local . |
| +CARLOS_BIN=/tmp/carlos-local scripts/provision-keys.sh kass /tmp/dev-pubkey.txt |
| +``` |
| + |
| +Expected output: `seed stored in SSM /carlos/release-key (kass); pubkey in /tmp/dev-pubkey.txt` |
| + |
| +- [ ] **Step 3: Verify the seed landed, byte-for-byte, without printing it** |
| + |
| +```bash |
| +AWS_PROFILE=kass aws ssm get-parameter --name /carlos/release-key \ |
| + --with-decryption --query Parameter.Value --output text | wc -c |
| +``` |
| + |
| +Expected: matches the known length of an base64/hex-encoded Ed25519 seed |
| +(non-zero, consistent with the flagship/Tito keys — do not print the value |
| +itself, only its length, to keep it out of the transcript). |
| + |
| +- [ ] **Step 4: Bootstrap the box over SSM** |
| + |
| +```bash |
| +INSTANCE=$(cd /path/to/platform-infrastructure/dev && tofu output -raw host_instance_id) |
| +scripts/bootstrap-box.sh kass "$INSTANCE" carlos-dev-640012953448 carlos-dev-ie-640012953448 /tmp/dev-pubkey.txt |
| +``` |
| + |
| +Expected: final lines `active` (carlos-edge is running) and a `carlos |
| +version` string. |
| + |
| +- [ ] **Step 5: Harden the box** |
| + |
| +```bash |
| +scripts/harden-host.sh kass "$INSTANCE" |
| +``` |
| + |
| +Expected: `status: Success`, output ending `hardening staged for: (restart |
| +pending per-unit)` (no tenant units yet — that's fine, Task 5 adds one). |
| + |
| +- [ ] **Step 6: Commit** (nothing to commit — this task is pure operations |
| + against live infra, no repo changes. Skip straight to Task 4.) |
| + |
| +--- |
| + |
| +## Task 4: Ship and route rastrillo's hello world for real |
| + |
| +**Files:** |
| +- No new files in `platform-infrastructure` — reuses `scripts/ship-app.sh`. |
| +- Reference: `carlosframework/rastrillo/examples/helloworld` (already |
| + built and verified locally per the v1 walking-skeleton work — see |
| + `carlos-framework-project` memory). |
| + |
| +**Interfaces:** |
| +- Consumes: `deployment_bucket` output from Task 1, the release key staged |
| + in Task 3, `platform.dev.carlosframework.com` / `*.dev.carlosframework.com` |
| + resolving from Task 2. |
| +- Produces: `https://helloworld.dev.carlosframework.com/` serving 200 — |
| + the deliverable this whole plan exists for. |
| + |
| +- [ ] **Step 1: Build the hello-world binary for arm64** |
| + |
| +```bash |
| +cd /path/to/rastrillo/examples/helloworld |
| +GOOS=linux GOARCH=arm64 go build -o /tmp/helloworld-arm64 ./cmd/helloworld |
| +``` |
| + |
| +- [ ] **Step 2: Ship and promote it for real** |
| + |
| +```bash |
| +cd /path/to/platform-infrastructure |
| +VER=$(git -C /path/to/rastrillo rev-parse --short HEAD) |
| +CARLOS_BIN=/tmp/carlos-local scripts/ship-app.sh kass carlos-dev-640012953448 \ |
| + helloworld "$VER" stable binary /tmp/helloworld-arm64 |
| +``` |
| + |
| +Expected: no error output; `ship` then `promote` both exit 0. |
| + |
| +- [ ] **Step 3: Verify the release landed in the bucket** |
| + |
| +Run: `AWS_PROFILE=kass aws s3 ls s3://carlos-dev-640012953448/apps/helloworld/` |
| +Expected: a release object matching `$VER`, plus a `channels/stable` |
| +pointer. |
| + |
| +- [ ] **Step 4: Route it, over SSM (the agent on the box owns the registry)** |
| + |
| +```bash |
| +INSTANCE=$(cd dev && tofu output -raw host_instance_id) |
| +d=$(mktemp -d) |
| +cat > "$d/route.sh" <<'EOF' |
| +set -e |
| +export $(grep -v '^#' /etc/carlos/host.env | xargs) |
| +/opt/carlos/carlos add -app helloworld -kind instance -channel stable helloworld.dev.carlosframework.com |
| +sleep 5 |
| +curl -s --max-time 20 https://helloworld.dev.carlosframework.com/ |
| +echo |
| +curl -s -o /dev/null -w 'healthz: %{http_code}\n' https://helloworld.dev.carlosframework.com/healthz |
| +EOF |
| +jq -n --rawfile s "$d/route.sh" '{commands: ($s | split("\n"))}' > "$d/params.json" |
| +CMDID=$(AWS_PROFILE=kass aws ssm send-command --instance-ids "$INSTANCE" \ |
| + --document-name AWS-RunShellScript --parameters "file://$d/params.json" \ |
| + --query Command.CommandId --output text) |
| +sleep 20 |
| +AWS_PROFILE=kass aws ssm get-command-invocation --command-id "$CMDID" \ |
| + --instance-id "$INSTANCE" --query '[Status,StandardOutputContent,StandardErrorContent]' --output text |
| +``` |
| + |
| +Expected: `Status` is `Success`; stdout includes `Hello, World — this is a |
| +rastrillo app.` and `healthz: 200`. |
| + |
| +- [ ] **Step 5: Verify from outside the box, with a real cert** |
| + |
| +Run: `curl -sv https://helloworld.dev.carlosframework.com/ 2>&1 | grep -i "SSL certificate verify ok\|subject:"` |
| +Expected: a verified chain (real Let's Encrypt cert — this is the |
| +"actually deployed on the platform" proof; not `-dev` mode, not a local |
| +directory store). |
| + |
| +- [ ] **Step 6: Update the rastrillo README's "Try it" section to also |
| + point at the live dev URL, in `rastrillo/README.md`** |
| + |
| +Find the `## See also` section and add a line above it: |
| + |
| +```markdown |
| +## Live |
| + |
| +`https://helloworld.dev.carlosframework.com` — the v1 walking skeleton's |
| +hello world, deployed for real on the platform-dev environment (see |
| +`carlosframework/platform`'s |
| +`docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md`). |
| +``` |
| + |
| +- [ ] **Step 7: Commit the README change in the rastrillo repo** |
| + |
| +```bash |
| +cd /path/to/rastrillo |
| +git add README.md |
| +git commit -m "docs: link the real live hello-world deployment on platform-dev" |
| +git push |
| +``` |
| + |
| +--- |
| + |
| +## Task 5: Wrap up — infra README and memory |
| + |
| +**Files:** |
| +- Modify: `platform-infrastructure/README.md` |
| +- Modify (memory, not a repo file): `carlos-framework-project.md` and |
| + `carlos-platform-project.md` under |
| + `/home/paulca/.claude/projects/-home-paulca-github-com-carlosframework-platform/memory/` |
| + |
| +- [ ] **Step 1: Add a `dev/` bullet to `platform-infrastructure/README.md`**, |
| + matching the existing flagship/tito bullet style: |
| + |
| +```markdown |
| +- `dev/` — the platform-dev deployment (AWS 640012953448, the kass |
| + account repurposed 2026-08-02): `platform.dev.carlosframework.com`, |
| + `*.dev.carlosframework.com`. |
| +``` |
| + |
| +- [ ] **Step 2: Commit** |
| + |
| +```bash |
| +git add README.md |
| +git commit -m "docs: note the dev deployment root" |
| +git push origin HEAD:add-dev-deployment # or whatever branch was used for Tasks 1-4 |
| +``` |
| + |
| +- [ ] **Step 3: Open a PR for the `platform-infrastructure` changes** |
| + (Tasks 1, 2, 5's README bullet — Task 3/4 are pure ops, no diff). |
| + |
| +```bash |
| +gh pr create --repo carlosframework/platform-infrastructure --draft \ |
| + --title "infra: add the platform-dev deployment (kass account, repurposed)" \ |
| + --body "Implements docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md from carlosframework/platform. New dev/ root: buckets, one IE host, DNS (platform.dev.carlosframework.com + *.dev.carlosframework.com). Bootstrapped, hardened, and proven live with rastrillo's hello world at https://helloworld.dev.carlosframework.com/ (real cert, real S3-backed bucket, real carlos binary — not a local-dir demo)." |
| +``` |
| + |
| +- [ ] **Step 4: Update memory** — rewrite the relevant sections of |
| + `carlos-framework-project.md` (rastrillo now has a REAL platform proof, |
| + not just the local-dir demo; link to `helloworld.dev.carlosframework.com`) |
| + and `carlos-platform-project.md` (kass account 640012953448 is now the |
| + platform-dev account, its teardown plan is superseded — update |
| + `session-exec-state.md`'s operator-queue item 2 accordingly, it no |
| + longer applies). |
| + |
| +--- |
| + |
| +## Self-review notes (for whoever executes this) |
| + |
| +- Every AWS/SSM/tofu command above is real and mutates real billed |
| + infrastructure or a real DNS zone. If any verification step's actual |
| + output doesn't match "Expected," stop and diagnose before the next |
| + step — don't proceed past a mismatch assuming it'll self-correct later. |
| +- The release key generated in Task 3 is new and dev-only — never reuse |
| + the flagship or Tito signing keys here, and never print the key |
| + material itself to a terminal or transcript (only its length, or |
| + `Success`/`Failed` status). |