Three hypotheses eliminated on evidence, and what was left standing was the expensive conclusion: the model could locate and describe a bug correctly, every time, and still could not hand-author unified-diff syntax reliably, even with the exact failure modes spelled out in the prompt. That’s not a prompt problem. Character-level diff syntax (exact hunk headers, exact prefix characters on every line, exact line-count math) is a narrow, unforgiving format, and asking a model to hand-type it perfectly on every attempt was asking for a skill the failures said it didn’t reliably have.

So the fix stopped asking for it.

The actual problem: the wrong thing was being generated

The model’s proposal schema had always asked for PatchProposal.diff, a full unified diff, authored by the model, character for character. Every fix attempt so far had tried to make that authoring more reliable: better retry feedback, more explicit anti-pattern warnings in the prompt. All real improvements. None of them addressed the actual gap, because the gap wasn’t in what the model understood. It was in the model’s ability to produce a specific, brittle text format without a single character error.

The redesign replaced PatchProposal.diff with PatchProposal.edits: List[FileEdit], a list of {path, old_text, new_text} triples. The model still does the part it’s good at: finding the exact text to change and writing the exact replacement. It just never has to know what a unified diff hunk header looks like.

What worked: a new module computes the diff, not the model

A new patching.py module, build_patch_from_edits(), takes those exact-match edits and computes the unified diff itself with Python’s own difflib, against the real file content on disk. The model’s output only has to clear a much lower, much more checkable bar: does old_text appear, exactly once, in the target file. The module enforces workspace containment (no absolute or ..-escaping paths), rejects no-op and empty edits, supports new-file creation via an empty old_text, handles the trailing-newline edge case correctly, and applies edits all-or-nothing. When an edit doesn’t match, the error string is written to be fed back to the model verbatim, in the model’s own vocabulary, not git apply’s stdin line numbers, which the earlier live runs had already proven the model couldn’t act on.

The change touched all three call sites that shared the old diff-authoring path, coding.bugfix, coding.refactor, and docs.update, and the computed diff still flows through the same fail-closed dry-run guard and human-gated apply as before. This changes who writes the diff. It does not change who approves it.

The result: a patch finally applied clean

The redesigned schema went live against real Ollama the same afternoon, and the very first real attempt did something no earlier attempt in this workflow’s history had done: it produced a diff that passed apply_patch_dry_run cleanly. The malformed-diff pattern that LCH-1 and LCH-2 had spent two sessions chasing was gone on the first live try.

And the loop still reported a failed test.

Read by hand, the patch looked correct. The model had found the right bug and written a fix that matched it. Something downstream of the dry-run guard didn’t add up, and line by line, it wasn’t the model’s diff.

What next

Removing the diff-authoring requirement didn’t just fix the malformed-diff pattern. It changed what could be observed for the first time, because a patch had never before survived long enough to reach the next stage of the loop. That next stage turned out to have its own bug, hiding in plain sight since the harness’s very first version, invisible only because nothing had ever gotten far enough to trip it.

What Next

The Bug Behind the Bug

Next in the Local Coding Harness arc: fixing the diff-authoring problem let a patch survive for the first time, and immediately exposed a test stage that had been silently checking the wrong tree all along.