docs: TodoMVC bakeoff design and plan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 26d8156344bafd7163b3933e188b518ed6ebba94
3 files changed, +554 −0
  • .gitignore +22 −0
  • docs/superpowers/plans/2026-10-23-todomvc-bakeoff.md +250 −0
  • docs/superpowers/specs/2026-10-23-todomvc-bakeoff-design.md +282 −0
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dc524b6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+# sandbox-masked dotfiles (not part of this repo)
+/.bash_profile
+/.bashrc
+/.gitconfig
+/.gitmodules
+/.idea
+/.mcp.json
+/.profile
+/.ripgreprc
+/.vscode
+/.zprofile
+/.zshrc
+/.claude/
+/.superpowers/
+.worktrees/
+node_modules/
+dist/
+tests/behaviour/test-results/
+tests/behaviour/playwright-report/
+*.db
+*.db-shm
+*.db-wal
diff --git a/docs/superpowers/plans/2026-10-23-todomvc-bakeoff.md b/docs/superpowers/plans/2026-10-23-todomvc-bakeoff.md
new file mode 100644
index 0000000..f95940b
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-23-todomvc-bakeoff.md
@@ -0,0 +1,250 @@
+# πŸ€– TodoMVC Bakeoff 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:** Nine TodoMVC apps (server-rendered, progressively enhanced) built by Opus 5.5 in nine language setups, deployed to Carloku account `bfj`, measured by one shared harness, and compared on a static report page on the same account.
+
+**Architecture:** This session (the orchestrator) first builds everything that must be identical for all contestants: the brief, the TodoMVC assets, a pinned HTTP and markup contract, a Playwright behaviour suite, and a Go measurement harness. Both are proven against a small reference fixture app that builders never see. Then headless `claude` builder sessions (one per language, each in its own sparse git worktree) build and deploy. Astra reviews through `codex exec`, the same builder session fixes, and Fable scores readability blind. Finally the harness measures the live apps and renders the report.
+
+**Tech Stack:** Go 1.26 (harness, reference fixture, report generator; stdlib only), Node 24 + `@playwright/test` (behaviour suite), `oha` (load generator), `qemu-aarch64-static` (local arm64 smoke runs), `carlos` CLI v0.28, `claude` CLI (Opus 5.5 builders, Fable 5.1 scorer), `codex` CLI (Astra reviewer), `amadan` CLI (platform requests).
+
+**Spec:** `docs/superpowers/specs/2026-10-23-todomvc-bakeoff-design.md`
+
+## Global Constraints
+
+- Contestant slugs, in report order: `go`, `go-rastrillo`, `rust`, `zig`, `ocaml`, `haskell`, `swift`, `mojo`, `spinel`.
+- Carloku account `bfj`. App names `todo-<slug>`. Hosts `todo-<slug>.bfj.oncarlos.com`. Report app `bakeoff` at `bakeoff.bfj.oncarlos.com`.
+- Process contract: `<bin> --socket <path> --db <path>`; `GET /healthz` β†’ 200; `GET /api/version` β†’ body is the build version; reads `CARLOS_HOST`, never parses the socket path; SQLite in WAL mode; additive migrations; clean exit on SIGTERM; built for `linux-arm64`, static or libc-only; `carlos vet --boot` passes before the first deploy; the app is created with `--build-probe /api/version`.
+- For local tests every binary also accepts `--listen 127.0.0.1:<port>` instead of `--socket`.
+- Builders set no resource, tier or memory fields; every app runs at the platform default.
+- Markup and CSS: TodoMVC `todomvc-app-template` markup with `todomvc-common/base.css` and `todomvc-app-css/index.css`, vendored once in `shared/`. Each app serves those two files byte-identical from `/static/base.css` and `/static/index.css`.
+- No client framework and no JS build step. Plain ES modules; vendored VanJS allowed.
+- One builder model: `claude-opus-5-5`. Reviewer: Astra through `codex exec`. Readability scorer: `claude-fable-5-1`.
+- Effort limit: 4 hours of wall-clock time per builder (`timeout 4h`), then `BLOCKED`.
+- One review-and-fix round per app; a second round only for security findings, counted separately.
+- Machine rules: servers bind `127.0.0.1` only; scratch in `$TMPDIR`; don't override `GOCACHE`; no `sudo`; stop every process and delete scratch at the end.
+- Never work around Carloku. A missing platform capability becomes an `amadan discuss new carlos/platform` Feature Request, and the report names it.
+- Commits end with `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
+
+## The pinned contract (`CONTRACT.md`, written in Task 1)
+
+The suite and harness drive every app the same way, so these are fixed for every builder:
+
+| Request | Without JS (no special headers) |
+|---|---|
+| `GET /`, `/active`, `/completed` | Full page, filtered; `a.selected` on the matching filter link (`href="/"`, `/active`, `/completed`) |
+| `GET /<filter>?edit=<id>` | Same page with that `li` given class `editing` and its `input.edit` inside a form posting to `/todos/<id>/edit` |
+| `POST /todos` (`title`) | Trimmed; empty β†’ no item. 303 β†’ current filter |
+| `POST /todos/<id>/toggle` | 303 |
+| `POST /todos/<id>/edit` (`title`) | Trimmed; empty β†’ delete. 303 |
+| `POST /todos/<id>/destroy` | 303 |
+| `POST /todos/toggle-all` | All complete, or all active if all were complete. 303 |
+| `POST /todos/clear-completed` | 303 |
+
+- Every POST form carries `<input type="hidden" name="filter" value="all|active|completed">`, and the 303 goes to `/`, `/active` or `/completed` to match. Anything else falls back to `/`.
+- Every `li` in `ul.todo-list` carries `data-id="<id>"`.
+- Controls for use without JS: the toggle is a submit control `[data-action="toggle"]` inside the item's toggle form. Editing is a link `a[data-action="edit"]` to `?edit=<id>`. `button.destroy`, `button.clear-completed` and `[data-action="toggle-all"]` are submit buttons in their forms. `input.new-todo` sits in a form that submits on Enter. With JS on, the page must match the reference visually. With JS off, extra controls may show if they're styled to fit.
+- Lists: an anonymous random cookie, `HttpOnly; SameSite=Lax; Path=/`, at least 128 bits of randomness. An unknown or garbled cookie value gets a fresh empty list, never an error.
+- Cross-site protection: SameSite plus an Origin check. A POST whose `Origin` is present and isn't `https://$CARLOS_HOST` (or the `--listen` origin locally) gets a 403. There's no CSRF token, so the harness can POST.
+- A POST naming an id that isn't in the caller's list gets a 404, never a 500 and never a change to someone else's list.
+- The JS may call the same endpoints with any request header it likes, to get fragments or JSON back. The no-header behaviour above must not change.
+- The footer reads `<p>Created by <a href="https://bakeoff.bfj.oncarlos.com">the CARLOS bakeoff</a> Β· <LANGUAGE></p>` in place of the template's "Created by you" line.
+
+## Review Focus
+
+1. **Titles with HTML in them** (`<img src=x onerror=alert(1)> & "q"`) must show as text and run nothing. Test: `escapes HTML in titles` (Task 3).
+2. **Non-ASCII titles** (`naΓ―ve πŸš€ ζ—₯本θͺž`) round-trip intact, and `Content-Length` equals the bytes sent. Spinel/tep truncated multi-byte responses before. Test: `round-trips UTF-8 and sends correct Content-Length` (Task 3).
+3. **Whitespace-only and 10,000-character titles:** the first creates nothing, and the second is stored and shown without a 500. Test: `rejects blank and accepts very long titles` (Task 3).
+4. **Separate visitors are separate:** two browser contexts see separate lists, and a garbled cookie gets an empty list. Test: `isolates lists per visitor and survives a garbled cookie` (Task 3).
+5. **Hostile POSTs:** a foreign `Origin` gets a 403, and an id from another list gets a 404 with that list unchanged. Test: `rejects cross-site and cross-list posts` (Task 3).
+
+---
+
+### Task 1: Repo, brief, contract and shared assets
+
+**Files:**
+- Create: `.gitignore` (`.worktrees/`, `node_modules/`, `dist/`, `tests/behaviour/test-results/`, `*.db*`)
+- Create: `AGENTS.md`: the bakeoff repo's rules (fairness rules from the spec's anti-goals, machine rules, layout below)
+- Create: `BRIEF.md`: the builder brief. It's the spec's "What every app must do" section, the Global Constraints that apply to apps, the per-app `Makefile` targets below, the `STATUS.md` protocol, and a pointer to `CONTRACT.md`.
+- Create: `CONTRACT.md`: the pinned contract above, verbatim
+- Create: `shared/base.css`, `shared/index.css` (from npm `todomvc-common` and `todomvc-app-css`, latest)
+- Create: `shared/reference.html`: the template markup with real-path filters and the contract's no-JS controls, showing three items in the visual fixture state: "Buy milk" completed, "Walk the dog", "Write the report", filter All, `2 items left`, Clear completed visible
+- Create: `vendor/carlos-skills/`: copy of `carlosframework/skills/skills/{getting-started,building-carlos-apps}` at the current commit, with the commit SHA in `vendor/carlos-skills/SOURCE`
+- Create: `apps/.keep`, `results/.keep`
+
+**Interfaces:**
+- Produces: the per-app `Makefile` targets every later task relies on. `make build` builds the host binary to `dist/app`. `make build-arm64` builds `dist/app-linux-arm64`. `make test` runs the app's own tests. `make clean` removes build output only, never the dependency caches. `make clean-deps` also removes every compiled third-party artifact but keeps downloaded sources, so the next `make build-arm64` is a cold build. Where a toolchain keeps compiled libraries in a shared per-user store (cabal, opam, zig, swiftpm), the Makefile points that store inside `apps/<slug>/.cache/` so `clean-deps` can delete it without touching anything else. Go is the exception: never change `GOCACHE`; instead `make build-arm64 COLD=1` passes `-a`, and the report notes that this also recompiles Go's standard library. `make bump` changes the text of one string constant in the file holding the `GET /` handler (so the next build must recompile it), and `make unbump` reverts it. `make deploy` runs `carlos deploy --account bfj --app todo-<slug> --target linux-arm64 ...` and needs no prompts.
+- Produces: `apps/<slug>/STATUS.md`, whose first line is exactly `LIVE https://todo-<slug>.bfj.oncarlos.com <version>` or `BLOCKED <one-line reason>`. A blocked app adds a section headed `## Where it got stuck`. The reason `needs-arm64-host` is reserved for "builds and passes on x86-64 but can't produce linux-arm64 on docs-t1".
+- Produces: `apps/<slug>/DEPS`, one direct third-party dependency per line (empty file if none).
+
+- [ ] **Step 1:** `git init -b main`, then write the files above.
+- [ ] **Step 2:** Check that `shared/reference.html` opened in Chromium looks like the TodoMVC template screenshot (`todomvc-app-css/screenshot.png`), apart from the item text. Run: `npx playwright screenshot --browser chromium file://$PWD/shared/reference.html $TMPDIR/ref.png` and look at it.
+- [ ] **Step 3:** Commit: `chore: bakeoff repo, brief, contract and shared TodoMVC assets`.
+
+### Task 2: Reference fixture app (never shown to builders)
+
+A minimal Go implementation of `CONTRACT.md`, used only to prove the suite and harness work before any builder starts. It keeps data in memory, has no SQLite and isn't a contestant.
+
+**Files:**
+- Create: `harness/go.mod` (module `bakeoff/harness`, go 1.26, stdlib only)
+- Create: `harness/refapp/main.go`, `harness/refapp/app.js`, `harness/refapp/page.html.tmpl`
+- Test: `harness/refapp/refapp_test.go`
+
+**Interfaces:**
+- Produces: `go run ./refapp --listen 127.0.0.1:<port>`, a server that fully meets `CONTRACT.md` with JS on and off, serving `shared/*.css` from `/static/`.
+
+- [ ] **Step 1: Write the failing tests** in `refapp_test.go` with `httptest`. `TestAddRedirectsToFilter`: POST `/todos` with `title=" a "` and `filter=active` β†’ 303 `Location: /active`, and the page then has one `li[data-id]` labelled `a`. `TestForeignOriginForbidden`: `Origin: https://evil.example` β†’ 403. `TestOtherListIDNotFound`: B toggles A's id β†’ 404, and A's item is unchanged. `TestGarbledCookieGetsEmptyList`: cookie `todo=%%%` β†’ 200 with no `li`.
+- [ ] **Step 2:** Run `cd harness && go test ./refapp/ -run . -v`. Expected: FAIL (package doesn't build yet).
+- [ ] **Step 3:** Implement the handlers and the page template from `shared/reference.html`. `app.js` (≀300 lines) adds double-click to edit, Enter/Escape/blur, and fetch-based toggle/add/destroy that swaps the `section.todoapp` HTML it gets back.
+- [ ] **Step 4:** Run `go test ./refapp/ -v`. Expected: PASS.
+- [ ] **Step 5:** Commit: `test: reference fixture app implementing the contract`.
+
+### Task 3: Behaviour suite (Playwright, JS on and JS off)
+
+**Files:**
+- Create: `tests/behaviour/package.json` (pin `@playwright/test` to the version matching the cached `chromium-1234`, otherwise latest plus `npx playwright install chromium`)
+- Create: `tests/behaviour/playwright.config.ts`: two projects, `js-on` and `js-off` (`javaScriptEnabled: false`); `baseURL` from `BASE_URL`; `workers: 4`; JSON reporter to `$RESULTS_FILE` when set
+- Create: `tests/behaviour/todomvc.spec.ts`: ported from `tastejs/todomvc/cypress/e2e/spec.cy.js`
+- Create: `tests/behaviour/robustness.spec.ts`: the five Review Focus tests
+- Create: `tests/behaviour/visual.spec.ts`, plus its golden `visual.spec.ts-snapshots/` generated from `shared/reference.html`
+- Create: `tests/behaviour/actions.ts`: `addTodo(page, title)`, `toggle(page, i)`, `edit(page, i, title)`, `destroy(page, i)`, `toggleAll(page)`, `clearCompleted(page)`, `gotoFilter(page, f)`. Each checks `testInfo.project.name` and performs the action the JS way (dblclick, checkbox click) or the no-JS way (the contract's submit controls and edit link).
+- Create: `scripts/test-app.sh <base-url> <results-file>`
+
+**Interfaces:**
+- Consumes: Task 2's refapp, used as the target during development.
+- Produces: `scripts/test-app.sh URL OUT.json`, which runs both projects and writes Playwright JSON. It exits 0 only when everything passes.
+
+Porting rules. Every Cypress `it` becomes a test in both projects, with these exceptions in `js-off`: `should save edits on blur` and `should cancel edits on escape` are skipped (they're JS-only). `should hide other controls when editing` uses `?edit=<id>`. Persistence checks reload the page instead of reading `localStorage`. Routing uses the real paths. Each test runs in a fresh browser context, which means a fresh cookie and an empty list, so there's no cleanup.
+
+- [ ] **Step 1: Write `robustness.spec.ts`** (both projects):
+ - `escapes HTML in titles`: add `<img src=x onerror=alert(1)> & "q"`; `li label` text equals it exactly; `page.on('dialog')` never fires.
+ - `round-trips UTF-8 and sends correct Content-Length`: add `naΓ―ve πŸš€ ζ—₯本θͺž`; the label equals it after a reload; `request.get('/')` has `content-length` (when present) equal to `body().length`.
+ - `rejects blank and accepts very long titles`: `' '` β†’ zero items; `'x'.repeat(10000)` β†’ one item whose label length is 10000.
+ - `isolates lists per visitor and survives a garbled cookie`: context A adds `a` and context B sees zero items; a third context holding every cookie name A received, each set to the value `%%%`, gets 200 and zero items.
+ - `rejects cross-site and cross-list posts`: `request.post('/todos', {headers:{Origin:'https://evil.example'}})` β†’ 403; B posts `/todos/<A's id>/toggle` β†’ 404, and A's item is still active.
+- [ ] **Step 2:** Write `todomvc.spec.ts` (the 28 ported tests) and `visual.spec.ts`. The visual test seeds the fixture state in the `js-on` project only, then runs `expect(page).toHaveScreenshot({ mask: [page.locator('footer.info')], maxDiffPixelRatio: 0.02 })` against the golden made from `reference.html`.
+- [ ] **Step 3:** Run `scripts/test-app.sh http://127.0.0.1:<port> $TMPDIR/ref.json` against the refapp. Expected: all pass; record the counts (passed/skipped per project) in `tests/behaviour/README.md`.
+- [ ] **Step 4:** Prove it can fail. Break one rule at a time in the refapp (send a 500 on a foreign id; drop HTML escaping; render `#/` filter links), run the suite, see the matching test fail, and revert.
+- [ ] **Step 5:** Commit: `test: TodoMVC behaviour suite, JS on and off, plus robustness and visual checks`.
+
+### Task 4: Measurement harness
+
+**Files (all in `harness/`, package `bakeoff`, plus `cmd/bakeoff/main.go` dispatch):**
+- `session.go`: `type Session`; `NewSession(base string) (*Session, error)` (cookie jar, doesn't follow redirects, sets `Origin` to base); `(*Session) Add(title string) error`; `(*Session) IDs(path string) ([]string, error)` (parses `li[data-id]`); `(*Session) Toggle(id string) error`; `(*Session) CookieHeader() string`; `Seed(s *Session, n int) ([]string, error)` adds `item 1..n` and toggles every second one
+- `ttfb.go`: `type Stats struct{ N int; P50, P95, P99, Min, Max float64 }` (milliseconds); `MeasureTTFB(s *Session, path string, warmup, n int) (Stats, error)`, measured to the first response byte, with a fresh connection for each request
+- `wake.go`: `IsAsleep(app, host string) (bool, error)` (runs `carlos instances list --account bfj --app <app> --health asleep` and reports whether `host` is listed); `MeasureWake(s *Session, path string) (float64, error)`, the TTFB of one request
+- `load.go`: `type LoadResult struct{ Scenario string; Concurrency int; RPS, P50Ms, P95Ms, P99Ms, ErrorRate float64 }`; `ParseOha(b []byte) (LoadResult, error)`; `RunLoad(s *Session, scenario string, concurrency int, d time.Duration) (LoadResult, error)`. Scenario `read` = `GET /`. Scenario `toggle` = `POST /todos/<first id>/toggle` with `filter=all`, sent with `--redirect 0`, where a 303 counts as success. Both send the session cookie and `Origin`. For `ErrorRate`, anything other than 200 (read) or 303 (toggle), and any transport error, counts as an error.
+- `pageweight.go`: `type Weight struct{ HTMLBytes, JSBytes int }`; `PageWeight(s *Session, path string) (Weight, error)` sums the HTML plus every same-origin `<script src>`
+- `codestats.go`: `type CodeStats struct{ Server, Templates, JS, Tests, Deps int }`; `CountCode(dir string) (CodeStats, error)` counts non-blank lines. JS is `*.js`/`*.mjs` except `van*.js`. Templates are `*.html`, `*.tmpl`, `*.erb`, `*.mustache`, plus anything under `templates/` or `views/`. Tests are files under `test/` or `tests/`, or named `*_test.*`, `*.test.*`, `*_spec.*`. Server is every other source file. `dist/`, `vendor/`, lockfiles, `*.css` and `*.md` are skipped. Deps is the line count of `DEPS`.
+- `buildstats.go`: `type Build struct{ ColdSeconds, WarmSeconds, EditSeconds float64; Bytes int64; Host string }`; `BuildStats(dir string, runs int) (Build, error)`. It runs each mode `runs` times and takes the median. Cold is `make clean clean-deps` then a timed `make build-arm64` (with `COLD=1` for the Go slugs). Warm is `make clean` then a timed `make build-arm64`. Edit is a build, then `make bump`, a timed `make build-arm64`, and `make unbump`. It also records the size of `dist/app-linux-arm64`.
+- `tokens.go`: `type Tokens struct{ Input, Output, CacheRead, CacheWrite int64; Turns int; Seconds float64; CostUSD float64 }`; `ParseClaudeLog(r io.Reader) (Tokens, error)` (the final `"type":"result"` event of `--output-format stream-json`: `usage.*`, `num_turns`, `duration_ms`, `total_cost_usd`); `ParseCodexLog(r io.Reader) (Tokens, error)` (sums the usage on `turn.completed` events from `codex exec --json`)
+- `results.go`: `type AppResult` holding the slug, language, status, URL, version, stack summary, behaviour pass/fail/skip per project, `TTFBEmpty`/`TTFB50 Stats`, `Wake []float64`, `Load []LoadResult`, `Weight`, `Code CodeStats`, `Build`, `TokensBuild`, `TokensFix`, `TokensSecurity`, `TokensReview Tokens`, `Readability` (scores and notes), `ReviewFindings []string`, `Notes []string`; `Update(slug string, f func(*AppResult)) error` reads and writes `results/<slug>.json` with indented JSON
+- `cmd/bakeoff`: subcommands `ttfb|wake|load|weight|code|build|tokens|report`, each taking `--slug` and writing into `results/<slug>.json` through `Update`. `tokens` also takes `--phase build|fix|security|review`, reads `builders/logs/<slug>.<phase>*.jsonl` (with `ParseCodexLog` for `review`, `ParseClaudeLog` otherwise) and sums them into `TokensBuild`, `TokensFix`, `TokensSecurity` or `TokensReview`.
+- Tests: `*_test.go` next to each file. Fixtures go in `testdata/`: a real `oha` JSON output, a real `claude` stream-json log and a real `codex --json` log, captured in Step 1.
+
+**Interfaces:**
+- Consumes: CONTRACT routes; Task 2's refapp for testing.
+- Produces: `bakeoff <subcommand> --slug <s> [--url U]` β†’ `results/<s>.json`; `type AppResult` (Task 10 renders it).
+
+- [ ] **Step 1:** Install `oha` from its GitHub release binary into `~/.local/bin`. Capture fixtures: run `oha --output-format json -z 2s` against the refapp; run `claude -p --model claude-opus-5-5 --output-format stream-json --verbose "say ok"`; run `codex exec --json "say ok"`. Save them under `testdata/`.
+- [ ] **Step 2: Write the failing tests.** `TestSeed` (refapp: 50 ids, 25 completed from `IDs("/completed")`); `TestParseOha` (fixture β†’ RPS > 0, P99 β‰₯ P50); `TestParseClaudeLog` / `TestParseCodexLog` (fixtures β†’ Output > 0); `TestCountCode` (a `testdata/codetree` with one file per category and a `vendor/` file that must be ignored β†’ exact counts); `TestPageWeight` (refapp β†’ JSBytes equals the size of `app.js`); `TestBuildStats` (a `testdata/buildtree` Makefile whose targets `sleep` known amounts β†’ Cold > Warm > Edit > 0, and `unbump` was called); `TestMeasureTTFB` (refapp, n=5 β†’ `N==5`, `Min ≀ P50 ≀ Max`).
+- [ ] **Step 3:** Run `go test ./... -short`. Expected: FAIL.
+- [ ] **Step 4:** Implement the files listed above.
+- [ ] **Step 5:** Run `go test ./... -short`. Expected: PASS. Then run `bakeoff load --slug refapp --url http://127.0.0.1:<port>` and check that `results/refapp.json` has 6 load rows (2 scenarios Γ— concurrency 1, 16, 64, 30 s each with a 60 s rest), then delete `results/refapp.json`.
+- [ ] **Step 6:** Commit: `feat: measurement harness`.
+
+### Task 5: Toolchains and arm64 smoke builds
+
+**Files:**
+- Create: `scripts/toolchains.sh`: idempotent; installs under `~` only; prints each tool's version
+- Create: `scripts/smoke/<slug>/`: hello-world HTTP-over-unix-socket in each language, plus `scripts/smoke.sh <slug>`
+- Create: `docs/toolchains.md`: versions installed and the arm64 route for each language
+
+**Interfaces:**
+- Produces: on `PATH` (through `~/.local/bin` or `source scripts/env.sh`): Go 1.26, `rustup` stable with `aarch64-unknown-linux-musl`, `zig` (latest stable), `opam` + OCaml 5.x, `ghcup` + GHC + cabal, Swift 6.2 + the Static Linux SDK, Mojo through `pixi`, Spinel + tep + spinelkit at the pins in `paulca/lpay/spinel`, `qemu-aarch64-static` (extracted with `apt-get download qemu-user-static && dpkg-deb -x`), and `oha`. Builders don't install compilers; they only add libraries.
+
+- [ ] **Step 1:** Write `toolchains.sh` and run it. GHC and Mojo downloads are approved (spec). Check `df -h ~` first.
+- [ ] **Step 2:** For each slug, `scripts/smoke.sh <slug>` builds a linux-arm64 hello binary that serves `GET /healthz` on `--socket`, checks `file` says `ARM aarch64` and `statically linked` (or libc-only), then runs it under `qemu-aarch64-static` and curls `/healthz` over the socket. Expected: `ok` for each slug.
+- [ ] **Step 3:** Any slug that fails gets the reason and the tito-studio fallback written in `docs/toolchains.md`. That slug's builder is told up front (Task 6 prompt `{ARM64_NOTE}`).
+- [ ] **Step 4:** Commit: `chore: toolchains and arm64 smoke builds`.
+
+### Task 6: Builder pipeline and the Go pilot
+
+**Files:**
+- Create: `builders/prompts/build.md`: template with `{SLUG}`, `{LANGUAGE}`, `{EXTRA}`, `{ARM64_NOTE}`
+- Create: `builders/extra/<slug>.md`, one per slug: `go` "Don't use Rastrillo"; `go-rastrillo` "Use Rastrillo v0.28 (`amadan.net/rastrillo/rastrillo`); read `~/amadan.net/rastrillo/rastrillo/SKILL.md`; use the TodoMVC CSS, not Rastrillo's UI"; `spinel` "Start from `paulca/lpay` `spinel/` (README, BUILD-arm64.md, patches/tep-carlos.patch)"; `mojo` "A single file is required; if Mojo can't produce one, stop with `BLOCKED single-binary`"; the others are empty
+- Create: `builders/worktree.sh <slug>`: `git worktree add .worktrees/<slug> -b app/<slug>` with sparse checkout `/*`, `!/harness/refapp/`, `!/apps/`, `/apps/<slug>/`
+- Create: `builders/run-build.sh <slug>`: `cd .worktrees/<slug> && timeout 4h claude -p --model claude-opus-5-5 --permission-mode bypassPermissions --output-format stream-json --verbose "$(render prompt)" > ../../builders/logs/<slug>.build.jsonl`. It records `session_id` from the first event into `builders/logs/<slug>.session`.
+
+The prompt tells the builder to: read `BRIEF.md`, `CONTRACT.md`, `vendor/carlos-skills/*/SKILL.md` and `references/platform.md`; work only in `apps/{SLUG}/`; not read other apps or the harness source; choose its stack and say why in `README.md`; write `AGENTS.md` first; get `make test` and `scripts/test-app.sh` passing locally against `--listen`; `carlos apps create --account bfj --app todo-{SLUG} --build-probe /api/version`, `carlos vet --boot`, `make deploy`; run `scripts/test-app.sh https://todo-{SLUG}.bfj.oncarlos.com`; commit on its branch; and finish by writing `STATUS.md`. If it hits a Carloku gap, it stops and writes `BLOCKED platform: <what's missing>` instead of working around it.
+
+**Interfaces:**
+- Consumes: Tasks 1, 3 and 5.
+- Produces: `builders/logs/<slug>.{build.jsonl,session}`; branch `app/<slug>` with `apps/<slug>/` and `STATUS.md`.
+
+- [ ] **Step 1:** Write the files. Run `builders/run-build.sh go` in the background.
+- [ ] **Step 2:** When it exits, check: `head -1 .worktrees/go/apps/go/STATUS.md` starts with `LIVE`; `curl -sI https://todo-go.bfj.oncarlos.com | grep -i x-carlos-version` matches the version; `scripts/test-app.sh https://todo-go.bfj.oncarlos.com $TMPDIR/go.json` passes; `bakeoff tokens --slug go --phase build` writes nonzero tokens.
+- [ ] **Step 3:** Fix any pipeline problem the pilot exposed, in the scripts or the brief only. Never fix the app by hand. If the brief changed, delete `todo-go` (`carlos apps delete`) and the branch, and rerun the pilot so all nine builders get the same brief.
+- [ ] **Step 4:** Commit: `feat: builder pipeline; Go pilot`.
+- [ ] **Step 5: Checkpoint with Paul.** Show him the pilot's live URL, its tokens and wall-clock time, and the estimated total for eight more. Wait for his go-ahead.
+
+### Task 7: The other eight builders
+
+- [ ] **Step 1:** For each of `go-rastrillo rust zig ocaml haskell swift mojo spinel`: `builders/worktree.sh <slug>`, then `builders/run-build.sh <slug>` in the background. All eight run in parallel.
+- [ ] **Step 2:** As each exits, read `STATUS.md`. For `BLOCKED needs-arm64-host`, ask Paul for tito-studio access, then continue in the same session (`claude -p --resume <session> "An arm64 host is available: <how>. Continue."`, appending to a separate `<slug>.build2.jsonl` that counts toward build tokens). Count that time against the same 4-hour limit. Any other `BLOCKED` stays blocked.
+- [ ] **Step 3:** For every `LIVE` app, run the same checks as Task 6 Step 2.
+- [ ] **Step 4:** Merge each `app/<slug>` branch into `main` (`git merge --no-ff`) and commit: `feat: <slug> builder output`.
+
+### Task 8: Astra review, one fix round, security round
+
+**Files:**
+- Create: `builders/prompts/review.md`: review `apps/{SLUG}/` against `BRIEF.md`, `CONTRACT.md` and the TodoMVC spec. Correctness, spec compliance and security only, with no performance tuning. Output is a numbered list, each item tagged `[security]`, `[correctness]` or `[spec]`, written to `builders/reviews/<slug>.md`.
+- Create: `builders/run-review.sh <slug> <n>`: `codex exec -s read-only --json -o builders/reviews/<slug>.<n>.md "$(render)" > builders/logs/<slug>.review<n>.jsonl` (`n` is 1 for the first review, 2 for the re-review)
+- Create: `builders/run-fix.sh <slug> <phase>`: `claude -p --resume $(cat <slug>.session) --output-format stream-json --verbose "Fix these review findings, redeploy, re-run the suite, update STATUS.md: $(cat review)" > builders/logs/<slug>.<phase>.jsonl`
+
+- [ ] **Step 1:** For each LIVE app: `run-review.sh <slug> 1`, then `run-fix.sh <slug> fix` with `builders/reviews/<slug>.1.md`.
+- [ ] **Step 2:** Run `run-review.sh <slug> 2`. If it has any `[security]` items, run the fix with phase `security` using only those items. There's no third round.
+- [ ] **Step 3:** Store the `[security]`/`[correctness]`/`[spec]` items that remain in `results/<slug>.json` `ReviewFindings`, and the tokens from every phase.
+- [ ] **Step 4:** Commit: `chore: review and fix round`.
+
+### Task 9: Blind readability scoring
+
+**Files:**
+- Create: `builders/prompts/readability.md`: rubric. Score 1–5 each on naming, structure, following one request end to end, error handling, and comments. Give an overall score of 1–10 and a justification of at most 120 words. Output only JSON `{"naming":n,"structure":n,"traceability":n,"errors":n,"comments":n,"overall":n,"notes":"..."}`.
+- Create: `builders/run-readability.sh <slug>`: concatenates `apps/<slug>/` source as `CountCode` defines it (server, templates, JS; no tests, vendor or docs) with relative paths, removes the strings `todo-<slug>` and `<slug>` from paths, names only the language, then runs `claude -p --model claude-fable-5-1 --output-format json` three times and stores the per-field median
+
+- [ ] **Step 1:** Run it for each LIVE app and write the scores to `results/<slug>.json`.
+- [ ] **Step 2:** Commit: `chore: readability scores`.
+
+### Task 10: Measurement on live apps
+
+Run one app at a time, in slug order, from docs-t1.
+
+- [ ] **Step 1:** Behaviour: `scripts/test-app.sh <live url> results/<slug>.behaviour.json` β†’ pass/fail/skip into `AppResult`.
+- [ ] **Step 2:** `bakeoff ttfb` (empty list and 50 items; warmup 3, n 30), `bakeoff weight`, `bakeoff code`, `bakeoff build --runs 3` (on the host recorded in `docs/toolchains.md`), `bakeoff load` (6 runs).
+- [ ] **Step 3:** Wake: once all load runs are done, leave every app idle. Every 5 minutes, check `IsAsleep` for each app. As soon as an app is asleep, `bakeoff wake` records one sample. Repeat until each app has 3 samples (each sample needs another idle period of at least 15 minutes).
+- [ ] **Step 4:** Memory: check `carlos instances list --account bfj --app todo-go` and the console's instance view for memory figures. If there are none, search `amadan discuss list carlos/platform` for an existing request. Then file or reply: `amadan discuss new carlos/platform -title "Member-visible memory use per instance" -body -` (type `Feature Request`), explaining the bakeoff use. Record the discussion id in every `AppResult.Notes`. If wake measurement needed anything the platform lacks (for example, a member-side "sleep now"), file that the same way.
+- [ ] **Step 5:** Commit: `chore: live measurements`.
+
+### Task 11: Report page
+
+**Files:**
+- Create: `harness/report/report.go`: `Render(results []AppResult, w io.Writer) error` (html/template plus inline SVG charts, no JS, no external assets)
+- Create: `harness/report/report.html.tmpl`, `harness/report/copy.md` (all prose on the page, as one file for copy review)
+- Test: `harness/report/report_test.go`: `TestRenderAllApps` (a fixture with one LIVE and one BLOCKED result β†’ output contains both slugs, the BLOCKED reason, and no `NaN` or `<no value>`)
+- Output: `report/site/index.html`
+
+Page sections: summary table (one row per slug, a column per measure, blocked apps shown with their reason), one chart per performance measure, a section per app (stack, notable choices, Astra's remaining findings, Fable's notes, live link, source link), "How we measured", and "What this does and doesn't show" (network included; Spinel's concurrency model; build hosts; blocked apps; memory status).
+
+- [ ] **Step 1:** Load the `dataviz` skill before writing any chart code. Write the failing test, run `go test ./report/`, and see it fail.
+- [ ] **Step 2:** Implement `Render`, then run the test and see it pass.
+- [ ] **Step 3:** Draft `copy.md`, then run the `copy-review` skill on it and apply Paul's edits verbatim.
+- [ ] **Step 4:** Source links: ask Paul where the repo should be published (amadan or GitHub, public). Publish, then put the links in the results.
+- [ ] **Step 5:** `bakeoff report > report/site/index.html`, then `carlos deploy --account bfj --app bakeoff --kind static --host bakeoff.bfj.oncarlos.com report/site`. Verify: `curl -sI https://bakeoff.bfj.oncarlos.com | grep -i x-carlos-version`, and open it in Chromium.
+- [ ] **Step 6:** Commit: `feat: bakeoff report page`.
+
+### Task 12: Clean up and hand off
+
+- [ ] **Step 1:** Stop every background process this work started (builders, refapp, local app servers, oha). Remove `.worktrees/` (`git worktree remove`) and `$TMPDIR/recon` and other scratch. Keep `builders/logs/`, because they're the token evidence.
+- [ ] **Step 2:** Confirm that `ss -ltnp` shows no listener from this work, and that `carlos apps list --account bfj` shows the nine `todo-*` apps (or fewer, matching the BLOCKED list) plus `bakeoff`.
+- [ ] **Step 3:** Run the `handoff` skill.
diff --git a/docs/superpowers/specs/2026-10-23-todomvc-bakeoff-design.md b/docs/superpowers/specs/2026-10-23-todomvc-bakeoff-design.md
new file mode 100644
index 0000000..e9f0ab9
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-23-todomvc-bakeoff-design.md
@@ -0,0 +1,282 @@
+# πŸ€– TodoMVC bakeoff on Carloku β€” design
+
+Status: draft for Paul's approval, 2026-10-23. Reviewed by Astra (builder check).
+
+## Why
+
+CARLOS apps don't have to be written in Go, and the CARLOS Platform runs
+any binary that keeps its process contract. This bakeoff shows that with
+working apps. It's driven by curiosity and acts as a showcase. It isn't
+meant to pick a winner or feed a decision.
+
+We build the same app, TodoMVC, in nine language setups. Each one renders
+HTML on the server and layers JavaScript on top, and all of them are
+deployed to the Carloku **bakeoff** account (`bfj`). A report page on the
+same account compares them.
+
+## Goals
+
+- Nine working TodoMVC apps, live on Carloku, one per setup:
+ 1. Go, without Rastrillo
+ 2. Go, with Rastrillo
+ 3. Rust
+ 4. Zig
+ 5. OCaml
+ 6. Haskell
+ 7. Swift
+ 8. Mojo
+ 9. Spinel (a Ruby compiler that produces native binaries)
+- Every app passes the same behaviour tests with JavaScript on and with
+ it off.
+- A report page that puts honest numbers side by side: what each build
+ cost in tokens, how readable the code is, how fast it responds, how it
+ holds up under load, and the other measures listed below.
+
+## Non-goals
+
+- Choosing a "best" language. The report presents numbers and doesn't
+ crown anyone.
+- Tuning one app beyond the others after the build. Every app gets the
+ same build process and the same review-and-fix round, and nothing more.
+- Rich features beyond TodoMVC: no sign-in, no sharing, no sync between
+ devices.
+
+## Anti-goals (failure even if everything technically works)
+
+- **A report that looks rigged.** One app gets a different UI, a different
+ database, a bigger instance, more tuning, extra review rounds, or quietly
+ drops part of the spec.
+- **An app missing from the report without comment.** If a setup can't be
+ built or can't run on Carloku, the report says so plainly and says why.
+- **Working around Carloku.** If the platform lacks something an app needs
+ (for example, a binary that needs files beside it), we file a request
+ against the platform and report the app as blocked on it. We don't
+ build workarounds. (Paul's standing rule.)
+
+## Decided with Paul (2026-10-23)
+
+- **One shared UI.** Every app uses TodoMVC's standard markup and CSS
+ (`tastejs/todomvc-app-template`, `todomvc-app-css`). For the bakeoff
+ this overrides the CARLOS skill's default that Rastrillo apps use
+ Rastrillo's own UI components. The Go + Rastrillo app uses the TodoMVC
+ CSS too.
+- **Server-rendered with progressive enhancement.** No client-side app
+ framework. The page is HTML from the server and still works with
+ JavaScript turned off.
+- **Each agent chooses its own stack.** Web server, router, templating,
+ SQLite binding and so on are each agent's own best recommendation for
+ its language.
+- **The builder is Opus 5.5 for every app.** Astra reviews every app
+ through the codex CLI, and the same Opus agent fixes what the review
+ raises.
+- **Spinel is already supported on Carloku.** The Spinel builder starts
+ from lpay's working setup (`paulca/lpay`, directory `spinel/`): tep
+ (a Sinatra-style layer that drives Spinel), the `tep-carlos.patch` for
+ the socket contract, and the zig-based cross-build to a static
+ aarch64-musl binary in `BUILD-arm64.md`. Mojo is expected to work if it
+ compiles to a single binary.
+- **Arm64 builds:** try docs-t1 first (cross-compiling or an emulated
+ arm64 container), and fall back to tito-studio over Tailscale.
+- **After the one fix round:** remaining correctness gaps ship as they
+ are, and the report shows them. Security findings get a second fix
+ round, counted separately, because the apps are public.
+- **Downloads approved:** GHC (via ghcup) and Mojo (via pixi) may be
+ installed under `~` on docs-t1.
+- **Everything lives on the bakeoff account** (`bfj`): nine apps and the
+ report page.
+
+## What every app must do (the shared brief)
+
+Each builder gets the same written brief. It contains:
+
+### Behaviour
+
+- The TodoMVC app spec (`tastejs/todomvc/app-spec.md`), with these changes
+ because the server holds the data:
+ - **Persistence:** in SQLite on the server, not in `localStorage`.
+ - **Routes:** real paths `/`, `/active` and `/completed`, not `#/`
+ fragments. The selected filter survives a reload because it's in the
+ URL.
+ - Editing state isn't persisted (same as the spec).
+- **With JavaScript off,** everything works through plain forms and links:
+ add, toggle, toggle all, edit, delete, clear completed, and the three
+ filters. Each action is a POST that redirects back to the current
+ filter (post/redirect/get). Editing without JS uses a link to an edit
+ view of that item, such as `/?edit=<id>`, rendered with the edit field
+ open.
+- **With JavaScript on,** the page behaves like the reference TodoMVC:
+ double-click to edit, Enter saves, Escape cancels, blur saves, an empty
+ title deletes, toggles happen without a full page reload, and the
+ counter and the "Clear completed" button update in place. The JS sends
+ the same POSTs and swaps in server-rendered HTML or applies a small
+ JSON reply (the builder chooses). There's no build step and no
+ framework. Plain ES modules are allowed, and so is vendored VanJS (the
+ one reactive library CARLOS sanctions).
+- **Who owns a list:** the whole app is one instance. Each visitor gets
+ their own list through an anonymous, random, `HttpOnly`, `SameSite=Lax`
+ cookie set on the first visit. There's no sign-in. Forms are protected
+ against cross-site posts (by SameSite plus an Origin check, or a CSRF
+ token; the builder chooses).
+
+### Platform contract
+
+- One binary for `linux-arm64` (the platform default; `carlos deploy
+ --target`), started as `<bin> --socket <path> --db <path>`. It listens
+ on the unix socket, has no `$PORT`, and serves `GET /healthz` and
+ `GET /api/version`.
+- **Linked statically, or with only libc.** Instances run sandboxed and
+ see only their own build, so the builder must not assume any shared
+ library exists on the box. `carlos vet --boot` (a local check that the
+ binary starts and answers `/healthz`) must pass before the first deploy.
+- Reads `CARLOS_HOST` when it needs its own hostname. Never parses the
+ socket path.
+- Uses SQLite in WAL mode. Migrations only ever add.
+- The app starts quickly after hibernation (sleeping when idle) and shuts
+ down cleanly on SIGTERM.
+- Configures the build probe (`carlos apps create --build-probe
+ /api/version`) so `carlos deploy` confirms that the new build is what's
+ actually serving.
+
+### Repo shape for each app
+
+- `apps/<slug>/` in this bakeoff repo. The slugs are `go`, `go-rastrillo`,
+ `rust`, `zig`, `ocaml`, `haskell`, `swift`, `mojo` and `spinel`.
+- `AGENTS.md` first. It holds the one rule and the tenancy declaration:
+ one instance, anonymous lists by cookie, trust class C (plaintext todo
+ text, chosen and written down).
+- A `README.md` with how to build, run locally, test and deploy. One
+ command for each (a `Makefile` or a script).
+- The app's own tests, whatever is normal for its language.
+- The Carloku app name is `todo-<slug>`, e.g. `todo-rust`.
+
+### What the builder may not do
+
+- Change the markup or the CSS beyond adding hooks the JS needs, such as
+ `data-` attributes or wrapping each toggle in a `<form>`. The rendered
+ page must look identical to the reference.
+- Add caching layers, a CDN or edge tricks that the other apps don't
+ have. Each app may set ordinary HTTP cache headers on its static
+ assets.
+- Spend effort on performance tuning beyond what an experienced developer
+ would write the first time. "Write it the way you'd normally write it
+ well" is the instruction.
+
+## How the apps get built
+
+1. **Shared tooling first, built by the orchestrator (this session)**
+ before any builder starts, so every app is judged by the same tools:
+ - The brief, as one file every builder reads.
+ - A **behaviour test suite** adapted from TodoMVC's Cypress tests
+ (`tastejs/todomvc/cypress`). It runs against any URL, once with JS
+ on and once with JS off. The JS-off run uses forms only and skips
+ the parts that are JS-only, such as double-click editing (replaced
+ by the edit link). Builders run it locally against their own app.
+ - A **load test script** and a **metrics collector** (see "Metrics").
+2. **Toolchains installed** per user, under `~` (`rustup`, Zig tarball,
+ `opam`, `ghcup`, Swift's static Linux SDK, Mojo through `pixi`,
+ Spinel built from source). GHC and Mojo each take several GB, so we
+ ask Paul before downloading them (machine rule).
+3. **Nine Opus 5.5 builders run in parallel**, each in its own headless
+ `claude` session with JSON output so token use is recorded. Each
+ starts from a fresh context with the same brief and the CARLOS skills
+ (`carlosframework/skills`), plus Rastrillo's own `SKILL.md` for
+ Go + Rastrillo. Each builds, tests locally, then deploys to `bfj` with
+ `carlos deploy` and checks it on the live URL.
+4. **Astra reviews each app** through `codex exec` against the brief, the
+ spec and the code. The review is limited to correctness, spec
+ compliance and security. It doesn't ask for performance tuning.
+5. **One fix round.** The same Opus builder session fixes what Astra
+ raised, then redeploys. Only one round, for every app. The one
+ exception is security: if Astra still finds a security problem after
+ that round, there's a second round for security fixes only, with its
+ tokens reported separately.
+6. **Blind readability scoring by Fable.** Each app's code is shown with
+ the language named but nothing else identifying, and scored against a
+ fixed rubric. Astra doesn't score readability because it already
+ reviewed the code.
+7. **Measurement and report** (below).
+
+All local servers bind to `127.0.0.1`. Scratch space goes in `$TMPDIR`
+and is removed at the end.
+
+## Metrics
+
+All of them are collected by scripts in this repo and can be re-run.
+
+| Measure | How |
+|---|---|
+| Tokens to build | From each builder's session log: input, output, cache read and cache write, plus turns and wall-clock time. The first build and the fix round are reported separately, and so is the Astra review. |
+| Correctness | Behaviour tests passed, JS on and JS off, against the live URL. Spec gaps found by review. |
+| Readability | Fable's rubric score (naming, structure, how easy it is to follow one request end to end, error handling, comments), with a short written justification. |
+| Size of the code | Lines of code split three ways: server code, templates, and JS. Also the number of third-party dependencies. |
+| Build | Three build times for the arm64 binary that ships: **cold** (the app and every third-party library compiled from already-downloaded sources; compiler and standard library as shipped), **warm** (libraries already compiled, app rebuilt from clean), and **one-line change** (rebuild after a trivial edit in the request-handling code). Plus binary size. Paul asked for cold and warm, 2026-10-23. Measured on docs-t1 where the app cross-compiles there; otherwise on the machine that built it, with the machine named beside the number and kept out of the ranking. |
+| Response time | Time to the first byte for `GET /` with an empty list and with 50 items, from docs-t1, median of repeated runs. |
+| Wake time | Time to the first byte on the first request after the instance has gone to sleep. |
+| Under load | Fixed scenarios at fixed concurrency levels (read-heavy: `GET /` on a 50-item list; write-heavy: add and toggle). Reports requests per second, p50/p95/p99 latency and error rate. |
+| Memory | Peak and idle memory from the platform, if it exposes them. If it doesn't, we file a request against the platform rather than measure inside the app. |
+| Page weight | Bytes of HTML and JS sent for `GET /`. |
+
+Load tests run from docs-t1 over the public internet, so the numbers
+include network time. That's the same for every app, and the report says
+so. Every app runs on the same box class and instance size, and we run
+the apps one after another, never at the same time.
+
+## The report page
+
+- A static site on `bfj` (`carlos deploy --kind static`), named
+ `bakeoff`.
+- One page with a summary table of every measure for every app, and a
+ chart per performance measure. Each app gets a short section covering
+ its stack, notable choices, Astra's main findings, Fable's readability
+ notes, a link to the live app and a link to its source.
+- A "How we measured" section and a "What this does and doesn't show"
+ section, including the network caveat, Spinel's single-threaded server
+ and any app that's blocked.
+- Built from the metrics files by a script, so a re-run regenerates it.
+- Report copy goes through the copy-review gate before it ships.
+
+## Known risks
+
+- **Everything targets `linux-arm64`, and docs-t1 is x86-64.** Rust, Zig,
+ Go and Swift (static Linux SDK) cross-compile well, and so does Spinel
+ (lpay's zig recipe). OCaml, Haskell and Mojo are harder. Each tries
+ docs-t1 first, by cross-compiling or in an emulated arm64 container,
+ and falls back to tito-studio. If neither works within the effort
+ limit, that app is reported as blocked.
+- **Mojo** may need runtime files beside the binary. The binary kind takes
+ a single file. If Mojo can't produce one, we file a platform request and
+ report Mojo as blocked.
+- **Spinel** has no threads. lpay's notes flag its aarch64 fiber context
+ switch as untested at runtime. Whatever concurrency model the Spinel
+ server ends up with, the report states it next to the load numbers.
+- **Load testing shared boxes** can affect other people's apps on the same
+ box. We keep load runs short, one app at a time.
+
+## Effort limit
+
+Every builder gets the same limit. If an app still doesn't have a local
+build passing the behaviour tests after **4 hours of wall-clock time**,
+the builder stops and writes down where it got stuck, and the app is
+reported as blocked. All tokens a builder spends are reported, including
+for blocked apps and time spent getting toolchains working.
+
+## Left to the builders
+
+- Each app's libraries, framework, server, templating and SQLite binding.
+- How the JS talks to the server (HTML fragments or small JSON).
+- Internal code layout, within the repo shape above.
+
+## Calls made by the orchestrator, not by Paul
+
+- Real paths instead of `#/` routes, and per-visitor lists by anonymous
+ cookie.
+- Fable for blind readability scoring, separate from Astra's review.
+- One review-and-fix round per app.
+- All apps in one repo under `apps/<slug>/`.
+- App names `todo-<slug>`, and the report app named `bakeoff`.
+- The shared test suite and load test are written once by the
+ orchestrator, not by each builder.
+- The effort limit: 4 hours of wall-clock time per builder.
+- Build time for apps that can't cross-compile on docs-t1 is measured
+ where they're built, with the machine named, and isn't ranked against
+ the others.