ZeNorm

Scoping work for AI agents

How to size a task so a coding agent can finish it coherently: decision count over line count, vertical slices over layers, and dependency order.

Alex Earll9 min read

There is a size of task that AI coding agents are good at, and it is narrower than most people assume in either direction.

Give an agent too much and it loses the thread partway through. Not dramatically — it doesn't crash or refuse. It quietly starts contradicting decisions it made forty minutes earlier, because those decisions are now buried under 60k tokens of tool output and it is reasoning from a summary of its own work rather than the work itself. Give it too little and you become the bottleneck: you spend more time carving up the problem and stitching the pieces back together than you would have spent just writing the code.

The interesting question isn't "how big should a task be." It's "what actually determines the size."

Line count is the wrong unit

The instinct is to measure in lines of code, or files, or story points. All of these correlate weakly with what actually breaks agents.

A 900-line change that adds thirty near-identical API route handlers, all following an established pattern in the same file, is trivially within reach. There is one decision — what the pattern is — repeated thirty times. The agent makes it once and executes mechanically.

A 40-line change that touches an auth middleware, a database transaction boundary, and a client-side cache invalidation is a different animal entirely. Three files, but each one requires a judgment call that constrains the other two. Get the transaction boundary wrong and the cache invalidation is subtly wrong too, in a way that only shows up under concurrent load.

Two things predict difficulty far better than size:

The number of independent decisions the task requires. Not steps — decisions. Points where more than one reasonable approach exists and picking wrong propagates. "Add a status column and backfill it" has one decision (what the backfill does with nulls). "Add a status column, migrate existing rows, expose it in the API, and filter on it in three views" has at least four, and they interact: the API shape constrains the views, the backfill semantics constrain the API contract.

Empirically, agents handle one to three coupled decisions in a single task well. Past that, coherence degrades — not because the model can't reason about five decisions, but because by decision four it is reasoning about decisions one and two through the lens of a compacted transcript rather than the actual code it wrote.

How many files must be held in mind simultaneously. Not touched — held. A task can edit twelve files and be easy if each edit is locally correct without reference to the others. A task can edit three files and be hard if getting file B right requires remembering exactly what you did in file A, which requires remembering the constraint from file C.

The practical version of this heuristic: if you can't describe the task's constraints without a diagram, the agent will not hold them either.

Vertical slices beat horizontal layers

This is the single highest-leverage decomposition choice, and it's the one most teams get wrong because horizontal layering is how we've organized work for decades.

A horizontal task: "add the database schema for comments." A vertical slice: "let a user post a comment on a spec and see it appear."

The vertical slice is bigger by every conventional measure. It touches the schema, a migration, an API route, a client mutation, and a component. It is still the better unit of agent work, for one reason: it has an observable end state the agent can verify itself.

The agent can start the dev server, post a comment, and see whether it appeared. That's a closed feedback loop. When it makes a wrong guess about the API shape, the loop catches it in the same session, while the reasoning that produced the guess is still in context.

The horizontal layer has no such loop. "Add the database schema for comments" is correct or incorrect only in relation to code that does not exist yet. The agent will write a migration, run pnpm db:generate, see it succeed, and declare victory. The schema might be missing an index that the yet-unwritten query needs, or model the parent relationship in a way that makes threading impossible. Nothing in the task's own scope can detect that. You find out two tasks later, and the fix is a second migration.

This gets worse with agents than with humans, because a human writing the schema layer carries an implicit model of the consumer in their head. An agent carries what is in the prompt. If the consumer isn't in the prompt, it isn't in the model.

The corollary: when you genuinely must split horizontally — a large migration that has to land before anything can consume it — write the consumer's requirements into the task itself. Not "add a comments table" but "add a comments table that supports the following three queries," with the queries written out. You are manually supplying the feedback loop the slice would have given you for free. That's the same discipline as writing acceptance criteria that actually work: make the end state checkable from inside the task.

When vertical doesn't work

Vertical slicing has real limits, and pretending otherwise produces its own failures.

Cross-cutting changes — renaming a core type, changing a shared error-handling convention, upgrading a framework major — have no meaningful vertical slice. Half a rename is worse than none. These want the opposite treatment: one large mechanical task, executed in a single pass, with the correctness check being "does the whole thing compile and do the tests pass." Agents are good at these precisely because there is exactly one decision (the new convention) applied uniformly.

The rule isn't "always vertical." It's: prefer the decomposition where each unit's correctness is checkable without the other units existing. Usually that's vertical. For mechanical sweeps, that's one big horizontal pass.

Dependency order matters more than you think

If task B depends on an interface that task A creates, and you run B first, the agent doesn't stop and ask. It invents the interface.

It will write a plausible call to getCommentsForSpec(specId) returning a plausible shape, build everything downstream on that shape, and hand you working-looking code. Then task A lands with a real function named listComments(specId, opts) returning something structurally different, and now you're reconciling two designs — one of which was never designed, just guessed.

The reconciliation is where it goes bad. The agent that wrote B is now asked to adapt to A, and its cheapest path is an adapter layer that translates between the invented shape and the real one. That adapter is pure debt. It exists only because of task ordering, and it will outlive everyone's memory of why.

Two things help:

Order by interface creation, not by perceived importance. Whoever defines a shape goes first. If two tasks both need a shape neither owns, pull the shape definition out into its own tiny task and run it first. A twenty-line task that defines a type and its contract is worth its overhead if it prevents two agents from inventing conflicting versions.

When order is genuinely unresolvable, freeze the interface in the spec. Write the signature into both tasks. The agent will not invent something it has been handed.

You can see the failure in the diff. Adapter functions, as unknown as casts at module boundaries, or a helper whose name describes a translation (normalizeCommentResponse) are usually artifacts of an ordering mistake, not of the domain.

Practical splitting heuristics

Things I actually apply when carving up work:

One reviewable sitting. If you can't review the resulting diff in one focused sitting without losing track, it was too big. This is a real constraint, not a nicety — an unreviewed agent diff is worse than no diff, because it carries the appearance of verification. See reviewing code when the AI wrote it.

One sentence, no conjunctions. If describing the task requires "and" between two different verbs on two different nouns, you have two tasks. "Add comment posting and comment deletion" is two. "Add comment posting to the spec detail view" is one.

Name the observable end state. If you can't finish the sentence "when this is done, I will be able to see ___," the task has no verifiable boundary, and neither the agent nor you will know when it's finished.

Split on decision boundaries, not file boundaries. The natural seam is where one judgment call ends and the next begins. Two tasks that each make one clean decision beat three tasks that each make two-thirds of a decision.

Keep the file set nameable. If you can list the files the task will touch, the agent can too. If you can't, it will discover them mid-flight, and discovery burns context that was supposed to go toward reasoning.

Signs, in hindsight, that a task was too big

You mostly can't tell in advance. You can tell afterward, and the signals are consistent enough to calibrate on:

The agent rewrote its own earlier file. This is the clearest signal. It wrote parser.ts in step two, then in step nine rewrote a chunk of it — not to fix a bug, but because it had forgotten the shape it chose and re-derived a different one. If you scroll back through the session and find the same file written twice with different assumptions, the task exceeded the coherence window. This is the concrete form of the problem in why AI coding agents lose context.

The diff sprawled past the stated scope. You asked for comment posting; the diff also reformats an unrelated utility, adds a logging helper nobody requested, and "fixes" a type in a module you didn't mention. Scope sprawl isn't the agent being disobedient — it's what happens when a task runs long enough that the agent starts filling ambiguity with plausible-looking initiative.

You couldn't review it in one sitting. If you found yourself skimming the last third, the task was too big regardless of whether the code is correct. You no longer know whether it's correct, and that's the same problem.

The final summary doesn't match the diff. The agent's own account of what it did omits things the diff contains, or describes things the diff doesn't. It is summarizing from a degraded memory of its work rather than reading the work.

Test coverage thins out toward the end. Early files have tests. Late files don't. Attention ran out before the task did.

The trade you're actually making

Smaller tasks cost more of your time per unit of code. That is a real cost, not an imaginary one, and the "just decompose more" advice ignores it. If you split a feature into eight tasks, you write eight specs, review eight diffs, and hold the integration in your own head — which is precisely the work you were trying to offload.

The right size is the largest task that stays coherent, not the smallest task you can define. Push toward bigger until you start seeing the hindsight signals above, then back off one notch. That threshold moves with model capability, with how well-structured your codebase is, and with how good your specs are — a task with a precise spec and clear acceptance criteria stays coherent at a size that would fall apart with a vague prompt.

Which is the actually useful lever. Most people trying to fix agent coherence reach for smaller tasks first. Better task descriptions usually buy more, and cost less. Writing specs an agent can execute does more for scope discipline than any amount of slicing, because a spec that names the decisions is a spec whose size you can actually measure.

Keep reading