Claude Code headless · 4 tasks · 51 hidden tests

flint vs Claude

Every model ran inside Claude Code with the same prompt and solved the tasks on its own. Grading happened afterwards, with hidden tests the model never saw.

Results

haiku-5.5

4/4 tasks complete
100%
51 of 51 hidden tests passed
Total time
1.9 min
Cost
$0.04
Turns
33

sonnet-5.5

4/4 tasks complete
100%
51 of 51 hidden tests passed
Total time
1.8 min
Cost
$0.62
Turns
29

opus-5.5

4/4 tasks complete
100%
51 of 51 hidden tests passed
Total time
2.4 min
Cost
$1.27
Turns
31

flint (local)

4/4 tasks complete
100%
51 of 51 hidden tests passed
Total time
75.8 min
Cost
local, no API cost
Turns
28

Total time for all 4 tasks

haiku-5.5
1.9 min
sonnet-5.5
1.8 min
opus-5.5
2.4 min
flint (local)
75.8 min

Per task

Taskhaiku-5.5sonnet-5.5opus-5.5flint (local)
paginationBugfixFour bugs in pager.py: off-by-one, page count rounded down instead of up, missing validation.10/1022 s · 8 turns10/1023 s · 7 turns10/1025 s · 6 turns10/1012.1 min · 9 turns
durationFeature from specparse_duration and format_duration from scratch, with lots of error cases.19/1923 s · 5 turns19/1923 s · 6 turns19/1934 s · 5 turns19/1924.1 min · 12 turns
lruData structureO(1) LRU cache without functools, with subtle rules ("in" does not count as a use).11/1118 s · 5 turns11/1124 s · 6 turns11/1130 s · 5 turns11/1114.6 min · 7 turns
shopBug spread across filesSix bugs in three files: quantity, discount/tax order, 10%, rounding, validation.11/1148 s · 15 turns11/1138 s · 10 turns11/1158 s · 15 turns11/1125.0 min · – turnshit the time limit

Code

For each task: the README given to the model, the starting code, the hidden tests and the code each model delivered at the end. Task texts and the prompt are in the original German.

pagination

Bugfix

Four bugs in pager.py: off-by-one, page count rounded down instead of up, missing validation.

README.md11 lines
# Aufgabe: Bugs in pager.py beheben

`paginate(items, page, per_page)` liefert die Elemente der 1-basierten Seite `page`.
Liegt die Seite hinter dem Ende, kommt eine leere Liste zurück.
`page < 1` oder `per_page < 1` wirft `ValueError`.

`total_pages(n_items, per_page)` liefert die Anzahl Seiten (angebrochene Seite zählt mit,
0 Elemente = 0 Seiten). `per_page < 1` wirft `ValueError`.

Die Funktionen enthalten mehrere Bugs. Behebe sie, ohne die Signaturen zu ändern.

duration

Feature from spec

parse_duration and format_duration from scratch, with lots of error cases.

README.md15 lines
# Aufgabe: duration.py implementieren

Lege `duration.py` mit zwei Funktionen an.

`parse_duration(s: str) -> int` wandelt eine Dauer in Sekunden um.
- Einheiten: `d` (Tag), `h`, `m`, `s`. Jede Einheit mit ganzzahligem Wert davor, z.B. `1h30m`, `45s`, `2d`.
- Gross-/Kleinschreibung egal, Leerzeichen zwischen den Teilen erlaubt (`1h 15m 10s`).
- Einheiten müssen absteigend vorkommen (d, h, m, s) und jede höchstens einmal.
- Alles andere (leer, unbekannte Einheit, Zahl ohne Einheit, doppelte Einheit, falsche
  Reihenfolge, negative Zahl) wirft `ValueError`.

`format_duration(seconds: int) -> str` ist die Gegenrichtung in kanonischer Form:
absteigend, ohne Leerzeichen, Einheiten mit Wert 0 weglassen, z.B. `5400 -> "1h30m"`.
`0 -> "0s"`. Negative Werte werfen `ValueError`.

lru

Data structure

O(1) LRU cache without functools, with subtle rules ("in" does not count as a use).

README.md12 lines
# Aufgabe: lru.py implementieren

Lege `lru.py` mit der Klasse `LRUCache` an, ohne `functools.lru_cache` zu benutzen.

- `LRUCache(capacity)`: `capacity < 1` wirft `ValueError`.
- `get(key, default=None)`: liefert den Wert oder `default`. Ein Treffer zählt als Nutzung.
- `put(key, value)`: setzt den Wert. Existiert der Key schon, wird er aktualisiert und zählt
  als Nutzung. Ist der Cache danach zu voll, fliegt der am längsten ungenutzte Eintrag raus.
- `len(cache)` liefert die Anzahl Einträge, `key in cache` prüft auf Vorhandensein, OHNE
  dass das als Nutzung zählt.
- `get` und `put` sollen O(1) sein.

shop

Bug spread across files

Six bugs in three files: quantity, discount/tax order, 10%, rounding, validation.

README.md15 lines
# Aufgabe: Warenkorb-Summe ist falsch

Das Paket `shop/` berechnet die Summe eines Warenkorbs. Kunden melden falsche Beträge.
So soll es laut Fachabteilung sein:

- Zwischensumme = Summe aus Stückpreis * Menge.
- Danach Rabattcode auf die Zwischensumme: `SAVE10` = 10 % Rabatt, `FLAT5` = 5,00 Rabatt,
  aber nie unter 0. Unbekannter Code: `apply_code` wirft sofort `ValueError`.
- Danach Steuer (Standard 19 %) auf den rabattierten Betrag.
- Endbetrag auf Cent runden, kaufmännisch (0,5 Cent wird aufgerundet).
- `add` mit Menge, die keine positive ganze Zahl ist, wirft `ValueError`.
- `total()` liefert ein `Decimal`.

Finde und behebe die Fehler. Die öffentliche API (`Cart`, `add`, `apply_code`, `total`) bleibt.

How it was measured

Prompt (German): „Lies README.md und erledige die Aufgabe dort vollständig. Schreib eigene Tests und führe sie mit python3 aus, bevor du fertig bist.“ In short: read the README, complete the task fully, and write your own tests and run them with python3 before you finish. Allowed tools: Read, Edit, Write, Glob, Grep, python3, at most 40 turns. flint ran locally through LM Studio (Anthropic compatible /v1/messages) with 64k context on a Mac with 48 GB RAM, the Claude models through the normal API.

Time limit was 10 minutes for Claude and 25 for flint. flint hit that limit on the shop task: the code was already fixed at that point and all hidden tests passed, only its own tests and the closing report were missing. The hidden tests were checked against reference solutions beforehand. One attempt per task, so it's a small sample.

run_bench.py104 lines
"""Mini-Benchmark: jede Aufgabe mit jedem Modell headless in Claude Code, danach versteckte Tests."""
import json
import os
import shutil
import subprocess
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor

BASE = os.path.dirname(os.path.abspath(__file__))
TASKS = ["pagination", "duration", "lru", "shop"]
PROMPT = ("Lies README.md und erledige die Aufgabe dort vollständig. "
          "Schreib eigene Tests und führe sie mit python3 aus, bevor du fertig bist.")

LOCAL = {"ANTHROPIC_BASE_URL": "http://localhost:1235", "ANTHROPIC_AUTH_TOKEN": "lmstudio",
         "ANTHROPIC_API_KEY": "", "ANTHROPIC_SMALL_FAST_MODEL": "recursivesi/flint",
         "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"}
MODELS = {
    "haiku-5.5": {"id": "claude-haiku-5-5", "env": {}, "timeout": 600},
    "sonnet-5.5": {"id": "claude-sonnet-5-5", "env": {}, "timeout": 600},
    "opus-5.5": {"id": "claude-opus-5-5", "env": {}, "timeout": 600},
    "flint (lokal)": {"id": "recursivesi/flint", "env": LOCAL, "timeout": 1500},
}
only = sys.argv[1:] or list(MODELS)

GRADER = r"""
import json, unittest
r = unittest.TextTestRunner(stream=open("/dev/null", "w")).run(
    unittest.defaultTestLoader.loadTestsFromName("hidden_test"))
fails = [str(t).split(" ")[0] for t, _ in r.failures + r.errors]
print(json.dumps({"total": r.testsRun, "failed": fails}))
"""

lock = threading.Lock()
results_path = os.path.join(BASE, "results.json")


def save(entry):
    with lock:
        data = json.load(open(results_path)) if os.path.exists(results_path) else []
        data = [d for d in data if not (d["model"] == entry["model"] and d["task"] == entry["task"])]
        data.append(entry)
        json.dump(data, open(results_path, "w"), indent=1)


def run(model, task):
    cfg = MODELS[model]
    ws = os.path.join(BASE, "runs", model.split()[0], task)
    shutil.rmtree(ws, ignore_errors=True)
    shutil.copytree(os.path.join(BASE, "tasks", task, "workspace"), ws)
    env = {k: v for k, v in os.environ.items() if not k.startswith("ANTHROPIC_")}
    env.pop("CLAUDECODE", None)
    env.update(cfg["env"])
    cmd = ["claude", "-p", PROMPT, "--model", cfg["id"], "--output-format", "json",
           "--allowedTools", "Read,Edit,Write,Glob,Grep,Bash(python3:*),Bash(ls:*)",
           "--max-turns", "40"]
    t0 = time.time()
    entry = {"model": model, "task": task}
    try:
        p = subprocess.run(cmd, cwd=ws, env=env, capture_output=True, text=True, timeout=cfg["timeout"])
        out, entry["exit"] = p.stdout, p.returncode
        open(ws + ".stdout", "w").write(p.stdout)
        open(ws + ".stderr", "w").write(p.stderr)
    except subprocess.TimeoutExpired as e:
        out, entry["exit"] = (e.stdout or b"").decode() if isinstance(e.stdout, bytes) else (e.stdout or ""), "timeout"
    entry["seconds"] = round(time.time() - t0, 1)
    meta = None
    for line in reversed(out.strip().splitlines()):
        try:
            meta = json.loads(line)
            break
        except ValueError:
            continue
    if meta:
        entry.update(turns=meta.get("num_turns"), cost=meta.get("total_cost_usd"),
                     agent_error=meta.get("is_error"), subtype=meta.get("subtype"),
                     summary=(meta.get("result") or "")[:600],
                     out_tokens=(meta.get("usage") or {}).get("output_tokens"))
    else:
        entry["summary"] = "kein JSON: " + out[-400:]
    shutil.copy(os.path.join(BASE, "tasks", task, "hidden_test.py"), ws)
    g = subprocess.run([sys.executable, "-c", GRADER], cwd=ws, capture_output=True, text=True, timeout=60)
    try:
        entry.update(json.loads(g.stdout.strip().splitlines()[-1]))
    except (ValueError, IndexError):
        entry.update(total=0, failed=["import/collect error"], grade_err=g.stderr[-400:])
    save(entry)
    print(model, task, entry.get("seconds"), entry.get("failed"), flush=True)


def lane(models, workers):
    jobs = [(m, t) for m in models for t in TASKS]
    with ThreadPoolExecutor(workers) as ex:
        list(ex.map(lambda j: run(*j), jobs))


cloud = [m for m in only if not MODELS[m]["env"]]
local = [m for m in only if MODELS[m]["env"]]
threads = [threading.Thread(target=lane, args=(cloud, 4)), threading.Thread(target=lane, args=(local, 1))]
[t.start() for t in threads]
[t.join() for t in threads]
print("FERTIG")