Build out all 27 modules + capstone (#1)
Co-authored-by: claude <claude@jpaul.io> Co-committed-by: claude <claude@jpaul.io>
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
# Module 7 — Worktrees: Running Agents in Parallel
|
||||
|
||||
> **A branch lets one agent try something risky. A worktree lets two agents try two things at the
|
||||
> same wall-clock time — in separate folders, on separate branches, without touching each other's
|
||||
> files.** This is the move that turns "I run an agent" into "I run agents."
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Module 6 — Branches** — you can create a branch, switch to it, merge it back, and resolve a
|
||||
conflict. A worktree is the physical counterpart to the logical isolation a branch already gives
|
||||
you, so this module makes no sense without it.
|
||||
- **Module 4 — Getting the AI out of the browser** — the agents in this module edit real files in a
|
||||
folder. You'll point an editor-integrated AI session at each worktree directory.
|
||||
- **Module 2 — Version control** — the `tasks-app` is already a Git repo with commits, and you read
|
||||
a project's state from `git status` / `git diff` / `git log`. Each worktree has its own answer to
|
||||
those, which is the whole point.
|
||||
- **Module 1 — the `tasks-app`** — the running example continues here.
|
||||
|
||||
If you parachuted in: you minimally need a Git repo with at least one commit and a working
|
||||
understanding of branches.
|
||||
|
||||
---
|
||||
|
||||
## Learning objectives
|
||||
|
||||
By the end of this module you can:
|
||||
|
||||
1. Explain why a single working directory is the bottleneck the moment you want two agents running
|
||||
at once, and why branches alone don't fix it.
|
||||
2. Create, list, and remove linked worktrees (`git worktree add` / `list` / `remove`), each on its
|
||||
own branch.
|
||||
3. Run two independent AI edit sessions on the same project simultaneously without them colliding on
|
||||
files, branches, or app state.
|
||||
4. Merge parallel work back to `main` and clean up worktrees without leaving stale state behind.
|
||||
5. State precisely what worktrees share (history/objects) and what they don't (working files,
|
||||
uncommitted changes, checked-out branch) — and where that bites.
|
||||
|
||||
---
|
||||
|
||||
## Key concepts
|
||||
|
||||
### Where branches alone run out
|
||||
|
||||
Module 6 gave you branches: spin one up, let the agent do something wild, keep it or throw it away
|
||||
with zero risk to `main`. That's logical isolation — two lines of history that don't affect each
|
||||
other.
|
||||
|
||||
But there's a physical fact branches don't change: **a repo has exactly one working directory, and
|
||||
only one branch can be checked out in it at a time.** The files on disk are *the* files. When you
|
||||
`git switch other-branch`, Git rewrites those same files in place to match the other branch. There's
|
||||
one floor, and switching branches yanks it out and lays a different one down.
|
||||
|
||||
That's fine when *you* are the only one standing on the floor. It falls apart the instant you want
|
||||
two things happening at once. Watch it break:
|
||||
|
||||
```bash
|
||||
# Agent A is mid-change on a feature branch — uncommitted edits in cli.py
|
||||
git switch -c feature/clear
|
||||
# ...agent A edits cli.py, hasn't committed...
|
||||
|
||||
# You want Agent B to start a different job, so you try to switch:
|
||||
git switch -c feature/count
|
||||
# error: Your local changes to the following files would be overwritten by checkout:
|
||||
# cli.py
|
||||
# Please commit your changes or stash them before you switch branches.
|
||||
```
|
||||
|
||||
Git stops you — correctly. But now you're stuck choosing between bad options:
|
||||
|
||||
- **Commit half-finished work** just to get it out of the way (pollutes history, and Agent A's job
|
||||
isn't done).
|
||||
- **Stash it** (now Agent A's context lives in a stash you have to remember to pop, and Agent A — a
|
||||
long-running session that thinks its files are right there — is now editing files that silently
|
||||
changed under it).
|
||||
- **Run both agents on the same branch in the same folder** — and watch them overwrite each other's
|
||||
edits, because they're both writing the same `cli.py` with no idea the other exists.
|
||||
|
||||
The branch was never the problem. The single working directory is. You need two floors.
|
||||
|
||||
### What a worktree is
|
||||
|
||||
`git worktree` gives you exactly that: **additional working directories attached to the same
|
||||
repository, each with its own checked-out branch.** One repo, many checkouts.
|
||||
|
||||
```bash
|
||||
cd ~/workflow-course/tasks-app # your existing repo from Module 2
|
||||
git worktree add ../tasks-app-count -b feature/count
|
||||
```
|
||||
|
||||
That command creates a brand-new folder, `~/workflow-course/tasks-app-count`, containing a full
|
||||
checkout of your project on a new branch `feature/count`. Your original folder is untouched, still
|
||||
on its own branch. You now have two real directories you can `cd` into, edit, and run independently:
|
||||
|
||||
```
|
||||
~/workflow-course/
|
||||
tasks-app/ ← the "main" worktree, on (say) main
|
||||
tasks-app-count/ ← a "linked" worktree, on feature/count
|
||||
```
|
||||
|
||||
Both are backed by **one** repository. There is a single `.git` — a single object store, a single
|
||||
history, a single set of branches and tags. The linked worktree doesn't get its own copy of the
|
||||
history; it gets its own copy of the *files*, and a pointer back to the shared `.git`. (If you peek,
|
||||
the linked worktree has a tiny `.git` *file*, not a directory — it just points at the real one in
|
||||
the main worktree.)
|
||||
|
||||
This is the distinction that makes the whole thing click:
|
||||
|
||||
> **A clone copies the history. A worktree copies the working files and shares the history.**
|
||||
|
||||
A clone is a second repository — separate objects, separate `.git`, you sync between them with
|
||||
pull/push (Module 8). A worktree is the *same* repository wearing two outfits. A commit you make in
|
||||
one worktree is instantly an object in the shared store — no pushing, no pulling, it's just *there*,
|
||||
because there's only one store.
|
||||
|
||||
### The mental model: one history, many present moments
|
||||
|
||||
Think of the shared object store as the project's single, settled past — every commit, on every
|
||||
branch, in one place. Each worktree is a different *present moment* checked out of that past: this
|
||||
folder is "the project as of `feature/count`," that folder is "the project as of `main`." They all
|
||||
write to the same past (commits go to the shared store), but each lives in its own present (its own
|
||||
files on disk).
|
||||
|
||||
That's why worktrees are the natural payoff of branches. A branch is a *logical* "what if." A
|
||||
worktree makes that "what if" a *place you can stand* — a folder you can open, run, and point an
|
||||
agent at — while every other "what if" stays open in its own folder at the same time.
|
||||
|
||||
### The core commands
|
||||
|
||||
```bash
|
||||
git worktree add <path> -b <new-branch> # new folder + new branch, checked out there
|
||||
git worktree add <path> <existing-branch> # new folder, checks out an existing branch
|
||||
git worktree list # every worktree, its path, and its branch
|
||||
git worktree remove <path> # delete a worktree (must be clean, or use --force)
|
||||
git worktree prune # forget worktrees whose folders were deleted by hand
|
||||
```
|
||||
|
||||
`git worktree list` is your map:
|
||||
|
||||
```bash
|
||||
$ git worktree list
|
||||
/home/you/workflow-course/tasks-app a1b2c3d [main]
|
||||
/home/you/workflow-course/tasks-app-count d4e5f6a [feature/count]
|
||||
/home/you/workflow-course/tasks-app-clear 7g8h9i0 [feature/clear]
|
||||
```
|
||||
|
||||
Three folders, one repo, three branches checked out simultaneously. No stashing, no switching, no
|
||||
collisions.
|
||||
|
||||
### How this maps onto running multiple agents
|
||||
|
||||
Here's the payoff the module exists for. An AI agent isn't a quick command — it's a **long-running
|
||||
session that holds a working directory and usually a running process** (your app, your test runner,
|
||||
a watcher). Two such sessions in one folder is a guaranteed mess:
|
||||
|
||||
- They edit the same files; their changes interleave and clobber each other.
|
||||
- One commits or switches branches and the floor moves under the other.
|
||||
- Their app runs and test runs share state and step on each other's output.
|
||||
|
||||
Give each agent its own worktree and every one of those collisions disappears *by construction*:
|
||||
|
||||
- **Separate folders** → separate files. Agent A literally cannot touch Agent B's `cli.py`; it's a
|
||||
different file on disk.
|
||||
- **Separate branches** → separate history lines. Neither can move the other's branch.
|
||||
- **Shared object store** → when both finish, merging their work back together is trivial — it's all
|
||||
already in one repo. No syncing between copies.
|
||||
|
||||
So "run two agents at once" stops being a coordination nightmare and becomes "open two folders."
|
||||
That's the local foundation; **doing this at scale — many agents, split work, kept reviewable — is
|
||||
Module 26 (Orchestrating Multiple Agents).** Worktrees are the primitive that module is built on.
|
||||
Learn the primitive here on two; the orchestration comes later.
|
||||
|
||||
---
|
||||
|
||||
## The AI angle
|
||||
|
||||
A generic devops course would mention worktrees as a niche convenience for the human who hates
|
||||
stashing. For AI-assisted work they're closer to essential, for a reason specific to how agents
|
||||
behave:
|
||||
|
||||
- **An agent assumes its working directory is stable.** It reads files, reasons about them, and
|
||||
writes them back over a session that can run for many minutes. If a *second* agent (or you,
|
||||
switching branches) rewrites those files underneath it, the first agent is now operating on a
|
||||
reality that silently changed — the worst kind of bug, because nothing errors; the work just comes
|
||||
out wrong. A worktree pins each agent to a directory nobody else will touch.
|
||||
- **Parallelism is the whole point of cheap agents.** The model is fast and you can run several at
|
||||
once — a feature here, a bugfix there, a doc update in a third. The constraint was never the
|
||||
model; it was that they'd trip over one repo. Worktrees remove the constraint.
|
||||
- **Each worktree is its own durable memory (Module 2).** A fresh agent dropped into
|
||||
`tasks-app-count` reads `git status` / `git diff` / `git log` and gets *that branch's* ground
|
||||
truth — not a blur of three agents' half-finished work. Per-agent isolation makes per-agent
|
||||
"where were we?" actually answerable.
|
||||
- **It keeps parallel AI output reviewable.** Each agent's work lands as its own branch with its own
|
||||
clean history, instead of a tangle of interleaved edits on one branch that no human could ever
|
||||
review. That reviewability is what later lets agents run with less supervision (Unit 5).
|
||||
|
||||
You don't reach for worktrees because you read about them. You reach for them the first time you try
|
||||
to run two agents and watch them eat each other's homework.
|
||||
|
||||
---
|
||||
|
||||
## Hands-on lab
|
||||
|
||||
**Lab language:** shell (Git commands), plus two AI edit sessions on the `tasks-app`.
|
||||
|
||||
In this lab you'll run **two AI sessions at the same time** on the same project — one adding a
|
||||
`clear` command, one adding a `count` command — each in its own worktree, and watch them *not*
|
||||
collide. Then you'll merge both back and clean up.
|
||||
|
||||
**You'll need:**
|
||||
|
||||
- The `tasks-app` Git repo from Module 2 (initialized, with a few commits). If you skipped ahead,
|
||||
`git init` it and make one commit first.
|
||||
- Git 2.5 or newer (worktrees landed in 2.5; any modern Git is fine — `git --version` to check).
|
||||
- **Two** editor-integrated AI sessions you can run at once (Module 4) — two editor windows, or two
|
||||
terminal AI sessions. If you only have a browser chat, you can still do the lab; just treat each
|
||||
worktree folder as a separate copy-paste context.
|
||||
- The starter scripts and prompts in this module's `lab/` folder.
|
||||
|
||||
### Part A — Feel the collision (1 minute)
|
||||
|
||||
Before fixing it, reproduce the bottleneck from "Where branches alone run out." In your `tasks-app`:
|
||||
|
||||
```bash
|
||||
cd ~/workflow-course/tasks-app
|
||||
git switch -c feature/scratch
|
||||
# make a fake uncommitted edit so the working dir is dirty:
|
||||
echo "# scratch" >> cli.py
|
||||
git switch -c feature/other
|
||||
```
|
||||
|
||||
Git refuses — it won't move you to another branch with uncommitted changes in the way. *That* is the
|
||||
wall. Clean up before continuing:
|
||||
|
||||
```bash
|
||||
git restore cli.py
|
||||
git switch main
|
||||
git branch -D feature/scratch feature/other
|
||||
```
|
||||
|
||||
### Part B — Create two worktrees
|
||||
|
||||
From inside `tasks-app`, run the setup script (or run the commands by hand):
|
||||
|
||||
```bash
|
||||
bash modules/07-worktrees-running-agents-in-parallel/lab/setup-worktrees.sh
|
||||
```
|
||||
|
||||
It runs:
|
||||
|
||||
```bash
|
||||
git worktree add ../tasks-app-clear -b feature/clear
|
||||
git worktree add ../tasks-app-count -b feature/count
|
||||
git worktree list
|
||||
```
|
||||
|
||||
You now have three folders backed by one repo. Confirm:
|
||||
|
||||
```bash
|
||||
git worktree list # should show main + feature/clear + feature/count
|
||||
```
|
||||
|
||||
### Part C — Run two AI sessions in parallel
|
||||
|
||||
This is the part to actually *do simultaneously*, not one then the other.
|
||||
|
||||
1. Open `~/workflow-course/tasks-app-clear` in one editor/AI session. Give it the prompt in
|
||||
`lab/agent-a-prompt.md` — *add a `clear` command that removes all tasks.*
|
||||
2. Open `~/workflow-course/tasks-app-count` in a **second** editor/AI session. Give it the prompt in
|
||||
`lab/agent-b-prompt.md` — *add a `count` command that prints the number of pending tasks.*
|
||||
3. Let both work at the same time. While they run, prove the isolation from a third terminal:
|
||||
|
||||
```bash
|
||||
cd ~/workflow-course/tasks-app-clear && python cli.py clear # agent A's feature
|
||||
cd ~/workflow-course/tasks-app-count && python cli.py count # agent B's feature
|
||||
```
|
||||
|
||||
Each app runs its own command in its own folder. Note that each worktree has its **own**
|
||||
`tasks.json` (it's gitignored runtime state, not shared history) — so the two running apps don't
|
||||
even share data. Total isolation.
|
||||
|
||||
4. In each worktree, commit the agent's work on its own branch:
|
||||
|
||||
```bash
|
||||
cd ~/workflow-course/tasks-app-clear && git add . && git commit -m "Add clear command"
|
||||
cd ~/workflow-course/tasks-app-count && git add . && git commit -m "Add count command"
|
||||
```
|
||||
|
||||
Two agents, two commits, two branches — neither ever saw the other's files.
|
||||
|
||||
### Part D — Merge back and clean up
|
||||
|
||||
Bring both features home to `main` in your original worktree:
|
||||
|
||||
```bash
|
||||
cd ~/workflow-course/tasks-app
|
||||
git switch main
|
||||
git merge feature/clear
|
||||
git merge feature/count
|
||||
```
|
||||
|
||||
Both commits are already in the shared object store, so there's nothing to fetch — the merges are
|
||||
local and instant. The second merge **may** hit a small conflict in `cli.py` if both agents added
|
||||
their `elif` branch in the same spot. That's expected, and it's a *merge-time* event, not a
|
||||
parallel-work collision — resolve it with the exact skill from Module 6, then `python cli.py list`
|
||||
to confirm both commands work.
|
||||
|
||||
Now tear down the worktrees:
|
||||
|
||||
```bash
|
||||
bash modules/07-worktrees-running-agents-in-parallel/lab/cleanup-worktrees.sh
|
||||
git worktree list # only the main worktree remains
|
||||
```
|
||||
|
||||
The script runs `git worktree remove` on both folders and `git worktree prune` to clear any stale
|
||||
records. The branches are already merged into `main`, so the work is safe.
|
||||
|
||||
---
|
||||
|
||||
## Where it breaks
|
||||
|
||||
Worktrees are sharp tools. The honest caveats:
|
||||
|
||||
- **You cannot check out the same branch in two worktrees.** Git refuses
|
||||
(`fatal: 'main' is already checked out at ...`). This is a feature, not a bug — it's exactly what
|
||||
stops two agents from writing the same branch — but it surprises people. One branch, one worktree.
|
||||
- **Uncommitted work is *not* shared.** Only commits go to the shared store. The edits sitting
|
||||
modified-but-uncommitted in `tasks-app-count` exist *only* in that folder. If you
|
||||
`git worktree remove` a dirty worktree, Git refuses unless you pass `--force` — and `--force`
|
||||
throws that uncommitted work away for good. Commit before you remove.
|
||||
- **Cleanup is a two-part chore.** Deleting a worktree folder with `rm -rf` does *not* tell Git it's
|
||||
gone — you'll have a stale entry in `git worktree list` forever until you run `git worktree prune`.
|
||||
Prefer `git worktree remove <path>`, which does both. (The cleanup script does this for you.)
|
||||
- **One shared object store means one shared fate.** All worktrees depend on the main repo's `.git`.
|
||||
Delete or move the main worktree and every linked worktree breaks — they're pointing at a `.git`
|
||||
that isn't there anymore. Worktrees are *not* independent backups; they're one repository. (The
|
||||
backup story is still Module 8: get the history off this one machine.)
|
||||
- **Worktrees don't prevent merge conflicts — they defer them.** Two agents editing the same lines
|
||||
will still conflict *when you merge*. What worktrees buy you is that the conflict happens once, on
|
||||
your terms, in one calm step (Module 6) — instead of two live agents corrupting each other's files
|
||||
in real time. Isolation during work; resolution after.
|
||||
- **Each worktree is a full set of working files.** Cheaper than a clone (the history is shared), but
|
||||
not free — a worktree per agent means a working tree per agent on disk, plus whatever each agent's
|
||||
running process consumes. Fine for two; something to plan for when Module 26 takes this to many.
|
||||
- **Tooling that hardcodes the repo root can get confused.** Anything keyed to an absolute path, a
|
||||
per-checkout cache, or "the one working directory" may need per-worktree setup. The committed AI
|
||||
config from Module 5 travels with each worktree (it's a tracked file), which is exactly why
|
||||
committing it pays off here — every agent in every worktree inherits the same instructions.
|
||||
|
||||
---
|
||||
|
||||
## Check for understanding
|
||||
|
||||
**You're done when:**
|
||||
|
||||
- `git worktree list` showed three entries at once, and you ran a different command of the
|
||||
`tasks-app` from two different worktree folders.
|
||||
- You ran two AI sessions in parallel — each in its own worktree on its own branch — and confirmed
|
||||
neither touched the other's files (different folders, different `tasks.json`, different branch).
|
||||
- You merged both feature branches back into `main` (resolving a conflict if one appeared) and the
|
||||
app has both new commands.
|
||||
- You cleaned up so that `git worktree list` shows only the main worktree and the stray folders are
|
||||
gone — no stale entries left behind.
|
||||
- You can state, without looking, what a worktree shares with the repo (history, objects, branches,
|
||||
tags) and what it keeps to itself (working files, uncommitted changes, its one checked-out branch).
|
||||
|
||||
When "run two agents at once" feels like "open two folders" instead of "orchestrate a stash dance,"
|
||||
you've got it. This is the primitive Module 26 scales up — for now, two is plenty.
|
||||
Reference in New Issue
Block a user