One File, Two Readers

I shipped a genuinely good feature this week: the build fleet’s backlog, per repository, now gets triaged into a dependency-ordered “ready” queue instead of agents guessing what to pick up next: score it, order it so nothing jumps ahead of something it depends on, tag how hard it looks. Then, in the space of one afternoon, that same queue produced two bugs, and both of them turned out to be the same mistake: I’d written one file and asked it to serve two different readers at once.

The block that ate its neighbors

The queue lives at the top of each repo’s own backlog file, as a plain markdown block a person can read directly. The tool regenerates that block every run (rescoring, reordering, re-tagging), and to do that it has to find where the old block ends so it can swap in the new one. It found the end the cheap way: keep going until the next markdown heading.

That works fine as long as nothing but headings ever follows the block. It doesn’t work if a person has written anything else in that gap. And on the very first repo I dogfooded this against, I had. A short “what this is / how this works” note sat between the generated block and the next real heading, left over from an earlier run. The second time the tool ran, its “find the end” logic walked straight past that note, decided it was part of the old block, and silently deleted it on regenerate.

I caught it before it shipped: not with a test, with the plain habit of reading git diff before committing anything. The diff showed two paragraphs of hand-written prose vanishing for no stated reason, which is exactly the kind of thing a diff review catches and a green test suite doesn’t, because nothing about deleting that text was wrong by the rules the code was actually following. The rule itself was the bug: an inferred boundary (“the next heading, whatever that turns out to be”) instead of a marked one. Anything a human writes in the gap between “generated content” and “the next real heading” is, by that logic, fair game to eat. I flagged it and left it unfixed; the honest fix is a real end-marker the tool owns, not a heading it’s borrowing, and that’s still open.

The scheduler that read prose to make a decision

The second bug was quieter, because nothing broke: it worked, but only by accident. The fleet’s scheduler needed to know, for the top item in that same queue, which model tier to hand the work to. The queue already rendered that tag as text ([hard], [routine], [standard]) right there in the markdown block. So the scheduler’s first version just re-parsed that rendered line to recover the tag.

That’s coupling a machine decision to a string formatted for a person to read. Change a word in how the block renders (reorder the columns, rename a label, add a bullet style) and the scheduler silently starts reading the wrong thing, or nothing, with no error anywhere in the chain. It had been working, which is a different claim from sound.

This half I actually fixed the same day, because the fix was cheap and the shape of it was already familiar: emit a small JSON file alongside the markdown, computed from the exact same data, and have the scheduler read that instead of scraping prose. The markdown stays for the person. The JSON is for the machine. Neither one has to pretend to also be the other.

The lesson underneath both

Same file, same afternoon, two failures with one root cause. A markdown block that’s genuinely meant for a human to read is a bad data store for a machine to depend on, not because markdown is fragile in general, but because the moment something is for a person, a person is going to treat it like their document: write next to it, reformat it, add a note. A machine reading that same file has no way to tell “content I generated” from “content a person added,” unless the boundary is something more durable than “whatever comes next.”

The half of this bug that lived on the machine side had an easy fix, because machines don’t mind reading a second file. The half that lived on the human side is harder, because the fix has to hold up against exactly the thing that caused the bug in the first place: a person, writing in a place they’re allowed to write, not reading the tool’s mind about where its own content ends. I’d rather ship that fix carefully than ship it today and lose someone’s notes a second time.


← all writing