DEV Community

Anton Brilliantov
Anton Brilliantov

Posted on

Where This Approach Does Not Work

Three kinds of tasks the method does not cover, and the price of splitting.


👋 I'm Anton — a backend engineer working in PHP/Symfony and Go, currently carving a live PHP
monolith into Go services. The last eight parts of this series described one way of handing work to
an agent, and every one of them was written from the inside, where it works. This part is the
opposite view: the three places I stopped using it, and why. Notes and code:
github.com/brilliant-almazov.

Maybe you draw the line somewhere else. That's the more interesting conversation, so I'd rather
publish my line than pretend there isn't one.


The whole method has one foundation

Strip away the folder layout, the batches, the waves, the per-package acceptance command, the list
of banned phrases — everything I described in the previous eight parts — and one assumption is
holding all of it up:

The facts are known before the task text is written.

Paths. Full type signatures. Names of the primitives that must be reused. Values — error names,
queue names, metric names, table names. One acceptance command that runs with no substitution.

That's not a nice-to-have; it's the load-bearing wall. The reason an executor is allowed to read at
most three named files and forbidden to search the tree is that it doesn't need to: everything it
needs is already in the text. Remove the assumption and the whole thing inverts — the executor has
to go find things, which is the single most expensive thing it can do, and it pays that cost again
on every executor, every time.

So the honest question isn't "does this method work?" It's "when is the assumption false?" I know
three answers, and they're not edge cases — they're most of what a normal week contains.

Case 1 — research tasks

The shape: I don't know what needs to be built yet.

Not "I know roughly and need to fill in details" — that's a normal task with missing facts, and the
fix is to go get the facts. I mean the case where the deliverable itself is the unknown. Which of
three designs survives contact with the data. Whether the thing is even possible on this schema.
What the actual behaviour is at the boundary nobody documented.

There is nothing to specify. A specification is a description of a decision, and the decision
hasn't been made. If I write the task text anyway, I get one of two outcomes, and I've had both:

  • The text is vague enough to be writable — "handle the edge cases appropriately" — which means it's a hole, and the executor falls into it.
  • The text is precise enough to be executable — and it encodes a guess. When the guess turns out wrong, I don't edit the task; I throw it away and write it again from scratch.

The second one is worse, because it looks like progress. Code comes back, tests are green, and the
whole thing is discarded a day later because the premise was wrong.

The order is not negotiable: research first, then the task text. Research is a different
activity with a different output — a decision, not a diff. Once the decision exists, the facts
exist, and then the method applies normally. Trying to do both in one pass gets you neither.

Case 2 — unfamiliar code

The shape: the facts exist, but only inside a tree I haven't read.

The task text I hand out carries a "facts of the set" section: exact package paths, full signatures
of every type the iteration plugs into, names of the existing generics that must be reused, values
spelled out as values. On a codebase I know, writing that section is transcription — I'm copying
things I already hold.

On a codebase I don't know, that section has exactly one source: reading. And the rule is that
the lead does the reading — reconnaissance across the tree is the one place it's allowed, and it
is not allowed anywhere else, least of all inside an iteration.

That's the part people underestimate, myself included. Two things make it expensive.

First, it doesn't parallelise. I can run two executors on the main work and one on fixes. I
cannot run two of me. Reading is a single-threaded prefix to everything else, and on an unfamiliar
tree it is most of the elapsed time — the code that follows is the short part.

Second, if I skip it, the cost doesn't disappear — it multiplies. In one of the early sets,
before the closed file list and the facts section existed, iterations were burning roughly
350,000 tokens each, because the executor was assembling its own context: finding the types,
walking neighbouring packages, working out what the local convention was. That number is exactly
what a missing facts section costs, and it recurs per iteration. Reading the tree once, myself, is
the cheap version of that — but "cheap" is relative to a disaster, not to free.

Facts already known Facts must be read first
Who does the reading nobody — transcription the lead, before anything is written
Parallelisable yes, two executors + one on fixes no, single-threaded prefix
Dominant cost writing the task texts reading the tree
If skipped every executor re-discovers it, per iteration

So this case isn't a hard "no" like case 1. It's a "yes, and the bill arrives before the first line
of code." Worth it on a tree I'll come back to twenty times. Not worth it on one I'm visiting once.

Case 3 — no definition of done

The shape: "make it nice." "Improve the error handling." "Clean this up."

These fail at a specific point, and it isn't the writing — it's acceptance. Every iteration in my
sets carries one command, scoped to its own package, that runs with no substitution:

go test ./internal/repository/order/... -race -count=1
Enter fullscreen mode Exit fullscreen mode

That command is not a formality. It is the only signal that closes an iteration. Green means
done, and I move on; red means not done, and it comes back. There's no third state, and — this is
the point — there's no judgement call, which is what makes the whole pipeline resumable by someone
who wasn't watching.

"Make it nice" has no such command. Nothing runs. Nothing goes green. Which means the iteration
never closes; it just stops being worked on, and I decide by eye whether I like the result. Once
I'm deciding by eye, the state of the work lives in my head, and every property I care about —
stopping at a batch boundary without loss, closing an executor the moment it's finished, picking a
set back up cold three weeks later — is gone.

The workaround is real but limited: convert the taste into a check. "Improve the error handling"
isn't executable; "every error returned from this package is matched with errors.Is / errors.As,
and a test asserts it" is. When the conversion is possible, do it — a check outlives a reminder,
which is the whole lesson of part 8. When it isn't possible, the task isn't ready to hand out, and
saying so is cheaper than finding out at acceptance.

case                         why the method stops
───────────────────────────  ──────────────────────────────────
RESEARCH                 ──▶ the text is rewritten
what to build is unknown

UNFAMILIAR CODE          ──▶ the lead reads, and it is slow
facts must be read first

NO DEFINITION OF DONE    ──▶ the iteration never closes
nothing to accept
Enter fullscreen mode Exit fullscreen mode

Three cases where the method does not apply: research — what to build is unknown, and the text is rewritten; unfamiliar code — facts must be read first, the lead reads and it is slow; no definition of done — nothing to accept, the iteration never closes

The price of splitting

Now the part that applies even when all three cases are clear and the method does fit.

Splitting has a cost, and it is not hidden or subtle. An iteration is one concern — one file,
sometimes two of the same shape, plus its test. The sets I've actually run were 13 iterations,
12 iterations, and 40 — that last one converted 44 copies of four read shapes onto a single
generic core, one file per iteration.

Forty iterations means forty written task texts.

40 iterations  =  40 statements to write
Enter fullscreen mode Exit fullscreen mode

Not forty bullet points. Forty texts, each with its own closed file list, its own facts, its own
acceptance command — and all of them written before anything runs. In the simple case, writing
the set takes longer than the code would have.

That trade pays when the work is repetitive and the conventions are already decided: the same
mechanical change across many files, in a codebase where the right shape isn't in question. That's
precisely what the 40-iteration set was, and there it paid — the writing was mechanical too, and
the executors ran in parallel batches.

On a one-off change it does not pay off. Not "it's a bit heavy" — it doesn't pay. One file, one
change, one test: writing the specification for it costs more than making it. I make that change
myself and don't open a folder for it. Refusing to say this plainly is how a method turns into a
ritual.

Two equal columns: 40 iterations on the left, 40 statements to write on the right, joined by an equals sign, with a line underneath reading that on a one-off change this does not pay off

Where the line actually falls

In practice I don't reason about this in the abstract. Before I start cutting a set, three
questions, each answerable in one line:

  1. Is the result known? Not the approach — the result. If I can't say what "done" looks like in a sentence, I'm in case 1, and the next thing to do is research.
  2. Are the paths and signatures known without reading the tree? If the honest answer is "I'd have to go look", I'm in case 2. That may still be fine, but the reading is now on my clock and it goes first.
  3. Is there a command that proves an iteration is done? One command, its own package, no substitution. If no, I'm in case 3, and the task needs converting into something checkable before it can be handed out.

Three yeses and I cut the set. A single "no" and it isn't a set yet — it's research, or reading,
or a definition-of-done problem wearing a task's clothing. All three of those have a fix; none of
the fixes is "write the task text anyway and see what comes back".

Question Answer is "no" What it actually is What to do first
Is the result known? case 1 research decide, then write
Paths and signatures known without reading? case 2 reconnaissance read it myself, on my clock
Is there a command that proves it's done? case 3 taste convert it into a check
┌──────────────────────────────────────────────┐
│ [ ] is the result known?                     │
│ [ ] are paths and signatures known           │
│     without reading the tree?                │
│ [ ] is there a command that proves it done?  │
└──────────────────────────────────────────────┘
                       │
          ┌────────────┴────────────┐
          ▼                         ▼
    all three checked          any one empty
    cut the set                not yet a set
Enter fullscreen mode Exit fullscreen mode

Three checkbox questions — is the result known, are paths and signatures known without reading the tree, is there a command that proves it is done — and a fork below: all three checked leads to cut the set, any one empty leads to not yet a set

The multiplier, honestly

The reason this method does anything at all is that it moves cost from execution to preparation.
Execution gets cheap and parallel; preparation gets expensive and stays mine. That's a good trade
when there's something to prepare — the facts exist, I hold them, writing them down is
transcription.

Where there's nothing to prepare from, there's nothing to move. The cost doesn't shrink; it just
sits where it always sat, in the thinking. No amount of tooling changes that, because it isn't a
tooling limit — it's a limit of the method. That distinction matters: it tells you what to fix.
Case 2 is fixable by reading. Case 1 is fixable by deciding. Case 3 is fixable by turning taste
into a test. None of the three is fixable by a better executor.

Over to you

That's my line, and it's drawn from one codebase and one year of doing this — not from a survey.

I'd genuinely like to hear where yours falls. Maybe you do this better than I do and have found a
way to specify research work that doesn't get thrown away. Maybe you've been through the
unfamiliar-code version of this and know what the reading actually costs on a tree that size. Maybe
you look at the whole thing differently and think the split is misdrawn.

How is this solved on your side — and what broke when you tried it?


Working with agents — Part 9. That closes this block.

Next block goes down a level, into the platform itself, starting with the claim I get the most
argument about: every service is required to be a copy-paste of every other one. Sameness
matters more than elegance, because readability comes from predictability — Part 1 of that block is
article #110.

Top comments (0)