Claude Code headless · 4 görev · 51 gizli test

flint vs Claude

Her model aynı prompt ile Claude Code içinde çalıştı ve görevleri kendi başına çözdü. Puanlama sonradan, modelin hiç görmediği gizli testlerle yapıldı.

Sonuç

haiku-5.5

4/4 görev tam
100%
51 gizli testten 51 tanesi geçti
Toplam süre
1,9 dk
Maliyet
$0,04
Turn
33

sonnet-5.5

4/4 görev tam
100%
51 gizli testten 51 tanesi geçti
Toplam süre
1,8 dk
Maliyet
$0,62
Turn
29

opus-5.5

4/4 görev tam
100%
51 gizli testten 51 tanesi geçti
Toplam süre
2,4 dk
Maliyet
$1,27
Turn
31

flint (lokal)

4/4 görev tam
100%
51 gizli testten 51 tanesi geçti
Toplam süre
75,8 dk
Maliyet
lokal, API ücreti yok
Turn
28

4 görevin toplam süresi

haiku-5.5
1,9 dk
sonnet-5.5
1,8 dk
opus-5.5
2,4 dk
flint (lokal)
75,8 dk

Görev bazında

Görevhaiku-5.5sonnet-5.5opus-5.5flint (lokal)
paginationBugfixpager.py içinde dört bug: off-by-one, sayfa sayısını yukarı yerine aşağı yuvarlama, eksik doğrulama.10/1022 sn · 8 turn10/1023 sn · 7 turn10/1025 sn · 6 turn10/1012,1 dk · 9 turn
durationSpec'ten featureparse_duration ve format_duration sıfırdan, birçok hata durumuyla.19/1923 sn · 5 turn19/1923 sn · 6 turn19/1934 sn · 5 turn19/1924,1 dk · 12 turn
lruVeri yapısıfunctools olmadan O(1) LRU cache, ince kurallarla (in kullanım sayılmaz).11/1118 sn · 5 turn11/1124 sn · 6 turn11/1130 sn · 5 turn11/1114,6 dk · 7 turn
shopÇok dosyaya dağılmış bugÜç dosyada altı hata: miktar, indirim/vergi sırası, %10, yuvarlama, doğrulama.11/1148 sn · 15 turn11/1138 sn · 10 turn11/1158 sn · 15 turn11/1125,0 dk · – turnsüre sınırı doldu

Code

Her görev için: modele verilen README, başlangıç kodu, gizli testler ve her modelin sonunda teslim ettiği kod. Görev metinleri ve prompt orijinal haliyle Almanca.

pagination

Bugfix

pager.py içinde dört bug: off-by-one, sayfa sayısını yukarı yerine aşağı yuvarlama, eksik doğrulama.

README.md11 satır
# 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

Spec'ten feature

parse_duration ve format_duration sıfırdan, birçok hata durumuyla.

README.md15 satır
# 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

Veri yapısı

functools olmadan O(1) LRU cache, ince kurallarla (in kullanım sayılmaz).

README.md12 satır
# 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

Çok dosyaya dağılmış bug

Üç dosyada altı hata: miktar, indirim/vergi sırası, %10, yuvarlama, doğrulama.

README.md15 satır
# 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.

Nasıl ölçüldü

Prompt (Almanca): „Lies README.md und erledige die Aufgabe dort vollständig. Schreib eigene Tests und führe sie mit python3 aus, bevor du fertig bist.“ Yani: README'yi oku, görevi tamamen yap, bitirmeden önce kendi testlerini yazıp python3 ile çalıştır. İzinli araçlar: Read, Edit, Write, Glob, Grep, python3, en fazla 40 turn. flint, LM Studio üzerinden (Anthropic uyumlu /v1/messages) 64k context ile 48 GB RAM'li bir Mac'te lokal çalıştı, Claude modelleri normal API üzerinden.

Süre sınırı Claude için 10, flint için 25 dakika. flint shop görevinde bu sınıra takıldı: kod o anda zaten düzeltilmişti ve gizli testlerin hepsi geçti, sadece kendi testleri ve kapanış raporu eksik. Gizli testler önceden referans çözümlerle doğrulandı. Görev başına tek deneme, yani küçük bir örneklem.

run_bench.py104 satır
"""Mini-Benchmark: jede Aufgabe mit jedem Modell headless in Claude Code, danach gizli testler."""
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")