Merge pull request 'Module 6: reframe Part C around the AI silently auto-resolving conflicts' (#99) from claude/issue-97 into main
Sync course wiki / sync-wiki (push) Successful in 5s

Reviewed-on: #99
This commit was merged in pull request #99.
This commit is contained in:
2026-06-23 09:07:34 -04:00
3 changed files with 111 additions and 77 deletions
+16 -18
View File
@@ -121,7 +121,7 @@ That's it. Notice what you did *not* do: no file-by-file restore, no manual undo
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 becomes your 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 becomes your unit of "maybe."
## Merge conflicts: when two changes collide (and the AI helps) ## Merge conflicts: when two changes collide (and the AI resolves them before you see them)
Most merges just work; Git is genuinely good at combining changes that touch *different* lines. A **conflict** only happens when two branches changed the *same* lines in different ways, and Git refuses to guess which you meant. It stops and marks the collision right inside the file: Most merges just work; Git is genuinely good at combining changes that touch *different* lines. A **conflict** only happens when two branches changed the *same* lines in different ways, and Git refuses to guess which you meant. It stops and marks the collision right inside the file:
@@ -135,27 +135,25 @@ Most merges just work; Git is genuinely good at combining changes that touch *di
Read it like this. Everything from `<<<<<<< HEAD` to `=======` is **your current branch's version**. Everything from `=======` to `>>>>>>> feature/stats` is **the incoming version**. The markers are real text Git inserted into your file. Resolving means editing the file so it holds the version you want (often a blend of both, here a usage string listing *both* commands) and deleting all three marker lines. Read it like this. Everything from `<<<<<<< HEAD` to `=======` is **your current branch's version**. Everything from `=======` to `>>>>>>> feature/stats` is **the incoming version**. The markers are real text Git inserted into your file. Resolving means editing the file so it holds the version you want (often a blend of both, here a usage string listing *both* commands) and deleting all three marker lines.
You can manufacture exactly this in `tasks-app`: make one branch where the AI adds a `stats` command (updating the usage string), then a *separate* branch off `main` where it adds a `purge` command (also updating the usage string). Both edit the same line. Merge one into the other and Git stops cold: Here's the twist, and it's the reason I'm not going to hand you a "read the markers, edit them out" drill and call it a skill. You can manufacture exactly this collision in `tasks-app`: make one branch where the AI adds a `stats` command (updating the usage string), then a *separate* branch off `main` where it adds a `purge` command (also updating the usage string). Both edit the same line. Then tell a current editor-agent to "merge `feature/stats` into `feature/purge`," and watch what *doesn't* happen: it doesn't stop. It reads both sides, picks the resolution, finishes the merge, and reports a clean result, all in one turn. You never see a marker. From your chair the conflict simply didn't occur.
That's the sweet spot for the AI (a small, perfectly bounded reasoning task with both sides and the surrounding code right there) and it's also the trap. So do this once, deliberately, to see the machine: ask it to stop instead of resolving.
> *"Merge `feature/stats` into `feature/purge`. If it conflicts, stop and show me the conflict; don't resolve it yet."*
Now Git pauses on the unmerged file and you can read the markers above with your own eyes. Then `git merge --abort` to rewind, and let the agent do it for real with no guard rail, the way you actually would:
> *"Merge `feature/stats` into `feature/purge`; the usage line collides, and the final version should list BOTH commands."*
It resolves silently and the merge lands. And here is the only part that's still your job, conflict or no conflict:
```bash ```bash
git merge feature/stats git diff HEAD~1 # what the merge actually changed; confirm no markers, both commands present
git status # cli.py listed under "Unmerged paths"
```
And here's where editor-integrated AI earns its keep, because a merge conflict is *the* sweet spot for it: a small, perfectly bounded reasoning task with both sides and the surrounding code right there. 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."*
It should hand back a single marker-free line. Then you settle it with Git:
```bash
git diff # check ONLY what you intended changed; no markers remain
python cli.py # run it: see the merged usage string python cli.py # run it: see the merged usage string
git add cli.py python cli.py stats && python cli.py purge # both actually work
git commit # opens an editor for the merge message; save and close
``` ```
Once you can read those three lines of markers, conflicts stop being scary and become a five-minute chore. The syntax is identical no matter the file or the project. (And if your AI's edits didn't happen to collide (they're nondeterministic), the course ships a little `make-conflict.sh` helper that manufactures one deterministically so you can still practice.) That `git diff` after *every* merge is the whole skill now. Not "edit the markers by hand," which the AI did for you before you could blink, but "know a conflict can happen and check the silent resolution," because a resolution that runs cleanly can still be wrong and it won't leave an error behind to warn you. (And if your AI's edits didn't happen to collide (they're nondeterministic), the course ships a little `make-conflict.sh` helper that manufactures one deterministically so you can still see the markers at least once.)
## The AI angle: why this matters *more* now ## The AI angle: why this matters *more* now
@@ -177,7 +175,7 @@ The honest limits, so you don't over-trust the sandbox:
## You're done when ## You're done when
You've created a branch, let the AI make a multi-file change on it, and confirmed `main` was untouched by switching back and watching the change vanish. You've **discarded** an experiment with `git branch -D` and seen `main` show no trace, and you've **merged** one in and seen it land. You can explain in one sentence why a branch costs essentially nothing (it's a movable pointer, not a copy). And you've read those `<<<<<<<` / `=======` / `>>>>>>>` markers, resolved a real conflict to a clean file that runs, and completed the merge. You've created a branch, let the AI make a multi-file change on it, and confirmed `main` was untouched by switching back and watching the change vanish. You've **discarded** an experiment with `git branch -D` and seen `main` show no trace, and you've **merged** one in and seen it land. You can explain in one sentence why a branch costs essentially nothing (it's a movable pointer, not a copy). And you've seen those `<<<<<<<` / `=======` / `>>>>>>>` markers at least once, then watched the AI merge for real and resolve the conflict silently, and you verified the result with `git diff` even though no marker was ever shown to you.
When "let the agent try something wild" feels like a one-line decision instead of a risk assessment, you've got it. When "let the agent try something wild" feels like a one-line decision instead of a risk assessment, you've got it.
@@ -36,8 +36,8 @@ By the end of this module you can:
2. Let the AI make a bold, multi-commit change on a branch while `main` stays untouched and runnable. 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, 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. 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 4. Recognize a merge conflict (the `<<<<<<<`/`=======`/`>>>>>>>` markers) when you see one, and
resolve, then verify the resolution is right before the merge completes. verify the AI's resolution even when the agent resolved it silently and you never saw a marker.
5. Tell the difference between a fast-forward merge and a merge commit, and know which one you got. 5. Tell the difference between a fast-forward merge and a merge commit, and know which one you got.
--- ---
@@ -181,13 +181,25 @@ Read it like this:
Resolving isn't picking a side mechanically. It's deciding what the line *should* say. Often that's 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`). 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 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 surrounding code. Once the file is correct and marker-free, telling Git the conflict is settled is
is correct and marker-free, telling Git the conflict is settled is two more commands the agent runs two more commands the agent runs (`git add cli.py` to mark the file resolved, then `git commit` to
(`git add cli.py` to mark the file resolved, then `git commit` to complete the merge). complete the merge).
`git status` during a conflict is your map; it lists every file still "unmerged." Your job is the Here's the part that has changed under your feet, and it's the real lesson of this module's lab. The
verify: read the resolution, confirm it's what you meant, and check `git status` comes back clean. If markers above are what a conflict looks like *if you ever see one*. Tell a current frontier
things go sideways, `git merge --abort` rewinds to before the merge with no harm done. editor-agent to "merge `feature/stats` into `feature/purge`" and it usually never stops: it reads
both sides, resolves the collision, completes the merge, and reports a clean result, all in one turn.
You never saw a marker. From your seat the conflict simply did not happen. That is convenient right
up until the silent resolution is wrong (it can keep the worse of the two sides, or blend them into a
line that satisfies neither), and now a bad merge is sitting in your history with nothing that looked
like an error.
So the skill is no longer "edit the markers by hand." It is two things: **know what a conflict is**
(so you recognize one when an agent does surface it) and **check `git diff` after every merge** (so a
silent resolution can't slip a wrong line past you). `git status` during a conflict is your map; it
lists every file still "unmerged." If you want to *see* the markers before the agent touches them,
tell it to stop on conflict and show you (you'll do exactly that in the lab). And if things go
sideways, `git merge --abort` rewinds to before the merge with no harm done.
--- ---
@@ -207,12 +219,15 @@ Everything above is standard Git. Here's why it matters *more* in an AI-assisted
- **Compare, don't commit-and-hope.** Ask the AI for approach A on one branch and approach B on - **Compare, don't commit-and-hope.** Ask the AI for approach A on one branch and approach B on
another. Run both. Keep the winner, delete the loser. You're using branches as cheap A/B another. Run both. Keep the winner, delete the loser. You're using branches as cheap A/B
experiments on implementation, something that's painful without them and trivial with them. experiments on implementation, something that's painful without them and trivial with them.
- **Conflicts are a great place to put the AI to work.** A merge conflict is a small, perfectly - **The AI resolves conflicts so well you may never see one.** A merge conflict is a small, perfectly
bounded reasoning task: here are two versions of the same lines and the surrounding code; produce bounded reasoning task: here are two versions of the same lines and the surrounding code; produce
the correct combined version. The AI can see both sides and the intent. You still decide whether the correct combined version. A current editor-agent is good enough at this that, told to "merge X
its resolution is right (it can absolutely merge two changes into something that satisfies neither), into Y," it usually resolves the collision and completes the merge in the same turn, no markers
but "explain this conflict and propose a resolution" is one of the highest-hit-rate uses of an shown, no question asked. That's the highest-hit-rate convenience of the tool and its sharpest trap:
editor-integrated agent. You'll do exactly this in the lab. you still decide whether the resolution is right (it can absolutely merge two changes into something
that satisfies neither), except now you might not even know there *was* a conflict to second-guess.
The defense is mechanical and non-negotiable: read `git diff` after every merge. You'll feel both
the convenience and the trap in the lab.
--- ---
@@ -221,8 +236,9 @@ Everything above is standard Git. Here's why it matters *more* in an AI-assisted
**Lab language:** shell (Git commands), driving the `tasks-app` from Modules 12 with your **Lab language:** shell (Git commands), driving the `tasks-app` from Modules 12 with your
editor-integrated AI from Module 4. editor-integrated AI from Module 4.
You'll do three things: let the AI try a bold change on a branch, decide its fate, and then You'll do three things: let the AI try a bold change on a branch, decide its fate, and then engineer
deliberately create and resolve a merge conflict, using the AI to help resolve it. a merge conflict so you can see one once, undo it, and watch the AI resolve it silently while you do
the one job that's still yours: verify the result.
**You'll need:** **You'll need:**
@@ -365,20 +381,22 @@ Merge conflicts have an outsized reputation for difficulty. You'll engineer a gu
Both branches changed the same `usage:` line, each adding a *different* command. Git won't be able Both branches changed the same `usage:` line, each adding a *different* command. Git won't be able
to auto-merge that line. to auto-merge that line.
3. Now trigger the conflict. Tell the agent: 3. **Witness the conflict first.** If you tell a current agent to just "merge them," it will resolve
the collision and finish the merge in one turn, and you'll never see a marker (you'll do exactly
that in step 5, on purpose). So this once, ask it to stop and show you instead, the same way
Module 26 does it:
> *"You're on `feature/purge`. Merge `feature/stats` into it."* > *"You're on `feature/purge`. Merge `feature/stats` into it. If it conflicts, stop and show me the
> conflict; do not resolve it yet."*
Git stops with a conflict. Confirm the conflict state yourself: The merge stops on the usage line. Confirm the conflict state yourself, then open `cli.py` and find
the markers (your usage string will be longer (it carries the commands from earlier modules), but
the collision is exactly this: both branches appended a different new command to the same line):
```bash ```bash
git status # cli.py listed under "Unmerged paths" git status # cli.py listed under "Unmerged paths"
``` ```
4. Open `cli.py` and find the conflict markers around the usage line (your usage string will be
longer (it carries the commands from earlier modules), but the collision is exactly this: both
branches appended a different new command to it):
```python ```python
<<<<<<< HEAD <<<<<<< HEAD
print("usage: python cli.py [add <title> | list | done <index> | purge]") print("usage: python cli.py [add <title> | list | done <index> | purge]")
@@ -387,50 +405,63 @@ Merge conflicts have an outsized reputation for difficulty. You'll engineer a gu
>>>>>>> feature/stats >>>>>>> feature/stats
``` ```
(The command bodies for `stats` and `purge` touch different lines, so Git merged *those* cleanly This is the whole point of the step: *see one real conflict* so you can recognize the shape. `HEAD`
on its own; the only collision is the usage string both branches edited.) is your current branch (`feature/purge`); the block below the `=======` is what `feature/stats`
wants. (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.** This is exactly the bounded task the agent is good at. Ask: 4. **Undo it.** You've seen the conflict; now rewind so the AI can handle it from scratch. Tell the
agent (or run it yourself, it's the safe-undo from the Key concepts section):
> *"`cli.py` has a merge conflict on the usage line. I want the final version to list BOTH the > *"Abort the merge."*
> `stats` and `purge` commands. Resolve the conflict and remove the markers."*
It should produce a single, marker-free line listing both commands, e.g.: ```bash
git merge --abort
git status # clean again, back on feature/purge, no merge in progress
```
You're now exactly where you were before step 3, mid-experiment with two colliding branches and no
merge underway.
5. **Now let the AI do it for real, and watch it auto-resolve.** This time, no stop-on-conflict guard.
Direct it the way you actually would in a real workflow:
> *"You're on `feature/purge`. Merge `feature/stats` into it. The usage line collides; the final
> version should list BOTH the `stats` and `purge` commands."*
Notice what happens: the agent hits the same conflict you just saw, resolves it, and completes the
merge in one turn. It probably never shows you a marker. From your seat the merge just "worked." It
should have produced a single, marker-free line listing both commands, e.g.:
```python ```python
print("usage: python cli.py [add <title> | list | done <index> | stats | purge]") print("usage: python cli.py [add <title> | list | done <index> | stats | purge]")
``` ```
**Verify its work; this is the part the AI can get subtly wrong.** A conflict resolver can **Here is the punchline of the whole module: you have no idea yet whether that's right, so verify.**
confidently drop one side, leave a stray marker, or "blend" the lines into something that runs but The conflict was invisible, which means a wrong resolution would have been invisible too. A resolver
means the wrong thing. Read the result and run it: can confidently drop one side, leave a stray marker, or "blend" the lines into something that runs
but means the wrong thing. The only thing standing between you and a silently-bad merge is the
`git diff` you run *after every merge*, conflict or not:
```bash ```bash
git diff # check ONLY what you intended changed; no markers remain git diff HEAD~1 # what the merge actually changed; confirm no markers remain
git log --oneline --graph # the fork-and-join: this is a merge commit
python cli.py # run with no args, see the merged usage string python cli.py # run with no args, see the merged usage string
python cli.py stats # both commands actually work python cli.py stats # both commands actually work
python cli.py purge python cli.py purge
``` ```
6. Once you've verified the resolution, have the agent finish the merge: If the usage line lists both commands and both run, the AI's silent resolution was correct. If it
dropped one, you just caught a bug that left no error message behind, which is precisely why the
> *"The resolution looks right. Stage `cli.py` and complete the merge."* check isn't optional. You directed the merge, the agent did the plumbing *and* the resolution, and
the verify was yours. That last part is the skill: not reading markers by hand, but knowing a
Then confirm the merge landed as a merge commit: conflict can happen and checking the AI's work even when it never tells you one did.
```bash
git log --oneline --graph # the fork-and-join: this is a merge commit
```
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 > **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 > same line on both branches and you *didn't* get a conflict in step 3, run the helper script to
> manufacture one deterministically, then practice steps 46 on it. Copy it into your `tasks-app` > manufacture one deterministically, then practice the witness-and-verify flow on it. Copy it into
> first (the course's lab scripts live in the course repo, not in `tasks-app`; see Module 4's > your `tasks-app` first (the course's lab scripts live in the course repo, not in `tasks-app`; see
> *You'll need*), then run it from inside the repo: > Module 4's *You'll need*), then run it from inside the repo:
> >
> ```bash > ```bash
> cp ~/ai-workflow-course/the-workflow-course/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 .
@@ -457,11 +488,14 @@ The honest limits, so you don't over-trust the sandbox:
branch isn't shared, backed up, or visible to anyone else until there's a remote; that's branch isn't shared, backed up, or visible to anyone else until there's a remote; that's
**Module 8**. Right now `git branch -D` deletes work that exists nowhere else, permanently. Treat **Module 8**. Right now `git branch -D` deletes work that exists nowhere else, permanently. Treat
an unpushed branch as exactly as fragile as the rest of your local-only repo. an unpushed branch as exactly as fragile as the rest of your local-only repo.
- **The AI can resolve a conflict into something plausible and wrong.** It sees both sides and the - **The AI can resolve a conflict into something plausible and wrong, and you may never know one
intent, which makes it good at this, but "good" isn't "trusted." A resolution that runs cleanly can happened.** It sees both sides and the intent, which makes it good at this, but "good" isn't
still mean the wrong thing (silently keeping the worse of two changes, or merging two behaviors "trusted." Worse, a current agent resolves silently: told to merge, it fixes the collision and
into one that satisfies neither). The `git diff` + run-it check in the lab isn't optional ceremony; finishes the merge in one turn, so a resolution that runs cleanly but means the wrong thing
it's the actual safeguard. Reviewing AI output is its own discipline; that's Module 10. (silently keeping the worse of two changes, or merging two behaviors into one that satisfies
neither) leaves no marker, no prompt, no error behind. That invisibility is exactly *why* the
post-merge `git diff` is the safeguard, not optional ceremony: it's the only thing that surfaces a
conflict the agent already swallowed. Reviewing AI output is its own discipline; that's Module 10.
- **Long-lived branches drift and conflict harder.** The longer a branch lives away from `main`, the - **Long-lived branches drift and conflict harder.** The longer a branch lives away from `main`, the
more `main` moves underneath it and the gnarlier the eventual merge. The defense is the same as more `main` moves underneath it and the gnarlier the eventual merge. The defense is the same as
"commit often": branch small, merge soon, delete promptly. A branch that's been open for three "commit often": branch small, merge soon, delete promptly. A branch that's been open for three
@@ -482,9 +516,9 @@ The honest limits, so you don't over-trust the sandbox:
trace, and you have **merged** one in and seen it land on `main`. 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 - You can explain, in one sentence, why creating a branch costs essentially nothing (it's a movable
pointer, not a copy). pointer, not a copy).
- You deliberately created a merge conflict, read the `<<<<<<<`/`=======`/`>>>>>>>` markers, had the - You saw a real merge conflict at least once (the `<<<<<<<`/`=======`/`>>>>>>>` markers), then let
AI resolve it to a marker-free file that runs, verified the result, and let the agent complete the the AI merge for real and resolve it silently, and you verified the result with `git diff` even
merge. though no marker was ever shown to you, confirming the merged file runs.
- You can name the limit: a branch isolates tracked files, not your database, ignored files, or the - You can name the limit: a branch isolates tracked files, not your database, ignored files, or the
outside world. outside world.
@@ -73,10 +73,12 @@ echo "================================================================"
echo echo
echo " Next steps (the skill you're practicing):" echo " Next steps (the skill you're practicing):"
echo " 1. git status # see $FILE under 'Unmerged paths'" echo " 1. git status # see $FILE under 'Unmerged paths'"
echo " 2. ask your agent to resolve the conflict in $FILE and complete the merge" echo " 2. open $FILE and read the <<<<<<< / ======= / >>>>>>> markers yourself FIRST"
echo " (this is your chance to see a real conflict before an agent resolves it away)"
echo " 3. 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 " (\"resolve the conflict markers in $FILE and finish the merge\")"
echo " 3. verify: open $FILE, confirm no <<<<<<< / ======= / >>>>>>> markers remain" echo " 4. verify: open $FILE, confirm no <<<<<<< / ======= / >>>>>>> markers remain"
echo " 4. git log --oneline --graph # confirm the merge commit landed" echo " 5. 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 " (to do it by hand instead: edit out the markers, then git add $FILE && git commit)"
echo echo
echo " Chicken out? Undo the whole thing with: git merge --abort" echo " Chicken out? Undo the whole thing with: git merge --abort"