feat: builder pipeline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev afa9d3f7bc172c9977b8aabd21cbb383de232470 parent 5566d4f
16 files changed, +122 −0
  • .gitignore +1 −0
  • builders/claude.sh +8 −0
  • builders/extra/go-rastrillo.md +10 −0
  • builders/extra/go.md +1 −0
  • builders/extra/haskell.md +0 −0
  • builders/extra/mojo.md +9 −0
  • builders/extra/ocaml.md +0 −0
  • builders/extra/rust.md +0 −0
  • builders/extra/spinel.md +11 −0
  • builders/extra/swift.md +0 −0
  • builders/extra/zig.md +0 −0
  • builders/prompts/build.md +20 −0
  • builders/run-build.sh +27 −0
  • builders/worktree.sh +15 −0
  • docs/toolchains.md +9 −0
  • harness/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])) }