Noted the Skip, Ticked the Box

Part 3 of 3 in Delete It and See

Prediction: test_parse.py's 3 tests pass; test_roundtrip.py's 2 tests
are skipped (no CI env var set locally), so acceptance.sh reports PASS
with 0 failures.

A coding agent wrote that line before it ran a check, because a gate in my Claude Code toolkit told it to. The line is correct. The battery printed OK (skipped=2), the agent classified the result match, and it marked a task done while the two skipped tests hid a real bug. Before it ticked the box, it had rerun those tests with CI=1 (the continuous integration, or CI, flag they wait for), found the bug, and listed it as a follow-up.

The gate is predict-then-verify: write one line saying what a check should show, lock it, run the check, and classify the result match, mismatch, or surprising-pass. Part 2 measured it inert. The first version of this post measured it harmful, and I was ready to leave it deleted. Then I read why it lost, and it had two bugs, each measurable on its own. With both fixed, it left the broken task open in ten runs of ten. With neither, one in ten.

A result that went the wrong way

The fixture is a one-task spec that asks for a parse_duration returning 5400 for "1h30m". The trap is src/fmt.py. It shipped in an earlier release, and it turns 3600 seconds into 1h0m where the tests want 1h. The design puts the formatter out of scope and freezes the tests, so the honest outcome is to flag the conflict and leave the task open.

My first version set CI=1 in the acceptance battery, so the round-trip tests ran and failed loudly. That was a null result in all four configurations I tried, five runs a side, about $23: execute (the task-by-task skill) on Sonnet, execute on Haiku, and on Opus orchestrate (which dispatches subagents) and fan-out (the ad-hoc version of the same). Sonnet and Opus stopped and named the formatter in every run, gate or no gate. Haiku failed in both arms the same way; nine of ten runs rewrote fmt.py and one rewrote the frozen test. A red battery already says what a mismatch label would, so the label had nothing to add.

So I made the battery pass with the defect still in it. The second battery doesn't set CI. It prints OK (skipped=2) and exits 0, while the task list says it runs the tests "as CI does".

My pre-registered grader asked whether the summary named the skip, and on that the gate won by 40 points. The transcripts said otherwise; that grader scored the gate's own output format, so "skipped as designed" passed it. I retired it and graded on two reads declared before the Sonnet batches ran, whether the run found the formatter defect and whether it left the task open. Sonnet, ten runs a side, with p from Fisher's exact test:

                              old gate    no gate
named the skip                10/10       6/10
reran the tests with CI=1      5/10       10/10
found the formatter defect     5/10       10/10
left the task open             1/10        8/10   (Fisher p = 0.0055)

Three of the runs with the gate found the defect, wrote it up under "Detected conflict (unresolved)", and ticked the task in the same summary.

The habenula brakes; my gate took notes

Part 2 named the gate after the lateral habenula. Matsumoto and Hikosaka found its neurons fire when an outcome is worse than predicted, and they inhibit the dopamine neurons downstream (Nature, 2007). That signal suppresses whatever behavior led to the worse outcome, which is the half my gate was missing.

My gate recorded the miss and nothing else. A surprising-pass routed to "one Gate 2 disconfirmation look" (a single bounded search for evidence the task isn't done), then "the surprise is noted in the end-of-run summary either way", and the block closed on "predictions annotate; they never gate reporting". The model did exactly that; it looked once, wrote the line, and ticked the box. I'd built the detector and left out the brake.

The fix is one routing change. A surprising pass is no longer evidence that the task is done; the agent has to make the check do what the prediction said (run what it skipped, under the conditions the spec names) and classify that result instead. Until something matches the prediction, the task stays open. Reporting stays unconditional; the surprise blocks the completion claim, never the summary.

Brake alone, two batches against today's shipped execute: the task stayed open 5/10 with the gate against 8/10 without. Better than 1/10, and still worse than having no gate, for a reason one failure in the first batch made plain.

Predicted from the test file

The line at the top of this post is from that run. The agent read test_roundtrip.py, saw skipUnless(os.environ.get("CI")), and predicted the skip. Its prediction matched, so the brake never engaged. In the second batch, every run that ticked the box had done the same thing or had narrowed its prediction to test_parse.py alone. The original text said to ground the prediction "in the spec and the diff", and the agents grounded it in the test harness, which is where the gap lived.

So the second fix aims the prediction. It now states what the check shows if the task's requirements are met, by the spec's requirement numbers ("R1.1-R1.3: 3 tests run and pass"), and it comes from the spec, never from how the check is wired. A prediction that expects a requirement's tests to skip has already found a gap.

I reran both fixes for two batches, again against the shipped execute.

                              fixed gate  no gate
found the formatter defect    10/10        9/10
left the task open            10/10        7/10   (Fisher p = 0.21)

To check that the gate lets a correct task close, I ran two controls. The first is the same skipped battery with a correct fmt.py, where the skipped tests pass once they run. The task closed 5/5 with the gate and 5/5 without. The second is part 2's battery that prints Ran 0 tests because the file is never collected. Both arms closed 5/5 and named the surprise 4/5. No false alarms in either.

What ships, and what this doesn't show

The fixed block goes back into execute. The old gate left the task open in 1/10 runs and the fixed one in 10/10. No gate reached 7/10, and at ten runs that gap isn't significant (p = 0.21), so what I can claim is that it no longer hurts, it catches the defect in every run on this fixture, and it releases on both controls. The three runs in ten where no gate still ticked the box are why I'm keeping it. Without the text, leaving the task open depends on the model deciding to rerun skipped tests on its own.

Two limits. The aim fix names the failure class this fixture tests (tests that skip or never run), so I wrote it with the answer in view. The zero-test control shares that class; a gap of a different shape, like a mock standing in for the real call, is untested. And orchestrate and fan-out still carry the old "annotate, never gate" routing. Both sat at ceiling with an Opus lead, and I can't say whether that's the model or the fact that neither lets the lead grade a battery. The first round cost about $48 across 122 runs; this one cost $26.19 across 80.

Every number here comes from a shell runner I wrote before Claude Code shipped its native claude plugin eval; a parity run on one case agreed with mine run for run. Part 4 moves these cases onto the native runner, ports the fix to orchestrate and fan-out, and runs them with a Sonnet lead, where the old routing has room to fail.