fix(M1-6): apply AI-drives-git reframe, lesson=theory, de-slop, + issue fixes

Phase 1 of the reframe. M1-3 stay manual-by-hand (browser chat); M4 is the pivot
to the AI agent (Claude Code as example); M5-6 are agent-driven.

- M1: de-slop (em-dashes), relocate the build-note out of the lab. Seam devices kept.
- M2: #78 tell learner how to paste cli.py into chat; #79 commit the delete so the
  tree ends clean. restore/cold-session devices kept.
- M3: #80 define ADR; #81 create-file-before-add; #82 ls before/after merge to prove
  branch isolation; #83 drop "prose"; M3 now owns the branch-basics intro.
- M4: #84 Claude Code as the worked example; #85 AI drives git (arithmetic->calculator);
  #86 /path/to -> ~/ai-workflow-course; #87 agent does the revert+verify.
- M5: #88 ask the agent which config files to commit, then let it stage/commit
  (CLAUDE.md example; repo still uses AGENTS.md).
- M6: #90 stop re-teaching branch basics; rescope to the AI experimenting on a branch;
  the engineered conflict is now AI-resolved, learner-verified.

Closes #78
Closes #79
Closes #80
Closes #81
Closes #82
Closes #84
Closes #85
Closes #87
Closes #88
Closes #90

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT
This commit is contained in:
2026-06-22 21:41:14 -04:00
parent 2467f25901
commit 896c4606c6
8 changed files with 578 additions and 481 deletions
+39 -35
View File
@@ -19,7 +19,7 @@ command is shown and explained.
By the end of this module you can:
1. Articulate *why* the chat-to-file copy-paste loop fails not vaguely, but at the three specific
1. Articulate *why* the chat-to-file copy-paste loop fails: not vaguely, but at the three specific
seams where it breaks.
2. State the course thesis and explain what "the workflow is the durable skill" means for your own
work.
@@ -44,20 +44,20 @@ Here is the workflow almost everyone starts with, and it genuinely works for a w
7. Go to 2.
For a single file you're poking at for an afternoon, this is fine. The friction is low and the
results are real. The problem isn't that this loop is *bad* — it's that it **doesn't scale along the
two axes every real project grows on: more than one file, and more than one day.**
results are real. The problem isn't that this loop is *bad*. It's that the loop **doesn't scale along
the two axes every real project grows on: more than one file, and more than one day.**
### Seam 1 — More than one file
The moment your project is two files instead of one, the chat window loses the thread. You paste in
`cli.py`, ask for a change, and the AI confidently edits it — but the change actually needed to touch
`cli.py`, ask for a change, and the AI confidently edits it. But the change actually needed to touch
`tasks.py` too, which it can't see because you only pasted one file. Or it *can* see it because you
pasted both, but now its reply rewrites both files and you're hand-merging two blobs of text back
into two real files, hoping you didn't drop a function in the shuffle.
You become the integration layer. Every change is a manual diff you perform in your head, between
what's in the chat and what's on disk. That's slow, and worse, it's *error-prone in a way you can't
see* there's no record of what actually changed.
see*: there's no record of what actually changed.
### Seam 2 — More than one day
@@ -71,33 +71,32 @@ disk, so every session starts cold.
### Seam 3 — No undo, no record, no safety
This is the quiet one, and it's the most dangerous. When the AI confidently makes a mess deletes a
This is the quiet one, and it's the most dangerous. The AI confidently makes a mess. It deletes a
function you needed, "refactors" something into a subtly broken state, rewrites a file you'd carefully
tuned — what's your recovery plan?
tuned. What's your recovery plan?
Right now it's probably: *Ctrl-Z until it looks right*, or *paste the old version back from the chat
history if I can find it*, or, too often, *retype it from memory*. There is no checkpoint you can
return to and no record of what changed between "working" and "broken." You're doing high-wire work
with no net, and the AI makes it *easier* to do a lot of risky changes fast — which means you fall
more often.
with no net, and the AI makes it *easier* to do a lot of risky changes fast. So you fall more often.
### The reframe
Notice what all three seams have in common: **none of them are about the AI's intelligence.** A
smarter model writes better code, but it doesn't give you a record of changes, a way to undo a mess,
or a memory that survives a closed tab. Those come from the *engineering scaffolding around* the
model version control, a real editor integration, hosting, review, automation.
model: version control, a real editor integration, hosting, review, automation.
That scaffolding is what this course teaches. And here's why it's worth your time specifically now:
> **The model is the cheap, swappable part. The workflow around it is the skill that lasts.**
Models change every few months. The one you're using today will be replaced probably by something
cheaper and better and when that happens, your prompts mostly carry over and your habits fully
Models change every few months. The one you're using today will be replaced, probably by something
cheaper and better, and when that happens your prompts mostly carry over and your habits fully
carry over. The version-control discipline, the review reflex, the CI pipeline, the way you give an
agent a branch instead of your whole repo *none of that depends on which model you run.* You learn
agent a branch instead of your whole repo: *none of that depends on which model you run.* You learn
it once and it pays out across every model you'll ever use. That's why this course is deliberately
model- and vendor-agnostic: we're teaching the part that doesn't expire.
model- and vendor-agnostic. We're teaching the part that doesn't expire.
---
@@ -107,14 +106,14 @@ A generic "intro to developer tools" course would teach the same git, the same e
CI. What makes this one different is that **AI changes the cost-benefit of every tool in it**, and
usually makes the tool *more* valuable, not less:
- AI makes changes **faster and more confidently** including the wrong ones. That raises the value
- AI makes changes **faster and more confidently**, including the wrong ones. That raises the value
of an undo you can trust (Module 2) and a review gate (Module 10).
- AI **can't remember** across sessions but your repo can. Version control becomes durable memory
- AI **can't remember** across sessions, but your repo can. Version control becomes durable memory
the AI reads back (Module 2).
- AI generates code that **looks right** and passes a human skim. That's exactly what automated
testing and CI exist to catch (Modules 1314).
- AI itself can become a **teammate inside the workflow** opening PRs, triaging issues, fixing
failing builds but only safely once the scaffolding is there to catch it (Unit 5).
- AI itself can become a **teammate inside the workflow**, opening PRs, triaging issues, fixing
failing builds, but only safely once the scaffolding is there to catch it (Unit 5).
You don't adopt this toolchain *despite* using AI. You adopt it *because* you're using AI. The pain
you already feel is the curriculum.
@@ -148,7 +147,7 @@ purpose** so you recognize it later.
Everything you'll run in this course lives in one repo. Grab it once, up front — no tools required
beyond a web browser:
1. Open the course's home page **`https://git.jpaul.io/justin/ai-workflow-course`** and use its
1. Open the course's home page, **`https://git.jpaul.io/justin/ai-workflow-course`**, and use its
**Download ZIP** (archive) link.
2. Unzip it under your home directory so the course's `modules/` folder lands at
`~/ai-workflow-course/modules/`. (Rename the unzipped folder to `ai-workflow-course` if your download
@@ -157,12 +156,9 @@ beyond a web browser:
You now have every module's files locally, including this one's under
`modules/01-the-copy-paste-problem/`.
> *A cleaner, **updatable** way to get the repo `git clone` arrives in **Module 8**, once you've
> *A cleaner, **updatable** way to get the repo, `git clone`, arrives in **Module 8**, once you've
> learned Git (Module 2). A one-time ZIP is all you need today; don't reach for `clone` yet.*
> *Verify-before-publish: confirm this download URL points at the published course host before
> shipping.*
### Part A — Stand up the project
1. Make a working directory and copy in the starter app from this module's `lab/starter/` folder:
@@ -197,16 +193,17 @@ You now have every module's files locally, including this one's under
Now reproduce each failure deliberately. Keep the AI strictly in the **browser chat** — no
editor-integrated tools yet (those arrive in Module 4). This is the "before" picture on purpose.
1. **Seam 1 (multiple files).** First mark a task done so there's something to hide `python cli.py
done 0`, then `python cli.py list` shows it as `[x]`. Now paste *only* `cli.py` into your chat and
ask: *"Make the `list` command hide tasks that are already done."* Apply whatever it gives you and
run `python cli.py list`. The clean version of this change lives in `tasks.py` the file you
*didn't* paste: open it and you'll see `render()` already owns the `[x]`/`[ ]` box-and-index
formatting, and a `pending()` helper already returns exactly the not-done tasks. But the chat
never saw that file, so it had to either guess at methods it couldn't see (and `python cli.py
list` errors out) or reach into the raw task list and *re-create* that box-and-index formatting
inside `cli.py` duplicating logic that already existed one file over. Either way, *you* had to
be the one who knew the change really belonged in the other file.
1. **Seam 1 (multiple files).** First mark a task done so there's something to hide. Run `python
cli.py done 0`, then `python cli.py list` shows it as `[x]`. Now paste *only* `cli.py` into your
chat and ask: *"Make the `list` command hide tasks that are already done."* Apply whatever it
gives you and run `python cli.py list`. The clean version of this change lives in `tasks.py`, the
file you *didn't* paste: open it and you'll see `render()` already owns the `[x]`/`[ ]`
box-and-index formatting, and a `pending()` helper already returns exactly the not-done tasks. But
the chat never saw that file, so it had to do one of two things. Either it guessed at methods it
couldn't see (and `python cli.py list` errors out), or it reached into the raw task list and
*re-created* that box-and-index formatting inside `cli.py`, duplicating logic that already existed
one file over. Either way, *you* had to be the one who knew the change really belonged in the
other file.
2. **Seam 2 (across time).** Close the chat tab. Open a new one. Ask it to *"continue where we left
off."* Watch it have no idea what you were doing. The project's real state is sitting right there
@@ -229,9 +226,9 @@ Be honest about the limits of this module's claims:
- **Copy-paste isn't *wrong*, it's *unscalable*.** For a one-file throwaway script, the loop is
genuinely the fastest path. Don't over-engineer a five-line utility. The toolchain earns its keep
as soon as a project has a second file or a second day which is most of them, but not all.
as soon as a project has a second file or a second day, which is most of them, but not all.
- **Tools don't fix judgment.** Version control will let you undo a bad AI change instantly; it won't
tell you the change was bad. That skill reviewing AI output is its own module (10), and no
tell you the change was bad. That skill, reviewing AI output, is its own module (10), and no
amount of scaffolding replaces it.
- **This module doesn't make you faster yet.** Setup rarely does. The payoff compounds over the next
six modules. If it feels like overhead right now, that's expected.
@@ -251,3 +248,10 @@ Be honest about the limits of this module's claims:
If all three are true, you're ready for Module 2, where we install the safety net that makes the
rest of the course safe to attempt.
---
## Verify-before-publish
- [ ] Confirm the **Download ZIP** URL (`https://git.jpaul.io/justin/ai-workflow-course`) points at
the published course host before shipping.
@@ -1,6 +1,6 @@
# Module 2 — Version Control as a Safety Net
> **Version control is undo for the AI and it's the AI's memory between sessions.** This is the one
> **Version control is undo for the AI, and it's the AI's memory between sessions.** This is the one
> module that makes every riskier thing in the rest of the course safe to attempt.
---
@@ -11,7 +11,7 @@
felt the three seams where copy-paste breaks. This module installs the fix for the third seam (no
undo, no record) and, surprisingly, the second (no memory across time) as well.
You do **not** need Git installed yet that's the first step of the lab.
You do **not** need Git installed yet; that's the first step of the lab.
---
@@ -19,13 +19,13 @@ You do **not** need Git installed yet — that's the first step of the lab.
By the end of this module you can:
1. Initialize a repository and capture your work as commits checkpoints you can always return to.
1. Initialize a repository and capture your work as commits: checkpoints you can always return to.
2. Read what changed with `git status`, `git diff`, and `git log`, and undo unwanted changes with
`git restore`.
3. Recover cleanly after an AI confidently makes a mess, without retyping anything.
4. Use the repo as **durable memory**: have a fresh AI session reconstruct "where were we?" entirely
from Git, with no chat history.
5. Explain the one thing Git *can't* see and why that's the argument for committing often.
5. Explain the one thing Git *can't* see, and why that's the argument for committing often.
---
@@ -35,10 +35,10 @@ By the end of this module you can:
Strip away the open-source mythology and Git is one thing: **a tool that records snapshots of your
files over time and lets you move between them.** Each snapshot is a *commit*. A commit is a labeled
checkpoint "here is exactly what every file looked like at this moment, and here's a note about
checkpoint: "here is exactly what every file looked like at this moment, and here's a note about
why." You can compare any two checkpoints, and you can return to any of them.
That's it. Everything else branches, remotes, merges is built on "snapshots you can move
That's it. Everything else (branches, remotes, merges) is built on "snapshots you can move
between." For now we only need the local core: `init`, `commit`, `diff`, `log`, `restore`.
### Reframe 1 — Commits are undo for the AI
@@ -48,12 +48,12 @@ Module 1's third seam was: when the AI makes a mess, you have no checkpoint to r
1. Get the project to a working state.
2. **Commit it.** Now this exact state is saved forever, with a message.
3. Let the AI try something anything, however risky.
3. Let the AI try something, anything, however risky.
4. If it worked, commit again. If it didn't, **`git restore` throws away the mess and you're back at
step 2's checkpoint, byte for byte.**
This is the unlock for the whole course. Every later module asks you to let the AI do something
bolder edit real files (Module 4), work on a branch (Module 6), open a PR (Module 10), run
This is what the whole course is built on. Every later module asks you to let the AI do something
bolder: edit real files (Module 4), work on a branch (Module 6), open a PR (Module 10), run
unattended (Unit 5). You can say yes to all of it *because* you can always get back to a known-good
checkpoint. Without this, every AI change is a gamble. With it, the downside is "throw away five
minutes of work."
@@ -72,17 +72,17 @@ git restore <file> # discard uncommitted changes to a file (the undo)
A note on `restore`: `git restore <file>` throws away **uncommitted** edits and resets the file to
the last commit. That's the everyday AI-undo. (Returning to an *older* commit, reverting a merge, and
the reflog are recovery topics with their own module Module 12 once you've got remotes and PRs to
the reflog are recovery topics with their own module (Module 12) once you've got remotes and PRs to
make them meaningful. Here we only need "undo back to my last checkpoint.")
### Reframe 2 — The repo is durable memory the AI can read
This is the part most people miss, and it directly fixes Module 1's *second* seam.
An AI session is ephemeral. Close the tab and the agent's working context is gone — it cannot
An AI session is ephemeral. Close the tab and the agent's working context is gone. It cannot
remember yesterday. But here's the thing: **the changes on disk aren't gone.** And Git turns the
disk into a structured, queryable record of exactly what happened and what's in flight. A fresh
session a brand-new chat, or tomorrow's agent that's never seen this project can answer "where
session (a brand-new chat, or tomorrow's agent that's never seen this project) can answer "where
were we?" entirely from ground truth by reading Git:
| Command | What it tells a cold session |
@@ -94,7 +94,7 @@ were we?" entirely from ground truth by reading Git:
Together those cover every state a change can be in: **untracked, uncommitted, committed, and
not-yet-pushed.** That's the entire surface area of "what's going on in this project," and a fresh
agent can read all of it in one pass no chat history required, no re-explaining yesterday.
agent can read all of it in one pass, with no chat history required and no re-explaining yesterday.
This reframes the whole point of committing. You're not just saving your work; you're **writing the
project's memory in a form the next AI session can read.** The chat forgets. The repo remembers.
@@ -103,9 +103,9 @@ project's memory in a form the next AI session can read.** The chat forgets. The
Put the two reframes together and the discipline falls out on its own:
- The more granular your commits, the **smaller the blast radius** when the AI makes a mess you
- The more granular your commits, the **smaller the blast radius** when the AI makes a mess: you
restore to a checkpoint ten minutes back, not yesterday.
- The more granular your commits, the **cleaner the reconstruction** `git log` reads like a
- The more granular your commits, the **cleaner the reconstruction**: `git log` reads like a
decision journal instead of one giant "stuff" commit.
Commit at every working state. Treat it as the autosave you control. "It runs and does what I
@@ -118,12 +118,12 @@ expect" is a good enough reason to commit.
Everything above is standard Git. What's *specific* to AI-assisted work:
- **The AI raises the value of undo.** You're making more changes, faster, with more confidence
(yours and the model's) and confidence is exactly what precedes a quiet mistake. The frequency of
(yours and the model's), and confidence is exactly what precedes a quiet mistake. The frequency of
"wait, undo that" goes *up* with AI, so cheap, reliable undo matters more, not less.
- **The AI has no memory; the repo is the memory you give it.** This is the single highest-leverage
habit in the course. When you start a session with *"read `git log`, `git status`, and `git diff`,
- **The AI has no memory; the repo is the memory you give it.** This is the habit that pays off most
across the course. When you start a session with *"read `git log`, `git status`, and `git diff`,
then tell me where we are,"* you've replaced "re-explain the project from memory" with "read the
ground truth." Agents are *good* at this reading state is what they're best at.
ground truth." Agents are *good* at this; reading state is what they're best at.
- **AI changes are reviewable as diffs.** `git diff` turns "the AI rewrote my file" into a precise,
line-by-line account of what it actually did. That's the foundation the review skill (Module 10) is
built on, and it starts here.
@@ -140,12 +140,12 @@ and your AI assistant.
> **How you work with the AI in this lab — still the browser.** You haven't moved the AI into your
> editor yet; that's **Module 4** ("Getting the AI Out of the Browser"), and it comes *after* this
> one on purpose. The whole point of this module is to install the safety net **first** you only
> one on purpose. The whole point of this module is to install the safety net **first**: you only
> let an AI edit your real files directly once you can see and revert exactly what it did. So for now,
> keep doing what you did in Module 1: **ask in your browser chat, then copy the result into the
> file yourself.** Every time you read "ask your AI" below, that means: paste the relevant file(s)
> into your chat, ask for the change, and paste the result back. Yes, it's the copy-paste loop from
> Module 1 that friction is exactly what Module 4 removes, and you'll appreciate it more for having
> Module 1, and that friction is exactly what Module 4 removes. You'll appreciate it more for having
> felt it one more time with a net underneath you.
### Part A — First checkpoint
@@ -179,8 +179,10 @@ and your AI assistant.
### Part B — A change you can see and trust
3. Ask your AI for a small feature — e.g. *"add a `count` command to `cli.py` that prints how many
tasks are pending."* Apply the change to the file.
3. Get `cli.py` in front of your AI first. The browser chat can't see your disk, so you have to hand
it the file: run `cat cli.py` and copy the output, or copy the contents straight from your editor.
Paste that into the chat, then ask for a small feature, e.g. *"add a `count` command to `cli.py`
that prints how many tasks are pending."* Paste the AI's version back over `cli.py`.
4. **Before committing, read the diff:**
@@ -188,7 +190,7 @@ and your AI assistant.
git diff
```
This is the habit that replaces "paste it back and hope." You're reading exactly what changed
This is the habit that replaces "paste it back and hope." You're reading exactly what changed,
nothing more, nothing less. Confirm it does what you asked and didn't touch anything it shouldn't.
Run it (`python cli.py count`), then commit:
@@ -200,7 +202,7 @@ and your AI assistant.
### Part C — Recover from a mess (the whole point)
5. Now let the AI make a mess on purpose. Ask it to *"aggressively refactor `tasks.py`"* and paste
the result over your file **without reading it**. Run the app — maybe it's broken, maybe it's
the result over your file **without reading it**. Run the app. Maybe it's broken, maybe it's
subtly wrong, maybe it's fine but unrecognizable. Doesn't matter.
6. Decide you don't want it. Undo it completely:
@@ -213,7 +215,7 @@ and your AI assistant.
```
You just recovered from a bad AI change in one command, with zero retyping and zero guesswork.
*This is the safety net.* Internalize how cheap that just was that cheapness is what lets you say
*This is the safety net.* Internalize how cheap that just was; that cheapness is what lets you say
yes to riskier AI work for the rest of the course.
### Part D — The repo as the AI's memory
@@ -238,10 +240,22 @@ and your AI assistant.
Then ask: *"Based only on this Git output, tell me where this project is: what's settled, what's
in progress, and what I should do next."*
Watch a session that has never seen your project reconstruct its exact state settled history
from `log`, in-flight work from `status`/`diff` with no chat history at all. **That's durable
Watch a session that has never seen your project reconstruct its exact state: settled history
from `log`, in-flight work from `status`/`diff`, with no chat history at all. **That's durable
memory.** Make this your standard way to start a session on any project.
9. Close the loop and leave the repo clean. The cold session just told you what's in progress and
what to do next: finish the `delete <index>` command. Do that with the AI (paste in `cli.py` the
same way as Part B), run it to confirm it works (`python cli.py delete 1`), then commit:
```bash
git add .
git commit -m "Add delete command"
git status # "nothing to commit, working tree clean"
```
No dangling uncommitted work follows you into Module 3.
---
## Where it breaks
@@ -251,7 +265,7 @@ up again in Module 8 for the *backup* half and Module 12 for the *recovery* half
- **Git only sees what was written to disk.** This is the one limit to teach yourself hard. If the
AI reasoned brilliantly about an approach in the conversation but you never wrote it to a file, it
is *gone* with the session Git can't recover what was never on disk. The repo is ground truth,
is *gone* with the session. Git can't recover what was never on disk. The repo is ground truth,
but only for things that became files. (This is also the practical argument for committing often:
the more you write down, the less lives only in ephemeral context.)
- **A single local repo is not a backup.** Everything in this module lives on one disk. Drop the
@@ -277,5 +291,5 @@ up again in Module 8 for the *backup* half and Module 12 for the *recovery* half
argues for committing often.
When undo feels free and starting a cold session feels like "just read the repo," you've got the
safety net. Module 3 puts it to work on the lowest-risk possible target documents, not code
safety net. Module 3 puts it to work on the lowest-risk possible target (documents, not code)
before Module 4 lets the AI edit your files directly.
+76 -59
View File
@@ -1,8 +1,8 @@
# Module 3 — Version Control for Words, Not Just Code
> **The safest possible place to practice Git is on prose — and it happens to be a genuinely useful
> skill on its own.** Branch an ADR, let the AI draft it, read the diff, merge it. Nothing breaks if
> it's wrong, so you build the muscle before the agent ever touches code.
> **The safest place to practice Git is on words, and it happens to be a genuinely useful skill on
> its own.** Branch an Architecture Decision Record (ADR), let the AI draft it, read the diff, merge
> it. Nothing breaks if it's wrong, so you build the muscle before the agent ever touches code.
---
@@ -25,12 +25,12 @@ is exactly the low-risk on-ramp that makes the copy-paste friction tolerable one
By the end of this module you can:
1. Explain why plain-text formats (markdown, AsciiDoc) version cleanly while `.docx`/`.pptx` version
uselessly and make the case to move a runbook or ADR out of Word.
2. Create a branch, do work on it, and merge it back the full branch → diff → commit → merge loop —
on a document where a mistake costs nothing.
uselessly, and make the case to move a runbook or ADR out of Word.
2. Create a branch, do work on it, and merge it back. That's the full branch → diff → commit → merge
loop, run on a document where a mistake costs nothing.
3. Have an AI draft a real engineering document (an ADR or a runbook) and review its work as a diff
before accepting it.
4. Recognize that the wikis on most Git hosts are themselves Git repositories so the docs you
4. Recognize that the wikis on most Git hosts are themselves Git repositories, so the docs you
thought lived "in a web UI" were version-controlled all along.
---
@@ -57,7 +57,7 @@ format Git can actually work with. That "if" is the whole argument.
### Why plain text wins: the diff is line-based
Git's core operation is the line-based diff. It compares two snapshots and reports which **lines**
changed. Everything good about Git readable history, reviewable changes, automatic merges is
changed. Everything good about Git (readable history, reviewable changes, automatic merges) is
built on that one capability. So a format versions well in exact proportion to how well it maps onto
*lines of text*.
@@ -90,7 +90,7 @@ This is a real, defensible engineering argument, not a style preference:
> drive.** The moment a document needs history, review, or more than one author, a binary format is
> actively costing you the thing version control exists to provide.
The honest counterpoint where binary formats still earn their place is in *Where it breaks*.
The honest counterpoint, where binary formats still earn their place, is in *Where it breaks*.
### The document types worth versioning
@@ -107,9 +107,9 @@ You don't need to convert everything. These are the high-value targets, all natu
- **Changelogs** — what changed in each release. A markdown `CHANGELOG.md` is the standard.
- **Specs / PRDs** — what you're going to build and why, before you build it.
For this audience the ADR is the gateway drug: small, structured, high-value, and the kind of thing
that *never* gets written because it feels like overhead right up until the AI will draft it for
you in ten seconds.
For this audience the ADR is the easiest win: small, structured, high-value, and the kind of thing
that *never* gets written because it feels like overhead, right up until the AI drafts it for you in
ten seconds.
### Branch → diff → commit → merge (the new verbs)
@@ -117,19 +117,21 @@ Module 2 worked on a straight line of commits. A **branch** is a second line you
disturbing the first. The mental model: `main` is the version everyone trusts; a branch is a private
copy where you draft something, and **merge** folds your finished work back into `main`.
For a document, the loop is:
Creating a branch is one command, and `git branch` shows you which line you're on:
```bash
git switch -c docs/adr-storage # create a branch and switch to it
# ...write the doc, with the AI's help...
git add docs/adr/0001-storage.md
git diff --staged # review exactly what's going onto the branch
git commit -m "Add ADR 0001: store tasks as JSON"
git switch main # back to the trusted version
git merge docs/adr-storage # fold the finished doc into main
git branch -d docs/adr-storage # delete the branch; its work is now in main
```console
$ git switch -c docs/adr-storage
Switched to a new branch 'docs/adr-storage'
$ git branch
* docs/adr-storage
main
```
The `*` marks your current branch. From there, the loop for a document is the same handful of verbs
every time: **draft** the doc (with the AI's help), **stage** it, read the **diff**, **commit** it on
the branch, **switch** back to `main`, then **merge** to fold the finished work in and delete the
spent branch. You'll run that whole sequence by hand in the lab; here, just hold the shape.
Two new-command notes for this audience:
- **`git switch -c <name>`** creates and moves onto a branch. (Older docs and muscle memory use
@@ -143,13 +145,13 @@ Two new-command notes for this audience:
Because this is one document on its own branch, the merge is trivial: nothing else touched `main`
while you worked, so Git **fast-forwards** — it just slides `main` up to your branch with no
conflict. That clean case is the whole reason we practice here first. What happens when two branches
edit the *same lines* a merge conflict is a real skill, and it gets its own treatment in
edit the *same lines* (a merge conflict) is a real skill, and it gets its own treatment in
**Module 6**, on code, where the stakes make it worth the depth. Practice the happy path now; the
hard path is easier once the verbs are reflexes.
### The aha: your wiki was a Git repo all along
Most Git hosts GitHub, GitLab, Gitea, and others ship a **wiki** alongside each repository. It
Most Git hosts (GitHub, GitLab, Gitea, and others) ship a **wiki** alongside each repository. It
looks like a web app: you click "New Page," type in a box, hit save. It feels like a different kind
of thing from your code.
@@ -159,9 +161,9 @@ Every page is a `.md` file. Every "save" in the web UI is a commit. The web edit
convenience layer over `git commit`.
The consequence: the documentation you've been editing in a browser textbox has had full version
history diffs, blame, the works the entire time. You can clone it, edit the markdown locally with
history (diffs, blame, the works) the entire time. You can clone it, edit the markdown locally with
the same branch/diff/merge loop you're learning here, and push it back. (Cloning and pushing to a
remote repo is **Module 8** remotes and hosting so you can't do the clone in *this* lab yet. But
remote repo is **Module 8** (remotes and hosting), so you can't do the clone in *this* lab yet. But
the realization changes how you see every wiki you'll ever touch: it's not a CMS, it's a repo
wearing a web UI.)
@@ -175,16 +177,16 @@ Here's why this module is more than "learn Git on easy mode":
models have — they were trained on oceans of it, and they reach for it by default. Asking an AI to
"write an ADR for this decision" or "turn these rough notes into a runbook" plays directly to its
strengths. The output is genuinely good and genuinely in the right format, with zero conversion.
- **"Draft it, branch it, diff it, merge it" is adoptable tomorrow.** You don't need new tools, a new
model, or editor integration. The exact workflow branch, paste the AI's draft into a `.md` file,
read the diff, merge — works today with the browser chat you already have open. Most of the rest of
this course unlocks capability you have to build up to. This one you can use on Monday.
- **Prose diffs are how you review AI writing.** Same skill as reviewing AI code (Module 10), lower
- **"Draft it, branch it, diff it, merge it" works today.** You don't need new tools, a new model, or
editor integration. The whole workflow (branch, paste the AI's draft into a `.md` file, read the
diff, merge) runs on the browser chat you already have open. Most of the rest of this course is
capability you have to build up to; this part you can put to work right now.
- **Reading the diff is how you review AI writing.** Same skill as reviewing AI code (Module 10), lower
stakes. The AI will write an ADR that *sounds* authoritative and confidently states a rationale it
invented. Reading the diff is how you catch "wait, that's not why we did this." The format makes the
review possible; your judgment makes it correct.
- **It seeds a habit the whole course depends on.** Once "the AI drafts, I review the diff, I decide"
is reflexive on documents where a mistake costs nothing you'll apply it without thinking when
is reflexive on documents, where a mistake costs nothing, you'll apply it without thinking when
the AI starts editing code, opening PRs, and running unattended later on.
---
@@ -222,28 +224,35 @@ zero.
### Part B — Let the AI draft the ADR
2. Make a home for decision records and copy in the template:
2. Make a home for decision records:
```bash
mkdir -p docs/adr
# copy modules/03-version-control-for-words/lab/adr-template.md
# to docs/adr/0001-task-storage-format.md
```
3. In your browser chat, give the AI the context and the template, and ask for the draft. Something
like:
3. Open `adr-template.md` from this module's `lab/` folder in the course repo (wherever you downloaded
it; it lives in the course repo, *not* inside `tasks-app`). In your browser chat, give the AI that
template plus the context and ask for the draft:
> *"Here's an ADR template (paste `adr-template.md`). Fill it out for this decision: the `tasks-app`
> CLI stores its state in a plain `tasks.json` file next to the code. We chose JSON over SQLite or
> a hosted database because the app is a single-user local tool and zero-setup matters more than
> query power. Keep it concise. Output markdown."*
> *"Here's an ADR template (paste the contents of `adr-template.md`). Fill it out for this decision:
> the `tasks-app` CLI stores its state in a plain `tasks.json` file next to the code. We chose JSON
> over SQLite or a hosted database because the app is a single-user local tool and zero-setup
> matters more than query power. Keep it concise. Output markdown."*
Paste the result into `docs/adr/0001-task-storage-format.md`, replacing the template body. (This is
the copy-paste loop from Module 1 — last stretch before Module 4 removes it.)
4. Now create the file and paste the draft in. In your editor, make a new file at this exact path
inside `tasks-app`:
```
docs/adr/0001-task-storage-format.md
```
Paste the AI's markdown into it and save. (This is the copy-paste loop from Module 1, the last
stretch before Module 4 removes it.) The file has to exist on disk before the next part can stage
it.
### Part C — Review the diff before you accept it
4. A brand-new file is untracked, so `git diff` shows nothing yet. Stage it, then review:
5. A brand-new file is untracked, so `git diff` shows nothing yet. Stage it, then review:
```bash
git status # the new file shows as "untracked"
@@ -251,12 +260,12 @@ zero.
git diff --staged # every line of the new doc, as additions
```
**Read it.** This is the point of the whole module: don't accept AI prose you haven't read. Check
the *substance*, not just that it's well-formatted — did it state a rationale you actually agree
with, or did it invent a confident-sounding reason? If it's wrong, edit the file and
`git add` again.
**Read it.** This is the point of the whole module: don't accept AI writing you haven't read. Check
the *substance*, not just that it's well-formatted. Did it state a rationale you actually agree
with, or did it invent a confident-sounding reason? If it's wrong, edit the file and `git add`
again.
5. When it's right, commit it on the branch:
6. When it's right, commit it on the branch:
```bash
git commit -m "Add ADR 0001: store tasks as JSON"
@@ -265,7 +274,7 @@ zero.
### Part D — Make a one-line edit and see the line-based diff
6. Edit one sentence in the ADR tighten a line, fix a claim, whatever. Save, then:
7. Edit one sentence in the ADR (tighten a line, fix a claim, whatever). Save, then:
```bash
git diff
@@ -281,17 +290,25 @@ zero.
### Part E — Merge it into main
7. Switch back to `main` and fold in the finished document:
8. First, switch back to `main` and prove the document isn't there yet. You created the whole
`docs/adr/` directory on the branch, so on `main` it doesn't exist:
```bash
git switch main
git log --oneline # note: your ADR commits aren't here yet
git merge docs/adr-storage # fast-forward — no conflict
git log --oneline # now they are
ls docs/adr/ # the ADR is on main
ls docs/adr/ # error: "No such file or directory" — it's only on the branch
git log --oneline # and your ADR commits aren't here either
```
8. Clean up the branch — its work now lives in `main`:
That's branch isolation: the work is real and committed, but completely invisible to `main` until
you merge. Now fold it in and watch the file appear:
```bash
git merge docs/adr-storage # fast-forward, no conflict
git log --oneline # the ADR commits are on main now
ls docs/adr/ # and the file is here too
```
9. Clean up the branch. Its work now lives in `main`:
```bash
git branch -d docs/adr-storage
@@ -303,9 +320,9 @@ doing the writing and you doing the reviewing. That's the loop the rest of the c
### Optional — do it again as a runbook
Repeat the loop on a different branch (`git switch -c docs/runbook-restore`) using
`lab/runbook-template.md`: ask the AI to write a runbook for "restore the tasks list after someone
deletes `tasks.json` by accident" given that the app recreates an empty list on next run. Same five
parts. Doing it twice is what turns the commands into reflexes.
`runbook-template.md` from this module's `lab/` folder: ask the AI to write a runbook for "restore the
tasks list after someone deletes `tasks.json` by accident," given that the app recreates an empty list
on next run. Same five parts. Doing it twice is what turns the commands into reflexes.
---
@@ -30,7 +30,7 @@ By the end of this module you can:
2. Install, authenticate, and point one of them at a real repository, then confirm it can actually
read the project.
3. Run the agentic edit → review → iterate loop: let the AI change real files, read the change as a
`git diff`, and either keep it or revert it.
`git diff`, and direct the AI to keep it (commit) or revert it.
4. Set the tool's permissions deliberately — what it may read, edit, and execute without asking.
5. Explain precisely why this is safe, in terms of Module 2's `restore`.
@@ -41,10 +41,10 @@ By the end of this module you can:
### What "out of the browser" actually means
In the browser-chat loop, the AI is blindfolded and handcuffed. It can't see your files unless you
paste them in, and it can't change them it can only hand you text to copy back. *You* are the
paste them in, and it can't change them; it can only hand you text to copy back. *You* are the
integration layer: you decide which files it sees, you apply its output, you are the one who notices
it forgot to update the second file. That's seam 1 from Module 1, and no smarter model fixes it,
because it isn't an intelligence problem it's an *access* problem.
because it isn't an intelligence problem, it's an *access* problem.
Getting the AI out of the browser means giving it two things it never had in the chat tab:
@@ -57,6 +57,24 @@ Everything in this module follows from those two capabilities. They're also exac
to come first: write access to your files is only acceptable when every edit is visible and
reversible.
### From here on, the AI drives git
Modules 13 had you type git by hand — `commit`, `branch`, `diff`, `restore` — on purpose. The AI
was stuck in the browser and couldn't touch your repo, so you built the muscle yourself. That was
learning arithmetic by hand before you're handed a calculator.
This module hands you the calculator. Once an agent runs inside your repo it can run commands too,
git included, so the work splits cleanly:
- **You describe the change** and **review the diff** it produces.
- **The AI edits the files and runs git** — it stages, commits, and reverts.
- **You verify the result**: the diff is what you asked for, the checkpoint landed, the tree is clean.
You don't stop understanding git; you stop typing it. The concepts from Modules 23 are exactly what
let you check the AI did the right thing. From this module on the course assumes this split: when a
step needs a commit or a revert, you tell the agent and verify its work instead of reaching for the
keyboard. The one thing that stays in your hands is reading the diff.
### The two categories
There are two shapes this tooling comes in. They overlap, and plenty of products do both, but the
@@ -70,11 +88,12 @@ review surface is right there: the editor highlights every changed line, and acc
click. If you already work in a graphical editor, this is the lowest-friction on-ramp.
**Agentic command-line tools.** These run in your terminal as a standalone program you talk to in
plain language. You launch the tool *inside* your project directory, and it reads files, runs
commands, and edits files on its own, reporting back what it did. They tend to be more autonomous —
better at "go do this multi-step thing" and they're editor-independent, so they work the same
whether you use a graphical editor, a terminal editor, or none. The review surface is `git diff`
itself (Module 2), which is the same review surface you'll use for everything else in this course.
plain language (Claude Code and Aider are two). You launch the tool *inside* your project directory,
and it reads files, runs commands, and edits files on its own, reporting back what it did. They tend
to be more autonomous, better at "go do this multi-step thing," and they're editor-independent, so
they work the same whether you use a graphical editor, a terminal editor, or none. The review surface
is `git diff` itself (Module 2), the same review surface you'll use for everything else in this
course.
| | Editor-integrated assistant | Agentic CLI tool |
|---|---|---|
@@ -109,17 +128,21 @@ brand:
Don't agonize. Any tool that shows diffs and has an approval mode is good enough to learn the loop.
The loop is the durable skill; the tool is swappable, same as the model.
**We'll use Claude Code as the worked example** from here on, so the commands below are concrete
instead of abstract. It's an agentic CLI; wherever you see `claude`, sub your own agent. The concepts
don't depend on it, same as the model.
### Wiring it up: from browser to repo
The exact clicks differ per tool and drift over time, so here is the shape every one of them
follows. Do these four steps and you're connected.
follows. Four steps connect any of them.
**1. Install it.** Editor-integrated assistants install from your editor's extension/plugin
marketplace — search, install, reload. Agentic CLIs install as a command-line program (commonly via a
package manager like `npm`/`pip`/`brew`, or a download) and then exist as a command you run, e.g.:
```bash
your-agent --version # confirm the tool is on your PATH
claude --version # sub your agent if using something else
```
**2. Authenticate.** On first run the tool will send you through a sign-in — usually a browser-based
@@ -132,7 +155,7 @@ whole point. The convention is **the current working directory is the project**:
```bash
cd ~/ai-workflow-course/tasks-app # the repo from Modules 12
your-agent # launch it from inside the project
claude # launch it from inside the project
```
For an editor-integrated assistant, the equivalent is **open the project folder** (`code .` or
@@ -140,21 +163,26 @@ File → Open Folder), exactly as you did in Module 1 — the assistant scopes i
that's open. Either way, the tool now treats this directory as its world: it can see every file in
it without you pasting a thing.
**4. Confirm it can actually read the project.** Don't assume verify, the same instinct you'd apply
to any new integration. Ask it a question only something that has read your files could answer:
**4. Confirm it can actually read the project.** Don't assume; verify, the same instinct you'd apply
to any new integration. The check is to ask a question only something that has read your files could
answer:
> *"What does this project do, which files is it split across, and what commands does the CLI
> support?"*
A correct answer names `tasks.py` and `cli.py`, describes the task app, and lists `add` / `list` /
`done` — pulled from the actual files, not guessed. If it asks you to paste code, or describes a
generic to-do app it clearly invented, it is **not** connected to the repo. Stop and fix the wiring
before going further; everything downstream assumes it can read.
A connected tool answers from the actual files, naming `tasks.py` and `cli.py` and listing `add` /
`list` / `done`:
A power move you already know from Module 2: ask it to read the *repo's* state, not just the files —
*"run `git log`, `git status`, and `git diff` and tell me where this project is."* An agentic tool
can run those itself. Now its first act is reading the durable memory you've been building, which is
exactly the "where were we?" reconstruction from Module 2, except the AI does the reading.
> *"It's a command-line to-do app. The logic lives in `tasks.py` (a `TaskList` class that persists to
> `tasks.json`), and `cli.py` is the front end that dispatches `add`, `list`, and `done`."*
If instead it asks you to paste code, or describes a generic to-do app it clearly invented, it is
**not** connected to the repo, and everything downstream assumes it can read.
Better still, point it at the *repo's* state, not just the files: *"run `git log`, `git status`, and
`git diff` and tell me where this project is."* An agentic tool runs those itself, so its first act
is reading the durable memory you built in Module 2 — the "where were we?" reconstruction, now done
by the AI instead of pasted by you.
### Operating it: the edit → review → iterate loop
@@ -167,21 +195,19 @@ replaces the entire copy-paste loop with this:
you what it did. No copying, no pasting, no you-as-integration-layer. This is the moment seam 1
dies: when the change spans `tasks.py` *and* `cli.py`, the tool edits both, because it can see
both.
3. **Review the diff.** This is the load-bearing step, and it's the Module 2 habit, unchanged:
```bash
git diff
```
Read exactly what changed — every line, across every file it touched. An editor-integrated tool
shows you the same thing in its diff view. You are reviewing the AI's work, not trusting it. (The
deep version of this skill — spotting the plausible-but-wrong change — is Module 10. Here, just
build the reflex: *nothing gets committed unread.*)
4. **Iterate or revert.**
- If it's right: run it, then commit (`git add . && git commit -m "…"`). New checkpoint.
3. **Review the diff.** This is the load-bearing step and it stays in your hands, the Module 2 habit
unchanged. The AI shows you what it changed: an agentic CLI runs `git diff`, an editor-integrated
tool shows the same thing in its diff view. You read every line, across every file it touched.
You're reviewing the AI's work, not trusting it. (The deep version of this skill, spotting the
plausible-but-wrong change, is Module 10. Here, just build the reflex: *nothing gets committed
unread.*)
4. **Keep it or revert it — the AI does the git, you verify.**
- If it's right: tell the AI to commit the reviewed change with a clear message. It stages and
commits; you confirm the checkpoint landed (`git log`). New checkpoint.
- If it's *close*: tell the AI what to fix and loop back to step 2. It already has the context.
- If it's wrong: **`git restore .`** and you're back to your last checkpoint, byte for byte. The
mess is gone. Try a different prompt.
- If it's wrong: tell the AI to throw away its uncommitted changes. It runs the restore; you
verify `git diff` is empty and you're back at your last checkpoint, byte for byte. The mess is
gone. Try a different prompt.
That fourth step is the entire reason this is safe, so let's be explicit about it.
@@ -194,17 +220,17 @@ makes is a visible, reversible delta from a known-good state.**
Concretely, the safety contract is:
- **Before you let it loose:** your work is committed (`git status` is clean). That's your restore
point.
- **Before you let it loose:** your work is committed and `git status` is clean. (You'll have the
agent confirm this and commit anything outstanding; you verify it.) That's your restore point.
- **While it works:** every change is on disk, and `git diff` shows you all of it. Nothing is hidden.
- **If it goes wrong:** `git restore .` discards every uncommitted edit it made and you're back at
the checkpoint, with zero retyping. Module 2's "undo for the AI," now pointed at an AI that edits
files itself.
- **If it goes wrong:** the agent runs `git restore`, discards every uncommitted edit it made, and
you're back at the checkpoint with zero retyping. You verify the diff is empty. Module 2's "undo
for the AI," now an undo the AI even performs for you.
This is the promise Module 2 made cashing out. Module 2 said *every later module asks you to let the
AI do something bolder, and you can say yes because you can always get back to a checkpoint.* This is
the first of those bolder things. The downside of any AI edit is now "throw away a few minutes and
re-prompt" never "lose work" and that asymmetry is what lets you move fast.
re-prompt," never "lose work," and that asymmetry is what lets you move fast.
> **The one rule:** start from a clean commit. If `git status` shows uncommitted work before you turn
> the AI loose, you've blurred the line between *your* work and *its* work — and `git restore .` will
@@ -227,7 +253,7 @@ an approval model, usually some version of:
The right setting is a function of your safety net, not your nerve. With a clean commit you can
afford a looser setting for edits, because the diff is reversible. Be more conservative about letting
it *run* commands unattended a deleted file is restorable; a command that hits a real external
it *run* commands unattended: a deleted file is restorable; a command that hits a real external
system may not be. Match the leash to what you can undo.
---
@@ -248,8 +274,8 @@ pays off. Map it straight back to Module 1's three seams:
The deeper point: notice that *none of this is model-specific.* You didn't get a smarter model. You
gave the same model **access** and wrapped it in **review and revert**. That's the course thesis in
miniature — the leverage came from the workflow around the model, not the model. Swap the model
underneath this loop and the loop is unchanged.
miniature: the workflow around the model did the work, not the model. Swap the model underneath this
loop and the loop is unchanged.
---
@@ -265,42 +291,44 @@ copy-paste loop back in Module 1, now done right.
**You'll need:**
- The `tasks-app` repo from Modules 12, as a Git repo with at least one commit.
- One AI-out-of-the-browser tool of your choice — either an editor-integrated assistant or an agentic
CLI. Use the "How to choose" criteria above; any tool that shows diffs and has an approval mode is
fine.
- One AI-out-of-the-browser tool. We'll use **Claude Code** as the example; sub your own agent (an
editor-integrated assistant or another agentic CLI). Use the "How to choose" criteria above; any
tool that shows diffs and has an approval mode is fine.
- Your model/provider credentials for that tool.
- The verify script in this module's `lab/verify.sh`. **Convention for every lab script from here on:**
the course's scripts live in the course repo under `modules/NN/lab/`, but your `tasks-app` is a
separate folder (Module 1) so when a step runs one, **copy the script into `tasks-app` first, then
run it by name**. (Same copy-it-in move you used for the instructions file in Module 5; use the real
path to wherever you unzipped the course in place of `/path/to/`.)
the course's scripts live under `~/ai-workflow-course/modules/NN/lab/`, but your `tasks-app` is a
separate folder (Module 1), so when a step runs one, **copy the script into `tasks-app` first, then
run it by name**. (Paths below assume the course unzipped to `~/ai-workflow-course/`; adjust if you
put it elsewhere.)
### Part A — Wire it up and confirm it can read
1. Install the tool and authenticate it (steps 12 in "Wiring it up").
2. Point it at the repo (step 3): `cd ~/ai-workflow-course/tasks-app` and launch the agentic CLI from
there, **or** open that folder in your editor and open the assistant's agent panel.
2. Point it at the repo (step 3): `cd ~/ai-workflow-course/tasks-app` and launch `claude` from there,
**or** open that folder in your editor and open the assistant's agent panel.
3. **Confirm read access** (step 4). Ask:
> *"What does this project do, which files is it split across, and what commands does the CLI
> support?"*
You're connected only if it names `tasks.py` and `cli.py` and lists `add` / `list` / `done` from
the real files. If it asks you to paste code, fix the wiring before continuing.
3. **Confirm read access** (step 4). Ask it the read-check question from "Wiring it up." You're
connected only if it answers from the real files; if it asks you to paste code, fix the wiring
before continuing.
### Part B — Start from a clean checkpoint
4. This is the one rule. Make sure your work is committed so the AI's change is the *only* thing in
the next diff:
4. This is the one rule: start clean, so the AI's change is the *only* thing in the next diff. **Tell
the agent to set the checkpoint**, then verify it yourself. Ask:
> *"Check `git status`. If anything's uncommitted, commit it with a clear message so we start from
> a clean tree."*
Then confirm with your own eyes:
```bash
git status # must be clean ("nothing to commit, working tree clean")
git status # you check: "nothing to commit, working tree clean"
```
If it isn't clean, commit your current work first (`git add . && git commit -m "…"`). Now you have
a known-good restore point, and anything that appears in `git diff` next is purely the AI's.
Now you have a known-good restore point, and anything that appears in `git diff` next is purely
the AI's. (Notice you directed the commit and verified the result — you didn't type it. That's the
split for every git step from here on.)
### Part C — Make a real multi-file change
@@ -329,43 +357,49 @@ copy-paste loop back in Module 1, now done right.
both files. Copy it into `tasks-app` first (see *You'll need*), then run it from there:
```bash
cp /path/to/modules/04-getting-the-ai-out-of-the-browser/lab/verify.sh .
cp ~/ai-workflow-course/modules/04-getting-the-ai-out-of-the-browser/lab/verify.sh .
bash verify.sh
```
It should add tasks, delete one by index, and confirm the right task remains. If it fails, don't
hand-fix it tell the AI what broke and let it iterate (step 4 of the loop), then re-run.
hand-fix it; tell the AI what broke and let it iterate (step 4 of the loop), then re-run.
8. **Commit the reviewed change — this is your new checkpoint.** It passed your own eyes and it
passes the check, so lock it in:
8. **Commit the reviewed change — tell the agent, then verify.** It passed your own eyes and it
passes the check, so lock it in. Ask the agent:
> *"Commit this with the message 'Add delete command (made via editor/CLI agent)'."*
It stages and commits. You verify the checkpoint landed:
```bash
git add .
git commit -m "Add delete command (made via editor/CLI agent)"
git log --oneline
git log --oneline # your new commit is on top
```
You just shipped a reviewed, multi-file change made by an AI editing your files directly and the
copy-paste loop never entered into it. This commit is now the clean state `git restore .` falls
back to in the next part.
You just shipped a reviewed, multi-file change an AI made by editing your files directly, and you
never typed the commit. This commit is now the clean state the AI's `git restore` falls back to in
the next part.
### Part D — Practice the revert (do this even though it works)
9. You only trust an undo you've used. Your tree is clean you just committed in Part C, which is
exactly the safe setup the one rule demands. Prove the net is under you: ask the tool for a
deliberately throwaway change
9. You only trust an undo you've used. Your tree is clean (you just committed in Part C, exactly the
safe setup the one rule demands). Prove the net is under you. Ask the tool for a deliberately
throwaway change:
> *"Rename every variable in `tasks.py` to single letters."*
— let it apply it, glance at `git diff` to see the damage, then throw it away:
Let it apply it, glance at `git diff` to see the damage, then **tell the agent to undo it**:
> *"Throw away everything you just did and get us back to the last commit."*
It runs the restore. Now you verify the rescue:
```bash
git restore .
git diff # empty — the AI's mess is gone, byte for byte
bash verify.sh # still passes — you're back at your good state (you copied it in at step 7)
```
That's the Module 2 safety net catching a Module 4 mistake. Internalize how cheap that was.
That's the Module 2 safety net catching a Module 4 mistake, and the AI even performed the undo on
your word. Internalize how cheap that was.
### Part E — Confirm you're back at your good state
@@ -387,13 +421,13 @@ copy-paste loop back in Module 1, now done right.
Be honest about the limits of working this way:
- **Access is not judgment.** The AI reading your whole repo makes it *informed*, not *correct*. It
will still make confident, plausible, wrong changes now across multiple files at once, which is a
will still make confident, plausible, wrong changes, now across multiple files at once, which is a
bigger mess to read. The diff review in step 3 of the loop is not optional, and the deep version of
that skill is a whole module of its own (Module 10). The tool removed the copy-paste; it did not
remove the reviewing.
- **`git restore .` only saves you if you committed first.** This is the one rule for a reason. If
you let the AI loose on a dirty tree, restore can't tell your work from its work and throws away
both. The discipline that makes this module safe is *commit before you turn it loose* the same
both. The discipline that makes this module safe is *commit before you turn it loose*, the same
"commit often" lesson from Module 2, now with teeth.
- **It can do more than edit — watch what it runs.** An agentic tool that can run commands can do
things `git restore` cannot undo: delete files outside the repo, hit a network service, mutate a
@@ -8,7 +8,7 @@
# care HOW the AI implemented it, only that `delete` works end to end.
#
# Copy this into your tasks-app project directory, then run it from there:
# cp /path/to/modules/04-getting-the-ai-out-of-the-browser/lab/verify.sh .
# cp ~/ai-workflow-course/modules/04-getting-the-ai-out-of-the-browser/lab/verify.sh .
# bash verify.sh
#
# (It self-locates cli.py, so it also still works if you run it in place as `bash lab/verify.sh`.)
+120 -83
View File
@@ -1,7 +1,7 @@
# Module 5 — Commit the AI's Config, Not Just the Code
> **The instructions you give the model are as worth versioning as the code it writes.** Write your
> project's conventions down once, commit them, and every teammate and every agent inherits the
> project's conventions down once, commit them, and every teammate (and every agent) inherits the
> same setup instead of each of you hand-tuning your own and quietly drifting apart.
---
@@ -22,8 +22,8 @@
By the end of this module you can:
1. Identify the repo-level instructions file your agentic tool reads, and explain what belongs in it.
2. Write an instructions file for a real project conventions, build/test commands, coding
standards, off-limits files, house style that an AI will actually act on.
2. Write an instructions file for a real project (conventions, build/test commands, coding
standards, off-limits files, house style) that an AI will actually act on.
3. Commit that file so the configuration travels with the repo, not with one person's machine.
4. Demonstrate the AI obeying the committed instructions, and changing its behavior when you change
the file.
@@ -65,7 +65,7 @@ a briefing for an agent that will edit this code. Keep it to what changes the AI
`python cli.py <command>`. Run tests with `python -m unittest`. Don't claim a change works until
the tests pass." This single line stops the AI from inventing a test runner you don't use.
- **Coding standards** — formatting, typing, error handling, the libraries you do and don't want.
"Use the standard library only no third-party packages. Type-hint public functions."
"Use the standard library only, no third-party packages. Type-hint public functions."
- **"Don't touch these files."** — the off-limits list. Generated files, vendored code, secrets,
anything the AI should read but never rewrite. "Never edit `tasks.json` by hand; it's generated."
- **House style** — the taste calls that otherwise come back wrong every time. "Keep functions
@@ -73,7 +73,7 @@ a briefing for an agent that will edit this code. Keep it to what changes the AI
cleverness."
The test of a good line: would you otherwise have to say it again next session? If yes, it belongs in
the file. If the AI already gets it right without being told, leave it out bloat dilutes the
the file. If the AI already gets it right without being told, leave it out; bloat dilutes the
signal (see *Where it breaks*).
### Why commit it instead of keeping it in your head (or your settings)
@@ -83,50 +83,77 @@ useful for personal preferences, but it's the wrong home for project knowledge,
lives: on *your* laptop, invisible to everyone else.
Picture a two-person project with no committed instructions file. You've trained your local setup to
run `python -m unittest` and avoid `tasks.json`. Your teammate's setup hasn't their agent reformats whole files
run `python -m unittest` and avoid `tasks.json`. Your teammate's setup hasn't, so their agent reformats whole files
and hand-edits the generated JSON. You're both "using AI on the same repo," but you're getting
different behavior, and neither of you can see the other's configuration. That's **drift**: the same
codebase, diverging because the rules live in two heads instead of one file.
Commit the file and that collapses. The configuration is now part of the repo. Clone the repo, get
the rules. A new teammate or a brand-new agent that's never seen the project is configured
the rules. A new teammate (or a brand-new agent that's never seen the project) is configured
correctly on the first run, because the setup travels *with the code* instead of with whoever set it
up. This is the same move as Module 2's "the repo is durable memory the AI can read," aimed one level
up: not just the code's history, but the instructions for working on it.
### The real unlock: AI behavior becomes reviewable
### Shared config vs. personal config
The instructions file is the main thing worth committing, but it's not the only AI config a tool drops
in a repo. Those files split cleanly into *shared* (belongs in the repo, so every collaborator and
every agent inherits it) and *personal* (your machine, your keys, your taste, kept out). Take Claude
Code as the concrete case (sub your own agent's filenames):
| File | Shared or personal |
| --- | --- |
| `CLAUDE.md` (the instructions file) | **Shared** — the whole point of this module |
| `.claude/settings.json` (project settings: permissions, hooks config) | **Shared** — the team runs the same setup |
| `.claude/settings.local.json` (your personal overrides) | **Personal** — gitignored for you |
| `.mcp.json` (the MCP servers the project uses) | **Shared if the project relies on them** |
| `.claude/commands/`, `.claude/agents/`, `.claude/hooks/` | **Shared if the project uses them** |
The principle is tool-agnostic. This very repo commits an `AGENTS.md` instead of a `CLAUDE.md` (same
job, vendor-neutral name) and keeps personal settings out. The line to hold: anything that defines
*how this project is worked on* is shared; anything that's your own machine or your secrets is not.
Rather than guess the split yourself, you can ask the agent which of its config files belong in the
repo. The lab does exactly that.
### AI behavior becomes reviewable
Here's the part that makes this more than a convenience. Once the instructions live in the repo, **a
change to how the AI works on this project is a change to a tracked file** so it shows up exactly
like a code change does:
change to how the AI works on this project is a change to a tracked file**, so it shows up exactly
like a code change. Tighten "keep functions small" into "no function over 30 lines" and `git diff`
reports it the same way it reports an edit to `tasks.py`:
```bash
git diff
```diff
## House style
-- Keep functions small and single-purpose.
+- No function over 30 lines; split anything longer.
```
When someone tightens "keep functions small" into "no function over 30 lines," or adds
`infra/` to the don't-touch list, that decision arrives as a *diff* you can read, question, and
accept or reject. It's no longer an invisible tweak in one person's settings that silently changes
what the AI does for everyone. The way your team works with AI becomes a reviewable artifact with a
history — you can `git log` it and see *why* a rule exists and when it was added.
That decision arrives as a *diff* you can read, question, and accept or reject. It's no longer an
invisible tweak in one person's settings that silently changes what the AI does for everyone. The way
your team works with AI becomes a reviewable artifact with a history: `git log` shows *why* a rule
exists and when it was added.
The full version of this lands in **Module 10**, where that diff becomes a pull request someone
actually reviews before it merges, and **Module 8**, where a shared remote means the file reaches the
whole team. You don't have those yet so for now the payoff is local: the file is committed, the
whole team. You don't have those yet, so for now the payoff is local: the file is committed, the
behavior is recorded, and `git diff` already shows changes to it as plainly as changes to any code.
The habit starts now; the team-scale payoff arrives on schedule.
### This course commits its own
You don't have to take this on faith this repo does exactly what the module teaches. At the root of
*The Workflow* is an `AGENTS.md` file: the committed instructions for the agents that help author the
course. It states what the repo is, the core promises (model-agnostic, GitHub-as-default-not-
requirement, the load-bearing dependency chain), the voice, the lab conventions, and a flat "Don't"
list. Open it:
You don't have to take this on faith: this repo does exactly what the module teaches. At the root of
*The Workflow* is an `AGENTS.md` file, the committed instructions for the agents that help author the
course. (Claude Code reads `CLAUDE.md` by default; `AGENTS.md` is the same job under a vendor-neutral
name, and most tools can be pointed at it.) It states what the repo is, the core promises
(model-agnostic, GitHub-as-default-not-requirement, the load-bearing dependency chain), the voice, the
lab conventions, and a flat "Don't" list. Because it's committed, its history reads like a changelog
of how agents work here:
```bash
git show HEAD:AGENTS.md # or just open AGENTS.md in your editor
git log --oneline AGENTS.md # its history — every change to how agents work on this repo
```text
$ git log --oneline AGENTS.md
4bd586b Tighten the no-slop voice rule; thin em-dashes
ced344d Add the git-reframe section (AI drives git from Module 4)
9e9bb51 Initial commit
```
That file is why every module in this course sounds like one course instead of twenty-seven
@@ -137,8 +164,8 @@ tutorials. It's the worked example for everything below.
A committed instructions file is the lightweight foundation. It says *how this project works* in
general — always-on context the AI reads every session. When you find yourself wanting to capture a
*specific repeatable procedure* ("here's exactly how we cut a release," "here's our playbook for
adding a new CLI command"), that's the structured big sibling: **Skills (Module 21)**. Same instinct
write the knowledge down, commit it, let the AI execute it your way but packaged as reusable
adding a new CLI command"), that's the structured big sibling: **Skills (Module 21)**. Same instinct
(write the knowledge down, commit it, let the AI execute it your way) but packaged as reusable
playbooks instead of a single always-on briefing. Start with the instructions file; graduate to
skills when a procedure earns its own page.
@@ -147,21 +174,21 @@ skills when a procedure earns its own page.
## The AI angle
This is the course thesis applied to your own configuration. **The model is the cheap, swappable
part; the setup you build around it is the durable artifact.** When you swap models next quarter
and you will your committed instructions file carries over unchanged. The new model reads the same
part; the setup you build around it is the durable artifact.** When you swap models next quarter (and
you will), your committed instructions file carries over unchanged. The new model reads the same
conventions, the same test command, the same don't-touch list, and behaves consistently on day one.
You configured the *project*, not the model.
Three things make this specifically an AI problem, not a generic config chore:
- **AI has no memory across sessions, but it reads files.** A committed instructions file is the
cleanest way to give an ephemeral agent durable, project-specific context written once, read
cleanest way to give an ephemeral agent durable, project-specific context: written once, read
every session, by every model.
- **AI is confidently inconsistent without a spec.** Unprompted, it'll pick a test runner, a
formatting style, a place to put new code and pick differently next time. The instructions file
formatting style, a place to put new code, and pick differently next time. The instructions file
is how you make "the way we do it here" the default instead of a coin flip.
- **AI behavior is otherwise invisible.** A teammate's hand-tuned local rules silently change what
the AI does. Committing the rules drags that into the open where it can be reviewed which is the
the AI does. Committing the rules drags that into the open where it can be reviewed, which is the
whole reason this audience trusts version control in the first place.
---
@@ -179,42 +206,53 @@ editor-integrated AI (Module 4) for the part where the AI obeys the file.
- Optionally, a test command for the AI to honor — Python's built-in `python -m unittest` works with
nothing to install (you'll write a real suite in Module 13; until then it simply reports no tests).
### Part A — Write the instructions file
### Part A — Write the instructions file and let the AI commit the config
1. Look up the instructions filename your tool reads. Copy this module's starter,
`lab/instructions-file-starter.md`, to that filename at the **root of your `tasks-app` repo**.
(If your tool reads several names, copy it to each, or symlink them.)
1. Look up the instructions filename your tool reads (Claude Code uses `CLAUDE.md`; sub your own).
Open an AI session in the `tasks-app` repo and direct it to create that file from this module's
starter, made true for the project:
```bash
cd ~/ai-workflow-course/tasks-app
# replace <YOUR_TOOL_FILE> with the name your tool actually reads:
cp /path/to/modules/05-commit-the-ai-config/lab/instructions-file-starter.md <YOUR_TOOL_FILE>
```
> *"Read `~/ai-workflow-course/modules/05-commit-the-ai-config/lab/instructions-file-starter.md`.
> Create my tool's instructions file at the root of this repo seeded from it, and adjust every line
> so it's accurate for this tasks-app. Don't commit yet — I want to review it first."*
2. Open it in your editor and make it true for *your* project. The starter is filled in for the
`tasks-app`, but read every line and confirm it matches reality — wrong instructions are worse
than none. At minimum, set the real test command (or delete the line if you don't have tests
yet).
You're handing the AI the file creation and placement. You keep the judgment over *content*: a
wrong instruction is worse than none.
3. Commit it. This is the point of the whole module:
2. Read what it produced, line by line. The starter is filled in for `tasks-app`, but confirm it
matches reality. At minimum, check the test command is real (or have it drop the line if you don't
have tests yet). Fix anything off before it gets committed.
```bash
git add <YOUR_TOOL_FILE>
git commit -m "Add committed AI instructions for tasks-app"
```
3. Now ask the AI which config should travel with the repo, then let it stage and commit:
The configuration now travels with the repo.
> *"Which of the AI config files in this repo should be committed so a teammate gets the same setup,
> and which are personal to my machine? Stage the shared ones and commit them with a clear message."*
A good answer separates *shared* from *personal*. For Claude Code that means commit `CLAUDE.md` and
`.claude/settings.json`; leave `.claude/settings.local.json` out (gitignored personal overrides);
commit `.mcp.json` and anything under `.claude/commands/`, `.claude/agents/`, or `.claude/hooks/`
*if the project uses them*. For a fresh `tasks-app` that's usually just the instructions file.
Letting the agent stage and commit is the point: from here on you direct the git work and check the
result.
4. Verify it landed the way you wanted:
> *"Show me what you just committed."*
Confirm the commit contains the instructions file and only the files you meant to share (no
`settings.local.json`, no secrets). This commit is the point of the whole module: the configuration
now travels with the repo.
### Part B — Watch the AI obey it
4. Start a **fresh** AI session in your editor (so it picks up the file cleanly) and give it a task
5. Start a **fresh** AI session in your editor (so it picks up the file cleanly) and give it a task
that the instructions constrain. Pick a command your app doesn't have yet (so this is a real
feature, not a re-add) — for example:
> *"Add a `search <term>` command that lists only the tasks whose title contains `term`. Then
> confirm it works."*
5. Watch for the file taking effect. A correctly-configured agent should, without you saying any of
6. Watch for the file taking effect. A correctly-configured agent should, without you saying any of
it this time:
- put the logic where your conventions said it goes (core in `tasks.py`, CLI wiring in `cli.py`);
- **not** hand-edit `tasks.json` (you marked it off-limits);
@@ -224,40 +262,38 @@ editor-integrated AI (Module 4) for the part where the AI obeys the file.
You're checking that behavior you'd normally have to *dictate every session* now happens by
default. That delta is the file working.
6. If it ignored a rule, that's signal too tighten the wording, commit the change, and try again.
7. If it ignored a rule, that's signal too: tighten the wording, commit the change, and try again.
Vague instructions get vague compliance; specific, imperative lines ("Never edit `tasks.json` by
hand it is generated") land far better than soft ones ("try to avoid editing generated files").
hand; it is generated") land far better than soft ones ("try to avoid editing generated files").
### Part C — Make a behavior change reviewable
7. Now change *how the AI works* and watch it show up as a diff. Add a house-style rule to the file —
say, a hard line length:
8. Now change *how the AI works* and watch it show up as a diff. Direct the AI to add a house-style
rule to the instructions file, say a hard line length:
> Add to the instructions file: `Keep functions under 20 lines; split anything longer.`
> *"Add this line to the instructions file under house style: `Keep functions under 20 lines; split
> anything longer.` Don't commit yet — I'll review the diff first."*
8. Before committing, read the change exactly as a reviewer would:
9. Before anything gets committed, read the change exactly as a reviewer would. This is your
verification step, so run it yourself:
```bash
git diff
```
That diff *is* the change to your AI workflow readable, attributable, revertable. Commit it:
That diff *is* the change to your AI workflow: readable, attributable, revertable. When it's right,
direct the AI to record it:
```bash
git add <YOUR_TOOL_FILE>
git commit -m "Require functions under 20 lines"
```
> *"Commit that with a message describing the rule."*
9. Look at the history of just this file:
10. Confirm the history. Ask the AI to surface it (or read it yourself):
```bash
git log --oneline <YOUR_TOOL_FILE>
```
> *"Show me the commit history of the instructions file."*
Every line is a decision about how the AI behaves on this project recorded, not lost in someone's
local settings. (In Module 8 this file reaches your whole team via a remote; in Module 10 that diff
becomes a PR someone reviews before it lands. The habit you just built is what those modules turn
into a team workflow.)
Every line is a decision about how the AI behaves on this project, recorded rather than lost in
someone's local settings. (In Module 8 this file reaches your whole team via a remote; in Module 10
that diff becomes a PR someone reviews before it lands. The habit you just built is what those
modules turn into a team workflow.)
---
@@ -267,22 +303,23 @@ Be honest about what a committed instructions file does and doesn't buy you:
- **It's guidance, not a guarantee.** The file biases the model strongly; it does not bind it. An AI
can still ignore a line, especially a vague one, especially deep in a long session. The enforcement
that *can't* be ignored tests that fail the build, scans that block a merge is **CI
that *can't* be ignored (tests that fail the build, scans that block a merge) is **CI
(Module 14)** and **security scanning (Module 15)**. The instructions file reduces how often the AI
goes wrong; it doesn't replace the gates that catch it when it does.
- **Bloat kills it.** A 300-line instructions file is read the way *you* read a 300-line terms-of-
service: not really. Every line you add dilutes the rest. Keep it to what actually changes behavior,
and prune lines the model already honors without being told.
- **Stale instructions are worse than none.** A file that says "run the tests with `python -m
unittest`" after you've switched to a different runner will actively misdirect the AI. The file is code-adjacent — it has to be
maintained like code, and reviewed like code. That's exactly why committing it (so changes are
unittest`" after you've switched to a different runner will actively misdirect the AI. The file is
code-adjacent: it has to be maintained like code, and reviewed like code. That's exactly why
committing it (so changes are
visible) matters.
- **The team payoff isn't here yet.** On a solo local repo, the "no more drift between teammates"
argument is theoretical there's only you. The full value lands with a shared remote
argument is theoretical: there's only you. The full value lands with a shared remote
(**Module 8**) and review (**Module 10**). What you get *now* is the habit and the local history;
don't oversell the team benefit until the team can actually pull the file.
- **It is not a security control.** Telling an agent "don't touch `secrets.env`" is a convention, not
a permission boundary a sufficiently confused or adversarial agent can still read or write it.
a permission boundary: a sufficiently confused or adversarial agent can still read or write it.
Real isolation and least-privilege for agents come later (**Modules 16 and 22**). The instructions
file expresses intent; it doesn't enforce it.
@@ -294,14 +331,14 @@ Be honest about what a committed instructions file does and doesn't buy you:
- Your `tasks-app` repo has a committed instructions file at the root, filled in to match the actual
project, and `git log` shows the commit that added it.
- You've watched a fresh AI session honor a rule from the file placing code where your conventions
said, respecting the don't-touch list, or running your stated test command *without you saying it
- You've watched a fresh AI session honor a rule from the file (placing code where your conventions
said, respecting the don't-touch list, or running your stated test command) *without you saying it
that session*.
- You've changed a behavior rule, read the change with `git diff`, and committed it so a change to
- You've changed a behavior rule, read the change with `git diff`, and committed it, so a change to
how the AI works is now a reviewable diff with a history.
- You can explain, in one sentence, why committing the file beats each teammate hand-tuning their own
setup: the configuration travels with the repo, so nobody drifts.
When the AI behaves like it already knows your project the moment you open it and you didn't say a
word this session the file is doing its job. Module 6 takes the safety net further: branches, so the
When the AI behaves like it already knows your project the moment you open it, and you didn't say a
word this session, the file is doing its job. Module 6 takes the safety net further: branches, so the
AI can try something wild in a sandbox you can throw away.
@@ -31,53 +31,35 @@ the next size up: isolating *a whole line of committed work* so you can keep or
By the end of this module you can:
1. Create a branch, switch between branches, and explain what a branch actually *is* (a movable
pointer, not a copy of your files).
2. Let an AI make a bold, multi-commit change on a branch while `main` stays untouched and runnable.
3. Decide the experiment's fate in one command: **merge** it into `main` to keep it, or **delete the
branch** to throw it away with zero trace.
4. Read a merge conflict the `<<<<<<<`/`=======`/`>>>>>>>` markers — and resolve it deliberately,
including handing the conflict to the AI to resolve.
5. Tell the difference between a fast-forward merge and a merge commit, and know which one you just
got.
1. Explain what a branch actually *is* (a movable pointer, not a copy of your files) and direct your
AI agent to create and switch between branches, verifying the result with `git branch`/`git status`.
2. Let the AI make a bold, multi-commit change on a branch while `main` stays untouched and runnable.
3. Decide the experiment's fate and have the agent carry it out: **merge** it into `main` to keep it,
or **delete the branch** to throw it away with zero trace. You make the call and check the result.
4. Read a merge conflict (the `<<<<<<<`/`=======`/`>>>>>>>` markers) and hand it to the AI to
resolve, then verify the resolution is right before the merge completes.
5. Tell the difference between a fast-forward merge and a merge commit, and know which one you got.
---
## Key concepts
### What a branch actually is
### What a branch actually is (quick recap)
You already drove this loop once — `git switch -c`, `git merge`, `git branch -d` on a doc in Module 3,
where the merge always fast-forwarded because nothing else had moved. Here the same verbs meet code
that diverges and conflicts, so it's worth pinning down what a branch really is before we lean on it.
You already drove the branch loop by hand in Module 3 (create, merge, delete) on a markdown doc,
where the merge always fast-forwarded because nothing else had moved. You won't re-learn those
commands here. From Module 4 on, the AI runs them for you; this module is about how the AI works
*inside* a branch and how you decide what to keep. So just one line of recap before we get there.
Strip the mystique and a branch is **a named, movable pointer to a commit.** That's the whole
definition. Your commit history is a chain of snapshots (Module 2); a branch is a sticky label that
points at one of them and *moves forward* every time you commit on it.
A branch is **a named, movable pointer to a commit.** Your commit history is a chain of snapshots
(Module 2); a branch is a sticky label that points at one of them and moves forward every time you
commit on it. `main` is the branch Git made for you in Module 2; every commit moved that label
forward. You were "on a branch" the whole time.
When you ran `git init -b main` in Module 2, Git made one branch for you automatically — named
`main` (the `-b main` is what guaranteed that name; in this course your repo is always on `main`).
Every commit you made moved the `main` label forward. You were "on a branch" the entire time
without thinking about it.
The thing that surprises people coming from an ops background: **creating a branch copies nothing.**
There's no second folder, no duplicated files, no disk cost worth mentioning. Git just writes a new
label pointing at the same commit you're standing on. That's why branches are *cheap enough to be
disposable* — and disposable is exactly the property we want.
```bash
git branch # list branches; the * marks the one you're on
git switch -c experiment # create a branch called "experiment" and switch to it
git switch main # switch back to main
git branch -d experiment # delete a branch you've already merged
git branch -D experiment # FORCE-delete a branch, merged or not (the "throw it away" button)
```
> **Naming note** (you saw the short version in Module 3). `git switch` (create/move between branches)
> and `git restore` (the Module 2 undo) were split out of the older, overloaded `git checkout` command.
> You'll still see `git checkout -b experiment` everywhere online — it does the same thing as
> `git switch -c experiment`. Both work; this module uses `switch`/`restore` because they say what they
> mean.
The property that makes branches the right tool here: **creating one copies nothing.** No second
folder, no duplicated files, no disk cost worth mentioning. Git writes a new label pointing at the
commit you're already on. That's why branches are cheap enough to be disposable, and disposable is
exactly what we want for an AI experiment you might throw away.
### The reframe: a branch is a sandbox you can blow away
@@ -87,10 +69,10 @@ You spin one up precisely *because* you're about to do something you might regre
clean way to make it never have happened.
In Module 2 the safety net was "commit, then `restore` if the AI makes a mess." That's perfect for a
single bad edit. But some experiments are bigger than one edit "rewrite the storage layer,"
"try a totally different CLI structure," "add a feature that touches four files." Those take *several
commits* to even evaluate, and you don't want that half-finished, possibly-broken work sitting on
`main`. A branch gives the whole experiment its own track:
single bad edit. But some experiments are bigger than one edit: "rewrite the storage layer," "try a
totally different CLI structure," "add a feature that touches four files." Those take several commits
to even evaluate, and you don't want that half-finished, possibly-broken work sitting on `main`. A
branch gives the whole experiment its own track:
```
main: A───B───C (always runnable; this is your "known good")
@@ -98,82 +80,80 @@ main: A───B───C (always runnable; this is y
experiment: D───E───F (the AI's bold attempt, however messy)
```
While you're on `experiment`, `main` is frozen at C runnable, shippable, untouched. The AI can
leave `experiment` in a smoking crater at F and `main` doesn't care. When you're done you make one
decision:
While you're on `experiment`, `main` is frozen at C: runnable, shippable, untouched. The AI can leave
`experiment` a broken mess at F and `main` doesn't care. When you're done you make one decision:
- **Keep it:** merge `experiment` into `main` (C gains D, E, F).
- **Kill it:** delete `experiment`. D, E, F evaporate. `main` is still exactly C, as if the
experiment never happened.
That "kill it, no trace" path is the one this module exists for. It's the difference between *"I have
to carefully undo everything the AI did"* and *"I delete the branch."*
That "kill it, no trace" path is the one this module exists for. It's the difference between "I have
to carefully undo everything the AI did" and "I delete the branch."
### Switching branches changes your files
Here's the part that feels like magic the first time. When you `git switch` to another branch, **Git
rewrites the files in your folder to match that branch.** Switch to `experiment` and the AI's
half-built feature appears in your editor. Switch back to `main` and it vanishes — your files are
back to commit C. Same folder, different contents, instantly.
One detail trips people up the first time. When you switch to another branch, **Git rewrites the
files in your folder to match that branch.** Switch to `experiment` and the AI's half-built feature
appears in your editor. Switch back to `main` and it's gone; your files are back to commit C. Same
folder, different contents, instantly.
This is why you can't switch with uncommitted changes lying around that would be clobbered: Git
stops you, because switching would silently throw work away. The fix is the Module 2 habit commit
(or stash) before you switch. On a branch, "commit often" pays off again: each commit is a safe
point to switch away from.
This is why you can't switch with uncommitted changes lying around that would be clobbered. Git stops
you, because switching would silently throw work away. The fix is the Module 2 habit: commit (or
stash) before you switch. On a branch, "commit often" pays off again, since each commit is a safe
point to switch away from. When the agent is driving, this is one of the things you verify after it
works: `git status` clean before a switch.
> **One folder, one branch at a time.** Switching swaps the *whole* folder between branches, which
> means you can only have one branch checked out at once. The moment you want *two* branches live
> simultaneously — say, two agents working in parallel without overwriting each other's files you've
> hit the limit of branches alone. That's exactly what **Module 7 (Worktrees)** solves: multiple
> working directories from one repo. Branches are the concept; worktrees are how you run several at
> once. Keep that in your back pocket.
> **One folder, one branch at a time.** Switching swaps the *whole* folder between branches, so you
> can only have one branch checked out at once. The moment you want *two* branches live at the same
> time (say, two agents working in parallel without overwriting each other's files) you've hit the
> limit of branches alone. That's what **Module 7 (Worktrees)** solves: multiple working directories
> from one repo. Branches are the concept; worktrees are how you run several at once.
### Merging: keeping the experiment
Merging takes the commits from one branch and brings them into another. You switch to the branch you
want to *receive* the work (usually `main`), then merge the other branch in:
Merging takes the commits from one branch and brings them into another. The receiving branch (usually
`main`) is the one you switch to, and the other branch merges into it. You don't type this; you tell
the agent "merge `experiment` into `main`," and it runs the equivalent of `git merge experiment`.
```bash
git switch main
git merge experiment
There are two outcomes, and it's worth recognizing which you got when you read the log:
- **Fast-forward.** If `main` hasn't moved since you branched (still at C), Git slides the `main`
label forward to F. The history stays a straight line. This is the common case for a solo
experiment.
- **Merge commit.** If `main` *did* move on (you committed to `main` while `experiment` was off doing
its thing), the two lines of history diverged. Git stitches them together with a new commit that
has two parents.
Git picks between these based on whether the branches diverged. You recognize them in the log: a
fast-forward is a straight line, a merge commit is a visible fork-and-join.
```console
$ git log --oneline --graph
* 9f3c1a2 Merge branch 'experiment'
|\
| * 4b8d0e1 Add task priorities (experiment)
* | 2a1f9c7 Fix list ordering on main
|/
* 7c0e3d4 Initial tasks app
```
There are two outcomes, and it's worth knowing which you got:
- **Fast-forward.** If `main` hasn't moved since you branched (it's still at C), Git doesn't need to
do anything clever — it just slides the `main` label forward to F. The history stays a straight
line. This is the common case for a solo experiment.
- **Merge commit.** If `main` *did* move on (someone — or you — committed to `main` while
`experiment` was off doing its thing), the two lines of history have diverged. Git stitches them
together with a new commit that has two parents. You'll be dropped into an editor to confirm the
merge message; save and close it.
You don't choose between these — Git picks based on whether the branches diverged. You just need to
recognize them in `git log --oneline --graph`, where a fast-forward is a straight line and a merge
commit is a visible fork-and-join.
After a successful merge, the branch has done its job. Delete it:
```bash
git branch -d experiment # -d refuses if it's NOT fully merged — a safety check
```
After a successful merge the branch has done its job, and `git branch -d experiment` deletes it. The
lowercase `-d` refuses if the branch isn't fully merged, which is a safety check. Again, the agent
runs this once you've decided; you confirm the branch is gone with `git branch`.
### Discarding: killing the experiment
This is the payoff. The AI tried something bold on the branch, you looked at it, and you don't want
it. You don't undo anything. You don't `restore` file by file. You switch away and delete the branch:
```bash
git switch main # your files snap back to known-good main
git branch -D experiment # -D force-deletes even though it was never merged
```
it. You don't undo anything. You don't `restore` file by file. You switch away and delete the branch
(`git switch main`, then `git branch -D experiment`, which force-deletes even though it was never
merged). The agent runs both on your say-so.
That's it. The experiment is gone. `main` never changed. `git log` on `main` shows no sign it ever
happened. **The whole bold attempt cost you one branch and one delete.**
This is the mental shift the module is selling: when discarding is this cheap, you stop being
precious about what you let the AI try. Risky refactor? Branch it. Want to compare two approaches?
A branch each, keep the winner, delete the loser. The branch is the unit of "maybe."
This is the mental shift the module is selling: when discarding is this cheap, you stop being precious
about what you let the AI try. Risky refactor? Branch it. Want to compare two approaches? A branch
each, keep the winner, delete the loser. The branch is the unit of "maybe."
### Merge conflicts: when two changes collide
@@ -198,19 +178,16 @@ Read it like this:
- Both markers and the divider are real text Git inserted into your file. Resolving means **editing
the file so it contains the version you want and deleting all three marker lines.**
You're not picking a side mechanically — you're deciding what the line *should* say. Often that's one
side, sometimes it's a blend of both (here: a usage string that lists *both* `stats` and `purge`).
Then you tell Git the conflict is settled:
Resolving isn't picking a side mechanically. It's deciding what the line *should* say. Often that's
one side; sometimes it's a blend of both (here, a usage string that lists *both* `stats` and `purge`).
This is the kind of bounded reasoning task the AI is good at: it sees both versions and the
surrounding code, so you hand it the conflict and let it produce the combined version. Once the file
is correct and marker-free, telling Git the conflict is settled is two more commands the agent runs
(`git add cli.py` to mark the file resolved, then `git commit` to complete the merge).
```bash
# edit the file: remove the markers, leave the correct content
git add cli.py # marks this file's conflict as resolved
git commit # completes the merge (opens an editor for the merge message)
```
`git status` during a conflict is your map — it lists every file still "unmerged." When that list is
empty and you've `git add`-ed them all, you commit and the merge is done. If you panic mid-conflict,
`git merge --abort` rewinds you to before the merge, no harm done.
`git status` during a conflict is your map; it lists every file still "unmerged." Your job is the
verify: read the resolution, confirm it's what you meant, and check `git status` comes back clean. If
things go sideways, `git merge --abort` rewinds to before the merge with no harm done.
---
@@ -260,78 +237,96 @@ deliberately create and resolve a merge conflict — using the AI to help resolv
### Part A — Branch it and let the AI go bold
1. Confirm you're on `main` and clean, then create an experiment branch and switch to it:
1. Make sure you're in the repo, then **tell the agent to set up the branch.** Ask:
> *"We're on the `tasks-app` repo. Confirm we're on `main` with a clean working tree, then create
> a branch called `experiment/priorities` and switch to it."*
Then **verify** it did what you asked, by hand:
```bash
cd ~/ai-workflow-course/tasks-app
git switch main
git status # must be clean
git switch -c experiment/priorities
git branch # the * is now on experiment/priorities
git status # should be clean, on experiment/priorities
git branch # the * should be on experiment/priorities
```
2. Give the AI a deliberately *bold* task — the kind you'd hesitate to run straight on `main`:
You're not typing the branch commands; you're confirming the agent ran them correctly. This is the
pattern for the whole module: you direct, the agent does the git, you check.
2. Give the AI a deliberately *bold* task, the kind you'd hesitate to run straight on `main`:
> *"Add task priorities (low/medium/high) to this app. Store a priority on each task, let me set
> it when adding (`add "thing" --priority high`), show it in `list`, and sort `list` so high
> priority comes first. Change whatever files you need to."*
Let it edit `tasks.py` and `cli.py` freely. This is a multi-file change — exactly the kind that's
nerve-wracking on `main` and relaxed on a branch.
Let it edit `tasks.py` and `cli.py` freely. This is a multi-file change: nerve-wracking on `main`,
relaxed on a branch.
3. Review and commit the experiment **on the branch**:
3. Review the change, then have the agent commit it **on the branch**. First read the diff and run
the app yourself:
```bash
git diff # read what it actually changed
python cli.py add "ship module 6" --priority high
python cli.py add "water plants" --priority low
python cli.py list # see if priorities work and sort
git add .
git commit -m "Add task priorities (experiment)"
```
4. Now prove the isolation. Switch back to `main` and watch the feature **disappear**:
Once the diff looks right and the feature runs, tell the agent:
> *"Commit this on the branch with a message like 'Add task priorities (experiment)'."*
The agent decides what to stage and writes the commit. Confirm it landed with `git log --oneline`.
4. Now prove the isolation. Ask the agent to switch back to `main`, then watch the feature
**disappear**:
> *"Switch back to `main`."*
```bash
git switch main
python cli.py list # no priorities — main is exactly as you left it
python cli.py list # no priorities; main is exactly as you left it
```
Your bold change exists only on the branch. `main` never saw it. Sit with that for a second —
that's the whole point.
Your bold change exists only on the branch. `main` never saw it, and that's the whole point.
### Part B — Decide its fate
Pick the path that matches reality. Do at least one; ideally do **Path 2 (discard)** on this
experiment so you feel how clean it is, then re-run Part A and do **Path 1 (keep)** so you've done both.
**The decision is yours; the execution is the agent's.** Pick the path that matches reality. Do at
least one; ideally do **Path 2 (discard)** on this experiment so you feel how clean it is, then re-run
Part A and do **Path 1 (keep)** so you've done both.
**Path 1 — Keep it (merge):**
**Path 1 — Keep it (merge).** Tell the agent:
> *"Merge `experiment/priorities` into `main`, then delete the branch."*
Then verify the result yourself:
```bash
git switch main
git merge experiment/priorities # likely a fast-forward: main slides up to the branch
git log --oneline --graph # see the history; straight line = fast-forward
git log --oneline --graph # straight line = fast-forward merge
python cli.py list # the feature is now on main
git branch -d experiment/priorities # branch did its job; -d is the safe delete
git branch # experiment/priorities is gone
```
**Path 2 — Throw it away (discard):**
**Path 2 — Throw it away (discard).** Tell the agent:
> *"Switch to `main` and discard the `experiment/priorities` branch entirely."*
Then verify:
```bash
git switch main # files snap back to known-good main
git branch -D experiment/priorities # force-delete the unmerged branch
git log --oneline # no trace of the experiment on main
python cli.py list # main is untouched, exactly as before
git branch # the branch is gone
```
Notice what you did *not* do in Path 2: no file-by-file `restore`, no manual undo, no hunting through
diffs. You deleted a label and the entire experiment was gone. That's the economics shift bold AI
attempts become free to reject.
diffs. The agent deleted a label and the entire experiment was gone. That's the economics shift: bold
AI attempts become free to reject.
### Part C — Create a merge conflict and resolve it with the AI
Now the skill everyone fears and nobody should. You'll engineer a guaranteed conflict by having
**two branches change the same line in different ways**, then resolve it.
Merge conflicts have an outsized reputation for difficulty. You'll engineer a guaranteed one by having
**two branches change the same line in different ways**, then resolve it with the agent.
> **Starting state.** By now your `tasks-app` has accumulated commands from earlier modules, so your
> `usage:` line is longer than the bare `[add <title> | list | done <index>]` you started with — and
@@ -341,47 +336,40 @@ Now the skill everyone fears and nobody should. You'll engineer a guaranteed con
> `purge`. (Any two brand-new commands would do; the point is the same line, edited two ways.) The
> marker examples below show the shape; your real markers will carry your fuller usage string.
1. Make sure you're on a clean `main`. Create the first branch and have the AI add a `stats` command:
1. From a clean `main`, set up the first branch and the `stats` command in one instruction to the
agent:
> *"From `main`, create a branch `feature/stats`, add a `stats` command to `cli.py` that prints how
> many tasks are total, done, and pending, update the usage string to include it, then commit it
> with the message 'Add stats command'."*
Verify the agent edited the usage line and committed:
```bash
git switch main
git switch -c feature/stats
git diff main # the usage line changed + the command was added
git log --oneline # the commit is there, on feature/stats
```
Ask the AI: *"Add a `stats` command to `cli.py` that prints how many tasks are total, done, and
pending, and update the usage string to include it."* Then:
2. Now the second branch, which touches **the same usage line** a different way:
> *"Switch back to `main`, create a branch `feature/purge`, add a `purge` command to `cli.py` that
> removes all completed (done) tasks, update the usage string to include it, then commit it with
> the message 'Add purge command'."*
Verify the collision is set up:
```bash
git diff # confirm it edited the usage line + added the command
git add . && git commit -m "Add stats command"
git diff main # feature/purge edited the same usage line
```
2. Switch back to `main` and create a *different* branch that touches **the same usage line**:
Both branches changed the same `usage:` line, each adding a *different* command. Git won't be able
to auto-merge that line.
```bash
git switch main
git switch -c feature/purge
```
3. Now trigger the conflict. Tell the agent:
Ask the AI: *"Add a `purge` command to `cli.py` that removes all completed (done) tasks, and update
the usage string to include it."* Then:
> *"You're on `feature/purge`. Merge `feature/stats` into it."*
```bash
git diff # it also edited the usage line — this is the collision to come
git add . && git commit -m "Add purge command"
```
Both branches changed the same `usage:` line, each adding a *different* command to it. Git will
not be able to auto-merge that line.
3. Merge them and watch it conflict. Merge `feature/stats` into `feature/purge` (you're on
`feature/purge`):
```bash
git merge feature/stats
```
Git stops with a conflict and tells you which file is unmerged. Confirm:
Git stops with a conflict. Confirm the conflict state yourself:
```bash
git status # cli.py listed under "Unmerged paths"
@@ -402,7 +390,7 @@ Now the skill everyone fears and nobody should. You'll engineer a guaranteed con
(The command bodies for `stats` and `purge` touch different lines, so Git merged *those* cleanly
on its own — the only collision is the usage string both branches edited.)
5. **Resolve it with the AI.** With your editor-integrated agent, this is its sweet spot. Ask:
5. **Resolve it with the AI.** This is exactly the bounded task the agent is good at. Ask:
> *"`cli.py` has a merge conflict on the usage line. I want the final version to list BOTH the
> `stats` and `purge` commands. Resolve the conflict and remove the markers."*
@@ -424,17 +412,19 @@ Now the skill everyone fears and nobody should. You'll engineer a guaranteed con
python cli.py purge
```
6. Tell Git the conflict is settled and complete the merge:
6. Once you've verified the resolution, have the agent finish the merge:
> *"The resolution looks right. Stage `cli.py` and complete the merge."*
Then confirm the merge landed as a merge commit:
```bash
git add cli.py
git commit # opens an editor for the merge message; save and close
git log --oneline --graph # see the fork-and-join: this is a merge commit
git log --oneline --graph # the fork-and-join: this is a merge commit
```
You just resolved a real merge conflict. The marker syntax is identical no matter the file or the
project — once you can read those three lines, conflicts stop being scary and become a five-minute
chore.
You just resolved a real merge conflict: you directed it, the agent did the plumbing, and you
verified the result. The marker syntax is identical no matter the file or the project. Once you can
read those three lines and check the resolution, a conflict is a short, routine task.
> **Guaranteed-conflict generator.** AI edits are nondeterministic, so if the agent didn't touch the
> same line on both branches and you *didn't* get a conflict in step 3, run the helper script to
@@ -443,12 +433,13 @@ Now the skill everyone fears and nobody should. You'll engineer a guaranteed con
> *You'll need*), then run it from inside the repo:
>
> ```bash
> cp /path/to/modules/06-branches-sandboxes-for-experiments/lab/make-conflict.sh .
> cp ~/ai-workflow-course/the-workflow-course/modules/06-branches-sandboxes-for-experiments/lab/make-conflict.sh .
> bash make-conflict.sh
> ```
>
> It creates two branches that both edit the same line of `README.md`, leaving you mid-conflict with
> on-screen instructions. The resolution mechanic is identical to the code case above.
> on-screen instructions. From there, hand it to the agent the same way (step 5), then verify. The
> resolution mechanic is identical to the code case above.
---
@@ -485,15 +476,15 @@ The honest limits, so you don't over-trust the sandbox:
**You're done when:**
- You created a branch, let the AI make a multi-file change on it, and confirmed `main` was untouched
by switching back and seeing the change vanish.
- You have **discarded** an experiment with `git branch -D` and confirmed `main` shows no trace, and
you have **merged** one in and seen it land on `main`.
- You directed the agent to branch, let the AI make a multi-file change on it, and confirmed `main`
was untouched by switching back and seeing the change vanish.
- You have **discarded** an experiment (the agent ran `git branch -D`) and confirmed `main` shows no
trace, and you have **merged** one in and seen it land on `main`.
- You can explain, in one sentence, why creating a branch costs essentially nothing (it's a movable
pointer, not a copy).
- You deliberately created a merge conflict, read the `<<<<<<<`/`=======`/`>>>>>>>` markers, resolved
it (with the AI's help) to a marker-free file that runs, and completed the merge with `git add` +
`git commit`.
- You deliberately created a merge conflict, read the `<<<<<<<`/`=======`/`>>>>>>>` markers, had the
AI resolve it to a marker-free file that runs, verified the result, and let the agent complete the
merge.
- You can name the limit: a branch isolates tracked files, not your database, ignored files, or the
outside world.
@@ -10,7 +10,7 @@
# then `git add` + `git commit`.
#
# Copy it into your tasks-app repo, then run it from inside the repo:
# cp /path/to/modules/06-branches-sandboxes-for-experiments/lab/make-conflict.sh .
# cp ~/ai-workflow-course/the-workflow-course/modules/06-branches-sandboxes-for-experiments/lab/make-conflict.sh .
# bash make-conflict.sh
#
# It is non-destructive to your real work: it only touches README.md on two throwaway practice
@@ -73,11 +73,11 @@ echo "================================================================"
echo
echo " Next steps (the skill you're practicing):"
echo " 1. git status # see $FILE under 'Unmerged paths'"
echo " 2. open $FILE and find the <<<<<<< / ======= / >>>>>>> markers"
echo " 3. edit it to the version you want; delete all three marker lines"
echo " (or ask your editor-integrated AI to resolve it, then verify)"
echo " 4. git add $FILE"
echo " 5. git commit # completes the merge"
echo " 2. ask your agent to resolve the conflict in $FILE and complete the merge"
echo " (\"resolve the conflict markers in $FILE and finish the merge\")"
echo " 3. verify: open $FILE, confirm no <<<<<<< / ======= / >>>>>>> markers remain"
echo " 4. git log --oneline --graph # confirm the merge commit landed"
echo " (to do it by hand instead: edit out the markers, then git add $FILE && git commit)"
echo
echo " Chicken out? Undo the whole thing with: git merge --abort"
echo