I write the spec before I open the coding model

Regs Isabelo9 min read

I plan with the best model I have, then hand a fast one a short spec with no code in it. Three studies say the same thing I feel every week: the agent isn't the expensive part. The re-explaining is.

I write the spec before I open the coding model

By the fourth prompt I'm not building anything. I'm repeating myself.

The same validation rule. The same repository. The same five controller methods, said to an agent that invented a sixth one while I was typing.

That repeating is the bill. The code is cheap.

So I stopped opening the coding model first. I write a short spec, using the best model I have that day. Then a fast model builds it.

  1. the best model I have writes the spec
  2. the spec holds the flow and the rules this repo already follows. Never code.
  3. a fast coding model builds it, preferably one that still reasons
  4. the file goes to the next person, so the standard isn't stuck in my chat history

Planning gets the best model I have

This is the one thing I won't downgrade. Bugfix, new feature, a folder that doesn't exist yet. Same rule. Whatever the strongest reasoning model I have access to that day is the one that writes spec.md.

It's the right place to spend the money because planning is judgment, not typing. What's in scope. What the service layer already covers. Where the flow branches. What "done" means when the ticket is one paragraph long.

A cheaper model will still hand me a document. It just hands me one with all the ambiguity left in it, and I pay for that ambiguity later, during the build, when it's expensive.

The planning session ends when the spec reads clean. I don't let that model write the feature.

The coding model only has to be fast and obedient

Building is volume. Lots of files, lots of small edits, lots of test runs. I want something quick enough to stay in the loop with me that still thinks for a second before it edits. A reasoning coding model.

When the budget is tight, a non-reasoning model like Composer is a genuinely good deal. It's fast and it follows a tight spec closely. It falls apart on a vague one. That's the whole trade, and the spec is what makes the cheap side of it safe.

Either way, neither one opens before spec.md exists.

The spec holds the flow, never the code

I don't put code in a spec. The moment I do, the agent copies it word for word, and now I'm maintaining two versions of the same class that drift apart by Thursday.

What goes in instead is the business path (a flowchart or plain pseudocode) plus the patterns this project already chose. Repository, service layer, one validation step per operation, the formatter that's already running. I name the pattern. I don't rebuild it in Markdown.

# Cancel an order

## Flow
customer taps cancel on an order
  → order already shipped? reject, with a reason
  → not their order? reject, forbidden
  → cancel through the order service, refund through the existing gateway call
  → notify the warehouse, return the updated order

## Keep
- one validation step per operation
- queries live in the repository, not the controller
- money changes go through the service, never a controller
- the formatter and lint rules this repo already runs, unchanged

## Done
- a shipped order cannot be cancelled
- another customer's order returns forbidden
- a partial refund does not double-refund
- the response doesn't leak the internal warehouse status

Greenfield or a ten-year-old app, same shape. On a new project I write the patterns I want invented once. On an existing one I point at what's already in the tree. For a Laravel API, that's the five stops a request already travels. Either way the file describes the shape. The code is the output.

A clear PRD and a real UAT make the spec sharper, and they aren't a requirement. A thin ticket still gets a spec. A thin ticket is exactly when I need one.

What this looks like on a Laravel ticket

Here's the whole loop, on a normal ticket: customers should be able to cancel an order before it ships.

  1. I open the planning model first and point it at the parts it needs: routes/api.php, the Order model, the OrderService that already exists. I ask for a spec, not code. Flow, rules, done list.
  2. I read it. This is the part that pays. Reading the flow out loud is where I notice nobody said what happens to an order the warehouse already picked but hasn't shipped, and that a partly refunded order would hit the gateway twice. Fixing that in the spec is one sentence. Fixing it after the build is a pull request, a review, and a second round of testing.
  3. Once it reads clean I open the fast coding model and say implement spec.md. It writes the CancelOrderRequest, the policy check, the service method, the button on the order page, and the Pest tests for the two rejections. I review a diff instead of a conversation.
  4. When it gets something wrong, and it does, I fix the spec and run implement again. I don't patch it in the chat. Patching the chat means the fix lives in a thread that closes tonight. Patching the file means the next ticket, and the next person, get it too.

A conversation also guesses early and sticks. That's my sixth controller method. Decided in prompt one, then four more prompts arguing with a decision I never made.

That's where the prompt count drops. I'm not explaining the repository pattern every session. I'm not listing which controller methods exist. And two engineers on the same team now ship the same shape, because the standard is a file in the repo instead of whatever each of us happened to type.

Twenty tickets, and nine that buy nothing

Someone measured the part I was only feeling. A preregistered benchmark of 24 coding tasks ran the same fixes twice: once with the scope, the acceptance criteria and a stop rule written down, once with just the wish: something in here is producing wrong results, improve it.

Leave the scope vague and the model thinks 44% harder to land the same fix. It also lands it less often: 83% of the time, against a near-perfect record when the scope was written down.

Forty-four percent is nothing on one ticket. Line up a month of them, say twenty, and it stops being nothing.

What vague scope costs in thinkingSpec first does the work. No spec does the same work plus 44% more re-explaining.No specSpec firstre-explaining+44%the workthe work

Same twenty tickets, same models, same people. Nine tickets' worth of spend that bought no feature and no test. Just an agent working out decisions that had already been made, and getting them wrong more often while it did.

The honest limit: those were small tasks, at most four files, and the only thing that changed was the prompt. Nobody has measured a real month, so the nine is one ratio stretched across twenty, not my invoice. I use it as a direction.

The model gets lost in the chat

These papers are why the spec is a file, not a thread. Same models. What they changed is how the work was handed over.

LLMs get Lost in ConversationSingle-turn fully specified versus multi-turn underspecified. Models move from high aptitude and low unreliability to lower aptitude (-15%) and very high unreliability (+112%).

A file is the whole brief in one go. Chat is the same brief, one piece at a time. Microsoft Research and Salesforce ran the same jobs two ways, across 15 models (Laban, Hayashi, Zhou, and Neville, 2025). Hand over everything in the first message and it does the job. Drip the same facts across a conversation and the score falls 39%. The answers swing around more than twice as much. Then they pasted those pieces into one ugly bullet list, no polish, and the score came back, about 95%. So it isn't the writing. It's handing everything over at once. That's spec.md, and it's why mine look like that cancel-order list.

The model also guesses on turn one and doesn't climb back out. That's the sixth controller method, measured.

Gaps cost more than long prompts. The same benchmark as the 44% found the other half. Splitting one task across two turns cost 31% more thinking. Restating the same requirements at length (a much longer prompt, no new information) cost nothing, about 1.0×. Length isn't what the model charges for. Undecided things are. I close those in the spec, not in prompt five.

I can't feel which way a session went. METR ran a real randomized trial (Becker, Rush, Barnes, and Rein, 2025): 16 experienced developers, 246 real tasks, in repositories they'd worked in for years. With AI tools they were 19% slower. They finished believing they'd been 20% faster. A file is something I can check tomorrow. My sense of how fast today felt isn't.

What I don't want to see

  • the coding model opened before spec.md exists
  • a spec that pastes a controller, a validation class, or a migration
  • the expensive model writing every line of the feature
  • a cheap model handed a vague ticket and told to just build it
  • the repository pattern explained again in prompt five
  • a fix that lives in the chat instead of the file
  • a different standard per teammate, because the rules lived in someone's history

None of that is the model's fault. It's starting in the wrong session, or putting code in the file that was supposed to constrain it. I write the spec first. Then I open the coding model.