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:
@@ -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 13–14).
|
||||
- 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.
|
||||
|
||||
@@ -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 1–3 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 2–3 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 1–2
|
||||
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 1–2, 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 1–2 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`.)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user