The Task Is Not the Unit of Work
A 41-task graph ran for 41 hours, produced 115 reviews, and merged nothing. Then I pivoted mid-flight to milestones, and the same session shipped five PRs in half the time.
Sekou M. Doumbouya
Listen to this article
The views expressed here are my own and do not represent those of any current or former employer.
Forty-one hours into an autonomous run, I typed a question I should never have had to type: is any of this actually merged?
The system was busy. Two hundred five background workflows had come and gone. Agents were implementing, reviewers were reviewing, evidence was being filed against a forty-one-task plan, and the status surface glowed with activity. And the answer to my question was no. Nothing was merged. Forty-one hours of coordinated, independently reviewed, evidence-gated work, and not one pull request had landed on main.
So I stopped it. Mid-flight, mid-session, with the graph half done, I banned implementation subagents and restructured everything that remained into five milestones that the main loop would build directly. The same session, the same codebase, the same model, finished the job in half the wall-clock time and merged five pull requests in sequence. This post is the story of that pivot: where it happened, what the meter said on each side of it, and why the fix is now becoming a feature of my task ledger instead of a lesson I try to remember.
Forty-One Plans, Forty-One Pipelines
The goal was a v2 of the operator CLI for anvil-serving, my local model-serving stack. I did what I always do: wrote a PRD, let Anvil break it into tasks with acceptance criteria and verification commands, and approved the graph. Forty-one tasks. Detailed, testable, individually sensible.
Then the execution loop made the move this whole post is about. It treated the planning graph as the execution graph. Each task got its own claim, its own branch, its own worktree, its own implementation agent, its own review cycle, its own evidence packet, its own integration step. Task granularity became branch granularity became agent granularity became review granularity.
Here is what the meter read when I finally paused it:
wall clock 41.2 h
background workflows 205
implementation task-cycles 30
review workflows 115
review tokens 1,910,000
implementation tokens 876,553
pull requests merged 0
Read the middle of that table twice. One hundred fifteen review runs for thirty implementation cycles. Nearly four reviews for every build. Review consumed more than twice the tokens implementation did. Independent review is the control I trust most in agent work, and I still stand by it, but nobody was budgeting it. A finding would reopen an adjacent task, the reopened task would get re-reviewed, and the cascade would run again. Meanwhile 81% of generated work was successfully delegated away from the main loop, which sounds like an orchestration win until you notice what was delegated: an inefficient policy, executed very efficiently.
Green Tasks, Broken Product
The sharper failure was quieter than the overhead, and it is the one that changed my mind about units.
Each task satisfied its own acceptance criteria. Its checks ran, its evidence was filed, Anvil marked it accepted. And the integrated CLI on the shared branch was broken anyway. The command tree required a --confirm flag that the leaf parsers rejected. The focused help text described behavior the real parser did not have. A route was simply missing. Every unit was green. The product was red.
That is not a testing gap you can patch task by task, because no task owned the seam. Shared CLI files and cross-command contracts are terrible boundaries for dozens of independent workers; the contract lives between the tasks, exactly where a task-scoped check cannot see. The defect got its own issue, and it earned it, because it is the cleanest evidence I have for the principle: a task is not a unit of work. It is a unit of proof.
The Moment It Turned
I went back through the session log to find the exact point where I lost confidence, because the shape of my own messages is the most honest instrument in the whole retro. Sixty-one human messages in this run. The early ones are product decisions: approve the plan, ground the topology, review this design. Then, somewhere past hour thirty, they degrade into something else entirely.
Verbatim from the session log. Sixty-one human messages in the run; these are the eight that tell the story.
- message 32
Have all changes been merged, or do we need to open PRs?
- message 36
is this still running
- message 37
I'm not sure why we can't complete this task. is taking forever to complete
- message 38
How much more do you have to do? Also, I’m seeing a lot of old agents on the screen…
- message 40
What are you doing now?
- message 41 the instrument
I know we’re still in the middle of the session, but can you run a session retro and put your findings in the post-session repo?
- message 42 the pivot
Okay, I think for right now, I want you to complete these last tasks without using any sub-agents at this point. I think that is not working…
- message 48 the new operating model
Okay, let’s go ahead and work on getting these milestones structured. Let’s work on the first one.
Look at what those messages are. They are not product judgment. They are requests for missing telemetry: is it alive, is it merged, what remains, whose agents are these. When I finally asked for an agent audit, fifteen completed agents were still sitting on the coordination surface and seven listed identifiers no longer existed. I was managing an org chart of ghosts.
Message 41 is the tell for how I actually work: before changing anything, I ran a retro mid-session and filed the findings, so the pivot decision would be grounded in the meter instead of frustration. Message 42 is the pivot itself, timestamped at hour 41 of what became a 61.6-hour session. And message 48 starts the new operating model. On the run’s own timeline, the moment is visible in the data:
Sixty-two hours, one session. The dashed line is the pivot: a mid-session retro, then "complete these last tasks without using any sub-agents."
Data table (active hours)
| hour | generated tokens | phase |
|---|
Everything blue is the fan-out phase: heavy, dense, expensive hours with nothing landing on main. Everything orange is milestone delivery. The orange region is visibly calmer, and it is doing something the blue region never did: finishing.
The Pivot Worked
Let me be blunt about the result, because I have seen this framed as “milestones are a compromise” and the data says otherwise. Milestones did not just make the run more pleasant to supervise. They beat the fan-out phase on nearly every measure that matters, in the same session, with the same model, on the same codebase.
I regrouped the remaining work into five milestones: contract repair, canonical commands, voice lifecycle, production closure, stabilization. One branch per milestone. Implementation stayed in the main loop with the coordinator. Agents were used for exactly one thing: independent review of a candidate commit that already existed. Review, fix, merge, next.
Toggle the operating model. The chips are the 41 acceptance tasks; the boxes are delivery cycles, each paying the full claim, branch, implement, review, evidence, integrate overhead.
Half the wall clock. 72% fewer generated tokens. Background workflows down from 205 to 12, review runs from 115 to 11, and the review-to-build cascade gone entirely. Five pull requests merged in sequence, the tracker fully reconciled at the end: every task closed, nothing claimed, nothing stale, nothing pretending. My own messages changed shape too: nineteen in the milestone phase, and almost all of them are gate decisions, start the next milestone, review and merge. That is the division of labor I actually want, human judgment at delivery boundaries, machine throughput in between.
And budgeted review kept earning its seat. The final milestone’s reviewer found a genuinely nasty quadratic scan in a hot path before it merged. The point was never to review less because review is wasteful. It was to review once, at a boundary that means something.
The Main Loop Is Not Overhead
Here is the part of the story I find most interesting, and it only shows up when you put this run next to the rest of my retro corpus.
The milestone phase did not win by delegating better. It won by delegating less. Its main loop generated 973,918 tokens, slightly more than the fan-out phase’s main loop produced in twice the wall-clock time, because implementation moved back into the coordinator where the integration contract was visible. The delegated share dropped from 81% to 26%, and throughput went up.
That inverts the assumption I had been operating under, that a more autonomous session is a more delegated session. Across twelve long sessions in my corpus, the delegated share ranges from 5% to 91%, and there are successful sessions at both ends. A 24-hour voice-pipeline build delegated 5% and its main loop generated nearly ten million tokens of direct implementation. The big Claude fleet runs delegated 90% and shipped releases. Both patterns work. What fails is the mismatch: fanning out shared-contract work that needs one pair of hands, or serializing mechanical work that forty agents could sweep in parallel.
Twelve long agent sessions from my retro corpus. Delegation is a dial, not a virtue: good sessions live at both ends.
Data table
| session | runtime | hours | delegated share |
|---|
Delegation is a dial, not a virtue. The dial should be set by the shape of the work: integration work with cross-module contracts wants a coordinator holding the whole seam in one context, with independent reviewers checking it. Bounded work with stable file boundaries wants fan-out. My mistake in the first phase was not using subagents; it was letting the dial be set by what was cheap to spawn instead of by what the work looked like.
This Is Sprint Planning, Wearing a Robe
Strip away the agents and this is a coordination failure any engineering manager would recognize in one glance. Nobody staffs forty-one engineers on forty-one one-day tickets, each with a personal release branch, a personal reviewer, and a personal deployment pipeline, and expects the product to converge. We break work into small tickets so humans can estimate it and trust the plan. Then we execute in bigger units: a sprint, a feature branch, a release train, because integration is where products live or die, and integration only happens when work shares a boundary.
I knew this. I have run this play with human teams for two decades. I still let a machine talk me out of it, one reasonable-looking task claim at a time. That is the part worth being honest about: cheap delegation is seductive. When spawning a worker costs nothing and every worker returns something locally valid, continuing looks like progress. The stop signal never fires on its own.
So I wrote down the alarms I should have been watching, and they transfer to any orchestrated system, human or agent:
- Review-to-build ratio above 2:1. Review effort outpacing implementation means findings are cascading, not converging. Stop and consolidate ownership.
- The second status-recovery question. The first time the operator asks “is anything merged?”, answer it. The second time, the answer is a replan, because the system has stopped exposing closure.
- Reviewed work sitting local across many subsequent tasks. Reviewed-but-unmerged is inventory. Inventory rots. Checkpoint it as a PR.
- Completed workers accumulating on the coordination surface. Ghosts cost attention even when they cost nothing else.
The Bill That Remains
The pivot worked, and I want to be precise about the parts it did not fix, because they are the next problems, not counter-evidence.
Continuing inside one enormous session context meant the milestone phase still paid 522 million cache-read tokens re-processing history it mostly did not need. A fresh session per milestone, resumed from a durable handoff, would have cut that dramatically, and that is how I run it now. Polling never improved: long test and CI runs still looked stalled, and the loop kept issuing wait calls at them. And this is not a model verdict. The tempting story, “the new model is worse,” is exactly the kind of claim one confounded session cannot carry. The workers were frequently good. The policy was bad. Until I run the same bounded bundle across model configurations with identical prompts and review rules, the model question stays open, and I would rather leave it open than close it with a vibe.
Making the Fix Structural
Everything above could live in my head as discipline: “next time, bundle the tasks.” I have a whole corpus of session retros that says controls left as memory decay, and controls built into the tool survive. So the fix is becoming an Anvil feature, not a resolution. I filed it as anvil#171: execution bundles as a first-class unit. A bundle groups tasks onto one branch with one owner and one independent reviewer, while each task keeps its own acceptance criteria, evidence, and disposition. Completing a bundle cannot silently mark a task done; the proof obligations stay per-task. Around it: a planning gate that warns when a graph is too big or too serial, a review budget that stops the re-review cascade, a status ledger that always answers committed, reviewed, merged, closed, and mandatory PR checkpoints so reviewed work stops aging in local branches.
None of the CLI names matter. The semantics do: planning granularity and execution granularity are different things, and the tool should hold them apart so I do not have to remember to.
Plan in units small enough to prove. Execute in units big enough to ship. The task is the unit of proof. The milestone is the unit of work. That rule predates agents by decades; the only new part is how expensively I got to relearn it, and how clearly the meter recorded the exact hour I did.
Co-authored with AI, based on the author's working sessions, dictations, and notes.
Explore the source
fakoli/anvil
This article discusses an open-source project. Star it, fork it, or open an issue.