3.1 · Spec-Driven Development and the Dispatch Pattern

3.1 · 30 min

Spec-Driven Development and the Dispatch Pattern

"Add rate limiting to the API." Six words, and every one of them is a decision you just delegated by accident: which endpoints, what limits, keyed on what, stored where, returning which status, tested how. Twenty minutes later you have a Redis dependency you did not approve, a middleware rewrite you did not ask for, and rate limiting on the health check endpoint that your load balancer polls every five seconds. Nothing malfunctioned. The agent filled every gap you left with a plausible guess, exactly as lesson 1.4 said it would, and lesson 1.5 gave you names for what grows in those gaps: drift, scope creep, a grader quietly redefined. The fix is not a smarter agent. The fix is a document.

Specification is the job now

Part 0 made the claim that the scarce skill is saying precisely what you want. This lesson turns that claim into a practice with three altitudes, because "spec" means three different documents depending on the decision being made:

  • Product spec. What gets built and why: users, the problem, what v1 excludes. Changes rarely. Lives at the repo root.
  • Technical spec. Architecture, data model, invariants, the design decisions with reasons. Changes on operator decision, not agent whim.
  • Dispatch. One session's worth of work, fully bounded. Written fresh every time, cheap, disposable, and the only one of the three an agent should ever execute directly.

The altitudes exist because handing an agent a product spec produces a guess at the architecture, and handing it a whole technical spec produces a fourteen-file diff no one can review. Each level constrains the one below it; the dispatch is where constraint becomes executable. Anthropic's own guidance for long-horizon work says the same thing from the other end: describe the goal, constraints, existing patterns, non-goals, and the verification step, because time spent making the spec precise pays off more than time spent watching the implementation.

Dispatch anatomy

A dispatch has five sections. Each one exists because a specific failure class from 1.5 walks in without it. Here is the reference template; Parts 4 and 5 of this course hand you pre-written dispatches in exactly this shape, so learn it now.

dispatch templatemarkdown
# DISPATCH: <one-line name>

## GOAL
What done looks like, in one or two sentences, stated as an outcome
("users are rate limited") not an activity ("work on rate limiting").
If you cannot state the goal as an observable outcome, you are not
ready to dispatch: go back up an altitude and decide.

## IN SCOPE
The files, directories, and behaviors this dispatch may create or
change. Name paths. "src/middleware/rateLimit.ts (new), src/app.ts
(mount the middleware only)". Scope is what the diff is allowed to
contain; anything else in the diff is a defect even if it works.

## OUT OF SCOPE
Work an agent would plausibly do while in there, explicitly declined:
refactors "for consistency", dependency upgrades, formatting sweeps,
adjacent bugs (log them, don't fix them). This section is your drift
and scope-creep countermeasure. Write it by asking: what would a
helpful intern over-deliver here?

## DON'T TOUCH
Files that must not change at all, even trivially: test files for this
feature, lockfiles, config, migrations, generated code. Where the cost
of violation is high, back this section with a hook (3.2); the list
still belongs here so the agent plans around the boundary instead of
discovering it.

## VERIFICATION
The checks that must pass before the work counts, decided now, before
any code exists. Two kinds, both required:
1. Machine gates: exact commands ("npm run typecheck && npm test").
2. Operator checks: what you will personally do ("read the middleware
 diff line by line; curl the endpoint 6 times, expect 429 on the 6th").
The agent runs the first kind in its loop. Only you run the second.
A report of "done" without this evidence is a claim, not a result.

Map the sections to the failure classes and the design stops looking like bureaucracy. GOAL kills the wrong-problem failure. IN SCOPE and OUT OF SCOPE are the drift countermeasure from 1.5, the explicit boundary that iteration 30 gets re-anchored against. DON'T TOUCH is aimed at silent scope creep, and at the reward-hacking variant where the "fix" lands in the test file. VERIFICATION is the big one: it takes the definition of success out of the same loop that gets graded by it, which is the only structural defense 1.5 offered against reward hacking. Every section is a scar with a name.

Here is the template inhabited, for a real task on a real repo:

PromptA worked dispatch: rate limiting, bounded

DISPATCH: rate-limit login endpoint

GOAL: failed login attempts are limited to 5 per IP per 15 minutes; the 6th attempt within the window returns 429 with a Retry-After header.

IN SCOPE: src/middleware/rateLimit.ts (new file); src/routes/auth.ts (apply the middleware to POST /login only); tests/middleware/rateLimit.test.ts (new file).

OUT OF SCOPE: rate limiting any other endpoint. Redis or any new dependency; use the in-memory store, we run one process. Refactoring auth.ts beyond the one mount line. The two TODO comments already in auth.ts: leave them.

DON'T TOUCH: tests/auth/login.test.ts (existing login behavior is the baseline); package.json, tsconfig.json, .env files.

VERIFICATION:

  1. npm run typecheck and npm test, all green, no existing test modified
  2. I will run: 6 curl POSTs to /login with bad credentials; expect 401 five times, then 429 with Retry-After
  3. I will read git diff --stat first; more than 3 files touched fails review before I read a single hunk

Before writing code, read src/routes/auth.ts and tell me your plan. If the in-memory store makes the 15-minute window awkward, stop and say so instead of working around it.

Two lines in that dispatch are not template sections and earn their place anyway. The forced plan ("tell me your plan") buys you a cheap review point before any diff exists, the explore-plan-code sequence the docs recommend, compressed into a dispatch. The escape hatch ("stop and say so") gives the agent a sanctioned alternative to the silent workaround, which is where a lot of scope creep is born.

The ask

"Add rate limiting to the API."

Every unstated decision gets made by the agent, silently, at generation time. The diff is a surprise. Review means reverse-engineering intent from code, and "did it work" is the only question you can ask, because you never wrote down what correct meant.

The dispatch

The dispatch above.

The decisions were made by you, at specification time, where they cost words instead of rework. The diff has a contract to be checked against. Review starts from "does this match the spec", which is a faster and harder question than "does this look right".

Living state: the STATUS.md pattern

Dispatches solve one session. Projects are many sessions, and Part 1 taught you why that boundary is dangerous: context dies at /clear, and whatever the last session knew, the next one does not. The docs' answer for conventions is CLAUDE.md. But CLAUDE.md is for what is always true about the repo. Project state, what just happened, what is next, what is broken, changes weekly and needs its own file. The pattern is a STATUS.md that every fresh session reads first and every completed dispatch updates:

STATUS.md templatemarkdown
# STATUS

## Active work
What is being built right now, one line, and the current dispatch
if one is in flight.

## Just shipped
Recent commits with one-line summaries. This is the "previously on"
recap that saves the next session twenty minutes of archaeology.

## Decisions
Choices made and their reasons, dated. The agent reads these so it
stops relitigating them; you read them in three months when you have
forgotten why Redis lost.

## Open questions
Anything needing a human decision. An agent that hits one of these
should add to this list and keep going with what is specified, not
guess.

## Known issues
Real problems found and deliberately not fixed yet. Never silently
ignore a defect: log it here or fix it. The forbidden move is
knowing and not writing it down.

## Don't touch
Standing boundaries that outlive any one dispatch, with reasons.

The rhythm that makes it work: boot a fresh session, the session reads STATUS.md (put "read STATUS.md before anything else" in CLAUDE.md so it happens without you asking), work the dispatch, update STATUS.md as part of finishing, commit, clear. State lives in the repo, not in the conversation, so killing the conversation costs nothing. That one property, sessions being disposable because state is durable, is what Part 1's context discipline was building toward, and it is the foundation the long-horizon harness in 3.9 stands on.

The worked example: the thing you are reading

This course was built by the pattern it is teaching, and the honest version of that story is more instructive than the brochure version, so here it is with the scars left in.

The build started as a spec pack: a product-level README, a technical SPEC.md, a CURRICULUM.md contract, content standards, and ten phase files, each phase file a dispatch with goal, scope, research directives, and a verification gate. A CLAUDE.md carries the standing rules ("work only on the phase named in STATUS.md", "never commit with failing verification"). Every phase runs in a fresh session that boots from STATUS.md, and every phase ends by appending a gate report to it. What the gates caught is the interesting part:

  • Phase 0, the lint gate. The MDX renderer needed a rule suppression with a justifying comment. The first attempt placed the eslint directive where it silently did not apply: nothing errored, nothing warned, the directive just sat there decorating the wrong statement while lint stayed red. A human skimming the file would have said it looked handled. The gate does not skim; npm run lint failed, and the directive had to be moved to the exact offending line before the phase could close. Cheap catch, and exactly the class of "looks done, is not" that skipping gates ships.
  • The stale spec, logged instead of laundered. Mid-build, SPEC.md's prose said 10 to 14 hours of content while the CURRICULUM contract said roughly 20. The tempting move for an agent (you know this from 1.5) is to silently harmonize, pick one number and edit the other file. The phase rules made the spec pack operator-owned, so the discrepancy went into STATUS.md's open questions instead, where a human could rule on it. A discrepancy you can see beats a consistency you cannot trust.
  • A real scope change, handled as a decision, not drift. Partway through, the operator demoted a full RAG build from the capstone to an elective: the goal was judged to be knowing the tools, not shipping that particular feature. The change touched the curriculum, checkpoints, phase files, and lesson stubs. It went in as a dated operator decision in STATUS.md with the full list of what changed, in its own commit. Compare that against 1.5's drift: the difference between a pivot and a failure is not the size of the change, it is whether a human decided it and wrote it down.

None of this required heroics. It required the state file, the gates, and the rule that surprises get logged, not smoothed over.

Where it breaks

Dispatch discipline has its own failure modes. The first is specification theater: a forty-line dispatch for renaming a variable. Rigor is a dial, not a religion; 3.10 gives you the full framework, but the short version is that the dispatch overhead must be proportional to blast radius. A one-line ask with a one-line verification ("rename X to Y everywhere, typecheck green") is a perfectly good dispatch for a trivially reversible change.

The second is specifying what you have not explored. If you do not yet know whether the work belongs in the middleware or the route handler, a confident IN SCOPE list is fiction, and the docs are explicit that vague prompts are the right tool when you are exploring and can afford to course-correct. Explore in plan mode, ask questions, then write the dispatch. Specs are downstream of understanding, not a substitute for it.

The third is the stale STATUS.md, and it is the worst of the three because it fails like context poisoning from 1.5: a fresh session boots from a state file that says the migration is not done when it shipped last week, and every plan built that session stands on the false fact. A state file is only as good as the discipline of updating it at every dispatch boundary. Update it as part of the work, never as a favor to your future self that can be skipped under deadline.

Lablab-3-1Write and execute a full dispatch

Goal: Turn a real task on your practice repo into a five-section dispatch, execute it in a fresh session, and grade the result against the spec you wrote.

Prereqs: the agentic-practice repo from 0.2, with the hooks from 2.6 still installed.

  1. Install the state file: create STATUS.md in the practice repo from the template above, filling in the real state of the repo (what exists, what the 2.x labs left behind, any known issues). Add one line to the repo's CLAUDE.md: "Read STATUS.md before starting any task."
  2. Pick a real small feature for the repo. If nothing calls to you, use: "a repo-stats script that prints file and line counts per directory, with a --json flag for machine-readable output."
  3. Write the dispatch in all five sections. Spend the most time on OUT OF SCOPE (what would an eager intern over-deliver?) and VERIFICATION (exact commands, plus what you personally will check). Save it as dispatches/2026-07-repo-stats.md so it is an artifact, not a chat message.
  4. Open a fresh session. Paste the dispatch. When the plan comes back, check it against IN SCOPE before approving; a plan that mentions files outside scope gets corrected now, not after the diff exists.
  5. When the agent reports done, run the VERIFICATION section yourself, in order, starting with git diff --stat against the IN SCOPE list.
  6. Close the loop: update STATUS.md (just shipped, any new open questions), commit the work and the dispatch file together, and /clear.

Verify

  • A committed dispatches/*.md file with all five sections, none empty.
  • git diff --stat at review time showed only files named in IN SCOPE. If it showed more, you caught it and redispatched with a tightened scope, which also counts as a pass: the point is that the boundary was checked.
  • Every command in your VERIFICATION section was actually run, by you, and passed.
  • STATUS.md's "Just shipped" section describes the change, and a fresh session asked "what's the state of this repo?" answers correctly from it.
>Troubleshooting
  • The plan step came back as code instead of a plan: your dispatch buried the "tell me your plan first" line. Move it to the end of the dispatch, where recency gives it weight (1.4), or run the session in plan mode so planning is enforced by the harness instead of the prose.
  • The agent honored the letter of IN SCOPE but changed a file "trivially" (formatting, import order): that is what DON'T TOUCH is for. Add the file there and note in the dispatch that trivial diffs are still diffs.
  • Your VERIFICATION section turned out to be unrunnable ("works correctly" is not a command): rewrite it until each line is either a command with an expected result or a concrete action you will take. This rewrite is the actual skill; most first dispatches fail here.

Knowledge check

Knowledge check

Q1Your dispatch said 'add a --json flag to the report script'. The diff adds the flag, and also renames three functions in the script 'for clarity' and reorders its imports. Which dispatch section was missing or too weak, and which 1.5 failure class walked through the gap?
Q2Why does the VERIFICATION section have to be written before the work starts, rather than after the agent reports done?
Q3A fresh session boots, reads STATUS.md, and builds a plan on its claim that 'the auth migration is still in progress'. The migration actually shipped ten days ago; nobody updated the file. What is this failure equivalent to, mechanically?
Q4You are about to dispatch 'migrate the session store from memory to SQLite' but you genuinely do not know yet which modules touch the session store. What does this lesson say to do?

Sources

  • Best practices for Claude Code (be specific upfront with the before/after prompt table, explore-plan-code workflow, verification criteria in the prompt, CLAUDE.md guidance, long-task spec advice, vague prompts for exploration): https://code.claude.com/docs/en/best-practices (fetched July 2026)
  • Common workflows (plan mode via --permission-mode plan and Shift+Tab, worktrees for parallel dispatches, resuming sessions): https://code.claude.com/docs/en/common-workflows (fetched July 2026)
  • The build-story claims about this course's own repository (lint-gate catch, logged spec discrepancy, RAG scope decision) are from its STATUS.md history and git log, verified July 2026.