An eval Day 8 has to beat

Forty tasks in a small fixture repository, each with a check a script can decide, run under three sampling seeds. Run 1 exposed two checks a wrong answer could pass. The fixed v2 checks give stock Qwen3-0.6B its baseline, and it mostly answers without calling a tool.

Day 8 will fine-tune Qwen to use tools. To know whether that worked, we need a fixed test that a script can grade, plus the stock model's score on it. This post builds that test and records the baseline.

The tasks

Forty small jobs in a toy shop

The fixture is a small Python package called shop, with prices, a cart, stock counts, text helpers, a config file, a CSV of customers and 12 passing unittest tests. Every task starts from a fresh copy. There are 40 tasks of four kinds, and each kind has its own check:

KindTasksExample promptPasses when
answer14Which port does the server use according to the config? Reply with the answer.the final answer contains the expected value as a whole token
file9Change the server port in config/settings.ini to 9090.the file matches a pattern and keeps the lines it should
hidden8Add a function clamp(value, low, high) to shop/pricing.py that limits value to the range from low to high. Keep the existing tests passing.a test the model never saw, copied in afterwards, passes
tests9apply_discount returns wrong prices. Run the tests, find the bug, and fix the code. Do not edit the tests.the suite passes and no original test file changed

Each tests task first plants one bug with an exact string replacement, such as percent / 100 becoming percent / 10. The check also requires every original test file to be byte-identical, which blocks the obvious cheat of deleting or weakening the failing test. The hidden kind borrows SWE-bench's idea: grade new code with a test the model never sees.

Checking the checks

Every task proves it can fail and pass

A check that always passes, or never can, quietly ruins an eval. SWE-bench runs every task's gold patch before using the task. octlm does the same with a validate stage that needs no model. Each task has a reference solution written as tool calls. For every task, validate confirms the check fails on the untouched repository. It then runs the reference calls through the real parser and tools, and confirms the check passes. For answer tasks it also confirms some tool output contains the answer, except two counting tasks whose answer no tool prints directly.

40 of 40 tasks passed validation, on the laptop and again on Kaggle before each eval.

octlm/harness.pyline 467
def validate(args: argparse.Namespace) -> None:
    tasks, fixtures = load_tasks(args.tasks), args.tasks.parent
    for task in tasks:
        with sandbox(task, fixtures) as root:
            before = passes(task, root, "", fixtures)
        with sandbox(task, fixtures) as root:
            results = replay(task, root)
            after = passes(task, root, task["solution"]["answer"], fixtures)
        check = task["check"]
        found = (
            check["kind"] != "answer"
            or check.get("derived")
            or any(check["expected"].casefold() in result.casefold() for result in results)
        )
        row = {"type": "task_validation", "task": task["id"], "kind": check["kind"]}
        _write(args.out, {**row, "fails_before": not before, "passes_after": after, "found": found})
        if before or not after or not found:
            raise RuntimeError(f"task {task['id']} failed validation")
    _write(args.out, {"type": "validation_summary", "tasks": len(tasks), "valid": True})
Seeds

Sampling makes one run a coin flip

The harness samples, as Qwen's model card recommends, so the same task can pass on one seed and fail on the next. Each task runs under seeds 0, 1, 2, for 120 runs in all. Two numbers summarize them. pass^1 is the plain success rate. pass^k comes from tau-bench and gives the chance that k independent tries all succeed. For a task with c successes in n tries:

passk=Etask[(ck)(nk)]pass@k=1−Etask[(n−ck)(nk)]\text{pass}^k = \mathbb{E}_{\text{task}}\left[\frac{\binom{c}{k}}{\binom{n}{k}}\right] \qquad \text{pass@}k = 1 - \mathbb{E}_{\text{task}}\left[\frac{\binom{n-c}{k}}{\binom{n}{k}}\right]

pass@k, the HumanEval metric, rewards a model that gets something right once. pass^k rewards one that's reliable. If a fine-tune only makes the model more consistent, pass^k moves first.

Baseline
k
0/338 tasks
1/31 tasks
2/31 tasks
3/30 tasks
pass^30.000all 3 sampled tries succeed
pass@30.050at least one of 3 succeeds

Bars count the 40 tasks by how many of their 3 seeds passed in the baseline. At k = 1 both metrics equal the mean success rate. As k grows, pass^k only counts tasks the model solves every time, and pass@k counts any task it solved once.

The baseline's per-task success counts, and both metrics for each k.
Result

110 of 120 runs never called a tool

Every run
Checks
answerfilehiddentestsseed 0seed 1seed 2
passedcalled a tool, failedno tool callscored differently by v1 and v2
Runs passed3 of 120pass^1 0.025
Runs with a tool call10 of 120v1 (run 1): 10
Picked runpassed1 turns, 0 of 0 calls valid

lamp-stock (answer): How many lamps are in stock according to the inventory module? Reply with the answer.
Final answer: "The number of lamps in stock is 12."

One cell per run: columns are tasks grouped by kind, rows are seeds. Click a cell to read its prompt and final answer. Switch to v1 to see the three passes the first checks got wrong.

Asked which port the server uses, the model doesn't read config/settings.ini. It replies "The server uses port 8080." The config says 8042. It does this on nearly every answer and tests task. It only uses tools when the prompt names a file to write, and even then most of its code arrives as broken JSON.

MetricRun 1, v1 checksRun 2, v2 checks
pass^1 per seed0.075, 0.050, 0.0250.050, 0.025, 0.000
pass^1 mean0.0500.025
Seed standard deviation0.0250.025
pass^30.0000.000
Valid-call rate0.3330.333
Mean turns1.181.18
Success, answer0.0480.024
Success, file0.1480.074
Success, hidden0.0000.000
Success, tests0.0000.000
The first checks were wrong

Read the passing transcripts

Run 1 scored 6 passes. Reading those transcripts turned up two bad checks:

The plan written before the run said the task set freezes, and that any change means a new version and a new run, not an edit. So run 1 stays on record and v2 fixes both. An answer must now match as a whole token: no letter, digit, dot or dash just before it, and no letter, digit, dash or decimal part just after. File edits now also list original lines that have to survive. Run 2 scored the same tasks with the v2 checks. 3 runs changed from pass to fail.

That comparison is unusually clean. 120 of 120 transcripts in run 2 match run 1 token for token. Fixed seeds reproduce sampled generation across two separate T4 sessions, so the scoring is the only thing that changed.

Even v2 has a known blind spot. lamp-stock on seed 0 passed without a single tool call. The model guessed a number and happened to be right. Only 2 of the 3 passes are grounded in a tool result. Every row stores its transcript, so Day 8 can report grounded success next to the raw rate without rerunning anything.

Decision: v2 is the Day 8 baseline. The fine-tuned model has to beat pass^1 0.025 by more than the seed spread of 0.025, on the same tasks, seeds and limits.

Skipped

Not built