mager-bench: the doom challenge

bench.mager.co

Simon Willison's pelican benchmark is one prompt — "Generate an SVG of a pelican riding a bicycle" — and the quality gap between models is immediately obvious when you look at the output. I wanted mager-bench to have its equivalent: one iconic, hard prompt where runnable output makes the difference self-evident.

The new doom challenge asks a model to build a Doom-style first-person raycasting FPS engine in a single HTML file, no external libraries. Open the file, play it.

The raycaster is not a Doom-themed prompt — it is literally Doom's engine. DDA raycasting, fish-eye correction, perspective-correct texture mapping, distance shading, a 16×16 map with rooms and dead ends, WASD + Pointer Lock mouse-look, AABB collision detection, a door that opens on E, an exit with a LEVEL COMPLETE timer, Z-buffer, minimap, FPS counter. All procedural — no images, no canvas assets loaded at runtime.

I grew up playing Doom. The technical core — casting rays through a 2D grid to fake a 3D world on underpowered hardware — is one of the most elegant ideas in the history of games. Models that actually understand the math produce something you can walk through. Models that fake it produce a gray box or a broken canvas.

The gap is instant and obvious. That's the benchmark.

All challenges are on GitHub.

AIbenchmarksevalsLLMWebGLJavaScriptDoom

mager-bench: free first

bench.mager.co

mager-bench v1.1 restructures the bench around cost. Default runs now use free models — Llama 3.3 70B and 3.1 8B through a new Groq adapter, plus Gemini Flash — and the judge defaults to a free model too, so a full 12-challenge leaderboard costs $0 and no longer requires an Anthropic key.

The README has carried the same two caveats since day one: the judge is a model too (by default, Claude grading Claude), and single-run variance is real. Both now have flags instead of apologies. --runs 3 reports mean ± stddev, so a score is a distribution rather than one roll. --judges gemini-2.0-flash,llama-3.3-70b averages a judge panel, so no model grades its own family alone.

export GROQ_API_KEY=... GEMINI_API_KEY=...
python bench.py --tier free --runs 3 \
  --judges gemini-2.0-flash,llama-3.3-70b

The expensive seats — Opus, GPT-4o, Sonnet — move to a public wishlist. There's a live /fund page and a FUND.md; Buy Me a Coffee is the working rail today, with GitHub Sponsors to follow. One rule either way: dollars only buy API tokens for published evals. Every funded run ships its raw responses and scores in results.json.

"Ships its raw responses" is now literal: every score on the dashboard links to a trace page with the exact prompt, the model's full response, and the judge's notes for each run. Haiku's 0.7 on doom stops being a number and becomes a readable failure.

Free tiers keep the leaderboard always on; crowdfunding unlocks the head-to-heads people actually argue about. All twelve challenges are on GitHub.

AIbenchmarksevalsLLMGroqGeminicrowdfunding

mager-bench

bench.mager.co

mager-bench expanded from 5 to 11 challenges. The four new ones cover territory the original set glossed over — testing discipline, debugging skill, async Python, and SQL fluency.

test-writing gives you a parse_duration() function and asks for a proper pytest suite using @pytest.mark.parametrize and pytest.raises. The interesting signal isn't whether models know pytest syntax — they all do — it's whether they parameterize across edge cases or just write three happy-path tests and call it done.

debug is a broken top_words() implementation with three distinct bugs. No stack trace, no error message — just wrong output. It tests careful reading more than raw code generation. Models that reach for the REPL in their heads before editing tend to do better here.

async-fetch asks for concurrent aiohttp requests with a per-request timeout and exponential backoff retry. It's a proxy for "can the model reason about failure modes at the call site, not just happy-path concurrency."

sql is a PostgreSQL query over an orders/customers schema that requires CTEs and window functions to answer cleanly. Most models can write either; the challenge is knowing when a window function is the right tool instead of a subquery.

Two more just landed: go-test asks for a table-driven WordCount test file using t.Run subtests and a benchmark — the idiomatic Go testing pattern that most models know in theory but often write awkwardly. elixir-test asks for an ExUnit suite with describe blocks and assert_raise, including a unicode string case that trips up anything relying on byte counts instead of String.length/1.

All eleven challenges are on GitHub.

AIPythonGoElixirbenchmarksevalsLLM

skill-evals

github.com/mager/skill-evals

skill-evals is a small Claude Code plugin for evaluating the skills and agents you build. It came out of reading the awesome-evals PATTERNS playbook and wanting the patterns as something I could run inside a project, not just a reference to nod along to. v0.1 is two skills.

The first, error-analysis, is the unglamorous step most people skip: read 20–100 real traces, write a one-line note on the first thing that broke in each, cluster those into a handful of named failure modes, and rank them by frequency × severity. The point is that you can't write a good eval for a failure you haven't seen yet, and generic metrics like "helpfulness" point nowhere. The output is a short taxonomy that tells you what to actually measure — and which failures need a cheap code assertion versus an LLM judge.

The second, build-judge, is the one I learned the most from. Using an LLM to grade subjective things (tone, faithfulness, did-it-follow-the-instruction) is easy; trusting that grader is the hard part. On an imbalanced set — say 90% of outputs are fine — a judge that stamps everything "pass" scores 90% accuracy and catches none of the real failures. So the skill ships a stdlib-only score.py that reports true-positive and true-negative rate separately and gates on both, exiting non-zero below threshold so it drops straight into CI. The rubber-stamp judge fails that gate even at 90% accuracy, which is exactly the trap it's there to catch.

Since then, v0.2 shipped exactly the patterns I'd left out: add-assertions (deterministic check scaffolding), passk (pass@k versus pass^k with the unbiased estimator), synth-data (grounded synthetic sets when you have no traffic), and an eval-runner agent that runs a whole suite and reports honestly. I also did the dogfood — pointing eval-runner at all 124 posts on this very blog. The first pass screamed that zero posts passed every check, which is exactly the kind of number that makes you panic and learn nothing. The honest finding was that my spec was wrong, not the blog: it was grading against categories and a keyword format the real schema never used. Once I fixed the spec, every post came back clean. That's the lesson the whole thing is built to teach — most "failures" are your metric lying, and you only catch it by looking.

If you want the conceptual map behind these patterns rather than the code, I wrote one up: a plain-English tour of the eval types worth knowing.

AISkillsEvalsClaude CodeLLM-as-judge

Loooom v1.0

github.com/mager/loooom.xyz

Loooom is v1.0 — tagged and released on GitHub. The shape hasn't changed since the pivot post: fifteen curated non-technical skills, each scored against a written rubric. What v1.0 adds is the layer of testing that was missing.

The two rubric gates judge the skill text. The new third gate runs each skill the way an agent actually would — SKILL.md as the system prompt — and unit-tests its Agent Behavior contract with promptfoo: hook has to make you say what the song is about in one sentence before writing anything, stack has to kill the 23% credit card before entertaining the crypto bet, focus has to send your phone to another room. Thirty tests, two per skill, deterministic assertions plus an LLM rubric, all on Groq's free tier.

The whole harness still costs $0 — judge and tests both run on Groq's free tier. The price shows up in a different currency: run the suite three times back-to-back and you blow through the tokens-per-minute cap, and everything crawls behind HTTP 429s. Promptfoo's cache makes that mostly painless (passing tests don't re-run), but a free eval stack rations your iteration speed instead of your wallet. For a project this size, that's the right trade.

The first run came back 27/30, and the failures were the educational part. Two were the tests' fault, not the skills': story and frame were correctly following their own "make them name the one thing first" contract while my rubrics demanded the whole lecture in turn one. The third was a token cap truncating train before it reached progressive overload. Behavioral tests don't just check the skills — they force you to decide what the skill is actually supposed to do on the first turn.

The audit also closed an embarrassing loop: voice had been shipping without a worked example — the one skill not practicing what it preached, and the spec gate had been flagging it since day one. Fixed in v1.0.

I ran this whole launch with Claude Code on Fable, Anthropic's new model — the skill audit, the test suite, the release, and this note. First project I've shipped with it.

AISkillsLoooomEvalsPromptfoo

Keeping an always-on agent alive across reboots

I run a Claude Code agent on a Mac mini in Chicago that I reach over Telegram. The hard part isn't the agent, it's keeping it up without me — across crashes, model swaps, and the occasional reboot. The fix is layered supervision, where each layer owns one kind of failure:

  • run.sh loops the agent and watches its exit code. An in-session model switch exits with code 42; the loop sees that and relaunches on the new model. Any other code stops the loop and hands control up.
  • tmux holds the session. The CLI is interactive and wants a PTY, so it runs inside a detached tmux session rather than a bare background process.
  • launchd is the floor. A LaunchAgent with RunAtLoad starts the tmux session at login (so it survives a reboot), and a StartInterval watchdog re-checks every couple of minutes and rebuilds the session if it's gone.

The thing I keep relearning: "restart it when it dies" is not one job. A reboot, a crash, and an intentional model swap are different failures, and each wants a different layer to catch it. Pile them all into one script and it's brittle; separate them and the whole thing just stays up.

agentsmacOSharness
$ curl ~/notes/rss.xml