|
|
|
@@ -1,22 +1,22 @@
|
|
|
|
|
# Module 7 — Worktrees: Running Agents in Parallel
|
|
|
|
|
# Module 7: Worktrees for 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
|
|
|
|
|
> 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
|
|
|
|
|
- **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
|
|
|
|
|
- **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
|
|
|
|
|
- **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.
|
|
|
|
|
- **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.
|
|
|
|
@@ -35,7 +35,7 @@ By the end of this module you can:
|
|
|
|
|
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.
|
|
|
|
|
uncommitted changes, checked-out branch), and where that bites.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
@@ -44,7 +44,7 @@ By the end of this module you can:
|
|
|
|
|
### 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
|
|
|
|
|
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
|
|
|
|
@@ -74,7 +74,7 @@ git switch feature/wipe
|
|
|
|
|
# Please commit your changes or stash them before you switch branches.
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Git stops you — correctly. Switching to `feature/wipe` would overwrite Agent B's uncommitted edits
|
|
|
|
|
Git stops you, and correctly so. Switching to `feature/wipe` would overwrite Agent B's uncommitted edits
|
|
|
|
|
to `cli.py` with Agent A's committed version of those same lines, so Git refuses rather than silently
|
|
|
|
|
destroy the work. But now you're stuck choosing between bad options:
|
|
|
|
|
|
|
|
|
@@ -83,7 +83,7 @@ destroy the work. But now you're stuck choosing between bad options:
|
|
|
|
|
- **Stash it** (now Agent B's context lives in a stash you have to remember to pop, and Agent B, 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
|
|
|
|
|
- **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.
|
|
|
|
@@ -111,24 +111,24 @@ independently:
|
|
|
|
|
tasks-app-remaining/ ← a "linked" worktree, on feature/remaining
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Both are backed by **one** repository. There is a single `.git` — a single object store, a single
|
|
|
|
|
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 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
|
|
|
|
|
A clone is a second repository: separate objects, separate `.git`, you sync between them with
|
|
|
|
|
pull/push (Module 8). A worktree is one repository checked out in two places. 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
|
|
|
|
|
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/remaining`," 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
|
|
|
|
@@ -162,7 +162,7 @@ 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
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
@@ -175,7 +175,7 @@ Give each agent its own worktree and every one of those collisions disappears *b
|
|
|
|
|
- **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
|
|
|
|
|
- **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."
|
|
|
|
@@ -187,20 +187,20 @@ Learn the primitive here on two; the orchestration comes later.
|
|
|
|
|
|
|
|
|
|
## The AI angle
|
|
|
|
|
|
|
|
|
|
Worktrees look like a niche convenience — a way to dodge `git stash` when you switch branches. For
|
|
|
|
|
Worktrees look like a niche convenience: a way to dodge `git stash` when you switch branches. 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.
|
|
|
|
|
reality that silently changed. That's 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
|
|
|
|
|
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-remaining` 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
|
|
|
|
|
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
|
|
|
|
@@ -215,19 +215,19 @@ to run two agents and watch them overwrite each other's work.
|
|
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
`wipe` command, one adding a `remaining` command — each in its own worktree, and watch them *not*
|
|
|
|
|
In this lab you'll run **two AI sessions at the same time** on the same project (one adding a
|
|
|
|
|
`wipe` command, one adding a `remaining` command), each in its own worktree, and watch them *not*
|
|
|
|
|
collide. Then you'll merge both back and clean up. (We use two commands your carried-forward
|
|
|
|
|
`tasks-app` doesn't have yet, so neither agent re-adds something that already exists — the lesson is
|
|
|
|
|
`tasks-app` doesn't have yet, so neither agent re-adds something that already exists: the lesson is
|
|
|
|
|
the parallel isolation, not the commands.)
|
|
|
|
|
|
|
|
|
|
**You'll need:**
|
|
|
|
|
|
|
|
|
|
- The `tasks-app` Git repo from Module 2 (initialized, with a few commits). If you skipped ahead,
|
|
|
|
|
run `git init -b main` and make one commit first — the `-b main` matches Module 2, so the
|
|
|
|
|
run `git init -b main` and make one commit first; the `-b main` matches Module 2, so the
|
|
|
|
|
`git switch main` steps below resolve.
|
|
|
|
|
- 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
|
|
|
|
|
- Git 2.5 or newer (worktrees landed in 2.5; any modern Git is fine, run `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, at
|
|
|
|
@@ -237,7 +237,7 @@ the parallel isolation, not the commands.)
|
|
|
|
|
to run the `git worktree` commands, or hand it `setup-worktrees.sh` / `cleanup-worktrees.sh` to
|
|
|
|
|
run, and you verify the result. You don't type the git by hand.
|
|
|
|
|
|
|
|
|
|
### Part A — Feel the collision (1 minute)
|
|
|
|
|
### Part A: Feel the collision (1 minute)
|
|
|
|
|
|
|
|
|
|
Before fixing it, reproduce the bottleneck from "Where branches alone run out." The wall only appears
|
|
|
|
|
when both branches touch the **same line** of `cli.py` (one committed, one not), so we make each
|
|
|
|
@@ -252,7 +252,7 @@ git switch -c feature/wipe
|
|
|
|
|
sed 's/done <index>/done <index> | wipe/' cli.py > cli.tmp && mv cli.tmp cli.py
|
|
|
|
|
git commit -am "Add wipe command (demo)"
|
|
|
|
|
|
|
|
|
|
# Agent B's branch, off main: start adding `remaining` to the SAME line — leave it uncommitted.
|
|
|
|
|
# Agent B's branch, off main: start adding `remaining` to the SAME line; leave it uncommitted.
|
|
|
|
|
git switch main
|
|
|
|
|
git switch -c feature/remaining
|
|
|
|
|
sed 's/done <index>/done <index> | remaining/' cli.py > cli.tmp && mv cli.tmp cli.py
|
|
|
|
@@ -265,8 +265,8 @@ git switch feature/wipe
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
(The `sed` matches `done <index>`, which is still in your usage line no matter how many commands
|
|
|
|
|
you've added since Module 1, and inserts a new one right after it — so both branches edit the same
|
|
|
|
|
line.) Git refuses — moving the one working directory to `feature/wipe` would overwrite Agent B's
|
|
|
|
|
you've added since Module 1, and inserts a new one right after it, so both branches edit the same
|
|
|
|
|
line.) Git refuses: moving the one working directory to `feature/wipe` would overwrite Agent B's
|
|
|
|
|
uncommitted edit with `feature/wipe`'s committed version of that line. *That* is the wall: one
|
|
|
|
|
directory can't hold two agents' in-progress work at once. These two branches existed only to feel
|
|
|
|
|
the collision, so clean them up before continuing:
|
|
|
|
@@ -277,7 +277,7 @@ git switch main
|
|
|
|
|
git branch -D feature/wipe feature/remaining # throw away the demo branches
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Part B — Create two worktrees
|
|
|
|
|
### Part B: Create two worktrees
|
|
|
|
|
|
|
|
|
|
An agent that lives *inside* a worktree can't create its own worktree, so the **coordinating
|
|
|
|
|
session** (the AI you already have pointed at `tasks-app` from Module 4) sets them up. That's Claude
|
|
|
|
@@ -298,15 +298,15 @@ git worktree list # should show main + feature/wipe + feature/remaining
|
|
|
|
|
Three folders backed by one repo, and you didn't type a git command. You directed, the agent did the
|
|
|
|
|
git, you confirmed.
|
|
|
|
|
|
|
|
|
|
### Part C — Run two AI sessions in parallel
|
|
|
|
|
### Part C: Run two AI sessions in parallel
|
|
|
|
|
|
|
|
|
|
This is the part to actually *do simultaneously*, not one then the other.
|
|
|
|
|
|
|
|
|
|
1. Open `~/ai-workflow-course/tasks-app-wipe` in one editor/AI session. Give it the prompt in
|
|
|
|
|
`lab/agent-a-prompt.md` — *add a `wipe` command that removes all tasks.*
|
|
|
|
|
`lab/agent-a-prompt.md`: *add a `wipe` command that removes all tasks.*
|
|
|
|
|
2. Open `~/ai-workflow-course/tasks-app-remaining` in a **second** editor/AI session. Give it the prompt
|
|
|
|
|
in `lab/agent-b-prompt.md` — *add a `remaining` 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 — but
|
|
|
|
|
in `lab/agent-b-prompt.md`: *add a `remaining` 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, but
|
|
|
|
|
use commands that **already exist**. (`wipe` and `remaining` don't yet; the agents are still
|
|
|
|
|
writing them.) Give each worktree its own task and list it:
|
|
|
|
|
|
|
|
|
@@ -334,7 +334,7 @@ This is the part to actually *do simultaneously*, not one then the other.
|
|
|
|
|
|
|
|
|
|
Two agents, two commits, two branches, and neither ever saw the other's files.
|
|
|
|
|
|
|
|
|
|
5. *Now* the new commands exist — run each in its own worktree to watch it work:
|
|
|
|
|
5. *Now* the new commands exist: run each in its own worktree to watch it work:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cd ~/ai-workflow-course/tasks-app-wipe && python cli.py wipe # agent A's new command
|
|
|
|
@@ -344,7 +344,7 @@ This is the part to actually *do simultaneously*, not one then the other.
|
|
|
|
|
`remaining` counts a single pending task, the one you added to worktree B in step 3, because B's
|
|
|
|
|
`tasks.json` is the only state it can see.
|
|
|
|
|
|
|
|
|
|
### Part D — Merge back and clean up
|
|
|
|
|
### Part D: Merge back and clean up
|
|
|
|
|
|
|
|
|
|
Both feature branches need to come home to `main`. Back in the **coordinating session** (the one on
|
|
|
|
|
`tasks-app`), direct the merges:
|
|
|
|
@@ -390,30 +390,30 @@ git worktree list # only the main worktree remains
|
|
|
|
|
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.
|
|
|
|
|
(`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-remaining` exist *only* in that folder. If you
|
|
|
|
|
`git worktree remove` a dirty worktree, Git refuses unless you pass `--force` — and `--force`
|
|
|
|
|
`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`.
|
|
|
|
|
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`
|
|
|
|
|
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
|
|
|
|
|
- **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
|
|
|
|
|
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
|
|
|
|
|
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.
|
|
|
|
|
committing it pays off here: every agent in every worktree inherits the same instructions.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
@@ -422,15 +422,15 @@ Worktrees are sharp tools. The honest caveats:
|
|
|
|
|
**You're done when:**
|
|
|
|
|
|
|
|
|
|
- `git worktree list` showed three entries at once, and you ran the `tasks-app` from two different
|
|
|
|
|
worktree folders — adding a different task in each and watching each keep its own `tasks.json`.
|
|
|
|
|
worktree folders, adding a different task in each and watching each keep its own `tasks.json`.
|
|
|
|
|
- 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.
|
|
|
|
|
gone, with 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.
|
|
|
|
|
you've got it. This is the primitive Module 26 scales up; for now, two is plenty.
|
|
|
|
|