Running-example consistency: paths, tasks.json, command collisions (#7,#10,#11) (#57)
Co-authored-by: claude <claude@jpaul.io> Co-committed-by: claude <claude@jpaul.io>
This commit was merged in pull request #57.
This commit is contained in:
@@ -269,7 +269,11 @@ copy-paste loop back in Module 1, now done right.
|
||||
CLI. Use the "How to choose" criteria above; any tool that shows diffs and has an approval mode is
|
||||
fine.
|
||||
- Your model/provider credentials for that tool.
|
||||
- The verify script in this module's `lab/verify.sh`.
|
||||
- The verify script in this module's `lab/verify.sh`. **Convention for every lab script from here on:**
|
||||
the course's scripts live in the course repo under `modules/NN/lab/`, but your `tasks-app` is a
|
||||
separate folder (Module 1) — so when a step runs one, **copy the script into `tasks-app` first, then
|
||||
run it by name**. (Same copy-it-in move you used for the instructions file in Module 5; use the real
|
||||
path to wherever you unzipped the course in place of `/path/to/`.)
|
||||
|
||||
### Part A — Wire it up and confirm it can read
|
||||
|
||||
@@ -322,10 +326,11 @@ copy-paste loop back in Module 1, now done right.
|
||||
gone.
|
||||
|
||||
7. **Verify it runs.** Use the provided script, which exercises the new command end to end across
|
||||
both files:
|
||||
both files. Copy it into `tasks-app` first (see *You'll need*), then run it from there:
|
||||
|
||||
```bash
|
||||
bash lab/verify.sh
|
||||
cp /path/to/modules/04-getting-the-ai-out-of-the-browser/lab/verify.sh .
|
||||
bash verify.sh
|
||||
```
|
||||
|
||||
It should add tasks, delete one by index, and confirm the right task remains. If it fails, don't
|
||||
@@ -357,7 +362,7 @@ copy-paste loop back in Module 1, now done right.
|
||||
```bash
|
||||
git restore .
|
||||
git diff # empty — the AI's mess is gone, byte for byte
|
||||
bash lab/verify.sh # still passes — you're back at your good state
|
||||
bash verify.sh # still passes — you're back at your good state (you copied it in at step 7)
|
||||
```
|
||||
|
||||
That's the Module 2 safety net catching a Module 4 mistake. Internalize how cheap that was.
|
||||
@@ -413,7 +418,8 @@ Be honest about the limits of working this way:
|
||||
- An agentic editor or CLI tool is wired to your `tasks-app` repo and correctly answers "what does
|
||||
this project do and which files is it in?" from the actual files — no pasting.
|
||||
- You have a committed `delete` command that you watched the AI write across **both** `tasks.py` and
|
||||
`cli.py`, that you reviewed with `git diff` before committing, and that `bash lab/verify.sh` passes.
|
||||
`cli.py`, that you reviewed with `git diff` before committing, and that `bash verify.sh` passes
|
||||
(after copying `verify.sh` into `tasks-app`).
|
||||
- You have, on purpose, let the AI make a change and then erased it with `git restore .`, watching
|
||||
`git diff` go empty.
|
||||
- You can explain, in one sentence, why letting an AI edit your files directly is safe — and your
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
# and the other two remain. This is a behavior check on the multi-file change — it does not
|
||||
# care HOW the AI implemented it, only that `delete` works end to end.
|
||||
#
|
||||
# Run it from inside your tasks-app project directory:
|
||||
# bash lab/verify.sh
|
||||
# Copy this into your tasks-app project directory, then run it from there:
|
||||
# cp /path/to/modules/04-getting-the-ai-out-of-the-browser/lab/verify.sh .
|
||||
# bash verify.sh
|
||||
#
|
||||
# (It self-locates cli.py, so it also still works if you run it in place as `bash lab/verify.sh`.)
|
||||
# It saves and restores your real tasks.json, so your actual task list is left untouched.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -207,9 +207,11 @@ editor-integrated AI (Module 4) for the part where the AI obeys the file.
|
||||
### Part B — Watch the AI obey it
|
||||
|
||||
4. Start a **fresh** AI session in your editor (so it picks up the file cleanly) and give it a task
|
||||
that the instructions constrain. For example:
|
||||
that the instructions constrain. Pick a command your app doesn't have yet (so this is a real
|
||||
feature, not a re-add) — for example:
|
||||
|
||||
> *"Add a `clear` command that removes all tasks. Then confirm it works."*
|
||||
> *"Add a `search <term>` command that lists only the tasks whose title contains `term`. Then
|
||||
> confirm it works."*
|
||||
|
||||
5. Watch for the file taking effect. A correctly-configured agent should, without you saying any of
|
||||
it this time:
|
||||
|
||||
@@ -175,9 +175,9 @@ decide:
|
||||
|
||||
```python
|
||||
<<<<<<< HEAD
|
||||
print("usage: python cli.py [add <title> | list | done <index> | count]")
|
||||
print("usage: python cli.py [add <title> | list | done <index> | stats]")
|
||||
=======
|
||||
print("usage: python cli.py [add <title> | list | done <index> | clear]")
|
||||
print("usage: python cli.py [add <title> | list | done <index> | purge]")
|
||||
>>>>>>> experiment
|
||||
```
|
||||
|
||||
@@ -190,7 +190,7 @@ Read it like this:
|
||||
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* `count` and `clear`).
|
||||
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:
|
||||
|
||||
```bash
|
||||
@@ -324,44 +324,52 @@ attempts become free to reject.
|
||||
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.
|
||||
|
||||
1. Make sure you're on a clean `main`. Create the first branch and have the AI add a `count` command:
|
||||
> **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
|
||||
> that's fine. This lab works *regardless* of what's on that line, because the collision is just "two
|
||||
> branches each appended a different new command to the same usage line." To make it reproduce even on
|
||||
> a carried-forward app, we deliberately add two commands you **haven't** built yet — `stats` and
|
||||
> `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:
|
||||
|
||||
```bash
|
||||
git switch main
|
||||
git switch -c feature/count
|
||||
git switch -c feature/stats
|
||||
```
|
||||
|
||||
Ask the AI: *"Add a `count` command to `cli.py` that prints how many tasks are pending, and update
|
||||
the usage string to include it."* Then:
|
||||
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:
|
||||
|
||||
```bash
|
||||
git diff # confirm it edited the usage line + added the command
|
||||
git add . && git commit -m "Add count command"
|
||||
git add . && git commit -m "Add stats command"
|
||||
```
|
||||
|
||||
2. Switch back to `main` and create a *different* branch that touches **the same usage line**:
|
||||
|
||||
```bash
|
||||
git switch main
|
||||
git switch -c feature/clear
|
||||
git switch -c feature/purge
|
||||
```
|
||||
|
||||
Ask the AI: *"Add a `clear` command to `cli.py` that deletes all tasks, and update the usage
|
||||
string to include it."* Then:
|
||||
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:
|
||||
|
||||
```bash
|
||||
git diff # it also edited the usage line — this is the collision to come
|
||||
git add . && git commit -m "Add clear command"
|
||||
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/count` into `feature/clear` (you're on
|
||||
`feature/clear`):
|
||||
3. Merge them and watch it conflict. Merge `feature/stats` into `feature/purge` (you're on
|
||||
`feature/purge`):
|
||||
|
||||
```bash
|
||||
git merge feature/count
|
||||
git merge feature/stats
|
||||
```
|
||||
|
||||
Git stops with a conflict and tells you which file is unmerged. Confirm:
|
||||
@@ -370,28 +378,30 @@ Now the skill everyone fears and nobody should. You'll engineer a guaranteed con
|
||||
git status # cli.py listed under "Unmerged paths"
|
||||
```
|
||||
|
||||
4. Open `cli.py` and find the conflict markers around the usage line:
|
||||
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
|
||||
<<<<<<< HEAD
|
||||
print("usage: python cli.py [add <title> | list | done <index> | clear]")
|
||||
print("usage: python cli.py [add <title> | list | done <index> | purge]")
|
||||
=======
|
||||
print("usage: python cli.py [add <title> | list | done <index> | count]")
|
||||
>>>>>>> feature/count
|
||||
print("usage: python cli.py [add <title> | list | done <index> | stats]")
|
||||
>>>>>>> feature/stats
|
||||
```
|
||||
|
||||
(The command bodies for `count` and `clear` touch different lines, so Git merged *those* cleanly
|
||||
(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:
|
||||
|
||||
> *"`cli.py` has a merge conflict on the usage line. I want the final version to list BOTH the
|
||||
> `count` and `clear` commands. Resolve the conflict and remove the markers."*
|
||||
> `stats` and `purge` commands. Resolve the conflict and remove the markers."*
|
||||
|
||||
It should produce a single, marker-free line listing both commands, e.g.:
|
||||
|
||||
```python
|
||||
print("usage: python cli.py [add <title> | list | done <index> | count | clear]")
|
||||
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
|
||||
@@ -401,8 +411,8 @@ Now the skill everyone fears and nobody should. You'll engineer a guaranteed con
|
||||
```bash
|
||||
git diff # check ONLY what you intended changed; no markers remain
|
||||
python cli.py # run with no args — see the merged usage string
|
||||
python cli.py count # both commands actually work
|
||||
python cli.py clear
|
||||
python cli.py stats # both commands actually work
|
||||
python cli.py purge
|
||||
```
|
||||
|
||||
6. Tell Git the conflict is settled and complete the merge:
|
||||
@@ -419,10 +429,13 @@ Now the skill everyone fears and nobody should. You'll engineer a guaranteed con
|
||||
|
||||
> **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
|
||||
> manufacture one deterministically, then practice steps 4–6 on it:
|
||||
> manufacture one deterministically, then practice steps 4–6 on it. Copy it into your `tasks-app`
|
||||
> first (the course's lab scripts live in the course repo, not in `tasks-app` — see Module 4's
|
||||
> *You'll need*), then run it from inside the repo:
|
||||
>
|
||||
> ```bash
|
||||
> bash modules/06-branches-sandboxes-for-experiments/lab/make-conflict.sh
|
||||
> cp /path/to/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
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
# read the <<<<<<< / ======= / >>>>>>> markers, edit to the version you want, remove the markers,
|
||||
# then `git add` + `git commit`.
|
||||
#
|
||||
# Run it from anywhere inside your tasks-app repo:
|
||||
# bash modules/06-branches-sandboxes-for-experiments/lab/make-conflict.sh
|
||||
# Copy it into your tasks-app repo, then run it from inside the repo:
|
||||
# cp /path/to/modules/06-branches-sandboxes-for-experiments/lab/make-conflict.sh .
|
||||
# bash make-conflict.sh
|
||||
#
|
||||
# It is non-destructive to your real work: it only touches README.md on two throwaway practice
|
||||
# branches and refuses to run if your working tree is dirty.
|
||||
|
||||
@@ -56,30 +56,30 @@ That's fine when *you* are the only one standing on the floor. It falls apart th
|
||||
two things happening at once. Watch it break:
|
||||
|
||||
```bash
|
||||
# Agent A added a `clear` command and committed it on its own branch:
|
||||
git switch -c feature/clear
|
||||
# ...agent A edits the usage line in cli.py to add `clear`...
|
||||
git commit -am "Add clear command"
|
||||
# Agent A added a `wipe` command and committed it on its own branch:
|
||||
git switch -c feature/wipe
|
||||
# ...agent A edits the usage line in cli.py to add `wipe`...
|
||||
git commit -am "Add wipe command"
|
||||
|
||||
# You start Agent B on a fresh branch off main; it begins editing the SAME
|
||||
# usage line to add `count`, and hasn't committed:
|
||||
# usage line to add `remaining`, and hasn't committed:
|
||||
git switch main
|
||||
git switch -c feature/count
|
||||
git switch -c feature/remaining
|
||||
# ...agent B edits cli.py, hasn't committed...
|
||||
|
||||
# You try to hop the working directory back to Agent A's branch to check on it:
|
||||
git switch feature/clear
|
||||
git switch feature/wipe
|
||||
# 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. Switching to `feature/clear` would overwrite Agent B's uncommitted edits
|
||||
Git stops you — correctly. 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:
|
||||
|
||||
- **Commit half-finished work** just to get it out of the way (pollutes history, and Agent B's
|
||||
`count` command isn't done).
|
||||
`remaining` command isn't done).
|
||||
- **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).
|
||||
@@ -95,17 +95,18 @@ 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
|
||||
git worktree add ../tasks-app-remaining -b feature/remaining
|
||||
```
|
||||
|
||||
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:
|
||||
That command creates a brand-new folder, `~/workflow-course/tasks-app-remaining`, containing a full
|
||||
checkout of your project on a new branch `feature/remaining`. 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
|
||||
tasks-app/ ← the "main" worktree, on (say) main
|
||||
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
|
||||
@@ -127,7 +128,7 @@ because there's only one store.
|
||||
|
||||
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
|
||||
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
|
||||
files on disk).
|
||||
|
||||
@@ -149,9 +150,9 @@ git worktree prune # forget worktrees whose folders were
|
||||
|
||||
```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]
|
||||
/home/you/workflow-course/tasks-app a1b2c3d [main]
|
||||
/home/you/workflow-course/tasks-app-remaining d4e5f6a [feature/remaining]
|
||||
/home/you/workflow-course/tasks-app-wipe 7g8h9i0 [feature/wipe]
|
||||
```
|
||||
|
||||
Three folders, one repo, three branches checked out simultaneously. No stashing, no switching, no
|
||||
@@ -197,7 +198,7 @@ behave:
|
||||
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
|
||||
`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
|
||||
"where were we?" actually answerable.
|
||||
- **It keeps parallel AI output reviewable.** Each agent's work lands as its own branch with its own
|
||||
@@ -214,8 +215,10 @@ to run two agents and watch them eat each other's homework.
|
||||
**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.
|
||||
`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
|
||||
the parallel isolation, not the commands.)
|
||||
|
||||
**You'll need:**
|
||||
|
||||
@@ -226,7 +229,10 @@ collide. Then you'll merge both back and clean up.
|
||||
- **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.
|
||||
- The starter scripts and prompts in this module's `lab/` folder. As established in Module 4, the
|
||||
course's lab scripts live in the course repo under `modules/NN/lab/`, while `tasks-app` is a
|
||||
separate folder — so **copy the scripts into `tasks-app` and run them by name** (`bash
|
||||
setup-worktrees.sh`), using your real course path in place of `/path/to/`.
|
||||
|
||||
### Part A — Feel the collision (1 minute)
|
||||
|
||||
@@ -238,71 +244,75 @@ the edit an agent would make.) In your `tasks-app`:
|
||||
```bash
|
||||
cd ~/workflow-course/tasks-app
|
||||
|
||||
# Agent A's branch: add `clear` to the usage line and commit it.
|
||||
git switch -c feature/clear
|
||||
sed 's/done <index>/done <index> | clear/' cli.py > cli.tmp && mv cli.tmp cli.py
|
||||
git commit -am "Add clear command (demo)"
|
||||
# Agent A's branch: add `wipe` to the usage line and commit it.
|
||||
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 `count` 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/count
|
||||
sed 's/done <index>/done <index> | count/' cli.py > cli.tmp && mv cli.tmp cli.py
|
||||
git switch -c feature/remaining
|
||||
sed 's/done <index>/done <index> | remaining/' cli.py > cli.tmp && mv cli.tmp cli.py
|
||||
|
||||
# Try to hop the working directory back to Agent A's branch:
|
||||
git switch feature/clear
|
||||
git switch feature/wipe
|
||||
# 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 refuses — moving the one working directory to `feature/clear` would overwrite Agent B's
|
||||
uncommitted edit with `feature/clear`'s committed version of that line. *That* is the wall: one
|
||||
(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
|
||||
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:
|
||||
|
||||
```bash
|
||||
git restore cli.py # drop Agent B's uncommitted edit
|
||||
git restore cli.py # drop Agent B's uncommitted edit
|
||||
git switch main
|
||||
git branch -D feature/clear feature/count # throw away the demo branches
|
||||
git branch -D feature/wipe feature/remaining # throw away the demo branches
|
||||
```
|
||||
|
||||
### Part B — Create two worktrees
|
||||
|
||||
From inside `tasks-app`, run the setup script (or run the commands by hand):
|
||||
Copy the setup script into `tasks-app` (see *You'll need*), then run it from inside the repo (or run
|
||||
the commands by hand):
|
||||
|
||||
```bash
|
||||
bash modules/07-worktrees-running-agents-in-parallel/lab/setup-worktrees.sh
|
||||
cp /path/to/modules/07-worktrees-running-agents-in-parallel/lab/setup-worktrees.sh .
|
||||
bash 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 add ../tasks-app-wipe -b feature/wipe
|
||||
git worktree add ../tasks-app-remaining -b feature/remaining
|
||||
git worktree list
|
||||
```
|
||||
|
||||
You now have three folders backed by one repo. Confirm:
|
||||
|
||||
```bash
|
||||
git worktree list # should show main + feature/clear + feature/count
|
||||
git worktree list # should show main + feature/wipe + feature/remaining
|
||||
```
|
||||
|
||||
### 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.*
|
||||
1. Open `~/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.*
|
||||
2. Open `~/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
|
||||
use commands that **already exist**. (`clear` and `count` don't yet; the agents are still writing
|
||||
them.) Give each worktree its own task and list it:
|
||||
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:
|
||||
|
||||
```bash
|
||||
cd ~/workflow-course/tasks-app-clear && python cli.py add "from worktree A" && python cli.py list
|
||||
cd ~/workflow-course/tasks-app-count && python cli.py add "from worktree B" && python cli.py list
|
||||
cd ~/workflow-course/tasks-app-wipe && python cli.py add "from worktree A" && python cli.py list
|
||||
cd ~/workflow-course/tasks-app-remaining && python cli.py add "from worktree B" && python cli.py list
|
||||
```
|
||||
|
||||
Each `list` shows only its own task — worktree A never sees "from worktree B" and vice versa. Each
|
||||
@@ -313,8 +323,8 @@ This is the part to actually *do simultaneously*, not one then the other.
|
||||
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"
|
||||
cd ~/workflow-course/tasks-app-wipe && git add . && git commit -m "Add wipe command"
|
||||
cd ~/workflow-course/tasks-app-remaining && git add . && git commit -m "Add remaining command"
|
||||
```
|
||||
|
||||
Two agents, two commits, two branches — neither ever saw the other's files.
|
||||
@@ -322,12 +332,12 @@ This is the part to actually *do simultaneously*, not one then the other.
|
||||
5. *Now* the new commands exist — run each in its own worktree to watch it work:
|
||||
|
||||
```bash
|
||||
cd ~/workflow-course/tasks-app-clear && python cli.py clear # agent A's new command
|
||||
cd ~/workflow-course/tasks-app-count && python cli.py count # agent B's new command
|
||||
cd ~/workflow-course/tasks-app-wipe && python cli.py wipe # agent A's new command
|
||||
cd ~/workflow-course/tasks-app-remaining && python cli.py remaining # agent B's new command
|
||||
```
|
||||
|
||||
`count` reports only worktree B's task — the one you added in step 3 — because B's `tasks.json` is
|
||||
the only state it can see. The isolation, one last time.
|
||||
`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. The isolation, one last time.
|
||||
|
||||
### Part D — Merge back and clean up
|
||||
|
||||
@@ -336,8 +346,8 @@ 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
|
||||
git merge feature/wipe
|
||||
git merge feature/remaining
|
||||
```
|
||||
|
||||
Both commits are already in the shared object store, so there's nothing to fetch — the merges are
|
||||
@@ -346,10 +356,12 @@ their `elif` branch in the same spot. That's expected, and it's a *merge-time* e
|
||||
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:
|
||||
Now tear down the worktrees (copy the cleanup script into `tasks-app` the same way, then run it from
|
||||
inside the repo):
|
||||
|
||||
```bash
|
||||
bash modules/07-worktrees-running-agents-in-parallel/lab/cleanup-worktrees.sh
|
||||
cp /path/to/modules/07-worktrees-running-agents-in-parallel/lab/cleanup-worktrees.sh .
|
||||
bash cleanup-worktrees.sh
|
||||
git worktree list # only the main worktree remains
|
||||
```
|
||||
|
||||
@@ -366,7 +378,7 @@ Worktrees are sharp tools. The honest caveats:
|
||||
(`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
|
||||
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`
|
||||
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
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# Agent A prompt — the `clear` command
|
||||
# Agent A prompt — the `wipe` command
|
||||
|
||||
Paste this into the AI session you've pointed at the `tasks-app-clear` worktree folder.
|
||||
Paste this into the AI session you've pointed at the `tasks-app-wipe` worktree folder.
|
||||
|
||||
---
|
||||
|
||||
Add a `clear` command to this task app that removes **all** tasks.
|
||||
Add a `wipe` command to this task app that removes **all** tasks.
|
||||
|
||||
- Put the deletion logic on `TaskList` in `tasks.py` (a `clear()` method that empties the list),
|
||||
and wire a `clear` command into the dispatch in `cli.py` that calls it and saves.
|
||||
- Running `python cli.py clear` should empty the list and print a short confirmation like
|
||||
`cleared all tasks`.
|
||||
- After `clear`, `python cli.py list` should print `(no tasks yet)`.
|
||||
- Put the deletion logic on `TaskList` in `tasks.py` (a `wipe()` method that empties the list),
|
||||
and wire a `wipe` command into the dispatch in `cli.py` that calls it and saves.
|
||||
- Running `python cli.py wipe` should empty the list and print a short confirmation like
|
||||
`wiped all tasks`.
|
||||
- After `wipe`, `python cli.py list` should print `(no tasks yet)`.
|
||||
|
||||
Make the change, then stop — I'll review the diff and commit it myself.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Agent B prompt — the `count` command
|
||||
# Agent B prompt — the `remaining` command
|
||||
|
||||
Paste this into the AI session you've pointed at the `tasks-app-count` worktree folder.
|
||||
Paste this into the AI session you've pointed at the `tasks-app-remaining` worktree folder.
|
||||
|
||||
---
|
||||
|
||||
Add a `count` command to this task app that prints how many tasks are still pending.
|
||||
Add a `remaining` command to this task app that prints how many tasks are still pending.
|
||||
|
||||
- Reuse the existing `pending()` method on `TaskList` in `tasks.py`; don't reimplement it.
|
||||
- Wire a `count` command into the dispatch in `cli.py`.
|
||||
- Running `python cli.py count` should print something like `2 pending` (the number of tasks not
|
||||
- Wire a `remaining` command into the dispatch in `cli.py`.
|
||||
- Running `python cli.py remaining` should print something like `2 pending` (the number of tasks not
|
||||
marked done).
|
||||
|
||||
Make the change, then stop — I'll review the diff and commit it myself.
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Module 7 lab — tear down the two worktrees created by setup-worktrees.sh.
|
||||
# Run from INSIDE your tasks-app repo:
|
||||
# Copy this into your tasks-app repo, then run it from inside:
|
||||
#
|
||||
# bash modules/07-worktrees-running-agents-in-parallel/lab/cleanup-worktrees.sh
|
||||
# cp /path/to/modules/07-worktrees-running-agents-in-parallel/lab/cleanup-worktrees.sh .
|
||||
# bash cleanup-worktrees.sh
|
||||
#
|
||||
# `git worktree remove` deletes the folder AND clears Git's record of it; `prune` mops up any
|
||||
# worktrees whose folders were deleted by hand (which leaves a stale record otherwise).
|
||||
#
|
||||
# NOTE: --force discards UNCOMMITTED work in a worktree. Commit (or merge) before cleaning up.
|
||||
# This script assumes you already merged feature/clear and feature/count back into main.
|
||||
# This script assumes you already merged feature/wipe and feature/remaining back into main.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
PARENT="$(cd "$ROOT/.." && pwd)"
|
||||
|
||||
git worktree remove "$PARENT/tasks-app-clear" --force 2>/dev/null || true
|
||||
git worktree remove "$PARENT/tasks-app-count" --force 2>/dev/null || true
|
||||
git worktree remove "$PARENT/tasks-app-wipe" --force 2>/dev/null || true
|
||||
git worktree remove "$PARENT/tasks-app-remaining" --force 2>/dev/null || true
|
||||
git worktree prune
|
||||
|
||||
echo
|
||||
@@ -26,4 +27,4 @@ git worktree list
|
||||
|
||||
echo
|
||||
echo "If you merged both branches you can also delete them:"
|
||||
echo " git branch -d feature/clear feature/count"
|
||||
echo " git branch -d feature/wipe feature/remaining"
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Module 7 lab — create two linked worktrees off the tasks-app repo, each on its own branch.
|
||||
# Run this from INSIDE your tasks-app repo (the one you git-init'd in Module 2):
|
||||
# Copy this into your tasks-app repo (the one you git-init'd in Module 2), then run it from inside:
|
||||
#
|
||||
# bash modules/07-worktrees-running-agents-in-parallel/lab/setup-worktrees.sh
|
||||
# cp /path/to/modules/07-worktrees-running-agents-in-parallel/lab/setup-worktrees.sh .
|
||||
# bash setup-worktrees.sh
|
||||
#
|
||||
# It places the new worktree folders next to the repo, so you end up with:
|
||||
#
|
||||
# <parent>/tasks-app (your existing repo, on its current branch)
|
||||
# <parent>/tasks-app-clear (new worktree on branch feature/clear)
|
||||
# <parent>/tasks-app-count (new worktree on branch feature/count)
|
||||
# <parent>/tasks-app (your existing repo, on its current branch)
|
||||
# <parent>/tasks-app-wipe (new worktree on branch feature/wipe)
|
||||
# <parent>/tasks-app-remaining (new worktree on branch feature/remaining)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
@@ -17,8 +18,8 @@ set -euo pipefail
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
PARENT="$(cd "$ROOT/.." && pwd)"
|
||||
|
||||
git worktree add "$PARENT/tasks-app-clear" -b feature/clear
|
||||
git worktree add "$PARENT/tasks-app-count" -b feature/count
|
||||
git worktree add "$PARENT/tasks-app-wipe" -b feature/wipe
|
||||
git worktree add "$PARENT/tasks-app-remaining" -b feature/remaining
|
||||
|
||||
echo
|
||||
echo "Worktrees created. One repo, three checked-out branches:"
|
||||
|
||||
@@ -206,6 +206,7 @@ real change, then review a diff the "AI" produced and catch the trap planted in
|
||||
```bash
|
||||
mkdir -p ~/workflow-course/review-lab && cd ~/workflow-course/review-lab
|
||||
cp /path/to/modules/10-reviewing-code-you-didnt-write/lab/tasks-app/*.py .
|
||||
printf 'tasks.json\n__pycache__/\n' > .gitignore # keep generated runtime state out of your review diffs (Module 2)
|
||||
git init -qb main && git add . && git commit -qm "base: tasks-app" # -b main so the git switch main / git diff main.. steps below resolve
|
||||
|
||||
python cli.py add "write the review module"
|
||||
|
||||
@@ -330,15 +330,21 @@ That's the entire client/server loop, end to end, with zero code you wrote. Now
|
||||
> *"Add a task: review the Module 20 lab."*
|
||||
|
||||
It should call `add_task("review the Module 20 lab")`. Then **verify the effect outside the AI**,
|
||||
which is the whole point — the change is real:
|
||||
which is the whole point — the change is real. Verify it the way you'd verify any runtime effect:
|
||||
by reading the *state*, not the repo:
|
||||
|
||||
```bash
|
||||
python cli.py list # the new task is there, because the server wrote the same tasks.json
|
||||
git diff # the change shows up in your repo, exactly like any other edit (Module 2)
|
||||
cat tasks.json # the raw state the server changed, end to end
|
||||
```
|
||||
|
||||
The AI just changed real state in a real system through a tool call. No copy-paste, no script you
|
||||
ran by hand, no pasting `tasks.json` into a chat. That's "hands."
|
||||
The AI just changed real state in a real system through a tool call. Notice what you did *not*
|
||||
reach for: `git diff`. `tasks.json` is deliberately gitignored (Module 2's `.gitignore` treats it
|
||||
as generated runtime state, not source), so `git diff` stays empty here — and that's correct, not a
|
||||
bug. The proof the task list changed is the live state (`python cli.py list` / `cat tasks.json`),
|
||||
not version control; runtime data the app owns is exactly the kind of thing you keep *out* of
|
||||
history. No copy-paste, no script you ran by hand, no pasting `tasks.json` into a chat. That's
|
||||
"hands."
|
||||
|
||||
7. (Optional, to feel the discovery point.) Edit the docstring on `add_task` to be vague — change it
|
||||
to just `"""Adds something."""` — reload, and try the same request. Notice the AI gets *less*
|
||||
@@ -392,8 +398,9 @@ The honest caveats — and one of them is large enough that it gets its own modu
|
||||
- You built `tasks_mcp_server.py`, wired it into your tool, and saw the `tasks` server report as
|
||||
connected with `list_tasks` and `add_task` available.
|
||||
- You asked the AI a question and it answered by **calling a tool** against the live system, and you
|
||||
asked it to add a task and then **verified the change outside the AI** with `python cli.py list`
|
||||
and `git diff`.
|
||||
asked it to add a task and then **verified the change outside the AI** by reading the runtime state
|
||||
(`python cli.py list` / `cat tasks.json`) — not `git diff`, because `tasks.json` is deliberately
|
||||
gitignored (Module 2).
|
||||
- You can explain the client/server model in one breath — *servers expose tools/resources/prompts;
|
||||
the client (your agentic tool) discovers and calls them on the AI's behalf* — and why "it's a
|
||||
protocol, not a vendor feature" means your server survives a model swap.
|
||||
|
||||
@@ -294,10 +294,10 @@ normal question) and the attacker (you plant content the agent reads).
|
||||
# the tool it is NOT exposed (a write) — in a least-privilege setup this path is simply absent
|
||||
```
|
||||
|
||||
Then clean up the planted task so your repo is honest again (Module 2):
|
||||
Then clean up the planted state so your repo is honest again (Module 2):
|
||||
|
||||
```bash
|
||||
git restore tasks.json # or: python cli.py and delete it, then commit a clean state
|
||||
rm tasks.json # tasks.json is gitignored runtime state — nothing tracked to restore, so just delete it; the app recreates it empty on the next run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -273,7 +273,10 @@ thing you're waiting on.
|
||||
three. Browser-only still works; treat each worktree as a separate copy-paste context, but you'll
|
||||
feel the coordination cost more sharply (which is fine — that's the lesson).
|
||||
- The starter files in this module's `lab/` folder: `orchestration-plan.md`, `fan-out.sh`,
|
||||
`status.sh`, `cleanup.sh`, and three prompts under `lab/agent-prompts/`.
|
||||
`status.sh`, `cleanup.sh`, and three prompts under `lab/agent-prompts/`. As established back in
|
||||
Module 4, the course's lab scripts live in the course repo while `tasks-app` is a separate folder —
|
||||
so **copy the scripts into `tasks-app` and run them by name** (`bash fan-out.sh`), using your real
|
||||
course path in place of `/path/to/`.
|
||||
|
||||
### Part A — Plan the split before you launch anything (this is the lab)
|
||||
|
||||
@@ -294,10 +297,11 @@ thing you're waiting on.
|
||||
|
||||
### Part B — Fan out
|
||||
|
||||
3. From inside `tasks-app`, create a worktree per issue:
|
||||
3. From inside `tasks-app`, copy this module's lab scripts in and create a worktree per issue:
|
||||
|
||||
```bash
|
||||
bash modules/26-orchestrating-multiple-agents/lab/fan-out.sh
|
||||
cp /path/to/modules/26-orchestrating-multiple-agents/lab/*.sh . # fan-out.sh, status.sh, cleanup.sh
|
||||
bash fan-out.sh
|
||||
```
|
||||
|
||||
It runs, in effect:
|
||||
@@ -318,10 +322,11 @@ thing you're waiting on.
|
||||
- `tasks-app-43-docs` ← `lab/agent-prompts/agent-43-docs.md`
|
||||
- `tasks-app-44-clear` ← `lab/agent-prompts/agent-44-clear.md`
|
||||
|
||||
While they run, watch the fleet from a fourth terminal:
|
||||
While they run, watch the fleet from a fourth terminal (run from inside `tasks-app`, where you
|
||||
copied the scripts in step 3):
|
||||
|
||||
```bash
|
||||
bash modules/26-orchestrating-multiple-agents/lab/status.sh
|
||||
bash status.sh
|
||||
```
|
||||
|
||||
It prints each worktree, its branch, and how many commits/changes are in flight — your fleet
|
||||
@@ -369,7 +374,12 @@ thing you're waiting on.
|
||||
git add cli.py && git commit
|
||||
```
|
||||
|
||||
9. Close the issues (Module 11 closes them automatically if the PRs referenced them).
|
||||
9. Close the issues (Module 11 closes them automatically if the PRs referenced them). Then tear the
|
||||
fleet down (from inside `tasks-app`):
|
||||
|
||||
```bash
|
||||
bash cleanup.sh
|
||||
```
|
||||
|
||||
### Part D — Score the orchestration honestly
|
||||
|
||||
|
||||
Reference in New Issue
Block a user