DEV Community

Cover image for Spec-driven development in reverse: write the code, keep the receipt
Ivan Yush
Ivan Yush

Posted on

Spec-driven development in reverse: write the code, keep the receipt

A while ago I had to make a change in a project with no SDD framework at all: no proposal, no design, no delta specs, not even a modest tasks.md hiding in a corner. I braced myself for a long agent onboarding, an hour of wandering through the code, mixing up modules and asking where the entry point is. Instead the model just worked out the code, found the spots that needed touching, made the change, and got through the code owner's review without drama, all without a single spec.

After that I looked at my work project on OpenSpec, at its openspec/ folder with a couple hundred files in it, and had to ask: why are we generating, storing and committing a pile of artifacts that a modern model doesn't need? Who is all this paperwork for, if its main reader gets along fine without it?

That question turned into SlimSpec, a plugin for Claude Code. It keeps the parts of SDD that work and throws out the rest. On a real project a comparable change cost it 29.7M tokens versus 45.7M for OpenSpec, and not a single line of process files ended up in git. This post covers where the idea came from, how the flow works, the numbers, and the bill. Spoiler: there is a bill. There's no free lunch, even when the lunch is made of tokens.

Why we needed SDD in the first place

Not long ago models were weaker, context windows were small, and subagents didn't exist. The routine was: write a detailed plan in one session, implement it in the next. The model kept losing context, like a goldfish discovering the little castle all over again on every lap of the tank. Project memory had to live somewhere, or every session started with "hi, I'm the new junior, walk me through everything from the top."

Spec-driven development was the fix for precisely that: small context, no subagents, the need for a detailed plan. You write it all down and put the paper in the repo, and the next session reads the paper. It worked like clockwork, so it became the standard.

Around the same time, the two frameworks I'd call the big ones showed up: OpenSpec and Superpowers. I worked with both, and OpenSpec became my daily driver. What I liked about them:

  • the explore mode (Superpowers calls it brainstorming), where the design takes shape before the plan. Probably the best thing to happen to AI-assisted development so far: think first, write code second, not the other way around;
  • the plan and the specs themselves;
  • in OpenSpec's case, spec merging too: the living spec keeps only what matters. Superpowers has nothing like it and just keeps piling up design files, like the drawer of plastic bags under the kitchen sink: throwing them out feels wrong, and finding the one you need stopped being possible long ago.

All of it fit neatly into the SDD flow the community had settled on.

Where OpenSpec started to drag

In my pet projects I used OpenSpec with cheaper Chinese models for a long time and saw no problems. If anything, the opposite: code got easier to write and maintain, semantic duplicates went away (you know, when a project suddenly turns out to have three date-formatting functions), and the agent got things right more often.

Our team picked OpenSpec for a commercial project too, and at first I was all in. A couple of months later it was obvious the flow was painfully slow, especially once we dropped manual review. We had a corporate subscription and no reason to save on limits, we ran several features in parallel across a row of terminals, and the process still crawled like a DMV line for a form certifying that you already have the form.

What slowed it down:

  • After the proposal come four more artifacts: design, tasks, delta specs and the rest. We barely read them, but they were dutifully written, committed and shipped off to the archive. Did everyone get the memo about the new cover sheets? We did. Every single time.
  • Any edit to an unarchived change turns into yet another change. Changed your mind halfway through? Kindly fill out form 27B/6.
  • A rebase is mandatory before archiving. After that you have to get the agent to re-check the spec merge, because someone else may have touched the section your MODIFIED block lands in. Archive day had strong "this is fine" dog energy: coffee in hand, room on fire.

My personal bureaucracy record: I needed to remove a feature, a piece of a screen nobody needed anymore. A deletion task, what could be simpler? Delete the code, delete the tests, close the ticket. The final diff:

Pull request summary: 99 files changed, 5,218 lines added, 5,495 removed, for a task that only deleted a feature

Almost 5,500 lines went out and almost as many came in. What came in, of course, was specs: a proposal on why we're deleting, a design on how we're deleting, tasks on what order we're deleting in, and REMOVED deltas on what exactly got deleted. To throw out 5k lines I had to write 5k lines. Yo dawg, I heard you like deleting code, so we put specs in your deletion. It's like hauling an old couch to the dump, and the dump wants a bill of sale, a condition report and a photo of every scratch first.

Then came that project with no framework, and it all clicked.

The main session is the design

I've been handing all the mechanics off to subagents for a while now. My main session has turned into a conversation: I brainstorm the feature with the agent, and the design and the plan take shape right there. Deviations from the plan get recorded in the same place without opening a new change. If a follow-up starts to grow, the agent itself suggests moving it out of scope, into non-goals. Nobody files a form. I say "let's do it differently", and that's that.

So I already have a source of truth for the feature: the session itself. "Wait, the session was the spec all along?" Always has been. Files only matter in case the session gets lost or chewed up by compaction. Think of a reserve parachute: good to have, but you don't expect to pull it on every jump. Hence "SDD in reverse": the spec doesn't drive the work, the work leaves a spec behind when it's done.

Artifacts per change: OpenSpec writes proposal, design, tasks and delta specs and archives them in git; SlimSpec keeps two gitignored files, explore.md and tasks.md, distills them into the module CLAUDE.md and deletes them

I built my own framework around that idea: take the best of what came before and make it lighter, faster and cheaper. A greatest-hits album of other people's frameworks, remastered with TDD.

How SlimSpec works

Inside there are only skills and agent definitions for Claude Code. There are no scripts, hooks or CLI, so there isn't much to break. Install and run:

/plugin marketplace add yushman/slimspec
/plugin install slimspec@slimspec

/slimspec <idea>
Enter fullscreen mode Exit fullscreen mode

The whole loop in one picture:

The SlimSpec cycle: explore, gate, plan, gate, apply, verify, ground; verify sends implementation bugs back to apply and design questions to refine

It all starts with explore. A scanner subagent reads the module specs, then the code, and comes back with a short brief: what's here, where things live, what might break. Then the main session grills me, one question per decision, with independent questions batched together. It feels like a job interview where the candidate is your own idea, and that's useful: half of the "obvious" decisions stop looking obvious by the second question. The result goes into explore.md: goal, non-goals, decisions with rejected alternatives, risks. There's no separate proposal or design; explore is the spec of the change. The session never moves on to the plan by itself; that's my call.

In plan, the planner lays the work out in tasks.md as waves. Each wave is split into lanes of related tasks. Lanes in the same wave touch non-overlapping files, so they run in parallel. You eat an elephant one bite at a time, but if the bites are on different plates, you can eat several at once. Each task gets its first failing test written down right away.

tasks.md split into waves; each wave is divided into parallel lanes of related tasks that touch disjoint files, one implementer per lane

apply is the short one: one implementer per lane works through its tasks in order and reports red and green for each. After each wave the main session runs the full test suite, so two parallel lanes don't step on each other's toes.

verify is more interesting. The reviewer checks the result against the plan and the design, and makes sure each red run failed for a real reason, not because of a typo or a forgotten import. A test that fails with ModuleNotFoundError isn't red, it's just broken. Task failed successfully. Code review is delegated to the built-in /code-review. Bugs in the code go back to apply automatically. Design questions come to me through refine: it patches explore.md and sends only the invalidated tasks back to apply, like a cashier voiding three items instead of ringing up the whole cart again. If two automatic fix rounds don't help, the session pulls me in.

ground gets its own section below, because that's where the important part is.

There are two stops: after explore and after plan. At each one the session walks me through everything in detail. After explore that's the design, with before/after diagrams and path:line references; after plan, all the waves, lanes and the test for each task. Everything else runs on its own and only pings me with questions. You can go get a coffee. You can even get a second one.

Subagents don't talk to the user

The main session makes the decisions. Subagents stay in their lane, literally: do your part, report back. What happens in the subagent stays in the subagent. The report is short and structured, without logs or diffs:

status: DONE | BLOCKED | FAILED
task: T3
files: [src/auth/token.ts, src/auth/token.test.ts]
decisions: ["used existing clock abstraction"]
deviations: ["renamed validate → verifyToken"]
tdd:
  red: "token.test.ts: rejects expired token — FAIL (expected 401, got 200)"
  green: "12 passed"
questions: []
Enter fullscreen mode Exit fullscreen mode

Hit an open question? The agent stops with BLOCKED instead of guessing. The main session asks me and relaunches the agent with the answer. Its context stays free of walls of test logs, yet every decision and every outcome is in there.

The main session in the center makes all decisions; scanner, planner, implementers, reviewer and grounder send it short YAML reports; a BLOCKED question goes through the main session to the user and the answer goes back to the agent

TDD and review are baked into the flow

TDD and code review are table stakes by now, so I'd rather they live in the framework than in somebody's self-discipline. Discipline, as we all know, runs out on Friday evening. TDD is enforced by evidence rather than a hook: verify won't accept a task without a red run. You shall not pass! (Gandalf, part-time reviewer.) Where TDD doesn't apply (config, styles, migrations), the task is marked test: none — <reason> with an alternative check, and all such exemptions are shown at the plan gate. You can't sneak an "oh, no tests needed here" into the middle of the list.

Two temporary files

A change lives in .slimspec/changes/<slug>/ as explore.md and tasks.md. SlimSpec adds the folder to .gitignore itself and deletes it after ground. No eulogy, and none needed: by then everything useful in it has already moved into the specs. History lives in the commits and the updated specs. Lost the session? /slimspec <slug> picks up where you left off.

Grounding: the only thing left of SDD

The last step, ground, is probably the only thing SlimSpec still has in common with classic SDD.

Durable knowledge gets distilled from the change: behavior, invariants, contracts, decisions. The plan and the process get thrown out, Marie Kondo style, after you thank them for their service. Who's going to care in six months that task T3 was in the second wave? Nobody. The distilled knowledge is then merged into the module spec. Phil Karlton said there are only two hard things in computer science; he never had to merge delta specs. Here's the main difference from OpenSpec: instead of ADDED/MODIFIED/REMOVED blocks, the merge is done by an agent that understands why the change was made and how it's designed. If the change contradicts something the spec says, that statement gets rewritten instead of getting a new one appended next to it. Otherwise the spec quickly turns into "it used to be like this, but then it became like that, although see item 7". Finally, the agent checks that the updated spec covers the change's goal and doesn't contradict the decisions that were made.

Ground: durable knowledge is distilled from the change, merged by the model into the module CLAUDE.md, checked against the goal of the change, and the change folder is deleted

The result goes into the nested CLAUDE.md of the relevant module. Claude Code loads these files on its own whenever it works in that directory, so the spec ends up in context for free, right where it's needed. One spec per module instead of a scatter of per-capability files. No need to remember which spec to feed the agent: it walks into the module, and the spec is already there.

The numbers

I moved two of my projects to SlimSpec. The measurements come from one of them, looqlog, a log viewer in Rust with a web UI. It used to live on OpenSpec and moved over with /slimspec:migrate-openspec.

Migration

We had 23 spec files, 23 archived changes, 200 files in openspec/ and 733 KB of history. Not that the agent needed all of that for the trip, but once you start collecting artifacts, it gets hard to stop.

OpenSpec SlimSpec
Behavior specs 23 files, 2,941 lines, ~155 KB 3 files, 484 lines, ~34 KB
Change archive 23 changes, 12,394 lines, ~733 KB 0
Root CLAUDE.md 130 lines 121 lines
Files in openspec/ 200 0

Bar chart of the looqlog migration: behavior specs 2,941 lines in OpenSpec vs 484 in SlimSpec, change archive 12,394 lines vs 0, files in openspec/ 200 vs 0

WHEN/THEN scenarios were folded into the requirements they described. Empty Purpose sections and boilerplate SHALLs are gone (SHALL, SHALL NOT, SHALL ALWAYS... the specs read like an army field manual). The agent reads only the spec of the module it's working in: an edit in web/ pulls in ~11 KB, not capability files from all over the tree. Four SlimSpec changes later, the module specs have grown to 4 files and 641 lines. That's still 78% less than OpenSpec had, and the growth tracks the code, not the paperwork.

What one change costs

I counted from Claude Code transcripts. Tokens here mean all the input the model processed (input + cache read + cache write), main session and subagents together. The closest pair in size:

OpenSpec (logcat-padded-tags) SlimSpec full (iana-tz-hash)
Code changed 409 lines, 4 files, 1 module 435 lines, 15 files, 3 modules
Tokens processed 45.7M 29.7M (−35%)
Tokens per line of code ~110k ~68k (−38%)
Review none 2 verify rounds at depth high, one real blocker fixed
Process files committed ~530 lines 0
Time, gates included 91 min 56 min

And SlimSpec did more work in that run: 5 decision questions, 3 gates, 9 tasks in 3 waves, ground into 4 specs, an ADR and both READMEs. The blocker the reviewer found was a panic on extreme dates, the kind of bug that shows up in prod at exactly 11:59 PM on December 31st. The OpenSpec run had no review at all.

Bar chart of tokens per change: OpenSpec 45.7M vs SlimSpec full 29.7M for the comparable pair; small changes OpenSpec 31.5M and 45.7M vs SlimSpec fast 6.8M and medium 12.5M; tokens per line of code ~110k vs ~68k

Small changes tell the same story. Two OpenSpec changes processed 31.5M and 45.7M tokens, two SlimSpec ones (fast and medium) 6.8M and 12.5M. A single OpenSpec implementer chewed through 13–16M tokens on its own; a SlimSpec subagent, 0–1M. Each one gets a single narrow lane and doesn't have to read the whole world to change one function.

Now the caveats, which I'd rather name myself before the comments do. n = 1–2 per approach, one project. The plural of anecdote isn't data, and I barely have the plural. The small SlimSpec changes were 5–10× smaller in code, so part of the gap is just size. Some SlimSpec agents ran on Sonnet or Haiku. On the other hand, the main session of the full run was dragging along unrelated context from earlier work, which inflates its count. So treat the numbers as an observation rather than a benchmark, though a hard one to ignore.

What didn't work along the way

It all looked great on paper. Some things I built one way first and then had to redo.

An implementer per task. It looked beautiful: a fresh agent, a narrow context, everything neat and tidy. In practice every cold start re-read explore.md, the specs and the code. Every task meant another round of "hi, so what's this project about?". 50 First Dates, with tokens on the meter. Now one implementer runs a lane of related tasks.

Worktrees for isolation in full mode. That didn't fly: a subagent's worktree branches off the last commit and can't see the uncommitted results of earlier waves. The second wave was building the second floor of a house with no first floor. I'd have had to commit after every wave. Instead I kept parallelism over non-overlapping files in a single working tree.

I wanted a hook that wouldn't let you edit code without a failing test. Sounds strict and reliable. But a prompt hook can't verify that a red run actually happened, and plugin hooks are always on, whatever the mode. A security guard who can't check badges, posted at every door. The check moved into verify.

Setup had its own comedy moment. First I wrote a bash wizard: menus, prompts, the works. Textbook yak shaving. Then it turned out a skill can't drive an interactive script, and ! in the session runs without a TTY. The wizard was pretty, it just couldn't be launched. Now /slimspec:init simply asks its questions right in the session.

And one small thing that annoyed me every day: in explore, questions came strictly one at a time. You waited a full round trip per decision, even when all the questions were known upfront. Very "your call is important to us, please stay on the line". Now independent ones come in a batch, up to four at a time.

What it cost

Here's the receipt, everything that went out the door along with the extra artifacts:

  • Scenarios as a ready-made list of test cases are gone.
  • Rationale for individual changes is partly lost. Some of it survived in ADRs, the rest lives only in git history. If you enjoy archaeology with git log, you'll love it.
  • The largest module spec (~14 KB) is loaded whole for any work in that module.
  • It only works with Claude Code. OpenSpec supports 30+ AI tools; Superpowers also works with many agents and does a bunch of things beyond specs: debugging, worktrees, review. SlimSpec only covers the spec-driven loop and doesn't pretend to be a Swiss Army knife.

Tuning it for your project

Config is optional, and every key has a default. Don't want to configure anything? Then don't; it'll run as is. /slimspec:init asks everything in the session and writes .slimspec/config.yaml:

mode: medium              # fast | medium | full
spec:
  format: claude-md       # claude-md | readme | central | native | none
  unit: auto              # auto (by build files) | dir glob, e.g. "src/*/"
  adr: false              # also keep decisions in docs/adr/NNNN-*.md
test: auto                # or an explicit command
commit: none              # none | lane | wave
apply:
  agents: 3               # max parallel implementers
review:
  depth: auto             # auto (by mode) | low | medium | high
models: {}                # per-role models: scanner, planner, implementer, reviewer, grounder
Enter fullscreen mode Exit fullscreen mode

mode sets how heavy the loop is. In fast, explore and plan share one file, and you get at most three questions, one implementer and review at low. That's for "change the button text without losing your mind". medium adds the scanner, grilling, the planner and parallel lanes, with review at medium. full adds an explicit gate on explore.md and review at high, for changes you'd still like to sleep at night after. You don't even have to pick: after the scan, /slimspec suggests a mode based on the size of the change.

spec.format decides where specs live. The default is nested CLAUDE.md files. You can also use a README.md next to the code, a shared specs/ folder for features that cut across modules, the language's native docs (Go doc.go, Rust //!, Python docstrings, KDoc), or none, in which case ground is skipped. That one's for the "the code is the documentation" crowd.

spec.unit says what counts as a module; auto finds the boundaries from package.json, go.mod, Cargo.toml, build.gradle(.kts), pyproject.toml.

commit turns on a commit after each lane or wave. Only the main session commits, agents never do. SlimSpec can't push at all, and that's a feature: no surprises in main at 3 AM.

models assigns a model per role. By default the scanner and implementer run on Sonnet, and the planner, reviewer and grounder on Opus. Thinking is expensive and doing is cheaper; that's the whole economics of it.

If your project already runs on OpenSpec or Superpowers, there's /slimspec:migrate-openspec and /slimspec:migrate-superpowers. Only specs are carried over: each one is mapped to modules, compressed and checked against the code. Where the code and the spec disagree, you decide, because sometimes the spec lies and sometimes the code does, and guessing isn't an option. Old files are deleted only after you confirm.

Bottom line

Strong models don't need heavy specs. Tell them what to do and where to look, and they'll reconstruct the rest from the code faster than they could read four artifacts. The tokens that go into a proposal nobody will open are better spent on the next feature, or better yet, on a new project. A million tokens here, a million there, and pretty soon it adds up to that pet project that's been sitting on the "someday" list for ages.

If a slow SDD flow is grinding you down too, try SlimSpec on a single change and compare it with your current process. I'm especially curious how it holds up in a team and on projects bigger than mine. Leave a comment or open an issue. Roasts are welcome too, just bring numbers.

Top comments (0)