I ran two small local models as build engines in my headless dispatch system — qwen3.6:35b and gpt-oss:20b, driving real GTD tasks under Claude Code — and neither one finished anything — not the hard tasks, and not the easy ones. They'd pick up an item, decide they needed to understand the codebase, and research it. Then research it more. The GTD task would fill with comments: researching codebase… researching codebase… researching codebase. They'd blow the context window, restart cold, and do it again, until the wall clock killed the run and the dispatch worker gave up. Hours of compute, zero merged commits. The work was, in some real sense, getting done inside those loops — and it never once said so.
That's a completed workout with a blank text box where the "done" should be. If you read the last post in this series, you know the image. This is that image at agent scale — and finish discipline turned out to be the last failure standing after everything else got solved, frontier and local alike: not the reasoning, the handoff.
Here's the sharper version of the same thing, and it's the reason I finally wrote this down. Earlier today my own Rust harness — the one I've been building across these posts, named Talos — failed to build a feature. Twice. Same task, same model, back to back. Then I handed the identical model weights to a different harness and it one-shot the whole thing clean. Same model, different machinery around it, and two failures became one success. That's the controlled experiment I've owed you since the last post — the same weights swapped between two harnesses, not a model that got smarter between attempts.
The control I admitted I ducked
In "It's Still Not the Model" I claimed the harness — not the model — is the variable almost nobody isolates, and then I said, plainly, that I hadn't run the clean control. Same weights, two harnesses, everything else held still. I ran directional evidence and called it directional. That debt has been sitting there. Today it got paid, not because I set out to pay it, but because the work forced it.
The task was the first item of the 0.4.0 bounded-autonomy wave: a finish-recovery protocol. Eighteen acceptance criteria across five files — a new RecoveryFacts schema type plus the loop-detection and nudge control flow that uses it. Real multi-file work, the shape of thing a dispatch engine actually has to do. I dispatched it to talos-glm: my young harness, driving glm-5.2:cloud on Ollama's cloud.
It failed. Then it failed again. The first run hit the iteration cap — MaxIterations at 24 — in about forty-eight seconds, never reaching a single verify cycle. The second spun all the way to fifty-six iterations, quit as StoppedWithoutFinish, and preserved no work in progress. Whatever it had built evaporated with the run.
So I pivoted. Same glm-5.2:cloud weights, but running inside Claude Code — a mature harness instead of my days-old one. It one-shot the entire eighteen-criterion item. Verified Done, all gates green, in about twenty-seven minutes. Commit 758cf4a on r7-research: 366 tests green, 97.72% coverage. Zero Anthropic spend, because GLM billed against Ollama cloud credits the whole way. I wrote the verdict straight into the commit message: after talos-glm failed it twice (MaxIterations@24, StoppedWithoutFinish@56): the gap was the harness, not the model.
There's an alibi that rules the model out cold. GLM scores 24/24 on my hardened eval ladder — tied with Sonnet on pass rate and ahead of it on efficiency. "The model wasn't capable enough" was never a credible reading of those two Talos failures. The weights that failed twice were the same weights that top my leaderboard.
And there's an irony that clinches it. Talos couldn't build finish-recovery precisely because Talos lacked finish-recovery. That second failure — fifty-six iterations, no work preserved — is a live demonstration of the exact two gaps the item exists to close: no nudge to force the completion, no preservation of good work when the model won't call it. The harness failed in the shape of the bug it was being asked to fix.
Two caveats before this gets oversold, because a result this convenient earns scrutiny. GLM is one model. This is a clean single-model control — same weights, two harnesses, opposite outcomes — not a full harness × model grid; I'm not claiming I mapped the space. And Talos's 24-iteration default was a blunt cap I'd tuned for tiny single-crate dogfood items; it blew out in forty-eight seconds on a five-file task before ever reaching verify. That cap is a harness design flaw, not a model limit or a safety feature, and I fixed it the plain way — bumped the default from 24 to 500 in d59b1d9. State the embarrassing part out loud; that's the deal.
The bottleneck is the last tool call
Name the bottleneck precisely, because it is not where people look.
The model does the work. The gates go green. The sealed holdout goes green. And then it never calls finish. It runs to the iteration cap and gets recorded as Failed. A real Done, mislabeled. Finish discipline was the universal residual failure across every one of the five models I measured before 0.4.0 — frontier and local alike, it was the last thing left standing after everything else got solved.
The tell that this is a harness gap and not a model gap: the identical failure shows up under claude -p dispatch too. When two unrelated model families fail the same way, that's a broken layer of the workflow they're both running inside, not an incapable model. This is the StoppedWithoutFinish I flagged in the last post as an unsolved "protocol-discipline failure" and left hanging at both ends of the capability spectrum. Here's the debt being paid.
The distinction that matters for the fix: the completed work is materialized correctly. The tree is right. The tests pass. Only the upward handoff is missing. It's a diligent junior who did the whole task and won't say the words "I'm done."
Force the handoff, never fake the claim
So the fix has one governing invariant, and it's the harness's oldest one: the harness never fabricates Done.
finish(done) is a claim. When the agent calls it, the harness runs the gate itself — cargo test against the acceptance criteria — and constructs a Done value only if it comes back green. Rust makes this structural: a Done is not constructible without its CheckReport evidence. The type carries its own proof, and only the harness ever writes the verified state. The agent literally cannot hand-author a green result. So whatever finish-recovery does, it is not allowed to invent completions.
What it does instead is force the handoff. Detect a green-and-static spin — the last run_checks came back green and the working tree hasn't changed for K iterations (K around 3) — and nudge the model: call finish(done) now, or give me a one-sentence status. Detection is deliberately high-precision. A model still actually working mutates the tree; a legitimately long task hasn't gone green yet. You only trip when work is demonstrably finished and demonstrably idle.
If the model still won't claim after N nudges (N around 2), the run terminates Failed{FinishDiscipline} — but it carries recovery facts out with it: gates_green_at_exit, tree_dirty, nudge_statuses. The worker reads those and preserves the WIP branch instead of deleting it. Because deleting a bad branch is cheap, and discarding hours of verified-green work on a signaling technicality is expensive. The claim moves up exactly one rung: if the model won't make it, the human lead gets an evidence-bearing package instead of a shrug. The harness never writes the Done the model didn't earn — it just refuses to throw away the work the model did.
The eval found the hole in the fix
Then I measured it, and this is the part that makes the post more than a victory lap.
I ran finish-recovery across the full six-model matrix — 144 trials — and it fired zero rescues. Not one. Zero FinishDiscipline terminals across every model. The fix fired zero times on the very suite I built to measure it. The null result is the finding. Finish-recovery catches models that spin — keep calling tools, green and tree-static for K iterations. It does not catch models that stop cold — emit a single no-tool-call turn and halt one iteration below the K-static threshold, before the nudge ever arms. Those two failure shapes look identical in the outcome column (StoppedWithoutFinish, work lost) and are completely different in the loop. My fix covered one of them.
Which one, and how many? That was undetermined in the matrix writeup — until a one-line instrument settled it. RunStats.gates_green_at_exit, mirroring the loop's last_gate_green at exit: seventy lines in engine.rs, seventy-four insertions across three files, commit bafb6ed. With it, I re-ran gpt-oss:20b — a fresh run, so its own StoppedWithoutFinish count — and classified its cold stops. Of fourteen StoppedWithoutFinish trials, six — about 43% — were post-green. Verified-green work, quit on and abandoned. The other eight stopped before the gate ever went green, which is honest non-convergence, nothing to rescue.
That ~43% scoped the next fix precisely: extend the nudge to also fire at the StoppedWithoutFinish terminal when the gate is green. Quantified upside — roughly double gpt-oss's pass rate on this run, six recoverable of twenty-four. I groomed it and dispatched it the same day. One caveat: gpt-oss:20b is high-variance across runs — six of twenty-four here, ten or eleven other times — so the robust signal is the post-green proportion, not any single count. And the stop-nudge extension is fresh: gate-green, built today, not yet battle-tested across a fleet. This is a scoped next step. I haven't shipped it yet.
The matrix
The hard evidence, on the 0.4.1 harness: eval config v5, every row: CODING_EVAL_K=3, max 24 iterations, eight fixtures — 24 trials per model, 144 total. Every "done" verified by the harness running the gate itself.
| model | where | pass^k | holdout | false dones |
|---|---|---|---|---|
claude-opus-4-8 |
Anthropic API | 24/24 | 12/12 | 0 |
claude-sonnet-4-6 |
Anthropic API | 24/24 | 12/12 | 0 |
claude-haiku-4-5 |
Anthropic API | 24/24 | 12/12 | 0 |
glm-5.2:cloud (~756B) |
Ollama cloud | 24/24 | 12/12 | 0 |
qwen3.6:35b |
localhost Ollama | 24/24 | 12/12 | 0 |
gpt-oss:20b |
localhost Ollama | 10/24 (0.417) | 8/12 | 0 |
Zero false dones across all 144 trials. That number is the choice/value boundary holding under load — because the harness only ever binds the value and never fabricates the claim, no run can report a Done it didn't verify. The one model the ladder still kicks is gpt-oss:20b; its v5 failures break down as ten StoppedWithoutFinish plus four MaxIterations.
Two things stand out. Efficiency is not capability: on the calc fixture, mean iterations run glm 4.00 < opus 7.33 < sonnet 9.00 < qwen 11.33 < haiku 15.33 — Haiku passes everything Opus does and takes more than twice the steps to get there. And the cap itself is load-bearing: Haiku's taskdeck run (the tier-4 write-your-own-tests fixture) averaged 19.0 iterations against the cap of 24 and burned 465k tokens. It would have failed outright at the old hardcoded cap of 12. The number in the config is a design decision that decides who passes.
The standing caveat: these fixtures are still much smaller than dispatch-scale GTD work. This is not the apples-to-apples rematch, and the small models did not become viable dispatch engines. Watch the gap between this table and the researching codebase… death spiral at the top of the post — same models, and the fixture is the reason one row reads 24/24 and the real dispatch read zero.
Finish is a Pick
All of this is one shape.
In "The Bounded-Choice Cascade" I named the mechanism under an orchestration fix: the reasoner never holds the pen on a value, only on a choice. Selection down, envelope up. I won't re-derive it — I want to extend it to the one boundary that post didn't touch: completion.
finish(done) is the executor's Pick. The model selects from a bounded disposition set — done, blocked, failed — and the harness never lets it emit the completion value. That's the Cascade verbatim, at the last rung: the pen is on a choice, not on "this is verified." The harness binds the value by running the gate itself; the disposition pick flows down into cargo test, and an evidence-bearing Done flows back up to the lead, who can merge or delete within it but can't re-litigate it. Selection down, envelope up — in a single call. The disposition-to-exit-code map is the concrete surface of that Pick: 0 verified Done, 10 Blocked, 20 Failed, 1 infra. Ratified back at 0.3.5, untouched by 0.4.0. No new exit code, because the shape didn't need one.
Now read the finish-discipline failure through that lens. It's the executor botching the envelope-up handoff. It materializes the whole thing — gates green, tree static, work done — and then just never hands it back. The completed workout, the blank text box. And finish-recovery closes it the Cascade way: it forces the handoff without ever making the pick for the model. If the model still won't claim, the claim moves one rung up to the human lead. The harness never fabricates a Done — which is exactly why zero-false-dones holds. Forcing the handoff and faking the claim are opposite moves, and the whole design is built to do the first and forbid the second.
And "the executor is a role, not a thing" stopped being a slogan today. Hold the model constant — GLM — swap the harness — Talos for Claude Code — and the outcome flips from two failures to a clean one-shot. The executor is a substitutable role, bounded by the spec and the harness around it. I asserted that last post. This post it's a measurement.
The factory that caught the trap
One more thing, because it's the reason I trust the fix and not just the story.
The stop-nudge extension in this very post was groomed by an adversarial critic workflow — draft, critic, synthesize — and that critic caught a trap CI could never have seen. The completion nudge has to be a fresh user message at the terminal, not an append onto the trailing message. Append it wrong and the nudge is silently dropped — and every mock test still passes, because the mocks don't model message boundaries. A green suite over a dead feature. The exact failure this whole post is about, one level up: the work looks done and never actually hands off. The critic flagged it before a line was written; then the fix went out to a cheaper model, claude-code-sonnet, to build. The instrument that measured the gap was a one-line instrument the loop's own tests pin.
0.4.0 shipped with 389 tests and about 97% coverage, gates before code, the way this repo has run since its first commit was the quality-gate scaffold rather than any harness logic. And claude-code-glm is now a proven zero-Anthropic-spend lane for complex, multi-file harness-core work — it shipped that 366-test, 97.72%-coverage finish-recovery change without touching the Anthropic account, because GLM runs on Ollama cloud credits. The cheap executor, doing real work, inside a harness that forces it to hand the work back.
Four posts, one variable
This is the fourth post on one idea. Name the stack. "It's Not the Model" named the disease: most AI failures are context or orchestration failures, not capability. "It's Still Not the Model" proved the harness is the uncontrolled variable — but admitted it never ran the clean control, and flagged StoppedWithoutFinish unsolved. "The Bounded-Choice Cascade" named the mechanism: pen on a choice not a value, selection down, envelope up, the executor as a role. This post ran the control the second one ducked, and solved the failure it flagged — the mechanism, landed on the completion boundary.
Read in order, it's one variable getting steadily isolated: named in theory, then shown to be uncontrolled, then given a mechanism, and now pinned by a control clean enough that the diagnostic is a measurement rather than a hedge about method. The remaining caveat isn't about how I held the variables still — it's just that I held one model still, and the grid is bigger than one row.
The honest coda, because the fix is not a free lunch. The harness forces the handoff and binds the value, but it's only ever as good as the finish contract on top and the gate underneath. A green gate on a weak spec still ships a mislabeled truth — claim-vs-verify is exactly as honest as what "verify" actually runs. The harness supplies the discipline. It does not supply the spec, and it can't verify what the gate doesn't check.
But the diagnostic is no longer a claim I'm asking you to take on faith. When a capable model stalls with the work done, suspect the harness before the model — I held the weights still and swapped the machine, and the machine was the answer.
So here's the one to keep in your pocket. When your executor goes quiet with the work already done, that's not a model that failed. That's a handoff the harness forgot to force.