Running-example consistency: paths, tasks.json, command collisions (#7,#10,#11) #57

Merged
claude merged 1 commits from fix/p1-running-example-consistency into main 2026-06-22 15:35:52 -04:00
14 changed files with 192 additions and 136 deletions
@@ -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
+4 -2
View File
@@ -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 46 on it:
> manufacture one deterministically, then practice steps 46 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