plan: zero-downtime deploys index — six plans, order, shared interfaces
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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. |