feat(labs): make every lab a self-contained, skip-friendly starting point
CI / check (pull_request) Successful in 7s

Each lab now stands on its own; no hard dependency on prior labs.
- App-based labs get a canonical tasks-app snapshot in lab/start/ (three
  baselines: v0 add/list/done; v1 +count; v2 +count/delete), assigned by where
  each module sits in the command timeline. Modules with a purpose-built app
  (M10 trap, M13 planted bug, M21) snapshot their own app; planted devices kept.
- Self-contained labs (M15/17/18/19/22/23/24/25/27, which operate on their own
  lab files) get a preamble pointing at modules/NN/lab/.
- Every module + capstone gets a "Starting point (skip-friendly)" preamble:
  copy the snapshot, git init -b main, commit -> clean status, then start.

Lets a learner skip around or recover: copy start/, commit, go. All snapshots
run; tools/check.sh passes; no em-dashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT
This commit is contained in:
2026-06-23 18:24:17 -04:00
parent edf3f34336
commit 07182429c4
85 changed files with 2724 additions and 0 deletions
+11
View File
@@ -127,6 +127,17 @@ swappable part; the workflow is the durable skill*), and you just lived it inste
## Hands-on lab
> **Starting point (this lab is skip-friendly).** The capstone runs the whole loop on one feature.
> To begin from a clean app, copy the snapshot into a fresh `tasks-app` and make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/capstone/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: capstone"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell + Python, on the `tasks-app` repo. You'll direct Claude Code (`claude`; sub
your own agent) to do the git and the edits (M4); you make the calls and verify each result.
+25
View File
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
+62
View File
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
elif command == "delete":
tlist.remove(int(argv[1]))
save(tlist)
print("deleted")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
+42
View File
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def remove(self, index: int) -> None:
del self.tasks[index]
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -122,6 +122,18 @@ you already feel is the curriculum.
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/01-the-copy-paste-problem/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 1"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell + a tiny bit of Python (just enough to have something real to run). You will
not write Python; you'll run a small app we provide.
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,56 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,39 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -132,6 +132,18 @@ Everything above is standard Git. What's *specific* to AI-assisted work:
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/02-version-control-as-a-safety-net/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 2"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell (Git commands), on the `tasks-app` project from Module 1.
**You'll need:** Git installed (`git --version`; if it's missing, install from
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,56 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,39 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -193,6 +193,18 @@ Here's why this module is more than "learn Git on easy mode":
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/03-version-control-for-words/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 3"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell (Git commands) plus a little markdown writing, on the `tasks-app` from
Modules 12. The AI stays in the **browser**; you copy its draft into the file yourself, exactly as
in Module 2.
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,58 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,39 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -281,6 +281,18 @@ loop and the loop is unchanged.
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/04-getting-the-ai-out-of-the-browser/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 4"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell + a small Python change *made by the AI, not by you*. You'll drive an agentic
tool; the tool writes the Python.
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,58 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,39 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
+12
View File
@@ -195,6 +195,18 @@ Three things make this specifically an AI problem, not a generic config chore:
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/05-commit-the-ai-config/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 5"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell + markdown, on the `tasks-app` project from Modules 12. You'll use your
editor-integrated AI (Module 4) for the part where the AI obeys the file.
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
elif command == "delete":
tlist.remove(int(argv[1]))
save(tlist)
print("deleted")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def remove(self, index: int) -> None:
del self.tasks[index]
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -233,6 +233,18 @@ Everything above is standard Git. Here's why it matters *more* in an AI-assisted
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/06-branches-sandboxes-for-experiments/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 6"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell (Git commands), driving the `tasks-app` from Modules 12 with your
editor-integrated AI from Module 4.
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
elif command == "delete":
tlist.remove(int(argv[1]))
save(tlist)
print("deleted")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def remove(self, index: int) -> None:
del self.tasks[index]
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -213,6 +213,18 @@ to run two agents and watch them overwrite each other's work.
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/07-worktrees-running-agents-in-parallel/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 7"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**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
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
elif command == "delete":
tlist.remove(int(argv[1]))
save(tlist)
print("deleted")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def remove(self, index: int) -> None:
del self.tasks[index]
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
+12
View File
@@ -295,6 +295,18 @@ A remote isn't only about durability. It's what the AI parts of this course run
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/08-remotes-and-hosting/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 8"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell (Git commands), plus one short provided shell script. Runs on macOS, Linux,
WSL, or Git Bash on Windows. Continues the `tasks-app` repo from Module 2.
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
elif command == "delete":
tlist.remove(int(argv[1]))
save(tlist)
print("deleted")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def remove(self, index: int) -> None:
del self.tasks[index]
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -225,6 +225,18 @@ valuable, not less.
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/09-issues-and-the-task-layer/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 9"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** Markdown + shell, against the `tasks-app` repo you pushed to a forge in Module 8.
You'll draft issues as Markdown locally (so you can version and reuse the format), then have your
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
elif command == "delete":
tlist.remove(int(argv[1]))
save(tlist)
print("deleted")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def remove(self, index: int) -> None:
del self.tasks[index]
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -191,6 +191,18 @@ you couldn't do yourself.
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/10-reviewing-code-you-didnt-write/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 10"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell + the Python `tasks-app`. You won't write Python; you'll open a PR for a
real change, then review a diff the "AI" produced and catch the trap planted in it.
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
python cli.py done 0
State is kept in tasks.json next to this file. The `done` command turns a bad index into a
clean error message and a non-zero exit code; note that behavior before you review the AI
change, so you can tell if the change quietly alters it.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
try:
tlist.complete(int(argv[1]))
except IndexError as exc:
print(f"error: {exc}")
return 1
save(tlist)
print("updated")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Same running example as Modules 1 and 2, with one addition: `complete` now validates the
index and raises a clear error for a bad one. That explicit edge-case handling is here on
purpose; it's the kind of thing an AI "refactor" likes to quietly remove. This is the
known-good base you'll review an AI change against in Module 10.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
if not 0 <= index < len(self.tasks):
raise IndexError(f"no task at index {index}")
self.tasks[index].done = True
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -253,6 +253,18 @@ You're not learning collaboration *and then* learning to work with agents. They'
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/11-collaboration-humans-and-agents/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 11"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell plus your host's web UI for the issue, PR, review, and merge steps. From
Module 4 on you direct the AI to do the git work and verify the result; the only commands you type by
hand here are read-only checks like `git branch` and `git show`. You'll implement the feature with
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
elif command == "delete":
tlist.remove(int(argv[1]))
save(tlist)
print("deleted")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def remove(self, index: int) -> None:
del self.tasks[index]
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -221,6 +221,18 @@ Recovery was always a real skill. AI raises its value on every axis:
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/12-revert-reset-and-recovery/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 12"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell (Git commands), on the `tasks-app` from Modules 12.
You'll do the two scenarios that matter most: **revert a bad merge** that's already on `main`, then
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
elif command == "delete":
tlist.remove(int(argv[1]))
save(tlist)
print("deleted")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def remove(self, index: int) -> None:
del self.tasks[index]
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -202,6 +202,18 @@ Passing for the right reason is the skill.
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/13-testing-in-the-ai-era/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 13"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** Python (standard-library `unittest`), with a couple of shell commands to run the
suite. Nothing to install.
@@ -0,0 +1,25 @@
# Demo app: `tasks` (Module 13 copy)
The same tiny task tracker from Modules 1 and 2, with one feature added: a `count` command backed
by `TaskList.pending_count()`. Use this copy for the Module 13 lab so everyone starts from the same
code, including the same latent bug.
If you already have a `tasks-app` from earlier modules, you can use that instead; just make sure it
has a `count` command (the Module 2 lab added one). The planted bug in this copy is there on purpose.
## Files
- `tasks.py`: core logic (`Task`, `TaskList`), now with `pending_count()`.
- `cli.py`: command-line front end. Adds `count`.
## Run it
```bash
python cli.py add "write the tests"
python cli.py add "fix the bug"
python cli.py done 0
python cli.py list
python cli.py count
```
Requires Python 3.10+. No third-party packages; tests use the standard library `unittest`.
@@ -0,0 +1,59 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
python cli.py count
State is kept in tasks.json next to this file. Same minimal app from Modules 1 and 2, with a
`count` command bolted on.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{tlist.pending_count()} task(s) pending")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,43 @@
"""Core task logic for the demo app.
Same running example from Modules 1 and 2, carried forward. It has grown one feature since then:
a `pending_count()` helper that the AI added to back a `count` command. The feature "works" in
the obvious case, which is exactly the kind of code this module teaches you to verify properly.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def pending_count(self) -> int:
# Added by the AI to support `cli.py count`. Looks right, ran fine in a quick check.
return len(self.tasks)
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -205,6 +205,18 @@ the more you need a reviewer that checks behavior instead of believing the diff.
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/14-continuous-integration/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 14"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** YAML (the CI config) plus the Python `tasks-app` and shell commands. You direct
the agent to place files, commit, and recover; you commit a starter workflow, watch it pass, then
break it on purpose and watch CI catch it.
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
elif command == "delete":
tlist.remove(int(argv[1]))
save(tlist)
print("deleted")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def remove(self, index: int) -> None:
del self.tasks[index]
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
+9
View File
@@ -203,6 +203,15 @@ add them *despite* using AI; using AI is what moves them from "nice to have" to
## Hands-on lab
> **Starting point (this lab is skip-friendly).** This lab is self-contained and does not depend
> on the earlier labs. Its files live in `modules/15-security-scanning/lab/`. Copy them into a working folder
> and make a first commit so you start clean:
>
> ```bash
> cp -r ~/ai-workflow-course/modules/15-security-scanning/lab ~/ai-workflow-course/15-security-scanning-lab
> cd ~/ai-workflow-course/15-security-scanning-lab && git init -b main && git add -A && git commit -m "start: module 15"
> ```
**Lab language:** shell, driving Python tooling, on the `tasks-app` from Module 1. You'll install two
scanners (both pip-installable, cross-platform), let the AI introduce all three problems, catch them,
and wire the catch into your pipeline.
@@ -164,6 +164,18 @@ Docker itself you may already know. What makes containers matter *more* in AI-as
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/16-containers-and-reproducible-environments/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 16"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell (Docker CLI) on the `tasks-app` from Module 1. You won't write Python; you'll
containerize and run the app you already have.
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
elif command == "delete":
tlist.remove(int(argv[1]))
save(tlist)
print("deleted")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def remove(self, index: int) -> None:
del self.tasks[index]
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -277,6 +277,15 @@ model will keep offering it. Concretely:
## Hands-on lab
> **Starting point (this lab is skip-friendly).** This lab is self-contained and does not depend
> on the earlier labs. Its files live in `modules/17-secrets-config-and-environments/lab/`. Copy them into a working folder
> and make a first commit so you start clean:
>
> ```bash
> cp -r ~/ai-workflow-course/modules/17-secrets-config-and-environments/lab ~/ai-workflow-course/17-secrets-config-and-environments-lab
> cd ~/ai-workflow-course/17-secrets-config-and-environments-lab && git init -b main && git add -A && git commit -m "start: module 17"
> ```
**Lab language:** Python + shell, on a new `sync` feature for the `tasks-app` from Module 1.
You'll take a file that hardcodes a secret (the exact thing an AI hands you) and refactor it so the
@@ -199,6 +199,15 @@ between an autonomous contributor and your users.
## Hands-on lab
> **Starting point (this lab is skip-friendly).** This lab is self-contained and does not depend
> on the earlier labs. Its files live in `modules/18-continuous-delivery-and-deployment/lab/`. Copy them into a working folder
> and make a first commit so you start clean:
>
> ```bash
> cp -r ~/ai-workflow-course/modules/18-continuous-delivery-and-deployment/lab ~/ai-workflow-course/18-continuous-delivery-and-deployment-lab
> cd ~/ai-workflow-course/18-continuous-delivery-and-deployment-lab && git init -b main && git add -A && git commit -m "start: module 18"
> ```
**Lab language:** shell, driving the container tooling from Module 16. You'll extend the `tasks-app`
into a tiny running service, then build a deploy script that ships it locally with a health check and
automatic rollback, the whole CD motion simulated on your own machine.
@@ -202,6 +202,15 @@ review reflex from Module 10 has to extend to the workflow files, not just the a
## Hands-on lab
> **Starting point (this lab is skip-friendly).** This lab is self-contained and does not depend
> on the earlier labs. Its files live in `modules/19-runners-the-compute-behind-automation/lab/`. Copy them into a working folder
> and make a first commit so you start clean:
>
> ```bash
> cp -r ~/ai-workflow-course/modules/19-runners-the-compute-behind-automation/lab ~/ai-workflow-course/19-runners-the-compute-behind-automation-lab
> cd ~/ai-workflow-course/19-runners-the-compute-behind-automation-lab && git init -b main && git add -A && git commit -m "start: module 19"
> ```
**Lab language:** shell, plus a one-line edit to the YAML workflow from Module 14. Runs on your own
machine and your own forge, with no hosted account required for the core of it.
@@ -225,6 +225,18 @@ That changes what matters about the integration.
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/20-mcp-servers-giving-the-ai-hands/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 20"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** Python (a ~15-line MCP server) plus your agentic tool's config. Runs on your own
machine, any OS.
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
elif command == "delete":
tlist.remove(int(argv[1]))
save(tlist)
print("deleted")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def remove(self, index: int) -> None:
del self.tasks[index]
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -160,6 +160,18 @@ On paper this is just "write a runbook." The AI-specific twist is what changes t
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/21-skills-teaching-the-ai-your-playbook/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 21"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** markdown (the skill file) plus shell and Python (the `tasks-app`). You'll write a
skill, then have your editor-integrated AI (Module 4) execute it.
@@ -0,0 +1,9 @@
# Changelog
Newest entries on top. One line per user-visible change.
## Unreleased
- Add `count` command: print how many tasks are still pending.
- Add `done <index>` command: mark a task complete.
- Initial CLI: `add` and `list`.
@@ -0,0 +1,59 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
python cli.py count
State is kept in tasks.json next to this file. The same minimal app from Module 1 onward; the
target your "add a command" skill extends.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{tlist.pending_count()} task(s) pending")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,41 @@
"""Core task logic for the demo app.
The same running example from Module 1 onward, carried forward with the `pending_count()` helper
that backs the `count` command. This is the codebase your "add a command" skill operates on.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def pending_count(self) -> int:
return len([t for t in self.tasks if not t.done])
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
@@ -0,0 +1,44 @@
"""Test suite for the tasks-app. Run from this folder with:
python -m unittest
Your "add a command" skill should ADD a test here for every new command. The point is to assert
intended behavior, not just that nothing crashed.
"""
import unittest
from tasks import TaskList
class TestTaskBasics(unittest.TestCase):
def test_add_appends_a_task(self):
tl = TaskList()
tl.add("write the skill")
self.assertEqual(len(tl.tasks), 1)
self.assertEqual(tl.tasks[0].title, "write the skill")
self.assertFalse(tl.tasks[0].done)
def test_complete_marks_done(self):
tl = TaskList()
tl.add("a")
tl.complete(0)
self.assertTrue(tl.tasks[0].done)
def test_pending_excludes_completed(self):
tl = TaskList()
tl.add("a")
tl.add("b")
tl.complete(0)
self.assertEqual([t.title for t in tl.pending()], ["b"])
def test_pending_count_ignores_done(self):
tl = TaskList()
tl.add("a")
tl.add("b")
tl.complete(0)
self.assertEqual(tl.pending_count(), 1)
if __name__ == "__main__":
unittest.main()
@@ -195,6 +195,15 @@ skills different from any dependency you've shipped before:
## Hands-on lab
> **Starting point (this lab is skip-friendly).** This lab is self-contained and does not depend
> on the earlier labs. Its files live in `modules/22-securing-third-party-mcp-and-skills/lab/`. Copy them into a working folder
> and make a first commit so you start clean:
>
> ```bash
> cp -r ~/ai-workflow-course/modules/22-securing-third-party-mcp-and-skills/lab ~/ai-workflow-course/22-securing-third-party-mcp-and-skills-lab
> cd ~/ai-workflow-course/22-securing-third-party-mcp-and-skills-lab && git init -b main && git add -A && git commit -m "start: module 22"
> ```
**Lab language:** shell, with a small Python file to read. You'll audit a deliberately sketchy
third-party skill, run a static red-flag scan over it, then reproduce a prompt-injection attack
against the Module 1 `tasks-app` and apply the least-privilege mitigation.
@@ -162,6 +162,15 @@ into a revertable diff.
## Hands-on lab
> **Starting point (this lab is skip-friendly).** This lab is self-contained and does not depend
> on the earlier labs. Its files live in `modules/23-working-with-existing-codebases/lab/`. Copy them into a working folder
> and make a first commit so you start clean:
>
> ```bash
> cp -r ~/ai-workflow-course/modules/23-working-with-existing-codebases/lab ~/ai-workflow-course/23-working-with-existing-codebases-lab
> cd ~/ai-workflow-course/23-working-with-existing-codebases-lab && git init -b main && git add -A && git commit -m "start: module 23"
> ```
**Lab language:** shell + the provided Python script (`orient.py`); you run it, you don't write it.
This lab does **not** use `tasks-app`; the entire point is a codebase you *didn't* write.
+9
View File
@@ -167,6 +167,15 @@ occasionally wrong with no consequences.
## Hands-on lab
> **Starting point (this lab is skip-friendly).** This lab is self-contained and does not depend
> on the earlier labs. Its files live in `modules/24-assistive-agents/lab/`. Copy them into a working folder
> and make a first commit so you start clean:
>
> ```bash
> cp -r ~/ai-workflow-course/modules/24-assistive-agents/lab ~/ai-workflow-course/24-assistive-agents-lab
> cd ~/ai-workflow-course/24-assistive-agents-lab && git init -b main && git add -A && git commit -m "start: module 24"
> ```
**Lab language:** Python (two small stdlib-only scripts) driven by Claude Code (`claude`; sub your
own agent). No `pip install`, no hosted account. The scripts do the deterministic halves (assemble
the prompt, validate and render the response, present the decision gate); the model does the one part
+9
View File
@@ -202,6 +202,15 @@ the job is non-deterministic and persuasive**, and that changes what "automation
## Hands-on lab
> **Starting point (this lab is skip-friendly).** This lab is self-contained and does not depend
> on the earlier labs. Its files live in `modules/25-autonomous-agents/lab/`. Copy them into a working folder
> and make a first commit so you start clean:
>
> ```bash
> cp -r ~/ai-workflow-course/modules/25-autonomous-agents/lab ~/ai-workflow-course/25-autonomous-agents-lab
> cd ~/ai-workflow-course/25-autonomous-agents-lab && git init -b main && git add -A && git commit -m "start: module 25"
> ```
**Lab language:** Python (one orchestrator script) plus a little shell and Git. It runs on your own
machine, any OS, against the `tasks-app` repo from Module 1, with no forge account or paid agent
required to complete it.
@@ -254,6 +254,18 @@ imaginary, and that the fix was a ten-minute coordination plan you skipped.
## Hands-on lab
> **Starting point (this lab is skip-friendly).** You do not need to have done the earlier labs.
> To begin from a clean, known state, copy this module's snapshot into a fresh `tasks-app` and
> make the first commit:
>
> ```bash
> mkdir -p ~/ai-workflow-course/tasks-app
> cp -r ~/ai-workflow-course/modules/26-orchestrating-multiple-agents/lab/start/. ~/ai-workflow-course/tasks-app/
> cd ~/ai-workflow-course/tasks-app && git init -b main && git add -A && git commit -m "start: module 26"
> ```
>
> Already carrying your `tasks-app` from earlier modules? Keep using it and ignore this box.
**Lab language:** shell (Git + a couple of helper scripts) driving multiple AI edit sessions on the
`tasks-app`, integrated through PRs.
@@ -0,0 +1,25 @@
# Demo app: `tasks`
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
enough to read in a minute but real enough to have more than one file, which is exactly where the
copy-paste workflow starts to hurt.
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
(where you put it under version control).
## Files
- `tasks.py`: the core logic (`Task`, `TaskList`).
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
## Run it
```bash
python cli.py add "read module 1"
python cli.py add "set up my editor"
python cli.py list
python cli.py done 0
python cli.py list
```
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
@@ -0,0 +1,62 @@
"""Tiny command-line front end for the demo task app.
Run it:
python cli.py add "write the lesson"
python cli.py list
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
is to be a realistic-but-small thing you change with an AI, not a product.
"""
import json
import sys
from pathlib import Path
from tasks import Task, TaskList
STATE = Path(__file__).parent / "tasks.json"
def load() -> TaskList:
if not STATE.exists():
return TaskList()
raw = json.loads(STATE.read_text())
return TaskList(tasks=[Task(**t) for t in raw])
def save(tlist: TaskList) -> None:
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
return 1
command = argv[0]
if command == "add":
title = " ".join(argv[1:])
tlist.add(title)
save(tlist)
print(f"added: {title}")
elif command == "list":
print(tlist.render())
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
elif command == "count":
print(f"{len(tlist.pending())} pending")
elif command == "delete":
tlist.remove(int(argv[1]))
save(tlist)
print("deleted")
else:
print(f"unknown command: {command}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
"""Core task logic for the demo app.
Deliberately small and deliberately split across two files (this and cli.py) so that the
copy-paste workflow has more than one place to go wrong. This is the running example used in
Modules 1 and 2.
"""
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
@dataclass
class TaskList:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str) -> Task:
task = Task(title=title)
self.tasks.append(task)
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
def remove(self, index: int) -> None:
del self.tasks[index]
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
def render(self) -> str:
if not self.tasks:
return "(no tasks yet)"
lines = []
for i, task in enumerate(self.tasks):
box = "[x]" if task.done else "[ ]"
lines.append(f"{i}. {box} {task.title}")
return "\n".join(lines)
+9
View File
@@ -212,6 +212,15 @@ That's the durable skill. Models are weather. The eval set is the thermometer yo
## Hands-on lab
> **Starting point (this lab is skip-friendly).** This lab is self-contained and does not depend
> on the earlier labs. Its files live in `modules/27-evals/lab/`. Copy them into a working folder
> and make a first commit so you start clean:
>
> ```bash
> cp -r ~/ai-workflow-course/modules/27-evals/lab ~/ai-workflow-course/27-evals-lab
> cd ~/ai-workflow-course/27-evals-lab && git init -b main && git add -A && git commit -m "start: module 27"
> ```
**Lab language:** Python + shell. You'll run a tiny eval harness, point an agent at a task, and run
a regression eval across a "model swap."