Skip to content

Working from a spec in small increments

In this lesson we add a feature to a small program with a coding agent, and the agent works from a written specification instead of from a chat message. The spec comes first: one page with the commands, the limits and a numbered list of success criteria. Then the agent splits the work into increments that each leave the program working. The increment we’re least sure about goes first, and we land it. While the agent works, the spec stays the document it reads and the one we check the result against. When code and spec disagree, we decide which one changes.

Where a bug fix has one assertion that says what “fixed” means, a feature has decisions in it: a new command, a new field in the data file, a new line in the output. The spec holds those decisions in one place, where the agent can read them and you can check against them. The playbook Anthropic publishes for engineering teams describes the same order of work, the intent and the requirements written down before the build starts [1].

The fixture is the to-do program from the previous lessons, after its clear bug was fixed. It has three modules, todo.py with the commands, store.py for the JSON file, and render.py for the output, and its tests pass. It is in the course repository under site/examples/coding-with-agents/spec-driven-increments/fixture-repo/. Copy that directory to a place of its own and change into it. A copy has no git history, so the reset is to delete the copy and copy again.

Run the program once before you change anything, so you know what the output looks like today.

Example · run it

Run this, and compare what you see with the output below.

Terminal window
python3 todo.py list
Output
1. [ ] Buy milk
2. [x] Call the plumber
3. [x] Renew the passport
4. [ ] Water the plants
2 open, 2 done

Output verified in CI from site/examples/coding-with-agents/spec-driven-increments/list.py.

Example · run it

Run the tests and keep only the last line of the output.

Terminal window
python3 -m unittest -q 2>&1 | tail -1
Output
OK

Output verified in CI from site/examples/coding-with-agents/spec-driven-increments/tests.py.

The examples in this lesson aren’t graded. Each one is a command you run in the fixture, with the output the course itself checks on every build. The checkpoints ask about the spec and the increments, because writing those is what this lesson teaches.

The feature is due dates. An item gets an optional date, list shows it, and a new overdue command lists the items whose date has passed. In Deciding what to build you wrote success criteria for a smaller version of this feature, with the date given on add and “any notion of overdue” out of scope. This spec takes that feature up again and changes two decisions on purpose. The date is set by its own due command, so an existing item can get one. overdue is back in, because it is the reason to store a date at all. Changing the decision in the spec, before any code, is what the spec is for. Before the agent sees any of that, write it down. A spec for a change this size fits on one page: a goal in one or two sentences, the commands with what each one prints, the limits, and success criteria you can check one by one. The limits say what must keep working and what the change may not do. Save it as SPEC.md in the fixture. The course keeps its copy next to the fixture scripts as SPEC.md, and this is the whole of it.

# Due dates
## Goal
An item can carry a due date, and a new command lists the items whose
date has passed.
## Commands
- `due N YYYY-MM-DD` sets the date on item N and prints
`due #N: <text> by YYYY-MM-DD`.
- `list` shows the date after the text: `1. [ ] Buy milk (due 2026-10-01)`.
An item without a date is shown as it is today.
- `overdue` prints the open items whose date is before today, numbered as
in `list`, or `nothing overdue`. `TODO_TODAY=YYYY-MM-DD` overrides today,
so a test can pick the day.
## Limits
- `add`, `list`, `done` and `clear` keep working as they do.
- An item in `todos.json` without a `due` field stays valid. No migration.
- A malformed date is refused with `bad date: <input>` and changes nothing.
- Standard library only.
## Success criteria
1. `due 1 2026-10-01` then `list` shows `1. [ ] Buy milk (due 2026-10-01)`,
and the other lines are unchanged.
2. The committed `todos.json` lists as before, with no date shown.
3. With `TODO_TODAY=2026-10-02`, `overdue` prints item 1 and nothing else.
With `TODO_TODAY=2026-09-30` it prints `nothing overdue`.
4. `due 1 tomorrow` prints `bad date: tomorrow`, exits with status 2, and
the file is unchanged.
5. `python3 -m unittest -q` passes after every increment.

Every line under success criteria is something you can run and tick. The line about TODO_TODAY is there because “before today” depends on the day you run it, and a criterion you can only check on one date isn’t a criterion. Writing the spec made that decision visible before any code existed.

The spec is the source of truth for the change while the agent works. The agent reads it and you review against it, and the agent’s opinion of what the feature should do doesn’t change it by itself. Code and spec disagree at some point, because the agent has a good idea, or because the spec turns out to be wrong. When that happens, one of the two is updated on purpose. Either you change the spec first and then the code follows, or the code is brought back to the spec. What never happens is that the code drifts and the spec stays as a record of what you once meant.

Checkpoint · choice

The agent’s diff prints the date before the text, and the spec says after. Both would work. What do you do?

An agent given the whole spec at once builds the whole spec at once, and hands you one diff with two new commands, a changed output line, input validation and their tests. Reviewing that diff means holding every one of those changes in your head while you read. So the brief asks for increments, and it says what an increment is: a step that leaves the program working and its tests passing, and that adds one thing you can show. Asking for increments is part of the brief, because the agent’s default is the big diff [2].

Start your coding agent from inside the fixture copy and brief it. The brief points at the spec instead of repeating it.

Prompt (illustrative, not a recorded transcript)

Read SPEC.md, AGENTS.md and the three modules. I want to land the due-dates feature in increments. Each increment must leave every existing command working and the tests passing, and add one thing I can run and see. Propose the increments and say which success criteria each one ticks. Don’t change anything yet.

Response

I read the spec and the code. The spec already lists three increments, and I would keep them, with one note on each.

1. `due` sets the field and `list` shows it. Touches todo.py and render.py,
adds test_due.py. Ticks criteria 1, 2 and 5. store.py needs no change,
because json.dump writes whatever keys an item has.
2. `overdue`, reading today from TODO_TODAY or the clock. Touches todo.py,
adds a test with the two dates from criterion 3. Ticks criterion 3.
3. Refuse a malformed date with the standard library's date parser. Touches the
`due` function, adds a test. Ticks criterion 4.

One question before I start. In list, would you like items with a date sorted to the top? The spec doesn’t say.

The transcripts in this lesson are illustrative. They show what a good exchange looks like and don’t record a specific model’s words. Your agent’s list may put the details differently.

The agent’s question is a spec question, and the spec answers it: the overdue line says “numbered as in list”, the list line says nothing about sorting, and the limits say list keeps working as it does. The answer is no, and if you did want sorting, you would add it to the spec before anyone wrote it.

Checkpoint · multi-choice

Which of these steps are increments in the sense the brief asked for?

Select exactly 2.

The increment you are least sure of goes first

Section titled “The increment you are least sure of goes first”

The spec lists three increments, and the order matters. Put first the one whose assumption you are least sure of, so that if it is wrong you find out while the change is small. Here the uncertain part is the data: every module today assumes an item has exactly a text and a done flag, and the first increment adds a field that some items have and some don’t, and carries it through all three modules and the existing tests. If that assumption fails, the data format and every module change, and you would rather know before overdue is written on top of it. The overdue command is arithmetic on a format that already works by then. Date validation waits until after it, even though a bad date could be stored meanwhile. The committed list has no dates and the test from increment 1 pins the format, so the only bad date is one you type yourself before increment 3 is done. Validation guards against input, and the question overdue answers first is whether the stored format is right. Wording changes to the messages come last of all, because nothing depends on them.

That first increment is also the thin end-to-end path: one command in and one line out, through every module the feature touches and nothing more. It proves that the modules pass the new field to each other before any detail is built on them. Early feedback turns a possible rewrite into a small correction [2].

Checkpoint · order
  1. Store the new field and show it in the output, through every module
  2. Add the command that computes something from the new field
  3. Refuse malformed input to the command that sets the field
  4. Reword the messages the new commands print

Checkpoint · scenario

Suppose a different feature, with five increments, and one of them talks to a service nobody on the team has called before. Where does it go in the order?

Ask the agent for the first increment and nothing else. Its own list said which files it touches and which criteria it ticks, so the brief can hold it to that.

Prompt (illustrative, not a recorded transcript)

No sorting. list keeps its order, as the spec says. Land increment 1 only: due and the date in list, with test_due.py. Touch todo.py, render.py and the new test file, and stop when criteria 1, 2 and 5 pass. Report which criteria you checked.

Response

Done. todo.py has a due function and a due branch in main, and render.list_items appends (due DATE) when an item has the field. I ran the suite, then due 1 2026-10-01 and list on a copy of the list, and checked criteria 1, 2 and 5. Criteria 3 and 4 wait for increments 2 and 3. store.py and todos.json are unchanged.

The report is a claim. Run the suite yourself.

Example · run it

Run the tests and keep the last line. The course applies the same increment to its own copy and checks this output against it.

Terminal window
python3 -m unittest -q 2>&1 | tail -1
Output
OK

Output verified in CI from site/examples/coding-with-agents/spec-driven-increments/increment_one_tests.py.

Then run the one thing this increment lets you show, against a copy of the list so the committed file doesn’t change.

Example · run it

Set a date on the first item and show the list.

Terminal window
cp todos.json /tmp/todos.json
TODO_FILE=/tmp/todos.json python3 todo.py due 1 2026-10-01
TODO_FILE=/tmp/todos.json python3 todo.py list
Output
due #1: Buy milk by 2026-10-01
1. [ ] Buy milk (due 2026-10-01)
2. [x] Call the plumber
3. [x] Renew the passport
4. [ ] Water the plants
2 open, 2 done

Output verified in CI from site/examples/coding-with-agents/spec-driven-increments/increment_one_due.py.

The first list line is criterion 1 word for word, and the three items without a date print as they did before, which is the second sentence of criterion 1. The agent’s diff is one new function and one new branch in main in todo.py, one if in render.py, and a test file. The change to render.py:

line = f"{number}. [{mark}] {item['text']}"
if item.get("due"):
line += f" (due {item['due']})"
lines.append(line)

item.get("due") is the whole answer to the question the increment was placed first to ask. An item without the field prints as before, and no migration of todos.json is needed. Had the answer been different, you would know now, with one increment landed instead of three.

The last check is the spec itself. The course keeps a script that runs every success criterion against its copy after the first increment and prints one line per criterion.

Example · run it

In a clone of the course repository, from site/examples/coding-with-agents/spec-driven-increments/, run the criteria check. It copies the fixture, lands the first increment and runs each criterion of SPEC.md.

Terminal window
python3 criteria.py
Output
criterion 1: pass
criterion 2: pass
criterion 3: not yet
criterion 4: not yet
criterion 5: pass
3 of 5 criteria pass

Output verified in CI from site/examples/coding-with-agents/spec-driven-increments/criteria.py.

Three criteria pass and two say not yet, and those two are the ones the agent said belong to increments 2 and 3. That line is what an increment looks like from the spec’s side: the criteria it claimed, and no others. Commit the increment now, before the next one starts, so there is a point to return to and a diff a reviewer can read in one sitting. Then brief the agent for increment 2 the same way, one increment, its files and its criterion.

Exercise

In a fresh copy of the fixture, land increment 2 from the spec with your coding agent: write nothing new in the spec, brief the agent for overdue only, with its files and criterion 3, and let it write the test with the two TODO_TODAY dates before the code. Run python3 -m unittest -q yourself and then the two overdue commands from criterion 3 on a copy of the list. Plan on ten minutes. This is the loop you use for any change whose design has more than one decision in it.

A good result: the suite passes, TODO_TODAY=2026-10-02 shows item 1 and nothing else, TODO_TODAY=2026-09-30 prints nothing overdue, and the diff touches todo.py and one new test file. Criterion 4 is still not yet, and that is right. Finish by deleting your copy and copying the fixture again. Then answer one question: did the agent ask a question the spec should have answered, and what line would you add to it?

Stretch: Now do the same for a feature you choose, for example tags on items with a `tag` command and a `list` filter. Write the spec first, and say in one line which increment is the least certain and why.

Recap

  1. A feature starts from a one-page spec with a goal, the commands, the limits and success criteria you can tick one by one. The spec is the source of truth while the agent works [1]. When code and spec disagree, one of them changes on purpose.
  2. Ask for increments in the brief. Each one leaves the program working and tested and adds one thing you can run, and the agent’s default without that ask is one large diff [2].
  3. Order the increments with the least certain assumption first, as the thin end-to-end path through every module before any detail is built on it.
  4. After each increment, run the tests and the criteria yourself, commit, and only then brief the next one.

You can now

  • Works in small increments, sequenced for early feedback

  1. Anthropic. The AI-native SDLC playbook. Claude Academy. Course. Academy ai-native-sdlc-playbook
  2. Brilliant. Developing incrementally. Brilliant, Coding with AI skills map. Reference. Brilliant INC