analysis_claude_code/docs/en/s07-task-system.md
2026-02-24 01:44:44 +08:00

176 lines
5.7 KiB
Markdown

# s07: Tasks
> Tasks are persisted as JSON files with a dependency graph, so state survives context compression and can be shared across agents.
## Problem
In-memory state (for example the TodoManager from s03) is fragile under compression (s06). Once earlier turns are compacted into summaries, in-memory todo state is gone.
s06 -> s07 is the key transition:
1. Todo list state in memory is conversational and lossy.
2. Task board state on disk is durable and recoverable.
A second issue is visibility: in-memory structures are process-local, so teammates cannot reliably share that state.
## When to Use Task vs Todo
From s07 onward, Task is the default. Todo remains for short linear checklists.
## Quick Decision Matrix
| Situation | Prefer | Why |
|---|---|---|
| Short, single-session checklist | Todo | Lowest ceremony, fastest capture |
| Cross-session work, dependencies, or teammates | Task | Durable state, dependency graph, shared visibility |
| Unsure which one to use | Task | Easier to simplify later than migrate mid-run |
## Solution
```
.tasks/
task_1.json {"id":1, "status":"completed", ...}
task_2.json {"id":2, "blockedBy":[1], "status":"pending"}
task_3.json {"id":3, "blockedBy":[2], "status":"pending"}
Dependency resolution:
+----------+ +----------+ +----------+
| task 1 | --> | task 2 | --> | task 3 |
| complete | | blocked | | blocked |
+----------+ +----------+ +----------+
| ^
+--- completing task 1 removes it from
task 2's blockedBy list
```
## How It Works
1. TaskManager provides CRUD with one JSON file per task.
```python
class TaskManager:
def create(self, subject: str, description: str = "") -> str:
task = {
"id": self._next_id,
"subject": subject,
"description": description,
"status": "pending",
"blockedBy": [],
"blocks": [],
"owner": "",
}
self._save(task)
self._next_id += 1
return json.dumps(task, indent=2)
```
2. Completing a task clears that dependency from other tasks.
```python
def _clear_dependency(self, completed_id: int):
for f in self.dir.glob("task_*.json"):
task = json.loads(f.read_text())
if completed_id in task.get("blockedBy", []):
task["blockedBy"].remove(completed_id)
self._save(task)
```
3. `update` handles status transitions and dependency wiring.
```python
def update(self, task_id, status=None,
add_blocked_by=None, add_blocks=None):
task = self._load(task_id)
if status:
task["status"] = status
if status == "completed":
self._clear_dependency(task_id)
if add_blocks:
task["blocks"] = list(set(task["blocks"] + add_blocks))
for blocked_id in add_blocks:
blocked = self._load(blocked_id)
if task_id not in blocked["blockedBy"]:
blocked["blockedBy"].append(task_id)
self._save(blocked)
self._save(task)
```
4. Task tools are added to the dispatch map.
```python
TOOL_HANDLERS = {
# ...base tools...
"task_create": lambda **kw: TASKS.create(kw["subject"]),
"task_update": lambda **kw: TASKS.update(kw["task_id"],
kw.get("status")),
"task_list": lambda **kw: TASKS.list_all(),
"task_get": lambda **kw: TASKS.get(kw["task_id"]),
}
```
## Key Code
TaskManager with dependency graph (from `agents/s07_task_system.py`, lines 46-123):
```python
class TaskManager:
def __init__(self, tasks_dir: Path):
self.dir = tasks_dir
self.dir.mkdir(exist_ok=True)
self._next_id = self._max_id() + 1
def _load(self, task_id: int) -> dict:
path = self.dir / f"task_{task_id}.json"
return json.loads(path.read_text())
def _save(self, task: dict):
path = self.dir / f"task_{task['id']}.json"
path.write_text(json.dumps(task, indent=2))
def create(self, subject, description=""):
task = {"id": self._next_id, "subject": subject,
"status": "pending", "blockedBy": [],
"blocks": [], "owner": ""}
self._save(task)
self._next_id += 1
return json.dumps(task, indent=2)
def _clear_dependency(self, completed_id):
for f in self.dir.glob("task_*.json"):
task = json.loads(f.read_text())
if completed_id in task.get("blockedBy", []):
task["blockedBy"].remove(completed_id)
self._save(task)
```
## What Changed From s06
| Component | Before (s06) | After (s07) |
|---|---|---|
| Tools | 5 | 8 (`task_create/update/list/get`) |
| State storage | In-memory only | JSON files in `.tasks/` |
| Dependencies | None | `blockedBy + blocks` graph |
| Persistence | Lost on compact | Survives compression |
## Design Rationale
File-based state survives compaction and process restarts. The dependency graph preserves execution order even when conversation details are forgotten. This turns transient chat context into durable work state.
Durability still needs a write discipline: reload task JSON before each write, validate expected `status/blockedBy`, then persist atomically. Otherwise concurrent writers can overwrite each other.
Course-level implication: s07+ defaults to Task because it better matches long-running and collaborative engineering workflows.
## Try It
```sh
cd learn-claude-code
python agents/s07_task_system.py
```
Suggested prompts:
1. `Create 3 tasks: "Setup project", "Write code", "Write tests". Make them depend on each other in order.`
2. `List all tasks and show the dependency graph`
3. `Complete task 1 and then list tasks to see task 2 unblocked`
4. `Create a task board for refactoring: parse -> transform -> emit -> test`