From 07182429c4fc90c9b52a7684471442cbe476f2f4 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 23 Jun 2026 18:24:17 -0400 Subject: [PATCH] feat(labs): make every lab a self-contained, skip-friendly starting point 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) Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT --- capstone/README.md | 11 ++++ capstone/lab/start/README.md | 25 ++++++++ capstone/lab/start/cli.py | 62 +++++++++++++++++++ capstone/lab/start/tasks.py | 42 +++++++++++++ modules/01-the-copy-paste-problem/README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 56 +++++++++++++++++ .../lab/start/tasks.py | 39 ++++++++++++ .../README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 56 +++++++++++++++++ .../lab/start/tasks.py | 39 ++++++++++++ .../03-version-control-for-words/README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 58 +++++++++++++++++ .../lab/start/tasks.py | 39 ++++++++++++ .../README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 58 +++++++++++++++++ .../lab/start/tasks.py | 39 ++++++++++++ modules/05-commit-the-ai-config/README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../05-commit-the-ai-config/lab/start/cli.py | 62 +++++++++++++++++++ .../lab/start/tasks.py | 42 +++++++++++++ .../README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 62 +++++++++++++++++++ .../lab/start/tasks.py | 42 +++++++++++++ .../README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 62 +++++++++++++++++++ .../lab/start/tasks.py | 42 +++++++++++++ modules/08-remotes-and-hosting/README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../08-remotes-and-hosting/lab/start/cli.py | 62 +++++++++++++++++++ .../08-remotes-and-hosting/lab/start/tasks.py | 42 +++++++++++++ .../09-issues-and-the-task-layer/README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 62 +++++++++++++++++++ .../lab/start/tasks.py | 42 +++++++++++++ .../README.md | 12 ++++ .../lab/start/cli.py | 62 +++++++++++++++++++ .../lab/start/tasks.py | 42 +++++++++++++ .../README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 62 +++++++++++++++++++ .../lab/start/tasks.py | 42 +++++++++++++ .../12-revert-reset-and-recovery/README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 62 +++++++++++++++++++ .../lab/start/tasks.py | 42 +++++++++++++ modules/13-testing-in-the-ai-era/README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../13-testing-in-the-ai-era/lab/start/cli.py | 59 ++++++++++++++++++ .../lab/start/tasks.py | 43 +++++++++++++ modules/14-continuous-integration/README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 62 +++++++++++++++++++ .../lab/start/tasks.py | 42 +++++++++++++ modules/15-security-scanning/README.md | 9 +++ .../README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 62 +++++++++++++++++++ .../lab/start/tasks.py | 42 +++++++++++++ .../README.md | 9 +++ .../README.md | 9 +++ .../README.md | 9 +++ .../README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 62 +++++++++++++++++++ .../lab/start/tasks.py | 42 +++++++++++++ .../README.md | 12 ++++ .../lab/start/CHANGELOG.md | 9 +++ .../lab/start/cli.py | 59 ++++++++++++++++++ .../lab/start/tasks.py | 41 ++++++++++++ .../lab/start/test_tasks.py | 44 +++++++++++++ .../README.md | 9 +++ .../README.md | 9 +++ modules/24-assistive-agents/README.md | 9 +++ modules/25-autonomous-agents/README.md | 9 +++ .../README.md | 12 ++++ .../lab/start/README.md | 25 ++++++++ .../lab/start/cli.py | 62 +++++++++++++++++++ .../lab/start/tasks.py | 42 +++++++++++++ modules/27-evals/README.md | 9 +++ 85 files changed, 2724 insertions(+) create mode 100644 capstone/lab/start/README.md create mode 100644 capstone/lab/start/cli.py create mode 100644 capstone/lab/start/tasks.py create mode 100644 modules/01-the-copy-paste-problem/lab/start/README.md create mode 100644 modules/01-the-copy-paste-problem/lab/start/cli.py create mode 100644 modules/01-the-copy-paste-problem/lab/start/tasks.py create mode 100644 modules/02-version-control-as-a-safety-net/lab/start/README.md create mode 100644 modules/02-version-control-as-a-safety-net/lab/start/cli.py create mode 100644 modules/02-version-control-as-a-safety-net/lab/start/tasks.py create mode 100644 modules/03-version-control-for-words/lab/start/README.md create mode 100644 modules/03-version-control-for-words/lab/start/cli.py create mode 100644 modules/03-version-control-for-words/lab/start/tasks.py create mode 100644 modules/04-getting-the-ai-out-of-the-browser/lab/start/README.md create mode 100644 modules/04-getting-the-ai-out-of-the-browser/lab/start/cli.py create mode 100644 modules/04-getting-the-ai-out-of-the-browser/lab/start/tasks.py create mode 100644 modules/05-commit-the-ai-config/lab/start/README.md create mode 100644 modules/05-commit-the-ai-config/lab/start/cli.py create mode 100644 modules/05-commit-the-ai-config/lab/start/tasks.py create mode 100644 modules/06-branches-sandboxes-for-experiments/lab/start/README.md create mode 100644 modules/06-branches-sandboxes-for-experiments/lab/start/cli.py create mode 100644 modules/06-branches-sandboxes-for-experiments/lab/start/tasks.py create mode 100644 modules/07-worktrees-running-agents-in-parallel/lab/start/README.md create mode 100644 modules/07-worktrees-running-agents-in-parallel/lab/start/cli.py create mode 100644 modules/07-worktrees-running-agents-in-parallel/lab/start/tasks.py create mode 100644 modules/08-remotes-and-hosting/lab/start/README.md create mode 100644 modules/08-remotes-and-hosting/lab/start/cli.py create mode 100644 modules/08-remotes-and-hosting/lab/start/tasks.py create mode 100644 modules/09-issues-and-the-task-layer/lab/start/README.md create mode 100644 modules/09-issues-and-the-task-layer/lab/start/cli.py create mode 100644 modules/09-issues-and-the-task-layer/lab/start/tasks.py create mode 100644 modules/10-reviewing-code-you-didnt-write/lab/start/cli.py create mode 100644 modules/10-reviewing-code-you-didnt-write/lab/start/tasks.py create mode 100644 modules/11-collaboration-humans-and-agents/lab/start/README.md create mode 100644 modules/11-collaboration-humans-and-agents/lab/start/cli.py create mode 100644 modules/11-collaboration-humans-and-agents/lab/start/tasks.py create mode 100644 modules/12-revert-reset-and-recovery/lab/start/README.md create mode 100644 modules/12-revert-reset-and-recovery/lab/start/cli.py create mode 100644 modules/12-revert-reset-and-recovery/lab/start/tasks.py create mode 100644 modules/13-testing-in-the-ai-era/lab/start/README.md create mode 100644 modules/13-testing-in-the-ai-era/lab/start/cli.py create mode 100644 modules/13-testing-in-the-ai-era/lab/start/tasks.py create mode 100644 modules/14-continuous-integration/lab/start/README.md create mode 100644 modules/14-continuous-integration/lab/start/cli.py create mode 100644 modules/14-continuous-integration/lab/start/tasks.py create mode 100644 modules/16-containers-and-reproducible-environments/lab/start/README.md create mode 100644 modules/16-containers-and-reproducible-environments/lab/start/cli.py create mode 100644 modules/16-containers-and-reproducible-environments/lab/start/tasks.py create mode 100644 modules/20-mcp-servers-giving-the-ai-hands/lab/start/README.md create mode 100644 modules/20-mcp-servers-giving-the-ai-hands/lab/start/cli.py create mode 100644 modules/20-mcp-servers-giving-the-ai-hands/lab/start/tasks.py create mode 100644 modules/21-skills-teaching-the-ai-your-playbook/lab/start/CHANGELOG.md create mode 100644 modules/21-skills-teaching-the-ai-your-playbook/lab/start/cli.py create mode 100644 modules/21-skills-teaching-the-ai-your-playbook/lab/start/tasks.py create mode 100644 modules/21-skills-teaching-the-ai-your-playbook/lab/start/test_tasks.py create mode 100644 modules/26-orchestrating-multiple-agents/lab/start/README.md create mode 100644 modules/26-orchestrating-multiple-agents/lab/start/cli.py create mode 100644 modules/26-orchestrating-multiple-agents/lab/start/tasks.py diff --git a/capstone/README.md b/capstone/README.md index 9d48256..615bc4c 100644 --- a/capstone/README.md +++ b/capstone/README.md @@ -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. diff --git a/capstone/lab/start/README.md b/capstone/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/capstone/lab/start/README.md @@ -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. diff --git a/capstone/lab/start/cli.py b/capstone/lab/start/cli.py new file mode 100644 index 0000000..91799e9 --- /dev/null +++ b/capstone/lab/start/cli.py @@ -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 | 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:])) diff --git a/capstone/lab/start/tasks.py b/capstone/lab/start/tasks.py new file mode 100644 index 0000000..3be57d9 --- /dev/null +++ b/capstone/lab/start/tasks.py @@ -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) diff --git a/modules/01-the-copy-paste-problem/README.md b/modules/01-the-copy-paste-problem/README.md index bfcf43e..ed89ccd 100644 --- a/modules/01-the-copy-paste-problem/README.md +++ b/modules/01-the-copy-paste-problem/README.md @@ -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. diff --git a/modules/01-the-copy-paste-problem/lab/start/README.md b/modules/01-the-copy-paste-problem/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/01-the-copy-paste-problem/lab/start/README.md @@ -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. diff --git a/modules/01-the-copy-paste-problem/lab/start/cli.py b/modules/01-the-copy-paste-problem/lab/start/cli.py new file mode 100644 index 0000000..36a7af8 --- /dev/null +++ b/modules/01-the-copy-paste-problem/lab/start/cli.py @@ -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:])) diff --git a/modules/01-the-copy-paste-problem/lab/start/tasks.py b/modules/01-the-copy-paste-problem/lab/start/tasks.py new file mode 100644 index 0000000..6913314 --- /dev/null +++ b/modules/01-the-copy-paste-problem/lab/start/tasks.py @@ -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) diff --git a/modules/02-version-control-as-a-safety-net/README.md b/modules/02-version-control-as-a-safety-net/README.md index 4078474..d2639a1 100644 --- a/modules/02-version-control-as-a-safety-net/README.md +++ b/modules/02-version-control-as-a-safety-net/README.md @@ -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 diff --git a/modules/02-version-control-as-a-safety-net/lab/start/README.md b/modules/02-version-control-as-a-safety-net/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/02-version-control-as-a-safety-net/lab/start/README.md @@ -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. diff --git a/modules/02-version-control-as-a-safety-net/lab/start/cli.py b/modules/02-version-control-as-a-safety-net/lab/start/cli.py new file mode 100644 index 0000000..36a7af8 --- /dev/null +++ b/modules/02-version-control-as-a-safety-net/lab/start/cli.py @@ -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:])) diff --git a/modules/02-version-control-as-a-safety-net/lab/start/tasks.py b/modules/02-version-control-as-a-safety-net/lab/start/tasks.py new file mode 100644 index 0000000..6913314 --- /dev/null +++ b/modules/02-version-control-as-a-safety-net/lab/start/tasks.py @@ -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) diff --git a/modules/03-version-control-for-words/README.md b/modules/03-version-control-for-words/README.md index 1bb7c4f..27123fe 100644 --- a/modules/03-version-control-for-words/README.md +++ b/modules/03-version-control-for-words/README.md @@ -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 1–2. The AI stays in the **browser**; you copy its draft into the file yourself, exactly as in Module 2. diff --git a/modules/03-version-control-for-words/lab/start/README.md b/modules/03-version-control-for-words/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/03-version-control-for-words/lab/start/README.md @@ -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. diff --git a/modules/03-version-control-for-words/lab/start/cli.py b/modules/03-version-control-for-words/lab/start/cli.py new file mode 100644 index 0000000..7dd143c --- /dev/null +++ b/modules/03-version-control-for-words/lab/start/cli.py @@ -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:])) diff --git a/modules/03-version-control-for-words/lab/start/tasks.py b/modules/03-version-control-for-words/lab/start/tasks.py new file mode 100644 index 0000000..6913314 --- /dev/null +++ b/modules/03-version-control-for-words/lab/start/tasks.py @@ -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) diff --git a/modules/04-getting-the-ai-out-of-the-browser/README.md b/modules/04-getting-the-ai-out-of-the-browser/README.md index 3e1a6f8..024be38 100644 --- a/modules/04-getting-the-ai-out-of-the-browser/README.md +++ b/modules/04-getting-the-ai-out-of-the-browser/README.md @@ -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. diff --git a/modules/04-getting-the-ai-out-of-the-browser/lab/start/README.md b/modules/04-getting-the-ai-out-of-the-browser/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/04-getting-the-ai-out-of-the-browser/lab/start/README.md @@ -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. diff --git a/modules/04-getting-the-ai-out-of-the-browser/lab/start/cli.py b/modules/04-getting-the-ai-out-of-the-browser/lab/start/cli.py new file mode 100644 index 0000000..7dd143c --- /dev/null +++ b/modules/04-getting-the-ai-out-of-the-browser/lab/start/cli.py @@ -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:])) diff --git a/modules/04-getting-the-ai-out-of-the-browser/lab/start/tasks.py b/modules/04-getting-the-ai-out-of-the-browser/lab/start/tasks.py new file mode 100644 index 0000000..6913314 --- /dev/null +++ b/modules/04-getting-the-ai-out-of-the-browser/lab/start/tasks.py @@ -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) diff --git a/modules/05-commit-the-ai-config/README.md b/modules/05-commit-the-ai-config/README.md index 3891148..e9876b0 100644 --- a/modules/05-commit-the-ai-config/README.md +++ b/modules/05-commit-the-ai-config/README.md @@ -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 1–2. You'll use your editor-integrated AI (Module 4) for the part where the AI obeys the file. diff --git a/modules/05-commit-the-ai-config/lab/start/README.md b/modules/05-commit-the-ai-config/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/05-commit-the-ai-config/lab/start/README.md @@ -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. diff --git a/modules/05-commit-the-ai-config/lab/start/cli.py b/modules/05-commit-the-ai-config/lab/start/cli.py new file mode 100644 index 0000000..91799e9 --- /dev/null +++ b/modules/05-commit-the-ai-config/lab/start/cli.py @@ -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:])) diff --git a/modules/05-commit-the-ai-config/lab/start/tasks.py b/modules/05-commit-the-ai-config/lab/start/tasks.py new file mode 100644 index 0000000..3be57d9 --- /dev/null +++ b/modules/05-commit-the-ai-config/lab/start/tasks.py @@ -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) diff --git a/modules/06-branches-sandboxes-for-experiments/README.md b/modules/06-branches-sandboxes-for-experiments/README.md index 2b5db71..936e0f2 100644 --- a/modules/06-branches-sandboxes-for-experiments/README.md +++ b/modules/06-branches-sandboxes-for-experiments/README.md @@ -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 1–2 with your editor-integrated AI from Module 4. diff --git a/modules/06-branches-sandboxes-for-experiments/lab/start/README.md b/modules/06-branches-sandboxes-for-experiments/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/06-branches-sandboxes-for-experiments/lab/start/README.md @@ -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. diff --git a/modules/06-branches-sandboxes-for-experiments/lab/start/cli.py b/modules/06-branches-sandboxes-for-experiments/lab/start/cli.py new file mode 100644 index 0000000..91799e9 --- /dev/null +++ b/modules/06-branches-sandboxes-for-experiments/lab/start/cli.py @@ -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:])) diff --git a/modules/06-branches-sandboxes-for-experiments/lab/start/tasks.py b/modules/06-branches-sandboxes-for-experiments/lab/start/tasks.py new file mode 100644 index 0000000..3be57d9 --- /dev/null +++ b/modules/06-branches-sandboxes-for-experiments/lab/start/tasks.py @@ -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) diff --git a/modules/07-worktrees-running-agents-in-parallel/README.md b/modules/07-worktrees-running-agents-in-parallel/README.md index 6d942b2..71c517b 100644 --- a/modules/07-worktrees-running-agents-in-parallel/README.md +++ b/modules/07-worktrees-running-agents-in-parallel/README.md @@ -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 diff --git a/modules/07-worktrees-running-agents-in-parallel/lab/start/README.md b/modules/07-worktrees-running-agents-in-parallel/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/07-worktrees-running-agents-in-parallel/lab/start/README.md @@ -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. diff --git a/modules/07-worktrees-running-agents-in-parallel/lab/start/cli.py b/modules/07-worktrees-running-agents-in-parallel/lab/start/cli.py new file mode 100644 index 0000000..91799e9 --- /dev/null +++ b/modules/07-worktrees-running-agents-in-parallel/lab/start/cli.py @@ -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:])) diff --git a/modules/07-worktrees-running-agents-in-parallel/lab/start/tasks.py b/modules/07-worktrees-running-agents-in-parallel/lab/start/tasks.py new file mode 100644 index 0000000..3be57d9 --- /dev/null +++ b/modules/07-worktrees-running-agents-in-parallel/lab/start/tasks.py @@ -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) diff --git a/modules/08-remotes-and-hosting/README.md b/modules/08-remotes-and-hosting/README.md index be0e11b..24b33d8 100644 --- a/modules/08-remotes-and-hosting/README.md +++ b/modules/08-remotes-and-hosting/README.md @@ -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. diff --git a/modules/08-remotes-and-hosting/lab/start/README.md b/modules/08-remotes-and-hosting/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/08-remotes-and-hosting/lab/start/README.md @@ -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. diff --git a/modules/08-remotes-and-hosting/lab/start/cli.py b/modules/08-remotes-and-hosting/lab/start/cli.py new file mode 100644 index 0000000..91799e9 --- /dev/null +++ b/modules/08-remotes-and-hosting/lab/start/cli.py @@ -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:])) diff --git a/modules/08-remotes-and-hosting/lab/start/tasks.py b/modules/08-remotes-and-hosting/lab/start/tasks.py new file mode 100644 index 0000000..3be57d9 --- /dev/null +++ b/modules/08-remotes-and-hosting/lab/start/tasks.py @@ -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) diff --git a/modules/09-issues-and-the-task-layer/README.md b/modules/09-issues-and-the-task-layer/README.md index b1fdf25..2607962 100644 --- a/modules/09-issues-and-the-task-layer/README.md +++ b/modules/09-issues-and-the-task-layer/README.md @@ -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 diff --git a/modules/09-issues-and-the-task-layer/lab/start/README.md b/modules/09-issues-and-the-task-layer/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/09-issues-and-the-task-layer/lab/start/README.md @@ -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. diff --git a/modules/09-issues-and-the-task-layer/lab/start/cli.py b/modules/09-issues-and-the-task-layer/lab/start/cli.py new file mode 100644 index 0000000..91799e9 --- /dev/null +++ b/modules/09-issues-and-the-task-layer/lab/start/cli.py @@ -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:])) diff --git a/modules/09-issues-and-the-task-layer/lab/start/tasks.py b/modules/09-issues-and-the-task-layer/lab/start/tasks.py new file mode 100644 index 0000000..3be57d9 --- /dev/null +++ b/modules/09-issues-and-the-task-layer/lab/start/tasks.py @@ -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) diff --git a/modules/10-reviewing-code-you-didnt-write/README.md b/modules/10-reviewing-code-you-didnt-write/README.md index b948e9e..f0c75e6 100644 --- a/modules/10-reviewing-code-you-didnt-write/README.md +++ b/modules/10-reviewing-code-you-didnt-write/README.md @@ -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. diff --git a/modules/10-reviewing-code-you-didnt-write/lab/start/cli.py b/modules/10-reviewing-code-you-didnt-write/lab/start/cli.py new file mode 100644 index 0000000..72aea21 --- /dev/null +++ b/modules/10-reviewing-code-you-didnt-write/lab/start/cli.py @@ -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:])) diff --git a/modules/10-reviewing-code-you-didnt-write/lab/start/tasks.py b/modules/10-reviewing-code-you-didnt-write/lab/start/tasks.py new file mode 100644 index 0000000..dd6d93b --- /dev/null +++ b/modules/10-reviewing-code-you-didnt-write/lab/start/tasks.py @@ -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) diff --git a/modules/11-collaboration-humans-and-agents/README.md b/modules/11-collaboration-humans-and-agents/README.md index e72859f..fd84e87 100644 --- a/modules/11-collaboration-humans-and-agents/README.md +++ b/modules/11-collaboration-humans-and-agents/README.md @@ -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 diff --git a/modules/11-collaboration-humans-and-agents/lab/start/README.md b/modules/11-collaboration-humans-and-agents/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/11-collaboration-humans-and-agents/lab/start/README.md @@ -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. diff --git a/modules/11-collaboration-humans-and-agents/lab/start/cli.py b/modules/11-collaboration-humans-and-agents/lab/start/cli.py new file mode 100644 index 0000000..91799e9 --- /dev/null +++ b/modules/11-collaboration-humans-and-agents/lab/start/cli.py @@ -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:])) diff --git a/modules/11-collaboration-humans-and-agents/lab/start/tasks.py b/modules/11-collaboration-humans-and-agents/lab/start/tasks.py new file mode 100644 index 0000000..3be57d9 --- /dev/null +++ b/modules/11-collaboration-humans-and-agents/lab/start/tasks.py @@ -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) diff --git a/modules/12-revert-reset-and-recovery/README.md b/modules/12-revert-reset-and-recovery/README.md index 11a482b..aa5055b 100644 --- a/modules/12-revert-reset-and-recovery/README.md +++ b/modules/12-revert-reset-and-recovery/README.md @@ -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 1–2. You'll do the two scenarios that matter most: **revert a bad merge** that's already on `main`, then diff --git a/modules/12-revert-reset-and-recovery/lab/start/README.md b/modules/12-revert-reset-and-recovery/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/12-revert-reset-and-recovery/lab/start/README.md @@ -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. diff --git a/modules/12-revert-reset-and-recovery/lab/start/cli.py b/modules/12-revert-reset-and-recovery/lab/start/cli.py new file mode 100644 index 0000000..91799e9 --- /dev/null +++ b/modules/12-revert-reset-and-recovery/lab/start/cli.py @@ -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:])) diff --git a/modules/12-revert-reset-and-recovery/lab/start/tasks.py b/modules/12-revert-reset-and-recovery/lab/start/tasks.py new file mode 100644 index 0000000..3be57d9 --- /dev/null +++ b/modules/12-revert-reset-and-recovery/lab/start/tasks.py @@ -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) diff --git a/modules/13-testing-in-the-ai-era/README.md b/modules/13-testing-in-the-ai-era/README.md index c32e923..f5bf42d 100644 --- a/modules/13-testing-in-the-ai-era/README.md +++ b/modules/13-testing-in-the-ai-era/README.md @@ -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. diff --git a/modules/13-testing-in-the-ai-era/lab/start/README.md b/modules/13-testing-in-the-ai-era/lab/start/README.md new file mode 100644 index 0000000..d031a55 --- /dev/null +++ b/modules/13-testing-in-the-ai-era/lab/start/README.md @@ -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`. diff --git a/modules/13-testing-in-the-ai-era/lab/start/cli.py b/modules/13-testing-in-the-ai-era/lab/start/cli.py new file mode 100644 index 0000000..0b0df68 --- /dev/null +++ b/modules/13-testing-in-the-ai-era/lab/start/cli.py @@ -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:])) diff --git a/modules/13-testing-in-the-ai-era/lab/start/tasks.py b/modules/13-testing-in-the-ai-era/lab/start/tasks.py new file mode 100644 index 0000000..82daeb0 --- /dev/null +++ b/modules/13-testing-in-the-ai-era/lab/start/tasks.py @@ -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) diff --git a/modules/14-continuous-integration/README.md b/modules/14-continuous-integration/README.md index d63fced..b82dfb4 100644 --- a/modules/14-continuous-integration/README.md +++ b/modules/14-continuous-integration/README.md @@ -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. diff --git a/modules/14-continuous-integration/lab/start/README.md b/modules/14-continuous-integration/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/14-continuous-integration/lab/start/README.md @@ -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. diff --git a/modules/14-continuous-integration/lab/start/cli.py b/modules/14-continuous-integration/lab/start/cli.py new file mode 100644 index 0000000..91799e9 --- /dev/null +++ b/modules/14-continuous-integration/lab/start/cli.py @@ -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:])) diff --git a/modules/14-continuous-integration/lab/start/tasks.py b/modules/14-continuous-integration/lab/start/tasks.py new file mode 100644 index 0000000..3be57d9 --- /dev/null +++ b/modules/14-continuous-integration/lab/start/tasks.py @@ -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) diff --git a/modules/15-security-scanning/README.md b/modules/15-security-scanning/README.md index ec16244..e31763f 100644 --- a/modules/15-security-scanning/README.md +++ b/modules/15-security-scanning/README.md @@ -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. diff --git a/modules/16-containers-and-reproducible-environments/README.md b/modules/16-containers-and-reproducible-environments/README.md index 1d55a38..f85e6ee 100644 --- a/modules/16-containers-and-reproducible-environments/README.md +++ b/modules/16-containers-and-reproducible-environments/README.md @@ -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. diff --git a/modules/16-containers-and-reproducible-environments/lab/start/README.md b/modules/16-containers-and-reproducible-environments/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/16-containers-and-reproducible-environments/lab/start/README.md @@ -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. diff --git a/modules/16-containers-and-reproducible-environments/lab/start/cli.py b/modules/16-containers-and-reproducible-environments/lab/start/cli.py new file mode 100644 index 0000000..91799e9 --- /dev/null +++ b/modules/16-containers-and-reproducible-environments/lab/start/cli.py @@ -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:])) diff --git a/modules/16-containers-and-reproducible-environments/lab/start/tasks.py b/modules/16-containers-and-reproducible-environments/lab/start/tasks.py new file mode 100644 index 0000000..3be57d9 --- /dev/null +++ b/modules/16-containers-and-reproducible-environments/lab/start/tasks.py @@ -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) diff --git a/modules/17-secrets-config-and-environments/README.md b/modules/17-secrets-config-and-environments/README.md index fa7da99..8d3ea35 100644 --- a/modules/17-secrets-config-and-environments/README.md +++ b/modules/17-secrets-config-and-environments/README.md @@ -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 diff --git a/modules/18-continuous-delivery-and-deployment/README.md b/modules/18-continuous-delivery-and-deployment/README.md index 9c38ea4..ae18025 100644 --- a/modules/18-continuous-delivery-and-deployment/README.md +++ b/modules/18-continuous-delivery-and-deployment/README.md @@ -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. diff --git a/modules/19-runners-the-compute-behind-automation/README.md b/modules/19-runners-the-compute-behind-automation/README.md index 78de079..2973fe9 100644 --- a/modules/19-runners-the-compute-behind-automation/README.md +++ b/modules/19-runners-the-compute-behind-automation/README.md @@ -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. diff --git a/modules/20-mcp-servers-giving-the-ai-hands/README.md b/modules/20-mcp-servers-giving-the-ai-hands/README.md index 7fcb388..808b6e6 100644 --- a/modules/20-mcp-servers-giving-the-ai-hands/README.md +++ b/modules/20-mcp-servers-giving-the-ai-hands/README.md @@ -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. diff --git a/modules/20-mcp-servers-giving-the-ai-hands/lab/start/README.md b/modules/20-mcp-servers-giving-the-ai-hands/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/20-mcp-servers-giving-the-ai-hands/lab/start/README.md @@ -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. diff --git a/modules/20-mcp-servers-giving-the-ai-hands/lab/start/cli.py b/modules/20-mcp-servers-giving-the-ai-hands/lab/start/cli.py new file mode 100644 index 0000000..91799e9 --- /dev/null +++ b/modules/20-mcp-servers-giving-the-ai-hands/lab/start/cli.py @@ -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:])) diff --git a/modules/20-mcp-servers-giving-the-ai-hands/lab/start/tasks.py b/modules/20-mcp-servers-giving-the-ai-hands/lab/start/tasks.py new file mode 100644 index 0000000..3be57d9 --- /dev/null +++ b/modules/20-mcp-servers-giving-the-ai-hands/lab/start/tasks.py @@ -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) diff --git a/modules/21-skills-teaching-the-ai-your-playbook/README.md b/modules/21-skills-teaching-the-ai-your-playbook/README.md index 4fb3b47..56bc24f 100644 --- a/modules/21-skills-teaching-the-ai-your-playbook/README.md +++ b/modules/21-skills-teaching-the-ai-your-playbook/README.md @@ -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. diff --git a/modules/21-skills-teaching-the-ai-your-playbook/lab/start/CHANGELOG.md b/modules/21-skills-teaching-the-ai-your-playbook/lab/start/CHANGELOG.md new file mode 100644 index 0000000..962e042 --- /dev/null +++ b/modules/21-skills-teaching-the-ai-your-playbook/lab/start/CHANGELOG.md @@ -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`. diff --git a/modules/21-skills-teaching-the-ai-your-playbook/lab/start/cli.py b/modules/21-skills-teaching-the-ai-your-playbook/lab/start/cli.py new file mode 100644 index 0000000..e1eb003 --- /dev/null +++ b/modules/21-skills-teaching-the-ai-your-playbook/lab/start/cli.py @@ -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:])) diff --git a/modules/21-skills-teaching-the-ai-your-playbook/lab/start/tasks.py b/modules/21-skills-teaching-the-ai-your-playbook/lab/start/tasks.py new file mode 100644 index 0000000..62ab400 --- /dev/null +++ b/modules/21-skills-teaching-the-ai-your-playbook/lab/start/tasks.py @@ -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) diff --git a/modules/21-skills-teaching-the-ai-your-playbook/lab/start/test_tasks.py b/modules/21-skills-teaching-the-ai-your-playbook/lab/start/test_tasks.py new file mode 100644 index 0000000..44ffcc7 --- /dev/null +++ b/modules/21-skills-teaching-the-ai-your-playbook/lab/start/test_tasks.py @@ -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() diff --git a/modules/22-securing-third-party-mcp-and-skills/README.md b/modules/22-securing-third-party-mcp-and-skills/README.md index 28fc985..bb30868 100644 --- a/modules/22-securing-third-party-mcp-and-skills/README.md +++ b/modules/22-securing-third-party-mcp-and-skills/README.md @@ -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. diff --git a/modules/23-working-with-existing-codebases/README.md b/modules/23-working-with-existing-codebases/README.md index 3401d09..5c32d8c 100644 --- a/modules/23-working-with-existing-codebases/README.md +++ b/modules/23-working-with-existing-codebases/README.md @@ -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. diff --git a/modules/24-assistive-agents/README.md b/modules/24-assistive-agents/README.md index 29dda03..42abd20 100644 --- a/modules/24-assistive-agents/README.md +++ b/modules/24-assistive-agents/README.md @@ -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 diff --git a/modules/25-autonomous-agents/README.md b/modules/25-autonomous-agents/README.md index be98d47..e8000a6 100644 --- a/modules/25-autonomous-agents/README.md +++ b/modules/25-autonomous-agents/README.md @@ -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. diff --git a/modules/26-orchestrating-multiple-agents/README.md b/modules/26-orchestrating-multiple-agents/README.md index 9bc6a1f..604e881 100644 --- a/modules/26-orchestrating-multiple-agents/README.md +++ b/modules/26-orchestrating-multiple-agents/README.md @@ -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. diff --git a/modules/26-orchestrating-multiple-agents/lab/start/README.md b/modules/26-orchestrating-multiple-agents/lab/start/README.md new file mode 100644 index 0000000..d963b11 --- /dev/null +++ b/modules/26-orchestrating-multiple-agents/lab/start/README.md @@ -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. diff --git a/modules/26-orchestrating-multiple-agents/lab/start/cli.py b/modules/26-orchestrating-multiple-agents/lab/start/cli.py new file mode 100644 index 0000000..91799e9 --- /dev/null +++ b/modules/26-orchestrating-multiple-agents/lab/start/cli.py @@ -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:])) diff --git a/modules/26-orchestrating-multiple-agents/lab/start/tasks.py b/modules/26-orchestrating-multiple-agents/lab/start/tasks.py new file mode 100644 index 0000000..3be57d9 --- /dev/null +++ b/modules/26-orchestrating-multiple-agents/lab/start/tasks.py @@ -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) diff --git a/modules/27-evals/README.md b/modules/27-evals/README.md index 8301fe2..e63369f 100644 --- a/modules/27-evals/README.md +++ b/modules/27-evals/README.md @@ -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." -- 2.52.0