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:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user