Spec-Driven Iteration with Claude Code: Taming Requirements That Won't Sit Still
Most of the time I lose with an AI coding agent is not spent on code. It goes on re-explaining. The requirements are still being discovered, the product evolves through iteration, and every few days the direction shifts a little. Each session starts from zero, so I paste the same context again, and the agent quietly fills the gaps with assumptions I only notice three commits later.
This post describes the method I use to get out of that loop with Claude Code. The agent did not make the requirements clearer. The method did. The agent just made it cheap enough to apply every single time.
The real cost of churn
When specs move, the expensive part is not the code change. It is two other things:
- Every session starts from zero. The agent has the code, but not the intent behind it. It cannot tell a deliberate rule from an accident.
- Every pivot leaves no trace. "The hold is now 10 minutes" lives in a chat, a meeting, or someone's memory. Next month nobody can say what it was before or who asked.
Add an agent that is happy to guess, and you get drift: what is built slowly diverges from what is wanted, and nobody can point at the moment it happened.
The thesis is simple. Make the spec a versioned artifact that lives in the repo, and treat every change of direction as an explicit, reviewed delta before any code is touched. The rest of this post is the mechanics.
To keep things concrete I will use an invented event ticketing platform: reserve seats with a hold that expires, confirm and pay, cancel with refund rules that depend on how close the event is, and transfer a ticket to another attendee. The backend is Scala with Cats, with CQRS-style command handling. The requirements shift as we go: the hold duration changes, a waitlist appears mid-way, transfer rules get stricter.
Step 0: reverse-engineer the baseline
If the code already exists, the first job is not to change anything. It is to find out what the system actually does today. I ask the agent to read the code, strictly read-only, and extract the business rules that are really implemented. For anything non-trivial I have it fan out with parallel exploration subagents, one per area (reservations, payments, cancellation, transfers), so each one reads deeply without flooding a single context.
Every extracted rule gets one of three labels:
- Implemented and tested. A test asserts the behaviour. This is the most trustworthy category.
- Implemented, untested. The code does it, but nothing proves it is intended.
- Ambiguous. The code allows several readings, or two places disagree.
The rule that matters most: never invent. If the agent cannot prove a rule from the code or a test, it does not get to write it down as fact. It becomes an open question. That one constraint is what keeps the baseline honest.
The living spec, in the repo
The output goes in the repository, next to the code, reviewed in the same pull requests:
docs/specs/
INDEX.md # what exists, where, and the current status
open-questions.md # ambiguity the agent logged instead of guessing
features/
reservation.md # RSV-xx
payment.md # PAY-xx
cancellation.md # CNL-xx
transfer.md # TRF-xx
waitlist.md # WAIT-xx
decisions/
0003-hold-duration-10min.md
Each feature file holds scenarios with stable IDs, written in Given/When/Then:
## CNL-03: No refund close to the event
Given a confirmed order for an event starting in 36 hours
When the attendee cancels the order
Then the order is cancelled
And the refund amount is 0
## CNL-04: Full refund well before the event
Given a confirmed order for an event starting in 10 days
When the attendee cancels the order
Then the order is cancelled
And the refund equals the amount paid
Given/When/Then is plain language. A product person can read CNL-03 and say "yes, that is the rule" or "no, it should be 24 hours" without opening an IDE. That is the point: the scenario becomes the contract between the people who decide and the people who build. The IDs never change, even when the wording does, so every other artifact can point at them.
The decisions/ folder holds lightweight ADRs. Each one is a few lines: what it was before, what it is now, why, and who asked. When someone asks in March why the hold is 10 minutes, the answer is a file, not an archaeology session.
open-questions.md: let the agent log ambiguity
This is the artifact I value most. When the agent hits something it cannot resolve from the code or the spec, it adds an entry instead of choosing an interpretation:
## OQ-07: Does a transfer reset the refund window?
- Logged: 2026-10-14, while mapping TRF-01 against CNL-03
- Evidence: the transfer handler keeps the original purchase date; no test covers it
- Affects: TRF-01, CNL-03
- Options: (A) keep the original window, (B) restart it for the new holder
- Status: open. Decision owner: product
Now I walk into the product meeting with a dated, factual list instead of a vague feeling that "some things are unclear". Each entry names the evidence, the affected scenarios, and the options. The burden of decision moves to where it belongs: the person who owns the product answers, and the answer becomes a scenario change plus an ADR.
It also changes how the agent behaves. "Log it and keep going on the parts that are clear" is a much better default than "pick something plausible".
/spec-change: separate changing the spec from changing the code
This is the core of the workflow, and I wrapped it in a skill so I do not have to re-explain it. The sequence is:
- Spec diff by scenario ID. Which scenarios are added, changed, or removed? "Hold goes from 15 to 10 minutes" touches RSV-02. "Add a waitlist" adds WAIT-01 and later ones.
- Impact analysis by a read-only
impact-analystsubagent. It maps each changed scenario to the code and tests it touches, and reports risks. It cannot edit anything. - Update the spec and write the ADR.
- Stop for human validation. No code yet.
- Only then: plan, implement, test.
An excerpt of the skill:
---
name: spec-change
description: Apply a change of product direction to the living spec before any code is touched.
---
1. Restate the change as a diff over scenario IDs (added / changed / removed).
2. Delegate to the impact-analyst subagent: which code and tests does each ID touch?
3. Update docs/specs/features/*.md and add an ADR in docs/specs/decisions/.
4. STOP. Show the spec diff and the impact report. Wait for explicit approval.
5. After approval only: enter plan mode, then implement and test.
Never edit source files before step 5. Unknowns go to open-questions.md.
Separating those two kinds of change is the whole point. Reviewing a three-line scenario diff is fast and cheap. Reviewing the same change after it has been smeared across eight files is neither. And the human gate at step 4 means the agent never gets to decide that a pivot is "obviously" what you meant.
Traceability for free
I name tests after the scenario they verify:
test("CNL-03: cancellation less than 48h before event gives no refund") {
val order = confirmedOrder(eventStart = now.plusHours(36))
val result = Cancellation.decide(order, now)
assertEquals(result.refund, Money.zero)
}
When CNL-03 changes, one grep -r "CNL-03" src/ docs/ shows the scenario, its tests, and anything referencing it. It works for me, and it works for the agent, which no longer has to guess which tests are relevant. There is no tooling to maintain and nothing to keep in sync, only a naming convention.
Guardrails
Three small pieces keep the agent inside the lines.
A short CLAUDE.md. It does not describe the product. It points to docs/specs/INDEX.md and states the rules: the spec is the source of truth, behaviour changes go through /spec-change, ambiguity goes to open-questions.md, tests carry scenario IDs. A page is too long; ten lines is about right.
A fast hook after edits. Format check, compile, and the tests of the touched module. It has to be fast or you will disable it. In practice that means a warm build server rather than a cold start each time. Feedback arrives while the agent is still working, so mistakes are corrected in the same turn rather than discovered later.
Plan mode for anything that comes out of /spec-change. The spec change was approved; the implementation plan deserves its own look before files change.
Architecture that absorbs churn
A method cannot fully compensate for a codebase that fights change. Three habits keep the cost of a pivot low, and they happen to suit Scala and Cats well:
- A pure domain core for state transitions. Cancellation, hold expiry, and transfer eligibility are functions from state and input to a decision, with no I/O. Changing the refund window means changing one function and its tests.
- An anti-corruption layer at the boundary. A payment provider callback is decoded into a typed domain object,
PaymentConfirmedorPaymentFailed, as soon as it enters the system. The provider's payload shape never leaks inwards, so a provider change touches one decoder. - One feature, one command, one handler. Adding the waitlist means adding
JoinWaitlistand its handler, not editing a growingmatchin the middle of the reservation code. New behaviour is added; existing behaviour is rarely reopened.
trait CommandHandler[F[_], C, E] {
def handle(command: C): F[Either[DomainError, List[E]]]
}
Packaging it: a generic plugin
None of this is specific to ticketing, Scala, or any one team, so I package the pieces as a plugin I call living-spec. Everything shares a spec- prefix so it groups together in autocomplete:
| Component | Name | Role |
|---|---|---|
| Skill | /spec-baseline | Reverse-engineers the current code into docs/specs/. Run once. |
| Skill | /spec-change | Delta, impact analysis, ADR, stop for validation, then plan and implement. |
| Skill | /spec-questions | Turns open-questions.md into a meeting-ready list sorted by what blocks the most work. |
| Agent | spec-archaeologist | Read-only. Looks backwards: which rules does the code implement today? |
| Agent | impact-analyst | Read-only. Looks forward: what will this change touch? |
| Hook | spec-guard | Format, compile, and module tests after edits. |
The two agents are named differently on purpose, because they do different jobs. One digs into what exists; the other predicts what a change will disturb. Both are read-only, which is what lets me run them without worrying about what they might "helpfully" fix.
What I would do first
If you are in the middle of a project where requirements keep moving, start small:
- Run the baseline once, read-only, and accept that a good share of the result will be "ambiguous".
- Create
open-questions.mdand take it to the next product conversation. - Give scenarios IDs and put those IDs in test names.
- Route the next change of direction through a spec diff and an explicit stop.
You do not need the full plugin on day one. You need the habit of writing the change down before the code changes.
Conclusion
The agent did not make the specs clearer. Clear specs came from a spec that lives in the repo, scenarios with stable IDs, ambiguity written down instead of guessed, and a hard stop between deciding and building. Each of those existed long before AI agents. What the agent changes is the price: producing the baseline, the impact analysis, and the ADR now costs minutes, so I actually do it on every change instead of promising myself I will.