plan: pipelines slice 1 — the ladder, enforced and visible
Six tasks: the rung vocabulary, ordered promotion with an explicit hotfix bypass, an audit trail, the console ladder view behind step-up for stable, the adopt-timer arming fix, and moving both production apps off their migration canary channels. Environments get their own plan — bucket format, secret references and activator materialisation are a subsystem, not a task. Build-on-merge needs a CI story this repo does not have yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 file changed,
+174
−0
docs/superpowers/plans/2026-08-02-plan-pipelines.md+174 −0
diff --git a/docs/superpowers/plans/2026-08-02-plan-pipelines.md b/docs/superpowers/plans/2026-08-02-plan-pipelines.md| new file mode 100644 |
| index 000000000..77a82c03f |
| --- /dev/null |
| +++ b/docs/superpowers/plans/2026-08-02-plan-pipelines.md |
| @@ -0,0 +1,174 @@ |
| +# Pipelines Implementation Plan — slice 1 |
| + |
| +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. |
| + |
| +**Goal:** Make the channel ladder real — ordered promotion with an explicit |
| +hotfix bypass, an audit trail, a console view of what is on each rung, and |
| +production moved off its migration channels onto `stable`. |
| + |
| +**Architecture:** The ladder is platform vocabulary in `internal/release`. |
| +`carlos promote` consults it and refuses a skip. The console renders one ladder |
| +per app from the channel pointers it already reads. Nothing about the artifact |
| +format, the signing scheme or adoption changes. |
| + |
| +**Spec:** `docs/superpowers/specs/2026-08-02-pipelines-design.md` |
| + |
| +## Global Constraints |
| + |
| +- Rungs, in order: `canary` → `edge` → `beta` → `stable`. Frozen vocabulary. |
| +- Ordered promotion applies **from `edge` upward only**: `edge → beta`, |
| + `beta → stable`. Shipping to a `canary/*` channel is never refused by |
| + ordering, and nothing is ever promoted *from* canary to edge — merging is what |
| + puts a change on edge. |
| +- `carlos promote -hotfix` may place a version on any rung and is recorded as a |
| + hotfix. |
| +- Promotion moves a signed channel pointer only. It must never alter a route's |
| + environment, and no task in this plan may add a code path that does. |
| +- Existing nested channel names (`canary/keymail`, `canary/platform-adoption`, |
| + `canary/rehearsal`) must keep working — they are `canary` rung channels and |
| + are what production currently runs on. |
| +- Every existing test must still pass. `go build ./...`, `go vet ./...`, |
| + `go test ./... -count=1` clean. |
| + |
| +--- |
| + |
| +### Task 1: The ladder as ordered vocabulary |
| + |
| +**Files:** |
| +- Create: `internal/release/ladder.go`, `internal/release/ladder_test.go` |
| + |
| +**Interfaces produced:** |
| +- `func Rung(channel string) (string, bool)` — the rung a channel name belongs |
| + to. `"stable"` → `("stable", true)`; `"canary/keymail"` → `("canary", true)`; |
| + an unknown name → `("", false)`. |
| +- `func RungIndex(rung string) int` — 0 canary, 1 edge, 2 beta, 3 stable; -1 unknown. |
| +- `func PromotionAllowed(from, to string) error` — nil when the transition is |
| + legal, otherwise an error naming `-hotfix`. `from` is the rung a version |
| + currently occupies (highest, if several), `to` is the target rung. |
| +- `const RungCanary/RungEdge/RungBeta/RungStable` |
| + |
| +Rules: `edge → beta` and `beta → stable` are legal. Any other pair is refused, |
| +including `edge → stable` (a skip), `canary → edge`, and same-rung |
| +re-promotion of a *different* version (re-promoting the SAME version to the rung |
| +it already holds is a no-op success, so a repeated command is not an error). |
| + |
| +- [ ] **Step 1: Write the failing test** covering: rung parsing for |
| + `stable`, `beta`, `edge`, `canary/anything`, `canary`, and an unknown name; |
| + every legal transition; every skip refused; the refusal text containing |
| + `-hotfix`. |
| +- [ ] **Step 2: Run it — expect failure (package does not exist)** |
| +- [ ] **Step 3: Implement `ladder.go`** |
| +- [ ] **Step 4: Run tests — expect pass** |
| +- [ ] **Step 5: Commit** |
| + |
| +--- |
| + |
| +### Task 2: `carlos promote` enforces the ladder |
| + |
| +**Files:** |
| +- Modify: `promote.go` (or wherever the promote verb lives — find it, do not |
| + assume), `internal/release/` channel resolution as needed |
| +- Test: extend the existing promote/e2e tests |
| + |
| +**Interfaces consumed:** Task 1's `PromotionAllowed`, `Rung`. |
| + |
| +The verb must determine which rung the version currently occupies by reading the |
| +existing channel pointers for that app+account, then check the transition. |
| + |
| +- [ ] **Step 1: Write the failing e2e test** — ship a version, promote to |
| + `edge`, then: promoting to `beta` succeeds; promoting a fresh version straight |
| + to `stable` is refused with a message naming `-hotfix`; `-hotfix` makes it |
| + succeed and the pointer records it. |
| +- [ ] **Step 2: Run it — expect failure** |
| +- [ ] **Step 3: Add the `-hotfix` flag and the ordering check** |
| +- [ ] **Step 4: Run the full suite** |
| +- [ ] **Step 5: Commit** |
| + |
| +**Constraint:** promoting to a `canary/*` channel must remain unconditionally |
| +allowed — production is on those channels right now and this must not break it. |
| + |
| +--- |
| + |
| +### Task 3: Promotion audit trail |
| + |
| +**Files:** |
| +- Modify: the channel pointer payload (`internal/release/`), console store |
| + |
| +Each promotion records: version, from-rung, to-rung, actor, hotfix bool, |
| +timestamp. The channel pointer already carries `history`; extend the payload |
| +additively so old pointers still parse. |
| + |
| +- [ ] **Step 1: Write the failing test** — a promotion appends an audit entry; |
| + a pointer written before this change still unmarshals with no audit data and |
| + no error. |
| +- [ ] **Step 2: Run — expect failure** |
| +- [ ] **Step 3: Implement, additively** |
| +- [ ] **Step 4: Run the suite** |
| +- [ ] **Step 5: Commit** |
| + |
| +--- |
| + |
| +### Task 4: Console ladder view |
| + |
| +**Files:** |
| +- Modify: `internal/console/server.go`, `internal/console/templates/` |
| + |
| +One section on the app page: four rungs in order, each showing the version it |
| +holds, when, and by whom; routes listed under the rung they subscribe to. A |
| +promote control between adjacent rungs, owner-only, and **`stable` promotion |
| +goes through `requireFreshAuth`** (the step-up gate shipped 2026-08-02). |
| + |
| +A route on a `canary/*` channel is badged; if its environment is a production |
| +one, badged distinctly and prominently. |
| + |
| +- [ ] **Step 1: Write the failing test** — the ladder renders four rungs with |
| + the right versions; a non-owner sees no promote control; a POST to promote to |
| + `stable` without step-up does not promote. |
| +- [ ] **Step 2: Run — expect failure** |
| +- [ ] **Step 3: Implement** |
| +- [ ] **Step 4: Run the suite** |
| +- [ ] **Step 5: Commit** |
| + |
| +--- |
| + |
| +### Task 5: Adoption timer is armed, and its health is visible |
| + |
| +**Files:** |
| +- Modify: `internal/host/` (bootstrap unit installation), |
| + `/tmp/claude-1001/pinfra/scripts/update-platform-binary.sh`, |
| + `internal/status/` |
| + |
| +`carlos-adopt.timer` was `enabled` but never `active` on the flagship, so no |
| +release ever adopted automatically. Bootstrap and the unit-refresh path must |
| +arm timers (`enable --now` semantics), and the fleet status snapshot must report |
| +whether adoption is running, so a dead timer is a visible defect. |
| + |
| +- [ ] **Step 1: Write the failing test** for the status field |
| +- [ ] **Step 2: Run — expect failure** |
| +- [ ] **Step 3: Implement; make bootstrap start the timer** |
| +- [ ] **Step 4: Run the suite** |
| +- [ ] **Step 5: Commit** |
| + |
| +--- |
| + |
| +### Task 6: Production onto `stable` |
| + |
| +**Not code — an operational task, run after 1-5 are deployed.** |
| + |
| +`go.tito.io` (channel `canary/platform-adoption`) and `keymail.dev` (channel |
| +`canary/keymail`) move onto `stable` at their current versions: promote the |
| +current version to `stable` with `-hotfix` (there is no `beta` history to come |
| +through), then update each route's channel subscription, then verify the site |
| +still serves and the version is unchanged. |
| + |
| +Tito has eighteen routes on that channel; all move together. |
| + |
| +--- |
| + |
| +## Deferred, and named |
| + |
| +Environments as named bundles (spec §4) — bucket format, secret references and |
| +activator materialisation are a subsystem of their own and get their own plan. |
| +Build-on-merge and the `edge`-scoped signing identity (§6) need a CI story this |
| +repo does not have. Sub-hourly adoption (§6.3). Retiring `canary.sh` and the |
| +keymail dev box (§8.4). |