carlos / platform Public

plan: console bake controls — nine tasks, spec amended to the FastTrack verb

Spec: fast-track becomes a dedicated verb (the Rollback precedent), and the
fleet-page nudge is deferred to the next snapshot-shape change. Plan carries
exact code per task, with CLAUDE.md model assignments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VioXzogWf1Xi4Rswz2B6EK
Paul Campbell 3840e96c618ad12369bd1eaca926289a8bfe081f parent 660ba46
2 files changed, +1772 −8
  • docs/superpowers/plans/2026-08-04-console-bake-controls.md +1761 −0
  • docs/superpowers/specs/2026-08-04-console-bake-controls-design.md +11 −8
diff --git a/docs/superpowers/plans/2026-08-04-console-bake-controls.md b/docs/superpowers/plans/2026-08-04-console-bake-controls.md
new file mode 100644
index 000000000..480b55ebb
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-04-console-bake-controls.md
@@ -0,0 +1,1761 @@
+# Console Bake Controls 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.
+
+**Goal:** Per-app production flag (default off) gating the supply-chain bake window, a ratcheted off-switch, a root-key fast-track waiver on channel pointers, console controls behind the existing passkey step-up, and the `edge: 0` hold fix.
+
+**Architecture:** Spec is `docs/superpowers/specs/2026-08-04-console-bake-controls-design.md` — read it first. The release package gains a signed per-app flag object (`apps/<acct>/<app>/production.json`), a `fast_track` field on channel pointers, box-side flag memory in the bake clock, and adopt wiring that finally passes `Hold`'s `ignore` parameter. The console gains two handlers, view plumbing, and ladder-section controls behind `requireFreshAuth`.
+
+**Tech Stack:** Go stdlib only; Ed25519 via the existing `signedWrapper` discipline; html/template.
+
+## Global Constraints
+
+- Working directory is the worktree root (`.claude/worktrees/console-bake-controls-spec`). Never `cd` to the main checkout.
+- Go commands need: `GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off` (and may need the sandbox disabled — "read-only file system" from the toolchain is the tell).
+- All wire changes are ADDITIVE: pointers/flags written before this change must still parse, and pointers written by this change must parse in old struct shapes (fields dropped, boxes bake — the safe direction).
+- `internal/release/laddercheck_test.go`'s pinned production channels must stay green untouched.
+- `gofmt -l` clean for every file you touch.
+- Commit messages end with:
+ `Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>` and
+ `Claude-Session: https://claude.ai/code/session_01VioXzogWf1Xi4Rswz2B6EK`
+- Test style: package-internal tests (`package release` / `package console`), table-driven where natural, comments explaining WHY in the repo's voice.
+
+**Delegation (CLAUDE.md policy):** Task 1 → Haiku. Tasks 2, 3, 4, 6, 8 → Sonnet. Tasks 5, 7, 9 → Opus (adopt is the production install path; handlers are security gates; 9 is the reviewer pass).
+
+---
+
+### Task 1: `edge` bakes zero (spec §8)
+
+**Files:**
+- Modify: `internal/release/bake.go` (the `DefaultHolds` map, ~line 36)
+- Test: `internal/release/bake_test.go`
+
+**Interfaces:**
+- Consumes: `HoldFor(holds map[string]time.Duration, channel string) time.Duration` (bake.go)
+- Produces: `DefaultHolds["edge"] == 0` — Tasks 5/6 rely on edge (and canary) being hold-free.
+
+- [ ] **Step 1: Write the failing test** (append to `internal/release/bake_test.go`)
+
+```go
+// The pipelines spec defines edge as "picked up as it lands — no human in
+// the loop, by definition". Without its own entry, HoldFor fail-closes edge
+// to 72h — longer than beta, and a spec violation (2026-08-04 console bake
+// controls spec, §1/§8).
+func TestHoldForEdgeIsZero(t *testing.T) {
+ if d := HoldFor(DefaultHolds, "edge"); d != 0 {
+ t.Fatalf("edge hold = %v, want 0", d)
+ }
+}
+```
+
+- [ ] **Step 2: Run it, verify it fails**
+
+Run: `GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/release/ -run TestHoldForEdgeIsZero -count=1`
+Expected: FAIL, `edge hold = 72h0m0s, want 0`
+
+- [ ] **Step 3: Add the entry** — in `DefaultHolds`, add `"edge": 0,` between `"canary/*"` and `"beta"`, and extend the map's doc comment block (bake.go lines 12–16) with one sentence: `edge has no window either — merged to main is picked up as it lands (pipelines spec §3).`
+
+- [ ] **Step 4: Run the package tests** — `go test ./internal/release/ -count=1` (with the env above). Expected: PASS (no existing test pins edge's 72h).
+
+- [ ] **Step 5: Commit** — `git add internal/release/bake.go internal/release/bake_test.go && git commit -m "release: edge bakes zero — merged to main is picked up as it lands"` (with the trailer lines from Global Constraints).
+
+---
+
+### Task 2: the signed production flag object (spec §2, §3)
+
+**Files:**
+- Create: `internal/release/production.go`
+- Test: `internal/release/production_test.go`
+
+**Interfaces:**
+- Consumes: `signedWrapper` (release.go), `ValidAccount`, `ValidApp`, `appPrefix`, `Store`, `ErrCASConflict`.
+- Produces (later tasks call these exactly):
+ - `type ProductionFlag struct { Schema int; Account, App string; Production bool; Actor string; SetAt time.Time }`
+ - `func ProductionKey(acct, app string) string`
+ - `func SetProduction(ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app string, production bool, actor string) error`
+ - `func GetProduction(ctx context.Context, s Store, pub ed25519.PublicKey, acct, app string) (*ProductionFlag, error)` — missing object is `(nil, nil)`; any other failure is the error, which callers treat as production=true (fail toward baking).
+
+- [ ] **Step 1: Write the failing tests** (`internal/release/production_test.go`)
+
+```go
+package release
+
+// The production flag (2026-08-04 console bake controls spec §2–§3): a
+// root-signed per-app object deciding whether the supply-chain bake applies.
+// The properties tested here, in the order they'd hurt if lost: missing is
+// the default (nil, nil); a set flag round-trips; a scoped key's wrapper is
+// refused even with a valid root grant; a tampered payload is refused; the
+// key sits OUTSIDE channels/ so pointer listings never try to parse it.
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "strings"
+ "testing"
+)
+
+func TestProductionFlagDefaultIsMissing(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ _, pub := testKeys(t)
+ f, err := GetProduction(context.Background(), s, pub, "bab", "kass")
+ if err != nil || f != nil {
+ t.Fatalf("GetProduction on empty store = %v, %v; want nil, nil (the default: not production)", f, err)
+ }
+}
+
+func TestProductionFlagRoundTrips(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ if err := SetProduction(ctx, s, priv, "bab", "kass", true, "paul@keymail.dev"); err != nil {
+ t.Fatalf("SetProduction: %v", err)
+ }
+ f, err := GetProduction(ctx, s, pub, "bab", "kass")
+ if err != nil {
+ t.Fatalf("GetProduction: %v", err)
+ }
+ if f == nil || !f.Production || f.Actor != "paul@keymail.dev" || f.SetAt.IsZero() {
+ t.Fatalf("flag = %+v, want production on, actor recorded, set_at stamped", f)
+ }
+ // Flip it off over the existing object — the CAS path.
+ if err := SetProduction(ctx, s, priv, "bab", "kass", false, "paul@keymail.dev"); err != nil {
+ t.Fatalf("SetProduction off: %v", err)
+ }
+ if f, _ = GetProduction(ctx, s, pub, "bab", "kass"); f == nil || f.Production {
+ t.Fatalf("flag = %+v after flipping off, want production false", f)
+ }
+}
+
+// The flag key lives beside channels/, never inside it: ChannelsPrefix
+// listings are parsed as pointers by the console and CLI.
+func TestProductionKeyIsOutsideChannels(t *testing.T) {
+ if k := ProductionKey("bab", "kass"); k != "apps/bab/kass/production.json" {
+ t.Fatalf("ProductionKey = %q", k)
+ }
+ if strings.HasPrefix(ProductionKey("bab", "kass"), ChannelsPrefix("bab", "kass")) {
+ t.Fatal("production flag key sits under channels/ — pointer listings would try to parse it")
+ }
+}
+
+// A scoped key must not be able to mint the flag: a wrapper carrying ANY
+// grant is refused before signature checking, and a wrapper signed by a key
+// other than the root fails signature checking. Together: root only.
+func TestProductionFlagRefusesScopedAndForeignKeys(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+
+ // Build a validly-shaped flag payload signed by a DIFFERENT key.
+ otherPub, otherPriv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _ = otherPub
+ payload, _ := json.Marshal(ProductionFlag{Schema: 1, Account: "bab", App: "kass",
+ Production: false, SetAt: timeNowTestHelper()})
+ b64 := base64.StdEncoding.EncodeToString(payload)
+ wrapper, _ := json.Marshal(signedWrapper{
+ Payload: b64,
+ Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(otherPriv, []byte(b64))),
+ })
+ if _, err := s.Put(ctx, ProductionKey("bab", "kass"), wrapper, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := GetProduction(ctx, s, pub, "bab", "kass"); err == nil {
+ t.Fatal("a flag signed by a non-root key verified")
+ }
+
+ // A wrapper WITH a grant is refused outright — even a grant the root
+ // really signed. Mint a real scoped key and have it sign the flag.
+ seed, grantB64, err := MintKeyGrant(priv, "builder", []string{RungEdge}, maxGrantTTL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ scopedKey, err := ParseSigningKey(seed, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ sg, err := parseGrantBlob(grantB64, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ wrapper, _ = json.Marshal(signedWrapper{
+ Payload: b64,
+ Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(scopedKey, []byte(b64))),
+ Grant: sg,
+ })
+ if _, err := s.Put(ctx, ProductionKey("bab", "kass"), wrapper, etagOfKey(t, s, "bab", "kass")); err != nil {
+ t.Fatal(err)
+ }
+ _, err = GetProduction(ctx, s, pub, "bab", "kass")
+ if err == nil || !strings.Contains(err.Error(), "root") {
+ t.Fatalf("a grant-bearing flag wrapper verified (err=%v), want a root-only refusal", err)
+ }
+}
+
+// etagOfKey reads the current etag of an app's flag so a test can CAS over it.
+func etagOfKey(t *testing.T, s Store, acct, app string) string {
+ t.Helper()
+ _, etag, err := s.Get(context.Background(), ProductionKey(acct, app))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return etag
+}
+
+func TestProductionFlagRefusesWrongApp(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ if err := SetProduction(ctx, s, priv, "bab", "kass", true, ""); err != nil {
+ t.Fatal(err)
+ }
+ // Copy kass's flag to keymail's key — a cross-app replay must be refused
+ // because the SIGNED payload names the app.
+ raw, _, err := s.Get(ctx, ProductionKey("bab", "kass"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, ProductionKey("bab", "keymail"), raw, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := GetProduction(ctx, s, pub, "bab", "keymail"); err == nil {
+ t.Fatal("another app's flag verified under keymail's key")
+ }
+}
+```
+
+Add this small helper at the bottom of the test file (tests must not depend on wall-clock ordering):
+
+```go
+// timeNowTestHelper is a stamped, truncated now — what signProductionFlag
+// itself writes — for tests that hand-build payloads.
+func timeNowTestHelper() time.Time { return time.Now().UTC().Truncate(time.Second) }
+```
+
+(and add `"time"` to the imports.)
+
+- [ ] **Step 2: Run tests, verify they fail** — `go test ./internal/release/ -run TestProductionFlag -count=1` (env from Global Constraints). Expected: compile errors (`undefined: ProductionKey` etc.).
+
+- [ ] **Step 3: Implement** (`internal/release/production.go`)
+
+```go
+// The per-app production flag — the console bake controls spec
+// (docs/superpowers/specs/2026-08-04-console-bake-controls-design.md, §2–§3).
+//
+// production: true means the supply-chain bake window (bake.go) applies to
+// this app's held channels; false — and MISSING, the default — means its
+// boxes adopt immediately. The object is root-signed in the pointer's own
+// signedWrapper envelope, and root-ONLY: a wrapper carrying a key grant is
+// refused before its signature is even checked, because a scoped key that
+// could flip the flag could skip the very window it bakes under.
+//
+// What verification here does NOT decide: whether a missing or unreadable
+// flag downgrades a box that once saw production: true. It never does —
+// that memory, the ratchet and the replay floor are the box's own
+// (bake.go's production memory), because anyone with bucket write but no
+// key can delete an object, and deletion must not be a downgrade (spec §4).
+package release
+
+import (
+ "context"
+ "crypto/ed25519"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "time"
+)
+
+// ProductionFlag is the signed payload: one app's declaration that it is (or
+// is not) in production, and therefore whether its releases bake.
+type ProductionFlag struct {
+ Schema int `json:"schema"` // 1
+ Account string `json:"account"`
+ App string `json:"app"`
+ Production bool `json:"production"`
+ // Actor is who flipped it — a console member's address. Recorded for the
+ // same reason the pointer's audit trail exists: an invisible bypass is a
+ // broken gate.
+ Actor string `json:"actor,omitempty"`
+ SetAt time.Time `json:"set_at"`
+}
+
+// ProductionKey is where an app's flag lives: beside channels/ and
+// releases/, deliberately NEVER under ChannelsPrefix — those listings are
+// parsed as channel pointers by the console and the CLI.
+func ProductionKey(acct, app string) string {
+ return appPrefix(acct, app) + "production.json"
+}
+
+// SetProduction writes the flag, root key only, CAS over whatever is there
+// (a concurrent flip surfaces as ErrCASConflict — re-ask, never overwrite
+// blind). SetAt is stamped here; boxes use their OWN sighting for the
+// ratchet and this stamp only as a replay floor.
+func SetProduction(ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app string, production bool, actor string) error {
+ if len(priv) != ed25519.PrivateKeySize {
+ return fmt.Errorf("set production: no usable release signing key")
+ }
+ if !ValidAccount(acct) {
+ return fmt.Errorf("set production: invalid account %q", acct)
+ }
+ if !ValidApp(app) {
+ return fmt.Errorf("set production: invalid app %q", app)
+ }
+ key := ProductionKey(acct, app)
+ _, etag, err := s.Get(ctx, key)
+ if errors.Is(err, os.ErrNotExist) {
+ etag = "" // first write: create-only
+ } else if err != nil {
+ return fmt.Errorf("set production: read %s: %w", key, err)
+ }
+ f := &ProductionFlag{
+ Schema: 1, Account: acct, App: app, Production: production,
+ Actor: actor, SetAt: time.Now().UTC().Truncate(time.Second),
+ }
+ if err := validateProductionFlag(f, acct, app); err != nil {
+ return fmt.Errorf("set production: %w", err)
+ }
+ payload, err := json.MarshalIndent(f, "", " ")
+ if err != nil {
+ return err
+ }
+ b64 := base64.StdEncoding.EncodeToString(payload)
+ wrapper, err := json.MarshalIndent(signedWrapper{
+ Payload: b64,
+ Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(priv, []byte(b64))),
+ }, "", " ")
+ if err != nil {
+ return err
+ }
+ _, err = s.Put(ctx, key, wrapper, etag)
+ return err
+}
+
+// GetProduction resolves an app's flag. A MISSING object is (nil, nil): the
+// default, not production — but never let a nil collapse the distinction the
+// spec draws: any OTHER failure (unreadable, unverifiable, misnamed) is the
+// error, and callers on the adopt path treat it as production=true, failing
+// toward baking.
+func GetProduction(ctx context.Context, s Store, pub ed25519.PublicKey, acct, app string) (*ProductionFlag, error) {
+ raw, _, err := s.Get(ctx, ProductionKey(acct, app))
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ return verifyProductionFlag(pub, raw, acct, app)
+}
+
+// verifyProductionFlag is verifyPointer's discipline for the flag: refuse a
+// grant outright, check the signature against the ROOT key alone, decode
+// after, validate the payload names the account/app whose key it sits at.
+func verifyProductionFlag(pub ed25519.PublicKey, wrapper []byte, wantAcct, wantApp string) (*ProductionFlag, error) {
+ var sw signedWrapper
+ if err := json.Unmarshal(wrapper, &sw); err != nil {
+ return nil, fmt.Errorf("production flag wrapper: %v", err)
+ }
+ if sw.Grant != nil {
+ return nil, fmt.Errorf("production flag %s/%s: signed by a scoped key — the flag takes the root key, because a delegate that could flip it could skip the window it bakes under", wantAcct, wantApp)
+ }
+ if len(pub) != ed25519.PublicKeySize {
+ return nil, fmt.Errorf("production flag: no usable release public key")
+ }
+ sig, err := base64.StdEncoding.DecodeString(sw.Sig)
+ if err != nil {
+ return nil, fmt.Errorf("production flag sig: %v", err)
+ }
+ if !ed25519.Verify(pub, []byte(sw.Payload), sig) {
+ return nil, fmt.Errorf("production flag signature verification failed")
+ }
+ payload, err := base64.StdEncoding.DecodeString(sw.Payload)
+ if err != nil {
+ return nil, fmt.Errorf("production flag payload: %v", err)
+ }
+ var f ProductionFlag
+ if err := json.Unmarshal(payload, &f); err != nil {
+ return nil, fmt.Errorf("production flag payload: %v", err)
+ }
+ if err := validateProductionFlag(&f, wantAcct, wantApp); err != nil {
+ return nil, err
+ }
+ return &f, nil
+}
+
+func validateProductionFlag(f *ProductionFlag, wantAcct, wantApp string) error {
+ if f.Schema != 1 {
+ return fmt.Errorf("unsupported production flag schema %d", f.Schema)
+ }
+ if f.Account != wantAcct || f.App != wantApp {
+ return fmt.Errorf("production flag names %s/%s, expected %s/%s", f.Account, f.App, wantAcct, wantApp)
+ }
+ if !ValidAccount(f.Account) || !ValidApp(f.App) {
+ return fmt.Errorf("production flag has invalid account/app %s/%s", f.Account, f.App)
+ }
+ if f.SetAt.IsZero() {
+ return fmt.Errorf("production flag has no set_at")
+ }
+ return nil
+}
+```
+
+- [ ] **Step 4: Run tests, verify pass** — `go test ./internal/release/ -run TestProduction -count=1`. Expected: PASS. Then the whole package: `go test ./internal/release/ -count=1`.
+
+- [ ] **Step 5: Commit** — `git add internal/release/production.go internal/release/production_test.go && git commit -m "release: the per-app production flag — root-signed, missing means not production"` (+ trailers).
+
+---
+
+### Task 3: fast_track on the pointer, and the FastTrack verb (spec §3, §5)
+
+**Files:**
+- Modify: `internal/release/release.go` (ChannelPointer ~line 68, Promotion ~line 90, verifyPointer ~line 344, signPointer ~line 263, new verb after Rollback ~line 850)
+- Test: `internal/release/release_test.go` (append)
+
+**Interfaces:**
+- Consumes: `signedWrapper`, `signPointer(sg Signer, p *ChannelPointer)`, `verifyPointer`, `GetQualified`, `prependCapped`, `Rung`, `RootSigner`.
+- Produces:
+ - `ChannelPointer.FastTrack bool` (json `fast_track,omitempty`) — Task 5's adopt and Task 6's views read it.
+ - `Promotion.FastTrack bool` (json `fast_track,omitempty`).
+ - `func FastTrack(ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app, channel, actor string) (string, error)` — returns the version waived; no-op success if already fast-tracked. Task 7's handler calls it.
+
+- [ ] **Step 1: Write the failing tests** (append to `internal/release/release_test.go`; reuse its existing helpers — read the file's top for `testKeys` and whatever ship/promote helpers exist and follow suit)
+
+```go
+// Fast-track (2026-08-04 console bake controls spec §5): re-sign the head
+// with the bake waiver — same version, same history, one new audit entry —
+// and never from a scoped key, at mint or at verification.
+func TestFastTrackWaivesTheHeadInPlace(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "kass", "v1", "linux-amd64", []byte("b1"))
+ shipRelease(t, s, "bab", "kass", "v2", "linux-amd64", []byte("b2"))
+ if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v2"); err != nil {
+ t.Fatal(err)
+ }
+
+ ver, err := FastTrack(ctx, s, priv, "bab", "kass", "stable", "paul@keymail.dev")
+ if err != nil {
+ t.Fatalf("FastTrack: %v", err)
+ }
+ if ver != "v2" {
+ t.Fatalf("FastTrack waived %q, want the head v2", ver)
+ }
+ p, err := Channel(ctx, s, pub, "bab", "kass", "stable")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !p.FastTrack || p.Version != "v2" {
+ t.Fatalf("pointer = version %s fast_track %v, want v2 waived", p.Version, p.FastTrack)
+ }
+ // History survives — a fast-tracked head must stay rollbackable.
+ if len(p.History) == 0 || p.History[0] != "v1" {
+ t.Fatalf("history = %v, want v1 preserved", p.History)
+ }
+ // The waiver is recorded on the trail it waives.
+ if len(p.Audit) == 0 || !p.Audit[0].FastTrack || p.Audit[0].Actor != "paul@keymail.dev" {
+ t.Fatalf("audit[0] = %+v, want a fast_track entry naming the actor", p.Audit)
+ }
+ // Idempotent: a second fast-track is a no-op success.
+ if _, err := FastTrack(ctx, s, priv, "bab", "kass", "stable", "paul@keymail.dev"); err != nil {
+ t.Fatalf("repeated FastTrack: %v", err)
+ }
+ // The NEXT promotion starts clean — fast_track is per write, never
+ // inherited (spec §5).
+ shipRelease(t, s, "bab", "kass", "v3", "linux-amd64", []byte("b3"))
+ if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v3"); err != nil {
+ t.Fatal(err)
+ }
+ if p, _ := Channel(ctx, s, pub, "bab", "kass", "stable"); p.FastTrack {
+ t.Fatal("fast_track survived onto the next promotion")
+ }
+}
+
+// Root only, both directions (mint AND verification, like the grant cap):
+// a scoped signer cannot SIGN a fast-tracked pointer, and a hand-built
+// grant-bearing wrapper whose payload carries fast_track does not VERIFY.
+func TestFastTrackIsRootOnly(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "kass", "v1", "linux-amd64", []byte("b1"))
+ if err := Promote(ctx, s, priv, "bab", "kass", "edge", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ seed, grantB64, err := MintKeyGrant(priv, "builder", []string{RungEdge}, maxGrantTTL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ key, err := ParseSigningKey(seed, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ scoped, err := ScopedSigner(key, grantB64, pub)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Mint side: the scoped signer refuses the payload.
+ if _, err := signPointer(scoped, &ChannelPointer{
+ App: "kass", Channel: "edge", Version: "v1", FastTrack: true,
+ }); err == nil || !strings.Contains(err.Error(), "root") {
+ t.Fatalf("a scoped signer signed a fast-tracked pointer (err=%v)", err)
+ }
+ // Verify side: build the wrapper by hand, exactly as a patched-out mint
+ // check would, and watch verification refuse it.
+ payload, _ := json.Marshal(ChannelPointer{Schema: 1, App: "kass", Channel: "edge",
+ Version: "v1", FastTrack: true, PromotedAt: timeNowTestHelper()})
+ b64 := base64.StdEncoding.EncodeToString(payload)
+ sg, err := parseGrantBlob(grantB64, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ wrapper, _ := json.Marshal(signedWrapper{
+ Payload: b64,
+ Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(key, []byte(b64))),
+ Grant: sg,
+ })
+ if _, err := verifyPointer(pub, wrapper, "kass", "edge"); err == nil ||
+ !strings.Contains(err.Error(), "root") {
+ t.Fatalf("a scoped-signed fast_track pointer verified (err=%v)", err)
+ }
+}
+
+// The wire change is additive both ways: a pre-change pointer still parses
+// (no fast_track key at all), and a fast-tracked pointer parses in a struct
+// that predates the field — the old binary simply keeps baking.
+func TestFastTrackIsAdditiveOnTheWire(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "kass", "v1", "linux-amd64", []byte("b1"))
+ if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := FastTrack(ctx, s, priv, "bab", "kass", "stable", ""); err != nil {
+ t.Fatal(err)
+ }
+ raw, _, err := GetQualified(ctx, s, ChannelKey("bab", "kass", "stable"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var sw signedWrapper
+ if err := json.Unmarshal(raw, &sw); err != nil {
+ t.Fatal(err)
+ }
+ payload, err := base64.StdEncoding.DecodeString(sw.Payload)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // The old binary's shape: every pre-fast_track field, nothing else.
+ var old struct {
+ Schema int `json:"schema"`
+ App string `json:"app"`
+ Channel string `json:"channel"`
+ Version string `json:"version"`
+ }
+ if err := json.Unmarshal(payload, &old); err != nil {
+ t.Fatalf("an old struct shape failed to parse a fast-tracked payload: %v", err)
+ }
+ if old.Version != "v1" {
+ t.Fatalf("old shape read version %q", old.Version)
+ }
+}
+```
+
+(If `release_test.go` lacks any imports these use — `strings`, `encoding/base64`, `encoding/json`, `crypto/ed25519` — add them. If `timeNowTestHelper` from Task 2 isn't visible because Task 2 hasn't merged, define the same helper locally; delete the duplicate at integration.)
+
+- [ ] **Step 2: Run, verify failure** — `go test ./internal/release/ -run TestFastTrack -count=1`. Expected: compile error (`unknown field FastTrack`).
+
+- [ ] **Step 3: Implement.** Four edits in `internal/release/release.go`:
+
+(a) `ChannelPointer` — after the `Audit` field:
+
+```go
+ // FastTrack waives the supply-chain bake window for THIS version on THIS
+ // channel (2026-08-04 console bake controls spec §5). Root-key-only at
+ // mint AND at verification — a scoped key must never skip the window it
+ // bakes under. Set per write and never inherited: the next promotion
+ // starts clean. ADDITIVE: old pointers have no key; old binaries drop the
+ // field and keep baking, the safe direction.
+ FastTrack bool `json:"fast_track,omitempty"`
+```
+
+(b) `Promotion` — after the `Hotfix` field:
+
+```go
+ // FastTrack records that this write waived the bake window — same
+ // reasoning as Hotfix: a bypass nobody can see is a broken gate.
+ FastTrack bool `json:"fast_track,omitempty"`
+```
+
+(c) `signPointer` — after the `sg.allows(...)` check:
+
+```go
+ if p.FastTrack && sg.scope != nil {
+ return nil, fmt.Errorf("fast-track is root-key-only — key grant %q cannot waive the bake window", sg.scope.Name)
+ }
+```
+
+(d) `verifyPointer` — after the `validatePointer` call and before the `if scope != nil` block:
+
+```go
+ if p.FastTrack && sw.Grant != nil {
+ return nil, fmt.Errorf("channel pointer %s/%s: fast_track is root-key-only — a scoped key cannot waive the bake window", p.App, p.Channel)
+ }
+```
+
+(e) The verb — after `Rollback` (follow its shape exactly):
+
+```go
+// FastTrack re-signs a channel's current pointer with the bake-window
+// waiver set — same version, same history, one new audit entry (the
+// Rollback precedent: a write that is not a promotion stays out of the
+// ladder). Root key only: the window defends subscribers against exactly
+// the keys that are not the root, so this takes priv directly and never a
+// Signer. A head already waived is a no-op success. Returns the version
+// waived either way.
+//
+// signPointer re-stamps PromotedAt, which is correct here: the bake clock
+// keys on the box's own first sighting of the VERSION (bake.go), which this
+// write does not change, and the console suppresses its bakes-until
+// estimate for a waived head.
+func FastTrack(ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app, channel, actor string) (string, error) {
+ sg := RootSigner(priv)
+ if !ValidAccount(acct) {
+ return "", fmt.Errorf("fast-track: invalid account %q", acct)
+ }
+ if !ValidApp(app) || !ValidChannel(channel) {
+ return "", fmt.Errorf("fast-track: invalid app/channel %s/%s", app, channel)
+ }
+ key := ChannelKey(acct, app, channel)
+ cur, etag, err := GetQualified(ctx, s, key)
+ if err != nil {
+ return "", fmt.Errorf("fast-track: %w", err)
+ }
+ old, err := verifyPointer(sg.verifyPub(), cur, app, channel)
+ if err != nil {
+ return "", fmt.Errorf("fast-track: existing pointer at %s: %w", key, err)
+ }
+ if old.FastTrack {
+ return old.Version, nil
+ }
+ rung, _ := Rung(channel)
+ next := &ChannelPointer{
+ App: app, Channel: channel, Version: old.Version,
+ History: old.History, FastTrack: true,
+ Audit: prependCapped(Promotion{
+ Version: old.Version, From: rung, To: rung, Actor: actor,
+ FastTrack: true, At: time.Now().UTC().Truncate(time.Second),
+ }, old.Audit, auditMax),
+ }
+ wrapper, err := signPointer(sg, next)
+ if err != nil {
+ return "", err
+ }
+ if _, err := s.Put(ctx, key, wrapper, etag); err != nil {
+ return "", err
+ }
+ return old.Version, nil
+}
+```
+
+- [ ] **Step 4: Run the package** — `go test ./internal/release/ -count=1`. PASS, including `laddercheck_test.go` and `golden_test.go` (the golden test pins root-signed bytes for pointers WITHOUT fast_track — `omitempty` keeps those byte-identical; if it fails, the field placement is wrong, not the golden file).
+
+- [ ] **Step 5: Commit** — `git add internal/release/release.go internal/release/release_test.go && git commit -m "release: fast_track waives the bake window — root-only at mint and at verification"` (+ trailers).
+
+---
+
+### Task 4: the box's production memory and the ratchet (spec §4)
+
+**Files:**
+- Modify: `internal/release/bake.go` (FirstSeen struct ~line 70, LoadFirstSeen ~line 118; new section at the bottom)
+- Test: `internal/release/bake_test.go` (append)
+
+**Interfaces:**
+- Consumes: `ProductionFlag` (Task 2), `HoldFor`, `RungStable`, `clockSep`, `FirstSeen.Jitter`.
+- Produces (Task 5 calls these exactly):
+ - `func (st *FirstSeen) ObserveProduction(acct, app string, flag *ProductionFlag, now time.Time) bool` — folds a VERIFIED flag into memory; nil flag is a no-op; returns changed (caller Saves).
+ - `func (st *FirstSeen) ProductionBake(acct, app string, flagErr error, hasBakeableHead bool, holds map[string]time.Duration, now time.Time) bool` — whether the window applies to this app on this box right now. Call ObserveProduction FIRST.
+
+- [ ] **Step 1: Write the failing tests** (append to `internal/release/bake_test.go`)
+
+```go
+// The box-side production memory (2026-08-04 console bake controls spec §4).
+// Scenario table, because the security content IS the state machine: the
+// default is off; on is instant; off ratchets on the box's own clock; a
+// DELETED flag never downgrades a box that saw true; a stale replayed off
+// cannot roll back a newer on.
+func TestProductionMemoryRatchet(t *testing.T) {
+ base := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
+ flag := func(prod bool, setAt time.Time) *ProductionFlag {
+ return &ProductionFlag{Schema: 1, Account: "bab", App: "kass",
+ Production: prod, SetAt: setAt}
+ }
+ newClock := func() *FirstSeen {
+ st := LoadFirstSeen(filepath.Join(t.TempDir(), "clock.json"))
+ st.Jitter = 0 // determinism: eligibility is exactly firstSeen + hold
+ return st
+ }
+
+ t.Run("default is off", func(t *testing.T) {
+ st := newClock()
+ if st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("an app with no flag ever seen bakes")
+ }
+ })
+
+ t.Run("unverifiable flag fails toward baking", func(t *testing.T) {
+ st := newClock()
+ if !st.ProductionBake("bab", "kass", fmt.Errorf("sig"), true, DefaultHolds, base) {
+ t.Fatal("a corrupt flag read as the default instead of production")
+ }
+ })
+
+ t.Run("on is instant, deletion is not a downgrade", func(t *testing.T) {
+ st := newClock()
+ if !st.ObserveProduction("bab", "kass", flag(true, base), base) {
+ t.Fatal("first observation did not change the clock")
+ }
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("production: true did not apply immediately")
+ }
+ // The object vanishes from the bucket (flag == nil every later pass):
+ // the box holds its verdict.
+ st.ObserveProduction("bab", "kass", nil, base.Add(time.Hour))
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(time.Hour)) {
+ t.Fatal("deleting the flag object downgraded a box that saw true")
+ }
+ })
+
+ t.Run("off ratchets for the stable window", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction("bab", "kass", flag(true, base), base)
+ off := flag(false, base.Add(time.Hour))
+ st.ObserveProduction("bab", "kass", off, base.Add(2*time.Hour)) // box sees it an hour late
+ hold := HoldFor(DefaultHolds, RungStable)
+ // Still baking just inside the window, measured from the box's OWN
+ // sighting (base+2h), not the flag's set_at (base+1h).
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(2*time.Hour).Add(hold-time.Minute)) {
+ t.Fatal("off was honored before the ratchet elapsed")
+ }
+ if st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(2*time.Hour).Add(hold+time.Minute)) {
+ t.Fatal("off was not honored after the ratchet elapsed")
+ }
+ })
+
+ t.Run("no bakeable head skips the ratchet", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction("bab", "kass", flag(true, base), base)
+ st.ObserveProduction("bab", "kass", flag(false, base.Add(time.Hour)), base.Add(time.Hour))
+ if st.ProductionBake("bab", "kass", nil, false, DefaultHolds, base.Add(time.Hour)) {
+ t.Fatal("carve-out: nothing at a held head, yet off still ratcheted")
+ }
+ })
+
+ t.Run("a stale off cannot be replayed over a newer on", func(t *testing.T) {
+ st := newClock()
+ staleOff := flag(false, base)
+ st.ObserveProduction("bab", "kass", staleOff, base)
+ st.ObserveProduction("bab", "kass", flag(true, base.Add(time.Hour)), base.Add(time.Hour))
+ // Attacker re-writes the old signed off object; the box refuses to
+ // regress below the newest set_at it has honored.
+ if st.ObserveProduction("bab", "kass", staleOff, base.Add(2*time.Hour)) {
+ t.Fatal("a replayed stale flag changed the clock")
+ }
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(100*time.Hour)) {
+ t.Fatal("a replayed stale off eventually turned the bake off")
+ }
+ })
+
+ t.Run("memory survives a save/load round trip", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction("bab", "kass", flag(true, base), base)
+ if err := st.Save(); err != nil {
+ t.Fatal(err)
+ }
+ st2 := LoadFirstSeen(st.path)
+ if !st2.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("production memory did not survive persistence")
+ }
+ })
+}
+```
+
+(`filepath`, `fmt` may need importing; `st.path` is unexported but the test is in-package.)
+
+- [ ] **Step 2: Run, verify failure** — `go test ./internal/release/ -run TestProductionMemoryRatchet -count=1`. Expected: compile errors.
+
+- [ ] **Step 3: Implement** — three edits in `internal/release/bake.go`:
+
+(a) `FirstSeen` struct, after `Channels`:
+
+```go
+ // Production is the per-app production-flag memory (2026-08-04 console
+ // bake controls spec §4), keyed "<acct>|<app>" with clockSep — the same
+ // collision argument as channelClockKey. ADDITIVE: old clock files have
+ // no key, old binaries drop this one.
+ Production map[string]*prodMemory `json:"production,omitempty"`
+```
+
+(b) `LoadFirstSeen`, next to the `st.Channels == nil` guard:
+
+```go
+ if st.Production == nil {
+ st.Production = map[string]*prodMemory{}
+ }
+```
+
+(c) New section at the bottom of the file:
+
+```go
+// ---- production memory ------------------------------------------------------
+
+// prodMemory is one app's production-flag memory on this box. It exists to
+// hold two properties no bucket read can: DELETION IS NOT A DOWNGRADE (once
+// a verified production: true is seen, absence never clears it — anyone
+// with bucket write but no key can delete an object), and A STALE FLAG
+// CANNOT BE REPLAYED (an off from a past era, re-written by someone with
+// bucket access alone, must not roll back a newer on).
+type prodMemory struct {
+ // WasProduction latches on the first verified production: true.
+ WasProduction bool `json:"was_production,omitempty"`
+ // NewestSetAt is the replay floor: the newest set_at this box has
+ // honored. Anything at or below it is old news.
+ NewestSetAt time.Time `json:"newest_set_at,omitempty"`
+ // OffSetAt identifies the current off-flag edition; OffFirstSeen is when
+ // THIS BOX first saw it — the ratchet clock, the box's own observation,
+ // never the flag's timestamp (same argument as the bake clock itself).
+ OffSetAt time.Time `json:"off_set_at,omitempty"`
+ OffFirstSeen time.Time `json:"off_first_seen,omitempty"`
+}
+
+func prodKey(acct, app string) string { return acct + clockSep + app }
+
+// ObserveProduction folds one VERIFIED flag read into this box's memory.
+// nil (no object in the bucket) changes nothing — absence is not evidence.
+// Returns whether anything changed; the caller persists via Save. Never
+// call it with a flag that failed verification: like Observe, the memory
+// only ever ticks for signed documents.
+func (st *FirstSeen) ObserveProduction(acct, app string, flag *ProductionFlag, now time.Time) bool {
+ if flag == nil {
+ return false
+ }
+ key := prodKey(acct, app)
+ mem := st.Production[key]
+ if mem == nil {
+ mem = &prodMemory{}
+ }
+ // The replay floor. Equal set_at is the object already recorded — the
+ // every-pass re-read — so nothing changes; older is a replay and is
+ // ignored outright.
+ if !flag.SetAt.After(mem.NewestSetAt) {
+ return false
+ }
+ mem.NewestSetAt = flag.SetAt
+ if flag.Production {
+ mem.WasProduction = true
+ mem.OffSetAt, mem.OffFirstSeen = time.Time{}, time.Time{}
+ } else {
+ mem.OffSetAt = flag.SetAt
+ mem.OffFirstSeen = now.UTC().Truncate(time.Second)
+ }
+ st.Production[key] = mem
+ return true
+}
+
+// ProductionBake is the adopt pass's one question: does the supply-chain
+// window apply to this app on this box right now? Call ObserveProduction
+// first — this reads memory, plus flagErr, which is non-nil when a flag
+// object EXISTS but would not read or verify: that fails toward baking (an
+// attacker who can corrupt but not sign must not shorten anything).
+// hasBakeableHead is the spec §4 carve-out: with no held channel head
+// resolved this pass, an off-flag needs no ratchet — there is nothing at a
+// baked head to protect.
+func (st *FirstSeen) ProductionBake(acct, app string, flagErr error, hasBakeableHead bool, holds map[string]time.Duration, now time.Time) bool {
+ if flagErr != nil {
+ return true
+ }
+ mem := st.Production[prodKey(acct, app)]
+ if mem == nil {
+ return false // never declared production — the default
+ }
+ if mem.OffSetAt.IsZero() {
+ // The newest verified word is production: true — or the flag has
+ // vanished since, which never downgrades (WasProduction latches).
+ return mem.WasProduction
+ }
+ // The newest verified word is production: false.
+ if !mem.WasProduction {
+ return false // off, and never was on: nothing to ratchet
+ }
+ if !hasBakeableHead {
+ return false // the carve-out
+ }
+ // The ratchet: off is honored only after the stable window has elapsed
+ // on this box's own sighting of the off-flag, jittered like any hold —
+ // the flag defends itself with the mechanism it controls.
+ d := HoldFor(holds, RungStable)
+ eligible := mem.OffFirstSeen.Add(time.Duration(float64(d) * (1 + st.Jitter)))
+ return now.Before(eligible)
+}
+```
+
+- [ ] **Step 4: Run the package** — `go test ./internal/release/ -count=1`. PASS.
+
+- [ ] **Step 5: Commit** — `git add internal/release/bake.go internal/release/bake_test.go && git commit -m "release: production memory on the bake clock — off ratchets, deletion never downgrades, replays refused"` (+ trailers).
+
+---
+
+### Task 5: adopt wiring — the `ignore` parameter finally gets its caller (spec §6) — OPUS
+
+**Files:**
+- Modify: `internal/release/adopt.go` (PassFilter ~lines 113–253, adoptHost ~lines 303–376)
+- Test: `internal/release/adopt_test.go` (append; also UPDATE `TestPassStableHeldByBakeWindow` ~line 187)
+
+**Interfaces:**
+- Consumes: `GetProduction`, `ObserveProduction`, `ProductionBake` (Tasks 2/4), `ChannelPointer.FastTrack` (Task 3), `HoldFor`, `Hold`.
+- Produces: `adoptHost` gains a trailing `ignore bool` parameter. No exported surface changes.
+
+**Behavior change to own explicitly:** with the flag defaulting to off, a beta/stable head with no production flag installs immediately. `TestPassStableHeldByBakeWindow` currently pins the old behavior and MUST be updated (not deleted): it becomes the production-on case.
+
+- [ ] **Step 1: Update the existing test and add the new ones** (in `internal/release/adopt_test.go`)
+
+Rewrite `TestPassStableHeldByBakeWindow` (keep the name — the property survives, it just needs the flag on now):
+
+```go
+func TestPassStableHeldByBakeWindow(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatalf("Promote: %v", err)
+ }
+ // The app is DECLARED production (2026-08-04 spec §2): the window is no
+ // longer unconditional — it is what the flag buys.
+ if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
+ t.Fatalf("SetProduction: %v", err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false) // "" = stable
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ // A brand-new stable head is inside its 72h supply-chain window: the
+ // clock started this pass, nothing installs.
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
+ t.Fatalf("live = %q, want unmoved (bake hold)", v)
+ }
+ // The pass persisted the first sighting so the window is measured from
+ // the box's own clock.
+ st := LoadFirstSeen(FirstSeenPath(opt))
+ if _, ok := st.EligibleAt("bab", "myapp", "stable", "v1", DefaultHolds); !ok {
+ t.Fatalf("Pass did not tick the first-seen clock")
+ }
+}
+```
+
+Append the new tests:
+
+```go
+// The default: no flag, no window (2026-08-04 spec §2). The whole fleet
+// runs on canary/* today, so nothing live loses protection — and a
+// rapid-iteration app's stable is just another channel.
+func TestPassNoProductionFlagInstallsImmediately(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false)
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "v1" {
+ t.Fatalf("live = %q, want v1 — no production flag means no window", v)
+ }
+}
+
+// Fast-track: a production app's held head installs the moment its pointer
+// carries the root-signed waiver (spec §5).
+func TestPassFastTrackSkipsTheWindow(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false)
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
+ t.Fatalf("live = %q, want held (production, no waiver yet)", v)
+ }
+ if _, err := FastTrack(ctx, s, priv, "bab", "myapp", "stable", "test"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "v1" {
+ t.Fatalf("live = %q, want v1 — the waiver did not skip the window", v)
+ }
+}
+
+// A production app whose flag OBJECT rots (exists, won't verify) keeps
+// baking — an attacker who can corrupt but not sign shortens nothing.
+func TestPassCorruptProductionFlagFailsTowardBaking(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, ProductionKey("bab", "myapp"), []byte("not a wrapper"), ""); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false)
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
+ t.Fatalf("live = %q, want held — a corrupt flag must read as production", v)
+ }
+}
+```
+
+- [ ] **Step 2: Run, verify the new tests fail** — `go test ./internal/release/ -run 'TestPassNoProductionFlag|TestPassFastTrack|TestPassCorruptProduction' -count=1`. Expected: FAIL (`TestPassNoProductionFlag` fails because today the head is held; the others fail or pass vacuously — confirm at least one real failure before implementing).
+
+- [ ] **Step 3: Implement in `PassFilter`.** After the pointer-resolution loop and BEFORE the bake-clock tick block, insert:
+
+```go
+ // Resolve each app's production flag once per pass — but only for apps
+ // with a HELD head resolved this pass (spec §6). The 2-3s canary fast
+ // loop filters to canary/* routes, which have no hold, so it never pays
+ // this read; the hourly pass pays one read per held app.
+ type acctApp2 struct{ acct, app string }
+ type prodState struct {
+ flag *ProductionFlag
+ err error // non-nil: object exists but would not read or verify
+ }
+ bakeable := map[acctApp2]bool{}
+ for k, ptr := range pointers {
+ if ptr != nil && HoldFor(DefaultHolds, k.channel) > 0 {
+ bakeable[acctApp2{k.acct, k.app}] = true
+ }
+ }
+ prod := map[acctApp2]prodState{}
+ for ak := range bakeable {
+ f, err := GetProduction(ctx, s, pub, ak.acct, ak.app)
+ prod[ak] = prodState{flag: f, err: err}
+ }
+```
+
+NOTE: `PassFilter` already declares a local `type acctApp struct{ acct, app string }` for the migration cache — REUSE that type instead of declaring `acctApp2` (the snippet names it `acctApp2` only because the plan cannot see your merge; in the file, use the existing `acctApp`, moving its declaration up if needed so both uses see it).
+
+In the bake-clock tick block (`for k, ptr := range pointers { ... }`), add after that loop, before `if changed {`:
+
+```go
+ for ak, ps := range prod {
+ if ps.err == nil && bake.ObserveProduction(ak.acct, ak.app, ps.flag, now) {
+ changed = true
+ }
+ }
+```
+
+Then compute `ignore` at both `adoptHost` call sites (awake and hibernating). Replace each call with:
+
+```go
+ ak := acctApp{RouteAccount(rt), rt.App}
+ ignore := !bake.ProductionBake(ak.acct, ak.app, prod[ak].err, bakeable[ak], DefaultHolds, now)
+ adoptHost(ctx, reg, s, optDir, bake, poisoned, rt, ptr, restart, now, cfg.nag, ignore)
+```
+
+(hibernating site passes `nil` for restart, as today.)
+
+- [ ] **Step 4: Implement in `adoptHost`.** Change the signature's tail from `now time.Time, nag io.Writer)` to `now time.Time, nag io.Writer, ignore bool)`, document the parameter in the function comment (`ignore is the app-level bake verdict from ProductionBake — the production flag's doing, never a publisher field`), and change the Hold call to:
+
+```go
+ if hold := Hold(bake, RouteAccount(rt), rt.App, channel, ver, DefaultHolds, ignore || ptr.FastTrack, now); hold != "" {
+```
+
+- [ ] **Step 5: Run the whole package** — `go test ./internal/release/ -count=1`. PASS, including the updated `TestPassStableHeldByBakeWindow` and everything in `laddercheck_test.go`.
+
+- [ ] **Step 6: Commit** — `git add internal/release/adopt.go internal/release/adopt_test.go && git commit -m "release: adopt honors the production flag and fast_track — Hold's ignore finally has its caller"` (+ trailers).
+
+---
+
+### Task 6: console view plumbing (spec §7 data)
+
+**Files:**
+- Modify: `internal/console/view.go` (AppView ~line 60, ChannelView ~line 126, Assemble ~lines 184–258, assembleChannels ~lines 290–335), `internal/console/ladder.go` (LadderChannel ~line 51, assembleLadder ~line 124)
+- Test: `internal/console/view_test.go` if it exists, else assert through `internal/console/ladder_test.go` in Task 8's page tests (view fields are exercised there; this task still needs `go build` + existing tests green)
+
+**Interfaces:**
+- Consumes: `release.GetProduction`, `release.HoldFor`, `release.DefaultHolds`, `release.RungStable`, `release.Rung`, `prodWiredCanaryHosts` (ladder.go), `ChannelView`, `AppView`.
+- Produces (Task 7/8 read these exactly):
+ - `type ProductionView struct { On, TurningOff bool; EffectiveAt, SetAt time.Time; Nudge bool; Err string }` in view.go
+ - `AppView.Production ProductionView`
+ - `ChannelView.FastTrack bool`
+ - `LadderChannel.FastTrack bool` and `LadderChannel.BakedUntil time.Time`
+
+- [ ] **Step 1: Add the types.** In `view.go`, add to `ChannelView` after `Hotfix`:
+
+```go
+ // FastTrack is the pointer's bake waiver (release.ChannelPointer) — the
+ // badge is permanent, like the hotfix badge.
+ FastTrack bool
+```
+
+Add after the `ChannelView` type:
+
+```go
+// ProductionView is the app's production-flag state as the console shows it
+// (2026-08-04 console bake controls spec §7).
+type ProductionView struct {
+ // On is the flag as the bucket has it; the default (no object) is off.
+ On bool
+ // TurningOff marks an off-flag younger than the stable window while the
+ // app still has held channels: hosts may still be ratcheting.
+ // EffectiveAt is the ~estimate shown (set_at + the stable hold) — each
+ // box's own clock and jitter govern, exactly as BakedUntil is only ever
+ // an approximation.
+ TurningOff bool
+ EffectiveAt time.Time
+ SetAt time.Time
+ // Nudge: the app looks production-wired while bake is off. Detection
+ // recommends; it never flips the flag.
+ Nudge bool
+ // Err is a flag object that exists but will not verify — shown, and
+ // boxes treat it as production (they keep baking).
+ Err string
+}
+```
+
+Add `Production ProductionView` to `AppView` (after `Ladder`).
+
+In `ladder.go`, add to `LadderChannel` after `Hotfix`:
+
+```go
+ // FastTrack and BakedUntil carry the channel's waiver and its ~hold
+ // estimate into the rung row, where the fast-track control lives.
+ FastTrack bool
+ BakedUntil time.Time
+```
+
+- [ ] **Step 2: Wire assembly.** In `assembleChannels`, in the ChannelView build, add `FastTrack: p.FastTrack` to the `cv := ChannelView{...}` literal.
+
+In `Assemble`'s per-app loop, replace the `out = append(out, AppView{...})` block with:
+
+```go
+ pv := assembleProduction(ctx, s, pub, acct, app, channels, byApp[app], now)
+ if pv.Err == "" && !pv.On {
+ // No production flag (or off): nothing bakes, so no estimate. An
+ // Err keeps the estimate — boxes fail toward baking.
+ for i := range channels {
+ channels[i].BakedUntil = time.Time{}
+ }
+ }
+ for i := range channels {
+ if channels[i].FastTrack {
+ channels[i].BakedUntil = time.Time{} // waived — nothing to wait out
+ }
+ }
+ out = append(out, AppView{
+ App: app,
+ Routes: byApp[app], // List orders by host already
+ Domains: domainsByApp[app],
+ Channels: channels,
+ Canaries: assembleCanaries(channels, byApp[app]),
+ Ladder: assembleLadder(channels, byApp[app], now),
+ Production: pv,
+ RouteAccount: mismatch,
+ })
+```
+
+Add the helper (bottom of view.go):
+
+```go
+// assembleProduction reads the app's flag and derives the console's view of
+// it: on/off, the turning-off ratchet estimate, and the prod-wiring nudge.
+func assembleProduction(ctx context.Context, s release.Store, pub ed25519.PublicKey, acct, app string, channels []ChannelView, routes []RouteView, now time.Time) ProductionView {
+ pv := ProductionView{}
+ flag, err := release.GetProduction(ctx, s, pub, acct, app)
+ switch {
+ case err != nil:
+ pv.Err = err.Error()
+ pv.On = true // what the boxes will do: fail toward baking
+ return pv
+ case flag != nil:
+ pv.On = flag.Production
+ pv.SetAt = flag.SetAt
+ }
+ held := false
+ for _, cv := range channels {
+ if cv.Err == "" && release.HoldFor(release.DefaultHolds, cv.Name) > 0 {
+ held = true
+ break
+ }
+ }
+ if flag != nil && !flag.Production && held {
+ hold := release.HoldFor(release.DefaultHolds, release.RungStable)
+ if until := flag.SetAt.Add(hold); now.Before(until) {
+ pv.TurningOff, pv.EffectiveAt = true, until
+ }
+ }
+ if !pv.On {
+ stableRoute := false
+ for _, rt := range routes {
+ if rung, ok := release.Rung(rt.Channel); ok && rung == release.RungStable {
+ stableRoute = true
+ break
+ }
+ }
+ pv.Nudge = stableRoute || len(prodWiredCanaryHosts(routes)) > 0
+ }
+ return pv
+}
+```
+
+In `assembleLadder` (ladder.go), the `entries[rung] = append(...)` literal for readable pointers gains `FastTrack: cv.FastTrack, BakedUntil: cv.BakedUntil,`.
+
+- [ ] **Step 3: Build and run existing console tests** — `go build ./... && go test ./internal/console/ -count=1` (env from Global Constraints). PASS — existing pages must render unchanged except: any existing test asserting `BakedUntil`/"bakes until" text for an app with NO production flag will now see it empty. `grep -rn "BakedUntil\|bakes until" internal/console/` — update any such assertion to seed the flag on first (`release.SetProduction(ctx, fx.rel, fx.priv, release.OpsAccount, app, true, "test")`) and note in the test why.
+
+- [ ] **Step 4: Commit** — `git add internal/console/view.go internal/console/ladder.go internal/console/*_test.go && git commit -m "console: production flag, fast-track and bake state plumbed into the app view"` (+ trailers).
+
+---
+
+### Task 7: console handlers — flag toggle, fast-track, promote checkbox (spec §7) — OPUS
+
+**Files:**
+- Modify: `internal/console/server.go` (route table ~line 231, handlePromote ~line 1305, new handlers after handleRollback ~line 1440)
+- Test: create `internal/console/bakecontrols_test.go`
+
+**Interfaces:**
+- Consumes: `s.accountApp`, `s.requireFreshAuth`, `s.audit`, `s.setFlash`, `RoleOwner`, `release.SetProduction`, `release.FastTrack`, `release.GetProduction`, `release.ValidChannel`; test harness: `newConsoleFixture`, `signInPaul`, `sessionToken`, `postForm`, `postGated`, `completeStepUp`, `get`, `fx.u`, `fx.rel`, `fx.priv`, `fx.st`, `fx.setUser`, `newBrowser`, `signUp`, `OpsAccountID`, `RoleMember` (see ladder_test.go for usage).
+- Produces: `POST /a/{sqid}/apps/{app}/production` (form: `csrf`, `production` = `"on"`/`"off"`), `POST /a/{sqid}/apps/{app}/fasttrack` (form: `csrf`, `channel`), and `enable_bake=1` honored on `POST .../promote`. Task 8's templates post to these.
+
+**Gate order is load-bearing and reviewed against CLAUDE.md:** session → CSRF → owner (byte-identical `http.NotFound`) → `requireFreshAuth` LAST. Turning ON and the promote checkbox take any member (the safe direction is cheap); turning OFF and fast-track take owner + passkey.
+
+- [ ] **Step 1: Write the failing tests** (`internal/console/bakecontrols_test.go`)
+
+```go
+package console
+
+// The bake controls (2026-08-04 console bake controls spec §7). Properties
+// in the order they'd hurt if lost: OFF cannot happen without owner +
+// passkey (and a member probing it gets the byte-identical 404); ON is any
+// member's, instantly; fast-track is owner + passkey and lands on the
+// pointer; the stable-promote checkbox flips the flag in the same confirmed
+// action.
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func productionFlag(t *testing.T, fx *consoleFixture, app string) *release.ProductionFlag {
+ t.Helper()
+ f, err := release.GetProduction(context.Background(), fx.rel, fx.priv.Public().(ed25519.PublicKey), release.OpsAccount, app)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return f
+}
+
+// NOTE for the implementer: if the fixture exposes the public key directly
+// (check consoleFixture's fields — many tests use fx.pub or similar), use
+// that instead of deriving from fx.priv; adjust the import of crypto/ed25519
+// accordingly.
+
+func TestProductionOnIsAnyMembers(t *testing.T) {
+ fx := newConsoleFixture(t)
+ if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ fx.setUser("sam@keymail.dev")
+ c := newBrowser(t)
+ signUp(t, fx, c, "code-sam-bake")
+ csrf := sessionToken(t, fx, c)
+
+ status, _, _ := postForm(t, c, fx.u("/apps/kass/production"),
+ url.Values{"csrf": {csrf}, "production": {"on"}})
+ if status != http.StatusSeeOther {
+ t.Fatalf("member turning bake ON = %d, want a plain redirect (no step-up)", status)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || !f.Production {
+ t.Fatalf("flag = %+v, want production on", f)
+ }
+}
+
+func TestProductionOffIsOwnerPasskeyGated(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+ // Seed on, as any member could.
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+
+ // Without the passkey the flag does not move.
+ status, loc, _ := postForm(t, c, fx.u("/apps/kass/production"),
+ url.Values{"csrf": {csrf}, "production": {"off"}})
+ if status != http.StatusSeeOther || !strings.HasPrefix(loc, fx.fk.srv.URL) {
+ t.Fatalf("un-stepped-up off = %d -> %q, want a redirect into Keymail", status, loc)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || !f.Production {
+ t.Fatal("the flag flipped off with no passkey confirmation")
+ }
+ // With it, the held action resumes.
+ if status, _, body := completeStepUp(t, fx, c, loc); status != http.StatusSeeOther {
+ t.Fatalf("resumed off = %d:\n%s", status, body)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || f.Production {
+ t.Fatal("the flag did not flip off after confirmation")
+ }
+}
+
+// A MEMBER probing the off direction meets the byte-identical 404 — before
+// any step-up redirect could confirm the account exists.
+func TestProductionOffMemberGets404(t *testing.T) {
+ fx := newConsoleFixture(t)
+ if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ fx.setUser("sam@keymail.dev")
+ c := newBrowser(t)
+ signUp(t, fx, c, "code-sam-bake2")
+ csrf := sessionToken(t, fx, c)
+ status, _, _ := postForm(t, c, fx.u("/apps/kass/production"),
+ url.Values{"csrf": {csrf}, "production": {"off"}})
+ if status != http.StatusNotFound {
+ t.Fatalf("member turning bake OFF = %d, want 404", status)
+ }
+}
+
+func TestFastTrackIsOwnerPasskeyGatedAndLands(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+ // The fixture's stable already points at v1; waive it as the owner with
+ // the full passkey round trip.
+ postGated(t, fx, c, fx.u("/apps/kass/fasttrack"),
+ url.Values{"csrf": {csrf}, "channel": {"stable"}})
+ p := fx.channelVersion(t, "kass", "stable")
+ if !p.FastTrack {
+ t.Fatalf("pointer = %+v, want fast_track set", p)
+ }
+ if len(p.Audit) == 0 || !p.Audit[0].FastTrack || p.Audit[0].Actor != "paul@keymail.dev" {
+ t.Fatalf("audit[0] = %+v, want the waiver recorded with its actor", p.Audit)
+ }
+}
+
+// The stable-promote prompt (spec §7): enable_bake rides the same
+// passkey-confirmed promote, and flips the flag on only when it is off.
+func TestPromoteEnableBakeFlipsTheFlag(t *testing.T) {
+ fx := newConsoleFixture(t)
+ shipVersion(t, fx, "kass", "v3")
+ seedChannel(t, fx, "kass", "beta", "v3")
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+
+ postGated(t, fx, c, fx.u("/apps/kass/promote"),
+ url.Values{"csrf": {csrf}, "channel": {"stable"}, "version": {"v3"}, "enable_bake": {"1"}})
+ if p := fx.channelVersion(t, "kass", "stable"); p.Version != "v3" {
+ t.Fatalf("stable = %s, want v3", p.Version)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || !f.Production {
+ t.Fatalf("flag = %+v after enable_bake promote, want production on", f)
+ }
+}
+```
+
+- [ ] **Step 2: Run, verify failure** — `go test ./internal/console/ -run 'TestProduction|TestFastTrackIs|TestPromoteEnableBake' -count=1`. Expected: 404s where routes don't exist yet (real failures, not compile errors, once imports are fixed against the fixture's actual field names — check `consoleFixture` in the harness file, e.g. `authcli_test.go` or wherever `newConsoleFixture` lives, and adjust `fx.fk.srv.URL` / pub-key access to match the fixture).
+
+- [ ] **Step 3: Register routes** (server.go route table, after the rollback line):
+
+```go
+ s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/production", s.handleProduction)
+ s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/fasttrack", s.handleFastTrack)
+```
+
+- [ ] **Step 4: Implement the handlers** (after `handleRollback`):
+
+```go
+// handleProduction flips the app's production flag (2026-08-04 console bake
+// controls spec §7). ON is any member's to do, instantly — the safe
+// direction stays cheap. OFF removes a supply-chain guard from every
+// subscriber of the app, so it is the owner's, behind the passkey, and the
+// fleet ratchets it in on each box's own clock. Gate order is the mandated
+// one: session → CSRF → owner (byte-identical 404) → requireFreshAuth LAST.
+func (s *Server) handleProduction(w http.ResponseWriter, r *http.Request) {
+ addr, raw, acct, role, app, ok := s.accountApp(w, r)
+ if !ok {
+ return
+ }
+ if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 {
+ s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.")
+ return
+ }
+ if s.cfg.Key == nil {
+ s.renderMessage(w, r, http.StatusServiceUnavailable, "Promotions disabled",
+ "No signing key on this host — the console is running read-only.")
+ return
+ }
+ back := func(kind, msg string) {
+ s.setFlash(w, kind, msg)
+ http.Redirect(w, r, "/a/"+url.PathEscape(acct.Sqid)+"/apps/"+url.PathEscape(app), http.StatusSeeOther)
+ }
+ var on bool
+ switch r.FormValue("production") {
+ case "on":
+ on = true
+ case "off":
+ on = false
+ default:
+ back("err", "Invalid production value.")
+ return
+ }
+ if !on {
+ if role != RoleOwner {
+ http.NotFound(w, r)
+ return
+ }
+ if !s.requireFreshAuth(w, r, addr, raw) {
+ return
+ }
+ }
+ ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
+ defer cancel()
+ if err := release.SetProduction(ctx, s.cfg.Releases, s.cfg.Key, acct.Sqid, app, on, addr); err != nil {
+ log.Printf("console: production %s -> %v: %v", app, on, err)
+ back("err", "Could not update the production flag: "+err.Error())
+ return
+ }
+ if on {
+ s.audit(acct, app, addr, "production", "enabled the supply-chain bake (production: on)")
+ back("ok", "Supply-chain bake enabled — beta and stable now hold before hosts adopt.")
+ } else {
+ s.audit(acct, app, addr, "production", "disabled the supply-chain bake (production: off)")
+ back("ok", "Supply-chain bake turning off — each host honors it after the stable window (~72h) on its own clock.")
+ }
+}
+
+// handleFastTrack waives the bake window for one channel's CURRENT head —
+// owner + passkey, always: this is the loud instant path past the window
+// (spec §5, §9), and the pointer wears the badge forever.
+func (s *Server) handleFastTrack(w http.ResponseWriter, r *http.Request) {
+ addr, raw, acct, role, app, ok := s.accountApp(w, r)
+ if !ok {
+ return
+ }
+ if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 {
+ s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.")
+ return
+ }
+ if s.cfg.Key == nil {
+ s.renderMessage(w, r, http.StatusServiceUnavailable, "Promotions disabled",
+ "No signing key on this host — the console is running read-only.")
+ return
+ }
+ back := func(kind, msg string) {
+ s.setFlash(w, kind, msg)
+ http.Redirect(w, r, "/a/"+url.PathEscape(acct.Sqid)+"/apps/"+url.PathEscape(app), http.StatusSeeOther)
+ }
+ channel := strings.TrimSpace(r.FormValue("channel"))
+ if !release.ValidChannel(channel) {
+ back("err", "Invalid channel name.")
+ return
+ }
+ if role != RoleOwner {
+ http.NotFound(w, r)
+ return
+ }
+ if !s.requireFreshAuth(w, r, addr, raw) {
+ return
+ }
+ ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
+ defer cancel()
+ ver, err := release.FastTrack(ctx, s.cfg.Releases, s.cfg.Key, acct.Sqid, app, channel, addr)
+ if err != nil {
+ log.Printf("console: fast-track %s %s: %v", app, channel, err)
+ back("err", "Fast-track failed: "+err.Error())
+ return
+ }
+ s.audit(acct, app, addr, "fast-track", "waived the bake window for "+ver+" on "+channel)
+ back("ok", "Fast-tracked "+ver+" on "+channel+" — hosts adopt on their next pass.")
+}
+```
+
+- [ ] **Step 5: The promote checkbox.** In `handlePromote`, after the `s.audit(...)` line and the `msg := ...` construction, insert (before `back("ok", msg)`):
+
+```go
+ // The spec §7 prompt: promote-to-stable asked "is this app in production
+ // now?" and the human said yes — in the same passkey-confirmed action.
+ // Turning ON is any member's anyway, so no extra gate; flips only when
+ // the flag is actually off, so a stray field on a repeat is a no-op.
+ if r.FormValue("enable_bake") == "1" {
+ if flag, ferr := release.GetProduction(ctx, s.cfg.Releases, s.cfg.ReleasePub, acct.Sqid, app); ferr == nil && (flag == nil || !flag.Production) {
+ if err := release.SetProduction(ctx, s.cfg.Releases, s.cfg.Key, acct.Sqid, app, true, addr); err != nil {
+ log.Printf("console: enable_bake %s: %v", app, err)
+ } else {
+ s.audit(acct, app, addr, "production", "enabled the supply-chain bake (production: on, at stable promote)")
+ msg += " Supply-chain bake enabled."
+ }
+ }
+ }
+```
+
+- [ ] **Step 6: Run** — `go test ./internal/console/ -count=1`. PASS.
+
+- [ ] **Step 7: Commit** — `git add internal/console/server.go internal/console/bakecontrols_test.go && git commit -m "console: bake controls — on for members, off and fast-track behind owner + passkey"` (+ trailers).
+
+---
+
+### Task 8: templates — status line, controls, badges, checkbox, nudge (spec §7 UI)
+
+**Files:**
+- Modify: `internal/console/templates/ladder.html`
+- Test: `internal/console/ladder_test.go` (append)
+
+**Interfaces:**
+- Consumes: `AppView.Production` (`.Detail.Production`), `LadderChannel.FastTrack`/`.BakedUntil`, page fields `$.CanPromote` (signing key present), `$.IsOwner`, `$.CSRF`, `$.Base`, template helpers `fmtTime`, `icon` (vendored Lucide names only — check `internal/console/icons*.go` for which names exist; `shield`, `hourglass`, `triangle-alert`, `zap` are safe bets, verify before use and pick existing ones).
+- Produces: markup the Task 7 handlers receive.
+
+- [ ] **Step 1: Write the failing tests** (append to `ladder_test.go`)
+
+```go
+// The bake status line and its controls (spec §7): off-by-default says so;
+// an owner gets the turn-on/turn-off controls; a held channel row offers
+// the owner fast-track; the stable promote form carries the enable_bake
+// prompt while the flag is off.
+func TestLadderShowsBakeStateAndControls(t *testing.T) {
+ fx := newConsoleFixture(t)
+ shipVersion(t, fx, "kass", "v3")
+ seedChannel(t, fx, "kass", "beta", "v3")
+ c := signInPaul(t, fx)
+
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, "Supply-chain bake") {
+ t.Fatalf("no bake status line:\n%s", body)
+ }
+ if !strings.Contains(body, `action="/a/bab/apps/kass/production"`) {
+ t.Fatal("owner is offered no production toggle")
+ }
+ // Off by default, and the stable promote form asks the question.
+ if !strings.Contains(body, `name="enable_bake"`) {
+ t.Fatal("the stable promote form does not carry the enable_bake prompt")
+ }
+
+ // Flip on; the held beta head now shows its window and the fast-track
+ // control appears on it.
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ _, _, body = get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, `action="/a/bab/apps/kass/fasttrack"`) {
+ t.Fatalf("no fast-track control on a held head:\n%s", rungBlock(t, body, "beta"))
+ }
+ if strings.Contains(body, `name="enable_bake"`) {
+ t.Fatal("the enable_bake prompt survived the flag turning on")
+ }
+
+ // Waive it; the badge is permanent and the control is gone.
+ if _, err := release.FastTrack(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", "beta", "test"); err != nil {
+ t.Fatal(err)
+ }
+ _, _, body = get(t, c, fx.u("/apps/kass"))
+ b := rungBlock(t, body, "beta")
+ if !strings.Contains(b, "fast-tracked") {
+ t.Errorf("no permanent fast-tracked badge:\n%s", b)
+ }
+ if strings.Contains(b, `action="/a/bab/apps/kass/fasttrack"`) {
+ t.Error("a waived head still offers the fast-track control")
+ }
+}
+
+// A member sees state, never the owner-only controls (off / fast-track) —
+// but IS offered turn-on, the safe direction.
+func TestLadderBakeControlsMemberView(t *testing.T) {
+ fx := newConsoleFixture(t)
+ if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ fx.setUser("sam@keymail.dev")
+ c := newBrowser(t)
+ signUp(t, fx, c, "code-sam-baketpl")
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, "Supply-chain bake") {
+ t.Fatal("member sees no bake status")
+ }
+ if !strings.Contains(body, `name="production" value="on"`) {
+ t.Error("member is not offered the turn-on control")
+ }
+ if strings.Contains(body, `name="production" value="off"`) {
+ t.Error("member is offered the owner-only turn-off control")
+ }
+ if strings.Contains(body, `/fasttrack"`) {
+ t.Error("member is offered the owner-only fast-track control")
+ }
+}
+
+// The prod-wiring nudge: a stable route with bake off gets the persistent
+// recommendation line.
+func TestLadderNudgesProdWiredBakeOff(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ // The fixture's kass.carlosapp.dev is on stable and the flag is off (the
+ // default): that IS the nudge condition.
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, "adopts releases immediately") {
+ t.Fatalf("no prod-wiring nudge:\n%s", body)
+ }
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ if _, _, body := get(t, c, fx.u("/apps/kass")); strings.Contains(body, "adopts releases immediately") {
+ t.Fatal("the nudge survived the flag turning on")
+ }
+}
+```
+
+- [ ] **Step 2: Run, verify failure** — `go test ./internal/console/ -run TestLadder -count=1`. New tests FAIL; old ladder tests still PASS.
+
+- [ ] **Step 3: Edit `ladder.html`.** Insert after the intro `<p class="small">` (line 5), a bake block:
+
+```html
+{{with .Detail.Production}}
+<div class="bake">
+ {{if .Err}}
+ <p class="error">Production flag unreadable: {{.Err}} — hosts treat this app as in production and keep baking.</p>
+ {{else if .On}}
+ <p class="small">Supply-chain bake: <strong>on</strong> — beta holds 24h and stable 72h on each host before it adopts.
+ {{if and $.CanPromote $.IsOwner}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="production" value="off">
+ <button type="submit" class="linkish">Turn off</button>
+ </form>
+ <span class="small">— confirms with your passkey; each host honors it after the stable window (~72h) on its own clock</span>
+ {{end}}
+ </p>
+ {{else if .TurningOff}}
+ <p class="small">Supply-chain bake: <strong>turning off</strong> — hosts honor it from ~{{fmtTime .EffectiveAt}}, each on its own clock.
+ {{if $.CanPromote}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="production" value="on">
+ <button type="submit" class="linkish">Turn back on</button>
+ </form>
+ {{end}}
+ </p>
+ {{else}}
+ <p class="small">Supply-chain bake: <strong>off</strong> (default) — hosts adopt releases immediately.
+ {{if $.CanPromote}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="production" value="on">
+ <button type="submit" class="linkish">Turn on</button>
+ </form>
+ {{end}}
+ </p>
+ {{end}}
+ {{if .Nudge}}
+ <p class="small nudge">{{icon "triangle-alert"}} This app looks production-wired but adopts releases immediately — consider enabling the supply-chain bake.</p>
+ {{end}}
+</div>
+{{end}}
+```
+
+In the channel row (after the Hotfix span, line 19):
+
+```html
+ {{if .FastTrack}}· fast-tracked past the bake window{{end}}</span></p>
+ {{if and (not .BakedUntil.IsZero) $.CanPromote $.IsOwner}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/fasttrack" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="channel" value="{{.Channel}}">
+ <button type="submit" class="linkish">Fast-track {{.Version}}</button>
+ </form>
+ <span class="small">— in its bake window until ~{{fmtTime .BakedUntil}}; confirms with your passkey</span>
+ {{end}}
+```
+
+(the first line replaces the existing `{{if .Hotfix}}...{{end}}</span></p>` tail — keep the Hotfix clause, append the FastTrack clause inside the same `<span class="small">`.)
+
+In the promote step form (inside the `{{if and .Up.Version $.CanPromote $.IsOwner}}` form, before the button):
+
+```html
+ {{if and (eq .Up.To "stable") (not $.Detail.Production.On)}}
+ <label class="small"><input type="checkbox" name="enable_bake" value="1" checked>
+ This app is in production — enable the supply-chain bake (recommended)</label>
+ {{end}}
+```
+
+Also `grep -n "BakedUntil" internal/console/templates/*.html` — if `app.html`'s channels table renders `BakedUntil` or a bake note, add the same `fast-tracked` clause there for consistency; if it doesn't, touch nothing.
+
+- [ ] **Step 4: Run the console package** — `go test ./internal/console/ -count=1`. PASS.
+
+- [ ] **Step 5: Commit** — `git add internal/console/templates/ladder.html internal/console/ladder_test.go && git commit -m "console: bake status line, production toggle, fast-track control and the stable-promote prompt"` (+ trailers).
+
+---
+
+### Task 9: full verification, docs sync, review — OPUS
+
+**Files:**
+- Verify all; no planned source edits (fix what verification finds).
+
+- [ ] **Step 1: Full gate** — from the worktree root:
+
+```
+GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && \
+GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go vet ./... && \
+GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./... -count=1
+```
+
+All 14+ packages PASS. Then `gofmt -l internal/ *.go` — none of the files this branch touched may appear (the three pre-existing unformatted files elsewhere are left alone).
+
+- [ ] **Step 2: Spec cross-check** — reread `docs/superpowers/specs/2026-08-04-console-bake-controls-design.md` section by section and point at the code satisfying each of §2–§8. Confirm specifically: (a) a box with `WasProduction` latched never downgrades on a missing object; (b) `verifyPointer` refuses grant-bearing `fast_track` wrappers; (c) the gate order in both new handlers is session → CSRF → owner 404 → step-up; (d) the canary fast loop pays no production-flag reads (only held-channel apps are fetched); (e) `laddercheck_test.go` untouched.
+
+- [ ] **Step 3: Commit anything the check fixed**, then run the repo's review skill (superpowers:requesting-code-review) over the branch diff with Opus as reviewer — this branch grants authority and touches production paths, which is exactly the CLAUDE.md case for an Opus review.
diff --git a/docs/superpowers/specs/2026-08-04-console-bake-controls-design.md b/docs/superpowers/specs/2026-08-04-console-bake-controls-design.md
index b42b9934a..b815d616f 100644
--- a/docs/superpowers/specs/2026-08-04-console-bake-controls-design.md
+++ b/docs/superpowers/specs/2026-08-04-console-bake-controls-design.md
@@ -122,12 +122,13 @@ the ratchet honest about what it protects.
## 5. Fast-track a release
For a production app whose beta or stable head is inside its window, the
-console offers **fast-track**: re-promote the same version with
-`FastTrack: true`, rewriting the pointer in place. Boxes then skip the hold
-for exactly that version on that channel.
+console offers **fast-track**: a dedicated `FastTrack` verb re-signs the
+channel's current pointer in place — same version, same history, the waiver
+set, one new audit entry (the `Rollback` precedent: a write that is not a
+promotion stays out of the ladder). Boxes then skip the hold for exactly
+that version on that channel.
-- `PromoteOptions` gains `FastTrack bool`. The promote no-op check learns one
- nuance: same version but *gaining* fast_track is a write, not a repeat.
+- An already-fast-tracked head is a no-op success, like a repeated promote.
- **Root key only, enforced at verification, not just at mint** — a
scoped-key-signed pointer bearing `fast_track` is refused by verifiers,
matching the grant model's mint-AND-verify rule. Otherwise one leaked
@@ -188,9 +189,11 @@ of the existing step-up gate.
(a route bound to the same environment as a stable route, or sitting on the
app's apex) plus "has a stable route at all": an app tripping them while
bake is off gets a persistent line on its ladder section — "this app looks
- production-wired but adopts releases immediately" — and the same line in
- the fleet view. Detection recommends; it never flips the flag (implicit
- gates are the thing this platform keeps refusing).
+ production-wired but adopts releases immediately". Detection recommends;
+ it never flips the flag (implicit gates are the thing this platform keeps
+ refusing). The fleet page renders the published status snapshot, not
+ console-assembled state, so the nudge lives on the app page; a fleet-page
+ line can follow whenever the snapshot shape next changes.
## 8. The edge fix