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

5.7 KiB

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.
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)
  1. Completing a task clears that dependency from other tasks.
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)
  1. update handles status transitions and dependency wiring.
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)
  1. Task tools are added to the dispatch map.
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):

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

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