# Working from a spec in small increments

> Write a one-page spec for a due-dates feature in the to-do fixture, have a coding agent split it into increments that each leave the program working, order them with the least certain one first, and land the first one against the spec's own success criteria.

Page: https://schubergphilis.github.io/ai-training/coding-with-agents/spec-driven-increments/
License: CC BY-SA 4.0, https://creativecommons.org/licenses/by-sa/4.0/

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 (The AI-native SDLC playbook, Claude Academy).

## The fixture

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: Show the list

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

```sh
python3 todo.py list
```

Output:

```text
1. [ ] Buy milk
2. [x] Call the plumber
3. [x] Renew the passport
4. [ ] Water the plants
2 open, 2 done
```

#### Example: Run the tests

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

```sh
python3 -m unittest -q 2>&1 | tail -1
```

Output:

```text
OK
```

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.

## Write the spec

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.

```md
# 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: The diff and the spec disagree

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

- Accept the diff. The agent read the code more closely than you did when you wrote the spec.
- Accept the diff and add a comment in the code that explains the difference.
- Reject the diff without comment, because the spec is fixed once the work starts.
- Decide which one is right. Then either change the spec first, or have the agent bring the code back to it.

## Ask for increments in the brief

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 (Developing incrementally, Brilliant, Coding with AI skills map).

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)

```text
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

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

```text
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.

#### Pitfall: The review that never finished

An engineer briefs the agent with the whole spec and no word about
increments. Forty minutes later the agent reports done: two new commands,
a changed output format, date validation, four new tests, and every
criterion ticked. The diff is long, and the review starts on a Friday
afternoon. On Monday it is still open. Nobody wanted to approve a change they
hadn't read to the end, and the two people who started reading each got
halfway. When a bug turns up in `overdue` the following week,
the only way back is to revert the whole feature. Ask for the work in
steps that each leave the program working, and review each one while it
is small.

#### Checkpoint: Which of these are working increments?

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

Select exactly 2.

- The `due` command stores the date, and `list` shows it. Tests pass.
- A `due` function with no command wired to it, so nothing new can be run yet. Tests pass.
- The `overdue` command works, but `list` is broken until the next step fixes the output.
- The `overdue` command reads today from `TODO_TODAY` and the clock, with a test for each date in the spec. Tests pass.

## 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 (Developing incrementally, Brilliant, Coding with AI skills map).

#### Checkpoint: Order the increments

- Add the command that computes something from the new field
- Refuse malformed input to the command that sets the field
- Reword the messages the new commands print
- Store the new field and show it in the output, through every module

#### Checkpoint: An increment nobody is sure about

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?

- Put the outside-service increment last, so the rest of the feature is done and reviewed before the hard part.
- Put the outside-service increment first, as the thin path from a real response to one line of output.
- Build the outside-service increment in a separate branch alongside the others, and merge it when both are done.
- Skip the outside service for now and hard-code its response, so the ordering question goes away.

## Land the first increment

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)

```text
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

```text
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 the suite after the first increment

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

```sh
python3 -m unittest -q 2>&1 | tail -1
```

Output:

```text
OK
```

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

#### Example: Set a date and list

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

```sh
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:

```text
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
```

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`:

```python
        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: Check the criteria

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`.

```sh
python3 criteria.py
```

Output:

```text
criterion 1: pass
criterion 2: pass
criterion 3: not yet
criterion 4: not yet
criterion 5: pass
3 of 5 criteria pass
```

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 (The AI-native SDLC playbook, Claude Academy). 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 (Developing incrementally, Brilliant, Coding with AI skills map).
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.

## References

- The AI-native SDLC playbook, Claude Academy, https://academy.claude.com/courses/ai-native-sdlc-playbook
- Developing incrementally, Brilliant, Coding with AI skills map
