The gap between an LLM that writes elegant code and an LLM that writes code you can ship is almost entirely on the input side. The model is doing the same thing in both cases. The difference is whether the prompt described the problem well enough for the model to solve the right one.
This post is the practical guide to writing specs that produce shippable code in an AI-accelerated workflow. The patterns that work, the patterns that produce regex-matching shells, and a worked example contrasting the two.
In 2024, the bottleneck in AI-augmented dev was the model. Engineers were still working around context-window limits, weak code generation outside of Python and JavaScript, and frequent hallucinations on anything more complex than a CRUD endpoint. As of 2026, the models are good. Claude, GPT-class systems, and Cursor’s underlying capabilities handle multi-file refactors, follow long codebase conventions, and produce code that runs the first time more often than not.
The bottleneck has moved. The thing that determines whether AI-augmented work produces shippable code is now the quality of the spec the engineer hands the model. A clear spec gets a clean implementation in one turn. A vague spec gets six turns of iteration to converge on something half-right.
Most teams haven’t internalized this yet. They’re spending engineering time on prompt engineering at the model interaction layer when the actual gain is at the spec layer. The pattern below is how to fix that.
A spec that produces shippable code in one or two turns shares four properties: atomic scope, given/when/then acceptance criteria, explicit data shapes, and a defined success condition. None of these are about the LLM. They’re the same properties that make a spec readable to a human engineer who isn’t already in your head.
Atomic scope. One function, one endpoint, one component, one migration. Not “build the order management feature.” A spec that scopes to “the function that validates an order before it enters the queue” produces a clean implementation. A spec that scopes to “the order management feature” produces a hallucinated CRUD app that doesn’t match anything in your codebase.
The threshold to apply: if the spec couldn’t be reasonably implemented in a single pull request by a human engineer in a day, the spec is too broad for one LLM turn. Split it.
Given/when/then acceptance criteria. The form forces specificity. “Given an order with no line items, when validateOrder is called, then the function returns ValidationError with code EMPTY_ORDER.” That’s a contract the LLM can implement and the engineer can test. “Validate the order properly” is not.
The criteria should cover at least the happy path and three failure modes. The failure modes are where LLM-generated code drifts most: the happy path is usually right, the failure handling is often hallucinated.
Explicit data shapes. Concrete examples of input and output, not “an order object.” If your order has 14 fields and the LLM only knows about 6, it will invent the other 8 in ways that don’t match your schema. Paste a real example with field names matching your actual code. JSON literals are fine. The model will use them as ground truth.
Defined success condition. What the test will check. If you write the failing test first and hand it to the LLM with the spec, the model has an objective function. It writes code until the test passes. This is the highest-yield pattern in the entire AI-augmented workflow.
The patterns that produce 90 minutes of iteration to converge on a half-right implementation. These are the failure modes; recognize them in your own prompts.
Vague improvement asks. “Improve performance.” “Make the code cleaner.” “Refactor this to be more maintainable.” None of these have a success condition. The LLM will produce changes that look like the spec asked for, and the changes will be different every time you ask. There’s no way to verify the change worked because the spec didn’t specify what working means.
If you actually mean “reduce the p99 latency on this endpoint below 200ms” or “extract the database access into a repository pattern matching the file at src/repositories/userRepository.ts,” say that.
UX hand-waves. “Make the form feel better.” “Improve the onboarding flow.” “More intuitive.” The LLM will produce visual changes that approximate the vibe of the request. The changes will not match what you actually wanted, because the spec described a feeling and the LLM produced a guess at what would produce the feeling. UX-level intent should be translated into specific design decisions before it reaches the model. “Move the submit button below the input fields, change the success state to a green check icon with the text ‘Order received,’ and remove the confirmation modal” is implementable. “Better onboarding” is not.
Feature-scope prompts. “Build the search feature.” “Add multi-tenancy.” “Implement notifications.” These prompts ask the LLM to make a thousand small decisions about scope, data shape, integration points, and behavior. The model will make those decisions; they will not match the decisions a senior engineer would make on your specific system; the output will require substantial rework. Decompose to atomic specs.
Architecture-without-context prompts. “What’s the best way to implement this?” with no context about your codebase, team capacity, or existing patterns. The LLM will give you a textbook answer that doesn’t fit your system. Architecture decisions should be made by humans with context, then handed to the LLM as implementation specs.
A spec is the start of a loop, not a one-shot. Even a well-formed spec usually goes through one or two refinement cycles before the code is shippable. The loop:
The compression in this loop comes from steps 3 and 4, where the LLM does in two minutes what the engineer would have done in 30. The decision-making in steps 1, 2, and 6 is unchanged. That’s the realistic shape of AI-accelerated development at the task level.
The trap to avoid in step 5: when the test fails, the lazy fix is to paste the failure into the chat and ask the LLM to debug. Sometimes that works. More often, the LLM will produce a fix that makes the test pass without solving the actual problem. Read the failure. Decide whether the test or the spec was wrong. Then refine.
Two versions of the same spec, one that produces shippable code and one that produces a regex shell.
The non-working spec:
Write a function that validates orders. It should check that the order is valid before it goes into the queue.
What the LLM does with this: produces a function called validateOrder that takes an “order” parameter and returns either true or an error message. The error messages are invented. The validation checks are inferred from generic e-commerce patterns: “must have at least one line item,” “total must be positive,” “customer must be present.” None of these match your actual schema. The function compiles, looks reasonable, fails on the first real order because your line items are nested differently than the LLM assumed and your customer field is on the order, not nested under it.
The working spec:
Implement the function
validateOrder(order: Order): ValidationResultinsrc/orders/validation.ts.The
Ordertype is defined insrc/types/order.ts(paste the type definition).The
ValidationResulttype returns either{ valid: true }or{ valid: false, errors: ValidationError[] }whereValidationErroris{ code: string, field: string, message: string }.Validation rules:
- Given an order with
lineItems: [], when validateOrder is called, then return{ valid: false, errors: [{ code: 'EMPTY_ORDER', field: 'lineItems', message: 'Order must have at least one line item' }] }.- Given an order with a
lineItemwherequantity <= 0, then return an error with codeINVALID_QUANTITY, fieldlineItems[N].quantity, where N is the index of the failing item.- Given an order with
customer.tier === 'standard'andtotal > 10000, then return an error with codeTIER_LIMIT_EXCEEDED.- Given an order with
shippingAddress.countrynot in the allowed list (['US', 'CA', 'GB']), then return an error with codeUNSUPPORTED_REGION.The failing tests are in
src/orders/validation.test.ts(paste the test file).
What the LLM does with this: implements the function. The tests pass on the first run. The error codes match what the rest of the codebase expects because they came from the spec. The data shape matches your schema because you pasted the schema.
The difference between the two outputs is the difference between code that ships and code that requires three rounds of rework. The model is the same. The spec is different.
The implication for an AI-accelerated dev team: spec writing is a craft skill that compounds in importance as model capability grows. The senior engineers on the team should be doing more spec work and less typing. The model handles the typing.
This changes what “senior engineer” looks like in 2026. The senior engineer is the person who can decompose a feature into atomic specs, write the failing tests first, anticipate the failure modes that should be in the spec, and read the LLM’s output critically enough to catch when it’s wrong. The typing speed of the senior engineer matters less than it used to; the clarity of their thinking matters more.
For a buyer evaluating a vendor, this suggests a different test than the usual code samples. Ask to see a recent feature spec the team wrote. A senior engineer at an AI-accelerated shop should be able to show you a spec that looks like the worked example above, scoped to a single function, with concrete data shapes and given/when/then criteria. If the specs look like the non-working spec above, the team is leaving most of the AI-accelerated gain on the table, regardless of which tools they have licenses for.
The velocity math in the cornerstone post (16-24 weeks compressing to 8-12) assumes the team writes specs at the working-spec level. Teams that write specs at the non-working level get marginal AI-tooling benefit because they spend the compressed time on iteration rather than progress.
Q: How do you write specs that produce shippable code from an LLM?
Use four properties: atomic scope (one function, endpoint, or component, not a feature), given/when/then acceptance criteria covering happy path and at least three failure modes, explicit data shapes with concrete examples matching your actual schema, and a defined success condition expressed as a failing test the LLM can run against. The model is doing the same thing in both cases; the spec quality determines whether the output is shippable or requires rework.
Q: What is the most common failure mode in spec-to-code with AI?
Vague improvement asks like “improve performance” or “make the code cleaner” have no success condition, so the LLM produces changes that look like the spec asked for but can’t be verified. The fix is to translate vague intent into specific, measurable criteria: “reduce p99 latency below 200ms” or “extract database access into a repository pattern matching the existing file at src/repositories/userRepository.ts.” UX-level prompts like “improve onboarding” have the same problem and should be translated into specific design decisions before reaching the model.
Q: Should you write the test before the LLM writes the implementation?
Yes. Test-first specs give the LLM an objective function: it writes code until the test passes. This is the highest-yield pattern in AI-augmented workflow because it shrinks the iteration loop and produces verifiable output. The test should cover the happy path and several failure modes; failure handling is where LLM-generated code drifts most.
Q: How do you scope a spec for a single LLM turn?
The threshold: if the spec couldn’t be reasonably implemented in a single pull request by a human engineer in a day, it’s too broad for one turn. One function, one endpoint, one component, one migration. Feature-scope prompts like “build the search feature” or “add multi-tenancy” force the LLM to make a thousand small decisions that won’t match what a senior engineer would decide on your system. Decompose into atomic specs.
Q: What does spec craft look like as a senior engineer skill in 2026?
It looks like decomposing features into atomic specs, writing the failing tests first, anticipating the failure modes that should be in the spec, and reading LLM output critically enough to catch when it’s wrong. Typing speed matters less; clarity of thinking matters more. Senior engineers at AI-accelerated shops spend more time on spec work and review and less time on implementation typing. The model handles the typing.
Motomtech writes specs at the worked-example level across our engagements and treats spec craft as a senior-engineer skill we develop deliberately. If you want to compare timelines on a custom build and see what our spec output looks like for a scope similar to yours, book a 15-min discovery call. We’ll walk through the decomposition for a feature you’re scoping.
Mirgen Hoxha, CEO, Motomtech.