carlos / platform Public

Add implementation roadmap and Plans A/B/C

Plan A: platform core extraction (registry, edge, lease, activator,
channels, static class, host verbs, ops) from Eleven and titogo,
grounded in a code survey of both. Plan B: flagship deployment in
271376211898 + Kass migration with peppers-first and DNS-hold rules.
Plan C: Tito IE deployment + titogo whole-app tenancy + staged
Hetzner cutover. Spec: lease wording aligned with the shipped
CAS-lease mechanics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 4e0e131aca43d31df691613e7fd9fa33302e2092 parent e8d056d
5 files changed, +617 −3
  • docs/superpowers/plans/2026-08-01-plan-a-platform-core.md +327 −0
  • docs/superpowers/plans/2026-08-01-plan-b-flagship-kass.md +148 −0
  • docs/superpowers/plans/2026-08-01-plan-c-tito-selfhosted.md +78 −0
  • docs/superpowers/plans/2026-08-01-roadmap.md +58 −0
  • docs/superpowers/specs/2026-08-01-carlos-platform-design.md +6 −3
diff --git a/docs/superpowers/plans/2026-08-01-plan-a-platform-core.md b/docs/superpowers/plans/2026-08-01-plan-a-platform-core.md
new file mode 100644
index 000000000..3452fd20f
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-01-plan-a-platform-core.md
@@ -0,0 +1,327 @@
+# πŸ€– Plan A: Platform Core 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.
+
+> πŸ€– Written by an LLM (Claude), per factor X.
+
+**Goal:** One static `carlos` binary β€” registry, edge, lease, activator, releases + channels, static sites, host verbs, ops β€” extracted from Eleven and titogo, testable without AWS.
+
+**Architecture:** Ports of proven code behind the seams Eleven already cut (`actBackend`, `actLease`, `autocert.Cache`), with titogo's three registry/router fixes carried in. All durable platform state is objects in a bucket (S3 in production, a directory in tests). No control-plane server.
+
+**Tech Stack:** Go 1.26, pure Go / CGO_ENABLED=0, `modernc.org/sqlite`, `github.com/benbjohnson/litestream v0.5.14` (library, for the lease), `aws-sdk-go-v2/service/s3`, `golang.org/x/crypto/acme/autocert`.
+
+## Global Constraints
+
+- Module path `github.com/carlosframework/platform`; binary name `carlos`.
+- Pure Go, `CGO_ENABLED=0`; build targets `linux/{arm64,amd64}`, `darwin/arm64`.
+- Env prefix `CARLOS_`; service user `carlos`; dirs `/opt/carlos`, `/data`, `/run/carlos` (all overridable: `CARLOS_OPT_DIR`, `CARLOS_DATA_DIR`, `CARLOS_RUN_DIR`).
+- SQLite pragma order everywhere: `busy_timeout=5000` **before** `journal_mode=WAL`, then `synchronous=NORMAL`; `SetMaxOpenConns(1)`. (titogo's lesson β€” WAL-first fails SQLITE_BUSY under concurrent open.)
+- Migrations additive-only: `CREATE TABLE IF NOT EXISTS` + idempotent `ALTER` list forgiving `duplicate column`.
+- Every source port cites its origin file in a header comment (`// Ported from lchat activator.go @ <sha>` style).
+- πŸ€– marker on every new prose file. Never on Go source.
+- Commit after every green test cycle; `make test` = `go vet ./... && gofmt -l . (empty) && go test ./...`.
+
+**Source checkouts (read-only quarries):**
+- Eleven: `/home/paulca/github.com/elevenmessenger/messenger` (module `lchat`)
+- titogo: `/home/paulca/github.com/tito/titogo` (module `github.com/teamtito/titogo`)
+- Kass: `/home/paulca/github.com/kasstraining/kass` (read via `git show origin/main:<path>` β€” working tree is stale)
+
+---
+
+### Task A1: Module scaffold
+
+**Files:**
+- Create: `go.mod`, `main.go`, `Makefile`, `internal/version/version.go`
+- Test: `internal/version/version_test.go`
+
+**Interfaces:**
+- Produces: `version.Build` (string var, set via `-ldflags "-X github.com/carlosframework/platform/internal/version.Build=<sha>"`), `main.go` subcommand dispatch `switch os.Args[1]` with cases `version` (implemented) and stubs that `log.Fatal("not implemented: <verb>")` for: `edge`, `agent`, `add`, `remove`, `instances`, `ship`, `promote`, `channels`, `canary`, `bootstrap`, `ops`, `vet`.
+
+- [ ] **Step 1: Write failing test** β€” `internal/version/version_test.go`:
+
+```go
+package version
+
+import "testing"
+
+func TestBuildDefaultsToDev(t *testing.T) {
+ if Build != "dev" {
+ t.Fatalf("Build = %q, want dev", Build)
+ }
+}
+```
+
+- [ ] **Step 2: Run** `go test ./internal/version/` β€” FAIL (package missing).
+- [ ] **Step 3: Implement** `internal/version/version.go` (`package version; var Build = "dev"`), `go.mod` (`module github.com/carlosframework/platform`, `go 1.26`), `main.go` dispatch printing `carlos <Build>` for `version`, `Makefile` with `test` and `build` targets (`CGO_ENABLED=0 go build -ldflags "-s -w -X .../internal/version.Build=$$(git rev-parse --short HEAD)" -o carlos .`).
+- [ ] **Step 4: Run** `make test && go run . version` β€” PASS, prints `carlos dev`.
+- [ ] **Step 5: Commit** `feat: carlos binary scaffold with subcommand dispatch`
+
+---
+
+### Task A2: Registry
+
+**Files:**
+- Create: `internal/registry/registry.go`, `internal/registry/registry_test.go`
+- Port from: titogo `internal/carlos/registry.go` (base + fixes) and Eleven `registry.go` (extra columns)
+
+**Interfaces:**
+- Produces:
+
+```go
+package registry
+
+type Route struct {
+ Host, Slug, Kind, Socket, Addr, Status, LastError, DBPath string
+ Version, PrevVersion string
+ App, Pool, Region, Channel string
+ Hibernate, Canary bool
+ Replication string // "" | "stream" | "batched" | "daily"
+ NextDue int64
+ CreatedAt int64
+}
+func Open(path string) (*Registry, error)
+func (r *Registry) List() ([]Route, error) // fresh connection (titogo fix)
+func (r *Registry) Get(host string) (*Route, error)
+func (r *Registry) Upsert(t Route) error
+func (r *Registry) Delete(host string) error
+func (r *Registry) SetVersions(host, ver, prev string) error
+func (r *Registry) SetNextDue(host string, due int64) error
+func (r *Registry) DueInstances(now int64) ([]Route, error)
+func (r *Registry) SetHibernate(host string, on bool) error
+func (r *Registry) Fingerprint() (string, error) // fresh-connection fingerprint query
+```
+
+- `Kind` vocabulary: `instance | service | static`. `Addr` is new: a TCP upstream `127.0.0.1:<port>` used when `Socket` is empty β€” this is what lets whole-app tenants (Kass v1, titogo v1) sit behind the edge without unix-socket support.
+- `Status` vocabulary (from titogo, all of it wired this time): `pending|creating|starting|issuing|ready|failed`.
+
+- [ ] **Step 1: Port tests first.** Copy titogo `internal/carlos/registry_test.go` → adapt package/type names; keep `TestListSeesOtherHandlesCommits` verbatim in spirit (two handles on one file; write via B; A's `List` must see the insert and a status flip). Add `TestDueInstances` (insert two routes, one due, one future; `DueInstances(now)` returns exactly the due one) and `TestUpsertRoundTripsAllColumns` (every field of `Route` survives Upsert→Get).
+- [ ] **Step 2: Run** `go test ./internal/registry/` β€” FAIL (no implementation).
+- [ ] **Step 3: Port implementation.** Base: titogo `registry.go` (pragma order, `SetMaxOpenConns(1)`, fresh-connection `List`, additive migrate). Add columns from Eleven `registry.go:137`: `hibernate INTEGER DEFAULT 0`, `canary INTEGER DEFAULT 0`, `replication TEXT DEFAULT ''`, `next_due INTEGER DEFAULT 0`, plus new `app TEXT DEFAULT ''`, `pool TEXT DEFAULT ''`, `region TEXT DEFAULT ''`, `channel TEXT DEFAULT ''`, `addr TEXT DEFAULT ''`. Fingerprint query: titogo `router.go`'s `SELECT COUNT(*) || ':' || COALESCE(MAX(rowid),0) || ':' || COALESCE(SUM(rowid*17 + LENGTH(status)*3 + LENGTH(socket)),0) FROM routes` β€” extend the SUM with `+ LENGTH(addr)`.
+- [ ] **Step 4: Run** `go test ./internal/registry/ -v` β€” PASS.
+- [ ] **Step 5: Commit** `feat: registry β€” routes table with activator and placement columns`
+
+---
+
+### Task A3: Edge (router)
+
+**Files:**
+- Create: `internal/edge/edge.go`, `internal/edge/certcache.go`, `internal/edge/edge_test.go`
+- Modify: `main.go` (wire `edge` verb)
+- Port from: Eleven `router.go` (identity-free shape), titogo `router.go` (watcher fixes), titogo `internal/carlos/certcache.go`
+
+**Interfaces:**
+- Consumes: `registry.Registry`, `registry.Route` (A2).
+- Produces:
+
+```go
+package edge
+
+type Edge struct { /* reg, upstreams atomic.Pointer[map[string]upstream], act Activator, fallbackURL string */ }
+type Activator interface { // implemented by A5; nil = no hibernation
+ ServeHTTP(w http.ResponseWriter, r *http.Request, rt registry.Route, proxy http.Handler) bool
+ RetireExcept(keep map[string]bool)
+}
+func New(reg *registry.Registry, act Activator) (*Edge, error)
+func (e *Edge) Reload() error
+func (e *Edge) Allowed(host string) bool // autocert HostPolicy
+func (e *Edge) ServeHTTP(w http.ResponseWriter, r *http.Request)
+func Run(args []string) // the `carlos edge` verb
+```
+
+- Route β†’ proxy resolution: `Socket != ""` β†’ unix proxy (port Eleven `unixProxy` + `dialUnixWithRetry`, 13s window); else `Addr != ""` β†’ standard `httputil.NewSingleHostReverseProxy("http://" + Addr)` with the same Host-preservation; `Kind == "static"` β†’ the static handler (stub until A8: 404).
+- Watcher: 2s fingerprint poll via `registry.Fingerprint()` on a fresh connection, **first observation also reloads** (titogo fix); SIGHUP forces reload.
+- Flags for `Run`: `-registry` (default `$CARLOS_DATA_DIR/registry.db`), `-http`, `-dev` (plain HTTP, no ACME), `-fallback` URL, `-acme-cache` (default `$CARLOS_DATA_DIR/acme`).
+- Strip on port: everything Stripe/webhook/identity/`platformIndex` from titogo; keep Eleven's `:80` ACME + 301 + fallback shape.
+
+- [ ] **Step 1: Write failing tests** β€” `internal/edge/edge_test.go`:
+
+```go
+func TestRoutesHostToTCPUpstream(t *testing.T) // httptest backend on 127.0.0.1; registry row with Addr; GET via edge in -dev mode returns backend body, Host header preserved
+func TestSkipsNotReadyRoutes(t *testing.T) // Status=pending route β†’ 404/fallback, Allowed()==false
+func TestReloadPicksUpNewRoute(t *testing.T) // add route via second registry handle, poke Reload(), served
+func TestUnixSocketUpstream(t *testing.T) // httptest.Server on a unix socket via net.Listen("unix"); Socket route proxies
+```
+
+- [ ] **Step 2: Run** β€” FAIL.
+- [ ] **Step 3: Port implementation** per the interface block above.
+- [ ] **Step 4: Run** `go test ./internal/edge/ -v` β€” PASS.
+- [ ] **Step 5: Commit** `feat: edge β€” host router with unix and TCP upstreams, ACME, live reload`
+
+---
+
+### Task A4: Lease + parked replication
+
+**Files:**
+- Create: `internal/lease/lease.go`, `internal/lease/lease_test.go`
+- Port from: Eleven `internal/activator/replica.go` + `replica_test.go` (near-verbatim)
+
+**Interfaces:**
+- Produces (same names as Eleven's package, so `activator.go`'s port in A5 is mechanical):
+
+```go
+package lease
+
+const LeaseTTL = 15 * time.Minute
+func Acquire(ctx context.Context, bucket, prefix, region string) (*Held, error)
+func (h *Held) Err() error
+func (h *Held) Release(ctx context.Context) error
+func (h *Held) Replicate(ctx context.Context, dbPath string, syncInterval time.Duration) (*Replicator, error)
+func (r *Replicator) SyncAndWait(ctx context.Context) error
+func (r *Replicator) Err() error
+func (r *Replicator) Abandon(ctx context.Context)
+func (r *Replicator) Close(ctx context.Context) error // release under ITS OWN 5s budget
+func Restore(ctx context.Context, dbPath, bucket, prefix, region string) (bool, error)
+type HeldError struct{ Owner string; ExpiresAt time.Time } // wraps litestream LeaseExistsError
+```
+
+Carry verbatim (these were learned live, keep the comments):
+1. Lease **before** restore; spawn **before** replicate.
+2. `SetMetaPath(".activator-<db>-litestream")` β€” never share a meta dir.
+3. `Close` releases under its own 5s context, never the caller's.
+4. S3-economics tuning: renew at TTL/3; `compactionLevels` 1h/6h/24h; `l0RetentionCheckInterval = 1h`; `dbMonitorInterval = 10s`.
+
+- [ ] **Step 1:** `go get github.com/benbjohnson/litestream@v0.5.14` and the aws-sdk-go-v2 s3 deps Eleven's go.mod pins (copy exact versions from `/home/paulca/github.com/elevenmessenger/messenger/go.mod`).
+- [ ] **Step 2: Port tests.** Copy Eleven `internal/activator/replica_test.go`; keep its live-vs-unit split exactly (unit tests must run with no AWS; anything needing S3 gates on `CARLOS_TEST_S3_BUCKET` being set and skips otherwise).
+- [ ] **Step 3: Run** β€” FAIL. **Step 4: Port `replica.go` β†’ `lease.go`** with renames only. **Step 5: Run** `go test ./internal/lease/` β€” PASS (live tests skipped).
+- [ ] **Step 6: Commit** `feat: lease β€” single-writer S3 lease + litestream replication (from Eleven)`
+
+---
+
+### Task A5: Activator
+
+**Files:**
+- Create: `internal/activator/activator.go`, `internal/activator/backend_exec.go`, `internal/activator/activator_test.go`
+- Port from: Eleven `activator.go` + `activator_test.go`
+
+**Interfaces:**
+- Consumes: `lease` (A4), `registry.Route` (A2).
+- Produces: `activator.Service` implementing `edge.Activator` (A3); the seam interfaces exactly as Eleven cut them (`actBackend`, `actLease`, `actRep`, `actProc`, `actDueStore` β€” exported as `Backend`, `Lease`, `Rep`, `Proc`, `DueStore`); `Config{Bucket, Region, Prefix string; Idle time.Duration}` from `CARLOS_HIBERNATE_{BUCKET,REGION,PREFIX,IDLE}`; `ExecBackend` (the s3Backend port: exec `live/<host>` symlink with `/data/<host>.env`, `childEnv` stripping every `CARLOS_*` var before layering the env file).
+
+Port decisions:
+- Keep the whole state machine, sweeper, `convergeDecision`, singleflight wake, in-flight tracking, `errLeaseHeld` β†’ 503 + Retry-After.
+- Keep sidecar co-spawn hooks (`hasSidecar` = `/data/<host>-bot.env` exists β†’ rename to `<host>-sidecar.env`) but the `X-Carlos-Sidecar` marker header is the only bot knowledge the platform keeps.
+- Constants verbatim: stop 20s, spawnHealth 30s, converge grace 90s, sidecar backoff 2s→1m.
+
+- [ ] **Step 1: Port tests.** Eleven `activator_test.go` uses fake backends β€” port wholesale; this is the highest-value test file in the whole extraction. Adapt names; zero AWS required.
+- [ ] **Step 2: Run** β€” FAIL. **Step 3: Port `activator.go`** (rename `lchat`β†’`carlos` env/paths; import `lease` from A4; drop `mesh` field β€” single-box v1, keep the field as an interface stub so the mesh port slots in later). **Step 4: Run** `go test ./internal/activator/ -v` β€” PASS.
+- [ ] **Step 5: Commit** `feat: activator β€” hibernation state machine with exec backend (from Eleven)`
+
+---
+
+### Task A6: Releases and channels
+
+**Files:**
+- Create: `internal/release/release.go` (manifest+signing), `internal/release/store.go` (bucket layout), `internal/release/bake.go`, `internal/release/versions.go`, `internal/release/release_test.go`, `internal/release/adopt.go`
+- Modify: `main.go` (`ship`, `promote`, `channels`, `canary` verbs)
+- Port from: Eleven `release.go`, `bake.go`, `versions.go`, parts of `updater.go`
+
+**Interfaces:**
+- Produces:
+
+```go
+package release
+
+// Bucket layout (deployment bucket):
+// apps/<app>/releases/<version>/manifest.json content-addressed, immutable
+// apps/<app>/releases/<version>/<artifact files> binaries or static tree tar
+// apps/<app>/channels/<channel>.json SIGNED channel pointer, CAS via If-Match
+type Manifest struct {
+ Schema int `json:"schema"` // 1
+ App string `json:"app"`
+ Version string `json:"version"` // git short sha
+ Kind string `json:"kind"` // "binary" | "static"
+ Commit, BuiltBy, BuiltAt string
+ Artifacts map[string]Artifact // key "linux-arm64" etc, or "site" for static
+}
+type Artifact struct { File, SHA256 string; Size int64 }
+type ChannelPointer struct {
+ Schema int; App, Channel, Version string
+ PromotedAt time.Time
+ History []string // previous versions, newest first, max 10 β€” rollback targets
+}
+type Store interface { // S3 in prod, DirStore in tests
+ Get(ctx context.Context, key string) ([]byte, string, error) // body, etag
+ Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) // etag CAS; ifMatch "" = If-None-Match:*
+ List(ctx context.Context, prefix string) ([]string, error)
+}
+func Ship(ctx context.Context, s Store, m Manifest, files map[string][]byte) error
+func Promote(ctx context.Context, s Store, priv ed25519.PrivateKey, app, channel, version string) error
+func Channel(ctx context.Context, s Store, pub ed25519.PublicKey, app, channel string) (*ChannelPointer, error)
+func Rollback(ctx context.Context, s Store, priv ed25519.PrivateKey, app, channel string) error
+```
+
+- Signing: port Eleven's sign-the-base64-payload-bytes wrapper (`signedManifest{Payload, Sig}`) applied to `ChannelPointer` (channels are the trust boundary; release manifests are content-addressed so their hash is their integrity). Keys: `CARLOS_RELEASE_KEY` (Ed25519 seed, env only), pubkey pinned in the deployment's host config (`CARLOS_RELEASE_PUBKEY`), not baked into the binary.
+- Bake windows: port `bake.go` intact (first-seen clock on the box, fail-closed, jitter; evacuation/rollback to a previously-seen version never bakes). Default holds: `canary/*`: 0, `beta`: 24h, `stable`: 72h β€” per-deployment override in host config.
+- Adoption: port `versions.go` (`/opt/carlos/versions/<ver>/<app>`, `live/<host>` symlink, `pointLive`, `waitHealthyVersion`) + a minimal `adopt.go` `Pass(reg, store)` that: reads each distinct (app, channel) among routes, resolves the pointer, verifies+downloads artifacts, stages, repoints symlinks β€” awake hosts restart via proc cycle (activator converge), hibernating hosts symlink-only (never woken). Poisoned-build map ported.
+
+- [ ] **Step 1: Write failing tests** β€” DirStore CAS semantics (`TestPutIfNoneMatchConflict`, `TestPutIfMatchStaleEtagFails`); `TestShipThenPromoteThenChannelRoundTrip` (ship v1, promote to canary/x, read back, verify signature); `TestRollbackNeverBakes` (promote v1β†’stable, first-seen now; promote v2β†’stable; rollback; pointer at v1 applies immediately); `TestTamperedPointerRejected` (flip a byte in sig).
+- [ ] **Step 2: Run** β€” FAIL. **Step 3: Implement** (DirStore first β€” `os.ReadFile`/`os.WriteFile` + a `.etag` sidecar file with sha256 as etag; then the S3 store with `If-Match`/`If-None-Match`; then ports). **Step 4: Run** β€” PASS.
+- [ ] **Step 5: Wire CLI verbs** in `main.go`: `carlos ship -app <a> -kind binary -version <sha> <files...>`, `carlos promote -app <a> <version> <channel>`, `carlos channels -app <a>`, `carlos canary -app <a> <files...>` = ship + promote to `canary/<branch>` + `add` a `<branch>.<canary-domain>` route on channel `canary/<branch>`.
+- [ ] **Step 6: Commit** `feat: releases and channels β€” content-addressed artifacts, signed CAS channel pointers, bake windows`
+
+---
+
+### Task A7: Host verbs β€” bootstrap, agent, add/remove/instances
+
+**Files:**
+- Create: `internal/host/bootstrap.go`, `internal/host/units/*.service|*.timer` (embedded), `internal/host/agent.go`, `internal/host/cli.go`, `internal/host/host_test.go`
+- Modify: `main.go`
+- Port from: Eleven `bootstrap.go` + `deploy/systemd/*`, titogo `internal/carlos/cli.go`
+
+**Interfaces:**
+- Consumes: everything above.
+- Produces: `carlos bootstrap` (writes units, dirs `/data /run/carlos /opt/carlos/{versions,live}`, `carlos` user, enables `carlos-edge.service` + `carlos-adopt.timer`); `carlos agent` = edge + activator + adopt loop in one process (the single-box role); `carlos add -app <a> -slug <s> -kind <k> [-addr|-socket] [-channel stable] [-hibernate] <host>`, `carlos remove <host>`, `carlos instances`.
+- Units (ported from Eleven's, renamed, same hardening): `carlos-edge.service` (`User=carlos`, `AmbientCapabilities=CAP_NET_BIND_SERVICE`, `ProtectSystem=strict`, `ReadWritePaths=/data /run/carlos`, `NoNewPrivileges`, `MemoryDenyWriteExecute=yes`), `carlos-adopt.service`+`.timer` (hourly, `OnBootSec=5min`, `RandomizedDelaySec=10min`), `carlos-litestream.service`, `carlos-restore-verify.service`+`.timer` (daily).
+
+- [ ] **Step 1: Tests** β€” `TestAddThenInstancesListsRoute`, `TestAddRejectsReservedSlug` (reserved list from `CARLOS_RESERVED_SLUGS`, comma-separated β€” configuration, not Tito's hardcoded list), `TestBootstrapWritesUnitsToStagingDir` (bootstrap with `-root <tmpdir>` writes every embedded unit; asserts `carlos-edge.service` contains `CAP_NET_BIND_SERVICE` and instance-adjacent units don't).
+- [ ] **Step 2–4:** FAIL β†’ implement β†’ PASS.
+- [ ] **Step 5: Commit** `feat: host verbs β€” bootstrap, agent, add/remove/instances`
+
+---
+
+### Task A8: Static serving class
+
+**Files:**
+- Create: `internal/edge/static.go`, `internal/edge/static_test.go`
+- Modify: `internal/edge/edge.go` (route `Kind=="static"`), `main.go` (`ship -kind static` packs a dir into `site.tar.gz`)
+
+**Interfaces:**
+- Consumes: `release.Store`, `release.Channel` (A6).
+- Produces: `newStaticHandler(app, channel string, store release.Store, pub ed25519.PublicKey, cacheDir string) http.Handler` β€” resolves the channel pointer (cached, re-checked every 60s), ensures `<cacheDir>/<app>/<version>/` is extracted (sha256-verified tar), serves with `http.FileServer` semantics + `index.html` fallback for extensionless paths + correct `Content-Type`. A pointer advance atomically swaps the served version (symlink `current` β†’ version dir, `os.Rename` of a temp symlink).
+
+- [ ] **Step 1: Tests** β€” `TestServesShippedTree` (ship a 3-file tree incl. `index.html` + `docs/page.html`; GET `/`, `/docs/page.html`, missing β†’ 404); `TestChannelAdvanceSwapsContent` (promote v2, poke re-check, GET sees new body); `TestTamperedTarRejected`.
+- [ ] **Step 2–4:** FAIL β†’ implement β†’ PASS.
+- [ ] **Step 5: Commit** `feat: static serving class β€” channel-tracked file trees at the edge`
+
+---
+
+### Task A9: Ops β€” fleet litestream + restore-verify
+
+**Files:**
+- Create: `internal/ops/litestream.go`, `internal/ops/ops_test.go`
+- Modify: `main.go` (`ops` verb: `litestream`, `restore-verify`)
+- Port from: titogo `internal/ops/litestream.go` (config gen, round-trip parse, restore-verify) β€” genericized: bucket/prefix/data-dir from flags/env, unit names `carlos-*`, **skip any DB whose route has `hibernate=1`** (Eleven's two-replicators lesson; read the registry the way Eleven's `fleet-backup.sh` does).
+
+- [ ] **Step 1: Tests** β€” `TestConfigGenSkipsHibernatingRoutes` (registry with 2 routes, one hibernating; generated YAML lists only the other's DB); `TestConfigRoundTrip` (titogo's parse test, ported); `TestCheckRestoredDBRejectsEmptySchema`.
+- [ ] **Step 2–4:** FAIL β†’ implement β†’ PASS. **Step 5: Commit** `feat: ops β€” generated fleet litestream config + daily restore proof`
+
+---
+
+### Task A10: `carlos vet` + end-to-end smoke
+
+**Files:**
+- Create: `internal/vet/vet.go`, `internal/vet/vet_test.go`, `e2e_test.go` (root)
+
+**Interfaces:**
+- Produces: `carlos vet -app <a> -version <v>` β€” checks manifest validity, artifact hashes, and (binary kind) boots the binary with `-socket <tmp>` and polls `/healthz` ≀10s.
+- `e2e_test.go` β€” the whole loop in a temp dir with DirStore and `-dev` edge: ship a tiny test app (build `testdata/echoapp` β€” 30-line http server answering `/healthz` and echoing its `BUILD` env), promote to `canary/x`, `add` a route, run agent, GET through the edge, promote v2, adopt pass, GET sees v2, rollback, GET sees v1.
+
+- [ ] **Steps:** tests FAIL β†’ implement β†’ PASS β†’ **Commit** `feat: vet + end-to-end release/adopt/rollback smoke test`
+
+---
+
+## Self-review notes (applied)
+
+- Spec Β§5 lease phrasing ("lock and manifest are one object") vs reality: Eleven's lease is a separate `lock.json` with ETag CAS providing the same torn-state guarantee; spec amended alongside this plan.
+- Mesh (heartbeats/HRW/route replication), root-domain identity, relocation, sidecar runtime: deliberately absent from Plan A (single-box deployments first); the ports are catalogued in the roadmap for the next plan cycle.
+- Every task runs without AWS; live S3 paths gate on `CARLOS_TEST_S3_BUCKET`.
diff --git a/docs/superpowers/plans/2026-08-01-plan-b-flagship-kass.md b/docs/superpowers/plans/2026-08-01-plan-b-flagship-kass.md
new file mode 100644
index 000000000..f83d65ece
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-01-plan-b-flagship-kass.md
@@ -0,0 +1,148 @@
+# πŸ€– Plan B: Flagship Deployment + Kass Migration 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.
+
+> πŸ€– Written by an LLM (Claude), per factor X.
+
+**Goal:** The flagship CARLOS deployment live in the Keymail AWS account (271376211898) with Kass serving production from it, off Kass's own account (640012953448).
+
+**Architecture:** One `region` of the flagship (eu-west-1) β€” one Graviton box running `carlos agent`, one deployment bucket, one data bucket β€” with Kass as a whole-app `routed` tenant (its own internal per-person machinery unchanged, fronted by the carlos edge over a TCP upstream). Kass's litestream replicas move buckets; its SES identity re-verifies in the flagship account, which already has production access.
+
+**Tech Stack:** OpenTofu (aws ~> 6.0, dnsimple ~> 1.5), AWS profile `keymail`, DNSimple account 74881 (`DNSIMPLE_TOKEN` env), Debian arm64 on t4g.small, `carlos` binary from Plan A.
+
+## Global Constraints
+
+- AWS account 271376211898 only (profile `keymail`); Kass account 640012953448 is READ-ONLY throughout (profile `kass`) β€” nothing there is modified or deleted in this plan.
+- Buckets: `carlos-flagship-271376211898` (deployment), `carlos-data-ie-271376211898` (regional data), `carlos-tfstate-271376211898` (state). eu-west-1.
+- Tofu owns boxes/buckets/IAM/static DNS; the platform owns routes/releases/dynamic DNS. Nothing account-shaped in tofu state.
+- **Irreplaceable-first rule:** the SSM peppers (`/kass/prod/pepper`, `/kass/devbox/pepper`) are copied and verified before any box in the new account touches a restored `registry.db`.
+- **Hostname stability rule:** `home.kass.training` keeps its exact name β€” passkey RP IDs bind to it; a rename without the RPID crossover drill permanently orphans training logs.
+- **Cutover hold:** the production DNS flip (B6) is staged, rehearsed, and HELD for explicit user go. Everything before it is non-destructive to the running service.
+- Keymail's existing infra in 271376211898 is untouched β€” new resources only, `carlos-` prefixed.
+
+---
+
+### Task B1: Flagship tofu skeleton
+
+**Files:**
+- Create: `infra/flagship/main.tf`, `infra/flagship/region-ie.tf`, `infra/flagship/host-init.sh`, `infra/flagship/outputs.tf`, `infra/flagship/.gitignore` (`*.tfstate*`, `.terraform/`)
+
+**Interfaces:**
+- Produces: outputs `ie_eip` (public IP), `deployment_bucket`, `data_bucket_ie`, `host_role_arn`.
+
+- [ ] **Step 1: Bootstrap the state bucket by hand** (state can't store its own bucket):
+
+```bash
+aws --profile keymail s3api create-bucket --bucket carlos-tfstate-271376211898 \
+ --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-1
+aws --profile keymail s3api put-bucket-versioning --bucket carlos-tfstate-271376211898 \
+ --versioning-configuration Status=Enabled
+aws --profile keymail s3api put-public-access-block --bucket carlos-tfstate-271376211898 \
+ --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
+```
+
+- [ ] **Step 2: Write `main.tf`:**
+
+```hcl
+terraform {
+ required_version = ">= 1.10"
+ backend "s3" {
+ bucket = "carlos-tfstate-271376211898"
+ key = "flagship/terraform.tfstate"
+ region = "eu-west-1"
+ profile = "keymail"
+ use_lockfile = true
+ }
+ required_providers {
+ aws = { source = "hashicorp/aws", version = "~> 6.0" }
+ dnsimple = { source = "dnsimple/dnsimple", version = "~> 1.5" }
+ }
+}
+provider "aws" { region = "eu-west-1", profile = "keymail" }
+provider "dnsimple" { account = "74881" } # DNSIMPLE_TOKEN from env, never a file
+
+locals { account = "271376211898" }
+
+resource "aws_s3_bucket" "deployment" {
+ bucket = "carlos-flagship-${local.account}"
+ lifecycle { prevent_destroy = true }
+}
+resource "aws_s3_bucket" "data_ie" {
+ bucket = "carlos-data-ie-${local.account}"
+ lifecycle { prevent_destroy = true }
+}
+# versioning Enabled + full public-access block + AES256 on BOTH buckets
+# (four aws_s3_bucket_versioning / _public_access_block / _server_side_encryption_configuration
+# resources, same shape as Kass's infra/main.tf backups bucket β€” copy that shape)
+```
+
+- [ ] **Step 3: Write `region-ie.tf`** β€” Debian 13 arm64 AMI (data lookup, owner `136693071363` Debian, name `debian-13-arm64-*`), `aws_instance` t4g.small 20GB gp3 encrypted, `aws_eip`, SG (80/443 world, 22 world for now), IAM role `carlos-flagship-host` with policy: full object CRUD on both buckets + `ssm:GetParameter` on `arn:aws:ssm:eu-west-1:271376211898:parameter/carlos/*`; instance profile; `user_data = host-init.sh` (`user_data_replace_on_change = true`).
+ `host-init.sh`: create `carlos` user, dirs `/data /opt/carlos /run/carlos` (tmpfiles.d for /run), install nothing else β€” the binary arrives in B2. Debian: `apt-get install -y unattended-upgrades`.
+- [ ] **Step 4:** `cd infra/flagship && tofu init && tofu plan` β€” review: creates only `carlos-*` resources; nothing keymail-named in the plan. Then `tofu apply -auto-approve`.
+- [ ] **Step 5: Verify** `aws --profile keymail ec2 describe-instances --filters Name=tag:Name,Values=carlos-flagship-ie --query '...PublicIpAddress'` answers; `ssh` reachable.
+- [ ] **Step 6: Commit** `infra: flagship skeleton β€” ie region, buckets, host role` (never commit tfstate).
+
+---
+
+### Task B2: Bootstrap the box with carlos
+
+- [ ] **Step 1:** Build `carlos` for linux/arm64 (`make build GOOS=linux GOARCH=arm64`), scp to the box, `sudo ./carlos bootstrap`, confirm `systemctl status carlos-edge` active, `curl -s http://127.0.0.1/healthz` (edge answers on :80 for unknown-host probe β†’ fallback; healthz endpoint on the edge itself).
+- [ ] **Step 2:** Host env `/etc/carlos/host.env`: `CARLOS_DATA_DIR=/data`, `CARLOS_HIBERNATE_BUCKET=carlos-data-ie-271376211898`, `CARLOS_HIBERNATE_REGION=eu-west-1`, `CARLOS_RELEASE_PUBKEY=<generated>` β€” generate the deployment's Ed25519 release keypair now: seed β†’ SSM `/carlos/release-key` (SecureString), pubkey β†’ host env + committed to `infra/flagship/README.md`.
+- [ ] **Step 3: Smoke:** ship the Plan A `testdata/echoapp` through the real bucket (`carlos ship`/`promote`/`add echo.<eip>.sslip.io -addr ...`), GET it over HTTPS with the ACME cert. This proves edge+release+adopt on real infra before Kass arrives.
+- [ ] **Step 4: Commit** any host-config docs; record box + EIP in `infra/flagship/README.md`.
+
+---
+
+### Task B3: Kass ships as an app
+
+**Interfaces:** Kass runs as ONE route: `Kind=instance`, `App=kass`, `Addr=127.0.0.1:8080`, `Hibernate=false` (resident v1 β€” Kass's own internal activator keeps doing per-person hibernation inside the app).
+
+- [ ] **Step 1:** Build Kass from `origin/main` in a throwaway worktree of `/home/paulca/github.com/kasstraining/kass` (linux/arm64, `CGO_ENABLED=0`, ldflags version stamp β€” same flags as its `scripts/deploy.sh`).
+- [ ] **Step 2:** `carlos ship -app kass -kind binary -version <sha>` + `carlos promote kass <sha> stable`.
+- [ ] **Step 3:** On the box: systemd unit `kass.service` from Kass's `infra/prod/prod-init.sh` (User=kass, `EnvironmentFile=/data/kass.env`, `ExecStart=/opt/carlos/live/kass serve`), where `live/kass` is the adopt symlink β€” **Kass's updates now ride carlos channels.** `/data/kass.env`: `KASS_ADDR=127.0.0.1:8080`, `KASS_ORIGIN=https://home.kass.training`, `KASS_RPID=home.kass.training`, `KASS_DATA=/data/kass`, pepper + SMTP from SSM (B4/B5).
+- [ ] **Step 4:** `carlos add -app kass -slug kass -kind instance -addr 127.0.0.1:8080 -channel stable home.kass.training` β€” route registered; edge will request the cert on first hit (post-DNS; until then a staging hostname `kass.<eip>.sslip.io` route serves for verification).
+
+---
+
+### Task B4: Move the irreplaceables
+
+- [ ] **Step 1: Peppers** (the single point of data loss β€” do first, verify byte-equality):
+
+```bash
+for p in /kass/prod/pepper /kass/devbox/pepper; do
+ v=$(aws --profile kass ssm get-parameter --name $p --with-decryption --query Parameter.Value --output text)
+ aws --profile keymail ssm put-parameter --name /carlos$p --type SecureString --value "$v"
+ n=$(aws --profile keymail ssm get-parameter --name /carlos$p --with-decryption --query Parameter.Value --output text)
+ [ "$v" = "$n" ] && echo "$p OK" || echo "$p MISMATCH β€” STOP"
+done
+```
+
+- [ ] **Step 2: SES in the flagship account** (add to `infra/flagship/mail.tf`, apply): `aws_ses_domain_identity` `kass.training` + `aws_ses_domain_dkim` + 3 `dnsimple_zone_record` DKIM CNAMEs (new tokens β€” the old account's records stay until teardown; DNSimple allows both), `aws_sesv2_configuration_set` `kass-signin` (same event destinations shape as Kass's, SNS topic `carlos-ses-deliverability` β†’ email `paul@rslw.com`), IAM SMTP user `carlos-kass-smtp` (ses:SendRawEmail/SendEmail), SMTP creds β†’ SSM `/carlos/kass/smtp-{user,pass}`. **Production access already enabled in this account β€” Kass's open sandbox case (178507863100598) becomes moot.**
+- [ ] **Step 3: Verify** DKIM shows `Success` (`aws --profile keymail ses get-identity-dkim-attributes --identities kass.training`) and a test send from the box delivers (to `paul@rslw.com`).
+
+---
+
+### Task B5: Data rehearsal β€” restore, run, verify (no production impact)
+
+- [ ] **Step 1:** On the flagship box, restore Kass prod from its live replica (read-only against the old bucket β€” grant via bucket policy is NOT needed: run the restore locally with the `kass` profile creds exported temporarily, or `aws s3 sync` the `prod/` prefix; never write to the old bucket):
+ `litestream restore -o /data/kass/registry.db s3://kass-litestream-640012953448/prod/registry.db` + each `prod/instances/<ref>.db`; `PRAGMA integrity_check` on every restored DB.
+- [ ] **Step 2:** Start `kass.service` with the copied pepper; `curl -s https://kass.<eip>.sslip.io/api/library` returns 200; sign-in email arrives (production SES!); a seeded smoke account's data reads correctly.
+- [ ] **Step 3:** Point Kass's litestream (the box's `carlos ops litestream` fleet config) at `carlos-data-ie-271376211898` prefix `kass/` and prove `carlos ops restore-verify` green β€” **the flagship now holds a complete, verified, independently-replicated copy.**
+- [ ] **Step 4:** Record rehearsal timing (restore duration = the cutover freeze window; expect minutes β€” Kass is small).
+
+---
+
+### Task B6: Cutover β€” staged, rehearsed, HELD
+
+The flip itself, for when the user says go (everything above leaves the old prod untouched and serving):
+
+1. Freeze: stop `kass.service` on OLD box (5-min outage window begins; users see connection errors, no data risk).
+2. Final sync: `litestream restore` the last generation on the flagship box (delta only β€” litestream replays from the old bucket).
+3. Start `kass.service` on flagship; smoke `/api/library` + a real sign-in via `https://home.kass.training` resolved via `curl --resolve home.kass.training:443:<eip>`.
+4. DNS: DNSimple `home.kass.training` A β†’ flagship EIP (TTL 300). **Rollback = flip the record back and restart old `kass.service`** β€” old box and bucket remain intact for weeks.
+5. Watch: edge issues the cert on first real hit; passkey sign-in verified by a real user (RP ID unchanged, must Just Work); `carlos ops restore-verify` green next morning.
+6. Old account: litestream on the old box now double-writes nothing (service stopped) β€” leave everything in 640012953448 frozen for a 30-day cooling period; teardown is a future plan.
+
+- [ ] **Step 1:** Rehearse steps 1–3 against the *rehearsal copy* (B5) with `--resolve`, timing it.
+- [ ] **Step 2:** Write the go/no-go checklist into `infra/flagship/README.md` (pepper verified βœ“, SES delivering βœ“, restore-verify green βœ“, rehearsal < 10 min βœ“, rollback command tested βœ“).
+- [ ] **Step 3: HOLD.** Report readiness to the user; execute on their go.
diff --git a/docs/superpowers/plans/2026-08-01-plan-c-tito-selfhosted.md b/docs/superpowers/plans/2026-08-01-plan-c-tito-selfhosted.md
new file mode 100644
index 000000000..aa756ac96
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-01-plan-c-tito-selfhosted.md
@@ -0,0 +1,78 @@
+# πŸ€– Plan C: Tito Self-Hosted Deployment 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.
+
+> πŸ€– Written by an LLM (Claude), per factor X.
+
+**Goal:** Tito's own CARLOS deployment live in the Tito AWS account (665106592142) β€” IE region first β€” with titogo running on it as its tenant, and the Hetzner cutover staged.
+
+**Architecture:** Per the Tito infra spec (`../specs/2026-08-01-tito-production-infra-design.md`): IE (eu-west-1) hosts the edge/control plane; v1 stands up IE only (the other four regions are later tofu instantiations of the same module). titogo adopts in two stages: (1) whole-app tenant behind the carlos edge (like Kass in Plan B β€” titogo keeps its own router+instances internally), then (2) full adoption where titogo's `internal/carlos` is replaced by the platform (that stage lives in the titogo repo and is planned there once stage 1 is verified).
+
+**Tech Stack:** OpenTofu (aws ~> 6.0), AWS profile `titogo` (assumes role `titogo-infra` into 665106592142), Route53 zone `tito.io`, Debian arm64 t4g.medium, Gotenberg on t4g.small, `carlos` binary from Plan A.
+
+## Global Constraints
+
+- AWS account 665106592142 (profile `titogo`), eu-west-1 first. DNS is **Route53** (`tito.io` zone exists) β€” not DNSimple.
+- Buckets per the Tito spec: `carlos-tito-platform-665106592142` (deployment), `carlos-tito-ie-665106592142` (IE data), `carlos-tfstate-665106592142` (state).
+- The Hetzner box (62.238.45.61) runs today's production untouched throughout; it has a **3.8 GB RAM redline** β€” never start extra processes there.
+- The existing litestream bucket `titogo-litestream-665106592142` is read-only source material.
+- **Cutover hold:** the `go.tito.io` + wildcard DNS flip is staged and HELD for explicit user go.
+- Tofu owns boxes/buckets/IAM/static DNS; platform owns routes/releases/dynamic DNS.
+
+---
+
+### Task C1: Tito tofu skeleton (spec slice 1)
+
+**Files:**
+- Create: `infra/tito/main.tf`, `infra/tito/region-ie.tf`, `infra/tito/svc-pdf.tf`, `infra/tito/dns.tf`, `infra/tito/host-init.sh`, `infra/tito/outputs.tf`
+
+- [ ] **Step 1:** Bootstrap `carlos-tfstate-665106592142` by hand (same three `aws s3api` commands as Plan B Step B1.1, profile `titogo`).
+- [ ] **Step 2:** `main.tf` β€” same shape as Plan B's but: backend bucket `carlos-tfstate-665106592142` key `tito/terraform.tfstate` profile `titogo`; no dnsimple provider; deployment bucket `carlos-tito-platform-665106592142`; data bucket `carlos-tito-ie-665106592142` (both versioned, encrypted, public-blocked, `prevent_destroy`).
+- [ ] **Step 3:** `region-ie.tf` β€” t4g.medium Debian arm64 + EIP + SG(80/443/22) + IAM role `carlos-tito-host-ie` scoped to the deployment bucket + the IE data bucket + `ssm:GetParameter` on `parameter/carlos/*`; same `host-init.sh` shape as Plan B.
+- [ ] **Step 4:** `svc-pdf.tf` β€” t4g.small, Docker + Gotenberg pinned image behind Caddy basic-auth (port Kass's caddy-unit shape), SG ingress 443 **only from the IE box's EIP**, no bucket access, no volume beyond the OS. Outputs `pdf_url`. (Tito's invoice work can consume this immediately.)
+- [ ] **Step 5:** `dns.tf` β€” Route53 records in the existing `tito.io` zone: `ie.go.tito.io` A β†’ IE EIP. **NOT touched:** `go.tito.io` and `*.go.tito.io` (they keep pointing at Hetzner until cutover).
+- [ ] **Step 6:** `tofu init && tofu plan` (review: nothing touches existing records or the litestream bucket) `&& tofu apply -auto-approve`; verify box + Gotenberg answer. **Commit** `infra: tito skeleton β€” ie region, buckets, svc-pdf, static DNS`.
+
+---
+
+### Task C2: Bootstrap + titogo as whole-app tenant on a canary hostname
+
+- [ ] **Step 1:** `carlos bootstrap` the IE box (as Plan B2), host env pointing at the Tito buckets; generate Tito's own release keypair β†’ SSM `/carlos/release-key` in 665106592142. (Separate deployment, separate trust root β€” by design.)
+- [ ] **Step 2:** Build titogo from `origin/main` (linux/amd64 flags exist today; add arm64: `GOOS=linux GOARCH=arm64 CGO_ENABLED=0` β€” pure Go, no change needed), `carlos ship -app titogo`, promote to `canary/platform-adoption`.
+- [ ] **Step 3:** Run titogo's OWN router as the app process on the box (`titogo router` with `TITO_REGISTRY=/data/titogo/registry.db`, dev-mode HTTP on `127.0.0.1:8443`), its instances as children exactly as on Hetzner (`titogo add` + systemd template ported by `carlos bootstrap`'s unit dir β€” copy `titogo@.service` from the titogo repo's `deploy/systemd/`, pointing at `/opt/carlos/live/titogo`).
+- [ ] **Step 4:** `carlos add -app titogo -kind instance -addr 127.0.0.1:8443 -channel canary/platform-adoption platform.ie.go.tito.io` β†’ seed one canary account (`titogo seed`) β†’ **verify the full titogo canary checklist over HTTPS at `https://platform.ie.go.tito.io`** (sign-in magic link via Tito's existing SES SMTP creds, seeded event renders, checkout loads).
+- [ ] **Step 5:** litestream: `carlos ops litestream` fleet config β†’ `carlos-tito-ie-665106592142` prefix `titogo/`; `carlos ops restore-verify` green. **Commit** docs/notes.
+
+---
+
+### Task C3: Rehearse the Hetzner restore (no production impact)
+
+- [ ] **Step 1:** Restore the full Hetzner fleet onto the IE box from `titogo-litestream-665106592142/hetzner/` (registry.db + every instance DB) into `/data/titogo-rehearsal/`; integrity-check all.
+- [ ] **Step 2:** Boot the restored registry+instances on a rehearsal port; `curl --resolve go.tito.io:443:<eip>` through the carlos edge with a temporary route; verify sign-in + a known account's event page against production data (read-only checks only; **no emails: leave SMTP env unset in the rehearsal so sends log instead** β€” titogo's `mail.Logged` behavior).
+- [ ] **Step 3:** Record restore duration (= cutover freeze window). Tear down the rehearsal processes.
+
+---
+
+### Task C4: Cutover β€” staged, HELD (spec slice 6)
+
+For when the user says go:
+
+1. Freeze: stop `titogo-router` + `titogo@*` on Hetzner (outage window begins).
+2. Final litestream delta restore on IE box; start titogo under carlos.
+3. Smoke via `--resolve` on `go.tito.io`: sign-in, `/api/version`, one instance page.
+4. Route53: `go.tito.io` A + `*.go.tito.io` β†’ IE EIP (TTL 60 first, raise later). **Rollback = point both records back at 62.238.45.61 and restart the Hetzner units.**
+5. Existing accounts get residency `ie` in the carlos registry; Hetzner box frozen (not wiped) for 30 days.
+
+- [ ] **Step 1:** Write the go/no-go checklist into `infra/tito/README.md` (rehearsal timing, rollback tested, restore-verify green, canary checklist passed).
+- [ ] **Step 2: HOLD.** Report readiness; execute on user go.
+
+---
+
+### Task C5: Stage-2 adoption plan (deferred pointer)
+
+Full adoption β€” titogo swaps `internal/carlos` for the platform (library or
+binary contract, Tito-spec open question 1), per-instance hibernation under
+the platform activator, root-domain identity port, self-serve signup,
+remaining four regions β€” is planned **in the titogo repo** after C4
+verifies, following its CLAUDE.md worktree/canary discipline. Not part of
+this plan.
diff --git a/docs/superpowers/plans/2026-08-01-roadmap.md b/docs/superpowers/plans/2026-08-01-roadmap.md
new file mode 100644
index 000000000..f59760130
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-01-roadmap.md
@@ -0,0 +1,58 @@
+# πŸ€– CARLOS Platform β€” Implementation Roadmap
+
+> πŸ€– Written by an LLM (Claude), on the ideas, instruction, and editing of
+> humans, per factor X.
+
+**Session goals (Paul, 2026-08-01):**
+1. Kass running on CARLOS platform flagship infra in the Keymail AWS
+ account (271376211898), rather than its own account (640012953448).
+2. Tito Go running on CARLOS platform self-hosted infra in the Tito AWS
+ account (665106592142).
+
+Migration plans for Keymail/Woodstar/Slopbox/Eleven come later, after
+these two are verified.
+
+## The three plans
+
+| Plan | File | Delivers | Depends on |
+|---|---|---|---|
+| A | `2026-08-01-plan-a-platform-core.md` | The `carlos` binary: registry, edge, lease, activator, releases+channels, static class, host verbs, ops | β€” |
+| B | `2026-08-01-plan-b-flagship-kass.md` | Flagship deployment in 271376211898; Kass migrated onto it | A (tasks A1–A7) |
+| C | `2026-08-01-plan-c-tito-selfhosted.md` | Tito deployment in 665106592142 (IE first); titogo adoption path; Hetzner cutover staged | A (tasks A1–A8) |
+
+B and C are independent of each other and can proceed in parallel once
+Plan A's core lands. Within B and C, all AWS infra creation is
+session-authorized; **production DNS cutovers are staged and held for
+explicit user go** unless rollback is proven trivial during execution.
+
+## Extraction sources (surveyed 2026-08-01)
+
+- **Eleven** (`~/github.com/elevenmessenger/messenger`, module `lchat`):
+ activator state machine + `actBackend` seam (`activator.go`), litestream
+ lease (`internal/activator/replica.go`), mesh heartbeats/HRW
+ (`mesh.go`, `internal/activator/heartbeat.go`), signed release feed +
+ bake windows (`release.go`, `updater.go`, `bake.go`), versioned
+ side-by-side deploys (`versions.go`), bootstrap/units, relocation.
+- **titogo** (`~/github.com/tito/titogo`, `internal/carlos`): the
+ registry + router with three fixes Eleven lacks (busy_timeout before
+ WAL; `List()` on a fresh connection; first-fingerprint reload), ops
+ verbs (deploy/litestream/restore-verify), root-domain identity
+ (deferred to a later plan β€” Kass brings its own identity).
+- **Kass** (`~/github.com/kasstraining/kass`): already CARLOS-shaped
+ (its own activator + park/lease with `DirStore`); migrates as a
+ whole-app tenant first, adopts platform primitives later.
+
+## Deliberately deferred (spec'd, not in these plans)
+
+- Mergeable stores (Eleven's multi-writer merge is designed, not built)
+- Graduation to attached databases (Postgres/MySQL)
+- `svc/mail` as a fronting service β€” v1 is per-app SES credentials +
+ configuration sets in the flagship account, which is the same
+ isolation without the service front
+- `svc/pdf` beyond the Gotenberg box (Plan C creates the box)
+- Replicated edges, multi-edge, cross-region mesh (single-box-per-
+ deployment first; the mesh code ports later)
+- Root-domain identity in the platform edge (titogo's
+ auth/TOTP/SAML port) β€” needed for Tito full adoption, not for Kass
+- Sidecar class beyond Eleven's co-spawn shape
+- Keymail/Woodstar/Slopbox/Eleven migrations; Kass account teardown
diff --git a/docs/superpowers/specs/2026-08-01-carlos-platform-design.md b/docs/superpowers/specs/2026-08-01-carlos-platform-design.md
index 00fff5b52..4d348ffd6 100644
--- a/docs/superpowers/specs/2026-08-01-carlos-platform-design.md
+++ b/docs/superpowers/specs/2026-08-01-carlos-platform-design.md
@@ -174,9 +174,12 @@ convention.
### Storage: ownership
-- **`exclusive`** β€” one database under the single-writer lease, where the
- lock and the manifest are one object (torn-state protection inherited
- from CARLOS). Replication never touches this invariant: read replicas
+- **`exclusive`** β€” one database under the single-writer lease: a lease
+ object beside the replica data, taken and renewed by conditional
+ writes (ETag compare-and-swap), so a takeover can never read a torn
+ state β€” leadership is lost loudly, never split. (Implementation
+ reality per Eleven: the lease is `lock.json` + CAS, not literally
+ lock-and-manifest-in-one-object.) Replication never touches this invariant: read replicas
are read-only, and there is no multi-writer mode for mutable stores.
- **`mergeable`** β€” for append-only-style apps (Eleven's shape).
"Multi-writer" is structured as **many single-writer streams plus a