Cover image for The graph said parallel. The files said otherwise.

The graph said parallel. The files said otherwise.


August 01, 2026

Cover image for The graph said parallel. The files said otherwise.
Hero: split panel, the dependency graph clearing two issues for parallel work on the left, the file map showing they collide on the right

I'm looking at a milestone with eight tracked issues, and I've already drawn the dependency graph. Two of them, right at the top of two separate branches, have no edge between them at all. One adds a relational link between two kinds of records. The other adds the columns for a new advance-payment and credit feature. Different corner of the data model, different reason for existing, nothing in the graph connecting them. My instinct says parallel: two agents, two branches, go.

Then I look at which file each one actually has to open, and it's the same file. The same schema. Underneath that, the same journal counting up the next database migration number, one at a time, in order. Two agents cannot both be writing migration eighteen.

What the graph doesn't show you

A milestone ago I wrote about running coding agents in dependency-ordered waves: one orchestrator holding the plan, worker agents each shipping a single issue, isolated git worktrees so they don't step on each other, a merge and a full re-verify on the main branch before the next wave starts. None of that changed here.

What changed is what decides the waves.

A dependency graph tells you what has to finish before what starts. It doesn't tell you what can happen at the same time. Two issues with zero dependency between them can still be unsafe to run in parallel if they both want to edit the same file; two agents editing the same file at once don't merge cleanly, they collide. A database migration is the sharpest version of this. Two agents each trying to write "the next migration" will grab the same number whether or not either one depends on the other.

So before I lay out any waves now, I map something the graph never captured: which issues touch which files. Any file that two or more issues need to open becomes one lane, one builder, sequential. Only the files nobody else needs are safe to hand to agents running at the same time.

Three waves, mostly serial

On this milestone, that map turned up three shared files, and between them they forced almost the whole plan into single-file lines.

The first wave was the data model, the same pair from a minute ago: the relational-link migration and the advance-payment columns, same schema file, same migration sequence, one agent, two pull requests, in order.

The second wave was the API. Two more changes needed the same route handler: the endpoint that creates a new record, and the logic that automatically applies a credit balance when one exists. No dependency edge between those two either. Same handler, same story. One agent, two pull requests, in sequence.

The third wave was the only one with real parallelism, and only because the two pieces genuinely didn't touch the same code. One agent added new options to an existing data-entry form. A second agent, working at the same time in its own isolated worktree, built a brand-new record detail page from scratch. Different files, different agents, running together. A small follow-on toggle landed after, once the form work it depended on had merged.

Flowchart: three waves, data model and API fully sequential on shared files, frontend forking into parallel lanes on disjoint files before a final sequential toggle

The milestone's own notes had already flagged part of this, without calling it a file collision: build the form work with a single builder who already knows that code, not fanned out across agents. The file map found the same constraint on its own, for the same reason. All three pieces of that form work open the same file.

Eight tracked issues, seventeen smaller checklist items nested under two of them, twenty-five pieces total, all closed by one orchestrator. Seven pull requests. The milestone hit 100% a day ahead of its own target.

The bug that was in the test, not the feature

The new detail page's end-to-end test failed. Twice.

Not a product bug. A test bug: the page rendered a record's name in four different places, and the test's element matcher was written loosely enough to match any of the four, so it kept grabbing the wrong one. The orchestrator read the failure, scoped the test's selectors down to the exact section of the page each assertion actually cared about, pushed the fix, and the suite went green.

I keep coming back to that one. The thing that broke wasn't the code I was worried about. It was the thing checking the code. Worth remembering the next time a red suite looks like a feature problem before anyone's read it closely enough to be sure.

Turning the plan into a tool

By this point I'd planned a few of these milestones by hand: draw the graph, map the files, decide the waves, write out the exact prompts for the orchestrator and for each worker agent. Doing it by hand a few times is how you find out it's the same shape every time. So the planning step itself became a skill. Point it at a milestone in the tracker, and it reads the work, spawns its own read-only agents to confirm which production files each issue actually touches, builds the dependency graph and the file map together, lays out the waves, and writes the whole plan back out: the plan document, the orchestrator prompt, and every per-issue worker prompt, ready to paste into a fresh session.

It also decides, per issue, which model should build it. A schema change or anything touching money math gets routed to the stronger, more expensive model. A mechanical fix with the file and the line already named gets the cheaper one. That judgment used to just live in my head each time I planned one of these; now it's a rule the tool applies the same way on every run.

It only writes the plan. It never runs the build itself. I still read the plan and start the orchestrator by hand.

Comparison: a hand-drawn plan redone from scratch each milestone on the left, a generated plan with orchestrator and worker prompts on the right

The part that told me it wasn't just a script for this one milestone is that I didn't build it to only understand milestones. It also recognizes a second shape of work: not a tracked milestone at all, but a batch of small bugs found during a screen-by-screen test pass, all hanging off one ticket. Same underlying question either way, which files are about to collide, just a different source of work to map it onto.

Staying in the loop

One more thing I only learned by getting it wrong. The orchestrator posts a short update to a monitoring channel at each wave boundary, so I can follow along without babysitting it. The first version of that posted as me. I never saw a single one of those messages land, because a chat service doesn't notify you about your own messages. The fix was almost embarrassingly small: post as a bot account instead of my own. Obvious once I saw it, invisible until the first overnight run I checked on the next morning and realized I'd missed the whole thing.

The next milestone I plan, I won't stare at a dependency graph and guess which lanes are actually safe to run together. I'll run the skill, get the file map back in a few minutes, and spend my own judgment on the two or three calls it flags as genuinely ambiguous, instead of re-deriving the other twenty-two from scratch. The planning didn't disappear. It got smaller, and it moved to the part that actually needed me.


#BuildInPublic #ClaudeCode #AIAgenticEngineer #AIEngineering