Ask an agent to "add rate limiting to the login endpoint" and you will get rate limiting. You will also get a decision about the window length, the storage backend, the response code, whether the limit is per-IP or per-account, and what happens to requests already in flight. You made none of those decisions. The agent made all of them, silently, in whatever shape its training distribution found most probable — and you won't see them until review, if you catch them at all.
That is the actual cost of a thin prompt. Not that the model can't write the code; modern agents write the rate limiter fine. The problem is that every gap in your description becomes a decision the agent makes on your behalf without telling you. A specification is the tool for closing those gaps before code exists. Its job is to convert silent decisions into stated ones.
A spec is a decision, not a description
The most common mistake is treating a spec as documentation — a paragraph of prose that reads as precise and constrains nothing. "Handle failed logins gracefully" is a sentence an agent can satisfy in a dozen incompatible ways, all of which technically match the words. The spec sounds specific and is not.
The fix is to write requirements that can only be read one way, and the cleanest discipline I know for this comes from safety-critical engineering rather than AI. EARS (Easy Approach to Requirements Syntax), developed while analyzing airworthiness rules for a jet engine controller, constrains every requirement to one of five sentence shapes built around a trigger and a response. It has quietly become the default acceptance-criteria format across AI coding tools for exactly the reason it works on jet engines: it leaves the reader — human or model — nowhere to guess.
Compare the two versions of the same requirement:
# Vague — the agent fills every gap for you
Handle failed logins gracefully.
# Testable — EARS trigger/response form
When a user submits valid credentials, the service shall return 200 and a session token.
If a user submits invalid credentials, then the service shall return 401 and shall not reveal which field was wrong.
While an account has 5 or more failed attempts within 15 minutes, the service shall reject further attempts with 429.
The second version is not longer because it is more verbose. It is longer because it contains three decisions the first version left to chance: the exact status codes, the information-leak constraint, and the lockout threshold. Each clause is also a test you can write. If a requirement can't be turned into an assertion, it isn't a requirement yet — it's a hope.
Structure the spec so the agent works in stages
A single wall of requirements still gives an agent too much room. The workflows that have converged in 2025 — GitHub's Spec Kit, Amazon's Kiro, and others — all split the work into ordered artifacts, each feeding the next:
specify → what and why: behavior, constraints, acceptance criteria. No implementation.
plan → architecture, data models, error handling, chosen dependencies.
tasks → atomic, ordered steps, each independently verifiable.
implement → generate against the frozen artifacts above.
The separation matters because it forces you to review the what before the agent commits to a how. Mixing them is where specs quietly fail: a "requirement" that says "store attempts in Redis with a sliding-window counter" has smuggled an implementation decision into the layer that was supposed to define behavior, and now nobody reviewing the spec is checking whether Redis was the right call. Keep functional intent and technical approach in different files. Let the plan cite the spec's requirement numbers so every design choice traces back to something a user actually needed.
There is a durability payoff too. When the spec is the source of truth and the code is a regenerable output, a changed requirement is a spec edit followed by regeneration of the affected slice — not an archaeology dig through code the agent wrote last week and neither of you remembers.
Right-size it, or the ceremony eats the work
Spec-driven development has a failure mode of its own, and it is worth naming plainly. A detailed analysis on martinfowler.com describes using the full spec workflow to fix a small bug and finding it "like using a sledgehammer to crack a nut" — the agent generated pages of markdown for a one-line change, and reviewing those pages cost more than reading the diff would have. The same piece notes agents ignoring parts of elaborate specs anyway. More structure is not free, and it is not always worth it.
A spec earns its keep in proportion to the number of decisions it prevents the agent from making wrong. Below some threshold of ambiguity, writing it down costs more than the mistakes it saves.
The practical rule I use: spec the work whose interfaces and edge cases are the hard part — a new endpoint, a state machine, a migration, anything with error paths and non-obvious behavior. Skip the ceremony for changes whose correctness you can verify at a glance. A typo fix does not need a requirements document. A payment flow does.
The takeaway
Before you hand an agent a task, write down the decisions you would otherwise be annoyed to find it made for you — and write each one as a sentence with a trigger and a response you could turn into a test. If a clause can't become an assertion, it isn't ready. If the whole spec is longer than the code and the code has no interesting edge cases, you are polishing ceremony instead of shipping. The spec is not there to impress a reviewer; it is there so the agent runs out of room to be plausibly wrong.
Sources: EARS — Easy Approach to Requirements Syntax, GitHub Spec Kit, Understanding Spec-Driven Development: Kiro, spec-kit, and Tessl (martinfowler.com), Spec-driven development — Thoughtworks