AIDive

Paket video

Arahkan subagent Claude Code ke Haiku 5.5: tabelnya, dua file, dan harness

14 menit baca

TL;DR

  • Subagent Claude Code tidak berjalan di Haiku 5.5 kecuali ada file yang menyuruhnya. Agent Explore bawaan dan setiap subagent kustom mewarisi model utama, dan variabel model subagent saja tidak menyentuh Explore.
  • Dua file memperbaikinya: file agent proyek Explore.md (Haiku 5.5, effort low) hanya untuk explorer, dan blok env settings.json dengan CLAUDE_CODE_SUBAGENT_MODEL_FORCE untuk mendorong setiap subagent ke Haiku.
  • Matriks kami, 182 run pada satu repo nyata: explore, tests, lint, commit, refactor, review, dan summary, semuanya diterima oleh tes tersembunyi atau aturan tetap di Haiku 5.5, baik di effort low maupun medium (masing-masing 70 dari 70).
  • Arahkan ketujuhnya ke Haiku 5.5 dengan effort low. Tetap pakai Sonnet 5.5 atau Opus 5.5 untuk coding agentik yang kompleks, yang juga saran Anthropic sendiri.
  • Meter plan yang sebenarnya nyaris tidak bergerak: jendela lima jam 21% ke 21% selama 70 run Haiku low, jendela mingguan 67% pada ke-182 run. Rincian /usage mengurutkan model dengan bobot lokal, bukan tagihan.
  • Pasang id model lengkap. Alias haiku adalah Haiku 4.5 di Bedrock, Vertex, dan Foundry.

Apa kata sumber-sumbernya

Haiku 5.5 berharga $0,10 per sejuta token input untuk prompt hingga 100k token dan $0,50 di atasnya, dengan output $0,50 dan $2,50. Haiku 4.5 berharga $1,00 dan $5,00, Sonnet 5.5 $2,00 dan $10,00 s1. Halaman harga mencantumkan Opus 5.5 di $4 dan $20 per sejuta token dan mencatat bahwa Haiku 5.5, tidak seperti model konteks 1M lainnya, dihargai menurut panjang prompt s20. Angka utama 90% adalah porsi request Haiku sebelumnya dengan prompt di bawah 100k token, bukan pemotongan harga s1.

Anthropic memposisikan Haiku 5.5 sebagai subagent di samping Opus 5.5 dan Sonnet 5.5, dan menyatakan Sonnet 5.5 dan Opus 5.5 tetap pilihan yang lebih baik untuk coding agentik yang kompleks, dengan Haiku 5.5 untuk pekerjaan yang lingkupnya sempit s1. Di Terminal-Bench 4.0, post peluncuran memberikan 39,2% untuk Haiku 5.5 dibanding 70,6% untuk Sonnet 5.5 s1.

Tidak ada yang masuk ke Haiku secara default. Sejak v2.1.198, agent Explore bawaan tidak lagi default ke Haiku dan mewarisi model utama s24. Urutan resolusi yang terdokumentasi untuk model sebuah subagent adalah: parameter per pemanggilan, frontmatter definisinya, CLAUDE_CODE_SUBAGENT_MODEL, lalu model percakapan utama s2. CLAUDE_CODE_SUBAGENT_MODEL saja tidak mengubah subagent Explore dan Plan bawaan s2. Subagent pengguna atau proyek bernama Explore menimpa yang bawaan, dan dokumentasi menyarankan untuk mendefinisikannya dengan model Haiku s2. CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1 menerapkan satu model ke setiap subagent, mulai v2.1.257 s2 s24.

Dokumen biaya merekomendasikan model: haiku untuk tugas subagent sederhana, dan menyatakan bahwa request subagent tetap memakai penggunaan yang sama s3. Meter mingguan ada satu untuk semua model ditambah satu untuk Fable jika disertakan s22, dan model lain menarik dari batas yang sama s23.

Di Claude Code, thinking tidak bisa dimatikan untuk Haiku 5.5, Opus 5.5, Sonnet 5.5, atau Fable: alwaysThinkingEnabled false dan MAX_THINKING_TOKENS=0 tidak berpengaruh. Effort berjalan dari low sampai max dan Haiku 5.5 default ke medium s21. Alias haiku diresolusikan ke Haiku 5.5 di Anthropic API dan ke Haiku 4.5 di Platform on AWS, Bedrock, Vertex, dan Foundry s21.

Setup seorang praktisi di plan Pro (Opus 5.5 utama di medium, implementer Sonnet 5.5, scoper Haiku 5.5 read-only, shipper Haiku 5.5 di low) melaporkan 13 PR dalam sepuluh sesi dengan sekitar 6% dari jendela lima jam untuk masing-masing, dan sekitar 1% dari counter mingguan untuk sesi satu PR s4. Catatannya sendiri: penghematan datang dari konteks utama yang tetap kecil, bukan dari model yang lebih murah, karena menghargai token subagent dengan tarif Opus hanya menambah 5% s4.

Dua file itu

Dua file, ambil salah satu atau keduanya. File agent proyek .claude/agents/Explore.md bernama Explore, sehingga menggantikan explorer bawaan. Id model lengkap, effort low, hanya tool baca dan cari.

Explore.md

---
name: Explore
description: Read-only codebase explorer. Use it to find where something is defined or called and to summarize what it found, with file paths and function names.
model: claude-haiku-5-5
effort: low
tools: Read, Glob, Grep, Bash
---

You explore a codebase and report back. Search with Glob and Grep, read only the files you need, and never edit anything. Answer with file paths and function names, then stop.

Explore.md on GitHub

File settings .claude/settings.json berisi blok env: model subagent, ditambah flag force yang membuatnya berlaku untuk setiap subagent.

settings.json

{
  "env": {
    "CLAUDE_CODE_SUBAGENT_MODEL": "claude-haiku-5-5",
    "CLAUDE_CODE_SUBAGENT_MODEL_FORCE": "1"
  }
}

settings.json on GitHub

Mana untuk siapa:

  • Pakai blok env jika setiap subagent harus murah. FORCE menyeret setiap agent ke Haiku, termasuk yang sengaja kamu pasang ke Opus.
  • Pakai file Explore jika hanya explorer yang perlu pindah. Subagent lain tetap memakai model masing-masing.
  • Menyetel CLAUDE_CODE_SUBAGENT_MODEL tanpa FORCE tidak berbuat apa-apa untuk Explore. Dua run headless kami, sesi utama di Fable, "use the Explore agent to find where the query cache is cleared": hasil tool menyebut Opus 5.5 tanpa pengaturan, dan Opus 5.5 lagi dengan variabel disetel ke Haiku 5.5 dan tidak ada yang lain. Dengan file Explore, atau dengan kedua variabel env, terbaca Haiku 5.5.

Pemeriksaannya: /tasks menampilkan model dan effort yang dijalankan tiap subagent (v2.1.243 ke atas s24), dan hasil tool Agent run headless memuat resolvedModel. Percayai baris itu, jangan pernah filenya.

Jebakan alias: tulis claude-haiku-5-5, bukan haiku. Alias itu adalah Haiku 5.5 hanya di Anthropic API; di Bedrock, Vertex, dan Foundry ia masih berarti Haiku 4.5 s21.

Laporan terbuka yang lebih lama menyebut model frontmatter sebuah subagent diabaikan secara diam-diam s25, dan yang lebih baru menghitung 1.149 dari 1.371 subagent berjalan di model Opus parent pada 2.1.277 sampai 2.1.290 s10. Di Claude Code 2.1.293, ke-182 run kami semuanya membawa model yang dikonfigurasi (model_ok True di tabel hasil). Tetap periksa.

Tabel routing

Tujuh tugas sehari-hari pada tinydb di 18d73a1, 226 tes, claude -p headless, satu pengecekan terima atau tolak yang deterministik per tugas. Setiap sel berisi run yang diterima, rata-rata harga list per run, rata-rata detik waktu nyata. Sel Haiku berisi 10 run per tugas, sel Sonnet dan Opus berisi 3 run per tugas.

Tugas Haiku 5.5 medium Haiku 5.5 low Sonnet 5.5 Opus 5.5 Rute
explore 10/10, $0,0047, 13,4 s 10/10, $0,0035, 10,7 s 3/3, $0,0679, 30,0 s 3/3, $0,1454, 18,0 s Haiku 5.5, effort low
tests 10/10, $0,0033, 9,8 s 10/10, $0,0031, 9,0 s 3/3, $0,0531, 20,0 s 3/3, $0,0892, 14,3 s Haiku 5.5, effort low
lint 10/10, $0,0048, 16,7 s 10/10, $0,0041, 14,0 s 3/3, $0,0693, 23,3 s 3/3, $0,1459, 27,7 s Haiku 5.5, effort low
commit 10/10, $0,0017, 5,1 s 10/10, $0,0018, 4,7 s 3/3, $0,0313, 7,0 s 3/3, $0,0593, 5,7 s Haiku 5.5, effort low
refactor 10/10, $0,0078, 26,3 s 10/10, $0,0052, 17,1 s 3/3, $0,1050, 30,3 s 3/3, $0,1675, 30,3 s Haiku 5.5, effort low
review 10/10, $0,0040, 16,0 s 10/10, $0,0030, 10,6 s 3/3, $0,0500, 20,3 s 3/3, $0,0951, 17,3 s Haiku 5.5, effort low
summary 10/10, $0,0037, 12,4 s 10/10, $0,0032, 8,4 s 3/3, $0,0347, 8,7 s 3/3, $0,0701, 7,7 s Haiku 5.5, effort low
Ketujuhnya 70/70, total $0,3006 70/70, total $0,2382 21/21, total $1,2339 21/21, total $2,3175

Tugas "refactor" adalah fitur kecil: menambah operasi update push dan pull ke tinydb.operations, dinilai oleh file tes tersembunyi. Biayanya $0,0052 di Haiku low dan $0,1675 di Opus, dalam 17,1 s dibanding 30,3 s.

Yang tetap di Sonnet atau Opus: coding agentik yang kompleks, menurut kata-kata Anthropic sendiri s1. Ketujuh tugas ini adalah tugas terbatas, masing-masing di bawah satu menit, pada satu repo kecil.

Harness

Benchmark yang menghasilkan tabel itu. Jalankan dari terminal biasa tanpa ada yang lain memakai akun, karena meter plan bersifat bersama. Checkout tinydb, tes tersembunyi, dan patch referensi tidak disertakan: arahkan ke repomu sendiri dan tulis pengecekanmu sendiri.

Runner dan meter:

  • README.md: apa itu harness dan apa yang ditinggalkan.

    README.md

# routing bench

The benchmark harness behind the routing table: run.sh runs one cell headless and reads the real meters from the rate_limit_event, matrix.sh runs cells sequentially, check.py decides accept or reject per task, analyze.py writes the results table. tasks/ holds the seven prompts, inject/ the planted bugs. The tinydb checkout (commit 18d73a1), the hidden tests and the reference patches are not included: bring your own repo and checks.

README.md on GitHub

- `run.sh`: satu sel, `claude -p` headless dengan model yang dipasang, salinan kerja baru, probe meter sesudahnya, lalu pengecekan.

run.sh

#!/usr/bin/env bash
# One run: fresh tinydb work copy + task injection, headless claude -p, extract tokens/meter, check.
# Usage: run.sh <config> <task> <rep>
#   config: a key of configs.json (haiku-med | haiku-low | sonnet | opus)
#   task:   a key of tasks.json   (explore | tests | lint | commit | refactor | review | summary)
set -uo pipefail
CFG="$1"; TASK="$2"; REP="${3:-1}"
HERE="$(cd "$(dirname "$0")" && pwd)"
. "$HERE/env.sh"
PY="$HERE/.venv/bin/python"
MAX_TURNS="${BENCH_MAX_TURNS:-40}"
NAME="${TASK}-${CFG}-r${REP}"
OUT="$HERE/runs/$NAME"; W="$HERE/work/$NAME"
rm -rf "$OUT"; mkdir -p "$OUT"
"$HERE/prep.sh" "$TASK" "$W" > "$OUT/prep.txt" 2>&1 || { echo "$NAME: prep failed"; cat "$OUT/prep.txt"; exit 2; }
( cd "$W" && git diff HEAD > "$OUT/pre.diff" )

# Config args (model pinned by full id, optional --effort)
CARGS=()
while IFS= read -r a; do CARGS+=("$a"); done < <("$PY" -c "
import json; [print(a) for a in json.load(open('$HERE/configs.json'))['$CFG']['args']]")
[ ${#CARGS[@]} -gt 0 ] || { echo "bad config $CFG" >&2; exit 2; }

# Fixed allow-list for every cell, including Bash(ruff:*) for the lint task.
TOOLS="Read,Edit,Write,MultiEdit,Glob,Grep,Skill,TodoWrite,Bash(uv:*),Bash(python:*),Bash(python3:*),Bash(pytest:*),Bash(ruff:*),Bash(git status:*),Bash(git diff:*),Bash(git log:*),Bash(ls:*),Bash(cat:*),Bash(mkdir:*),Bash(mktemp:*),Bash(head:*),Bash(tail:*),Bash(grep:*),Bash(wc:*)"

cd "$W"
START=$(date +%s)
claude -p "$(cat "$HERE/tasks/$TASK.md")" "${CARGS[@]}" \
  --output-format stream-json --verbose --max-turns "$MAX_TURNS" \
  --permission-mode acceptEdits --allowedTools "$TOOLS" \
  --setting-sources project --strict-mcp-config \
  > "$OUT/stream.jsonl" 2> "$OUT/stderr.txt"
echo "$?" > "$OUT/exit_code.txt"
END=$(date +%s)
echo "$((END-START))" > "$OUT/wall_s.txt"

# After-meter: one cheap Haiku probe (its own stream is kept for audit).
"$HERE/probe.sh" "$OUT/probe.jsonl" > "$OUT/after.json" 2>/dev/null

# Diff of the agent's work (tracked + untracked) vs the post-injection state
# (index left untouched: the commit task's staged changes stay staged for the check)
git diff HEAD -- . ':(exclude).coverage' > "$OUT/post.diff"
git diff HEAD --numstat -- . ':(exclude).coverage' > "$OUT/numstat.txt"
git ls-files --others --exclude-standard | while IFS= read -r f; do
  git diff --no-index -- /dev/null "$f" >> "$OUT/post.diff"
  echo "$(wc -l < "$f" | tr -d ' ')	0	$f" >> "$OUT/numstat.txt"
done

"$PY" "$HERE/extract.py" "$OUT" "$CFG" "$TASK" "$REP" > "$OUT/result.json"
"$PY" "$HERE/check.py" "$TASK" "$W" "$OUT/result.json" > "$OUT/check.txt"; echo "$?" >> "$OUT/check.txt"
"$PY" "$HERE/extract.py" "$OUT" "$CFG" "$TASK" "$REP" --score

run.sh on GitHub

- `matrix.sh`: menjalankan sel-sel berurutan satu demi satu dan menambahkan ke log.

matrix.sh

#!/usr/bin/env bash
# Run an ordered list of "<config> <task> <rep>" lines SEQUENTIALLY (the plan meter is shared:
# never run two cells at once). Blank lines and #comments are skipped. Appends to matrix.log.
# Usage: matrix.sh [matrix-file]   (default: matrix.txt)
#   SKIP_DONE=1 matrix.sh ...      skip cells whose runs/<task>-<config>-r<rep>/score.json exists (resume)
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"; cd "$HERE"
LIST="${1:-matrix.txt}"
# one matrix at a time: the meter is shared
mkdir "$HERE/.matrix.lock" 2>/dev/null || { echo "another matrix.sh is running (remove .matrix.lock if stale)" >&2; exit 1; }
trap 'rmdir "$HERE/.matrix.lock"' EXIT
echo "=== $(date '+%F %T') start $LIST" >> matrix.log
grep -vE '^\s*(#|$)' "$LIST" | while read -r CFG TASK REP; do
  if [ "${SKIP_DONE:-0}" = 1 ] && [ -f "runs/${TASK}-${CFG}-r${REP}/score.json" ]; then
    echo "$(date '+%F %T') skip ${TASK}-${CFG}-r${REP}" >> matrix.log; continue
  fi
  line=$(./run.sh "$CFG" "$TASK" "$REP" < /dev/null 2>&1 | tail -1)
  echo "$(date '+%F %T') $line" | tee -a matrix.log
done
echo "=== $(date '+%F %T') done $LIST" >> matrix.log

matrix.sh on GitHub

- `gen_matrix.py`: menulis matriks berurutan, tiap config sebagai blok yang berkesinambungan, config murah lebih dulu, putaran kedua dibalik.

gen_matrix.py

#!/usr/bin/env python3
"""Write the ordered matrix file.

gen_matrix.py [--reps 3] [--passes 2] [--configs haiku-low,haiku-med,sonnet,opus] [--out matrix.txt]

Order: configs run as contiguous BLOCKS (all tasks of one rep set), cheap configs first; tasks are
rotated inside each block so no task always runs first. With --passes 2 the second pass runs the
configs in reverse order (ABBA), which interleaves configs over time without breaking the
contiguous segments that analyze.py needs to beat the 1% meter resolution.
"""
import argparse, json, os

HERE = os.path.dirname(os.path.abspath(__file__))
ap = argparse.ArgumentParser()
ap.add_argument("--reps", type=int, default=3, help="reps per config x task per pass")
ap.add_argument("--passes", type=int, default=2)
ap.add_argument("--configs", default="haiku-low,haiku-med,sonnet,opus")
ap.add_argument("--start-rep", type=int, default=1, help="first rep number (use 2 to keep the r1 smoke runs)")
ap.add_argument("--out", default=os.path.join(HERE, "matrix.txt"))
a = ap.parse_args()
tasks = list(json.load(open(os.path.join(HERE, "tasks.json"))))
cfgs = a.configs.split(",")
lines, rep = [], a.start_rep
for p in range(a.passes):
    order = cfgs if p % 2 == 0 else list(reversed(cfgs))
    lines.append(f"# pass {p + 1}: {' -> '.join(order)}")
    for c in order:
        lines.append(f"# block {c}")
        for r in range(a.reps):
            k = rep + r
            rot = (k - 1) % len(tasks)
            for t in tasks[rot:] + tasks[:rot]:
                lines.append(f"{c} {t} {k}")
    rep += a.reps
open(a.out, "w").write("\n".join(lines) + "\n")
n = sum(1 for l in lines if not l.startswith("#"))
print(f"{a.out}: {n} runs ({len(cfgs)} configs x {len(tasks)} tasks x {a.reps} reps x {a.passes} passes)")

gen_matrix.py on GitHub

- `prep.sh`: membuat salinan kerja baru dan menerapkan perubahan yang ditanam tugas.

prep.sh

#!/usr/bin/env bash
# Build a fresh work copy for one task: upstream tinydb, baseline commit, task injection.
# Usage: prep.sh <task> <workdir>
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
TASK="$1"; W="$2"
read -r INJ MODE < <("$HERE/.venv/bin/python" -c "
import json,sys; t=json.load(open('$HERE/tasks.json'))['$TASK']; print(t['inject'] or '-', t['mode'] or '-')")
rm -rf "$W"; mkdir -p "$(dirname "$W")"
cp -R "$HERE/upstream" "$W"
cd "$W"
/usr/bin/find . -name __pycache__ -type d -prune -exec rm -rf {} +
rm -f .coverage
git update-index -q --refresh || true
G() { git -c user.email=dev@example.com -c user.name=dev "$@"; }
# Injections that belong to the baseline are folded into the top upstream commit
# (amend, same message/author/date), so `git log` shows no extra "bug" commit.
if [ "$MODE" = "commit" ]; then
  git apply "$HERE/$INJ"
  git add -A
  G commit -q --amend --no-edit --no-verify
fi
case "$MODE" in
  stage)    git apply --index "$HERE/$INJ" ;;
  worktree) git apply "$HERE/$INJ" ;;
esac

prep.sh on GitHub

- `env.sh`: auth dan environment yang dipakai bersama oleh setiap panggilan.

env.sh

# Sourced by run.sh and probe.sh.
# Auth: an isolated CLAUDE_CONFIG_DIR is not logged in
# (`claude auth status` -> loggedIn false), so runs use the user's default config dir:
# CLAUDE_CONFIG_DIR is deliberately left unset.
unset CLAUDE_CONFIG_DIR
# Fresh work copies at the same path must not inherit memory written by an earlier rep.
export CLAUDE_CODE_DISABLE_AUTO_MEMORY=1
# The bench venv (pytest, ruff, mypy) comes first, so `python -m pytest` and `ruff` just work.
export PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.venv/bin:$PATH"

env.sh on GitHub

- `probe.sh`: satu giliran Haiku yang mencetak meter plan saat ini sebagai JSON.

probe.sh

#!/usr/bin/env bash
# Cheapest possible call: one Haiku turn answering "ok". Prints the current plan
# meter read from its rate_limit_event as one JSON line. The probe itself costs a
# few hundred tokens of Haiku (well under the 1% meter resolution).
# Usage: probe.sh [raw-stream-out-file]
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
[ -f "$HERE/env.sh" ] && . "$HERE/env.sh"
RAW="${1:-$(mktemp -t hrb-probe)}"; case "$RAW" in /*) ;; *) RAW="$PWD/$RAW";; esac
cd "$(mktemp -d -t hrb-probe-cwd)"
claude --model claude-haiku-5-5 -p "ok" --max-turns 1 --output-format stream-json --verbose \
  --setting-sources project --strict-mcp-config > "$RAW" 2>/dev/null
"$HERE/.venv/bin/python" "$HERE/meter.py" "$RAW"

probe.sh on GitHub

- `meter.py`: membaca jendela lima jam dan tujuh hari dari baris `rate_limit_event` sebuah stream.

meter.py

#!/usr/bin/env python3
"""Read a stream-json file and print the plan meter carried by its rate_limit_event lines.

Output (one JSON line): {"ts", "five_hour", "five_hour_resets_at", "seven_day",
"seven_day_resets_at", "n_events", "raw_first", "raw_last"}: utilizations are the
values of the FIRST event (the meter as the run began). `--last` uses the last event.
"""
import json, sys, time

def events(path):
    out = []
    for line in open(path, errors="replace"):
        line = line.strip()
        if not line.startswith("{"):
            continue
        try:
            o = json.loads(line)
        except Exception:
            continue
        if o.get("type") == "rate_limit_event":
            out.append(o)
    return out

def windows(ev):
    info = ev.get("rate_limit_info") or {}
    uw = info.get("unifiedWindows") or {}
    def w(name):
        x = uw.get(name) or {}
        return x.get("utilization"), x.get("resetsAt")
    fh, fhr = w("five_hour")
    sd, sdr = w("seven_day")
    return {"five_hour": fh, "five_hour_resets_at": fhr, "seven_day": sd, "seven_day_resets_at": sdr}

def meter(path, which="first"):
    evs = events(path)
    if not evs:
        return {"ts": int(time.time()), "n_events": 0, "five_hour": None, "seven_day": None}
    ev = evs[0] if which == "first" else evs[-1]
    d = {"ts": int(time.time()), "n_events": len(evs)}
    d.update(windows(ev))
    d["raw_first"] = evs[0].get("rate_limit_info")
    d["raw_last"] = evs[-1].get("rate_limit_info")
    return d

if __name__ == "__main__":
    which = "last" if "--last" in sys.argv else "first"
    path = [a for a in sys.argv[1:] if not a.startswith("--")][0]
    print(json.dumps(meter(path, which)))

meter.py on GitHub

- `extract.py`: menarik token, biaya, giliran, dan meter dari stream sebuah run menjadi record skor.

extract.py

#!/usr/bin/env python3
"""Extract one run's numbers from runs/<name>/stream.jsonl.

extract.py <outdir> <config> <task> <rep>          -> prints the stream's `result` line (result.json)
extract.py <outdir> <config> <task> <rep> --score  -> writes score.json, prints a one-line summary
"""
import json, os, sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from meter import meter  # noqa: E402

out, cfg, task, rep = sys.argv[1:5]
score_mode = "--score" in sys.argv
stream = os.path.join(out, "stream.jsonl")

lines = []
for raw in open(stream, errors="replace") if os.path.exists(stream) else []:
    raw = raw.strip()
    if raw.startswith("{"):
        try:
            lines.append(json.loads(raw))
        except Exception:
            pass
result = next((o for o in reversed(lines) if o.get("type") == "result"), None)
init = next((o for o in lines if o.get("type") == "system" and o.get("subtype") == "init"), {})

if not score_mode:
    print(json.dumps(result or {"type": "result", "result": "", "missing": True}))
    sys.exit(0)

def rd(name, default=None):
    p = os.path.join(out, name)
    try:
        return open(p).read().strip()
    except Exception:
        return default

def jl(name):
    try:
        return json.loads(rd(name) or "null")
    except Exception:
        return None

cfgs = json.load(open(os.path.join(HERE, "configs.json")))
before = meter(stream, "first") if os.path.exists(stream) else None
after = jl("after.json")
r = result or {}
usage = r.get("usage") or {}
tok = {k: usage.get(k) or 0 for k in ("input_tokens", "cache_read_input_tokens", "cache_creation_input_tokens", "output_tokens")}
mu = r.get("modelUsage") or {}
assistant_models = sorted({(o.get("message") or {}).get("model") for o in lines
                           if o.get("type") == "assistant" and (o.get("message") or {}).get("model") not in (None, "<synthetic>")})
check_txt = (rd("check.txt", "") or "").split("\n")
verdict_line = check_txt[0] if check_txt else ""
numstat = [l.split("\t") for l in (rd("numstat.txt", "") or "").split("\n") if l.strip()]
added = sum(int(a) for a, d, f in numstat if a.isdigit())
deleted = sum(int(d) for a, d, f in numstat if d.isdigit())
changed_by_agent = (rd("pre.diff", "") or "") != (rd("post.diff", "") or "")

def strip_raw(m):
    if not m:
        return m
    return {k: v for k, v in m.items() if not k.startswith("raw_")}

s = {
    "run": os.path.basename(out), "config": cfg, "task": task, "rep": int(rep),
    "expected_model": cfgs[cfg]["model"], "config_args": cfgs[cfg]["args"],
    "init_model": init.get("model"), "assistant_models": assistant_models,
    "model_usage_keys": sorted(mu.keys()),
    "model_ok": init.get("model") == cfgs[cfg]["model"] and set(mu.keys()) <= {cfgs[cfg]["model"]} and bool(mu),
    "claude_code_version": init.get("claude_code_version"),
    "meter_before": strip_raw(before), "meter_after": strip_raw(after),
    "n_rate_limit_events": (before or {}).get("n_events", 0),
    "has_result_line": result is not None,
    "subtype": r.get("subtype"), "is_error": r.get("is_error"), "stop_reason": r.get("stop_reason"),
    "num_turns": r.get("num_turns"), "duration_ms": r.get("duration_ms"), "duration_api_ms": r.get("duration_api_ms"),
    "total_cost_usd": r.get("total_cost_usd"), "usage": usage, "tokens": tok, "modelUsage": mu,
    "wall_s": int(rd("wall_s.txt", "0") or 0), "exit_code": rd("exit_code.txt"),
    "final_text": (r.get("result") or "")[-2000:],
    "diff": {"files": len(numstat), "added": added, "deleted": deleted, "changed_by_agent": changed_by_agent},
    "check": {"verdict": verdict_line.split(" ")[0] if verdict_line else None, "reason": verdict_line[5:] if verdict_line else None},
}
s["accepted"] = s["check"]["verdict"] == "PASS"
json.dump(s, open(os.path.join(out, "score.json"), "w"), indent=2)
mb, ma = s["meter_before"] or {}, s["meter_after"] or {}
print(f"{s['run']}: {s['check']['verdict']} turns={s['num_turns']} cost=${(s['total_cost_usd'] or 0):.3f} "
      f"in={tok['input_tokens']} cr={tok['cache_read_input_tokens']} cc={tok['cache_creation_input_tokens']} out={tok['output_tokens']} "
      f"wall={s['wall_s']}s 5h {mb.get('five_hour')}->{ma.get('five_hour')} 7d {mb.get('seven_day')}->{ma.get('seven_day')} "
      f"model_ok={s['model_ok']} | {s['check']['reason']}")

extract.py on GitHub

- `check.py`: keputusan terima atau tolak per tugas.

check.py

#!/usr/bin/env python3
"""Deterministic accept/reject check for one run.

Usage: check.py <task> <workdir> <result.json>
  result.json = the `result` line of the run's stream (only its "result" text is used).
Prints `PASS <reason>` or `FAIL <reason>`; exit 0 on PASS, 1 on FAIL.
"""
import json, os, re, subprocess, sys

HERE = os.path.dirname(os.path.abspath(__file__))
PY = os.path.join(HERE, ".venv", "bin", "python")
RUFF = os.path.join(HERE, ".venv", "bin", "ruff")


def final_text(path):
    try:
        r = json.load(open(path))
    except Exception:
        return ""
    return r.get("result") or "" if isinstance(r, dict) else ""


def sh(args, cwd, env=None):
    p = subprocess.run(args, cwd=cwd, capture_output=True, text=True, env=env)
    return p.returncode, p.stdout + p.stderr


def suite(wd):
    rc, out = sh([PY, "-m", "pytest", "-q", "-p", "no:cacheprovider", "-o", "addopts=", "tests"], wd)
    m = re.search(r"(\d+) passed", out)
    f = re.search(r"(\d+) failed", out)
    e = re.search(r"(\d+) error", out)
    return rc == 0, f"suite {m.group(1) if m else 0} passed, {f.group(1) if f else 0} failed, {e.group(1) if e else 0} errors"


def git_diff(wd, *paths):
    # tracked changes vs the baseline commit + untracked files under paths
    _, d = sh(["git", "diff", "HEAD", "--", *paths], wd)
    _, u = sh(["git", "ls-files", "--others", "--exclude-standard", "--", *paths], wd)
    return d.strip(), u.strip()


def words(s):
    return re.findall(r"[A-Za-z0-9_']+", s)


def strip_fences(s):
    lines = s.strip().split("\n")
    if lines and lines[0].strip().startswith("```"):
        lines = lines[1:]
    if lines and lines[-1].strip().startswith("```"):
        lines = lines[:-1]
    return "\n".join(lines).strip()


def c_explore(wd, txt):
    t = txt.lower()
    miss = []
    if "table.py" not in t:
        miss.append("table.py")
    if "clear_cache" not in t and "_query_cache" not in t:
        miss.append("clear_cache/_query_cache")
    names = ["insert_multiple", "update_multiple", "insert", "update", "upsert", "remove", "truncate"]
    found = {n for n in names if re.search(r"(?<![a-z_])" + n + r"(?![a-z_])", t)}
    if len(found) < 3:
        miss.append(f"only {len(found)} mutating methods {sorted(found)}")
    return (not miss), ("missing " + ", ".join(miss)) if miss else f"table.py + cache method + {len(found)} methods {sorted(found)}"


def c_tests(wd, txt):
    ok, s = suite(wd)
    d, u = git_diff(wd, "tests")
    if d or u:
        return False, f"tests/ modified; {s}"
    return ok, s + "; tests/ untouched"


def c_lint(wd, txt):
    rc, out = sh([RUFF, "check", "tinydb", "--output-format", "concise", "--no-cache"], wd)
    cfg_d, _ = git_diff(wd, "ruff.toml", "pyproject.toml")
    d, _ = git_diff(wd, "tinydb")
    noqa = [l for l in d.split("\n") if l.startswith("+") and "noqa" in l.lower()]
    ok, s = suite(wd)
    why = []
    if rc != 0:
        found = [l for l in out.split("\n") if l.startswith("Found ")]
        why.append("ruff: " + (found[0] if found else out.strip().split("\n")[-1]))
    if cfg_d:
        why.append("lint config changed")
    if noqa:
        why.append(f"{len(noqa)} noqa added")
    if not ok:
        why.append(s)
    return (not why), ("; ".join(why) if why else f"ruff clean; {s}")


DIFF_LINE = re.compile(r"^(diff --git |index [0-9a-f]{6,}\.\.|@@ |\+\+\+ |--- a/|--- /dev/null|[+-] {3,}\S|[+-](def|class|import|from|return) )")


def c_commit(wd, txt):
    msg = strip_fences(txt)
    if not msg:
        return False, "empty answer"
    first = msg.split("\n")[0].strip()
    why = []
    if len(first) > 72:
        why.append(f"first line {len(first)} chars")
    if "push" not in first.lower():
        why.append("no 'push' in first line")
    if "pull" not in first.lower():
        why.append("no 'pull' in first line")
    dl = [l for l in msg.split("\n") if DIFF_LINE.match(l)]
    if dl:
        why.append(f"{len(dl)} diff lines")
    return (not why), ("; ".join(why) if why else f"first line {len(first)} chars: {first!r}")


def c_refactor(wd, txt):
    env = dict(os.environ, TINYDB_TREE=wd)
    rc, out = sh([PY, "-m", "pytest", "-q", "-c", os.path.join(HERE, "hidden", "pytest.ini"),
                  "--rootdir", os.path.join(HERE, "hidden"), os.path.join(HERE, "hidden", "test_T1.py")], wd, env)
    m = re.search(r"(\d+) passed", out)
    hp = int(m.group(1)) if m else 0
    ok, s = suite(wd)
    return (hp == 5 and ok), f"hidden {hp}/5; {s}"


REVIEW_PATTERNS = [
    r"limit\s*=?=\s*0\b",
    r"limit\s+(of\s+)?(0|zero)\b",
    r"\b(0|zero)\s+limit",
    r"zero[- ]limit",
    r"limit.{0,150}\b(falsy|falsey|truthy|truthiness)",
    r"\b(falsy|falsey|truthy|truthiness)\b.{0,150}limit",
    r"if limit:.{0,150}\b(0|zero)\b",
    r"\b(0|zero)\b.{0,150}if limit:",
]


def c_review(wd, txt):
    t = txt.lower().replace("`", "")
    for p in REVIEW_PATTERNS:
        m = re.search(p, t, re.S)
        if m:
            return True, f"matched {p!r}: {m.group(0)[:80]!r}"
    return False, "planted limit=0 defect not mentioned"


def c_summary(wd, txt):
    n = len(words(txt))
    t = txt.lower()
    mods = [m for m in ["database", "table", "queries", "storages", "middlewares", "operations"] if m in t]
    why = []
    if n > 170:
        why.append(f"{n} words > 170")
    if len(mods) < 4:
        why.append(f"only {len(mods)} modules {mods}")
    return (not why), ("; ".join(why) if why else f"{n} words, modules {mods}")


CHECKS = {"explore": c_explore, "tests": c_tests, "lint": c_lint, "commit": c_commit,
          "refactor": c_refactor, "review": c_review, "summary": c_summary}

if __name__ == "__main__":
    task, wd, res = sys.argv[1], os.path.abspath(sys.argv[2]), sys.argv[3]
    ok, why = CHECKS[task](wd, final_text(res))
    print(("PASS " if ok else "FAIL ") + why)
    sys.exit(0 if ok else 1)

check.py on GitHub

- `analyze.py`: mengagregasi run menjadi tabel per tugas dan per config.

analyze.py

#!/usr/bin/env python3
"""Aggregate runs/*/score.json into results.json and results.md.

Per config x task: accept rate, mean tokens, mean cost, mean turns, mean wall.
Per config: measured plan-meter deltas (five_hour, seven_day, in percentage points), computed two ways:
  - per_run_sum: sum over the config's runs of (after - before). Quantized at 1% per run, so
    runs far below 1% mostly read 0: a lower-bound-ish, very noisy number.
  - segments:    runs sorted by start time are cut into maximal contiguous segments of the same
    config; each segment contributes (after of its last run - before of its first run). This
    telescopes the quantization: one +-1 pt error per segment instead of per run. PREFERRED.
  Segments/runs whose window resetsAt changed between before and after are excluded for that window.
Plus the /usage-style local heuristic: weighted = cached*1 + uncached*10 + cache_create*12.5 + output*50,
times the model tier (haiku 1 / sonnet 3 / opus 5). The probe's own Haiku tokens after each run are
counted as overhead in the heuristic (they also move the meter).
Usage: analyze.py [--runs-dir runs] [--out-prefix results]
"""
import glob, json, os, statistics, sys

HERE = os.path.dirname(os.path.abspath(__file__))
RUNS = os.path.join(HERE, sys.argv[sys.argv.index("--runs-dir") + 1]) if "--runs-dir" in sys.argv else os.path.join(HERE, "runs")
OUTP = sys.argv[sys.argv.index("--out-prefix") + 1] if "--out-prefix" in sys.argv else "results"
CFGS = json.load(open(os.path.join(HERE, "configs.json")))
TASKS = list(json.load(open(os.path.join(HERE, "tasks.json"))).keys())
W = {"cache_read_input_tokens": 1, "input_tokens": 10, "cache_creation_input_tokens": 12.5, "output_tokens": 50}
TIER = {c: v["tier"] for c, v in CFGS.items()}


def weighted(tok, tier):
    raw = sum((tok.get(k) or 0) * w for k, w in W.items())
    return raw, raw * tier


MU = {"cacheReadInputTokens": "cache_read_input_tokens", "inputTokens": "input_tokens",
      "cacheCreationInputTokens": "cache_creation_input_tokens", "outputTokens": "output_tokens"}


def model_tier(name, fallback):
    n = (name or "").lower()
    return 5 if "opus" in n else 3 if "sonnet" in n else 1 if "haiku" in n else fallback


def heuristic_run(s):
    """Tiered heuristic per model actually used (modelUsage), so helper calls on another
    model are weighted with their own tier; falls back to `usage` x config tier."""
    mu = s.get("modelUsage") or {}
    if not mu:
        return weighted(s["tokens"], TIER[s["config"]])[1]
    return sum(weighted({MU[k]: v.get(k) or 0 for k in MU}, model_tier(m, TIER[s["config"]]))[1] for m, v in mu.items())


def probe_tokens(run_dir):
    p = os.path.join(run_dir, "probe.jsonl")
    if not os.path.exists(p):
        return {}
    for line in reversed(open(p, errors="replace").read().strip().split("\n")):
        try:
            o = json.loads(line)
        except Exception:
            continue
        if o.get("type") == "result":
            u = o.get("usage") or {}
            return {k: u.get(k) or 0 for k in W}
    return {}


def mean(xs):
    xs = [x for x in xs if x is not None]
    return statistics.mean(xs) if xs else None


def window_delta(b, a, w):
    if not b or not a or b.get(w) is None or a.get(w) is None:
        return None, "missing"
    if b.get(w + "_resets_at") != a.get(w + "_resets_at"):
        return None, "reset"
    return round((a[w] - b[w]) * 100, 2), "ok"


scores = []
for p in sorted(glob.glob(os.path.join(RUNS, "*", "score.json"))):
    s = json.load(open(p))
    if s.get("config") not in CFGS:
        continue
    s["_dir"] = os.path.dirname(p)
    s["_start"] = (s.get("meter_before") or {}).get("ts") or os.path.getmtime(os.path.join(s["_dir"], "stream.jsonl"))
    s["_probe_tok"] = probe_tokens(s["_dir"])
    scores.append(s)
# meter_before.ts is written at extraction time; order by stream file creation instead
for s in scores:
    st = os.stat(os.path.join(s["_dir"], "stream.jsonl"))
    s["_start"] = getattr(st, "st_birthtime", st.st_mtime)
scores.sort(key=lambda s: s["_start"])

cells, per_cfg = {}, {}
for s in scores:
    cells.setdefault((s["config"], s["task"]), []).append(s)
    per_cfg.setdefault(s["config"], []).append(s)

cell_rows = []
for (c, t), ss in sorted(cells.items(), key=lambda kv: (list(CFGS).index(kv[0][0]), TASKS.index(kv[0][1]) if kv[0][1] in TASKS else 99)):
    row = {"config": c, "task": t, "n": len(ss),
           "accept_rate": sum(1 for s in ss if s.get("accepted")) / len(ss),
           "mean_turns": mean([s.get("num_turns") for s in ss]),
           "mean_cost_usd": mean([s.get("total_cost_usd") for s in ss]),
           "mean_wall_s": mean([s.get("wall_s") for s in ss]),
           "mean_tokens": {k: mean([s["tokens"].get(k) for s in ss]) for k in W},
           "mean_weighted_heuristic": mean([heuristic_run(s) for s in ss]),
           "model_ok_all": all(s.get("model_ok") for s in ss)}
    cell_rows.append(row)

# contiguous segments in global start order
segments, cur = [], None
for s in scores:
    if cur and cur["config"] == s["config"]:
        cur["runs"].append(s)
    else:
        cur = {"config": s["config"], "runs": [s]}
        segments.append(cur)

cfg_rows = []
for c, ss in per_cfg.items():
    tok_sum = {k: sum(s["tokens"].get(k) or 0 for s in ss) for k in W}
    probe_sum = {k: sum(s["_probe_tok"].get(k, 0) for s in ss) for k in W}
    raw_w, _ = weighted(tok_sum, TIER[c])
    heur = sum(heuristic_run(s) for s in ss)
    probe_raw, probe_heur = weighted(probe_sum, 1)
    row = {"config": c, "runs": len(ss), "accept_rate": sum(1 for s in ss if s.get("accepted")) / len(ss),
           "tokens_sum": tok_sum, "probe_tokens_sum": probe_sum,
           "cost_sum_usd": round(sum(s.get("total_cost_usd") or 0 for s in ss), 4),
           "weighted_raw_M": raw_w / 1e6, "heuristic_M": (heur + probe_heur) / 1e6}
    for w in ("five_hour", "seven_day"):
        per_run, excl = 0.0, 0
        for s in ss:
            d, why = window_delta(s.get("meter_before"), s.get("meter_after"), w)
            if d is None:
                excl += 1
            else:
                per_run += d
        seg_sum, seg_n, seg_excl = 0.0, 0, 0
        for g in segments:
            if g["config"] != c:
                continue
            d, why = window_delta(g["runs"][0].get("meter_before"), g["runs"][-1].get("meter_after"), w)
            if d is None:
                seg_excl += 1
            else:
                seg_sum += d; seg_n += 1
        row[w] = {"per_run_sum_pts": round(per_run, 2), "per_run_excluded": excl,
                  "segments_sum_pts": round(seg_sum, 2), "segments": seg_n, "segments_excluded": seg_excl,
                  "quantization_error_pts": f"+-{seg_n}",
                  "pts_per_M_heuristic": round(seg_sum / row["heuristic_M"], 4) if row["heuristic_M"] else None,
                  "pts_per_M_weighted_raw": round(seg_sum / row["weighted_raw_M"], 4) if row["weighted_raw_M"] else None}
    cfg_rows.append(row)
cfg_rows.sort(key=lambda r: list(CFGS).index(r["config"]))

out = {"generated_from": len(scores), "cells": cell_rows, "configs": cfg_rows,
       "segments": [{"config": g["config"], "runs": [s["run"] for s in g["runs"]],
                     "before": g["runs"][0].get("meter_before"), "after": g["runs"][-1].get("meter_after")} for g in segments],
       "heuristic_weights": {"tokens": W, "tier": TIER}}
json.dump(out, open(os.path.join(HERE, OUTP + ".json"), "w"), indent=2, default=str)

f = lambda x, d=0: "-" if x is None else (f"{x:,.{d}f}")
md = ["# Results", "", f"{len(scores)} runs. Meter deltas in percentage points of the plan window (1-pt resolution).", "",
      "## Per config x task", "",
      "| config | task | n | accept | turns | input | cache read | cache create | output | cost $ | wall s | heuristic (M) | model ok |",
      "|---|---|---|---|---|---|---|---|---|---|---|---|---|"]
for r in cell_rows:
    t = r["mean_tokens"]
    md.append(f"| {r['config']} | {r['task']} | {r['n']} | {r['accept_rate']:.0%} | {f(r['mean_turns'],1)} | {f(t['input_tokens'])} | "
              f"{f(t['cache_read_input_tokens'])} | {f(t['cache_creation_input_tokens'])} | {f(t['output_tokens'])} | "
              f"{f(r['mean_cost_usd'],4)} | {f(r['mean_wall_s'],1)} | {f(r['mean_weighted_heuristic']/1e6,2)} | {r['model_ok_all']} |")
md += ["", "## Per config: measured meter vs local heuristic", "",
       "| config | runs | accept | cost $ | heuristic (M units) | 5h pts (segments) | 5h pts (per-run sum) | 7d pts (segments) | segments | 5h pts / M heuristic | 7d pts / M heuristic |",
       "|---|---|---|---|---|---|---|---|---|---|---|"]
for r in cfg_rows:
    fh, sd = r["five_hour"], r["seven_day"]
    md.append(f"| {r['config']} | {r['runs']} | {r['accept_rate']:.0%} | {r['cost_sum_usd']} | {r['heuristic_M']:.2f} | "
              f"{fh['segments_sum_pts']} (excl {fh['segments_excluded']}) | {fh['per_run_sum_pts']} | {sd['segments_sum_pts']} (excl {sd['segments_excluded']}) | "
              f"{fh['segments']} | {fh['pts_per_M_heuristic']} | {sd['pts_per_M_heuristic']} |")
md += ["", "If the /usage heuristic matched the real meter, `pts / M heuristic` would be the same for every config.",
       "Each segment carries up to +-1 pt of quantization error; trust a config's number only when its segment sum is several points.", ""]
open(os.path.join(HERE, OUTP + ".md"), "w").write("\n".join(md))
print("\n".join(md))

analyze.py on GitHub

Konfigurasi:

  • configs.json: keempat pekerja, dengan id model lengkap dan --effort low untuk blok low.

    configs.json

{
  "haiku-med": {"args": ["--model", "claude-haiku-5-5"], "model": "claude-haiku-5-5", "tier": 1, "note": "default effort"},
  "haiku-low": {"args": ["--model", "claude-haiku-5-5", "--effort", "low"], "model": "claude-haiku-5-5", "tier": 1},
  "sonnet":    {"args": ["--model", "claude-sonnet-5-5"], "model": "claude-sonnet-5-5", "tier": 3, "note": "default effort"},
  "opus":      {"args": ["--model", "claude-opus-5-5"], "model": "claude-opus-5-5", "tier": 5, "note": "default effort"}
}

configs.json on GitHub

- `tasks.json`: per tugas, perubahan yang ditanam dan cara menerapkannya.

tasks.json

{
  "explore":  {"inject": null,                     "mode": null,       "kind": "answer"},
  "tests":    {"inject": "inject/tests.patch",     "mode": "commit",   "kind": "code"},
  "lint":     {"inject": "inject/lint.patch",      "mode": "commit",   "kind": "code"},
  "commit":   {"inject": "reference/T1.patch",     "mode": "stage",    "kind": "answer"},
  "refactor": {"inject": null,                     "mode": null,       "kind": "code"},
  "review":   {"inject": "inject/review.patch",    "mode": "worktree", "kind": "answer"},
  "summary":  {"inject": null,                     "mode": null,       "kind": "answer"}
}

tasks.json on GitHub

- `ruff.toml`: config lint terpasang untuk tugas lint.

ruff.toml

# Pinned lint config for the `lint` task: a small explicit rule set.
# Upstream tinydb @ 18d73a1 is clean under it (the two per-file ignores cover
# pre-existing F401s: the __version__ re-export and middlewares' Optional).
[lint]
select = ["F401", "F541", "F841", "E722"]

[lint.per-file-ignores]
"tinydb/__init__.py" = ["F401"]
"tinydb/middlewares.py" = ["F401"]

ruff.toml on GitHub

Perubahan yang ditanam:

  • inject/tests.patch: membalik perbandingan di Query.__ge__ agar satu tes gagal.

    inject/tests.patch

diff --git a/tinydb/queries.py b/tinydb/queries.py
index cdab9fb..db00a2a 100644
--- a/tinydb/queries.py
+++ b/tinydb/queries.py
@@ -322,7 +322,7 @@ class Query(QueryInstance):
         :param rhs: The value to compare against
         """
         return self._generate_test(
-            lambda value: value >= rhs,
+            lambda value: value > rhs,
             ('>=', self._path, rhs)
         )
 

inject/tests.patch on GitHub

- `inject/lint.patch`: menambahkan config lint terpasang dan lima temuan.

inject/lint.patch

diff --git a/ruff.toml b/ruff.toml
new file mode 100644
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,6 @@
+[lint]
+select = ["F401", "F541", "F841", "E722"]
+
+[lint.per-file-ignores]
+"tinydb/__init__.py" = ["F401"]
+"tinydb/middlewares.py" = ["F401"]
diff --git a/tinydb/operations.py b/tinydb/operations.py
index 7101a23..f8541fe 100644
--- a/tinydb/operations.py
+++ b/tinydb/operations.py
@@ -10,6 +10,7 @@ This would delete the ``foo`` field from all documents where ``foo`` equals 2.
 
 
 from collections.abc import Callable, MutableMapping
+import sys
 from typing import Any, Union
 
 
@@ -18,7 +19,7 @@ def delete(field: str) -> Callable[[MutableMapping], None]:
     Delete a given field from the document.
     """
     def transform(doc: MutableMapping):
-        del doc[field]
+        removed = doc.pop(field)
 
     return transform
 
diff --git a/tinydb/queries.py b/tinydb/queries.py
index cdab9fb..489a2be 100644
--- a/tinydb/queries.py
+++ b/tinydb/queries.py
@@ -227,7 +227,7 @@ class Query(QueryInstance):
         :return: A :class:`~tinydb.queries.QueryInstance` object
         """
         if not self._path and not allow_empty_path:
-            raise ValueError('Query has no path')
+            raise ValueError(f'Query has no path')
 
         def runner(value):
             try:
diff --git a/tinydb/table.py b/tinydb/table.py
index f4027bf..eb8058c 100644
--- a/tinydb/table.py
+++ b/tinydb/table.py
@@ -818,7 +818,7 @@ class Table:
 
         try:
             raw_table = tables[self.name]
-        except KeyError:
+        except:
             # The table does not exist yet, so it is empty
             raw_table = {}
 
diff --git a/tinydb/utils.py b/tinydb/utils.py
index 43cad4e..07114d5 100644
--- a/tinydb/utils.py
+++ b/tinydb/utils.py
@@ -2,6 +2,7 @@
 Utility functions.
 """
 
+import os
 from collections import OrderedDict, abc
 from collections.abc import Iterator
 from typing import TypeVar, Generic, Union, Optional, TYPE_CHECKING, cast

inject/lint.patch on GitHub

- `inject/review.patch`: diff fitur dengan edge case `if limit:` untuk di-review.

inject/review.patch

diff --git a/tinydb/table.py b/tinydb/table.py
index f4027bf..4e06ba1 100644
--- a/tinydb/table.py
+++ b/tinydb/table.py
@@ -244,10 +244,21 @@ class Table:
 
         return list(iter(self))
 
-    def search(self, cond: QueryLike) -> list[Document]:
+    def search(
+        self,
+        cond: QueryLike,
+        *,
+        sort_by: Optional[str] = None,
+        reverse: bool = False,
+        limit: Optional[int] = None,
+    ) -> list[Document]:
         """
         Search for all documents matching a 'where' cond.
 
+        Results can optionally be sorted by a field (``sort_by``, descending
+        with ``reverse=True``; documents without the field always come last)
+        and truncated to the first ``limit`` results after sorting.
+
         Note: a repeated search with the same ``cond`` is served from an
         internal cache. The returned list is a fresh copy, but its
         documents are the same objects held by the cache -- treat them as
@@ -255,8 +266,34 @@ class Table:
         what a later identical search returns.
 
         :param cond: the condition to check against
+        :param sort_by: optional field name to sort the results by
+        :param reverse: sort in descending order
+        :param limit: optional maximum number of results
         :returns: list of matching documents
         """
+        if limit is not None and limit < 0:
+            raise ValueError('limit must not be negative')
+
+        # The cache always holds the plain, unsorted, untruncated result;
+        # _search returns a fresh list we are free to reorder/slice
+        docs = self._search(cond)
+
+        if sort_by is not None:
+            field = sort_by
+            present = [doc for doc in docs if field in doc]
+            missing = [doc for doc in docs if field not in doc]
+            present.sort(key=lambda doc: doc[field], reverse=reverse)
+            docs = present + missing
+
+        if limit:
+            docs = docs[:limit]
+
+        return docs
+
+    def _search(self, cond: QueryLike) -> list[Document]:
+        """
+        Plain search backed by the query cache. Returns a fresh list.
+        """
 
         # First, we check the query cache to see if it has results for this
         # query

inject/review.patch on GitHub

Prompt dan ground truth:

  • tasks/explore.md: di mana cache query diinvalidasi dan method publik mana yang memicunya.

    tasks/explore.md

Where does TinyDB invalidate its query cache, and which public methods trigger it? Give file paths and function names.

tasks/explore.md on GitHub

- `tasks/tests.md`: pytest gagal, perbaiki bug library, jangan sentuh tesnya.

tasks/tests.md

pytest fails on this repo. Find the bug in the library code and fix it. Do not change the tests.

tasks/tests.md on GitHub

- `tasks/lint.md`: perbaiki setiap temuan ruff tanpa mengubah perilaku.

tasks/lint.md

ruff reports issues in tinydb/ (run `ruff check tinydb`). Fix them all without changing behaviour.

tasks/lint.md on GitHub

- `tasks/commit.md`: tulis pesan commit untuk perubahan yang di-stage.

tasks/commit.md

Write the commit message for the staged changes. Output only the message.

tasks/commit.md on GitHub

- `tasks/refactor.md`: fitur operasi push dan pull.

tasks/refactor.md

I keep writing little lambdas to append things to list fields, so can you add two update operations to `tinydb.operations`, next to `increment`, `delete` and friends? `push(field, value)` should append `value` to the list stored in `field`, keeping the existing order, and `pull(field, value)` should remove every occurrence of `value` from that list, not just the first one. If a document doesn't have the field yet, `push` should create it as a one-element list, while `pull` should just leave that document alone without raising. If the field exists but isn't a list, `push` has to raise a `TypeError` instead of doing something weird with it. They should be used exactly like the existing operations, e.g. `db.update(push('tags', 'python'), User.name == 'john')`.

tasks/refactor.md on GitHub

- `tasks/review.md`: review perubahan yang belum di-commit untuk bug dan edge case.

tasks/review.md

Review the uncommitted changes for bugs and edge cases. List findings only.

tasks/review.md on GitHub

- `tasks/summary.md`: rangkum organisasi repo dalam paling banyak 150 kata.

tasks/summary.md

Summarize how TinyDB is organised for a new contributor in at most 150 words: the main modules and what each owns.

tasks/summary.md on GitHub

- `tasks/explore.truth.md`: ground truth dan aturan pencocokan untuk pengecekan explore.

tasks/explore.truth.md

# explore: ground truth (tinydb @ 18d73a1)

- The query cache is `Table._query_cache`, an `LRUCache` (tinydb/utils.py) created in
  `Table.__init__` (tinydb/table.py, `query_cache_class = LRUCache`, capacity
  `default_query_cache_capacity = 10`). It is filled in `Table.search`.
- Invalidation happens in one place: `Table._update_table()` (tinydb/table.py) ends with
  `self.clear_cache()`, and `Table.clear_cache()` does `self._query_cache.clear()`.
- Every public write goes through `_update_table`, so these trigger it:
  `insert`, `insert_multiple`, `update` (all three branches: callable/fields, by doc_ids, by cond),
  `update_multiple`, `upsert` (via `update` or `insert`), `remove` (by cond or doc_ids), `truncate`.
  `Table.__init__(persist_empty=True)` also calls it. `clear_cache()` itself is public.
  `TinyDB` (tinydb/database.py) forwards these via `__getattr__` to the default table;
  `drop_table`/`drop_tables` do not clear a cache, they forget the `Table` instances (`self._tables`).

## Check rule (check.py explore)
PASS iff the final answer
1. contains `table.py`, AND
2. contains `clear_cache` or `_query_cache`, AND
3. names at least 3 distinct methods among
   insert, insert_multiple, update, update_multiple, upsert, remove, truncate (word match, case-insensitive).

tasks/explore.truth.md on GitHub

- `tasks/review.truth.md`: ground truth dan aturan pencocokan untuk pengecekan review.

tasks/review.truth.md

# review: planted defect and matching rule

The uncommitted change is the reference T4 patch (`Table.search(cond, *, sort_by, reverse, limit)`)
with one defect: the truncation guard is `if limit:` instead of `if limit is not None:`, so
`search(cond, limit=0)` returns ALL matching documents instead of `[]` (0 is falsy).
Everything else in the change is correct (missing-field docs last in both directions,
negative limit -> ValueError, cache keeps the plain result).

## Check rule (check.py review)
PASS iff the final answer (case-insensitive) matches at least one of:
- `limit\s*=?=\s*0` (e.g. "limit=0", "limit == 0")
- `limit\s+(of\s+)?(0|zero)\b` (e.g. "limit of 0", "limit zero")
- `\b(0|zero)\s+limit` / `zero[- ]limit`
- `limit` within 150 characters of `falsy|falsey|truthy|truthiness` (either order)
- `if limit:` together with `0` or `zero` within 150 characters
Decoy answers (generic review, or only the sort order) must FAIL.

tasks/review.truth.md on GitHub

Lakukan ini hari Senin

  • Jalankan Claude Code 2.1.257 atau lebih baru, agar flag force ada, dan 2.1.243 atau lebih baru, agar /tasks menampilkan model tiap subagent.
  • Putuskan file mana yang kamu mau: file Explore untuk explorer saja, blok env untuk setiap subagent.
  • Simpan Explore.md sebagai .claude/agents/Explore.md, atau gabungkan blok env ke .claude/settings.json.
  • Pakai id lengkap claude-haiku-5-5, bukan alias haiku, terutama di Bedrock, Vertex, atau Foundry.
  • Mulai sesi, minta pencarian codebase lewat agent Explore, lalu buka /tasks dan baca model serta effort yang dijalankannya.
  • Tetap pakai Sonnet 5.5 atau Opus 5.5 untuk subagent yang mengerjakan coding agentik yang kompleks, dan jangan setel FORCE jika kamu sengaja memasang agent seperti itu.
  • Jalankan ulang ketujuh tugasmu sendiri lewat harness sebelum mempercayai tabel kami di repomu.
  • Baca /usage sebagai "skill atau agent mana yang menghabiskan riwayat lokalku", bukan sebagai ramalan kuota.

Pengukuran

Meter plan, dibaca dari rate_limit_event setiap run headless, dengan resolusi satu poin. Setiap blok berkesinambungan, tanpa ada yang lain menyentuh akun. Jendela mingguan terbaca 67% pada ke-182 run.

Blok Run Lima jam sebelum Lima jam sesudah Mingguan sebelum Mingguan sesudah Harga list
Haiku 5.5 low 70 21% 21% 67% 67% $0,2382
Haiku 5.5 medium 70 21% 21% 67% 67% $0,3006
Sonnet 5.5 21 21% 22% 67% 67% $1,2339
Opus 5.5 21 22% 23% 67% 67% $2,3175

Ringkasan matriks: 182 run, seluruh matriks sekitar $4,09 dalam harga list, dua poin dari jendela lima jam dan nol poin dari minggu itu. Setiap blok membawa pembulatan hingga satu poin, jadi satu poin milik Sonnet dan satu poin milik Opus tidak bisa dibedakan. Tugas terbatas tidak terlihat di meter ini pada model mana pun. Halaman dukungan tentang biaya relatif per giliran ketiga model tidak memberikan pengali s9.

Dibandingkan dengan rincian lokal, per config:

Config Run Diterima Harga list Heuristik /usage (juta unit) Poin lima jam
haiku-med 70 100% $0,3006 29,48 0,0
haiku-low 70 100% $0,2382 23,91 0,0
sonnet 21 100% $1,2339 16,15 1,0
opus 21 100% $2,3175 27,30 1,0

Rincian /usage dihitung dari riwayat lokal di mesinmu s3. Rumusnya, dibaca dari biner klien, adalah tier model dikali bobot record. Tier model: Fable 10, Opus 5, Haiku 1, selain itu 3. Bobot record: token cache 1, input tanpa cache 10, pembuatan cache 12,5, output 50. Ini adalah bobot atribusi lokal klien, bukan tagihan, dan bukan bukti cara server menghitung kuotamu. Selisihnya terlihat: blok Haiku medium mendapat skor 29,48 dan blok Opus 27,30, jadi panel menempatkan blok Haiku di atas blok Opus, sementara meter sungguhan terbaca nol lawan satu poin.

Ke mana token log sebulan satu akun pergi: 2.631 run subagent, 42 di Haiku 4.5, subagent pada 48,1% dari semua token, Haiku 4.5 pada 0,27% dari biaya terbobot, dan median launch prompt subagent 47.117 token apa pun modelnya.

Effort mengubah nafsu makan pada fitur diterima yang sama (refactor): Haiku medium membaca 232.985 token cache-read dalam 13,7 giliran, Haiku low 139.801 dalam 7,6 giliran, Opus 87.612 dalam 8,3 giliran.

Lebih lanjut

  • Tebing harga 100k: di atas 100.000 token prompt, tier yang lebih tinggi berlaku untuk seluruh request, bukan untuk kelebihannya s8. Apakah token cache dihitung ke dalam 100k adalah pembacaan penulisnya, bukan aturan yang dikutip. Itu penting untuk ekonomi API key, bukan untuk meter langganan. Halaman harga: s20.
  • Tokenizer: teks yang sama dihitung sekitar 30% lebih banyak token di Haiku 5.5 dibanding Haiku 4.5, dan adaptive thinking aktif secara default di API dengan effort sebagai tuasnya s12.
  • Effort dan token per tugas: satu run independen mengukur sekitar 162k token output per tugas Intelligence Index pada effort max, dan sekitar 55k pada effort high untuk skor 38 s7.
  • Setup praktisi lengkap, dengan angkanya dan catatannya sendiri bahwa penghematan datang dari konteks utama yang kecil: s4.
  • Issue terbuka #100082, model frontmatter subagent diabaikan di beberapa build: s10.
  • Issue terbuka #100154, transkrip subagent melaporkan output_tokens terlalu kecil, 461 lawan 15.575 untuk brief yang sama, tagihan sisi server tidak terpengaruh s11. Karena /usage mengalikan output dengan 50 dari riwayat lokal, kekurangan hitung ini sampai ke panel.
  • Tidak diuji di sini: Haiku sebagai model utama (sumber-sumber membingkai Haiku 5.5 sebagai subagent), campuran model lintas provider lewat gateway (bukan pengaturan Claude Code), prompt konteks 1M, dan kredit API yang dibundel dengan plan Max (Claude Platform, bukan kuota Claude Code).

FAQ

Apakah menyetel CLAUDE_CODE_SUBAGENT_MODEL menaruh Explore di Haiku?

Tidak. Sendirian, ia tidak mengubah subagent Explore dan Plan bawaan s2. Tambahkan file agent Explore, atau flag force.

Apakah FORCE akan merusak subagent Opus saya?

Ia menerapkan satu model ke setiap subagent, jadi ya: agent yang sengaja kamu pasang ke Opus juga berjalan di Haiku. Pakai file Explore jika hanya explorer yang perlu murah.

Apakah Haiku 5.5 cukup baik untuk pekerjaan subagent?

Untuk ketujuh tugas terbatas ini pada satu repo kecil, ia diterima 70 dari 70 kali di effort low dan 70 dari 70 di effort medium. Anthropic sendiri menyimpan Sonnet 5.5 dan Opus 5.5 untuk coding agentik yang kompleks s1, dan selisih Terminal-Bench 4.0 adalah 39,2% lawan 70,6%.

Berapa banyak kuota plan yang dikembalikan oleh peralihan ini?

Untuk tugas terbatas, kurang dari satu poin meter di model mana pun: seluruh matriks 182 run menggerakkan jendela lima jam dua poin dan jendela mingguan nol. Tempat seminggu yang sebenarnya habis adalah konteks utama dan launch prompt yang dibawa setiap subagent.

Bisakah saya mematikan thinking untuk menghemat lebih banyak?

Tidak di model-model ini di Claude Code s21. Effort adalah tuas yang tersisa.

File