Skip to content

Splitting the work into components the agent can build one at a time

The to-do program gets a feature: “Let me import my old list. I have a text file with one task per line, and I don’t want to type forty add commands.” The last lesson taught how to write success criteria a stranger could check, and this feature has them. In this lesson we take the feature apart before any agent sees it. We split it into components with one responsibility each, we draw the dependencies between them and put them in order, and we write the check for the first component before it exists. Then we run that check and watch it fail, which is the state the agent starts from.

An agent given a whole feature makes the split itself, and it makes a different one every session. It also builds the parts together and tests them at the end, so a mistake in the first part is hidden by the parts built on it. Brilliant’s skills map for coding with AI lists decomposition, dependency analysis, and designing the verification as skills of the engineer [1], and Claude Academy’s playbook for an AI-native development process writes the intent down in a file before the code and keeps the checks running throughout [2]. Both put the split in your hands. The agent gets one component at a time, with its check.

The fixture is the to-do program from Your first session with a coding agent, some weeks later. It has grown a clear command and been split into three modules: todo.py holds the commands, store.py reads and writes the list, and render.py formats it. It is in the course repository under site/examples/coding-with-agents/decomposing-the-work/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. Working inside a clone of the course repository also works, and there git checkout -- . is the reset, but the agent then reads the course’s own instruction files from the directories above the fixture. Claude Code, for example, loads the instruction files in every directory above the one it starts in [3]. The copy is the better default.

Run the program before you plan anything, so you know what the list looks like now.

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/decomposing-the-work/list.py.

The suite passes, and nothing in it changes until you add the first test of this lesson.

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/decomposing-the-work/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 split and the check, because making those is what this lesson teaches.

The success criteria for the import feature: todo import tasks.txt reads a text file with one task per line. Spaces before and after a line are dropped, a line that starts with x is a done item, and blank lines are skipped. A task already in the list isn’t added twice, and the command prints how many items it imported and how many it skipped. add, list, done and clear keep working.

The request names one feature, and the work in it is three components. A component has one responsibility, an interface you can state in a sentence, and a size that fits one agent session and one change you can review in a few minutes. Cut the feature where the data changes form: text lines become items, items are merged into the list, and the list is shown to the user.

ComponentResponsibilityInterface
ParserTurns text lines into itemsimporter.parse_lines(lines) returns a list of items, each {"text": ..., "done": ...}
MergeAdds new items to the list and leaves duplicates outimporter.merge(items, new_items) appends the items whose text isn’t in items and returns the count added
CommandWires todo import <file> to the two above and reportstodo.py reads the file, calls the parser and the merge, prints imported N items, skipped M

Each row answers three questions with one sentence each. If a row needs two sentences for its responsibility, it is two components. If the interface sentence has to describe the code inside, the boundary is in the wrong place. Trailing spaces and the x mark are the parser’s problem and nobody else’s, and “already in the list” is the merge’s problem, so the command has nothing to decide.

Write the list before the brief, and put it in the brief. The agent builds the parser to the interface you named, and the brief for the merge names the same interface, whether the same agent writes it or not.

Checkpoint · multi-choice

Which of these are components with one responsibility?

Section titled “Which of these are components with one responsibility?”

Which of these are components with one responsibility each?

Select exactly 2.

Three components, and they can’t be built in any order. The table fixes the item format, so the merge could be written against it today. Its check couldn’t. The merge’s test has to run on items the real parser produced, or it proves the merge works on items you typed by hand and says nothing about the imported ones. So the dependency runs from a component to the checked output of the one before it. Draw the arrows. The parser has no dependency. The merge depends on a checked parser, and the command depends on both. That order is the plan, and each step is checked before the next one starts.

The order also names the assumption everything rests on: the item format the parser produces. The parser goes first and is checked alone with its own test, and only then is the merge briefed. A component that passes its own check is a fixed point the next brief can name.

The dependency drawing also shows which components have no arrow between them. If the feature had a fourth component, a render change that shows how many items came from an import, its arrow would point at the command and not at the parser. A second agent session could build it once the command was checked, while the merge was still in progress. Naming the dependencies is what tells you what can run side by side and what has to wait.

Checkpoint · order
  1. Brief the parser and check that its test passes
  2. Brief the merge and check its test on items the parser produced
  3. Brief the command that reads the file and calls both
  4. Run the whole import against a copy of the list

The parser doesn’t exist yet, and this is the moment to decide how you know it is right. Write the acceptance test now, put it in the suite, and name it in the brief as the command that means done. The agent then has a target it can run itself, and “done” means the line reads OK instead of the agent saying so.

The test has to do one more thing: it has to fail on a stub. A parser that returns an empty list, or that returns every line as an open item, is the kind of first draft an agent writes when the target is vague. One input with a blank line, a done mark and a line with spaces around it is enough to reject each of those. Create test_importer.py with this content.

import unittest
import importer
class ImporterTests(unittest.TestCase):
def test_parse_lines_keeps_text_and_done_mark_and_skips_blank_lines(self):
lines = ["Buy milk", "", "x Call the plumber", " Water the plants "]
self.assertEqual(
importer.parse_lines(lines),
[
{"text": "Buy milk", "done": False},
{"text": "Call the plumber", "done": True},
{"text": "Water the plants", "done": False},
],
)

Run the suite. importer.py doesn’t exist, so the test can’t even import it, and the suite reports an error.

Example · run it

With test_importer.py in place, run the tests and keep the last line. The course checks this against its own copy of the test, which is the one shown above.

Terminal window
python3 -m unittest -q 2>&1 | tail -1
Output
FAILED (errors=1)

Output verified in CI from site/examples/coding-with-agents/decomposing-the-work/failing_test.py.

Now prove the test does its job. Create importer.py with a stub that has the right name and does nothing, and run the suite again.

def parse_lines(lines):
return []
Example · run it

With the stub in place, run the tests and keep the last line. The course checks this against its own copy of the stub.

Terminal window
python3 -m unittest -q 2>&1 | tail -1
Output
FAILED (failures=1)

Output verified in CI from site/examples/coding-with-agents/decomposing-the-work/stub_test.py.

An error became a failure: the module imports, and the assertion rejects what it returns. Delete importer.py again. The agent writes the real one. Your brief for the first component now fits in a few lines: the parser’s responsibility and interface from the table, the file it may create, and the command that means done.

Add importer.py with parse_lines(lines), which turns text lines into
items as test_importer.py describes. Create only importer.py. Done when
`python3 -m unittest -q` prints OK. Do not change the test.

An agent that can’t make the test pass may change the test until it does, and then the check doesn’t check anything. The last line of the brief closes that door.

Checkpoint · choice

Which of these tests would fail on a stub parser?

Exercise

Reset the fixture and take the import feature apart yourself, without looking at the table above. Write the components as a list with one responsibility and one interface sentence each, draw the dependency arrows between them, and put them in build order. Then write the failing acceptance test for the first component, run python3 -m unittest -q, and check that the last line starts with FAILED. Add a stub with the right name and run it again, and the line must still start with FAILED. Fifteen lines of notes and one test file is enough. You do this so that the agent builds one checked component at a time and never gets to choose the split.

A good result: each component’s interface fits in a sentence, the first one in your order is the one the others take input from, the suite says FAILED before the component exists and again on the stub, and the brief names the test run as what done means. Reflect: which of your components would you have merged if the agent had been given the whole feature, and what mistake could then have hidden inside it?

Stretch: Brief the agent for the parser with the brief above, in a fresh session, and read the diff: it should touch `importer.py` and nothing else.

Recap

  1. A component has one responsibility, an interface that fits in a sentence, and a size that fits one agent session and one reviewable change. Cut where the data changes form.
  2. Dependencies set the build order. The component whose output the others take is built and checked first, so a mistake in it never reaches the next one unseen.
  3. A component checked alone is a fixed point for the next brief. Feed the next component’s test what the first one produces, or the second inherits and hides the first one’s mistakes.
  4. Write the acceptance test before the component exists, put it in the suite and name the test run in the brief as what done means.
  5. A check that passes on a stub doesn’t check the work. Run it against a version with the right name and no work, and the line must still say it failed.

You can now

  • Decomposes a problem into components with clear dependencies
  • Designs how the work will be verified before it is built

  1. Brilliant. Specification and design. Brilliant, Coding with AI skills map. Reference. Brilliant SPC
  2. Anthropic. The AI-native SDLC playbook. Claude Academy. Course. Academy ai-native-sdlc-playbook
  3. Anthropic. How Claude remembers your project. Claude Code documentation. Reference. Claude Code memory