docs(wiki): sync from modules/ @ 95e59119

2026-06-24 00:25:12 +00:00
parent 7a4e1f6491
commit a9e77b5675
22 changed files with 129 additions and 127 deletions
+14 -12
@@ -151,11 +151,13 @@ purpose** so you recognize it later.
- Python 3.10 or newer (`python --version` or `python3 --version` to check). - Python 3.10 or newer (`python --version` or `python3 --version` to check).
- Your usual AI chat assistant, open in a browser tab. - Your usual AI chat assistant, open in a browser tab.
> **One command name, the whole course through:** whichever of `python` / `python3` just printed a > **One command name, the whole course through:** the labs are written with `python3`, the command
> 3.10+ version is the command to use in *every* lab from here on. The labs are written with > name current macOS and default Debian/Ubuntu actually ship (they install Python only as `python3`,
> `python`; if that's "command not found" on your machine (common on current macOS and default > with no bare `python` on PATH). Run `python3 --version`; if it prints a 3.10+ version, use `python3`
> Debian/Ubuntu, where Python is installed only as `python3`), read it as `python3` (and `pip3` > in *every* lab from here on. If `python3` is "command not found" but `python --version` shows a
> wherever a lab uses `pip`). This note holds course-wide; we won't repeat it. > 3.10+ version (older or some Windows setups), read every `python3` in the labs as `python` instead.
> Where a lab runs `pip`, use whichever pairs with your Python (`pip3` commonly goes with `python3`).
> This note holds course-wide; we won't repeat it.
### Get the course materials ### Get the course materials
@@ -196,8 +198,8 @@ You now have every module's files locally, including this one's under
3. Run it in your terminal to confirm it works: 3. Run it in your terminal to confirm it works:
```bash ```bash
python cli.py add "finish module 1" python3 cli.py add "finish module 1"
python cli.py list python3 cli.py list
``` ```
You should see your task listed. **This is your "real local project, an editor, and a terminal."** You should see your task listed. **This is your "real local project, an editor, and a terminal."**
@@ -208,14 +210,14 @@ You now have every module's files locally, including this one's under
Now reproduce each failure deliberately. Keep the AI strictly in the **browser chat**; no Now reproduce each failure deliberately. Keep the AI strictly in the **browser chat**; no
editor-integrated tools yet (those arrive in Module 4). This is the "before" picture on purpose. editor-integrated tools yet (those arrive in Module 4). This is the "before" picture on purpose.
1. **Seam 1 (multiple files).** First mark a task done so there's something to hide. Run `python 1. **Seam 1 (multiple files).** First mark a task done so there's something to hide. Run `python3
cli.py done 0`, then `python cli.py list` shows it as `[x]`. Now paste *only* `cli.py` into your cli.py done 0`, then `python3 cli.py list` shows it as `[x]`. Now paste *only* `cli.py` into your
chat and ask: *"Make the `list` command hide tasks that are already done."* Apply whatever it chat and ask: *"Make the `list` command hide tasks that are already done."* Apply whatever it
gives you and run `python cli.py list`. The clean version of this change lives in `tasks.py`, the gives you and run `python3 cli.py list`. The clean version of this change lives in `tasks.py`, the
file you *didn't* paste: open it and you'll see `render()` already owns the `[x]`/`[ ]` file you *didn't* paste: open it and you'll see `render()` already owns the `[x]`/`[ ]`
box-and-index formatting, and a `pending()` helper already returns exactly the not-done tasks. But box-and-index formatting, and a `pending()` helper already returns exactly the not-done tasks. But
the chat never saw that file, so it had to do one of two things. Either it guessed at methods it the chat never saw that file, so it had to do one of two things. Either it guessed at methods it
couldn't see (and `python cli.py list` errors out), or it reached into the raw task list and couldn't see (and `python3 cli.py list` errors out), or it reached into the raw task list and
*re-created* that box-and-index formatting inside `cli.py`, duplicating logic that already existed *re-created* that box-and-index formatting inside `cli.py`, duplicating logic that already existed
one file over. Either way, *you* had to be the one who knew the change really belonged in the one file over. Either way, *you* had to be the one who knew the change really belonged in the
other file. other file.
@@ -254,7 +256,7 @@ Be honest about the limits of this module's claims:
**You're done when:** **You're done when:**
- You can run `python cli.py list` in your terminal and see output; your project, editor, and - You can run `python3 cli.py list` in your terminal and see output; your project, editor, and
terminal are working together. terminal are working together.
- You can name the three seams where copy-paste breaks (more than one file, more than one day, no - You can name the three seams where copy-paste breaks (more than one file, more than one day, no
undo) without looking back at the lesson. undo) without looking back at the lesson.
+3 -3
@@ -210,7 +210,7 @@ and your AI assistant.
This is the habit that replaces "paste it back and hope." You're reading exactly what changed, This is the habit that replaces "paste it back and hope." You're reading exactly what changed,
nothing more, nothing less. Confirm it does what you asked and didn't touch anything it shouldn't. nothing more, nothing less. Confirm it does what you asked and didn't touch anything it shouldn't.
Run it (`python cli.py count`), then commit: Run it (`python3 cli.py count`), then commit:
```bash ```bash
git add . git add .
@@ -229,7 +229,7 @@ and your AI assistant.
git status # shows tasks.py as modified git status # shows tasks.py as modified
git restore tasks.py # discard the change; back to your last commit, byte for byte git restore tasks.py # discard the change; back to your last commit, byte for byte
git diff # empty: nothing changed. you're clean. git diff # empty: nothing changed. you're clean.
python cli.py list # works again python3 cli.py list # works again
``` ```
You just recovered from a bad AI change in one command, with zero retyping and zero guesswork. You just recovered from a bad AI change in one command, with zero retyping and zero guesswork.
@@ -264,7 +264,7 @@ and your AI assistant.
9. Close the loop and leave the repo clean. The cold session just told you what's in progress and 9. Close the loop and leave the repo clean. The cold session just told you what's in progress and
what to do next: finish the `delete <index>` command. Do that with the AI (paste in `cli.py` the what to do next: finish the `delete <index>` command. Do that with the AI (paste in `cli.py` the
same way as Part B), run it to confirm it works (`python cli.py delete 1`), then commit: same way as Part B), run it to confirm it works (`python3 cli.py delete 1`), then commit:
```bash ```bash
git add . git add .
+5 -5
@@ -54,7 +54,7 @@ committed instructions file from the repo, and you control what's in it.**
> content if so. The principle outlives any one vendor's filename. > content if so. The principle outlives any one vendor's filename.
Without this file, you re-explain your project every session: "we use 4-space indent," "run the tests Without this file, you re-explain your project every session: "we use 4-space indent," "run the tests
with `python -m unittest` before you say you're done," "don't touch the generated `tasks.json`." You say it, with `python3 -m unittest` before you say you're done," "don't touch the generated `tasks.json`." You say it,
the AI complies, the session ends, the memory evaporates (Module 1's second seam), and tomorrow you the AI complies, the session ends, the memory evaporates (Module 1's second seam), and tomorrow you
say it all again. The instructions file is where that knowledge stops being something you retype and say it all again. The instructions file is where that knowledge stops being something you retype and
becomes something the project *carries*. becomes something the project *carries*.
@@ -68,7 +68,7 @@ a briefing for an agent that will edit this code. Keep it to what changes the AI
uses. "Core logic lives in `tasks.py`; the CLI front end is `cli.py`; state persists to uses. "Core logic lives in `tasks.py`; the CLI front end is `cli.py`; state persists to
`tasks.json`." `tasks.json`."
- **Build and test commands**: the exact commands, copy-pasteable. "Run the app with - **Build and test commands**: the exact commands, copy-pasteable. "Run the app with
`python cli.py <command>`. Run tests with `python -m unittest`. Don't claim a change works until `python3 cli.py <command>`. Run tests with `python3 -m unittest`. Don't claim a change works until
the tests pass." This single line stops the AI from inventing a test runner you don't use. the tests pass." This single line stops the AI from inventing a test runner you don't use.
- **Coding standards**: formatting, typing, error handling, the libraries you do and don't want. - **Coding standards**: formatting, typing, error handling, the libraries you do and don't want.
"Use the standard library only, no third-party packages. Type-hint public functions." "Use the standard library only, no third-party packages. Type-hint public functions."
@@ -89,7 +89,7 @@ useful for personal preferences, but it's the wrong home for project knowledge,
lives: on *your* laptop, invisible to everyone else. lives: on *your* laptop, invisible to everyone else.
Picture a two-person project with no committed instructions file. You've trained your local setup to Picture a two-person project with no committed instructions file. You've trained your local setup to
run `python -m unittest` and avoid `tasks.json`. Your teammate's setup hasn't, so their agent reformats whole files run `python3 -m unittest` and avoid `tasks.json`. Your teammate's setup hasn't, so their agent reformats whole files
and hand-edits the generated JSON. You're both "using AI on the same repo," but you're getting and hand-edits the generated JSON. You're both "using AI on the same repo," but you're getting
different behavior, and neither of you can see the other's configuration. That's **drift**: the same different behavior, and neither of you can see the other's configuration. That's **drift**: the same
codebase, diverging because the rules live in two heads instead of one file. codebase, diverging because the rules live in two heads instead of one file.
@@ -221,7 +221,7 @@ editor-integrated AI (Module 4) for the part where the AI obeys the file.
- The `tasks-app` repo from Module 2 (already a Git repo with some history). - The `tasks-app` repo from Module 2 (already a Git repo with some history).
- Your agentic coding tool from Module 4, and knowledge of which filename it reads for repo-level - Your agentic coding tool from Module 4, and knowledge of which filename it reads for repo-level
instructions (check its docs; see the note in *Key concepts*). instructions (check its docs; see the note in *Key concepts*).
- Optionally, a test command for the AI to honor; Python's built-in `python -m unittest` works with - Optionally, a test command for the AI to honor; Python's built-in `python3 -m unittest` works with
nothing to install (you'll write a real suite in Module 13; until then it simply reports no tests). nothing to install (you'll write a real suite in Module 13; until then it simply reports no tests).
### Part A: Write the instructions file and let the AI commit the config ### Part A: Write the instructions file and let the AI commit the config
@@ -327,7 +327,7 @@ Be honest about what a committed instructions file does and doesn't buy you:
- **Bloat kills it.** A 300-line instructions file is read the way *you* read a 300-line terms-of- - **Bloat kills it.** A 300-line instructions file is read the way *you* read a 300-line terms-of-
service: not really. Every line you add dilutes the rest. Keep it to what actually changes behavior, service: not really. Every line you add dilutes the rest. Keep it to what actually changes behavior,
and prune lines the model already honors without being told. and prune lines the model already honors without being told.
- **Stale instructions are worse than none.** A file that says "run the tests with `python -m - **Stale instructions are worse than none.** A file that says "run the tests with `python3 -m
unittest`" after you've switched to a different runner will actively misdirect the AI. The file is unittest`" after you've switched to a different runner will actively misdirect the AI. The file is
code-adjacent: it has to be maintained like code, and reviewed like code. That's exactly why code-adjacent: it has to be maintained like code, and reviewed like code. That's exactly why
committing it (so changes are committing it (so changes are
+14 -14
@@ -170,9 +170,9 @@ decide:
```python ```python
<<<<<<< HEAD <<<<<<< HEAD
print("usage: python cli.py [add <title> | list | done <index> | stats]") print("usage: python3 cli.py [add <title> | list | done <index> | stats]")
======= =======
print("usage: python cli.py [add <title> | list | done <index> | purge]") print("usage: python3 cli.py [add <title> | list | done <index> | purge]")
>>>>>>> experiment >>>>>>> experiment
``` ```
@@ -301,9 +301,9 @@ the one job that's still yours: verify the result.
```bash ```bash
git diff # read what it actually changed git diff # read what it actually changed
python cli.py add "ship module 6" --priority high python3 cli.py add "ship module 6" --priority high
python cli.py add "water plants" --priority low python3 cli.py add "water plants" --priority low
python cli.py list # see if priorities work and sort python3 cli.py list # see if priorities work and sort
``` ```
Once the diff looks right and the feature runs, tell the agent: Once the diff looks right and the feature runs, tell the agent:
@@ -318,7 +318,7 @@ the one job that's still yours: verify the result.
> *"Switch back to `main`."* > *"Switch back to `main`."*
```bash ```bash
python cli.py list # no priorities; main is exactly as you left it python3 cli.py list # no priorities; main is exactly as you left it
``` ```
Your bold change exists only on the branch. `main` never saw it, and that's the whole point. Your bold change exists only on the branch. `main` never saw it, and that's the whole point.
@@ -337,7 +337,7 @@ Then verify the result yourself:
```bash ```bash
git log --oneline --graph # straight line = fast-forward merge git log --oneline --graph # straight line = fast-forward merge
python cli.py list # the feature is now on main python3 cli.py list # the feature is now on main
git branch # experiment/priorities is gone git branch # experiment/priorities is gone
``` ```
@@ -349,7 +349,7 @@ Then verify:
```bash ```bash
git log --oneline # no trace of the experiment on main git log --oneline # no trace of the experiment on main
python cli.py list # main is untouched, exactly as before python3 cli.py list # main is untouched, exactly as before
git branch # the branch is gone git branch # the branch is gone
``` ```
@@ -417,9 +417,9 @@ Merge conflicts have an outsized reputation for difficulty. You'll engineer a gu
```python ```python
<<<<<<< HEAD <<<<<<< HEAD
print("usage: python cli.py [add <title> | list | done <index> | purge]") print("usage: python3 cli.py [add <title> | list | done <index> | purge]")
======= =======
print("usage: python cli.py [add <title> | list | done <index> | stats]") print("usage: python3 cli.py [add <title> | list | done <index> | stats]")
>>>>>>> feature/stats >>>>>>> feature/stats
``` ```
@@ -452,7 +452,7 @@ Merge conflicts have an outsized reputation for difficulty. You'll engineer a gu
should have produced a single, marker-free line listing both commands, e.g.: should have produced a single, marker-free line listing both commands, e.g.:
```python ```python
print("usage: python cli.py [add <title> | list | done <index> | stats | purge]") print("usage: python3 cli.py [add <title> | list | done <index> | stats | purge]")
``` ```
**Here is the punchline of the whole module: you have no idea yet whether that's right, so verify.** **Here is the punchline of the whole module: you have no idea yet whether that's right, so verify.**
@@ -464,9 +464,9 @@ Merge conflicts have an outsized reputation for difficulty. You'll engineer a gu
```bash ```bash
git diff HEAD~1 # what the merge actually changed; confirm no markers remain git diff HEAD~1 # what the merge actually changed; confirm no markers remain
git log --oneline --graph # the fork-and-join: this is a merge commit git log --oneline --graph # the fork-and-join: this is a merge commit
python cli.py # run with no args, see the merged usage string python3 cli.py # run with no args, see the merged usage string
python cli.py stats # both commands actually work python3 cli.py stats # both commands actually work
python cli.py purge python3 cli.py purge
``` ```
If the usage line lists both commands and both run, the AI's silent resolution was correct. If it If the usage line lists both commands and both run, the AI's silent resolution was correct. If it
+7 -7
@@ -329,8 +329,8 @@ This is the part to actually *do simultaneously*, not one then the other.
writing them.) Give each worktree its own task and list it: writing them.) Give each worktree its own task and list it:
```bash ```bash
cd ~/ai-workflow-course/tasks-app-wipe && python cli.py add "from worktree A" && python cli.py list cd ~/ai-workflow-course/tasks-app-wipe && python3 cli.py add "from worktree A" && python3 cli.py list
cd ~/ai-workflow-course/tasks-app-remaining && python cli.py add "from worktree B" && python cli.py list cd ~/ai-workflow-course/tasks-app-remaining && python3 cli.py add "from worktree B" && python3 cli.py list
``` ```
Each `list` shows only its own task: worktree A never sees "from worktree B" and vice versa. Each Each `list` shows only its own task: worktree A never sees "from worktree B" and vice versa. Each
@@ -355,8 +355,8 @@ 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: 5. *Now* the new commands exist: run each in its own worktree to watch it work:
```bash ```bash
cd ~/ai-workflow-course/tasks-app-wipe && python cli.py wipe # agent A's new command cd ~/ai-workflow-course/tasks-app-wipe && python3 cli.py wipe # agent A's new command
cd ~/ai-workflow-course/tasks-app-remaining && python cli.py remaining # agent B's new command cd ~/ai-workflow-course/tasks-app-remaining && python3 cli.py remaining # agent B's new command
``` ```
`remaining` counts a single pending task, the one you added to worktree B in step 3, because B's `remaining` counts a single pending task, the one you added to worktree B in step 3, because B's
@@ -384,9 +384,9 @@ Then **verify** the result before you trust it, the same way you did in Module 6
```bash ```bash
cd ~/ai-workflow-course/tasks-app cd ~/ai-workflow-course/tasks-app
git diff # no conflict markers remain git diff # no conflict markers remain
python cli.py list # the app still runs python3 cli.py list # the app still runs
python cli.py wipe # both new commands work python3 cli.py wipe # both new commands work
python cli.py remaining python3 cli.py remaining
``` ```
Now tear down the worktrees. Direct the coordinating session: Now tear down the worktrees. Direct the coordinating session:
+3 -3
@@ -111,8 +111,8 @@ well-formed version of the same bug:
> **Title:** `done` command crashes on an out-of-range or non-integer index > **Title:** `done` command crashes on an out-of-range or non-integer index
> >
> **Context:** `python cli.py done 99` on a list with 3 tasks raises an uncaught `IndexError` and > **Context:** `python3 cli.py done 99` on a list with 3 tasks raises an uncaught `IndexError` and
> dumps a traceback. `python cli.py done abc` raises `ValueError`. Either way the user sees a stack > dumps a traceback. `python3 cli.py done abc` raises `ValueError`. Either way the user sees a stack
> trace instead of a helpful message. > trace instead of a helpful message.
> >
> **Acceptance criteria:** > **Acceptance criteria:**
@@ -270,7 +270,7 @@ plenty it still can't do. Because it's carried forward across modules, skip anyt
already built (a `delete` command, task priorities) and pick work that's genuinely still missing. already built (a `delete` command, task priorities) and pick work that's genuinely still missing.
Good candidates: Good candidates:
1. **A bug**: `python cli.py done 99` (an out-of-range index) and `python cli.py done abc` (a 1. **A bug**: `python3 cli.py done 99` (an out-of-range index) and `python3 cli.py done abc` (a
non-integer) both crash with an uncaught traceback. Run them and watch. non-integer) both crash with an uncaught traceback. Run them and watch.
2. **A small, patterned feature**: an `undone <index>` command that clears a task's done flag, 2. **A small, patterned feature**: an `undone <index>` command that clears a task's done flag,
mirroring the existing `done` command (it's the inverse). mirroring the existing `done` command (it's the inverse).
+7 -7
@@ -249,8 +249,8 @@ real change, then review a diff the "AI" produced and catch the trap planted in
Then see the baseline behavior with your own eyes, because the trap is going to change it: Then see the baseline behavior with your own eyes, because the trap is going to change it:
```bash ```bash
python cli.py add "write the review module" python3 cli.py add "write the review module"
python cli.py done 99 # baseline: prints "error: no task at index 99", exits non-zero python3 cli.py done 99 # baseline: prints "error: no task at index 99", exits non-zero
echo "exit code: $?" echo "exit code: $?"
``` ```
@@ -302,12 +302,12 @@ real change, then review a diff the "AI" produced and catch the trap planted in
5. Now verify your read by running the *failure* path, not the happy one: 5. Now verify your read by running the *failure* path, not the happy one:
```bash ```bash
python cli.py add "a real task" python3 cli.py add "a real task"
python cli.py delete 0 # the requested feature: works fine on the happy path python3 cli.py delete 0 # the requested feature: works fine on the happy path
python cli.py add "another" python3 cli.py add "another"
python cli.py done 99 # the trap: compare this to your Part A baseline python3 cli.py done 99 # the trap: compare this to your Part A baseline
echo "exit code: $?" echo "exit code: $?"
python cli.py list # did task 99 (which doesn't exist) get marked done? did anything? python3 cli.py list # did task 99 (which doesn't exist) get marked done? did anything?
``` ```
In the base app, `done 99` was a clean error with a non-zero exit. After this "add a delete In the base app, `done 99` was a clean error with a non-zero exit. After this "add a delete
+5 -5
@@ -361,11 +361,11 @@ the server say *no* is the point: "never commit to `main`" is now a rule, not a
the CLI), and it does what you asked. Run it: the CLI), and it does what you asked. Run it:
```bash ```bash
python cli.py add "keeper" ; python cli.py add "trash" python3 cli.py add "keeper" ; python3 cli.py add "trash"
python cli.py list # note the index shown next to "trash" python3 cli.py list # note the index shown next to "trash"
python cli.py done <trash-index> # use the index "list" just printed, NOT a fixed 1 python3 cli.py done <trash-index> # use the index "list" just printed, NOT a fixed 1
python cli.py clear-done # expect it to remove the completed one python3 cli.py clear-done # expect it to remove the completed one
python cli.py list # "keeper" remains, "trash" is gone python3 cli.py list # "keeper" remains, "trash" is gone
``` ```
Read the index off `list` rather than assuming it: `done` is positional, and your `tasks-app` has Read the index off `list` rather than assuming it: `done` is positional, and your `tasks-app` has
+8 -8
@@ -301,9 +301,9 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
4. **Now feel the bug.** It passes the first skim: 4. **Now feel the bug.** It passes the first skim:
```bash ```bash
python cli.py add "ship it" python3 cli.py add "ship it"
python cli.py clear # prints "cleared all tasks", looks fine! python3 cli.py clear # prints "cleared all tasks", looks fine!
python cli.py list # CRASHES: it corrupted tasks.json, load() blows up python3 cli.py list # CRASHES: it corrupted tasks.json, load() blows up
``` ```
This is the AI plausibility trap made concrete: the change reviewed fine and "worked," and broke This is the AI plausibility trap made concrete: the change reviewed fine and "worked," and broke
@@ -343,8 +343,8 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
```bash ```bash
rm -f tasks.json # drop the corrupted state file the bug wrote rm -f tasks.json # drop the corrupted state file the bug wrote
python cli.py add "back to normal" python3 cli.py add "back to normal"
python cli.py list # works again, the clear command is gone python3 cli.py list # works again, the clear command is gone
git log --oneline # the bad merge is STILL there, with a revert after it git log --oneline # the bad merge is STILL there, with a revert after it
``` ```
@@ -366,7 +366,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
```bash ```bash
git log --oneline -1 # "Add version command" git log --oneline -1 # "Add version command"
python cli.py version # prints the version python3 cli.py version # prints the version
``` ```
2. Now destroy it the way an over-eager "clean up the history" cleanup (or an agent) would, with a 2. Now destroy it the way an over-eager "clean up the history" cleanup (or an agent) would, with a
@@ -375,7 +375,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
```bash ```bash
git reset --hard HEAD~1 git reset --hard HEAD~1
git log --oneline -2 # the "Add version command" commit is GONE from the branch git log --oneline -2 # the "Add version command" commit is GONE from the branch
python cli.py version 2>/dev/null || echo "command no longer exists" python3 cli.py version 2>/dev/null || echo "command no longer exists"
``` ```
It's not in `log`. It feels permanently lost. It isn't. It's not in `log`. It feels permanently lost. It isn't.
@@ -390,7 +390,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
```bash ```bash
git log --oneline -1 # "Add version command" is back git log --oneline -1 # "Add version command" is back
python cli.py version # works again python3 cli.py version # works again
``` ```
You just recovered a commit that `log` swore was gone. Note the honest limit: step 2's `--hard` You just recovered a commit that `log` swore was gone. Note the honest limit: step 2's `--hard`
+9 -9
@@ -55,7 +55,7 @@ that runs a piece of your code and asserts that the result is what it should be.
holds, the test passes silently. If it doesn't, the test fails loudly and tells you exactly which holds, the test passes silently. If it doesn't, the test fails loudly and tells you exactly which
expectation broke. expectation broke.
You've already been testing, by hand. Every time you ran `python cli.py list` and eyeballed the You've already been testing, by hand. Every time you ran `python3 cli.py list` and eyeballed the
output, you ran a manual test: *do something, check the result looks right.* The problem with the output, you ran a manual test: *do something, check the result looks right.* The problem with the
manual version is the same problem copy-paste had in Module 1: it doesn't scale across files or manual version is the same problem copy-paste had in Module 1: it doesn't scale across files or
across time. You can't re-run "eyeball every command" on every change, so you don't, so regressions across time. You can't re-run "eyeball every command" on every change, so you don't, so regressions
@@ -77,12 +77,12 @@ class TestTaskList(unittest.TestCase):
self.assertEqual(tl.tasks[0].title, "write the tests") self.assertEqual(tl.tasks[0].title, "write the tests")
``` ```
The whole suite runs from the project folder with a single command: `python -m unittest` The whole suite runs from the project folder with a single command: `python3 -m unittest`
auto-discovers files named `test_*.py`, and `-v` prints each test name and its result. A verbose run auto-discovers files named `test_*.py`, and `-v` prints each test name and its result. A verbose run
looks like: looks like:
```text ```text
$ python -m unittest -v $ python3 -m unittest -v
test_add_appends_a_task (test_tasks.TestTaskList) ... ok test_add_appends_a_task (test_tasks.TestTaskList) ... ok
---------------------------------------------------------------------- ----------------------------------------------------------------------
@@ -173,7 +173,7 @@ intent has to come from you.
One more framing before the lab. A test file just sitting in your repo is useful when you remember to One more framing before the lab. A test file just sitting in your repo is useful when you remember to
run it; like the manual eyeball check, you eventually won't. The full payoff comes in run it; like the manual eyeball check, you eventually won't. The full payoff comes in
**Module 14**, where Continuous Integration runs this exact `python -m unittest` command **Module 14**, where Continuous Integration runs this exact `python3 -m unittest` command
automatically on every push, so a regression can't reach `main` without something going red first. automatically on every push, so a regression can't reach `main` without something going red first.
That's why this module comes immediately before CI: **tests are the content CI runs.** You can't That's why this module comes immediately before CI: **tests are the content CI runs.** You can't
@@ -262,7 +262,7 @@ Do this once yourself so the tool isn't magic. From inside your working copy of
2. Run it: 2. Run it:
```bash ```bash
python -m unittest -v python3 -m unittest -v
``` ```
You should see one test, and `OK`. That's the entire mechanism. Everything else is more of these. You should see one test, and `OK`. That's the entire mechanism. Everything else is more of these.
@@ -292,7 +292,7 @@ Do this once yourself so the tool isn't magic. From inside your working copy of
5. Run the suite: 5. Run the suite:
```bash ```bash
python -m unittest -v python3 -m unittest -v
``` ```
At least one `pending_count` test should **FAIL**, with something like At least one `pending_count` test should **FAIL**, with something like
@@ -316,8 +316,8 @@ Do this once yourself so the tool isn't magic. From inside your working copy of
return len(self.pending()) return len(self.pending())
``` ```
Re-run `python -m unittest -v`; green. Confirm the app agrees: Re-run `python3 -m unittest -v`; green. Confirm the app agrees:
`python cli.py add a && python cli.py add b && python cli.py done 0 && python cli.py count` `python3 cli.py add a && python3 cli.py add b && python3 cli.py done 0 && python3 cli.py count`
should report **1 task(s) pending**. should report **1 task(s) pending**.
> Using your own app from earlier modules instead? If your `count` command was already correct, > Using your own app from earlier modules instead? If your `count` command was already correct,
@@ -371,7 +371,7 @@ The honest limits, because a green suite invites overconfidence:
**You're done when:** **You're done when:**
- You can run `python -m unittest -v` in your `tasks-app` and see your own tests pass. - You can run `python3 -m unittest -v` in your `tasks-app` and see your own tests pass.
- You watched an intent-encoding test **fail**, traced it to the real `pending_count` bug, fixed the - You watched an intent-encoding test **fail**, traced it to the real `pending_count` bug, fixed the
*code*, and watched it pass. *code*, and watched it pass.
- You can articulate, in your own words, the difference between a test that asserts current behavior - You can articulate, in your own words, the difference between a test that asserts current behavior
+4 -4
@@ -84,7 +84,7 @@ Almost every CI configuration, on every forge, is the same four moves:
4. **Run the checks**: lint, then test. Any check that exits non-zero fails the whole run. 4. **Run the checks**: lint, then test. Any check that exits non-zero fails the whole run.
That last point is the load-bearing one. CI's entire enforcement mechanism is the **exit code**. That last point is the load-bearing one. CI's entire enforcement mechanism is the **exit code**.
Every tool you'd run in a terminal returns 0 for success and non-zero for failure. `python -m Every tool you'd run in a terminal returns 0 for success and non-zero for failure. `python3 -m
unittest` exits non-zero if a test fails. `ruff check` exits non-zero if it finds a lint problem. CI runs your unittest` exits non-zero if a test fails. `ruff check` exits non-zero if it finds a lint problem. CI runs your
commands and watches those exit codes; one failure turns the run red. You're not learning a new commands and watches those exit codes; one failure turns the run red. You're not learning a new
testing system; you're wiring the tools you already have to a trigger. testing system; you're wiring the tools you already have to a trigger.
@@ -160,7 +160,7 @@ When CI goes red, the skill is triage, and it's fast once you know the shape:
3. **Read that step's log.** It's the same output the tool prints in your terminal: a failing 3. **Read that step's log.** It's the same output the tool prints in your terminal: a failing
`unittest` assertion, a `ruff` finding with a file and line number. CI didn't invent a new error `unittest` assertion, a `ruff` finding with a file and line number. CI didn't invent a new error
format; it's showing you the command's own output. format; it's showing you the command's own output.
4. **Reproduce it locally.** The same command from the failed step (`python -m unittest` or 4. **Reproduce it locally.** The same command from the failed step (`python3 -m unittest` or
`ruff check .`) fails the same way on your own machine, because CI ran exactly that command. That `ruff check .`) fails the same way on your own machine, because CI ran exactly that command. That
reproducibility is the point: fix locally, confirm green locally, push again. reproducibility is the point: fix locally, confirm green locally, push again.
@@ -260,7 +260,7 @@ your machine first.
that CI is nothing more than these same two commands is what makes the rest of the module click. that CI is nothing more than these same two commands is what makes the rest of the module click.
```bash ```bash
python -m unittest # should report all tests passing python3 -m unittest # should report all tests passing
ruff check . # should report no issues (or fix what it flags) ruff check . # should report no issues (or fix what it flags)
``` ```
@@ -331,7 +331,7 @@ and watch CI stop it.
`git restore` (Module 12). What the agent runs looks like: `git restore` (Module 12). What the agent runs looks like:
```bash ```bash
python -m unittest # fails locally too: same command, same failure python3 -m unittest # fails locally too: same command, same failure
git revert --no-edit HEAD # new commit that undoes "Simplify pending()" (Module 12) git revert --no-edit HEAD # new commit that undoes "Simplify pending()" (Module 12)
git push # CI re-runs on the fixed code and goes green again git push # CI re-runs on the fixed code and goes green again
``` ```
+8 -8
@@ -96,10 +96,10 @@ Set one for a single command:
```bash ```bash
# macOS / Linux # macOS / Linux
TASKS_API_KEY="sk-live-..." python sync.py TASKS_API_KEY="sk-live-..." python3 sync.py
# Windows PowerShell # Windows PowerShell
$env:TASKS_API_KEY="sk-live-..."; python sync.py $env:TASKS_API_KEY="sk-live-..."; python3 sync.py
``` ```
Read it back in code, and **fail loudly if it's missing**, because a silent empty string is worse Read it back in code, and **fail loudly if it's missing**, because a silent empty string is worse
@@ -313,7 +313,7 @@ type the commands by hand. Then you'll make it select config per environment.
```bash ```bash
cd ~/ai-workflow-course/tasks-app cd ~/ai-workflow-course/tasks-app
python sync.py python3 sync.py
``` ```
It prints a simulated request, including `Authorization: Bearer sk-live-...`. Open `sync.py` and It prints a simulated request, including `Authorization: Bearer sk-live-...`. Open `sync.py` and
@@ -413,7 +413,7 @@ type the commands by hand. Then you'll make it select config per environment.
5. Run it reading from your `.env`: 5. Run it reading from your `.env`:
```bash ```bash
python sync.py # loads .env -> dev URL, key from the file python3 sync.py # loads .env -> dev URL, key from the file
``` ```
6. Now prove the 12-factor point: **same code, different environment, no edit.** Override at the 6. Now prove the 12-factor point: **same code, different environment, no edit.** Override at the
@@ -421,13 +421,13 @@ type the commands by hand. Then you'll make it select config per environment.
```bash ```bash
# macOS / Linux # macOS / Linux
APP_ENV=staging python sync.py APP_ENV=staging python3 sync.py
APP_ENV=prod TASKS_API_KEY="sk-live-prod-key" python sync.py APP_ENV=prod TASKS_API_KEY="sk-live-prod-key" python3 sync.py
``` ```
```powershell ```powershell
# Windows PowerShell # Windows PowerShell
$env:APP_ENV="staging"; python sync.py $env:APP_ENV="staging"; python3 sync.py
``` ```
Watch the backend URL change with `APP_ENV` while the source never does. That's config in the Watch the backend URL change with `APP_ENV` while the source never does. That's config in the
@@ -489,7 +489,7 @@ left behind a `.env.example` so the next person (or agent) knows what to supply.
- `sync.py` runs entirely from the environment, and `grep "sk-live" sync.py` prints nothing. - `sync.py` runs entirely from the environment, and `grep "sk-live" sync.py` prints nothing.
- A real `.env` exists, contains your secret, and does **not** appear in `git status`, while - A real `.env` exists, contains your secret, and does **not** appear in `git status`, while
`.env.example` is tracked. `.env.example` is tracked.
- `APP_ENV=staging python sync.py` and the default run hit different backend URLs with **zero** - `APP_ENV=staging python3 sync.py` and the default run hit different backend URLs with **zero**
source edits between them. source edits between them.
- You can state, in one sentence, why deleting a committed secret and re-committing does not fix the - You can state, in one sentence, why deleting a committed secret and re-committing does not fix the
leak, and what the actual fix is (rotation). leak, and what the actual fix is (rotation).
+1 -1
@@ -256,7 +256,7 @@ A CLI that exits immediately is awkward to "deploy." Give the app a long-running
2. Run the service locally first, no container, to see it work: 2. Run the service locally first, no container, to see it work:
```bash ```bash
python serve.py # serves on http://localhost:8000 python3 serve.py # serves on http://localhost:8000
``` ```
In another terminal: In another terminal:
+4 -4
@@ -336,7 +336,7 @@ That's the entire client/server loop, end to end, with zero code you wrote. Now
> contents so I can read it."* > contents so I can read it."*
Then open the copied file yourself and read it. (It reuses `tasks.py` and shares the same Then open the copied file yourself and read it. (It reuses `tasks.py` and shares the same
`tasks.json`, so anything it changes shows up in `python cli.py list`.) The whole server is two `tasks.json`, so anything it changes shows up in `python3 cli.py list`.) The whole server is two
tools: tools:
```python ```python
@@ -417,14 +417,14 @@ That's the entire client/server loop, end to end, with zero code you wrote. Now
the way you'd verify any runtime effect, by reading the *state*, not the repo: the way you'd verify any runtime effect, by reading the *state*, not the repo:
```bash ```bash
python cli.py list # the new task is there, because the server wrote the same tasks.json python3 cli.py list # the new task is there, because the server wrote the same tasks.json
cat tasks.json # the raw state the server changed, end to end 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. Notice what you did *not* 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 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 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`), bug. The proof the task list changed is the live state (`python3 cli.py list` / `cat tasks.json`),
not version control; runtime data the app owns is exactly the kind of thing you keep *out* of 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 history. No copy-paste, no script you ran by hand, no pasting `tasks.json` into a chat. That's
"hands." "hands."
@@ -483,7 +483,7 @@ The caveats, and one of them is large enough that it gets its own module.
connected with `list_tasks` and `add_task` available. 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 - 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** by reading the runtime state 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 (`python3 cli.py list` / `cat tasks.json`), not `git diff`, because `tasks.json` is deliberately
gitignored (Module 2). gitignored (Module 2).
- You can explain the client/server model in one breath (*servers expose tools/resources/prompts; - 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 the client (your agentic tool) discovers and calls them on the AI's behalf*) and why "it's a
+7 -7
@@ -93,7 +93,7 @@ This is the distinction to lock in, because the two are siblings and easy to con
| Analogy | The standing house rules posted on the wall | A labeled recipe card you pull out when you cook that dish | | Analogy | The standing house rules posted on the wall | A labeled recipe card you pull out when you cook that dish |
They're complementary. The instructions file is the right home for facts true *all the time* ("tests They're complementary. The instructions file is the right home for facts true *all the time* ("tests
run with `python -m unittest`"). A skill is the right home for a procedure you run *sometimes* ("here run with `python3 -m unittest`"). A skill is the right home for a procedure you run *sometimes* ("here
is exactly how we add a command"). Module 5 even told you this was coming: start with the always-on is exactly how we add a command"). Module 5 even told you this was coming: start with the always-on
file; graduate a procedure into a skill when it earns its own page. file; graduate a procedure into a skill when it earns its own page.
@@ -153,7 +153,7 @@ On paper this is just "write a runbook." The AI-specific twist is what changes t
- **The AI will execute the playbook, not just read it.** A runbook for a human is a reminder; a skill - **The AI will execute the playbook, not just read it.** A runbook for a human is a reminder; a skill
for an agent is something it *performs*. The precision pays off immediately: vague step, vague for an agent is something it *performs*. The precision pays off immediately: vague step, vague
result; imperative step ("run `python -m unittest`; do not claim success until it's green"), reliable result; imperative step ("run `python3 -m unittest`; do not claim success until it's green"), reliable
result. result.
- **The AI is confidently incomplete without one.** Asked to "add a command," it'll happily stop at - **The AI is confidently incomplete without one.** Asked to "add a command," it'll happily stop at
the code and skip the test, the changelog, the clean commit, and sound finished doing it. The skill the code and skip the test, the changelog, the clean commit, and sound finished doing it. The skill
@@ -228,8 +228,8 @@ seen, producing all four parts without you listing the steps.
5. Watch it perform the procedure. A correctly-followed skill will, without you saying any of it: 5. Watch it perform the procedure. A correctly-followed skill will, without you saying any of it:
- add `clear()` to `tasks.py` and wire a `clear` branch into `cli.py` (logic in the right file); - add `clear()` to `tasks.py` and wire a `clear` branch into `cli.py` (logic in the right file);
- add a real test to `test_tasks.py` that asserts the list is empty afterward (not just "no crash"); - add a real test to `test_tasks.py` that asserts the list is empty afterward (not just "no crash");
- run `python -m unittest` and show it green; - run `python3 -m unittest` and show it green;
- smoke-test `python cli.py clear` and show the output; - smoke-test `python3 cli.py clear` and show the output;
- add a `CHANGELOG.md` line; - add a `CHANGELOG.md` line;
- stage code + test + changelog into one commit, **without** `tasks.json`. - stage code + test + changelog into one commit, **without** `tasks.json`.
@@ -238,8 +238,8 @@ seen, producing all four parts without you listing the steps.
6. Don't take the AI's word for it. Check against the skill's own done-criteria: 6. Don't take the AI's word for it. Check against the skill's own done-criteria:
```bash ```bash
python -m unittest # green, and a clear-related test is present python3 -m unittest # green, and a clear-related test is present
python cli.py add "x" && python cli.py clear && python cli.py list # -> (no tasks yet) python3 cli.py add "x" && python3 cli.py clear && python3 cli.py list # -> (no tasks yet)
git show --stat HEAD # one commit: tasks.py, cli.py, test_tasks.py, CHANGELOG.md; no tasks.json git show --stat HEAD # one commit: tasks.py, cli.py, test_tasks.py, CHANGELOG.md; no tasks.json
``` ```
@@ -324,7 +324,7 @@ time:
that the example skill format stays generic (when-to-use / inputs / steps / done-criteria). that the example skill format stays generic (when-to-use / inputs / steps / done-criteria).
- [ ] **Dependency chain intact.** Confirm Module 20 (MCP) and Module 22 (securing servers/skills) are - [ ] **Dependency chain intact.** Confirm Module 20 (MCP) and Module 22 (securing servers/skills) are
still numbered as referenced, and that nothing here leans on a tool introduced after Module 20. still numbered as referenced, and that nothing here leans on a tool introduced after Module 20.
- [ ] **Lab still runs.** `python -m unittest` is green in `lab/tasks-app/`, and the `clear`-command - [ ] **Lab still runs.** `python3 -m unittest` is green in `lab/tasks-app/`, and the `clear`-command
walkthrough still matches the starter files (`add`/`list`/`done`/`count`, `test_tasks.py`, walkthrough still matches the starter files (`add`/`list`/`done`/`count`, `test_tasks.py`,
`CHANGELOG.md`). `CHANGELOG.md`).
+4 -4
@@ -271,14 +271,14 @@ normal question) and the attacker (you plant content the agent reads).
```bash ```bash
cd ~/ai-workflow-course/tasks-app cd ~/ai-workflow-course/tasks-app
python cli.py add "$(cat ~/ai-workflow-course/modules/22-securing-third-party-mcp-and-skills/lab/poisoned-task.txt)" python3 cli.py add "$(cat ~/ai-workflow-course/modules/22-securing-third-party-mcp-and-skills/lab/poisoned-task.txt)"
python cli.py list python3 cli.py list
``` ```
`poisoned-task.txt` contains a normal-looking task followed by an injected instruction (a fake `poisoned-task.txt` contains a normal-looking task followed by an injected instruction (a fake
"system" directive telling the assistant to reveal local secrets / run a command and hide it). "system" directive telling the assistant to reveal local secrets / run a command and hide it).
2. **Be the victim.** Paste the full output of `python cli.py list` into your agent's chat (Claude 2. **Be the victim.** Paste the full output of `python3 cli.py list` into your agent's chat (Claude
Code in these examples; sub your own) and ask the thing you'd actually ask: *"Here's my task list, Code in these examples; sub your own) and ask the thing you'd actually ask: *"Here's my task list,
summarize what's pending and tell me what to summarize what's pending and tell me what to
work on first."* Watch what happens. Depending on the model, it may flag the injection, or it may work on first."* Watch what happens. Depending on the model, it may flag the injection, or it may
@@ -309,7 +309,7 @@ normal question) and the attacker (you plant content the agent reads).
```bash ```bash
# the "tool" the agent is allowed to call in read-only mode # the "tool" the agent is allowed to call in read-only mode
python cli.py list # works python3 cli.py list # works
# the tool it is NOT exposed (a write); in a least-privilege setup this path is simply absent # the tool it is NOT exposed (a write); in a least-privilege setup this path is simply absent
``` ```
+1 -1
@@ -202,7 +202,7 @@ This lab does **not** use `tasks-app`; the entire point is a codebase you *didn'
git clone <repo-url> unfamiliar-repo git clone <repo-url> unfamiliar-repo
cd unfamiliar-repo cd unfamiliar-repo
# copy modules/23-working-with-existing-codebases/lab/orient.py into this folder # copy modules/23-working-with-existing-codebases/lab/orient.py into this folder
python orient.py > ORIENT.md python3 orient.py > ORIENT.md
``` ```
2. Read `ORIENT.md` yourself first. In 30 seconds you should know the language, the likely entry 2. Read `ORIENT.md` yourself first. In 30 seconds you should know the language, the likely entry
+9 -9
@@ -190,7 +190,7 @@ This is the real production loop with the forge plumbing simulated locally.
**You'll need:** **You'll need:**
- Python 3.10+ (`python --version`). - Python 3.10+ (`python3 --version`).
- The lab files in `~/ai-workflow-course/modules/24-assistive-agents/lab/`. - The lab files in `~/ai-workflow-course/modules/24-assistive-agents/lab/`.
- Claude Code (`claude --version`; sub your own agent), the editor/CLI agent from Module 4. - Claude Code (`claude --version`; sub your own agent), the editor/CLI agent from Module 4.
@@ -211,7 +211,7 @@ it runs the scripts and writes the files. You verify at the gate.
``` ```
You: In ~/ai-workflow-course/modules/24-assistive-agents/lab, run You: In ~/ai-workflow-course/modules/24-assistive-agents/lab, run
`python reviewer.py apply ai-review.sample.json` and show me the output. `python3 reviewer.py apply ai-review.sample.json` and show me the output.
``` ```
Read what comes back: comments sorted by severity, a recommendation, and then the **human decision Read what comes back: comments sorted by severity, a recommendation, and then the **human decision
@@ -221,7 +221,7 @@ it runs the scripts and writes the files. You verify at the gate.
the reviewer, and write its JSON review to a file: the reviewer, and write its JSON review to a file:
``` ```
You: Run `python reviewer.py prompt`, follow the rubric in that output to review the diff, and You: Run `python3 reviewer.py prompt`, follow the rubric in that output to review the diff, and
save your review as JSON to my-review.json. save your review as JSON to my-review.json.
``` ```
@@ -232,7 +232,7 @@ it runs the scripts and writes the files. You verify at the gate.
3. Have the agent render its own review through the gate: 3. Have the agent render its own review through the gate:
``` ```
You: Run `python reviewer.py apply my-review.json` and show me the result. You: Run `python3 reviewer.py apply my-review.json` and show me the result.
``` ```
4. **Make the human decision. This part stays yours.** Open `feature.patch` and check the agent's 4. **Make the human decision. This part stays yours.** Open `feature.patch` and check the agent's
@@ -249,7 +249,7 @@ A new issue just arrived: `sample-issue.md` (the `done` command crashes on an em
1. See the loop with the canned response: 1. See the loop with the canned response:
``` ```
You: Run `python triage.py apply ai-triage.sample.json` and show me the output. You: Run `python3 triage.py apply ai-triage.sample.json` and show me the output.
``` ```
Read the suggested labels, the route, and the **human confirm gate**. The agent applied nothing. Read the suggested labels, the route, and the **human confirm gate**. The agent applied nothing.
@@ -258,14 +258,14 @@ A new issue just arrived: `sample-issue.md` (the `done` command crashes on an em
and save its suggestion: and save its suggestion:
``` ```
You: Run `python triage.py prompt`, follow it to triage the issue using only the committed You: Run `python3 triage.py prompt`, follow it to triage the issue using only the committed
taxonomy, and save your JSON suggestion to my-triage.json. taxonomy, and save your JSON suggestion to my-triage.json.
``` ```
3. Render the suggestion through the gate: 3. Render the suggestion through the gate:
``` ```
You: Run `python triage.py apply my-triage.json` and show me the result. You: Run `python3 triage.py apply my-triage.json` and show me the result.
``` ```
4. **Watch the guardrail.** The script validates every suggested label against the committed 4. **Watch the guardrail.** The script validates every suggested label against the committed
@@ -346,8 +346,8 @@ This is expansion-zone material; the agent-tooling landscape moves fast. Re-chec
merge/close, so comment/label-only is actually grantable? Name two that do. merge/close, so comment/label-only is actually grantable? Name two that do.
- [ ] Is the turnkey "AI review bot / app" framing still accurate, or has the dominant pattern shifted - [ ] Is the turnkey "AI review bot / app" framing still accurate, or has the dominant pattern shifted
(e.g. baked into the forge, or into editor agents)? Keep the description vendor-neutral. (e.g. baked into the forge, or into editor agents)? Keep the description vendor-neutral.
- [ ] Confirm the lab scripts run on a current Python (`python reviewer.py apply ai-review.sample.json` - [ ] Confirm the lab scripts run on a current Python (`python3 reviewer.py apply ai-review.sample.json`
and `python triage.py apply ai-triage.sample.json`) with no dependencies. and `python3 triage.py apply ai-triage.sample.json`) with no dependencies.
- [ ] Re-verify the cross-references resolve to the right module numbers (9, 10, 13, 14, 15, 22, 25) - [ ] Re-verify the cross-references resolve to the right module numbers (9, 10, 13, 14, 15, 22, 25)
if any modules were renumbered. if any modules were renumbered.
- [ ] Check that nothing here pins a specific LLM vendor or a specific bot's config filename. - [ ] Check that nothing here pins a specific LLM vendor or a specific bot's config filename.
+4 -4
@@ -262,7 +262,7 @@ out of the agent's `git add -A`, so the change you review in Part B is clean. Th
```bash ```bash
# Simulate an agent that produces a BROKEN change, then run the gate on it: # Simulate an agent that produces a BROKEN change, then run the gate on it:
python agent_runner.py issue-to-pr issue-delete-command.md --simulate bad python3 agent_runner.py issue-to-pr issue-delete-command.md --simulate bad
``` ```
The orchestrator creates and switches to its own `agent/issue-delete-command` branch first (the same The orchestrator creates and switches to its own `agent/issue-delete-command` branch first (the same
@@ -276,7 +276,7 @@ reached `main`.
### Part B: See a good change land as a PR proposal ### Part B: See a good change land as a PR proposal
```bash ```bash
python agent_runner.py issue-to-pr issue-delete-command.md --simulate good python3 agent_runner.py issue-to-pr issue-delete-command.md --simulate good
``` ```
This time the planted change is correct. The gate passes, the script commits to the branch and prints This time the planted change is correct. The gate passes, the script commits to the branch and prints
@@ -290,7 +290,7 @@ stops at a PR; it never merges.
### Part C: Run the self-healing loop ### Part C: Run the self-healing loop
```bash ```bash
python agent_runner.py self-heal --simulate bad python3 agent_runner.py self-heal --simulate bad
``` ```
The orchestrator switches to its own `agent/self-heal` branch (again, you direct the automation, not The orchestrator switches to its own `agent/self-heal` branch (again, you direct the automation, not
@@ -308,7 +308,7 @@ Two ways to go from simulation to a genuine autonomous run:
```bash ```bash
export AGENT_CMD='your-agent-cli --print --prompt-file {prompt_file}' # your tool's one-shot mode export AGENT_CMD='your-agent-cli --print --prompt-file {prompt_file}' # your tool's one-shot mode
python agent_runner.py issue-to-pr issue-delete-command.md python3 agent_runner.py issue-to-pr issue-delete-command.md
``` ```
The script builds the prompt from the issue **and** your committed config (Module 5), runs your The script builds the prompt from the issue **and** your committed config (Module 5), runs your
+1 -1
@@ -407,7 +407,7 @@ thing you're waiting on.
```bash ```bash
cd ~/ai-workflow-course/tasks-app cd ~/ai-workflow-course/tasks-app
python cli.py list && python cli.py count && python cli.py clear # all three features live python3 cli.py list && python3 cli.py count && python3 cli.py clear # all three features live
``` ```
If any of those three commands fails, the resolution was wrong. That's why you verify the result If any of those three commands fails, the resolution was wrong. That's why you verify the result
+6 -6
@@ -251,7 +251,7 @@ output.
```bash ```bash
cd modules/27-evals/lab cd modules/27-evals/lab
python run_eval.py candidates/current_model python3 run_eval.py candidates/current_model
echo "exit code: $?" echo "exit code: $?"
``` ```
@@ -264,7 +264,7 @@ output.
2. Now simulate the swap: run the *exact same eval set* against the other candidate: 2. Now simulate the swap: run the *exact same eval set* against the other candidate:
```bash ```bash
python run_eval.py candidates/swapped_model python3 run_eval.py candidates/swapped_model
echo "exit code: $?" echo "exit code: $?"
``` ```
@@ -281,7 +281,7 @@ output.
yourself and read the scorecard: yourself and read the scorecard:
```bash ```bash
python run_eval.py candidates/my_run_1 python3 run_eval.py candidates/my_run_1
``` ```
4. Now actually swap something. Either change the model Claude Code uses, or change the *prompt* (ask 4. Now actually swap something. Either change the model Claude Code uses, or change the *prompt* (ask
@@ -315,10 +315,10 @@ output.
`working-directory:` line makes the CI job `cd` into the lab folder first, so the `working-directory:` line makes the CI job `cd` into the lab folder first, so the
`candidates/...` path and `run_eval.py`'s own `from eval_set import CASES` resolve exactly as they `candidates/...` path and `run_eval.py`'s own `from eval_set import CASES` resolve exactly as they
did on your machine. (Drop it and point a repo-root job straight at did on your machine. (Drop it and point a repo-root job straight at
`python modules/27-evals/lab/run_eval.py candidates/current_model`, and `candidates/` `python3 modules/27-evals/lab/run_eval.py candidates/current_model`, and `candidates/`
won't exist from the repo root: the gate crashes with a *false* failure, which is worse than no won't exist from the repo root: the gate crashes with a *false* failure, which is worse than no
gate. If the agent prefers a single line, it can spell both paths out from the repo root: gate. If the agent prefers a single line, it can spell both paths out from the repo root:
`python modules/27-evals/lab/run_eval.py modules/27-evals/lab/candidates/current_model `python3 modules/27-evals/lab/run_eval.py modules/27-evals/lab/candidates/current_model
--threshold 1.0`.) --threshold 1.0`.)
Below threshold exits non-zero and the pipeline blocks, exactly like a failing test. The guardrail Below threshold exits non-zero and the pipeline blocks, exactly like a failing test. The guardrail
@@ -394,7 +394,7 @@ This is an expansion-zone module over fast-moving ground. Re-check at build/publ
- [ ] **Module cross-references.** Confirm Modules 13, 14, 10, and 2426 still carry the - [ ] **Module cross-references.** Confirm Modules 13, 14, 10, and 2426 still carry the
responsibilities referenced here (tests, CI gating, review, the agent autonomy ladder) and that responsibilities referenced here (tests, CI gating, review, the agent autonomy ladder) and that
none were renumbered. none were renumbered.
- [ ] **Lab still runs.** `python run_eval.py candidates/current_model` exits 0 at 100%, and - [ ] **Lab still runs.** `python3 run_eval.py candidates/current_model` exits 0 at 100%, and
`candidates/swapped_model` exits 1 below threshold, on a current Python 3.x. `candidates/swapped_model` exits 1 below threshold, on a current Python 3.x.
+5 -5
@@ -53,7 +53,7 @@ already standing; it doesn't re-pour the foundation.
Pick something small enough to finish in one sitting and real enough to touch the whole stack. We'll Pick something small enough to finish in one sitting and real enough to touch the whole stack. We'll
add **due dates**: add **due dates**:
- A task can carry an optional due date: `python cli.py add "file taxes" --due <YYYY-MM-DD>`. - A task can carry an optional due date: `python3 cli.py add "file taxes" --due <YYYY-MM-DD>`.
- A new `overdue` command lists pending tasks whose due date has already passed. - A new `overdue` command lists pending tasks whose due date has already passed.
- The deployed service grows a matching `GET /overdue` endpoint, so the change is visible in the - The deployed service grows a matching `GET /overdue` endpoint, so the change is visible in the
running container, not just the CLI. running container, not just the CLI.
@@ -190,9 +190,9 @@ agent), your forge account, and a working Docker install.
in the future, one safely in the past) so the assertion below holds whenever you run this: in the future, one safely in the past) so the assertion below holds whenever you run this:
```bash ```bash
python cli.py add "file taxes" --due <a date a few months out> # future → NOT overdue python3 cli.py add "file taxes" --due <a date a few months out> # future → NOT overdue
python cli.py add "renew domain" --due 2020-01-01 # past → overdue python3 cli.py add "renew domain" --due 2020-01-01 # past → overdue
python cli.py overdue # should list "renew domain", not "file taxes" python3 cli.py overdue # should list "renew domain", not "file taxes"
``` ```
> *Verify-before-publish: refresh the example due dates so the "future" one is still in the future > *Verify-before-publish: refresh the example due dates so the "future" one is still in the future
@@ -205,7 +205,7 @@ agent), your forge account, and a working Docker install.
them by name. Confirm the suite is green: them by name. Confirm the suite is green:
```bash ```bash
pytest # or: python -m unittest pytest # or: python3 -m unittest
``` ```
Once it's green, tell the AI to commit the change. Then verify what it actually staged and wrote: Once it's green, tell the AI to commit the change. Then verify what it actually staged and wrote: