carlos / platform Public

plan: zero-downtime deploys index — six plans, order, shared interfaces

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 295ac989986b81fd39d67814461be56edb2b1ddc parent c29e301
1 file changed, +178 −0
  • docs/superpowers/plans/2026-10-07-zero-downtime-deploys-0-index.md +178 −0

CI cancelled — superseded by a newer push — run details

diff --git a/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-0-index.md b/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-0-index.md
new file mode 100644
index 000000000..940204c63
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-0-index.md
@@ -0,0 +1,178 @@
+# Zero-Downtime Deploys: Plan Index
+
+> **For agentic workers:** this index holds no tasks. It fixes the order of
+> the six plans and the interfaces they share. Read it before any plan, and
+> read the spec before both.
+
+**Spec:** `docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md`
+(rev 13, accepted by Astra in round 13, approved by Paul on 2026-10-07).
+The spec is the authority. Where a plan and the spec disagree, the spec
+wins and the plan is the bug. §16 of the spec records every review
+finding; none may be reopened.
+
+**Branch:** `design-zero-downtime-deploys`, in the worktree
+`/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys`.
+Implementation happens on per-plan branches cut from `main` (the plan
+names each one). The design branch only holds docs.
+
+## Order
+
+| # | Plan | Lands | Depends on |
+|---|------|-------|------------|
+| 1 | `…-1-generations.md`: generation identity, bounded generation sockets, generation handles, `AppSocket`, every app-socket consumer, the per-connection edge dial, per-socket idle pools, the cache tagged with serving route and generation, the version header from the serving generation | dormant: flag off keeps canonical paths and one generation per entry | none |
+| 2 | `…-2-containment.md`: per-generation child cgroups, parent limits and their raise and restore, per-generation strict reservation, per-child `KillAndEmpty`, parent stop evidence, the nested generation template `carlos-instance-gen@<id>:<n>` and the control service's generation argument | dormant | 1 |
+| 3 | `…-3-swap.md`: the entry's operation set and barrier, `Shutdown` covering waking and swapping entries, admission through `hostwrite` (claims off and on), the overlap swap (§7.5), the `hold` swap (§7.8), canary settle observation, intentional-stop cancellation, scheduled-work rules (§7.10), legacy sidecar binding (§7.5), and exec of an attempt's staged path | dormant: reachable only through `Service.Swap`, which nothing calls until plan 4 | 1, 2 |
+| 4 | `…-4-release-side.md`: the manifest `swap` field, raw-byte staging, the canonical-digest check and refusal to restage referenced copies, retained artifact identity, `SidecarAdoptable`, worker recycling onto the captured serving build, worker identity versus reported version, and the signed retry request | additive; the `swap` field is read by nobody until plan 5 | none (can run in parallel with 1–3) |
+| 5 | `…-5-scheduler.md`: the per-app scheduler, attempts and their identity, cohorts, canary gating, `swap_concurrency`, failure records and their clearing, `GoodVersion` with its digest and demand, bootstrap, version-sweep pins, the first-wake budget and fallback, delivered and active config revisions, restart and config attempts, the converge leg and sweep feeding the scheduler, stage-relay retransmission, events | **this plan wires `CARLOS_SWAP`**. Flag off: today's paths. Flag on: everything | 1, 2, 3, 4 |
+| 6 | `…-6-surfaces.md`: app swap policy (`swap`, `swap_concurrency`) in console and CLI, delivered with app config; `carlos deploy --swap hold`; `--retry`; `carlos instances retry`; deploy-watch completion tied to the watched build; `carlos instances list` showing installed, serving and reason; console instance view; event wording (copy review); docs for the migrations rule; roll notes | additive | 4, 5 |
+
+Plans 1, 2 and 3 can merge without plan 5. While the flag is off nothing
+changes on a box. Plan 4 can merge at any time.
+
+## Shared interfaces
+
+These names and types are fixed here so plans written in parallel agree.
+A plan may add fields. It may not rename these.
+
+```go
+// internal/activator (plan 1 defines, plans 2, 3, 5 use)
+
+// Gen is one spawn of one instance (spec §3 "Generation").
+type Gen struct {
+ N uint64 // process-wide, never reused (spec §7.1)
+ Socket string // generation app socket (bounded path, spec §7.1)
+ Control string // Socket + ".control"
+ Build string // version name the generation runs
+ StagedPath string // immutable staged executable it exec'd (spec §7.5 step 3)
+ Digest string // sha256 of StagedPath at exec, hex
+ Demand string // the release's own `swap` demand verified for this artifact: "hold" or "" (spec §4.2, §8.5)
+}
+
+// AppSocket names the CURRENT generation's socket for traffic (spec §7.1).
+func (s *Service) AppSocket(host string) (socket string, ok bool)
+
+// GenSocketPath is the bounded flat generation path (spec §7.1):
+// /run/carlos/g/<first 12 hex of sha256(host)>.<n>.sock
+func GenSocketPath(host string, n uint64) string
+```
+
+```go
+// internal/activator (plan 3 defines, plan 5 calls)
+
+type Cause string
+
+const (
+ CauseRelease Cause = "release"
+ CauseInstall Cause = "install"
+ CauseRetry Cause = "retry"
+ CauseRestart Cause = "restart"
+ CauseConfig Cause = "config"
+)
+
+type SwapMode string
+
+const (
+ ModeOverlap SwapMode = "overlap"
+ ModeHold SwapMode = "hold"
+)
+
+// Attempt is one try at moving one instance to a new generation (spec §6.3).
+type Attempt struct {
+ Host string
+ Cause Cause
+ Build string
+ StagedPath string
+ Digest string // expected sha256 of StagedPath, verified before exec
+ Demand string // release demand for the artifact: "hold" or ""
+ Mode SwapMode
+ ConfigRev string
+ RestartID string
+ SchedRev uint64 // scheduler revision; the newest-attempt check compares it
+ Canary bool // observe the 10 s settle
+}
+
+type SwapOutcome string
+
+const (
+ SwapDone SwapOutcome = "done" // switched (overlap) or replaced (hold)
+ SwapFailed SwapOutcome = "failed" // successor failed health or settle; incumbent still serving (overlap)
+ SwapAbandoned SwapOutcome = "abandoned" // superseded or a re-check failed; not a build failure
+ SwapDown SwapOutcome = "down" // hold mode failed: nothing serving
+)
+
+type SwapResult struct {
+ Outcome SwapOutcome
+ Gen Gen // the generation now current (on SwapDone)
+ Reason string
+}
+
+// Swap runs one attempt on an awake entry (spec §7.3–§7.5, §7.8).
+func (s *Service) Swap(ctx context.Context, a Attempt, newest func(host string) uint64) SwapResult
+```
+
+```go
+// internal/release (plan 4 defines, plan 5 uses)
+
+// Demand reads B's own `swap` demand from its CANONICAL manifest and
+// checks the staged artifact against the manifest's digest (spec §4.2).
+// mode "hold" is returned whenever the manifest is unreadable or the
+// digests can't be compared.
+func CanonicalDemand(ctx context.Context, s Store, acct, app, version, platform, stagedPath string) (demand string, digest string, err error)
+
+// StagedIdentity is the retained identity of a staged artifact (spec §4.2, §8.5).
+type StagedIdentity struct {
+ Version string
+ StagedPath string
+ Digest string
+ Demand string
+}
+```
+
+```go
+// internal/registry (plan 5 adds columns)
+// GoodVersion string // spec §8.5
+// GoodDigest string
+// GoodDemand string
+// ServingVersion string // what the current generation runs (spec §3, §9.1)
+```
+
+## Global constraints (every plan)
+
+- `go build ./...`, `go vet ./...`, `gofmt -l .` and the package tests are
+ clean before every commit (AGENTS.md). Use the default Go build cache,
+ not one under `/tmp`. Socket-binding tests need a short `TMPDIR`
+ (`/tmp/k`). See the "gate tmp exec wrapper" note in AGENTS.md.
+- CI cross-compiles darwin, windows and linux-arm64. Every Linux-only
+ syscall lives in a `_linux.go` file with a `!linux` stub.
+- **Dormant until plan 5's flag.** With `CARLOS_SWAP` unset, a box behaves
+ exactly as before: canonical socket paths, one generation per entry,
+ stop-then-wake. Every plan proves that with a flag-off test.
+- No user-facing string ships without copy review (the copy-review
+ skill). No em dashes in any user-facing string.
+- The test trap from #231 applies: `activator.Service.Owns` stays true
+ while an entry drains, so assert stops from the backend's spawn count
+ after a request.
+- Commit after every task, with a message that names the plan and task.
+ Never push to `main`. Merge with `amadan branch merge carlos/platform
+ <branch> -squash -expect <sha>`, after CI is green.
+- Concurrency-heavy plans (3 and 5) end with an invariant simulation at
+ high seed counts and mutation checks, run serially at nice 19 (spec §12;
+ the CPU budget note in AGENTS.md).
+
+## Review focus (cross-plan)
+
+1. **The flag off must be byte-identical behaviour.** Socket paths, cgroup
+ layout, unit names, event text and spawn order are unchanged with
+ `CARLOS_SWAP` unset. Every plan has a flag-off test.
+2. **A box rolled mid-swap.** Agent restart while a successor is spawning
+ or draining: `Shutdown` joins the whole operation set, and the next wake
+ boots the installed build (spec §8.6). Plan 3 owns the test.
+3. **Long hostnames.** A 120-byte hostname gets generation and control
+ sockets under 108 bytes (spec §7.1). Plan 1 owns it.
+4. **An app that never answers `GET /`** with a status below 500. The
+ successor fails health and the incumbent keeps serving, with no request
+ dropped (spec §8.1). Plan 3 owns it.
+5. **Two releases promoted 20 s apart.** The second supersedes queued
+ attempts for the first, per instance. A swap in flight finishes or is
+ abandoned at its re-check, never switching to a superseded build (spec
+ §6.3). Plan 5 owns it.