carlos / platform Public

plan: carloku deploy story (deploy URL output, hello demo, site update)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell a1e15c3ce2384dba84951e267a76f7314e6eb68e parent e52a9d5
1 file changed, +480 −0
  • docs/superpowers/plans/2026-08-09-carloku-deploy-story.md +480 −0
diff --git a/docs/superpowers/plans/2026-08-09-carloku-deploy-story.md b/docs/superpowers/plans/2026-08-09-carloku-deploy-story.md
new file mode 100644
index 000000000..99c78fc39
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-09-carloku-deploy-story.md
@@ -0,0 +1,480 @@
+# Carloku Deploy Story 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.
+
+**Goal:** `carlos deploy` names its destination URL on every path, and carloku.com's homepage shows a real captured deploy session of a real hello-world app at `hello.<sqid>.oncarlos.com`.
+
+**Architecture:** A small output addition to the deploy verb (platform repo), a ~20-line demo app in a new public repo, production provisioning done entirely through the carlos CLI, and a copy/terminal update to the carloku site's flagship-redesign lineage.
+
+**Tech Stack:** Go 1.26 (platform + hello), static HTML/CSS (carloku site, zero dependencies).
+
+**Spec:** `docs/superpowers/specs/2026-08-09-carloku-deploy-story-design.md`
+
+## Global Constraints
+
+- Go commands in the sandbox: `GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off`; disable sandbox if the toolchain reports "read-only file system".
+- Platform gates before any commit lands: `go build ./...`, `go vet ./...`, `go test ./... -count=1` all clean, `gofmt -l` clean on touched files, plus `GOOS=darwin go build .` (the CLI ships to Macs).
+- Delegation (CLAUDE.md): Task 1 and Task 3 are Sonnet implementation briefs; the platform change gets an Opus review before merge (member-facing production CLI). Tasks 4–7 are driver-run (production credentials, outward-facing publishing).
+- Operate via API/CLI only for provisioning — no AWS, no SSM; a missing CLI verb is a filed product gap.
+- Carloku site rules: zero dependencies, no JS, light+dark via `prefers-color-scheme`, 🤖 marker cascades from headings, no pricing copy, hero copy verbatim from the spec.
+- Runtime substitution tokens used below: `«SQID»` = the demo account's minted sqid (Task 4), `«VER»` = the hello build's short git SHA (Task 5). Every occurrence must be substituted before commit; grep for `«` to verify none remain.
+
+---
+
+### Task 1: `deployingLine` in the deploy verb (Sonnet)
+
+**Files:**
+- Modify: `deploy.go` (worktree `.claude/worktrees/deploy-url-output`)
+- Test: `deploy_test.go` (new test func), `deploy_e2e_test.go` (extend both e2e tests)
+
+**Interfaces:**
+- Produces: `deployingLine(hosts []string, channel string) string` — "" for 0 hosts, `"deploying https://<host>"` for 1, `"deploying <N> instances on <channel>"` for N>1. Printed by `runDeploy` after `deployTargets` resolves, before `runShipAPI`.
+
+- [ ] **Step 1: Write the failing unit test** in `deploy_test.go`:
+
+```go
+// TestDeployingLine pins the destination line deploy prints before ship:
+// the one place every later path (held, watch, even a ship failure) gets
+// its "where is this going" from. Zero hosts is the no-instance path,
+// which keeps its own honest "nothing to watch" message instead.
+func TestDeployingLine(t *testing.T) {
+ cases := []struct {
+ name string
+ hosts []string
+ channel string
+ want string
+ }{
+ {"no hosts", nil, "stable", ""},
+ {"one host", []string{"hello.baf.oncarlos.com"}, "stable",
+ "deploying https://hello.baf.oncarlos.com"},
+ {"fleet", []string{"a.example.com", "b.example.com", "c.example.com"}, "stable",
+ "deploying 3 instances on stable"},
+ }
+ for _, c := range cases {
+ if got := deployingLine(c.hosts, c.channel); got != c.want {
+ t.Errorf("%s: deployingLine = %q, want %q", c.name, got, c.want)
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Run it to verify it fails**
+
+Run: `go test -run TestDeployingLine .`
+Expected: FAIL — `undefined: deployingLine`
+
+- [ ] **Step 3: Extend the e2e tests.** In `TestE2EDeployThroughConsole` (deploy_e2e_test.go), add the deploying line to the ordered wants and assert it prints before ship:
+
+```go
+ // (in the existing wants loop, add as the FIRST entry:)
+ "deploying https://" + host,
+```
+
+and after the existing shipped/promoted order check:
+
+```go
+ if i, j := strings.Index(out, "deploying https://"), strings.Index(out, "shipped"); i < 0 || i > j {
+ t.Fatalf("deploying line missing or printed after shipped:\n%s", out)
+ }
+```
+
+In `TestE2EStaticDeployThroughConsole`, add `"deploying https://" + host` to that test's expected-output list, and assert the watching note no longer repeats the host:
+
+```go
+ if strings.Contains(out, "watching "+host) {
+ t.Fatalf("static note still repeats the host the deploying line just printed:\n%s", out)
+ }
+```
+
+(The existing `"re-checks its channel"` assertion stays — the window explanation survives.)
+
+- [ ] **Step 4: Run e2e tests to verify they fail**
+
+Run: `go test -run 'TestE2EDeployThroughConsole|TestE2EStaticDeployThroughConsole' .`
+Expected: FAIL — output missing "deploying https://…"
+
+- [ ] **Step 5: Implement.** In `deploy.go`, add after the `deployTargets` error check (currently `log.Fatalf("deploy: %v", err)`, before the `runShipAPI` call and its printBanner comment):
+
+```go
+ if line := deployingLine(hosts, channel); line != "" {
+ fmt.Fprintln(stdout, line)
+ }
+```
+
+Add the function (near `deployTargets`, with a doc comment in the file's voice):
+
+```go
+// deployingLine names the destination a deploy is about to write to,
+// printed BEFORE ship so every later path — the bake hold, the watch,
+// even a ship failure — has already said where this deploy was going.
+// One instance gets its URL; a fleet-sized channel gets a count (the
+// per-host "live" lines and timeout diagnostics name hosts as they
+// resolve), and the no-instance path gets nothing: there is no URL to
+// name, and its own "promoted, nothing to watch" message stays the
+// honest report.
+func deployingLine(hosts []string, channel string) string {
+ switch len(hosts) {
+ case 0:
+ return ""
+ case 1:
+ return "deploying https://" + hosts[0]
+ }
+ return fmt.Sprintf("deploying %d instances on %s", len(hosts), channel)
+}
+```
+
+Change the static note (drop the host list, keep the window):
+
+```go
+ mutedf(stdout, "a static route re-checks its channel about every %ds, so the URL can take that long to flip\n",
+ int(staticRecheckWindow.Seconds()))
+```
+
+(`strings` stays imported — other call sites use it.)
+
+- [ ] **Step 6: Run the full package tests**
+
+Run: `go test -count=1 .`
+Expected: PASS (including `TestDeployingLine` and both e2es)
+
+- [ ] **Step 7: Gates**
+
+Run: `gofmt -l deploy.go deploy_test.go deploy_e2e_test.go` (no output), `go build ./... && go vet ./... && go test ./... -count=1`, `GOOS=darwin go build .`
+Expected: all clean.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add deploy.go deploy_test.go deploy_e2e_test.go
+git commit -m "deploy: name the destination URL before ship on every path
+
+One instance prints its URL; a fleet-sized channel prints an aggregate
+count (the live lines and timeout diagnostics already name hosts one by
+one); the static note stops repeating hosts the deploying line just
+printed. The held-for-bake path now names where the promote was going
+before it returns."
+```
+
+---
+
+### Task 2: Platform PR, Opus review, merge (driver)
+
+**Files:** none new — branch `worktree-deploy-url-output` (spec + plan + Task 1 commits).
+
+- [ ] **Step 1: Push and open the PR**
+
+```bash
+git push -u origin worktree-deploy-url-output
+gh pr create --title "carlos deploy: name the destination URL on every path" \
+ --body "spec: docs/superpowers/specs/2026-08-09-carloku-deploy-story-design.md — deploying line (aggregate for fleets) printed before ship; static note stops repeating hosts.
+
+🤖 Generated with [Claude Code](https://claude.com/claude-code)"
+```
+
+- [ ] **Step 2: Opus review** — dispatch an Opus subagent (superpowers:requesting-code-review shape) over the branch diff; address findings with the receiving-code-review discipline; re-run gates after any change.
+- [ ] **Step 3: Merge once CI is green** (`gh pr checks`, then `gh pr merge --merge`). Do not merge red.
+- [ ] **Step 4: Build the fresh CLI from merged main** into `$CLAUDE_JOB_DIR/tmp/carlos` (`git fetch origin && git -C <main checkout> ... go build -o $CLAUDE_JOB_DIR/tmp/carlos .` from origin/main — never a stale checkout; verify with `$CLAUDE_JOB_DIR/tmp/carlos -h` and `git rev-parse origin/main`).
+
+---
+
+### Task 3: carlosframework/hello repo (Sonnet writes files; driver publishes)
+
+**Files:**
+- Create: `~/github.com/carlosframework/hello/{main.go,main_test.go,go.mod,README.md,LICENSE}`
+
+**Interfaces:**
+- Produces: a binary matching the exec-backend contract — launched as `hello --socket <path> --db <path>` (backend_exec.go:440), serving `hello world\n` over the unix socket. The activator removes stale sockets and normalizes the socket mode to 0660 after the health wait; the app does neither.
+
+- [ ] **Step 1: Scaffold**
+
+```bash
+mkdir -p ~/github.com/carlosframework/hello && cd ~/github.com/carlosframework/hello && git init -b main
+printf 'module github.com/carlosframework/hello\n\ngo 1.26\n' > go.mod
+```
+
+LICENSE: MIT, `Copyright (c) 2026 Paul Campbell` (org convention, as in the carloku repo).
+
+- [ ] **Step 2: Write the failing test** (`main_test.go`):
+
+```go
+package main
+
+import (
+ "context"
+ "io"
+ "net"
+ "net/http"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+// TestServesHelloWorld drives the app exactly as the platform's activator
+// does: a unix socket path, then HTTP over that socket.
+func TestServesHelloWorld(t *testing.T) {
+ socket := filepath.Join(t.TempDir(), "hello.sock")
+ go func() {
+ if err := run(socket); err != nil {
+ t.Error(err)
+ }
+ }()
+ client := &http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
+ var d net.Dialer
+ return d.DialContext(ctx, "unix", socket)
+ },
+ },
+ Timeout: 5 * time.Second,
+ }
+ deadline := time.Now().Add(5 * time.Second)
+ for {
+ resp, err := client.Get("http://hello/")
+ if err == nil {
+ defer resp.Body.Close()
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp.StatusCode != 200 || string(body) != "hello world\n" {
+ t.Fatalf("GET / = %d %q, want 200 %q", resp.StatusCode, body, "hello world\n")
+ }
+ return
+ }
+ if time.Now().After(deadline) {
+ t.Fatalf("server never came up: %v", err)
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
+}
+```
+
+- [ ] **Step 3: Run to verify it fails** — `go test .` → FAIL, `undefined: run`.
+
+- [ ] **Step 4: Implement** (`main.go`):
+
+```go
+// Command hello is the app on the carloku.com homepage: the smallest real
+// CARLOS app. The platform launches it as `hello --socket <path> --db
+// <path>` and proxies HTTPS traffic to the unix socket; everything it
+// answers is "hello world".
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+)
+
+func main() {
+ socket := flag.String("socket", "", "unix socket path the platform proxies to")
+ flag.String("db", "", "per-instance database path (unused)")
+ flag.Parse()
+ if *socket == "" {
+ log.Fatal("usage: hello --socket <path> [--db <path>]")
+ }
+ log.Fatal(run(*socket))
+}
+
+func run(socket string) error {
+ l, err := net.Listen("unix", socket)
+ if err != nil {
+ return err
+ }
+ return http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintln(w, "hello world")
+ }))
+}
+```
+
+- [ ] **Step 5: Run tests + gates** — `go test -count=1 . && go vet ./... && gofmt -l .` all clean; `GOOS=linux GOARCH=arm64 go build -o hello-linux-arm64 .` compiles (delete the artifact; it is built again at deploy time and never committed).
+
+- [ ] **Step 6: README.md** (exact content; substitute `«SQID»` in Task 5 — commit the README only after substitution):
+
+```markdown
+# 🤖 hello
+
+The app running on the [carloku.com](https://carloku.com) homepage — the
+smallest real CARLOS app. It serves `hello world` at
+<https://hello.«SQID».oncarlos.com>, and the terminal session on the
+homepage is a real capture of deploying it:
+
+ GOOS=linux GOARCH=arm64 go build -o hello-linux-arm64 .
+ carlos deploy -app hello ./hello-linux-arm64
+
+`carlos deploy` ships the binary, promotes it to the channel the app's
+instance follows, and waits until the URL serves the new build. When it
+prints `live`, it is.
+
+The platform runs the binary with `--socket <path>` and proxies HTTPS to
+that unix socket; when nobody visits for a while the process hibernates,
+and the first request wakes it.
+```
+
+- [ ] **Step 7 (driver): create the public repo and push**
+
+```bash
+cd ~/github.com/carlosframework/hello && git add -A && git commit -m "hello: the carloku.com homepage demo app"
+gh repo create carlosframework/hello --public --source . --push \
+ --description "The hello-world app on the carloku.com homepage — the smallest real CARLOS app"
+```
+
+(README lands in a second commit in Task 5 once «SQID» is real.)
+
+---
+
+### Task 4: Provision demo account + app + instance (driver, production)
+
+All through the fresh CLI from Task 2 Step 4 (`$CLAUDE_JOB_DIR/tmp/carlos`), against the flagship console as paul@keymail.dev.
+
+- [ ] **Step 1:** `carlos accounts create -name demo` — capture the minted sqid from the output; that value is `«SQID»` everywhere below. Expect the next sequential sqid (likely `baf`).
+- [ ] **Step 2:** `carlos apps create -app hello -account demo`
+- [ ] **Step 3:** `carlos instances enable -app hello -account demo -channel stable`
+- [ ] **Step 4:** `carlos instances create -app hello -account demo -host hello.«SQID».oncarlos.com`
+- [ ] **Step 5: Wait for the domains pass** to mint `*.«SQID».oncarlos.com` DNS + wildcard cert. Poll (up to ~10 min):
+
+```bash
+until curl -sI --max-time 10 https://hello.«SQID».oncarlos.com/ >/dev/null 2>&1; do sleep 30; done
+curl -sIv https://hello.«SQID».oncarlos.com/ 2>&1 | grep -i 'subject\|HTTP/'
+```
+
+A non-2xx here is fine (nothing deployed yet) — the deliverable is a clean TLS handshake on the wildcard. If the pass doesn't mint within ~15 min, check `carlos` channel/route listing for the account before touching anything else; an unmintable account is a product gap to file, not an SSM session.
+
+---
+
+### Task 5: Deploy hello and capture the real session (driver)
+
+- [ ] **Step 1:** In `~/github.com/carlosframework/hello`: `GOOS=linux GOARCH=arm64 go build -o hello-linux-arm64 .`; `«VER»` = `git rev-parse --short HEAD`.
+- [ ] **Step 2:** Run and capture verbatim:
+
+```bash
+script -qec 'carlos deploy -app hello -account demo ./hello-linux-arm64' $CLAUDE_JOB_DIR/tmp/deploy-session.txt
+```
+
+Expected shape (this becomes the homepage terminal): `using https://console.carloku.com as paul@keymail.dev` / `deploying https://hello.«SQID».oncarlos.com` / `shipped hello «VER»` / `promoted hello «VER» -> stable` / `live https://hello.«SQID».oncarlos.com`.
+
+- [ ] **Step 3: Verify against the thing changed, with the binary built:**
+
+```bash
+curl -s https://hello.«SQID».oncarlos.com/ # exactly "hello world\n"
+curl -sI https://hello.«SQID».oncarlos.com/ | grep -i x-carlos-version # «VER»
+```
+
+- [ ] **Step 4:** Substitute `«SQID»` into the hello README, commit, push (`git add README.md && git commit -m "README: the live URL" && git push`).
+
+---
+
+### Task 6: carloku.com pages (driver)
+
+**Files** (worktree of the carloku repo, branched off `origin/flagship-redesign`):
+- Modify: `index.html`, `get-started/index.html`; touch `site.css` only if the term-note link needs a rule that `hero-note` doesn't already give.
+
+```bash
+cd ~/github.com/carlosframework/carloku && git worktree add .claude/worktrees/deploy-hero -b deploy-hero origin/flagship-redesign
+```
+
+Read `DESIGN.md` first; reuse existing classes (`term`, `line cmd/ok/cmt`, `data-type/data-out`, `hero-note`, `steps`). All copy below is exact; substitute «SQID»/«VER» from Tasks 4–5, then `grep -n '«' *.html get-started/*.html` must be empty.
+
+- [ ] **Step 1: Hero copy** (index.html) — replace the `<h1>` content with `The uncompromising app platform` (keep the h1 element and classes; drop the nobr/dot spans) and the `.lede` paragraph with, verbatim:
+
+```
+Carloku is an app hosting platform that accepts no tradeoffs. Deploy in
+seconds. Scale to billions. Simple commands, massive power. Cloud, hybrid,
+and on-prem: all first-class. Welcome to the new standard. Get ready to ship.
+```
+
+- [ ] **Step 2: Head metadata** — `<title>Carloku — the uncompromising app platform</title>`; `<meta name="description">` = `🤖 ` + the subtitle verbatim; `og:title` unchanged; `og:description` = the subtitle verbatim (no emoji, matching the current pattern).
+
+- [ ] **Step 3: Terminal** — replace the `.term-body` lines with the captured session, and update the aria-label:
+
+```html
+<div class="term" role="img" aria-label="A real terminal session: carlos deploy ships the app, promotes it, and waits until the URL serves the new build; it ends with the app's live URL.">
+ <div class="term-bar" aria-hidden="true"><i></i><i></i><i></i><span>you@laptop — carlos</span></div>
+ <div class="term-body" aria-hidden="true">
+ <span class="line cmd" data-type>carlos deploy -app hello ./hello-linux-arm64</span>
+ <span class="line" data-out>using https://console.carloku.com as you@keymail.dev</span>
+ <span class="line" data-out>deploying https://hello.«SQID».oncarlos.com</span>
+ <span class="line ok" data-out>shipped hello «VER»</span>
+ <span class="line ok" data-out>promoted hello «VER» -&gt; stable</span>
+ <span class="line ok" data-out>live https://hello.«SQID».oncarlos.com</span>
+ <span class="line"><span class="term-caret"></span></span>
+ </div>
+</div>
+<p class="hero-note">That session is real, and so is the app —
+<a href="https://hello.«SQID».oncarlos.com">open it</a> ·
+<a href="https://github.com/carlosframework/hello">source</a>.</p>
+```
+
+(If the captured session differs in any way from the expected shape, the page shows the CAPTURE, not the plan's guess — the whole point is that this is a paste of reality. `you@keymail.dev` in the `using` line is the one permitted cosmetic substitution, replacing `paul@keymail.dev`.)
+
+- [ ] **Step 4: Workflow section** (index.html `#workflow`) — replace the three `.steps` items:
+
+```html
+<li>
+ <h3><code>carlos deploy</code></h3>
+ <p>One command: it ships your app, promotes it, and waits until the URL
+ serves the new build. When it prints <code>live</code>, it is — seconds,
+ not pipelines.</p>
+</li>
+<li>
+ <h3>Want to look first?</h3>
+ <p>Take it in two motions: <code>carlos ship</code> gives you a private
+ link to the real, running thing; <code>carlos promote</code> makes the
+ exact thing you approved the public thing. Nothing is rebuilt behind
+ your back.</p>
+</li>
+<li>
+ <h3>Changed your mind?</h3>
+ <p>Point at yesterday's version and promote that instead. Going back is
+ the same small motion as going forward.</p>
+</li>
+```
+
+Update the section lede's first words to match ("One command — or three small motions. No pipelines to babysit, no containers to wrap your app in — you ship the thing itself.").
+
+- [ ] **Step 5: get-started** — after the Promote section (before its `p.note`), add:
+
+```html
+<p>Once you're comfortable, the two steps collapse into one motion:</p>
+<pre><code>carlos deploy -app hello -kind static -host &lt;your-app-host&gt; ./public</code></pre>
+<p>It ships, promotes, and waits until your URL serves the new build —
+when it prints <code>live</code>, it is.</p>
+```
+
+- [ ] **Step 6: Light/dark + zero-JS check** — no new colors introduced (reused classes only); `grep -ci script index.html` unchanged from the branch base.
+- [ ] **Step 7: Commit + PR targeting flagship-redesign**
+
+```bash
+git add index.html get-started/index.html && git commit -m "homepage: the uncompromising hero + a real carlos deploy session
+
+The terminal is a verbatim capture of deploying carlosframework/hello;
+the app really serves hello world at hello.«SQID».oncarlos.com."
+git push -u origin deploy-hero
+gh pr create --base flagship-redesign --title "Real carlos deploy session + uncompromising hero" --body "Stacks under #1. Terminal content is a paste of a real session (see carlosframework/hello).
+
+🤖 Generated with [Claude Code](https://claude.com/claude-code)"
+```
+
+---
+
+### Task 7: Ship carloku.com and verify (driver)
+
+- [ ] **Step 1:** From the deploy-hero worktree, clean export + member deploy (recipe from carloku PR #3):
+
+```bash
+cd $(mktemp -d) && git -C ~/github.com/carlosframework/carloku/.claude/worktrees/deploy-hero archive HEAD | tar -x
+carlos deploy -app carloku -kind static -host carloku.com -version $(git -C ~/github.com/carlosframework/carloku/.claude/worktrees/deploy-hero rev-parse --short HEAD) .
+```
+
+If the member path 403s (ops-account membership), fall back to the pinfra `scripts/ship-app.sh` / `promote-app.sh` recipe and say so in the report.
+
+- [ ] **Step 2: Verify live** — `curl -sI https://carloku.com/ | grep -i x-carlos-version` reports the shipped sha (allow the ~60s static recheck window); `curl -s https://carloku.com/ | grep -c "uncompromising"` ≥ 1; `curl -s https://carloku.com/ | grep -o 'hello.«SQID».oncarlos.com' | head -1` present.
+- [ ] **Step 3:** Update memory (carloku-launch / new demo-app memory: sqid, account, live URLs, what shipped) and report.
+
+## Self-Review
+
+- Spec coverage: A→Task 1–2, B→Task 3, C→Task 4–5, D→Task 6–7. ✓
+- Placeholders: «SQID»/«VER» are declared runtime substitutions with a grep gate; no TBDs. ✓
+- Type consistency: `deployingLine(hosts []string, channel string) string` used identically in test and impl; `run(socket string) error` matches test. ✓