carlos / todomvc-bakeoff Public
đŸ¤– Builder brief: TodoMVC on CARLOS
You're building one entry in a bakeoff. The same TodoMVC app is built in nine language setups and deployed to Carloku, and the results are compared on a public report page. The point is to show that CARLOS apps, and the CARLOS Platform, aren't tied to Go. It isn't a race to crown a winner, so write the app the way an experienced developer in your language would write it well the first time.
Your setup, your slug and anything specific to it are in the prompt that handed you this brief.
Read first
CONTRACT.md: the routes, forms and markup every app shares. It's binding.shared/reference.html,shared/base.css,shared/index.css: the TodoMVC page and styles you serve.vendor/carlos-skills/getting-started/SKILL.mdandvendor/carlos-skills/building-carlos-apps/SKILL.mdwithreferences/platform.md: what a CARLOS app is and how thecarlosCLI deploys one. Read them for the platform. Where they assume Go, Rastrillo, amadan hosting, canary reviews or Rastrillo's UI, this brief overrides them.- The TodoMVC app spec, adapted by the contract: https://github.com/tastejs/todomvc/blob/master/app-spec.md. The server holds the data in SQLite, not
localStorage, and the routes are real paths, not#/.
What to build
A server-rendered TodoMVC that works fully with JavaScript turned off, with a small amount of JavaScript layered on top so it behaves like the reference TodoMVC when JavaScript is on. There's no client framework and no JS build step. Plain ES modules are fine, and so is vendored VanJS.
You choose the stack: HTTP server, router, templating, SQLite binding, and anything else, as your best recommendation for this language. Explain the choices in your README.
The platform contract
- One binary for linux-arm64, started as
<bin> --socket <path> --db <path>. It listens on that unix socket. There is no$PORT. - For local runs and tests it also accepts
--listen 127.0.0.1:<port>in place of--socket. Never bind anything other than127.0.0.1. - It serves
GET /healthzandGET /api/version(seeCONTRACT.md). - When it needs its own hostname, it reads
CARLOS_HOST. It never parses the socket path. - Linked statically, or depending on nothing but libc. Instances run sandboxed and see only their own build, so don't count on any other shared library being there.
- SQLite in WAL mode, one file at
--db. Migrations only ever add; never drop or rewrite data. - Starts fast (instances sleep when idle and wake on the next request) and exits cleanly on SIGTERM.
- Don't set any memory, CPU or tier fields. Every app runs at the platform default.
Your directory
Work only in apps/<slug>/. Don't read other apps' directories or the harness source. Everything else in the repo is shared and read-only to you.
Write these files:
AGENTS.md, first, before code. It holds the one rule this app lives by, and the tenancy declaration: one instance for the whole app, each visitor's list kept separately by an anonymous cookie, no sign-in, trust class C (the todo text is stored as plain text; that's chosen and written down here).README.md: the stack you chose and why, and how to build, run, test and deploy.DEPS: one direct third-party dependency per line. Leave the file empty if there are none.Makefilewith these targets:
| Target | Does | |---|---| | make build | Builds for this machine into dist/app | | make build-arm64 | Builds the shipped binary into dist/app-linux-arm64 | | make test | Runs your own tests | | make clean | Removes build output only. Leaves dependency caches alone | | make clean-deps | Also removes every compiled third-party artifact, but keeps downloaded sources, so the next make build-arm64 compiles every library from scratch. If your toolchain keeps compiled libraries in a shared per-user store (cabal, opam, zig, swiftpm and so on), point that store inside apps/<slug>/.cache/ from the Makefile so this target can delete it without touching anything else. Go is the exception: never change GOCACHE. Instead, make build-arm64 COLD=1 passes -a | | make bump / make unbump | bump changes the text of one string constant in the file that holds the GET / handler, so the next build must recompile that file. unbump puts it back | | make deploy | Deploys dist/app-linux-arm64 with carlos deploy (see below) and needs no prompts |
STATUS.md, written last. Its first line is exactly one of: -LIVE https://todo-<slug>.bfj.oncarlos.com <version>-BLOCKED <one-line reason>, followed by a section headed## Where it got stuck.
Use BLOCKED needs-arm64-host only when the app builds and passes locally but you can't produce a linux-arm64 binary on this machine. Use BLOCKED platform: <what's missing> when Carloku lacks something the app needs. Don't work around the platform.
.cache/, dist/ and database files are git-ignored.
Testing
- Your own tests, in whatever form is normal for your language.
- The shared behaviour suite. Run your app with
dist/app --listen 127.0.0.1:<port> --db $TMPDIR/<slug>.db, then runscripts/test-app.sh http://127.0.0.1:<port> $TMPDIR/<slug>-suite.json. It runs every test with JavaScript on and again with it off. It must pass before you deploy. qemu-aarch64-staticis onPATH(seedocs/toolchains.md) if you want to run the arm64 binary locally first.
Deploying to Carloku
Account bfj, app todo-<slug>, host todo-<slug>.bfj.oncarlos.com. The first time:
carlos apps create --account bfj --app todo-<slug> --build-probe /api/version
carlos instances enable --account bfj --app todo-<slug>
carlos instances create --account bfj --app todo-<slug> --host todo-<slug>.bfj.oncarlos.com --channel edgeAfter that, make deploy runs carlos deploy --account bfj --app todo-<slug> --target linux-arm64 --message "<what changed>" dist/app-linux-arm64. The version is the git short SHA of your commit. Before the first deploy, run scripts/boot-check.sh dist/app-linux-arm64. It starts the arm64 binary under qemu on a unix socket, checks /healthz and /api/version, and checks for a clean exit on SIGTERM. (carlos vet needs operator access, which you don't have.) After deploying, run the shared suite against https://todo-<slug>.bfj.oncarlos.com. It must pass there too.
If a carlos command refuses or behaves unexpectedly, read its -h and references/platform.md before trying something else.
Scratch files
$TMPDIR is shared with other sessions. Make your own directory with mktemp -d -p "$TMPDIR", keep every scratch file in it, and delete only that directory. Never delete with a glob in $TMPDIR.
Git
Commit on your branch (app/<slug>) as you go, with clear messages ending in Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>. Don't push and don't merge.
Limits
You have 4 hours of wall-clock time. If you're stuck before then, stop, write BLOCKED in STATUS.md with where you got stuck, and end.