DEV Community

Stephan Miller
Stephan Miller

Posted on • Originally published at stephanmiller.com on

The Bottleneck Was Me: How I Stopped Racing My AI Builder and Started Pacing It

The last post ended on a clean little problem. I’d gotten my PLAN.md lean: the decisions up top, the history filed away, a skill that keeps it that way so I don’t have to. The plan was finally generating work faster than I could build it. And that exposed a new bottleneck on the build side: my setup hands one task at a time to a background Sonnet agent and waits. The builder finishes, then sits there idle while I plan the next one. So I asked the next question: how do you keep the builder always busy?

The goal turned out to be a decent description of the symptom and a terrible description of what I actually wanted. This is the post where I figured out the difference, by doing it backwards first.

This is part three of a series about working with AI coding agents using a single living PLAN.md instead of vibe coding or spec-kit ceremony. Post one is the thesis. The doc is the deliverable, the code is the byproduct. The last post was about keeping that doc from turning into a 28,000-word novella.

The Batch That Hooked Me

The seed of this whole idea was a session a few weeks back where I did something different. Instead of speccing one hardening task, launching it, and waiting, I lined up a batch of small mechanical jobs and drove them back-to-back as one verified sequence. Each one: Sonnet builds it, I re-read the diff and the tests, then I launch the next. The test count climbed 140 → 234 across the run.

Driving them as a batch freed up enough of my attention that I was working a second project at the same time, jumping back and forth between this content pipeline and my other repo. The fact that the builder grinding away in the background bought me time to go be useful somewhere else.

The Batch That Hooked Me

I filed that away as a direction and went back to feeding the plan. But it nagged at me, because I’d named it wrong in my own head. I called it “keeping the builder busy,” and I built the next experiment around that name.

The Experiment: A Pre-Specified Queue

Here’s how I decided to actually test it. I had a real feature coming up: making the content pipeline able to require embedded artifacts in an article. A brief could say “this post needs at least two of: a code block, a comparison table, a diagram,” and the pipeline would produce them, preserve them through the humanizer, and verify they’re actually there at the end. Four moving parts, real dependencies between them.

So instead of speccing it one task at a time the way I always had, I pre-specified the entire arc up front as an ordered queue. Four tasks. Task A, the schema that declares the requirement, had to land first, because the other three all depend on it. Then B (the writer produces the artifacts), C (the humanizer preserves them), and D (the checker verifies them) could each go independently. I wrote all four specs, mapped the dependency, and flagged the genuine design forks, the questions only I could answer, to settle before anything ran.

The point of pre-specifying the whole thing wasn’t speed. It was to find out where pre-specifying breaks down. Which specs would look airtight on paper and fall apart the moment a builder touched real code.

Then I ran it. And immediately optimized the wrong variable.

I Optimized the Wrong Thing

Task A landed first. It had to; everything blocked on it. But B, C, and D were independent. So Claude decided to run them concurrently.

Three Sonnet agents, in parallel, each on its own task, each on a disjoint set of files. And it worked. All three landed clean. The test suite went from 234 to 328. If you’d asked me to measure throughput, I’d just tripled it. By every number I’d have put on a dashboard, this was the win.

It felt awful.

I Optimized the Wrong Thing

Because the second those three agents came back, they came back together. The background hum that let me work on the other project and do slow human thinking. Gone. I’d taken a process whose entire value was spreading work out over time and I’d compressed it back into a single spike of “everything needs you, now.”

I’d been accidentally optimizing for throughput : get the most build done per unit time. But throughput was never what bought me the second project. What bought me the second project was the gap. Parallelism doesn’t widen that gap. It removes it.

The Gap Was the Point

The builder’s grind is not a cost to be minimized. It’s a resource. A long-enough synchronous build is a gap I spend doing the slow human work: investigating, answering my own open questions, and making the design calls that only I can make. Often on a completely different project. The ideal loop isn’t “builder always busy.” It’s two slow things overlapping in time: the machine grinding through mechanical work while I grind through judgment work. Pacing, not racing.

Once I saw it that way, the design fell out immediately:

  • Synchronous, one-task-at-a-time, is the default , because that’s what manufactures the gap. One builder, working a queue in order, then stopping to report. The stop is a feature. It’s the handoff point where I come back, review, and re-aim.
  • Parallel is a rare opt-in burst, for the specific case where I’m actively babysitting this project and want raw speed more than I want freed attention. But it’s not a default. The session that taught me this lesson proved parallel works. It just also proved it’s the wrong thing to reach for ninety percent of the time.

The Gap Was the Point

The Model: A Serial Queue

So the task handoff file got reshaped. It used to be TASK.md: singular, one task, overwrite it for the next one. Now it’s TASKS.md, and the active task is a serial queue of numbered pieces. The builder works them top to bottom, in one pass, then stops and reports. Finished work doesn’t get overwritten and lost. It collapses into a ✅ Done archive with a stable tag, so the file carries its own history.

Two Ways to Stack

Because once the active task is a queue you can stack as deep as you want, two genuinely different working modes fall out of the exact same mechanism:

Pacing. Stack a couple of pieces. The builder grinds them while I plan the next batch a step ahead. By the time it stops, I’ve got the next chunk ready to go. This is the everyday rhythm, the overlap of machine-grind and human-thinking I’ve been describing the whole post.

Unattended. Stack a lot of pieces (an hour’s worth) and walk away entirely. Go do something else, something not-this-project, and come back to a pile of finished, tested work and one report. Same serial builder, same queue. The only difference is how deep I stack it and whether I’m in the room.

They’re mechanically identical, but the unattended mode forced two rules that the pacing mode never exposed, because pacing rarely runs more than a piece or two before I’m back:

A blocked piece must not halt the queue. If the builder hits piece 3 of 8 and it turns out underspecified (some fork I didn’t see), the old rule was “stop and report.” Fine when I’m sitting right there. Catastrophic when I’m gone for an hour, because pieces 4 through 8 never run even if they don’t depend on the broken one. So the rule is now: mark the blocked piece, skip it, and keep going with anything that doesn’t depend on it. Halt only when nothing left can proceed. An hour away should come back with six of eight done, not two.

Two Ways to Stack

State gets logged on every stop. This is the one that matters most for walking away. Whenever the builder stops, finished or blocked, it writes its state directly into TASKS.md: which pieces are done, which are blocked and why, what’s left, where to resume. It flips a status box on each piece as it goes.

## Design — pieces
- [x] 1. Schema: declare required_artifacts on the brief
- [x] 2. Writer produces the artifacts
- [!] 3. Humanizer preserves them — BLOCKED: heading-protection
       order is ambiguous, needs a call
- [x] 4. Checker verifies presence (depends on #1)

### ▶ Run state
3 of 4 done. #3 blocked on a design fork (see above). Resume there.

Enter fullscreen mode Exit fullscreen mode

Why in the task file and not somewhere clever? Because that file is already what the builder is working in, it survives the session getting killed, and if I come back tomorrow having completely forgotten where I left off, the file tells me. No memory required, mine or the agent’s. The thing that makes unattended runs safe to walk away from is that every piece keeps the test suite green, so “done” actually means done, and the state log means “interrupted” never means “lost.”

The Real Bottleneck Was Never the Task Slot

I’d assumed the constraint on keeping a builder fed was the single-task slot: one TASK.md, one job at a time, obviously that’s the chokepoint. It isn’t. The real bottleneck is spec throughput. The builder only stays fed if there’s a backlog of fully-specified, mechanical tasks waiting for it. The moment a task hits a real design fork, a genuine “should it do A or B” that only I can answer, the queue stalls, no matter how clever the queue is.

And pre-specifying that whole embedded-artifacts arc up front is what made this visible, because it told me exactly where my specs leak. The verdict: pre-specifying nails structure and leaks on judgment. The file decomposition was right. The dependency order (A blocks the rest, B/C/D are independent) was right. What broke was subtler:

The Real Bottleneck Was Never the Task Slot

  • One task rested on a wrong premise. I’d specified the “diagram” artifact as something the pipeline could satisfy with an ASCII drawing. But my actual diagram practice is to render real images, with ASCII only as a fallback. That’s not a fork the builder could catch or resolve. It’s a wrong assumption baked into the spec, and only I could see it, because it lived in my head and not in the code.
  • A judgment call hid inside a “mechanical” task. The artifact checker had to count distinct artifacts, and my supposedly airtight spec quietly let one image get counted twice toward the requirement. The builder implemented exactly what I wrote. What I wrote had a bug. I only caught it because I read the diff.

Which is the callback to post one’s whole “trust but verify” spine, and it’s more load-bearing in a pipeline, not less. The batch doesn’t skip verification. It removes the round-trip with me between tasks, not the rigor. I still read every diff the builder actually wrote. Both of those bugs, the double-count and a separate one where a config value silently broke its file format, got caught at my review, not by the agents. The lesson for the queue is precise: a task is only “ready” when its premises and its judgment semantics are pinned, not just its files. Pinning the files is the easy 80%. The leak is always in the other 20%, and the other 20% is mine.

The Name

The seed of this post had a whole brainstorm in it about finally renaming the skill. Its name describes one of the three things it does and I can never remember it. I had candidates: planwright, for the one who authors plans, and foreman, for the one who keeps the line moving and the builder fed. And given that this entire post is about keeping the line moving, foreman was right there, practically gift-wrapped.

The Name

I did not rename it. I codified the whole convention, ported it into the skill, patched both projects, and left the dumb name exactly where it was. The rename is real work. It’s wired into three repos now, and I wasn’t going to fake-land it just to give this post a tidy bow. So the name is still wrong, foreman is still leading, and it’ll get fixed when it gets fixed. Consider this the third post in a row where I’ve promised to rename the thing.

The Honest Scope Note

Every post gets one. Here’s this one’s: this is the frontier of the method, not a settled result.

The pacing-over-throughput call is fresh. I made it by running the throughput version, hating it, and reasoning backward, which is a strong signal but a sample size of basically one. The unattended mode, with its skip-and-continue and its state logging, is built but barely road-tested; I haven’t actually walked away for a real hour and come back to judge what I found, and I’d bet the skip-and-continue logic has edge cases I haven’t hit. The “promote a recurring judgment into an automatic rule” idea from the last post is still unproven. And the deepest claim here, that spec throughput, not the task slot, is the true bottleneck, is a hypothesis I’ve confirmed exactly once.

What I’m confident about is the shape of the mistake, because I made it cleanly: optimizing throughput when I wanted pacing is a real and seductive wrong turn, and “keep the builder busy” is exactly the kind of goal that leads you into it. The metric that’s easy to put on a dashboard, work per unit time, is not always the one you actually care about. Sometimes the gap is the product.

What’s Next

I don’t know yet, and I’m not going to force a cliffhanger. The plan stays lean, the builder runs on a paced queue, and I can stack it shallow to work alongside it or deep to walk away from it. The pieces are in place. What I don’t have is a real verdict on the unattended mode under fire, or a clue whether the “spec throughput is the bottleneck” insight holds up across more than one feature arc. Those are the next things to actually live with rather than theorize about.

There may be a post four. There may not. The method’s still moving, and I’d rather tell you what actually happened than what would make a clean ending. So far that’s served the series fine. We’ll see what the gap fills with.

Top comments (0)