Skip to main content
The Task Is Not the Unit of Work
ai-infrastructureagentsorchestrationanvilplatform-engineering

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

Sekou M. Doumbouya

· 12 min read

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.

The pivot, in my own messages

Verbatim from the session log. Sixty-one human messages in the run; these are the eight that tell the story.

  1. message 32
    Have all changes been merged, or do we need to open PRs?
  2. message 36
    is this still running
  3. message 37
    I'm not sure why we can't complete this task. is taking forever to complete
  4. message 38
    How much more do you have to do? Also, I’m seeing a lot of old agents on the screen…
  5. message 40
    What are you doing now?
  6. 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?
  7. 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…
  8. 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:

Generated tokens per hour, across the whole run

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."

Task fan-out (hours 0–41) Milestone delivery (hours 41–62)
500k
250k
hour 41: the pivot
hours into the run
Data table (active hours)
hourgenerated tokensphase
Hourly output-token buckets from the session retro, both main-loop and delegated. Quiet hours are real: the run spanned three days and the operator slept. The pivot marker sits at the retro checkpoint used for the phase split (41.2 hours in), not a to-the-minute timestamp.

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.

The same 41 tasks, two execution shapes

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.

Measured from one session retro: a snapshot delta between the fan-out phase and the milestone phase, not a controlled benchmark. The milestone phase also includes the recovery work around the broken integration branch, so if anything the comparison understates it. The bundle grouping shown is illustrative; the real milestones grouped the remaining work, not all 41 tasks.

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.

Delegated share of generated tokens, by session

Twelve long agent sessions from my retro corpus. Delegation is a dial, not a virtue: good sessions live at both ends.

This run (two phases) Other sessions
Data table
sessionruntimehoursdelegated share
Share of output tokens generated by delegated workflows rather than the main loop, from each session's retro stats. The two highlighted bars are one continuous session split at the pivot. High delegation and low delegation both shipped; the failures came from mismatched work shape, not from either end of the dial.

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.

0 claps

Enjoyed this? Tap to applaud, hold to really applaud.

Share this post
LinkedInXHacker NewsBluesky

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.