carlos / platform Public

pipelines v2: conformance sweep + docs

Spec conformance pass against docs/superpowers/specs/2026-08-20-release-pipelines-v2-design.md:
every §-claim maps to an implementing test (full mapping in
.superpowers/sdd/2026-08-20-release-pipelines-v2/task-12-report.md). Verified
laddercheck_test.go has zero diff against both local main and origin/main,
canary_ttl.go/canary_ttl_test.go untouched, and the legacy stable-passkey
refusal string is byte-identical everywhere it appears.

HoldFor sweep: the last unfixed console call site was confirm.go's
confirmPromote card, rendered before handlePromote's own pipeline read is
threaded through. Now takes the caller's pipe, uses holdWindow instead of
release.HoldFor(DefaultHolds, ...), mirrors handlePromote's post-confirm
"pipe != nil || baking" condition, hides the legacy enable_bake checkbox for
a pipelined channel (it only flips a flag pipelined bake ignores), and
extends the passkey note to a pipelined channel's own Passkey rule instead of
only the legacy "stable" rung name. flow.go, view.go and apicli.go's HoldFor
sites were already pipeline-aware or are legacy-list-page-only by the Task 9
applyPipelineView precedent, so left as is.

Docs: CLAUDE.md gains the pipeline-absence rule; the spec's Status line notes
implementation landed on this branch.

Full gate green: go build/vet/test ./... (28 packages), GOOS=darwin build,
gofmt -l clean on every branch-touched .go file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 7af8d4a3f77716b4dac950127a0873906bf7b4e0 parent b47a43a
5 files changed, +71 −6
  • CLAUDE.md +4 −0
  • docs/superpowers/specs/2026-08-20-release-pipelines-v2-design.md +5 −1
  • internal/console/confirm.go +19 −4
  • internal/console/confirm_test.go +42 −0
  • internal/console/server.go +1 −1
diff --git a/CLAUDE.md b/CLAUDE.md
index 9fa6ab4a3..5bf29eab2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -147,6 +147,10 @@ read as a single piece rather than commit by commit.
`.carlos/config`. The fallback is the operator's real login and default
console; a logged-in laptop running `go test ./...` minted 40+ real accounts
on the production console before this was isolated.
+- **Absence of `apps/<acct>/<app>/pipeline.json` = the legacy ladder**,
+ byte-for-byte; new apps get a one-channel `edge` pipeline at create;
+ per-channel rules are console-enforced, bake windows box-enforced through
+ the `FirstSeen.Holds` ratchet.
## Deploy reality (2026-08-02; convergence rewritten 2026-08-08)
diff --git a/docs/superpowers/specs/2026-08-20-release-pipelines-v2-design.md b/docs/superpowers/specs/2026-08-20-release-pipelines-v2-design.md
index 2d123db82..3035bed3e 100644
--- a/docs/superpowers/specs/2026-08-20-release-pipelines-v2-design.md
+++ b/docs/superpowers/specs/2026-08-20-release-pipelines-v2-design.md
@@ -1,6 +1,10 @@
# Release pipelines v2 — one channel by default, pipelines opt-in
-**Status:** approved 2026-08-20 (design review in chat with Paul).
+**Status:** approved 2026-08-20 (design review in chat with Paul). Implemented
+2026-08-22 on branch `edge-beta-stable` (12 tasks, plan at
+`docs/superpowers/plans/`); §9's deferrals (key grants on app-defined
+channels, channel rename, auto-promotion, migrating the fleet's `canary/*`
+production channels) remain open follow-ups.
**Amends** the pipelines design (2026-08-02) §3: the ladder stops being the
platform's frozen vocabulary and becomes the built-in *template*. **Extends**
the console bake controls design (2026-08-04): the production flag becomes a
diff --git a/internal/console/confirm.go b/internal/console/confirm.go
index 95ad7d7e3..5a65152aa 100644
--- a/internal/console/confirm.go
+++ b/internal/console/confirm.go
@@ -98,7 +98,17 @@ func holdText(d time.Duration) string {
// confirmPromote is the promote door's card. Everything here is best-effort
// reading for the card's sentences — the confirmed POST re-validates from
// scratch, so a read that fails here costs precision, never safety.
-func (s *Server) confirmPromote(w http.ResponseWriter, r *http.Request, addr, raw string, acct *Account, role, app, channel, version string) {
+//
+// pipe is the caller's own already-read copy (handlePromote reads it before
+// the confirmed check, to gate the owner-only rung correctly) — not read
+// again here, so the card and the write it precedes can never disagree about
+// which pipeline they saw. Its hold window, and whether a bake applies at
+// all, mirror handlePromote's post-confirm logic (holdWindow, and
+// `pipe != nil || baking`) exactly: a pipelined channel bakes on its OWN
+// window regardless of the legacy production flag (spec §3), and the
+// legacy enable_bake checkbox — which only ever flips that flag — has
+// nothing to offer a pipelined channel, so it stays hidden for one.
+func (s *Server) confirmPromote(w http.ResponseWriter, r *http.Request, addr, raw string, acct *Account, role, app, channel, version string, pipe *release.Pipeline) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
cv := &ConfirmView{
@@ -133,15 +143,20 @@ func (s *Server) confirmPromote(w http.ResponseWriter, r *http.Request, addr, ra
// the card tell the truth about what happens next.
flag, ferr := release.GetProduction(ctx, s.cfg.Releases, s.cfg.ReleasePub, acct.Sqid, app)
baking := ferr != nil || (flag != nil && flag.Production)
- if hold := release.HoldFor(release.DefaultHolds, channel); hold > 0 && baking {
+ if hold := holdWindow(pipe, channel); hold > 0 && (pipe != nil || baking) {
cv.Lines = append(cv.Lines, "This app has the safety delay on: each copy waits "+holdText(hold)+" on "+channel+" before running "+version+".")
}
- if stable && ferr == nil && !baking {
+ if stable && pipe == nil && ferr == nil && !baking {
cv.CheckboxName = "enable_bake"
cv.CheckboxChecked = true
cv.CheckboxLabel = "This app is live for real users — keep new versions on the safety delay (recommended)"
}
- if stable {
+ // The legacy rung's name carries the passkey requirement implicitly
+ // (promoteGate); a pipelined channel carries it explicitly, on ANY name
+ // — "production" in the Edge→Production template needs the same warning
+ // "stable" gets, or the passkey prompt that promoteGate still enforces
+ // right after this card would land as a surprise.
+ if stable || (pipe != nil && channelRules(pipe, channel).Passkey) {
cv.Note = "You'll confirm it's really you with your passkey."
}
s.renderConfirm(w, r, addr, raw, acct, role, app, cv)
diff --git a/internal/console/confirm_test.go b/internal/console/confirm_test.go
index 10d6e9e03..4d50bbe73 100644
--- a/internal/console/confirm_test.go
+++ b/internal/console/confirm_test.go
@@ -157,6 +157,48 @@ func TestConfirmStablePromoteOffersBakeAndHidesFromMembers(t *testing.T) {
}
}
+// TestConfirmPromoteCardIsPipelineAware: the HoldFor sweep (release
+// pipelines v2, Task 12) — the confirm card must never tell a member
+// something the boxes will not do. A pipelined channel's bake comes from
+// ITS OWN window, on a name the legacy rung table has never heard of; the
+// legacy enable_bake checkbox — which only ever flips the production flag a
+// pipelined app's boxes ignore (spec §3) — has nothing to offer here and
+// must not appear; and the passkey note follows the channel's OWN rule
+// rather than the legacy "stable" name.
+func TestConfirmPromoteCardIsPipelineAware(t *testing.T) {
+ fx, c, csrf := memberFixture(t) // paul owns, kim is a member
+ pipelinedApp(t, fx, "widget", "abc1234",
+ release.PipelineChannel{Name: "edge"},
+ release.PipelineChannel{Name: "production", Bake: "24h", Rules: release.ChannelRules{Passkey: true}})
+
+ // Any member — not just the owner — reaches the card: the pipeline's own
+ // rules govern this channel, not the legacy stable-is-owner-only gate.
+ status, _, body := postForm(t, c, fx.u("/apps/widget/promote"),
+ url.Values{"csrf": {csrf}, "channel": {"production"}, "version": {"abc1234"}})
+ card := confirmCard(t, status, body, "Promote abc1234 to production?", csrf)
+ if !strings.Contains(card, "waits ~24h on production") {
+ t.Fatalf("card does not carry the pipeline's OWN 24h window:\n%s", card)
+ }
+ if strings.Contains(card, `name="enable_bake"`) {
+ t.Fatalf("a pipelined channel's card offered the legacy enable_bake checkbox, which its boxes ignore:\n%s", card)
+ }
+ if !strings.Contains(card, "really you with your passkey") {
+ t.Fatalf("channel production carries a passkey rule but the card said nothing about it:\n%s", card)
+ }
+
+ // An unruled channel of the same pipeline (edge: no bake, no rules)
+ // carries neither line — nothing to wait for, nothing to confirm.
+ status, _, body = postForm(t, c, fx.u("/apps/widget/promote"),
+ url.Values{"csrf": {csrf}, "channel": {"edge"}, "version": {"abc1234"}})
+ card = confirmCard(t, status, body, "Promote abc1234 to edge?", csrf)
+ if strings.Contains(card, "safety delay") {
+ t.Fatalf("edge has no bake window in this pipeline, but the card mentions one:\n%s", card)
+ }
+ if strings.Contains(card, "your passkey") {
+ t.Fatalf("edge carries no passkey rule, but the card promised one:\n%s", card)
+ }
+}
+
// The rollback card names BOTH versions — a rollback card that cannot say
// where it is going has no business asking anyone to confirm it — and the
// confirmed post re-points the channel.
diff --git a/internal/console/server.go b/internal/console/server.go
index 46f0826ae..ca1306310 100644
--- a/internal/console/server.go
+++ b/internal/console/server.go
@@ -2737,7 +2737,7 @@ func (s *Server) handlePromote(w http.ResponseWriter, r *http.Request) {
return
}
if !confirmed(r) {
- s.confirmPromote(w, r, addr, raw, acct, role, app, channel, version)
+ s.confirmPromote(w, r, addr, raw, acct, role, app, channel, version, pipe)
return
}
// The channel's own gate, LAST — after membership, after the app check,