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>
5 files changed,
+617
−3
docs/superpowers/plans/2026-08-01-plan-a-platform-core.md+327 −0docs/superpowers/plans/2026-08-01-plan-b-flagship-kass.md+148 −0docs/superpowers/plans/2026-08-01-plan-c-tito-selfhosted.md+78 −0docs/superpowers/plans/2026-08-01-roadmap.md+58 −0docs/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 |