Break work into steps
- Lesson 6
- 45 min
- Module 2: Write actionable tasks
Lesson 6 of 15 · AI-Assisted Software DevelopmentBreak work into steps
1. Establish a safe working setup
2. Write actionable tasks
3. Review generated work
4. Debug and test with assistance
5. Deliver maintainable work
What you will be able to do
When the code is new to you, do not ask for the whole change at once. Ask for an investigation and a plan first, then one step at a time, and check each step before the next. Each piece is small enough to understand.
Before you startWhat to know and to have ready
What you need to know
- The task brief of the category option, from Problem and acceptance criteria, and the bug report, from Supply relevant context.
- How a pytest fixture gives a test its data, from Focused tests.
- How to read a diff, from Read and review diffs in Git and Team Development Essentials.
What you need on your computer
- The project
~/projects/ticket-cleaner-assistwith the brief and the bug report committed, and its environment active.python -m pytestprints25 passed, andgit statussaysworking tree clean. - If the project is missing or broken, use the reset steps of Context and boundaries, then do the steps of the last two lessons again.
A builder in an old house
Section titled “A builder in an old house”You hire a builder to put a new kitchen into an old house. A good builder does not start by knocking down walls. First, they look: where do the pipes run, and is that wall holding up the floor above? Then they give you a plan, room by room, with what each part costs. You agree to it. Then they do one part, and you both check it before the next one starts.
If the builder finds an old lead pipe on the way, they tell you. They do not quietly build the new kitchen around it.
The problem: everything at once, in code you do not know
Section titled “The problem: everything at once, in code you do not know”Amira has not worked on ticket-cleaner since the Python course. In Problem and acceptance criteria, the assistant did the whole feature in one turn (transcript feature-bounded). All 8 checks passed. But to accept the change, Amira must read 5 files and 149 changed lines at once. They hold the option, the filter, the new key, the tests and the README. In code that is new to her, she cannot easily tell a good line from a strange one.
And something is missing from that turn. The new filter compares the chosen names with each ticket’s category, as clean_category() stored it. In Grace’s export, a stored category can still be LOGIN, because of Grace’s bug. Then --category login misses that ticket. The change does not say so, and its tests use small letters only. Nobody decided this: it just happened.
The idea: four stages
Section titled “The idea: four stages”When the code is new to you, separate the work into four stages:
- Investigate. Read the code and explain how it works now. Change nothing. Many assistants have a mode for this, where they may answer but not edit files. Aider calls it ask mode. In a chat window, every turn is like this.
- Plan. Write the change as small, numbered steps. For each step, name the file that changes and the test that shows the step works. You read the plan and approve it, or change it.
- Change one step. Ask for that step only, and then stop.
- Verify. Run the tests and ruff, and read the step’s diff with the plan beside it. Then go back to stage 3 with the next step.
The investigation also gives you facts: statements about the current code that you can check. A fact that the change must keep can become a test before you change anything. Then the test protects it during every step.
A tiny example: one question, one fact
Section titled “A tiny example: one question, one fact”Criterion 5 of the brief says that valid_records and rejected must not change with --category. Before you add the option, ask a smaller question: do they change with --status today?
Read build_summary() in report.py: line 29 is "valid_records": len(tickets),. tickets is the list of every valid ticket. The status filter makes a second list, selected, and only the counts after it use that list. So the answer is “no”. Now check it by running the program:
| Command | valid_records |
rejected rows | selected |
|---|---|---|---|
--status open |
9 | 6 | 4 |
--status pending |
9 | 6 | 2 |
--status closed |
9 | 6 | 3 |
The first two columns stay the same; only selected changes. That is a fact about the current code. It also tells you where the new step belongs: after the status filter, next to selected, and away from valid_records.
Text description of the diagram
The diagram follows the sample file through build_summary(). data/tickets.csv has 15 rows. split_records() makes 9 valid tickets and 6 rejected rows. filter_by_status() keeps the 4 open tickets: billing 2, login 1 and shipping 1. Next is a dashed green box, the new step 1: keep the chosen categories. With –category billing, 2 tickets stay. Last, average_priority() gives 2.5, and count_by_category() gives {"billing": 2}. A note under split_records() says that this step counts valid_records 9 and rejected, 6 rows, from all the tickets. So the status and the categories do not change them. That is criterion 5. A second note says that the investigation’s test pins this: valid_records is the same for open, pending and closed.
The recorded session: plan first
Section titled “The recorded session: plan first”The course team gave the same brief and the same five files to the same model, in two turns (transcript feature-plan-first). In the first turn, Aider was in ask mode, so it could not change files. The brief ended with this request:
Do not change any file yet. First read the code and explain, in a few lines, how a ticket's category goes from the input file to the report. Then propose a plan in small numbered steps. For each step, say which file changes and which test shows that the step works.The assistant explained the flow, and asked for three more files: files.py, parsing.py and records.py. “I won’t change these files,” it added. Then it gave a plan of four steps:
1. **Add optional category filtering to the report.** Change: `ticket_cleaner/report.py`, `tests/test_report.py`. …2. **Expose the repeatable CLI option.** Change: `ticket_cleaner/cli.py`, `tests/test_cli.py`. …3. **Document the option.** Change: `README.md`. …4. **Verify the completed change.**With the three new files, it confirmed the flow, and it found something:
3. `clean_category()` strips surrounding spaces and maps aliases such as `Sign-in` to `login`. **Other names retain their capitalization**—for example, `LOGIN`stays uppercase.That is Grace’s bug, found by reading the code. The one-turn session never mentioned it.
In the second turn, the prompt was short:
Do step 1 of your plan only, with its test. Then stop.The assistant changed only report.py and tests/test_report.py: +82 −4. It said “No CLI or documentation changes are included.” Then the checks ran: 33 passed, and 1 of 8 hidden checks. That is the right result for step 1: the option --category does not exist yet, so the checks that use it must fail.
Text description of the diagram
The diagram shows four stages in a row. An arrow goes from the fourth stage back to the third, labelled “next step of the plan”. Stage 1, investigate: read and change nothing. In turn 1, in ask mode, the assistant explained how a category reaches the report. It asked for 3 more files, and found that “LOGIN stays uppercase”. No file changed. Stage 2, plan: small numbered steps. The plan had 4 steps, and each named the file that changes and the test that shows it works. You read and approve it. Stage 3, change one step and stop. Turn 2 asked: “Do step 1 of your plan only, with its test. Then stop.” It changed 2 files, 82 lines added and 4 removed. Stage 4, verify before the next step. The tests gave 33 passed. ruff format –check said that 2 files would be reformatted. 1 of 8 acceptance checks passed, because the –category option does not exist yet. A note below says that the same brief in one shot (feature-bounded) gave 5 files, +145 −4, to review at once.
Verify step 1: read it with the plan
Section titled “Verify step 1: read it with the plan”Step 1 is 86 changed lines, not 149, so Amira can read every line. One new test stops her:
def test_category_comparison_preserves_stored_labels(): tickets = [ Ticket(id="T-1", status="open", category="Billing", priority=3), ] summary = build_summary(tickets, [], status="open", categories=[" billing "]) assert summary["selected"] == 1 assert summary["by_category"] == {"Billing": 1}In turn 1, the assistant found that LOGIN stays in capital letters. In turn 2, it built the filter around that: it compares in small letters, but the report keeps Billing. This test then makes the split a promise: by_category must say {"Billing": 1}. When Module 4 fixes Grace’s bug, this test fails.
This is what the stages are for. The investigation found a fact. The decision about it belongs to Amira: the bug is a separate task, with its own report. So the plan says that the option does not work around it. Amira’s review of step 1 rejects this test. The change that the team accepts in Module 3 does not work around the bug either: you check that there.
Write the plan and pin the fact
Section titled “Write the plan and pin the fact”You do the investigation yourself, add its test, and commit the plan.
-
Find what
build_summary()counts, and where:Terminal window grep -n "def \|valid_records\|rejected" ticket_cleaner/report.pyTerminal window Select-String -Pattern "def |valid_records|rejected" -Path ticket_cleaner\report.pyOutput (macOS) 4:def filter_by_status(tickets: list[Ticket], status: str = "open") -> list[Ticket]:9:def count_by_category(tickets: list[Ticket]) -> dict[str, int]:17:def average_priority(tickets: list[Ticket]) -> float | None:24:def build_summary(tickets: list[Ticket], rejected: list[Rejected], status: str) -> dict:29: "valid_records": len(tickets),33: "rejected": [{"row": r.row, "problems": r.problems} for r in rejected], -
Check the fact with the program. Run it three times, with
--status open,--status pendingand--status closed. After each run, openreports/summary.json:Terminal window python -m ticket_cleaner data/tickets.csv --status pendingEach report has
"valid_records": 9and the same six rejected rows.selectedis 4, 2 and 3, as in the table above. -
Pin the fact in a test. Copy
tests/test_report.pyfrom the snapshotend-of-m02, then look at the diff:Terminal window cp ../nextia-learning-labs/C15/snapshots/end-of-m02/tests/test_report.py tests/git diffTerminal window Copy-Item ..\nextia-learning-labs\C15\snapshots\end-of-m02\tests\test_report.py tests\git diffOutput (the end) @@ -30,3 +30,11 @@ def test_summary_for_a_status_with_no_tickets(tickets):assert summary["selected"] == 0assert summary["by_category"] == {}assert summary["average_priority"] is None+++def test_whole_file_counts_do_not_depend_on_the_status(tickets):+ rejected = [Rejected(row=4, problems=["missing id"])]+ for status in ("open", "pending", "closed"):+ summary = build_summary(tickets, rejected, status=status)+ assert summary["valid_records"] == 3+ assert summary["rejected"] == [{"row": 4, "problems": ["missing id"]}]The diff also adds
Rejectedto the import at the top of the file. The test uses the file’s fixturetickets: three tickets, two open and one closed. For each status, it checks thatvalid_recordsis 3 andrejectedis the same. -
Run the tests:
Terminal window python -m pytestOutput (the end) ============================== 26 passed in 0.04s ============================== -
Optional: check that the test can fail. In
ticket_cleaner/report.py, change line 29 to"valid_records": len(selected),, and runpython -m pytest tests/test_report.py:Output (part) E assert 2 == 3FAILED tests/test_report.py::test_whole_file_counts_do_not_depend_on_the_status========================= 1 failed, 4 passed in 0.02s ==========================With
open, the status filter keeps two of the three tickets, so the wrong code gives 2. Then put the line back withgit restore ticket_cleaner/report.py. -
Copy the plan, and run the checks:
Terminal window cp ../nextia-learning-labs/C15/snapshots/end-of-m02/docs/tasks/category-option-plan.md docs/tasks/Terminal window Copy-Item ..\nextia-learning-labs\C15\snapshots\end-of-m02\docs\tasks\category-option-plan.md docs\tasks\Terminal window python -m pytestruff check .ruff format --check .Output (the end of each command) ============================== 26 passed in 0.03s ==============================All checks passed!20 files already formatted -
Commit the test and the plan together:
Terminal window git add tests/test_report.py docs/tasks/category-option-plan.mdgit commit -m "Plan the category option, and pin what the investigation found"Output [feature/category-option c15e9d8] Plan the category option, and pin what the investigation found2 files changed, 43 insertions(+), 1 deletion(-)create mode 100644 docs/tasks/category-option-plan.md
Here is Amira’s plan:
# Plan: the --category option
Written before any change, after reading `cli.py`, `report.py`, `parsing.py`and the tests.
## How a category reaches the report now
1. `files.read_rows` reads each row as text.2. `parsing.to_ticket` cleans it: `clean_category` changes the web form's names (`Sign-in` to `login`).3. `report.build_summary` keeps the tickets of the chosen status (`filter_by_status`), then counts them by category (`count_by_category`).4. `cli.main` saves the summary as JSON.
`valid_records` and `rejected` come from all the tickets, before the status ischosen. A test now shows that (`test_whole_file_counts_do_not_depend_on_the_status`).
## Found while reading
`clean_category` keeps the capital letters of a name that is not one of theweb form's names, so `LOGIN` and `login` are two categories. That is Grace'sbug (`docs/tasks/category-names-bug.md`), a separate task. The option comparesthe chosen names with the stored category, and does not work around the bug.
## Steps
| Step | Change | File | Test that shows it works ||---|---|---|---|| 1 | Keep the tickets of the chosen categories, after the status | `report.py` (`build_summary` gets `categories`) | `test_report.py`: one, two, none, an unknown category || 2 | Add `--category` (repeatable) and pass it on | `cli.py` | `test_cli.py`: the command with `--category " Billing "` || 3 | Add the `categories` key only when the option is used | `report.py` | `test_baseline.py` still passes; a new test for the key || 4 | Describe the option | `README.md`, `--help` | `test_cli.py`: `--help` shows `--category` |
After each step: `python -m pytest`, `ruff check .`, `ruff format --check .`.It has the same shape as the assistant’s plan, with two differences. It records the finding and Amira’s decision about it. And each step has its own test, so the verify stage has something to run.
When to break the work up, and what it costs
Section titled “When to break the work up, and what it costs”Break the work up when the code is new to you, when the change touches several files, or when a step is hard to undo. A plan also helps a reviewer: the pull request can follow it, step by step.
It costs more turns. The plan-first session made 3 model calls and cost US$0.050, and it did only step 1. The whole feature in one turn made 1 call and cost US$0.032. The larger cost is your time: you read a plan and approve each step. Prices change, and with a free tier or no assistant you pay nothing.
Do not plan a one-line fix in code that you know: the plan is longer than the change. And a plan is not a promise. If step 2 shows that step 3 is wrong, change the plan, in the file, and say why.
Common mistake
The step passes its tests and fails the format check
Symptom: after step 1, python -m pytest prints 33 passed, but ruff format --check . prints 2 files would be reformatted. This happened in the recorded session.
Cause: the assistant wrote lines longer than the project’s limit. Tests check behaviour, not style, so they cannot see this. A step that is “done” by its tests is not done by the brief, whose constraints ask for ruff format --check . to pass.
Fix: verify every step with all three checks of the plan, not only the tests. Run ruff format . and read git diff afterwards, because it changes files. The scope file says so too. Then run the three checks again before the next step.
Module practice: a feature request and a minimal bug report
Section titled “Module practice: a feature request and a minimal bug report”The module’s practice task: write a feature request and a minimal bug report for the practice project, as Amira did. Work in a disposable copy, so that your project stays as it is.
- In
~/projects, copy the snapshotend-of-m02to a new folder,ticket-cleaner-try. Make its environment, and check thatpython -m pytestprints26 passed. - The feature. Grace asks: “Can the report also say how many of the selected tickets are urgent?” In
ticket-cleaner, priority 1 is the most urgent level:Ticket.is_urgent()returnsTruefor it. Writedocs/tasks/urgent-count.mdwith the five parts of a brief. Give at least three criteria with commands ondata/tickets.csvand exact expected values. - The bug. Grace’s team says that a new form sends the category
log in, with a space. Make a minimal input of two rows, and run it. Then writedocs/tasks/log-in-bug.md: the observation, the expected result, the minimal example, the file references and what is not affected. Is this Grace’s bug again? - Delete the folder when you finish.
Show an example answer
The values. The open tickets of data/tickets.csv are T-1001 (priority 2), T-1003 (1), T-1005 (3) and T-1015 (2): one urgent ticket. Pending: T-1004 (2) and T-1010 (1): one. Closed: T-1002 (3), T-1006 (1) and T-1012 (3): one.
# Task: count the urgent tickets in the report
Request: Grace wants to see how many of the selected tickets are urgent.
Observed now: the report has no count of urgent tickets.
Expected: the report has a key "urgent": the number of selected ticketswith priority 1.
## Acceptance criteria
1. `python -m ticket_cleaner data/tickets.csv` gives "urgent": 1 (T-1003).2. With `--status pending`, "urgent" is 1 (T-1010); with `--status closed`, it is 1 (T-1006).3. With no selected tickets, "urgent" is 0, not null.4. Every other key of the report stays the same. `tests/test_baseline.py` gets the new key in the same commit, after Grace confirms that her spreadsheet accepts it.
## Constraints
- Change only ticket_cleaner/report.py, README.md and the tests. No new dependency. `ruff check .` and `ruff format --check .` pass.
## Evidence of done
- `python -m pytest` passes, with new tests for criteria 1 to 3.- The commands of criteria 1 and 2 give the values above.Criterion 4 is the hard one. A new key changes every report, so the baseline test of Module 1 must fail. The brief says that, and names the decision that Grace must make first.
The bug. The minimal input and its result:
id,status,category,priority,subjectT-1,open,login,1,OneT-2,open,log in,1,Twopython -m ticket_cleaner two-names.csv --output reports/two-names.json prints 2 rows: 2 valid, 0 rejected., and the report has:
"by_category": { "log in": 1, "login": 1},- Expected:
{"login": 2}, if Grace confirms thatlog inis the login team. - File references:
ticket_cleaner/records.py:CATEGORY_ALIASEShaslog-inandsign-in, but notlog in.ticket_cleaner/parsing.py:clean_category()looks names up there. - Not affected, and not Grace’s bug:
log inis in small letters, so capital letters are not the cause. The name is missing from the list of web form names. Fixing Grace’s bug alone does not fix this one. Say so in the report, so that nobody merges the two.
Module check
Section titled “Module check”The module’s completion check: someone else could check your task without guessing what you meant. Test your own brief with these four questions. Answer each one with yes or no:
- Does every criterion have a command or a test, and an exact expected result?
- Did you calculate the expected results before any code exists?
- Do the constraints name the files that may change?
- Could a colleague who has not read Grace’s email decide “done” or “not done” with your brief alone?
Then apply them to this brief from Tomás:
# Task: urgent tickets
Add the urgent tickets to the report. Make sure it works and is tested.Don't break anything.Show the answer
It fails all four questions. “Add the urgent tickets” can mean a count, a list of IDs, or a filter. “Make sure it works” has no command and no expected value. Nothing names the files, and “don’t break anything” does not say whether a new key is allowed in every report. A colleague must ask Tomás, and an assistant would decide alone, as in the vague session. The example answer of the module practice passes all four questions.
You have finished the module when you can also show these on your own project:
-
git log --onelineshows six commits onfeature/category-option. The last three areWrite the task brief for the category option,Write Grace's bug report with a minimal inputandPlan the category option, and pin what the investigation found. -
python -m pytestprints26 passed, andruff format --check .prints20 files already formatted. -
On macOS and Linux, this command prints nothing, because your project matches the snapshot
end-of-m02:Terminal window diff -rq . ../nextia-learning-labs/C15/snapshots/end-of-m02 -x .git -x .venv -x reports -x __pycache__ -x .pytest_cache -x .ruff_cache
Reset. If your project is broken, follow the reset steps of Context and boundaries with the snapshot end-of-m02 in place of end-of-m01. The last line of python -m pytest is then 26 passed.
Knowledge check
Section titled “Knowledge check”Check your understanding
Name the four stages for a change in code that is new to you, and say what each stage produced in the recorded session feature-plan-first.
Show the answer
- Investigate: turn 1, in ask mode. The assistant explained how a category reaches the report, asked for three more files, and found that
LOGINstays in capital letters. No file changed. - Plan: four numbered steps, each with the files that change and the tests.
- Change one step: turn 2, “Do step 1 of your plan only, with its test. Then stop.” Two files, +82 −4.
- Verify:
33 passed, butruff format --check .failed on 2 files. 1 of 8 hidden checks passed, which is right for step 1. The review found a test that makes Grace’s bug a promise.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Why does the test test_whole_file_counts_do_not_depend_on_the_status come before any change, and what does it protect in step 1?
Show the answer
It pins a fact of the current code: valid_records and rejected describe the whole file, whatever the status. The investigation found the fact by reading line 29 of report.py and running three statuses. If it is a test before the change, it protects the fact during every step. Step 1 adds a filter next to the status filter. If the filter goes in the wrong place, and the whole-file counts start to follow it, this test fails. That would break criterion 5 of the brief.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
In turn 1, the assistant found that LOGIN stays uppercase. In turn 2, it wrote a test that asserts {'Billing': 1}. What should Amira do, and why is it her decision?
Show the answer
She rejects that test, and she writes the finding into the plan as a separate task: Grace’s bug, with its own report. The test makes the bug a promise, so the fix in Module 4 would break it. An assistant can find a fact, but the decision about it belongs to the person who owns the task. Is it a bug, should this change work around it, and who must agree? Here, Grace already reported it, so the option does not work around it.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Recap
This lesson
- In code that is new to you, separate four stages: investigate, plan, change one step, verify. Then the next step.
- The recorded investigation changed nothing and found Grace’s bug. Step 1 alone was +82 −4, small enough to read, and the review found a test that made the bug a promise.
- An investigation gives facts. Pin a fact that the change must keep in a test, before the change:
test_whole_file_counts_do_not_depend_on_the_status. - The finding is the assistant’s; the decision is yours.
Module 2: write actionable tasks
- A task brief has observed and expected behaviour, inputs, constraints and evidence of done. Its acceptance criteria each have a check with an exact result.
- Relevant context is the observation, the expected result, a minimal example, file references and interfaces, and no secrets.
- Your project matches
end-of-m02: six commits onfeature/category-optionand26 passed.
Next, in Module 3, an assistant’s change for the feature arrives, and you review it line by line.
Next: Inspect the diff