plan: carloku deploy story (deploy URL output, hello demo, site update)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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» -> 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 <your-app-host> ./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. ✓ |