Spec-Driven Development with Agents, 71 Specs Later
I recently released Speakroom, a voice-first app for practicing a foreign language. For me that means Japanese, but the app now supports 24…
Spec-Driven Development with Agents, 71 Specs Later
I recently released Speakroom, a voice-first app for practicing a foreign language. For me that means Japanese, but the app now supports 24 languages.
The only reason that scope was realistic is because of how well a constrained AI-first development workflow works today.

Screenshot of Speakroom | Kamen Zhekov
To read this story if you’re not a member, click here for the friend link!
I built it from the ground up using my own flavor of spec-driven development, and an architecture I made sure my agents respected from the start. I wrote about the architecture separately, so I won’t turn this into a product post, but I counted the `agents/work`` folder (where my agent stores its specs) in the repo, and there are 71 specs in there now.
Most of them are just the shape of the work before I let an agent touch the code: what exists now, what should change, which files are probably involved, what is out of scope, how we verify it, and where the implementation is likely to go wrong. That sounds boring, but it has become one of the main reasons coding agents work well for me instead of turning into a very fast source of slop.
I don’t let agents start with code on non-trivial tasks anymore. I want the plan first, then I review it, then the agent can implement against the plan, run the checks, and report what actually happened. The point is constraint, because I am not spiritually moved by paperwork, and the agent needs a good path to follow before it starts editing.

Screenshot of a few specs in the Speakroom repo | Kamen Zhekov
The mechanism is pretty simple: models are very good at filling empty space. If the request is “add live voice support” or “make beginner replies easier”, the agent will create a shape for the work. Sometimes that shape even works at first glance, which is exactly the scary part. The spec is where I decide the shape before the diff exists.
The examples here are from Speakroom, which is a Python / React app, but I do not think the pattern is specific to that stack. The important part is that the agent can read the repo, edit code, run checks, and make mistakes very quickly.
If you’re curious, Speakroom is here:
What the spec is supposed to do
A good agent spec should be much smaller than a product requirements doc, and much more useful than a Jira ticket with nicer formatting.
In practice, I want it to do a few boring but useful things:
- Capture the current behavior before changing it.
- Define the target behavior in a way that can be tested.
- Name the likely files and boundaries involved.
- State what should not change.
- Define the verification commands before implementation starts.
- Surface risks while they are still cheap to discuss.
That last point matters a lot. A coding agent will happily solve the visible request while quietly changing a durable boundary, adding a shortcut, or moving logic into the wrong layer if the task does not make those constraints explicit. I would rather argue with a spec than debug a plausible diff that went in the wrong direction.
The template I use is intentionally small:
# Spec + Plan: [Feature or change name]
## Goal
[One paragraph. What are we trying to change, and why?]
## Current behavior / context
- [What exists now?]
- [Which architecture constraints matter?]
- [What recent implementation detail should the agent know?]
## Acceptance criteria
- [Observable behavior that must be true after the change.]
- [Security/privacy/runtime constraints that must remain true.]
- [Tests or docs that should exist after the change.]
## Planned touchpoints
- `path/to/file.py` - [why this file likely changes]
- `path/to/component.tsx` - [why this file likely changes]
- `docs/something.md` - [why docs likely change]
## Implementation approach
[The intended shape of the solution. This is where you explain boundaries, sequencing, and tradeoffs.]
## Plan
1. [Small implementation step.]
2. [Small implementation step.]
3. [Small implementation step.]
4. [verify] [Targeted check.]
5. [verify] [Repo-level check.]
## Test / verification plan
- [Specific unit/integration tests.]
- [Specific commands to run.]
- [Manual smoke checks if needed.]
## Risks / open questions
- [Things that might be wrong or need human judgment.]
## Out of scope
- [Things the agent should not do even if they look related.]
This format makes sure to force the conversation to happen before there is a diff to get attached to.
A real example shape
One Speakroom spec was about hardening the live audio protocol.
The visible request could have been something vague like “make the audio protocol safer”, which gives the agent too much room to invent. The spec made it concrete:
## Goal
Harden the live-session audio protocol against client-controlled audio sample-rate abuse and keep live replay URLs constrained to authenticated same-origin backend audio routes.
## Current behavior / context
- Backend live audio chunks accept an optional client-supplied `sample_rate_hz` between `1` and `192000`.
- Provider adapters pass the client sample rate into PCM resampling.
- A very low source rate can expand output bytes massively during resampling.
- Live WebSocket event validation accepts any string playback URL.
## Acceptance criteria
- Backend rejects unsupported `sample_rate_hz` values before provider resampling.- Backend rejects audio turns whose byte count implies a duration beyond the max turn length.
- Backend resampling has a defensive maximum output-byte guard.
- Frontend live event parsing rejects absolute, cross-origin, malformed, or non-audio-route replay URLs.
That spec gives the agent something much more useful than a vague goal. It names the bug class, it names the boundary, it defines what the browser is allowed to send, and it says exactly what must be rejected.
Once the spec says that clearly, the implementation becomes pretty boring:
# backend/live_protocol.py
SUPPORTED_AUDIO_SAMPLE_RATES_HZ = frozenset({16_000, 24_000, 44_100, 48_000})
def validate_audio_sample_rate_hz(sample_rate_hz: int | None) -> int | None:
if sample_rate_hz is None:
return None
if sample_rate_hz not in SUPPORTED_AUDIO_SAMPLE_RATES_HZ:
raise LiveProtocolError(
code='audio_sample_rate_unsupported',
message='Audio sample rate is not supported.',
details={'supported_sample_rates_hz': sorted(SUPPORTED_AUDIO_SAMPLE_RATES_HZ)},
)
return sample_rate_hz
// frontend/audioSchemas.ts
import { z } from 'zod'
const exchangeAudioPath = /^\/api\/tutoring\/exchanges\/[0-9a-f-]{36}\/audio\/(user|tutor)$/i
export const audioPlaybackUrlSchema = z.string().refine(
(value) => exchangeAudioPath.test(value),
'Audio playback URLs must be relative backend exchange audio routes.',
)
Then the verification plan makes sure the agent does not stop at “looks implemented”:
## Test / verification plan
- Parsing rejects `sample_rate_hz` values such as `1`, `8000`, and `192000`.
- Valid rates such as `16000`, `24000`, and `48000` still pass.- The resampler raises before allocation when normalized output would exceed `max_output_bytes`.
- Live event parsing accepts `/api/tutoring/exchanges/<uuid>/audio/user`.
- Live event parsing rejects `https://...`, `//host/...`, `/assets/...`, malformed UUIDs, and wrong audio sides.
Commands:
- `uv run --project backend pytest backend/tests/services/test_live_session_protocol.py`
- `npm --prefix frontend run test -- --run src/features/live-session/reducer.test.ts src/features/tutoring/api.test.ts`
- `npm --prefix frontend run typecheck`
This is what I mean by spec-driven development with agents. I am trying to make the task clear enough that the agent can execute it without improvising most parts.
The repo handbook matters too
The feature spec is only half the story. If the repo itself does not explain how it wants to be changed, every spec has to repeat the same architectural lecture.
In Speakroom I keep that in AGENTS.md. It explains the product shape, architecture rules, anti-patterns, and verification expectations. A shortened version looks like this:
# Agent handbook
This is an application, not a generic framework. Prefer small, explicit, feature-local code over speculative abstractions.
## Fast path for agents
1. Classify the change: backend API, domain logic, jobs/worker, provider integration, storage, frontend, docs, or runtime.
2. Read the architecture, operations, and security docs, then the nearest existing files.
3. Extend the existing pattern before creating a new abstraction.
4. If the change alters a durable boundary, stop and surface the conflict instead of improvising.
5. Implement the smallest clear change in the right layer.
6. Run targeted verification, then the strongest relevant repo-level check.
7. Update tests, docs, and config surfaces that changed along with the code.
## Architecture rules
- Routes parse input, call services, and return responses.
- Domain logic lives in feature-local modules.
- Worker tasks accept IDs/primitives, not ORM objects.
- Provider adapters own transport and error mapping only.
- Prompt text and product contracts stay near the feature that owns them.
- Browser requests use app APIs and product resources, not provider payloads or queue internals.
The handbook gives the agent the default rules. The spec gives the agent the task-specific rules.
That combination works much better than putting everything into one giant prompt, because the stable rules stay stable, and the spec can focus on the change in front of us.
The agent has to enforce the stop sign too
The repo handbook tells the agent how the codebase works. My software-engineer agent prompt tells it when it is allowed to change anything at all.
For my main coding agent, the rule is pretty simple: for non-trivial implementation work, it cannot edit application code until it has written a spec and I have approved it.
This is what it looks like:
For non-trivial software/code implementation work, use a lightweight spec + plan gate before editing application or code-adjacent files.
If no approved spec + plan exists yet, inspect only what is needed to understand likely touchpoints, then create or update:
agents/work/<date>_<work-item>/spec.md
The spec must include:
- Goal
- Current behavior / context
- Acceptance criteria
- Planned touchpoints
- Implementation approach
- Plan
- Test / verification plan
- Risks / open questions
- Out of scope
After writing or updating the spec, stop and tell the user:
"I wrote/updated the spec + plan at <path>. Please review it. Reply with go/approved to implement, or tell me what to change."
Do not edit application/source/test/build/runtime files until the user explicitly approves.
That rule prevents a lot of annoying agent behavior and without it, a normal implementation request can turn into edits immediately. Or worse, we discuss a plan in chat, the agent assumes that means approval, and suddenly there is a diff (cough Gemini cough).
I even had to make the approval rule explicit. If we only discussed a plan in chat, “sounds good” means “write the spec”, not “start editing code”, so the agent can implement only after it has created the spec artifact and I’ve (hopefully) read and approved it.
There is more in the prompt, but the rest is basically the same kind of guardrail: delegate to a code researcher when the relevant files are unclear, ask for a code review after meaningful changes, stop if implementation exceeds the approved scope, and do not guess verification commands if the repo does not make them clear.
That might sound like too much ceremony, but it is really just guardrails around the failure modes I kept seeing. Agents are useful because they keep moving, but sometimes he problem is that they keep moving even when the next step should be “wait, we need to decide this first.”
My working loop
With those rules in place, the loop is usually this:
- I describe the messy human version of the task.
- The agent inspects the repo and writes a spec.
- I review the spec like I would review an implementation plan from a developer.
- The agent edits the spec if needed.
- The agent implements against the spec.
- The agent runs targeted checks and then the strongest relevant repo check.
- The agent reports what changed and what verification actually returned.
The important part is that the agent does not get to jump from step 1 to step 5 on anything non-trivial.
A prompt I would actually use for the first phase looks like this:
We need to implement [change].
Before writing code, inspect the repo and create a spec under `agents/work/YYYY-MM-DD_short-name/spec.md`.
Use the spec shape above. Focus especially on existing architecture boundaries. If the change appears to alter a durable boundary, call that out instead of silently designing around it.
After writing the spec, stop and summarize the plan. Do not implement until I approve the spec.
Then after review:
Implement the approved spec in `agents/work/YYYY-MM-DD_short-name/spec.md`.
Follow the plan unless you discover repo evidence that makes it wrong. If that happens, update the spec with the new decision before continuing.
Run the targeted verification commands from the spec, then the strongest relevant repo-level check. Report the exact commands and results.
This is also why I like specs living in the repo instead of disappearing into chat history. The agent can read them, update them, and future agents can understand why the change was shaped that way.
One actual loop
A recent Guided Scenario change is a good example because it started with design, not code.

Screenshot of using Claude Design for Speakroom | Kamen Zhekov
For visual work, I often let Claude Design explore the UI and produce a handoff package. Then I give that handoff to my local software-engineer agent in OpenCode, running GPT 5.5, and make it do the boring repo-aware part: read the real components, check the API types, find the tests, and tell me where the handoff does not match the product.

Screenshot of using OpenCode for Speakroom | Kamen Zhekov
That split matters to me. The design handoff is allowed to be a bit idealized. It can propose structure, spacing, motion, and interaction details. But the local coding agent has to deal with the actual app: current React components, backend contracts, tests, accessibility, and whatever weird constraints already exist in the codebase.
In this case, I asked it roughly this: there is a Claude design handoff for merging goals and beats in the guided scenario UI, explore it, critique it, and explain how it would fit into the repo.
The agent did not implement first. It read the handoff package, the current GuidedSessionLayout, the tests, the frontend API types, and the backend scenario catalog. The useful bit was that it caught a product contract problem in the mock.
The handoff wanted completed goals to show something like:
You said 「オーツミルクはありますか?」
That looked nice in the mock, but it was wrong for Speakroom.
The backend goal-evaluation prompt says satisfaction_evidence must summarize the learner's completed action, and must not quote learner utterances, tutor text, or scenario phrases. So the UI needed direct evidence instead of quote styling:
Asked whether oat milk was available.
That is exactly the kind of thing I want to catch before implementation. If the agent had jumped straight to code, it probably would have copied the mock, added a youSaid localization key, and shipped a UI that lied about what the backend field meant. We would’ve ended up with
You said "Asked whether oat milk was available."
which is just plain wrong.
After that, the spec became much clearer: merge Goals and Phrases by beat into one accordion, reuse PhraseRow instead of rewriting audio logic, keep it frontend-only, show evidence directly, avoid backend/schema changes, and add tests for collapsed rows, expansion, evidence, script toggle behavior, and zero-phrase goals.
Then I approved it, and only then did the agent implement. The implementation still hit normal code issues. A test failed because the new cases leaked DOM state between tests, lint rejected a state reset inside an effect, and a reviewer agent suggested a small accessibility fix for non-expandable goal rows. That is the boring version I want: the agent had a narrow task, checks caught issues, and the final diff stayed inside the approved spec.
Make touchpoints explicit before implementation
The Planned touchpoints section is one of the most useful parts of the spec because it makes the agent name where it expects to work.
For example:
## Planned touchpoints
- `backend/src/app/domains/tutoring/prompts.py` - restructure live prompt construction and Beginner response contract.
- `backend/tests/services/test_tutoring_prompts.py` - cover prompt structure, ordering, resume context, and guardrails.
- `backend/tests/api/test_live_sessions.py` - prove saved Beginner settings reach the live provider system instruction.
That gives me something to review before there is a diff. If the agent says it needs to edit backend/src/app/integrations/llm/gemini_live.py for a product prompt change, I can ask why. Maybe there is a valid reason, but maybe the agent is about to put product behavior in the provider adapter because that file happens to call Gemini.
The touchpoints section also helps future review. If the final diff touches 14 files and the spec named 3, I want to know whether the spec was incomplete or the implementation drifted.
You can even turn that into a small check by comparing git diff — name-only against the paths listed under Planned touchpoints.
I would not make that a hard rule for every repo, because sometimes good implementation work discovers the real touchpoints after inspection, but it is a useful review prompt: if the diff moved beyond the spec, the spec should probably be updated with the decision.
Verification has to be part of the spec
I don’t want agents to say “implemented” if they did not run the thing that proves it.
So the spec should include exact commands. For a Python / React app, that might look like this:
## Test / verification plan
Backend:
- `uv run --project backend pytest backend/tests/services/test_live_session_protocol.py`
- `uv run --project backend pytest backend/tests/api/test_live_sessions.py`
Frontend:
- `npm --prefix frontend run test -- --run src/features/live-session/reducer.test.ts`
- `npm --prefix frontend run typecheck`
Repo-level:
- `make check`
That forces the verification conversation to happen before implementation, which matters because otherwise the agent will often run the smallest thing that happens to pass and call it done.
I also like marking verification steps in the plan itself:
## Plan
1. Add protocol validation helpers.
2. Update the coordinator to enforce duration by sample count.
3. Add provider resampler output caps.
4. Update frontend live event URL validation.
5. [verify] Run targeted backend protocol tests.
6. [verify] Run targeted frontend protocol tests.
7. [verify] Run `make check` if targeted checks pass.
That little [verify] marker sounds silly, but it makes the final phase harder to skip. When the agent reports back, I expect exact commands and real outputs, not "tests should pass" fan fiction.
Specs make parallel agent work less chaotic
The other thing I did not expect was how much this helps with parallel work.
If I have 2 or 3 independent changes, I can have agents create specs for each one, review the specs, and then let them work in parallel as long as the touchpoints do not overlap too much. The spec becomes the handoff object, which is much better than “go do the thing we discussed 40 messages ago.”
The workflow I like is:
Task A: create spec only.
Task B: create spec only.
Task C: create spec only.
Human review:
- Are the touchpoints overlapping?
- Are the acceptance criteria compatible?
- Are the verification commands realistic?
- Does any spec change a boundary another spec depends on?
Then implementation can start.
This is where the repo handbook matters again. If every agent follows the same architectural rules and each task has its own spec, parallel work becomes much less weird. Still weird, because code is code and agents are agents, but less weird.
What I review in the spec
I review the spec with almost the same seriousness as a code diff, just faster.
The questions I ask are:
- Does it describe the current behavior accurately?
- Are the acceptance criteria observable, or are they vague vibes?
- Does the plan name the likely files?
- Are product boundaries preserved?
- Are the tests specific enough that a passing result would mean something?
- Is the out-of-scope section protecting us from adjacent shenanigans?
- Is there any hidden migration, runtime, privacy, billing, or auth impact?
In Speakroom, “product boundaries” means things like provider secrets, prompts, model names, queue internals, and durable product state all staying in the right layer.
For my own repo, a lot of that maps to standing rules:
- Routes stay thin.
- Domain logic stays feature-local.
- Provider adapters own transport and error mapping only.
- Product prompt text lives near the feature that owns the behavior.
- Worker tasks accept IDs/primitives and open their own database sessions.
- The frontend talks to app resources, not queue tables or provider payloads.
- Durable product state belongs to app-owned tables.
Those rules are not universal, but every repo should have its version of them. If the agent has to infer your architecture from folder names and vibes, you are asking for slop and hoping for taste.
What changed after 71 specs
The biggest change is that I catch bad implementation shapes before I have to care about the diff.
Before this workflow, I would sometimes discover the architectural problem after the agent had already written a bunch of code. The diff would pass some tests, the feature would kind of work, and the annoying part would be explaining why the shape was wrong even though the visible behavior looked right.
Now a lot of those conversations happen at the spec stage:
- Should this be a foreground WebSocket call or a background job?
- Should the frontend know this state, or should it poll an app-owned resource?
- Should the prompt live in the provider adapter or in the feature domain?
- Should this be runtime behavior or an ops-only command?
- Should we add a new abstraction, or extend the existing pattern?
- Are we inventing provider config that does not exist?
Those are much cheaper questions before implementation.
The other change is that the repo has a memory of decisions. When I look at old specs, I can see why something was out of scope, which tests were supposed to protect it, and what tradeoffs were accepted at the time. That is useful for me, and it is very useful for agents that do not have my memory unless I write it down somewhere they can read.
The tradeoff
This workflow is slower at the start.
For tiny changes, it would be ridiculous. If I am fixing a typo or renaming a button, I do not need a spec, a plan, and a small ceremony where everyone pretends to be coding for NASA. But for anything that touches architecture, product behavior, auth, billing, provider integration, background jobs, migrations, runtime behavior, or frontend/backend contracts, the spec pays for itself very quickly.
It also makes the agent feel less magical, which I think is good. I don’t want magic. I want a very fast junior developer with root access to operate inside constraints I actually believe in.
That is mostly where I have landed after using coding agents every day: the more freedom I give them, the more they need explicit boundaries. The spec is one of the simplest ways I have found to provide those boundaries without turning the workflow into a process cosplay exercise.
If you want to try this, pick one non-trivial task and make the agent write the spec before it touches code. Review the spec like you would review a technical plan from a teammate. Push back on the boundaries, touchpoints, and verification, then let it implement only after the plan is boringly clear.
For agent work, boringly clear is usually the state I want.
Thanks for reading, see you in the next one!
메타데이터
- post_id
- f8e14538f5cf
- slug
- spec-driven-development-with-agents-71-specs-later-f8e14538f5cf
- url
- https://medium.com/@kzhekov/spec-driven-development-with-agents-71-specs-later-f8e14538f5cf
- canonical_url
- https://medium.com/@kzhekov/spec-driven-development-with-agents-71-specs-later-f8e14538f5cf
- author_url
- https://medium.com/@kzhekov
- status
- ok
- fetched_at
- 2026-06-13 00:08:42