Skip to content
← All guides

When to stop the agent and write it yourself

6 min read

Six signals that another prompt is wasted, and the skeleton-and-fill pattern that beats both retrying and taking over completely.

Forty minutes gone. Six attempts, the test still red, the diff now touching four files you never intended to change, and you have not once opened the file yourself. Each attempt felt cheaper than the alternative, which is exactly how you got here.

The instinct to keep going is not laziness, it is arithmetic that used to be correct. Another prompt costs thirty seconds. Reading the module properly costs ten minutes. On attempt one that trade is obviously right. By attempt six the expected value has inverted, and nothing in the interface tells you so, because attempt six looks identical to attempt one.

Breadth is cheap, precision is not

An agent is extraordinarily fast at breadth. Twenty similar cases, a rename across forty files, a test suite for a module with a clear contract, boilerplate in a framework it has seen a thousand variations of. It is slow and unreliable at precision on anything only you know: why this queue must drain in order, which customer still depends on the legacy field, the fact that the payment provider lies about its own status codes.

That is the line. Not hard versus easy, not big versus small. The question is whether the information needed to be correct exists somewhere the agent can read. If it does, keep going and point at it. If it lives only in your head or in a conversation from six weeks ago, every additional attempt is a guess wearing confident prose.

Six stop signals

Two failed attempts on the same error with no new information. Not two failures — two failures where nothing was learned in between. If attempt two produced a different stack trace, a narrower failure, or ruled something out, that is progress and you continue. If the same assertion fails the same way and the explanation has started to hedge, the loop has closed. That specific failure mode has its own article: The retry loop: spotting it and breaking it.

The fix requires a decision only you can make. Should a partial import fail loudly or skip the bad rows? Is 300ms at p99 acceptable here? Does this endpoint stay backwards compatible for the mobile client that has not shipped an update since March? The agent will choose, and it will choose silently and plausibly. You find out which way in review if you are lucky, and in production if you are not. Make the call yourself, write it down in one sentence, hand back.

It has started editing the test instead of the code. Sometimes legitimate, because tests do encode wrong expectations. But an agent that is failing a test and reaches for the test file has usually reframed the goal from "make this correct" to "make this green". Open the assertion yourself. You will know within a minute whether the test or the code is wrong, and that minute is not delegable.

The change is five lines but the context is five hundred words. If you are about to write three paragraphs explaining an invariant so the agent can produce a five-line diff, you have already done the expensive part. Type the five lines. The explanation was the work.

It is guessing at something it cannot read. An internal package from a private registry, an undocumented response shape, a service whose behaviour lives nowhere in the repo. The output will look right. It is reconstructed from naming conventions. Either give it the real thing — paste the response, point it at the vendored types, run the call and show it the output — or write that part by hand.

You cannot yet articulate what correct means. The most common signal and the least recognised. If you cannot state the expected behaviour in one sentence, you are using generation to think, which is expensive and produces code you then have to unpick. Five minutes with a notepad. Then start again.

Skeleton and fill

Stopping entirely is rarely right, and this is the part most advice gets wrong. The useful move is narrower: you write the part that encodes decisions, the agent writes the volume.

Concretely, you write the types and one reference implementation. It writes the other twenty against your pattern.

export type Rule = {
  id: string;
  appliesTo: (order: Order) => boolean;
  evaluate: (order: Order) => Violation | null;
};

// Reference implementation. Note: missing data returns null
// rather than throwing — absent is not the same as invalid.
export const minimumOrderValue: Rule = {
  id: "min-order-value",
  appliesTo: (order) => order.channel === "web",
  evaluate: (order) =>
    order.totalMinor == null || order.totalMinor >= 1000
      ? null
      : { ruleId: "min-order-value", severity: "block" },
};

// AGENT: implement maxLineItems, restrictedRegion, expiredPromotion
// and blockedCustomer following exactly the shape above.

Seeding the first example is the cheapest control you have over the next fifty. The signature fixes the shape, the comment fixes the one judgement call that would otherwise be resolved differently in every single case, and the naming establishes a convention that gets copied for free. Fifteen minutes of your typing removes fifteen rounds of correction.

The same trick works in reverse inside a single function. Write the hard ten lines yourself — the concurrency guard, the ordering logic, the arithmetic that has to be exactly right — then hand back the boring two hundred: error mapping, logging, the twelve call sites that need updating. You are not choosing between doing it yourself and delegating. You are choosing where the boundary sits, and the boundary belongs at the point where judgement stops and volume starts.

Where hand-writing usually wins

  • Concurrency. Locks, retries, idempotency, anything where two things interleave. Generated concurrency code is usually plausible and occasionally wrong in ways no test you currently have will catch.
  • Performance work with a measurement loop. Measure, change one thing, measure again. That cycle does not survive delegation, because you need the number in front of you when you decide the next move.
  • Subtle numerical or ordering behaviour. Money in minor units, rounding, time zones, stable sorts, pagination cursors. The failure mode is off by a cent or off by one row, and it stays silent for months.
  • Anything whose spec was a conversation. If the requirement was never written down, writing it down is the task, and you may as well write the code while you are in there.

The reviewability limit

There is a ceiling on generation that has nothing to do with the agent's ability. If you cannot review what has been produced, producing more of it is not progress. It is deferred work accruing interest.

Watch for the moment your reading of the diff crosses from "I read this" to "I skimmed this and it looked structurally reasonable". That is the real stop signal, and it fires long before the tests do. Two hundred lines you have actually read beats a thousand you have not, and Reviewing code you did not write is worth having a system for before you need one.

The two-strike time box

Set a timer when you start anything you expect to be fiddly. Ten minutes for a bug, twenty for a feature. When it fires, or on the second failed attempt against the same error, whichever comes first, you stop generating and do exactly one of three things:

  1. Run one diagnostic yourself and paste the raw output back in.
  2. Write the skeleton and hand back the fill.
  3. Write the whole thing.

The specific number matters far less than having one. The failure mode is never that you chose the wrong timer. It is that forty minutes went past without you noticing a choice was on the table.

workflowdelegationskeleton-and-fillcode-review