Skip to content

Keeping every agent change easy to undo

In this lesson the agent commits the three increments of the due-dates spec one by one on a branch, and then we take the middle one out again with one command. The point of the exercise is the boundary between the commits. A change on a branch, in commits small enough to revert one at a time and with no step in it that can’t be undone, is safe to hand to an agent. The worst outcome costs a revert. The second half of the lesson covers the changes that don’t revert that way, data and schema, and then the commit messages and the pull request description that let a reviewer see what the agent did and what was checked.

The previous lesson landed increment 1 of the spec and stopped. Here the agent does all three, and the commits are the record of the work. The course keeps scripts that apply the increments to a copy for you, and each example below names its script.

Start from a fresh copy of the fixture from Working from a spec in small increments, with its SPEC.md, and make the copy a git repository before the agent touches it: git init, one commit with the program as it is, one commit with the spec, and a branch due-dates to work on. The branch is what makes the whole change reversible at once. Deleting it, or leaving it unmerged, undoes everything, and main never changes while the agent works. Coding agents commit when asked, and the brief has to ask. Left alone, most agents edit until the task is done and leave you one working tree with every change in it [1].

Prompt (illustrative, not a recorded transcript)

Read SPEC.md and land its three increments on this branch, one commit per increment. After each increment run python3 -m unittest -q, and commit only when it passes. Use a one-line commit message that starts with feat: and says what the increment adds. Don’t touch main, don’t rewrite a commit once it exists, and don’t run any git command that deletes or force-pushes anything. Stop after the third commit and show me git log --oneline main..HEAD.

Response

The branch due-dates has one commit per increment, and the suite passes after each one.

a3f9c1e feat: refuse a malformed date
5b27d04 feat: overdue command with TODO_TODAY
e81a6b2 feat: due command and the date in list

Each commit touches todo.py and adds one test file. The first also changes render.py. store.py and todos.json are unchanged.

The transcripts in this lesson are illustrative. They show what a good exchange looks like and don’t record a specific model’s words. The hashes in your log differ from the ones above, so the course’s own check prints the log without them.

Example · run it

In a clone of the course repository, from site/examples/coding-with-agents/reversible-changes/, run the script that commits the three increments on a copy. Its output is the commit subjects on the branch, newest first, with the last line of the test run under them.

Terminal window
python3 commits.py
Output
feat: refuse a malformed date
feat: overdue command with TODO_TODAY
feat: due command and the date in list
OK

Output verified in CI from site/examples/coding-with-agents/reversible-changes/commits.py.

The examples in this lesson aren’t graded. Each one is a command you run, with the output the course itself checks on every build. The checkpoints ask about which changes revert and what a reviewer needs to read, the judgment this lesson teaches.

Now the reason for the boundaries. Suppose the overdue command turns out to be wrong in a way you don’t want to fix today, and due and the date check should stay. With three commits, the fix is one command that adds a fourth commit, and nothing is rewritten.

Terminal window
git revert --no-edit HEAD~1
Example · run it

The course’s script makes the three commits and reverts the middle one. Then it prints the log, the last line of the test run, the output of due 1 tomorrow, and the exit status of overdue.

Terminal window
python3 revert.py
Output
Revert "feat: overdue command with TODO_TODAY"
feat: refuse a malformed date
feat: overdue command with TODO_TODAY
feat: due command and the date in list
OK
bad date: tomorrow
overdue exits with status 2

Output verified in CI from site/examples/coding-with-agents/reversible-changes/revert.py.

The revert is a fourth commit, so the history still says that overdue was there and was taken out. The suite passes, because the test for overdue left with the code it tested. The third increment still refuses a bad date, and overdue now gets the usage message and status 2, as any unknown command does. Had the agent made one commit for the whole feature, taking overdue out would have meant editing the code by hand and hoping the tests still cover what’s left. The revert only works because the second commit changed the lines that belong to overdue and no others. That’s a property of the brief as much as of the agent. “One commit per increment” gave the agent a boundary to keep, and the increments from the spec gave it boundaries that make sense.

Checkpoint · choice

Reverting the middle commit removed overdue, kept due and the date check, and left the suite green. Which property of the work made that possible?

Everything so far is reversible because it is text in a repository. A revert undoes it, and the branch bounds it. Some steps an agent can take have no undo, and those are the ones to find in the plan before they run. In a code repository they’re the git commands that throw work away. reset --hard discards every uncommitted change to a tracked file, and clean -f deletes the untracked files. Neither has a way back, because nothing was ever committed. The commits that reset --hard or branch -D move away from are less lost than they look: git keeps them for a while, under ORIG_HEAD and in the reflog, and the documentation of reset shows the way back [2]. push --force overwrites a branch on the remote, where no reflog of yours applies. Outside the repository the steps with no undo are a deleted file that isn’t tracked, a dropped table, an email sent, a payment made, a deploy to a system other people use. The brief above rules out the git commands by name. The rule in general is that a step with no way back is the step you read before it runs, and the agent runs everything else without waiting for you [3].

The question to ask of every step is “if this is wrong, what undoes it, and how long does that take?” When the answer is “a revert, in a minute”, the agent may take the step. When it is “nothing” or “a restore from last night’s backup”, the step is yours to approve.

Checkpoint · sort

Which steps can the agent take on its own?

Section titled “Which steps can the agent take on its own?”

Data changes with the way back written down

Section titled “Data changes with the way back written down”

Code is stored in the repository, and a revert brings back the old version in a second. Data is stored in a database, a file or an index, and it has no history unless you keep one. When a change touches data, the code revert alone isn’t enough, because the old code meets data in the new form. So a data change is designed with its way back written down before it runs, and the write-up is part of the change the reviewer reads.

The usual design is to add before you remove. The migration adds the new column, and the code starts writing to both columns. Then a backfill copies the old column into the new one for every row written before that, and the code starts reading from the new one. Dropping the old column is a separate change, days later, after the new code has run in production and nothing still reads the old one. After each of those steps, the previous version of the code works against the current table, so a revert of the code is still a revert. The to-do fixture follows the same rule in miniature: the spec says an item without a due field is still valid, so the first increment added a field that some items have and some don’t, and an older todo.py still reads the file.

Cheaper still is a change that can be turned off. A feature flag or a configuration switch that’s off by default lets the new code ship in the same deploy as the old, and turning the new behavior off takes a setting change and no deploy at all. When the agent proposes a change whose undo is “revert and redeploy”, ask whether it could be “flip the switch” instead. Ship the new ranking behind a flag, enable it for the team first, and the way back is a configuration edit. The flag is removed later, in its own commit, once the old path has no callers.

Checkpoint · order
  1. Add the new column and backfill it from the old one
  2. Deploy code that writes to both columns and reads from the new one
  3. Deploy code that writes only to the new column
  4. Drop the old column, in its own change, after the previous step has run for a while

The commit message and the pull request description

Section titled “The commit message and the pull request description”

The commits are on the branch, and a reviewer now reads them. Two texts carry the change to the team: the commit messages and the pull request description. A commit message says what changed and why, in a line a reader of git log can act on. The messages on the branch do that: feat: overdue command with TODO_TODAY tells the reader what the commit adds and how it picks the day, and a revert of it explains itself. A message like update todo.py says where the diff is and nothing the diff doesn’t say better.

The pull request description is where the reviewer’s attention is set. It names the spec the change follows, and the reviewer checks against the same document the agent worked from. It says what was verified and how, criterion by criterion. The reviewer then knows what has been checked and what’s left to them. And it says what the agent did and what you did, because a reviewer reads a diff differently when they know a machine wrote it and a person checked the criteria. That last line is the information the reviewer needs to spend their time where a human hasn’t looked yet. Ask the agent to draft it from the commits and the spec, and then read it against what you did.

Prompt (illustrative, not a recorded transcript)

Draft the pull request description for this branch. Name the spec, list the increments as committed, and say for each success criterion how it was checked. I ran criteria 1, 3 and 4 by hand on a copy of the list; you ran the suite after each commit. Say which of us did what.

Response

The draft below follows the commits and the spec.

# Due dates
Implements `SPEC.md` (due dates) in three increments, one commit each,
on top of the spec commit, in the order the spec lists them.
## Changes
- `due N YYYY-MM-DD` sets a date on an item, and `list` shows it.
- `overdue` lists open items whose date is before today.
`TODO_TODAY=YYYY-MM-DD` overrides today.
- A malformed date is refused with `bad date: <input>`, status 2, and
the file is unchanged.
## Verification
- Criterion 1, 3, 4: run by hand by the author on a copy of `todos.json`.
- Criterion 2: the committed `todos.json` has no `due` field, and
`list` shows it as before. Checked by the author.
- Criterion 5: `python3 -m unittest -q` passes after each commit. Run by
the agent after each increment and by the author at the tip.
## How this was made
A coding agent wrote the code and the tests from `SPEC.md` and made the
three commits. The author wrote the spec, reviewed each diff, and
checked the criteria above. `store.py` and `todos.json` are unchanged.

Read the draft against what happened. The verification section makes claims, and each one is a line you can check. The line about criterion 2 says the file has no due field, and git diff main -- todos.json says so in a second. The line about criterion 5 says the agent ran the suite after each increment, and git log can’t show that, so the description is where it is written down. A reviewer who reads this knows that the suite was the agent’s check, the criteria were the author’s, and the author read every diff. Their time goes to the code itself.

Checkpoint · multi-choice

What does the reviewer need from the description?

Section titled “What does the reviewer need from the description?”

Which of these belong in the pull request description for the due-dates branch?

Select exactly 3.

Exercise

In a fresh copy of the fixture, make a git repository with the two commits and the due-dates branch as this lesson did. Brief your coding agent to commit the three increments of SPEC.md one by one, with the rule that it commits only when the suite passes and doesn’t run a git command that deletes or rewrites. Then revert the middle commit yourself, run the suite, and have the agent draft the pull request description with the spec, the criteria, and who checked what. Plan on fifteen minutes. After this, a change an agent made is a change your team can review and, when needed, undo.

A good result: four commits on the branch after the revert, OK from the suite, due 1 tomorrow still refused, and a description whose verification lines you can each check in a command. Finish by deleting your copy. Then answer one question: which line of the description could you not check from the repository alone, and where’s it written down instead?

Stretch: Then have the agent put the `overdue` command back with `git revert` of the revert, and check that the description still tells the truth. Which lines had to change?

Recap

  1. A change is reversible when it is on a branch in commits small enough to revert one at a time, and no step in it is one that can’t be undone. That’s what makes it safe to let an agent try [1].
  2. Ask for one commit per increment in the brief, and rule out the git commands that discard history by name. The step with no way back is the one you read before it runs [3].
  3. A data or schema change is designed with its way back written down before it runs: add and backfill first, drop later, and prefer a switch that turns the change off over a revert that needs a deploy.
  4. The commit message says what changed and why. The pull request description names the spec, says what was verified and how, and says what the agent did and what you did. The reviewer then spends their time where no person has looked yet.

You can now

  • Keeps every change reversible

  1. Brilliant. Developing incrementally. Brilliant, Coding with AI skills map. Reference. Brilliant INC
  2. The Git project. git-reset. Git reference documentation. Reference. Git docs git-reset
  3. DeepLearning.AI. Claude Code: A Highly Agentic Coding Assistant. DeepLearning.AI. Course. DLAI-5