carlos / todomvc-bakeoff Public
π€ 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,nim,spinel(nimreplacedmojoon 2026-10-24). - Carloku account
bfj. App namestodo-<slug>. Hoststodo-<slug>.bfj.oncarlos.com. Report appbakeoffatbakeoff.bfj.oncarlos.com. - Process contract:
<bin> --socket <path> --db <path>;GET /healthzβ 200;GET /api/versionβ body is the build version; readsCARLOS_HOST, never parses the socket path; SQLite in WAL mode; additive migrations; clean exit on SIGTERM; built forlinux-arm64, static or libc-only;carlos vet --bootpasses 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-templatemarkup withtodomvc-common/base.cssandtodomvc-app-css/index.css, vendored once inshared/. Each app serves those two files byte-identical from/static/base.cssand/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 throughcodex exec. Readability scorer:claude-fable-5-1. - Effort limit: 4 hours of wall-clock time per builder (
timeout 4h), thenBLOCKED. - One review-and-fix round per app; a second round only for security findings, counted separately.
- Machine rules: servers bind
127.0.0.1only; scratch in$TMPDIR; don't overrideGOCACHE; nosudo; stop every process and delete scratch at the end. - Never work around Carloku. A missing platform capability becomes an
amadan discuss new carlos/platformFeature 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/,/activeor/completedto match. Anything else falls back to/. - Every
liinul.todo-listcarriesdata-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 linka[data-action="edit"]to?edit=<id>.button.destroy,button.clear-completedand[data-action="toggle-all"]are submit buttons in their forms.input.new-todosits 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
Originis present and isn'thttps://$CARLOS_HOST(or the--listenorigin 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
- 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). - Non-ASCII titles (
naΓ―ve π ζ₯ζ¬θͺ) round-trip intact, andContent-Lengthequals the bytes sent. Spinel/tep truncated multi-byte responses before. Test:round-trips UTF-8 and sends correct Content-Length(Task 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). - 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). - Hostile POSTs: a foreign
Origingets 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-appMakefiletargets below, theSTATUS.mdprotocol, and a pointer toCONTRACT.md. - Create:
CONTRACT.md: the pinned contract above, verbatim - Create:
shared/base.css,shared/index.css(from npmtodomvc-commonandtodomvc-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 ofcarlosframework/skills/skills/{getting-started,building-carlos-apps}at the current commit, with the commit SHA invendor/carlos-skills/SOURCE - Create:
apps/.keep,results/.keep
Interfaces:
- Produces: the per-app
Makefiletargets every later task relies on.make buildbuilds the host binary todist/app.make build-arm64buildsdist/app-linux-arm64.make testruns the app's own tests.make cleanremoves build output only, never the dependency caches.make clean-depsalso removes every compiled third-party artifact but keeps downloaded sources, so the nextmake build-arm64is a cold build. Where a toolchain keeps compiled libraries in a shared per-user store (cabal, opam, zig, swiftpm), the Makefile points that store insideapps/<slug>/.cache/soclean-depscan delete it without touching anything else. Go is the exception: never changeGOCACHE; insteadmake build-arm64 COLD=1passes-a, and the report notes that this also recompiles Go's standard library.make bumpchanges the text of one string constant in the file holding theGET /handler (so the next build must recompile it), andmake unbumpreverts it.make deployrunscarlos deploy --account bfj --app todo-<slug> --target linux-arm64 ...and needs no prompts. - Produces:
apps/<slug>/STATUS.md, whose first line is exactlyLIVE https://todo-<slug>.bfj.oncarlos.com <version>orBLOCKED <one-line reason>. A blocked app adds a section headed## Where it got stuck. The reasonneeds-arm64-hostis 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.htmlopened 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.pngand 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(modulebakeoff/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 meetsCONTRACT.mdwith JS on and off, servingshared/*.cssfrom/static/.
- [ ] Step 1: Write the failing tests in
refapp_test.gowithhttptest.TestAddRedirectsToFilter: POST/todoswithtitle=" a "andfilter=activeβ 303Location: /active, and the page then has oneli[data-id]labelleda.TestForeignOriginForbidden:Origin: https://evil.exampleβ 403.TestOtherListIDNotFound: B toggles A's id β 404, and A's item is unchanged.TestGarbledCookieGetsEmptyList: cookietodo=%%%β 200 with noli. - [ ] 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 thesection.todoappHTML 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/testto the version matching the cachedchromium-1234, otherwise latest plusnpx playwright install chromium) - Create:
tests/behaviour/playwright.config.ts: two projects,js-onandjs-off(javaScriptEnabled: false);baseURLfromBASE_URL;workers: 4; JSON reporter to$RESULTS_FILEwhen set - Create:
tests/behaviour/todomvc.spec.ts: ported fromtastejs/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 goldenvisual.spec.ts-snapshots/generated fromshared/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 checkstestInfo.project.nameand 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 labeltext equals it exactly;page.on('dialog')never fires. -round-trips UTF-8 and sends correct Content-Length: addnaΓ―ve π ζ₯ζ¬θͺ; the label equals it after a reload;request.get('/')hascontent-length(when present) equal tobody().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 addsaand 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) andvisual.spec.ts. The visual test seeds the fixture state in thejs-onproject only, then runsexpect(page).toHaveScreenshot({ mask: [page.locator('footer.info')], maxDiffPixelRatio: 0.02 })against the golden made fromreference.html. - [ ] Step 3: Run
scripts/test-app.sh http://127.0.0.1:<port> $TMPDIR/ref.jsonagainst the refapp. Expected: all pass; record the counts (passed/skipped per project) intests/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, setsOriginto base);(*Session) Add(title string) error;(*Session) IDs(path string) ([]string, error)(parsesli[data-id]);(*Session) Toggle(id string) error;(*Session) CookieHeader() string;Seed(s *Session, n int) ([]string, error)addsitem 1..nand toggles every second onettfb.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 requestwake.go:IsAsleep(app, host string) (bool, error)(runscarlos instances list --account bfj --app <app> --health asleepand reports whetherhostis listed);MeasureWake(s *Session, path string) (float64, error), the TTFB of one requestload.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). Scenarioread=GET /. Scenariotoggle=POST /todos/<first id>/togglewithfilter=all, sent with--redirect 0, where a 303 counts as success. Both send the session cookie andOrigin. ForErrorRate, 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/*.mjsexceptvan*.js. Templates are*.html,*.tmpl,*.erb,*.mustache, plus anything undertemplates/orviews/. Tests are files undertest/ortests/, or named*_test.*,*.test.*,*_spec.*. Server is every other source file.dist/,vendor/, lockfiles,*.cssand*.mdare skipped. Deps is the line count ofDEPS.buildstats.go:type Build struct{ ColdSeconds, WarmSeconds, EditSeconds float64; Bytes int64; Host string };BuildStats(dir string, runs int) (Build, error). It runs each moderunstimes and takes the median. Cold ismake clean clean-depsthen a timedmake build-arm64(withCOLD=1for the Go slugs). Warm ismake cleanthen a timedmake build-arm64. Edit is a build, thenmake bump, a timedmake build-arm64, andmake unbump. It also records the size ofdist/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 onturn.completedevents fromcodex exec --json)results.go:type AppResultholding 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)) errorreads and writesresults/<slug>.jsonwith indented JSONcmd/bakeoff: subcommandsttfb|wake|load|weight|code|build|tokens|report, each taking--slugand writing intoresults/<slug>.jsonthroughUpdate.tokensalso takes--phase build|fix|security|review, readsbuilders/logs/<slug>.<phase>*.jsonl(withParseCodexLogforreview,ParseClaudeLogotherwise) and sums them intoTokensBuild,TokensFix,TokensSecurityorTokensReview.- Tests:
*_test.gonext to each file. Fixtures go intestdata/: a realohaJSON output, a realclaudestream-json log and a realcodex --jsonlog, 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
ohafrom its GitHub release binary into~/.local/bin. Capture fixtures: runoha --output-format json -z 2sagainst the refapp; runclaude -p --model claude-opus-5-5 --output-format stream-json --verbose "say ok"; runcodex exec --json "say ok". Save them undertestdata/. - [ ] Step 2: Write the failing tests.
TestSeed(refapp: 50 ids, 25 completed fromIDs("/completed"));TestParseOha(fixture β RPS > 0, P99 β₯ P50);TestParseClaudeLog/TestParseCodexLog(fixtures β Output > 0);TestCountCode(atestdata/codetreewith one file per category and avendor/file that must be ignored β exact counts);TestPageWeight(refapp β JSBytes equals the size ofapp.js);TestBuildStats(atestdata/buildtreeMakefile whose targetssleepknown amounts β Cold > Warm > Edit > 0, andunbumpwas 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 runbakeoff load --slug refapp --url http://127.0.0.1:<port>and check thatresults/refapp.jsonhas 6 load rows (2 scenarios Γ concurrency 1, 16, 64, 30 s each with a 60 s rest), then deleteresults/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, plusscripts/smoke.sh <slug> - Create:
docs/toolchains.md: versions installed and the arm64 route for each language
Interfaces:
- Produces: on
PATH(through~/.local/binorsource scripts/env.sh): Go 1.26,rustupstable withaarch64-unknown-linux-musl,zig(latest stable),opam+ OCaml 5.x,ghcup+ GHC + cabal, Swift 6.2 + the Static Linux SDK, Mojo throughpixi, Spinel + tep + spinelkit at the pins inpaulca/lpay/spinel,qemu-aarch64-static(extracted withapt-get download qemu-user-static && dpkg-deb -x), andoha. Builders don't install compilers; they only add libraries.
- [ ] Step 1: Write
toolchains.shand run it. GHC and Mojo downloads are approved (spec). Checkdf -h ~first. - [ ] Step 2: For each slug,
scripts/smoke.sh <slug>builds a linux-arm64 hello binary that servesGET /healthzon--socket, checksfilesaysARM aarch64andstatically linked(or libc-only), then runs it underqemu-aarch64-staticand curls/healthzover the socket. Expected:okfor 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 frompaulca/lpayspinel/(README, BUILD-arm64.md, patches/tep-carlos.patch)";mojo"A single file is required; if Mojo can't produce one, stop withBLOCKED 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 recordssession_idfrom the first event intobuilders/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}; branchapp/<slug>withapps/<slug>/andSTATUS.md.
- [ ] Step 1: Write the files. Run
builders/run-build.sh goin the background. - [ ] Step 2: When it exits, check:
head -1 .worktrees/go/apps/go/STATUS.mdstarts withLIVE;curl -sI https://todo-go.bfj.oncarlos.com | grep -i x-carlos-versionmatches the version;scripts/test-app.sh https://todo-go.bfj.oncarlos.com $TMPDIR/go.jsonpasses;bakeoff tokens --slug go --phase buildwrites 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>, thenbuilders/run-build.sh <slug>in the background. All eight run in parallel. - [ ] Step 2: As each exits, read
STATUS.md. ForBLOCKED 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.jsonlthat counts toward build tokens). Count that time against the same 4-hour limit. Any otherBLOCKEDstays blocked. - [ ] Step 3: For every
LIVEapp, run the same checks as Task 6 Step 2. - [ ] Step 4: Merge each
app/<slug>branch intomain(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: reviewapps/{SLUG}/againstBRIEF.md,CONTRACT.mdand 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 tobuilders/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(nis 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, thenrun-fix.sh <slug> fixwithbuilders/reviews/<slug>.1.md. - [ ] Step 2: Run
run-review.sh <slug> 2. If it has any[security]items, run the fix with phasesecurityusing only those items. There's no third round. - [ ] Step 3: Store the
[security]/[correctness]/[spec]items that remain inresults/<slug>.jsonReviewFindings, 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>: concatenatesapps/<slug>/source asCountCodedefines it (server, templates, JS; no tests, vendor or docs) with relative paths, removes the stringstodo-<slug>and<slug>from paths, names only the language, then runsclaude -p --model claude-fable-5-1 --output-format jsonthree 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 intoAppResult. - [ ] 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 indocs/toolchains.md),bakeoff load(6 runs). - [ ] Step 3: Wake: once all load runs are done, leave every app idle. Every 5 minutes, check
IsAsleepfor each app. As soon as an app is asleep,bakeoff wakerecords 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-goand the console's instance view for memory figures. If there are none, searchamadan discuss list carlos/platformfor an existing request. Then file or reply:amadan discuss new carlos/platform -title "Member-visible memory use per instance" -body -(typeFeature Request), explaining the bakeoff use. Record the discussion id in everyAppResult.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 noNaNor<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
datavizskill before writing any chart code. Write the failing test, rungo 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 thecopy-reviewskill 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, thencarlos 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/reconand other scratch. Keepbuilders/logs/, because they're the token evidence. - [ ] Step 2: Confirm that
ss -ltnpshows no listener from this work, and thatcarlos apps list --account bfjshows the ninetodo-*apps (or fewer, matching the BLOCKED list) plusbakeoff. - [ ] Step 3: Run the
handoffskill.