Self-contained, skip-friendly lab starting points #103
@@ -127,6 +127,17 @@ swappable part; the workflow is the durable skill*), and you just lived it inste
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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.
|
your own agent) to do the git and the edits (M4); you make the calls and verify each result.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
elif command == "delete":
|
||||||
|
tlist.remove(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("deleted")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
del self.tasks[index]
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -122,6 +122,18 @@ you already feel is the curriculum.
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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.
|
not write Python; you'll run a small app we provide.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -132,6 +132,18 @@ Everything above is standard Git. What's *specific* to AI-assisted work:
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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.
|
**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
|
**You'll need:** Git installed (`git --version`; if it's missing, install from
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -193,6 +193,18 @@ Here's why this module is more than "learn Git on easy mode":
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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
|
Modules 1–2. The AI stays in the **browser**; you copy its draft into the file yourself, exactly as
|
||||||
in Module 2.
|
in Module 2.
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -281,6 +281,18 @@ loop and the loop is unchanged.
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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.
|
tool; the tool writes the Python.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -195,6 +195,18 @@ Three things make this specifically an AI problem, not a generic config chore:
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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.
|
editor-integrated AI (Module 4) for the part where the AI obeys the file.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
elif command == "delete":
|
||||||
|
tlist.remove(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("deleted")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
del self.tasks[index]
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -233,6 +233,18 @@ Everything above is standard Git. Here's why it matters *more* in an AI-assisted
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**Lab language:** shell (Git commands), driving the `tasks-app` from Modules 1–2 with your
|
||||||
editor-integrated AI from Module 4.
|
editor-integrated AI from Module 4.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
elif command == "delete":
|
||||||
|
tlist.remove(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("deleted")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
del self.tasks[index]
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -213,6 +213,18 @@ to run two agents and watch them overwrite each other's work.
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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`.
|
**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
|
In this lab you'll run **two AI sessions at the same time** on the same project (one adding a
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
elif command == "delete":
|
||||||
|
tlist.remove(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("deleted")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
del self.tasks[index]
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -295,6 +295,18 @@ A remote isn't only about durability. It's what the AI parts of this course run
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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,
|
**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.
|
WSL, or Git Bash on Windows. Continues the `tasks-app` repo from Module 2.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
elif command == "delete":
|
||||||
|
tlist.remove(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("deleted")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
del self.tasks[index]
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -225,6 +225,18 @@ valuable, not less.
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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.
|
**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
|
You'll draft issues as Markdown locally (so you can version and reuse the format), then have your
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
elif command == "delete":
|
||||||
|
tlist.remove(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("deleted")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
del self.tasks[index]
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -191,6 +191,18 @@ you couldn't do yourself.
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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.
|
real change, then review a diff the "AI" produced and catch the trap planted in it.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. The `done` command turns a bad index into a
|
||||||
|
clean error message and a non-zero exit code; note that behavior before you review the AI
|
||||||
|
change, so you can tell if the change quietly alters it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
try:
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
except IndexError as exc:
|
||||||
|
print(f"error: {exc}")
|
||||||
|
return 1
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Same running example as Modules 1 and 2, with one addition: `complete` now validates the
|
||||||
|
index and raises a clear error for a bad one. That explicit edge-case handling is here on
|
||||||
|
purpose; it's the kind of thing an AI "refactor" likes to quietly remove. This is the
|
||||||
|
known-good base you'll review an AI change against in Module 10.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
if not 0 <= index < len(self.tasks):
|
||||||
|
raise IndexError(f"no task at index {index}")
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -253,6 +253,18 @@ You're not learning collaboration *and then* learning to work with agents. They'
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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
|
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
|
hand here are read-only checks like `git branch` and `git show`. You'll implement the feature with
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
elif command == "delete":
|
||||||
|
tlist.remove(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("deleted")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
del self.tasks[index]
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -221,6 +221,18 @@ Recovery was always a real skill. AI raises its value on every axis:
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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.
|
**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
|
You'll do the two scenarios that matter most: **revert a bad merge** that's already on `main`, then
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
elif command == "delete":
|
||||||
|
tlist.remove(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("deleted")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
del self.tasks[index]
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -202,6 +202,18 @@ Passing for the right reason is the skill.
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**Lab language:** Python (standard-library `unittest`), with a couple of shell commands to run the
|
||||||
suite. Nothing to install.
|
suite. Nothing to install.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks` (Module 13 copy)
|
||||||
|
|
||||||
|
The same tiny task tracker from Modules 1 and 2, with one feature added: a `count` command backed
|
||||||
|
by `TaskList.pending_count()`. Use this copy for the Module 13 lab so everyone starts from the same
|
||||||
|
code, including the same latent bug.
|
||||||
|
|
||||||
|
If you already have a `tasks-app` from earlier modules, you can use that instead; just make sure it
|
||||||
|
has a `count` command (the Module 2 lab added one). The planted bug in this copy is there on purpose.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: core logic (`Task`, `TaskList`), now with `pending_count()`.
|
||||||
|
- `cli.py`: command-line front end. Adds `count`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "write the tests"
|
||||||
|
python cli.py add "fix the bug"
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
python cli.py count
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+. No third-party packages; tests use the standard library `unittest`.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py count
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. Same minimal app from Modules 1 and 2, with a
|
||||||
|
`count` command bolted on.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{tlist.pending_count()} task(s) pending")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Same running example from Modules 1 and 2, carried forward. It has grown one feature since then:
|
||||||
|
a `pending_count()` helper that the AI added to back a `count` command. The feature "works" in
|
||||||
|
the obvious case, which is exactly the kind of code this module teaches you to verify properly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def pending_count(self) -> int:
|
||||||
|
# Added by the AI to support `cli.py count`. Looks right, ran fine in a quick check.
|
||||||
|
return len(self.tasks)
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -205,6 +205,18 @@ the more you need a reviewer that checks behavior instead of believing the diff.
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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
|
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.
|
break it on purpose and watch CI catch it.
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
elif command == "delete":
|
||||||
|
tlist.remove(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("deleted")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
del self.tasks[index]
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -203,6 +203,15 @@ add them *despite* using AI; using AI is what moves them from "nice to have" to
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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,
|
scanners (both pip-installable, cross-platform), let the AI introduce all three problems, catch them,
|
||||||
and wire the catch into your pipeline.
|
and wire the catch into your pipeline.
|
||||||
|
|||||||
@@ -164,6 +164,18 @@ Docker itself you may already know. What makes containers matter *more* in AI-as
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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.
|
containerize and run the app you already have.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
elif command == "delete":
|
||||||
|
tlist.remove(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("deleted")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
del self.tasks[index]
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -277,6 +277,15 @@ model will keep offering it. Concretely:
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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.
|
**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
|
You'll take a file that hardcodes a secret (the exact thing an AI hands you) and refactor it so the
|
||||||
|
|||||||
@@ -199,6 +199,15 @@ between an autonomous contributor and your users.
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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`
|
**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
|
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.
|
automatic rollback, the whole CD motion simulated on your own machine.
|
||||||
|
|||||||
@@ -202,6 +202,15 @@ review reflex from Module 10 has to extend to the workflow files, not just the a
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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.
|
machine and your own forge, with no hosted account required for the core of it.
|
||||||
|
|
||||||
|
|||||||
@@ -225,6 +225,18 @@ That changes what matters about the integration.
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**Lab language:** Python (a ~15-line MCP server) plus your agentic tool's config. Runs on your own
|
||||||
machine, any OS.
|
machine, any OS.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
elif command == "delete":
|
||||||
|
tlist.remove(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("deleted")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
del self.tasks[index]
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -160,6 +160,18 @@ On paper this is just "write a runbook." The AI-specific twist is what changes t
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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.
|
skill, then have your editor-integrated AI (Module 4) execute it.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Newest entries on top. One line per user-visible change.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Add `count` command: print how many tasks are still pending.
|
||||||
|
- Add `done <index>` command: mark a task complete.
|
||||||
|
- Initial CLI: `add` and `list`.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py count
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. The same minimal app from Module 1 onward; the
|
||||||
|
target your "add a command" skill extends.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{tlist.pending_count()} task(s) pending")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
The same running example from Module 1 onward, carried forward with the `pending_count()` helper
|
||||||
|
that backs the `count` command. This is the codebase your "add a command" skill operates on.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def pending_count(self) -> int:
|
||||||
|
return len([t for t in self.tasks if not t.done])
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""Test suite for the tasks-app. Run from this folder with:
|
||||||
|
|
||||||
|
python -m unittest
|
||||||
|
|
||||||
|
Your "add a command" skill should ADD a test here for every new command. The point is to assert
|
||||||
|
intended behavior, not just that nothing crashed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from tasks import TaskList
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskBasics(unittest.TestCase):
|
||||||
|
def test_add_appends_a_task(self):
|
||||||
|
tl = TaskList()
|
||||||
|
tl.add("write the skill")
|
||||||
|
self.assertEqual(len(tl.tasks), 1)
|
||||||
|
self.assertEqual(tl.tasks[0].title, "write the skill")
|
||||||
|
self.assertFalse(tl.tasks[0].done)
|
||||||
|
|
||||||
|
def test_complete_marks_done(self):
|
||||||
|
tl = TaskList()
|
||||||
|
tl.add("a")
|
||||||
|
tl.complete(0)
|
||||||
|
self.assertTrue(tl.tasks[0].done)
|
||||||
|
|
||||||
|
def test_pending_excludes_completed(self):
|
||||||
|
tl = TaskList()
|
||||||
|
tl.add("a")
|
||||||
|
tl.add("b")
|
||||||
|
tl.complete(0)
|
||||||
|
self.assertEqual([t.title for t in tl.pending()], ["b"])
|
||||||
|
|
||||||
|
def test_pending_count_ignores_done(self):
|
||||||
|
tl = TaskList()
|
||||||
|
tl.add("a")
|
||||||
|
tl.add("b")
|
||||||
|
tl.complete(0)
|
||||||
|
self.assertEqual(tl.pending_count(), 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -195,6 +195,15 @@ skills different from any dependency you've shipped before:
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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
|
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.
|
against the Module 1 `tasks-app` and apply the least-privilege mitigation.
|
||||||
|
|||||||
@@ -162,6 +162,15 @@ into a revertable diff.
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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.
|
**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.
|
This lab does **not** use `tasks-app`; the entire point is a codebase you *didn't* write.
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,15 @@ occasionally wrong with no consequences.
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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
|
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
|
the prompt, validate and render the response, present the decision gate); the model does the one part
|
||||||
|
|||||||
@@ -202,6 +202,15 @@ the job is non-deterministic and persuasive**, and that changes what "automation
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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
|
machine, any OS, against the `tasks-app` repo from Module 1, with no forge account or paid agent
|
||||||
required to complete it.
|
required to complete it.
|
||||||
|
|||||||
@@ -254,6 +254,18 @@ imaginary, and that the fix was a ten-minute coordination plan you skipped.
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**Lab language:** shell (Git + a couple of helper scripts) driving multiple AI edit sessions on the
|
||||||
`tasks-app`, integrated through PRs.
|
`tasks-app`, integrated through PRs.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo app: `tasks`
|
||||||
|
|
||||||
|
A deliberately tiny command-line task tracker. It exists to be *changed by an AI*, so it's small
|
||||||
|
enough to read in a minute but real enough to have more than one file, which is exactly where the
|
||||||
|
copy-paste workflow starts to hurt.
|
||||||
|
|
||||||
|
This is the running example for **Module 1** (where you feel the copy-paste problem) and **Module 2**
|
||||||
|
(where you put it under version control).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `tasks.py`: the core logic (`Task`, `TaskList`).
|
||||||
|
- `cli.py`: the command-line front end. Reads/writes `tasks.json`.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cli.py add "read module 1"
|
||||||
|
python cli.py add "set up my editor"
|
||||||
|
python cli.py list
|
||||||
|
python cli.py done 0
|
||||||
|
python cli.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Python 3.10+ (it uses `list[Task]` style type hints). No third-party packages.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Tiny command-line front end for the demo task app.
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
python cli.py add "write the lesson"
|
||||||
|
python cli.py list
|
||||||
|
|
||||||
|
State is kept in tasks.json next to this file. It's intentionally minimal; the point of this app
|
||||||
|
is to be a realistic-but-small thing you change with an AI, not a product.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tasks import Task, TaskList
|
||||||
|
|
||||||
|
STATE = Path(__file__).parent / "tasks.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> TaskList:
|
||||||
|
if not STATE.exists():
|
||||||
|
return TaskList()
|
||||||
|
raw = json.loads(STATE.read_text())
|
||||||
|
return TaskList(tasks=[Task(**t) for t in raw])
|
||||||
|
|
||||||
|
|
||||||
|
def save(tlist: TaskList) -> None:
|
||||||
|
STATE.write_text(json.dumps([t.__dict__ for t in tlist.tasks], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
tlist = load()
|
||||||
|
if not argv:
|
||||||
|
print("usage: python cli.py [add <title> | list | done <index> | count | delete <index>]")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = argv[0]
|
||||||
|
if command == "add":
|
||||||
|
title = " ".join(argv[1:])
|
||||||
|
tlist.add(title)
|
||||||
|
save(tlist)
|
||||||
|
print(f"added: {title}")
|
||||||
|
elif command == "list":
|
||||||
|
print(tlist.render())
|
||||||
|
elif command == "done":
|
||||||
|
tlist.complete(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("updated")
|
||||||
|
elif command == "count":
|
||||||
|
print(f"{len(tlist.pending())} pending")
|
||||||
|
elif command == "delete":
|
||||||
|
tlist.remove(int(argv[1]))
|
||||||
|
save(tlist)
|
||||||
|
print("deleted")
|
||||||
|
else:
|
||||||
|
print(f"unknown command: {command}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Core task logic for the demo app.
|
||||||
|
|
||||||
|
Deliberately small and deliberately split across two files (this and cli.py) so that the
|
||||||
|
copy-paste workflow has more than one place to go wrong. This is the running example used in
|
||||||
|
Modules 1 and 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
title: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskList:
|
||||||
|
tasks: list[Task] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, title: str) -> Task:
|
||||||
|
task = Task(title=title)
|
||||||
|
self.tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete(self, index: int) -> None:
|
||||||
|
self.tasks[index].done = True
|
||||||
|
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
del self.tasks[index]
|
||||||
|
|
||||||
|
def pending(self) -> list[Task]:
|
||||||
|
return [t for t in self.tasks if not t.done]
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
if not self.tasks:
|
||||||
|
return "(no tasks yet)"
|
||||||
|
lines = []
|
||||||
|
for i, task in enumerate(self.tasks):
|
||||||
|
box = "[x]" if task.done else "[ ]"
|
||||||
|
lines.append(f"{i}. {box} {task.title}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -212,6 +212,15 @@ That's the durable skill. Models are weather. The eval set is the thermometer yo
|
|||||||
|
|
||||||
## Hands-on lab
|
## 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
|
**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."
|
a regression eval across a "model swap."
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user