carlos / todomvc-bakeoff Public
feat: builder pipeline
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
16 files changed,
+122
−0
.gitignore+1 −0builders/claude.sh+8 −0builders/extra/go-rastrillo.md+10 −0builders/extra/go.md+1 −0builders/extra/haskell.md+0 −0builders/extra/mojo.md+9 −0builders/extra/ocaml.md+0 −0builders/extra/rust.md+0 −0builders/extra/spinel.md+11 −0builders/extra/swift.md+0 −0builders/extra/zig.md+0 −0builders/prompts/build.md+20 −0builders/run-build.sh+27 −0builders/worktree.sh+15 −0docs/toolchains.md+9 −0harness/cmd/language/main.go+11 −0
diff --git a/.gitignore b/.gitignore| index 235c0f1..c8f9a30 100644 |
| --- a/.gitignore |
| +++ b/.gitignore |
| @@ -21,3 +21,4 @@ tests/behaviour/playwright-report/ |
| *.db-shm |
| *.db-wal |
| .cache/ |
| +builders/logs/*.stderr |
diff --git a/builders/claude.sh b/builders/claude.sh| new file mode 100755 |
| index 0000000..e97b5c7 |
| --- /dev/null |
| +++ b/builders/claude.sh |
| @@ -0,0 +1,8 @@ |
| +#!/usr/bin/env bash |
| +# The one way builders are invoked, so every session gets identical |
| +# settings: Opus 5.5, stream-json logs, no MCP servers, no sandbox, |
| +# no permission prompts. |
| +exec claude --model claude-opus-5-5 --output-format stream-json --verbose \ |
| + --permission-mode bypassPermissions \ |
| + --mcp-config '{"mcpServers":{}}' --strict-mcp-config \ |
| + --settings '{"sandbox":{"enabled":false}}' "$@" |
diff --git a/builders/extra/go-rastrillo.md b/builders/extra/go-rastrillo.md| new file mode 100644 |
| index 0000000..040ddfb |
| --- /dev/null |
| +++ b/builders/extra/go-rastrillo.md |
| @@ -0,0 +1,10 @@ |
| +Use Rastrillo v0.28 (module `amadan.net/rastrillo/rastrillo`, available |
| +through the Go module proxy). Read `~/amadan.net/rastrillo/rastrillo/SKILL.md` |
| +first, and build the app the way Rastrillo recommends: its platform |
| +contract (`Resolve`/`Serve`/`Run`), routing, sessions, migrations and so |
| +on. Two exceptions, both from the bakeoff's shared rules: the UI is |
| +TodoMVC's markup and CSS (not Rastrillo's design system), and the |
| +contract's Origin check is the cross-site protection, because the shared |
| +tests and load tests post without a CSRF token. If Rastrillo's CSRF |
| +middleware would reject those posts, configure it accordingly and say so |
| +in your README. |
diff --git a/builders/extra/go.md b/builders/extra/go.md| new file mode 100644 |
| index 0000000..4921a2d |
| --- /dev/null |
| +++ b/builders/extra/go.md |
| @@ -0,0 +1 @@ |
| +Don't use Rastrillo. Otherwise choose freely among Go libraries, or use none. |
diff --git a/builders/extra/haskell.md b/builders/extra/haskell.md| new file mode 100644 |
| index 0000000..e69de29 |
diff --git a/builders/extra/mojo.md b/builders/extra/mojo.md| new file mode 100644 |
| index 0000000..d9963e7 |
| --- /dev/null |
| +++ b/builders/extra/mojo.md |
| @@ -0,0 +1,9 @@ |
| +Read the Mojo row and notes in `docs/toolchains.md` before anything else. |
| +Mojo builds and runs for arm64 inside the arm64 root, but every Mojo |
| +binary needs `libKGENCompilerRTShared.so` beside it, and Carloku's binary |
| +kind takes a single file. Look for a supported way to make a |
| +self-contained Mojo binary. If there isn't one, still build the app and |
| +pass the shared suite locally, then stop with |
| +`BLOCKED platform: single-file binaries only (Mojo needs libKGENCompilerRTShared.so)`. |
| +Don't bundle, embed or self-extract libraries to get around the |
| +single-file rule. |
diff --git a/builders/extra/ocaml.md b/builders/extra/ocaml.md| new file mode 100644 |
| index 0000000..e69de29 |
diff --git a/builders/extra/rust.md b/builders/extra/rust.md| new file mode 100644 |
| index 0000000..e69de29 |
diff --git a/builders/extra/spinel.md b/builders/extra/spinel.md| new file mode 100644 |
| index 0000000..a2e7f50 |
| --- /dev/null |
| +++ b/builders/extra/spinel.md |
| @@ -0,0 +1,11 @@ |
| +Spinel is Matz's ahead-of-time Ruby compiler. Start from a working CARLOS |
| +app built with it: `gh repo clone paulca/lpay`, directory `spinel/`. |
| +Read its `README.md` and `BUILD-arm64.md`. It uses tep (a Sinatra-style |
| +layer that drives Spinel) and `patches/tep-carlos.patch`, which teaches |
| +tep the platform's socket contract and fixes `Content-Length` for |
| +multi-byte UTF-8. Spinel, tep and spinelkit are checked out at tep's pin |
| +in `~/.local/share/bakeoff-toolchains/spinel-src/`, and an arm64 Spinel |
| +built inside the arm64 root is at |
| +`~/.local/share/bakeoff-toolchains/spinel-arm64/` (see |
| +`docs/toolchains.md`). Copy what you need into `apps/spinel/` rather than |
| +editing those checkouts. |
diff --git a/builders/extra/swift.md b/builders/extra/swift.md| new file mode 100644 |
| index 0000000..e69de29 |
diff --git a/builders/extra/zig.md b/builders/extra/zig.md| new file mode 100644 |
| index 0000000..e69de29 |
diff --git a/builders/prompts/build.md b/builders/prompts/build.md| new file mode 100644 |
| index 0000000..8c11fbb |
| --- /dev/null |
| +++ b/builders/prompts/build.md |
| @@ -0,0 +1,20 @@ |
| +You are the builder for one entry in the CARLOS TodoMVC bakeoff. |
| + |
| +- Setup: **{LANGUAGE}** |
| +- Slug: `{SLUG}` (your directory is `apps/{SLUG}/`, your branch is `app/{SLUG}`) |
| +- Carloku app: `todo-{SLUG}` on account `bfj`, host `todo-{SLUG}.bfj.oncarlos.com` |
| + |
| +You are running unattended. Nobody will answer questions, approve plans or |
| +review designs during this session: the design is settled in `BRIEF.md` and |
| +`CONTRACT.md`, so don't brainstorm, ask or wait for approval. Build, test, |
| +deploy, and finish by writing `apps/{SLUG}/STATUS.md`. If you're truly |
| +stuck, write `BLOCKED` in `STATUS.md` as the brief describes and end. |
| + |
| +Start by reading, in this order: `BRIEF.md`, `CONTRACT.md`, |
| +`docs/toolchains.md` (your language's route to a static linux-arm64 |
| +binary is already worked out there), then the CARLOS skills the brief |
| +points to. Toolchains are installed; put them on PATH with |
| +`source scripts/env.sh`. You may add libraries, but don't reinstall |
| +compilers. |
| + |
| +{EXTRA} |
diff --git a/builders/run-build.sh b/builders/run-build.sh| new file mode 100755 |
| index 0000000..f00cf84 |
| --- /dev/null |
| +++ b/builders/run-build.sh |
| @@ -0,0 +1,27 @@ |
| +#!/usr/bin/env bash |
| +# Runs one builder: a headless Opus 5.5 session in the app's worktree, with |
| +# a 4-hour limit. Logs go to builders/logs/<slug>.build.jsonl. |
| +# builders/run-build.sh <slug> |
| +set -euo pipefail |
| +slug=$1 |
| +repo=$(cd "$(dirname "$0")/.." && pwd) |
| +wt="$repo/.worktrees/$slug" |
| +[ -d "$wt" ] || { echo "no worktree: run builders/worktree.sh $slug" >&2; exit 1; } |
| +language=$(cd "$repo/harness" && go run ./cmd/language "$slug") |
| +prompt=$(python3 - "$repo" "$slug" "$language" <<'PY' |
| +import sys, pathlib |
| +repo, slug, lang = sys.argv[1:] |
| +t = pathlib.Path(repo, "builders/prompts/build.md").read_text() |
| +extra = pathlib.Path(repo, "builders/extra", slug + ".md").read_text().strip() |
| +print(t.replace("{SLUG}", slug).replace("{LANGUAGE}", lang).replace("{EXTRA}", extra)) |
| +PY |
| +) |
| +log="$repo/builders/logs/$slug.build.jsonl" |
| +cd "$wt" |
| +set +e |
| +timeout 4h "$repo/builders/claude.sh" -p "$prompt" > "$log" 2> "$repo/builders/logs/$slug.build.stderr" |
| +status=$? |
| +set -e |
| +python3 -c 'import json,sys; print(json.loads(open(sys.argv[1]).readline())["session_id"])' "$log" > "$repo/builders/logs/$slug.session" || true |
| +echo "$slug build exited $status (124 means the 4-hour limit)" |
| +exit $status |
diff --git a/builders/worktree.sh b/builders/worktree.sh| new file mode 100755 |
| index 0000000..13f1e98 |
| --- /dev/null |
| +++ b/builders/worktree.sh |
| @@ -0,0 +1,15 @@ |
| +#!/usr/bin/env bash |
| +# Creates the builder's sparse worktree: everything shared, its own app |
| +# directory, and nothing of the reference fixture or other apps. |
| +# builders/worktree.sh <slug> |
| +set -euo pipefail |
| +slug=$1 |
| +repo=$(cd "$(dirname "$0")/.." && pwd) |
| +wt="$repo/.worktrees/$slug" |
| +cd "$repo" |
| +git worktree add --no-checkout -b "app/$slug" "$wt" HEAD |
| +cd "$wt" |
| +git sparse-checkout set --no-cone '/*' '!/harness/refapp/' '!/apps/' "/apps/$slug/" '/apps/.keep' |
| +git checkout -q |
| +mkdir -p "apps/$slug" |
| +echo "$wt" |
diff --git a/docs/toolchains.md b/docs/toolchains.md| index 0ad9860..2420b77 100644 |
| --- a/docs/toolchains.md |
| +++ b/docs/toolchains.md |
| @@ -48,6 +48,15 @@ To run any arm64 binary on docs-t1: |
| builder can't find a supported way to make a self-contained binary, the |
| app is `BLOCKED platform: single-file binaries only (Mojo needs |
| libKGENCompilerRTShared.so)`. Don't work around it. |
| +- **Mojo and "standalone binaries":** in 2023 Mojo's authors said it |
| + compiles to fully standalone binaries |
| + ([news.ycombinator.com/item?id=35797485](https://news.ycombinator.com/item?id=35797485)). |
| + In Mojo 1.2.0 every executable, even an 18 KB hello-world, calls the |
| + runtime's start-up functions (`KGEN_CompilerRT_SetArgV`, |
| + `KGEN_CompilerRT_DestroyGlobals`, …) from `libKGENCompilerRTShared.so` |
| + (1.3 MB), which in turn needs `libstdc++`, `libMSupportGlobals.so` and |
| + `libAsyncRTRuntimeGlobals.so`. No static runtime ships with it, so |
| + "standalone" now means no Mojo install is needed, not one file. |
| - **Fallback:** if a route above fails for a real app, the builder writes |
| `BLOCKED needs-arm64-host`, and the orchestrator arranges a native |
| arm64 machine. |
diff --git a/harness/cmd/language/main.go b/harness/cmd/language/main.go| new file mode 100644 |
| index 0000000..97e946e |
| --- /dev/null |
| +++ b/harness/cmd/language/main.go |
| @@ -0,0 +1,11 @@ |
| +// Command language prints the display name of a contestant slug. |
| +package main |
| + |
| +import ( |
| + "fmt" |
| + "os" |
| + |
| + b "bakeoff/harness" |
| +) |
| + |
| +func main() { fmt.Println(b.Language(os.Args[1])) } |