diff --git a/s12_task_system/README.en.md b/s12_task_system/README.en.md
index 756aedb42..cfb2ce52f 100644
--- a/s12_task_system/README.en.md
+++ b/s12_task_system/README.en.md
@@ -159,7 +159,7 @@ Here `claim` / `complete` are actions, while `pending` / `in_progress` / `comple
- **claim_task**: `pending` → `in_progress`. Sets owner, begins work.
- **complete_task**: `in_progress` → `completed`. Marks the task done and unblocks downstream.
-CC has no `in_progress → pending` release path. If a teammate terminates or shuts down, CC unassigns its unfinished tasks (clears owner) and resets status to `pending`, allowing other agents to reclaim them. The teaching version omits this recovery path.
+The teaching version has no `in_progress → pending` release path. In real CC, if a teammate terminates or shuts down, CC unassigns its unfinished tasks (clears owner) and resets status to `pending`, allowing other agents to reclaim them. The teaching version omits this recovery path.
### Putting It Together
diff --git a/s12_task_system/README.ja.md b/s12_task_system/README.ja.md
index ebc8c7e06..6fcb84add 100644
--- a/s12_task_system/README.ja.md
+++ b/s12_task_system/README.ja.md
@@ -159,7 +159,7 @@ pending ──claim──→ in_progress ──complete──→ completed
- **claim_task**: `pending` → `in_progress`。owner を設定し、作業を開始。
- **complete_task**: `in_progress` → `completed`。タスクを完了済みにし、下流をアンロック。
-CC には `in_progress → pending` の release パスがない。teammate が終了または shutdown した場合、CC は未完了タスクの owner をクリアし、status を `pending` にリセットし、他の agent が再認識できるようにする。教学版はこの復旧パスを省略。
+教学版には `in_progress → pending` の release パスがない。実際の CC では、teammate が終了または shutdown した場合、CC は未完了タスクの owner をクリアし、status を `pending` にリセットし、他の agent が再認識できるようにする。教学版はこの復旧パスを省略。
### 組み合わせて実行
diff --git a/s12_task_system/README.md b/s12_task_system/README.md
index 03a925281..fcca7b721 100644
--- a/s12_task_system/README.md
+++ b/s12_task_system/README.md
@@ -159,7 +159,7 @@ pending ──claim──→ in_progress ──complete──→ completed
- **claim_task**: `pending` → `in_progress`。设置 owner,开始工作。
- **complete_task**: `in_progress` → `completed`。把任务标记为完成,并解锁下游。
-CC 没有 `in_progress → pending` 的 release 路径。如果 teammate 终止或 shutdown,CC 会把它未完成的任务 unassign(清除 owner),并将 status 重置为 `pending`,方便其他 agent 重新认领。教学版省略了这一恢复路径。
+教学版没有 `in_progress → pending` 的 release 路径。真实 CC 中,如果 teammate 终止或 shutdown,CC 会把它未完成的任务 unassign(清除 owner),并将 status 重置为 `pending`,方便其他 agent 重新认领。教学版省略了这一恢复路径。
### 合起来跑
diff --git a/web/src/data/generated/docs.json b/web/src/data/generated/docs.json
index 4e50b7380..4261f9540 100644
--- a/web/src/data/generated/docs.json
+++ b/web/src/data/generated/docs.json
@@ -201,19 +201,19 @@
"version": "s12",
"locale": "en",
"title": "s12: Task System — Break Big Goals into Small Tasks",
- "content": "# s12: Task System — Break Big Goals into Small Tasks\n\ns01 → ... → s10 → s11 → `s12` → [s13](/en/s13) → s14 → ... → s20\n\n> *\"Break big goals into small tasks, order them, persist\"* — File-persisted task graph, the foundation for multi-agent collaboration.\n>\n> **Harness Layer**: Tasks — Persisted goals, recoverable progress.\n\n---\n\n## The Problem\n\nThe agent receives a project: set up a database, write APIs, add tests. It uses s05's TodoWrite to create a checklist, then starts writing the API first, gets halfway through and realizes there are no database tables, goes back to fix them; when adding tests, discovers the API interface signatures have changed again...\n\nYou can't build the roof before laying the foundation. Tasks have ordering. Task dependencies should form a Directed Acyclic Graph (DAG); the teaching version only demonstrates `blockedBy` checking, without cycle detection.\n\ns05's TodoWrite is an execution checklist for the current task, kept in session memory. What you need here is a **task system**: each task is a JSON file, tasks have `blockedBy` dependencies, and they persist across sessions on disk.\n\n---\n\n## The Solution\n\n\n\nTeaching code keeps a basic agent loop, omitting S11's full error recovery (RecoveryState, backoff, escalation, reactive compact, fallback model) to stay focused on the task system. Added: 5 new task tools + `.tasks/` directory for persistence + `blockedBy` dependency checking. The task system and error recovery are independent layers: in CC source, `utils/tasks.ts` only handles CRUD, while `query.ts`'s with_retry/RecoveryState handles error recovery, with no coupling between them.\n\nTodoWrite vs Task System:\n\n| | TodoWrite (s05) | Task System (s12) |\n|---|---|---|\n| Role | Execution checklist for the current task | Recoverable task system |\n| Storage | In-process / session state | `.tasks/{id}.json` |\n| Dependencies | None | `blockedBy` / `blocks` graph |\n| Lifecycle | Current session / current task | Cross-session |\n| Coordination | No task claiming | `owner` / claim |\n| Status | pending / in_progress / completed | pending / in_progress / completed |\n| Granularity | The agent's own steps | Tasks that can be claimed, tracked, and unblocked |\n\n---\n\n## How It Works\n\n\n\n### Task: Data Structure\n\nEach task is a JSON file, stored in the `.tasks/` directory:\n\n```python\n@dataclass\nclass Task:\n id: str\n subject: str\n description: str\n status: str # pending | in_progress | completed\n owner: str | None # Agent name (multi-agent scenarios)\n blockedBy: list[str] # List of dependency task IDs\n```\n\nIDs are generated with `timestamp + random hex`, simple but sufficient. CC uses sequential IDs + a highwatermark file to prevent ID reuse, which is a more rigorous design.\n\n### create_task: Create Tasks\n\n```python\ndef create_task(subject: str, description: str = \"\",\n blockedBy: list[str] | None = None) -> Task:\n task = Task(\n id=f\"task_{int(time.time())}_{random_hex(4)}\",\n subject=subject, description=description,\n status=\"pending\", owner=None,\n blockedBy=blockedBy or [],\n )\n save_task(task)\n return task\n```\n\nAutomatically calls `save_task` on creation to write `.tasks/{id}.json`. `blockedBy` declares dependencies, for example \"write API\" has `blockedBy: [\"task_schema\"]`.\n\n### can_start: Dependency Check\n\nA task can only start after all its `blockedBy` dependencies are **completed**:\n\n```python\ndef can_start(task_id: str) -> bool:\n task = load_task(task_id)\n for dep_id in task.blockedBy:\n if not _task_path(dep_id).exists():\n return False # missing dependency = blocked\n dep = load_task(dep_id)\n if dep.status != \"completed\":\n return False\n return True\n```\n\n`can_start` is a prerequisite check for `claim_task`: if any `blockedBy` dependency is not completed, the task cannot be claimed. Missing dependencies are treated as blocked, avoiding crashes from referencing wrong IDs.\n\n### claim_task: Claim a Task\n\nWhen the agent starts working on a task, it calls `claim_task`: sets `owner`, changes status from `pending` → `in_progress`. The `owner` field records who is working on the task, preventing duplicate claims in multi-agent scenarios:\n\n```python\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n task = load_task(task_id)\n if task.status != \"pending\":\n return f\"Task {task_id} is {task.status}, cannot claim\"\n if not can_start(task_id):\n deps = [d for d in task.blockedBy\n if load_task(d).status != \"completed\"]\n return f\"Blocked by: {deps}\"\n task.owner = owner\n task.status = \"in_progress\"\n save_task(task)\n return f\"Claimed {task_id} ({task.subject})\"\n```\n\nIf the task is already claimed by someone else (`status != \"pending\"`), or dependencies aren't met (`can_start` returns False), the claim is rejected.\n\n### complete_task: Complete and Unblock\n\nWhen a task is done, set it to `completed`. Simultaneously scan all other tasks to find downstream tasks that were **just unblocked**:\n\n```python\ndef complete_task(task_id: str) -> str:\n task = load_task(task_id)\n task.status = \"completed\"\n save_task(task)\n # Find newly unblocked downstream tasks\n unblocked = [t.subject for t in list_tasks()\n if t.status == \"pending\" and t.blockedBy\n and can_start(t.id)]\n msg = f\"Completed {task_id} ({task.subject})\"\n if unblocked:\n msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n return msg\n```\n\nAfter completing \"schema\", `can_start` returns True for \"endpoints\" and \"docs\"; they can begin.\n\n### get_task: View Full Details\n\n`list_tasks` only shows a one-line summary. `get_task` returns the full task JSON, including description and dependency details. When recovering across sessions, the agent needs to read the full description to continue work:\n\n```python\ndef get_task(task_id: str) -> str:\n task = load_task(task_id)\n return json.dumps(asdict(task), indent=2)\n```\n\n### State Machine: Two Actions, Three States\n\n```\npending ──claim──→ in_progress ──complete──→ completed\n```\n\nHere `claim` / `complete` are actions, while `pending` / `in_progress` / `completed` are states:\n\n- **claim_task**: `pending` → `in_progress`. Sets owner, begins work.\n- **complete_task**: `in_progress` → `completed`. Marks the task done and unblocks downstream.\n\nCC has no `in_progress → pending` release path. If a teammate terminates or shuts down, CC unassigns its unfinished tasks (clears owner) and resets status to `pending`, allowing other agents to reclaim them. The teaching version omits this recovery path.\n\n### Putting It Together\n\n```python\n# Create tasks with dependencies\nschema = create_task(\"setup database schema\")\nendpoints = create_task(\"create API endpoints\", blockedBy=[schema.id])\ntests = create_task(\"write tests\", blockedBy=[endpoints.id])\ndocs = create_task(\"write docs\", blockedBy=[schema.id])\n\n# Agent claims the first available task\nclaim_task(schema.id) # ✓ Claimed (no dependencies)\ncomplete_task(schema.id) # ✓ Completed → unblocks endpoints, docs\n\nclaim_task(endpoints.id) # ✓ Claimed (schema completed)\ncomplete_task(endpoints.id) # ✓ Completed → unblocks tests\n\nclaim_task(docs.id) # ✓ Claimed (schema completed)\ncomplete_task(docs.id) # ✓ Completed\n\nclaim_task(tests.id) # ✓ Claimed (endpoints completed)\ncomplete_task(tests.id) # ✓ Completed\n```\n\nEach `create_task` writes a JSON file, each `claim_task` / `complete_task` updates the file. Across sessions, the `.tasks/` directory persists — the agent reads the files to recover progress.\n\n---\n\n## Changes from s11\n\n| Component | Before (s11) | After (s12) |\n|-----------|-------------|-------------|\n| Task management | None | Task dataclass + 5 tools |\n| New types | — | Task (id, subject, description, status, owner, blockedBy) |\n| Storage | No persistence | `.tasks/{id}.json` cross-session |\n| Dependencies | None | `blockedBy` graph + `can_start` check |\n| Tools | bash, read_file, write_file (3) | + create_task, list_tasks, get_task, claim_task, complete_task (8) |\n| Lifecycle | — | pending → in_progress → completed (no release rollback) |\n\n---\n\n## Try It\n\n```sh\ncd learn-claude-code\npython s12_task_system/code.py\n```\n\nTry these prompts:\n\n1. `Create tasks: setup database schema, create API endpoints (depends on schema), write tests (depends on endpoints), write docs (depends on schema)`\n2. `List all tasks and their statuses`\n3. `Claim the first unblocked task and complete it`\n4. `List tasks again — which ones are now unblocked?`\n\nWhat to observe: Are JSON files generated in the `.tasks/` directory? After completing a task, are the blocked tasks unblocked?\n\n---\n\n## What's Next\n\nThe task graph is in place. But some tasks take a long time — like running full test suites or deploying to a server. The agent calls the LLM billed by token, it can't afford to wait on a slow operation.\n\ns13 Background Tasks → Slow operations go to the background. The agent continues processing other tasks, and gets notified when the background work is done.\n\n\nDeep Dive into CC Source
\n\n> The following is a complete analysis based on CC source code `utils/tasks.ts` (862 lines), `tools/TaskCreateTool/TaskCreateTool.ts` (138 lines), `tools/TaskUpdateTool/TaskUpdateTool.ts` (406 lines), `tools/TaskGetTool/TaskGetTool.ts` (128 lines), `tools/TaskListTool/TaskListTool.ts` (116 lines), `hooks/useTaskListWatcher.ts` (221 lines).\n\n### 1. TaskRecord's Full Fields\n\nThe tutorial only covers id, subject, status, owner, blockedBy. CC actually has 9 fields (`utils/tasks.ts:76-89`):\n\n| Field | Type | Purpose |\n|------|------|---------|\n| `id` | string | Incrementing integer ID |\n| `subject` | string | Short title |\n| `description` | string | Free-form description |\n| `activeForm` | string? | Present tense form, shown in spinner when in_progress |\n| `owner` | string? | Assigned agent ID |\n| `status` | pending/in_progress/completed | Lifecycle |\n| `blocks` | string[] | Task IDs blocked by this task (downstream) |\n| `blockedBy` | string[] | Task IDs blocking this task (upstream) |\n| `metadata` | Record? | Arbitrary extension key-value pairs |\n\nStorage location: `~/.claude/tasks/{taskListId}/{id}.json`. One file per task.\n\n### 2. Not a TodoWrite Upgrade — Two Independent Systems\n\nIn CC, Task System and TodoWrite **coexist**, toggled by `isTodoV2Enabled()` (`utils/tasks.ts:133`) — interactive sessions default to Task (V2), non-interactive/SDK sessions default to TodoWrite. The `CLAUDE_CODE_ENABLE_TASKS` env var can force-enable Task. Task has what TodoWrite lacks: file-lock concurrency protection, dependency enforcement, ownership, fs.watch reactive monitoring, lifecycle hooks.\n\n### 3. Concurrent Claim Locking\n\n`claimTask()` (`utils/tasks.ts:541-612`) uses dual locking to prevent races:\n\n**Task file lock**: `proper-lockfile` locks `{taskId}.json` (up to 30 retries, exponential backoff 5-100ms). Inside the lock:\n1. Re-read task (prevent TOCTOU)\n2. Check already claimed by another → `already_claimed`\n3. Check already completed → `already_resolved`\n4. Check upstream not completed → `blocked`\n5. Set owner\n\n**List-level lock** (agent busy check): `.lock` file, atomic scan of all tasks to check if the agent already has other open tasks.\n\nNote: The teaching version combines claiming and starting work into one step (claim = set owner + in_progress); real CC's `claimTask` primarily resolves owner competition — it only sets owner without changing status. Status updates are handled by `TaskUpdate`.\n\n### 4. High-Water Mark to Prevent ID Reuse\n\nThe `.highwatermark` file records the highest task ID ever assigned. Even if a task is deleted, its ID won't be reused.\n\n### 5. Four Task Tools\n\nCC's task system has four tools (not the tutorial's single generic Task tool): `TaskCreate`, `TaskGet`, `TaskUpdate`, `TaskList`. All set `isConcurrencySafe: true` and `shouldDefer: true` (tool schemas aren't in the initial prompt; only visible after ToolSearch).\n\nThe teaching version's `create_task(blockedBy=...)` declares dependencies at creation time, which is a reasonable simplification. Real CC's `TaskCreate` only accepts subject/description/activeForm/metadata — dependencies are maintained via `TaskUpdate`'s `addBlocks/addBlockedBy`.\n\n \n\n\n"
+ "content": "# s12: Task System — Break Big Goals into Small Tasks\n\ns01 → ... → s10 → s11 → `s12` → [s13](/en/s13) → s14 → ... → s20\n\n> *\"Break big goals into small tasks, order them, persist\"* — File-persisted task graph, the foundation for multi-agent collaboration.\n>\n> **Harness Layer**: Tasks — Persisted goals, recoverable progress.\n\n---\n\n## The Problem\n\nThe agent receives a project: set up a database, write APIs, add tests. It uses s05's TodoWrite to create a checklist, then starts writing the API first, gets halfway through and realizes there are no database tables, goes back to fix them; when adding tests, discovers the API interface signatures have changed again...\n\nYou can't build the roof before laying the foundation. Tasks have ordering. Task dependencies should form a Directed Acyclic Graph (DAG); the teaching version only demonstrates `blockedBy` checking, without cycle detection.\n\ns05's TodoWrite is an execution checklist for the current task, kept in session memory. What you need here is a **task system**: each task is a JSON file, tasks have `blockedBy` dependencies, and they persist across sessions on disk.\n\n---\n\n## The Solution\n\n\n\nTeaching code keeps a basic agent loop, omitting S11's full error recovery (RecoveryState, backoff, escalation, reactive compact, fallback model) to stay focused on the task system. Added: 5 new task tools + `.tasks/` directory for persistence + `blockedBy` dependency checking. The task system and error recovery are independent layers: in CC source, `utils/tasks.ts` only handles CRUD, while `query.ts`'s with_retry/RecoveryState handles error recovery, with no coupling between them.\n\nTodoWrite vs Task System:\n\n| | TodoWrite (s05) | Task System (s12) |\n|---|---|---|\n| Role | Execution checklist for the current task | Recoverable task system |\n| Storage | In-process / session state | `.tasks/{id}.json` |\n| Dependencies | None | `blockedBy` / `blocks` graph |\n| Lifecycle | Current session / current task | Cross-session |\n| Coordination | No task claiming | `owner` / claim |\n| Status | pending / in_progress / completed | pending / in_progress / completed |\n| Granularity | The agent's own steps | Tasks that can be claimed, tracked, and unblocked |\n\n---\n\n## How It Works\n\n\n\n### Task: Data Structure\n\nEach task is a JSON file, stored in the `.tasks/` directory:\n\n```python\n@dataclass\nclass Task:\n id: str\n subject: str\n description: str\n status: str # pending | in_progress | completed\n owner: str | None # Agent name (multi-agent scenarios)\n blockedBy: list[str] # List of dependency task IDs\n```\n\nIDs are generated with `timestamp + random hex`, simple but sufficient. CC uses sequential IDs + a highwatermark file to prevent ID reuse, which is a more rigorous design.\n\n### create_task: Create Tasks\n\n```python\ndef create_task(subject: str, description: str = \"\",\n blockedBy: list[str] | None = None) -> Task:\n task = Task(\n id=f\"task_{int(time.time())}_{random_hex(4)}\",\n subject=subject, description=description,\n status=\"pending\", owner=None,\n blockedBy=blockedBy or [],\n )\n save_task(task)\n return task\n```\n\nAutomatically calls `save_task` on creation to write `.tasks/{id}.json`. `blockedBy` declares dependencies, for example \"write API\" has `blockedBy: [\"task_schema\"]`.\n\n### can_start: Dependency Check\n\nA task can only start after all its `blockedBy` dependencies are **completed**:\n\n```python\ndef can_start(task_id: str) -> bool:\n task = load_task(task_id)\n for dep_id in task.blockedBy:\n if not _task_path(dep_id).exists():\n return False # missing dependency = blocked\n dep = load_task(dep_id)\n if dep.status != \"completed\":\n return False\n return True\n```\n\n`can_start` is a prerequisite check for `claim_task`: if any `blockedBy` dependency is not completed, the task cannot be claimed. Missing dependencies are treated as blocked, avoiding crashes from referencing wrong IDs.\n\n### claim_task: Claim a Task\n\nWhen the agent starts working on a task, it calls `claim_task`: sets `owner`, changes status from `pending` → `in_progress`. The `owner` field records who is working on the task, preventing duplicate claims in multi-agent scenarios:\n\n```python\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n task = load_task(task_id)\n if task.status != \"pending\":\n return f\"Task {task_id} is {task.status}, cannot claim\"\n if not can_start(task_id):\n deps = [d for d in task.blockedBy\n if load_task(d).status != \"completed\"]\n return f\"Blocked by: {deps}\"\n task.owner = owner\n task.status = \"in_progress\"\n save_task(task)\n return f\"Claimed {task_id} ({task.subject})\"\n```\n\nIf the task is already claimed by someone else (`status != \"pending\"`), or dependencies aren't met (`can_start` returns False), the claim is rejected.\n\n### complete_task: Complete and Unblock\n\nWhen a task is done, set it to `completed`. Simultaneously scan all other tasks to find downstream tasks that were **just unblocked**:\n\n```python\ndef complete_task(task_id: str) -> str:\n task = load_task(task_id)\n task.status = \"completed\"\n save_task(task)\n # Find newly unblocked downstream tasks\n unblocked = [t.subject for t in list_tasks()\n if t.status == \"pending\" and t.blockedBy\n and can_start(t.id)]\n msg = f\"Completed {task_id} ({task.subject})\"\n if unblocked:\n msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n return msg\n```\n\nAfter completing \"schema\", `can_start` returns True for \"endpoints\" and \"docs\"; they can begin.\n\n### get_task: View Full Details\n\n`list_tasks` only shows a one-line summary. `get_task` returns the full task JSON, including description and dependency details. When recovering across sessions, the agent needs to read the full description to continue work:\n\n```python\ndef get_task(task_id: str) -> str:\n task = load_task(task_id)\n return json.dumps(asdict(task), indent=2)\n```\n\n### State Machine: Two Actions, Three States\n\n```\npending ──claim──→ in_progress ──complete──→ completed\n```\n\nHere `claim` / `complete` are actions, while `pending` / `in_progress` / `completed` are states:\n\n- **claim_task**: `pending` → `in_progress`. Sets owner, begins work.\n- **complete_task**: `in_progress` → `completed`. Marks the task done and unblocks downstream.\n\nThe teaching version has no `in_progress → pending` release path. In real CC, if a teammate terminates or shuts down, CC unassigns its unfinished tasks (clears owner) and resets status to `pending`, allowing other agents to reclaim them. The teaching version omits this recovery path.\n\n### Putting It Together\n\n```python\n# Create tasks with dependencies\nschema = create_task(\"setup database schema\")\nendpoints = create_task(\"create API endpoints\", blockedBy=[schema.id])\ntests = create_task(\"write tests\", blockedBy=[endpoints.id])\ndocs = create_task(\"write docs\", blockedBy=[schema.id])\n\n# Agent claims the first available task\nclaim_task(schema.id) # ✓ Claimed (no dependencies)\ncomplete_task(schema.id) # ✓ Completed → unblocks endpoints, docs\n\nclaim_task(endpoints.id) # ✓ Claimed (schema completed)\ncomplete_task(endpoints.id) # ✓ Completed → unblocks tests\n\nclaim_task(docs.id) # ✓ Claimed (schema completed)\ncomplete_task(docs.id) # ✓ Completed\n\nclaim_task(tests.id) # ✓ Claimed (endpoints completed)\ncomplete_task(tests.id) # ✓ Completed\n```\n\nEach `create_task` writes a JSON file, each `claim_task` / `complete_task` updates the file. Across sessions, the `.tasks/` directory persists — the agent reads the files to recover progress.\n\n---\n\n## Changes from s11\n\n| Component | Before (s11) | After (s12) |\n|-----------|-------------|-------------|\n| Task management | None | Task dataclass + 5 tools |\n| New types | — | Task (id, subject, description, status, owner, blockedBy) |\n| Storage | No persistence | `.tasks/{id}.json` cross-session |\n| Dependencies | None | `blockedBy` graph + `can_start` check |\n| Tools | bash, read_file, write_file (3) | + create_task, list_tasks, get_task, claim_task, complete_task (8) |\n| Lifecycle | — | pending → in_progress → completed (no release rollback) |\n\n---\n\n## Try It\n\n```sh\ncd learn-claude-code\npython s12_task_system/code.py\n```\n\nTry these prompts:\n\n1. `Create tasks: setup database schema, create API endpoints (depends on schema), write tests (depends on endpoints), write docs (depends on schema)`\n2. `List all tasks and their statuses`\n3. `Claim the first unblocked task and complete it`\n4. `List tasks again — which ones are now unblocked?`\n\nWhat to observe: Are JSON files generated in the `.tasks/` directory? After completing a task, are the blocked tasks unblocked?\n\n---\n\n## What's Next\n\nThe task graph is in place. But some tasks take a long time — like running full test suites or deploying to a server. The agent calls the LLM billed by token, it can't afford to wait on a slow operation.\n\ns13 Background Tasks → Slow operations go to the background. The agent continues processing other tasks, and gets notified when the background work is done.\n\n\nDeep Dive into CC Source
\n\n> The following is a complete analysis based on CC source code `utils/tasks.ts` (862 lines), `tools/TaskCreateTool/TaskCreateTool.ts` (138 lines), `tools/TaskUpdateTool/TaskUpdateTool.ts` (406 lines), `tools/TaskGetTool/TaskGetTool.ts` (128 lines), `tools/TaskListTool/TaskListTool.ts` (116 lines), `hooks/useTaskListWatcher.ts` (221 lines).\n\n### 1. TaskRecord's Full Fields\n\nThe tutorial only covers id, subject, status, owner, blockedBy. CC actually has 9 fields (`utils/tasks.ts:76-89`):\n\n| Field | Type | Purpose |\n|------|------|---------|\n| `id` | string | Incrementing integer ID |\n| `subject` | string | Short title |\n| `description` | string | Free-form description |\n| `activeForm` | string? | Present tense form, shown in spinner when in_progress |\n| `owner` | string? | Assigned agent ID |\n| `status` | pending/in_progress/completed | Lifecycle |\n| `blocks` | string[] | Task IDs blocked by this task (downstream) |\n| `blockedBy` | string[] | Task IDs blocking this task (upstream) |\n| `metadata` | Record? | Arbitrary extension key-value pairs |\n\nStorage location: `~/.claude/tasks/{taskListId}/{id}.json`. One file per task.\n\n### 2. Not a TodoWrite Upgrade — Two Independent Systems\n\nIn CC, Task System and TodoWrite **coexist**, toggled by `isTodoV2Enabled()` (`utils/tasks.ts:133`) — interactive sessions default to Task (V2), non-interactive/SDK sessions default to TodoWrite. The `CLAUDE_CODE_ENABLE_TASKS` env var can force-enable Task. Task has what TodoWrite lacks: file-lock concurrency protection, dependency enforcement, ownership, fs.watch reactive monitoring, lifecycle hooks.\n\n### 3. Concurrent Claim Locking\n\n`claimTask()` (`utils/tasks.ts:541-612`) uses dual locking to prevent races:\n\n**Task file lock**: `proper-lockfile` locks `{taskId}.json` (up to 30 retries, exponential backoff 5-100ms). Inside the lock:\n1. Re-read task (prevent TOCTOU)\n2. Check already claimed by another → `already_claimed`\n3. Check already completed → `already_resolved`\n4. Check upstream not completed → `blocked`\n5. Set owner\n\n**List-level lock** (agent busy check): `.lock` file, atomic scan of all tasks to check if the agent already has other open tasks.\n\nNote: The teaching version combines claiming and starting work into one step (claim = set owner + in_progress); real CC's `claimTask` primarily resolves owner competition — it only sets owner without changing status. Status updates are handled by `TaskUpdate`.\n\n### 4. High-Water Mark to Prevent ID Reuse\n\nThe `.highwatermark` file records the highest task ID ever assigned. Even if a task is deleted, its ID won't be reused.\n\n### 5. Four Task Tools\n\nCC's task system has four tools (not the tutorial's single generic Task tool): `TaskCreate`, `TaskGet`, `TaskUpdate`, `TaskList`. All set `isConcurrencySafe: true` and `shouldDefer: true` (tool schemas aren't in the initial prompt; only visible after ToolSearch).\n\nThe teaching version's `create_task(blockedBy=...)` declares dependencies at creation time, which is a reasonable simplification. Real CC's `TaskCreate` only accepts subject/description/activeForm/metadata — dependencies are maintained via `TaskUpdate`'s `addBlocks/addBlockedBy`.\n\n \n\n\n"
},
{
"version": "s12",
"locale": "zh",
"title": "s12: Task System — 目标太大,拆成小任务",
- "content": "# s12: Task System — 目标太大,拆成小任务\n\ns01 → ... → s10 → s11 → `s12` → [s13](/zh/s13) → s14 → ... → s20\n\n> *\"大目标拆成小任务, 排好序, 持久化\"* — 文件持久化的任务图, 多 agent 协作的基础。\n>\n> **Harness 层**: 任务 — 持久化的目标, 可恢复的进度。\n\n---\n\n## 问题\n\nAgent 接到一个项目:搭数据库、写 API、加测试。它用 s05 的 TodoWrite 列了一张清单,然后开始写 API,写到一半发现没数据库表,回头补;加测试时发现 API 接口签名又变了...\n\n盖房子不能先盖屋顶再打地基。任务之间有先后。任务依赖应该形成有向无环图(DAG);教学版只演示 `blockedBy` 检查,没有实现环检测。\n\ns05 的 TodoWrite 是当前任务的执行清单,保存在会话内存中。这里需要的是**任务系统**:每个任务是一个 JSON 文件,任务之间有 `blockedBy` 依赖,跨会话持久化在磁盘上。\n\n---\n\n## 解决方案\n\n\n\n教学代码保留基础 agent loop,为聚焦任务系统省略了 S11 的完整错误恢复(RecoveryState、退避、升级、reactive compact、fallback model)。新增 5 个任务工具 + `.tasks/` 目录持久化 + `blockedBy` 依赖检查。任务系统与错误恢复是独立层:CC 源码中 `utils/tasks.ts` 只管 CRUD,`query.ts` 的 with_retry/RecoveryState 管错误恢复,互不耦合。\n\nTodoWrite vs Task System:\n\n| | TodoWrite (s05) | Task System (s12) |\n|---|---|---|\n| 定位 | 当前任务的执行清单 | 可恢复的任务系统 |\n| 存储 | 进程内 / 会话状态 | `.tasks/{id}.json` |\n| 依赖 | 无 | `blockedBy` / `blocks` 依赖图 |\n| 生命周期 | 当前会话 / 当前任务 | 跨会话保留 |\n| 分工 | 不负责任务认领 | `owner` / claim |\n| 状态 | pending / in_progress / completed | pending / in_progress / completed |\n| 粒度 | Agent 自己的步骤 | 可被认领、追踪、解锁的任务 |\n\n---\n\n## 工作原理\n\n\n\n### Task: 数据结构\n\n每个任务是一个 JSON 文件,存于 `.tasks/` 目录:\n\n```python\n@dataclass\nclass Task:\n id: str\n subject: str\n description: str\n status: str # pending | in_progress | completed\n owner: str | None # Agent 名(多 Agent 场景)\n blockedBy: list[str] # 依赖的任务 ID 列表\n```\n\nID 用 `timestamp + random hex` 生成,简单但够用。CC 用顺序 ID + highwatermark 文件防止 ID 重用,是更严谨的设计。\n\n### create_task: 创建任务\n\n```python\ndef create_task(subject: str, description: str = \"\",\n blockedBy: list[str] | None = None) -> Task:\n task = Task(\n id=f\"task_{int(time.time())}_{random_hex(4)}\",\n subject=subject, description=description,\n status=\"pending\", owner=None,\n blockedBy=blockedBy or [],\n )\n save_task(task)\n return task\n```\n\n创建时自动 `save_task` 到 `.tasks/{id}.json`。`blockedBy` 声明依赖,比如 \"写 API\" 的 `blockedBy` 是 `[\"task_schema\"]`。\n\n### can_start: 依赖检查\n\n一个任务只能在它的 `blockedBy` **全部 completed** 之后才能开始:\n\n```python\ndef can_start(task_id: str) -> bool:\n task = load_task(task_id)\n for dep_id in task.blockedBy:\n if not _task_path(dep_id).exists():\n return False # missing dependency = blocked\n dep = load_task(dep_id)\n if dep.status != \"completed\":\n return False\n return True\n```\n\n`can_start` 是 `claim_task` 的前置检查:`blockedBy` 里有任何一个不是 completed,就不能认领。不存在的依赖视为 blocked,避免引用错误 ID 时崩溃。\n\n### claim_task: 认领任务\n\nAgent 开始做一个任务时,调用 `claim_task`:设置 `owner`,状态从 `pending` → `in_progress`。`owner` 字段记录谁在做这个任务,多 Agent 场景下防止重复认领:\n\n```python\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n task = load_task(task_id)\n if task.status != \"pending\":\n return f\"Task {task_id} is {task.status}, cannot claim\"\n if not can_start(task_id):\n deps = [d for d in task.blockedBy\n if load_task(d).status != \"completed\"]\n return f\"Blocked by: {deps}\"\n task.owner = owner\n task.status = \"in_progress\"\n save_task(task)\n return f\"Claimed {task_id} ({task.subject})\"\n```\n\n如果任务已被别人认领(`status != \"pending\"`),或者依赖没完成(`can_start` 返回 False),拒绝认领。\n\n### complete_task: 完成与解锁\n\n任务做完后,设为 `completed`。同时扫描所有其他任务,找出**刚刚被解锁**的下游任务:\n\n```python\ndef complete_task(task_id: str) -> str:\n task = load_task(task_id)\n task.status = \"completed\"\n save_task(task)\n # 找出被解锁的下游任务\n unblocked = [t.subject for t in list_tasks()\n if t.status == \"pending\" and t.blockedBy\n and can_start(t.id)]\n msg = f\"Completed {task_id} ({task.subject})\"\n if unblocked:\n msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n return msg\n```\n\n完成 \"schema\" 后,\"endpoints\" 和 \"docs\" 的 `can_start` 返回 True,它们可以开始。\n\n### get_task: 查看完整细节\n\n`list_tasks` 只显示一行摘要。`get_task` 返回完整的任务 JSON,包括 description 和依赖细节。跨会话恢复时,Agent 需要读取完整描述才能继续工作:\n\n```python\ndef get_task(task_id: str) -> str:\n task = load_task(task_id)\n return json.dumps(asdict(task), indent=2)\n```\n\n### 状态机: 两个动作,三个状态\n\n```\npending ──claim──→ in_progress ──complete──→ completed\n```\n\n这里的 `claim` / `complete` 是动作,`pending` / `in_progress` / `completed` 是状态:\n\n- **claim_task**: `pending` → `in_progress`。设置 owner,开始工作。\n- **complete_task**: `in_progress` → `completed`。把任务标记为完成,并解锁下游。\n\nCC 没有 `in_progress → pending` 的 release 路径。如果 teammate 终止或 shutdown,CC 会把它未完成的任务 unassign(清除 owner),并将 status 重置为 `pending`,方便其他 agent 重新认领。教学版省略了这一恢复路径。\n\n### 合起来跑\n\n```python\n# 创建有依赖的任务\nschema = create_task(\"setup database schema\")\nendpoints = create_task(\"create API endpoints\", blockedBy=[schema.id])\ntests = create_task(\"write tests\", blockedBy=[endpoints.id])\ndocs = create_task(\"write docs\", blockedBy=[schema.id])\n\n# Agent 认领第一个可做的任务\nclaim_task(schema.id) # ✓ Claimed (无依赖)\ncomplete_task(schema.id) # ✓ Completed → 解锁 endpoints, docs\n\nclaim_task(endpoints.id) # ✓ Claimed (schema 已完成)\ncomplete_task(endpoints.id) # ✓ Completed → 解锁 tests\n\nclaim_task(docs.id) # ✓ Claimed (schema 已完成)\ncomplete_task(docs.id) # ✓ Completed\n\nclaim_task(tests.id) # ✓ Claimed (endpoints 已完成)\ncomplete_task(tests.id) # ✓ Completed\n```\n\n每个 `create_task` 写一个 JSON 文件,每个 `claim_task` / `complete_task` 更新文件。跨会话时,`.tasks/` 目录还在,Agent 读文件就能恢复进度。\n\n---\n\n## 相对 s11 的变更\n\n| 组件 | 之前 (s11) | 之后 (s12) |\n|------|-----------|-----------|\n| 任务管理 | 无 | Task dataclass + 5 个工具 |\n| 新类型 | — | Task(id, subject, description, status, owner, blockedBy) |\n| 存储 | 无持久化 | `.tasks/{id}.json` 跨会话 |\n| 依赖 | 无 | `blockedBy` 图 + `can_start` 检查 |\n| 工具 | bash, read_file, write_file (3) | + create_task, list_tasks, get_task, claim_task, complete_task (8) |\n| 生命周期 | — | pending → in_progress → completed(无 release 回退) |\n\n---\n\n## 试一下\n\n```sh\ncd learn-claude-code\npython s12_task_system/code.py\n```\n\n试试这些 prompt:\n\n1. `Create tasks: setup database schema, create API endpoints (depends on schema), write tests (depends on endpoints), write docs (depends on schema)`\n2. `List all tasks and their statuses`\n3. `Claim the first unblocked task and complete it`\n4. `List tasks again — which ones are now unblocked?`\n\n观察重点:`.tasks/` 目录下是否生成了 JSON 文件?完成任务后,被阻塞的任务是否解锁?\n\n---\n\n## 接下来\n\n任务图有了。但有些任务要跑很久——比如全量测试、部署到服务器。Agent 调 LLM 按量计费,不能干等一个慢操作。\n\ns13 Background Tasks → 慢操作放后台。Agent 继续处理其他任务,后台跑完了通知它。\n\n\n深入 CC 源码
\n\n> 以下基于 CC 源码 `utils/tasks.ts`(862 行)、`tools/TaskCreateTool/TaskCreateTool.ts`(138 行)、`tools/TaskUpdateTool/TaskUpdateTool.ts`(406 行)、`tools/TaskGetTool/TaskGetTool.ts`(128 行)、`tools/TaskListTool/TaskListTool.ts`(116 行)、`hooks/useTaskListWatcher.ts`(221 行)的分析。\n\n### 一、TaskRecord 的完整字段\n\n教学版只讲了 id、subject、status、owner、blockedBy。CC 实际有 9 个字段(`utils/tasks.ts:76-89`):\n\n| 字段 | 类型 | 用途 |\n|------|------|------|\n| `id` | string | 递增整数 ID |\n| `subject` | string | 简短标题 |\n| `description` | string | 自由格式描述 |\n| `activeForm` | string? | 进行时态,in_progress 时在 spinner 显示 |\n| `owner` | string? | 分配的 agent ID |\n| `status` | pending/in_progress/completed | 生命周期 |\n| `blocks` | string[] | 此任务阻塞的任务 ID(下游) |\n| `blockedBy` | string[] | 阻塞此任务的任务 ID(上游) |\n| `metadata` | Record? | 任意扩展键值对 |\n\n存储位置:`~/.claude/tasks/{taskListId}/{id}.json`。每个任务一个文件。\n\n### 二、不是 TodoWrite 的升级,是两个独立系统\n\nCC 中 Task System 和 TodoWrite **同时存在**,通过 `isTodoV2Enabled()` 切换(`utils/tasks.ts:133`)——交互式会话默认启用 Task(V2),非交互式/SDK 默认用 TodoWrite。环境变量 `CLAUDE_CODE_ENABLE_TASKS` 可强制启用 Task。Task 有 TodoWrite 没有的:文件锁并发保护、依赖强制执行、ownership、fs.watch 响应式监听、生命周期 hooks。\n\n### 三、并发认领的锁机制\n\n`claimTask()`(`utils/tasks.ts:541-612`)用双重锁防竞争:\n\n**任务文件锁**:`proper-lockfile` 锁住 `{taskId}.json`(最多重试 30 次,指数退避 5-100ms)。锁内:\n1. 重新读取任务(防 TOCTOU)\n2. 检查已被他人认领 → `already_claimed`\n3. 检查已完成 → `already_resolved`\n4. 检查上游未完成 → `blocked`\n5. 设置 owner\n\n**列表级锁**(agent busy 检查时):`.lock` 文件,原子性扫描所有任务并检查该 agent 是否已有其他 open task。\n\n注意:教学版把 claim 和开始工作合成一步(claim = set owner + in_progress);真实 CC 的 `claimTask` 主要解决 owner 竞争,只设 owner 不改 status,状态更新由 `TaskUpdate` 完成。\n\n### 四、高水位标防 ID 重用\n\n`.highwatermark` 文件记录曾分配过的最高任务 ID。即使任务被删除,ID 也不会被重用。\n\n### 五、四个 Task 工具\n\nCC 的任务系统有四个工具(不是教学版的一个通用 Task 工具):`TaskCreate`、`TaskGet`、`TaskUpdate`、`TaskList`。全部设置 `isConcurrencySafe: true` 和 `shouldDefer: true`(工具 schema 不在初始 prompt 中,需 ToolSearch 后才可见)。\n\n教学版的 `create_task(blockedBy=...)` 在创建时直接声明依赖,是合理简化。真实 CC 的 `TaskCreate` 只接受 subject/description/activeForm/metadata,依赖关系由 `TaskUpdate` 的 `addBlocks/addBlockedBy` 维护。\n\n \n\n\n"
+ "content": "# s12: Task System — 目标太大,拆成小任务\n\ns01 → ... → s10 → s11 → `s12` → [s13](/zh/s13) → s14 → ... → s20\n\n> *\"大目标拆成小任务, 排好序, 持久化\"* — 文件持久化的任务图, 多 agent 协作的基础。\n>\n> **Harness 层**: 任务 — 持久化的目标, 可恢复的进度。\n\n---\n\n## 问题\n\nAgent 接到一个项目:搭数据库、写 API、加测试。它用 s05 的 TodoWrite 列了一张清单,然后开始写 API,写到一半发现没数据库表,回头补;加测试时发现 API 接口签名又变了...\n\n盖房子不能先盖屋顶再打地基。任务之间有先后。任务依赖应该形成有向无环图(DAG);教学版只演示 `blockedBy` 检查,没有实现环检测。\n\ns05 的 TodoWrite 是当前任务的执行清单,保存在会话内存中。这里需要的是**任务系统**:每个任务是一个 JSON 文件,任务之间有 `blockedBy` 依赖,跨会话持久化在磁盘上。\n\n---\n\n## 解决方案\n\n\n\n教学代码保留基础 agent loop,为聚焦任务系统省略了 S11 的完整错误恢复(RecoveryState、退避、升级、reactive compact、fallback model)。新增 5 个任务工具 + `.tasks/` 目录持久化 + `blockedBy` 依赖检查。任务系统与错误恢复是独立层:CC 源码中 `utils/tasks.ts` 只管 CRUD,`query.ts` 的 with_retry/RecoveryState 管错误恢复,互不耦合。\n\nTodoWrite vs Task System:\n\n| | TodoWrite (s05) | Task System (s12) |\n|---|---|---|\n| 定位 | 当前任务的执行清单 | 可恢复的任务系统 |\n| 存储 | 进程内 / 会话状态 | `.tasks/{id}.json` |\n| 依赖 | 无 | `blockedBy` / `blocks` 依赖图 |\n| 生命周期 | 当前会话 / 当前任务 | 跨会话保留 |\n| 分工 | 不负责任务认领 | `owner` / claim |\n| 状态 | pending / in_progress / completed | pending / in_progress / completed |\n| 粒度 | Agent 自己的步骤 | 可被认领、追踪、解锁的任务 |\n\n---\n\n## 工作原理\n\n\n\n### Task: 数据结构\n\n每个任务是一个 JSON 文件,存于 `.tasks/` 目录:\n\n```python\n@dataclass\nclass Task:\n id: str\n subject: str\n description: str\n status: str # pending | in_progress | completed\n owner: str | None # Agent 名(多 Agent 场景)\n blockedBy: list[str] # 依赖的任务 ID 列表\n```\n\nID 用 `timestamp + random hex` 生成,简单但够用。CC 用顺序 ID + highwatermark 文件防止 ID 重用,是更严谨的设计。\n\n### create_task: 创建任务\n\n```python\ndef create_task(subject: str, description: str = \"\",\n blockedBy: list[str] | None = None) -> Task:\n task = Task(\n id=f\"task_{int(time.time())}_{random_hex(4)}\",\n subject=subject, description=description,\n status=\"pending\", owner=None,\n blockedBy=blockedBy or [],\n )\n save_task(task)\n return task\n```\n\n创建时自动 `save_task` 到 `.tasks/{id}.json`。`blockedBy` 声明依赖,比如 \"写 API\" 的 `blockedBy` 是 `[\"task_schema\"]`。\n\n### can_start: 依赖检查\n\n一个任务只能在它的 `blockedBy` **全部 completed** 之后才能开始:\n\n```python\ndef can_start(task_id: str) -> bool:\n task = load_task(task_id)\n for dep_id in task.blockedBy:\n if not _task_path(dep_id).exists():\n return False # missing dependency = blocked\n dep = load_task(dep_id)\n if dep.status != \"completed\":\n return False\n return True\n```\n\n`can_start` 是 `claim_task` 的前置检查:`blockedBy` 里有任何一个不是 completed,就不能认领。不存在的依赖视为 blocked,避免引用错误 ID 时崩溃。\n\n### claim_task: 认领任务\n\nAgent 开始做一个任务时,调用 `claim_task`:设置 `owner`,状态从 `pending` → `in_progress`。`owner` 字段记录谁在做这个任务,多 Agent 场景下防止重复认领:\n\n```python\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n task = load_task(task_id)\n if task.status != \"pending\":\n return f\"Task {task_id} is {task.status}, cannot claim\"\n if not can_start(task_id):\n deps = [d for d in task.blockedBy\n if load_task(d).status != \"completed\"]\n return f\"Blocked by: {deps}\"\n task.owner = owner\n task.status = \"in_progress\"\n save_task(task)\n return f\"Claimed {task_id} ({task.subject})\"\n```\n\n如果任务已被别人认领(`status != \"pending\"`),或者依赖没完成(`can_start` 返回 False),拒绝认领。\n\n### complete_task: 完成与解锁\n\n任务做完后,设为 `completed`。同时扫描所有其他任务,找出**刚刚被解锁**的下游任务:\n\n```python\ndef complete_task(task_id: str) -> str:\n task = load_task(task_id)\n task.status = \"completed\"\n save_task(task)\n # 找出被解锁的下游任务\n unblocked = [t.subject for t in list_tasks()\n if t.status == \"pending\" and t.blockedBy\n and can_start(t.id)]\n msg = f\"Completed {task_id} ({task.subject})\"\n if unblocked:\n msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n return msg\n```\n\n完成 \"schema\" 后,\"endpoints\" 和 \"docs\" 的 `can_start` 返回 True,它们可以开始。\n\n### get_task: 查看完整细节\n\n`list_tasks` 只显示一行摘要。`get_task` 返回完整的任务 JSON,包括 description 和依赖细节。跨会话恢复时,Agent 需要读取完整描述才能继续工作:\n\n```python\ndef get_task(task_id: str) -> str:\n task = load_task(task_id)\n return json.dumps(asdict(task), indent=2)\n```\n\n### 状态机: 两个动作,三个状态\n\n```\npending ──claim──→ in_progress ──complete──→ completed\n```\n\n这里的 `claim` / `complete` 是动作,`pending` / `in_progress` / `completed` 是状态:\n\n- **claim_task**: `pending` → `in_progress`。设置 owner,开始工作。\n- **complete_task**: `in_progress` → `completed`。把任务标记为完成,并解锁下游。\n\n教学版没有 `in_progress → pending` 的 release 路径。真实 CC 中,如果 teammate 终止或 shutdown,CC 会把它未完成的任务 unassign(清除 owner),并将 status 重置为 `pending`,方便其他 agent 重新认领。教学版省略了这一恢复路径。\n\n### 合起来跑\n\n```python\n# 创建有依赖的任务\nschema = create_task(\"setup database schema\")\nendpoints = create_task(\"create API endpoints\", blockedBy=[schema.id])\ntests = create_task(\"write tests\", blockedBy=[endpoints.id])\ndocs = create_task(\"write docs\", blockedBy=[schema.id])\n\n# Agent 认领第一个可做的任务\nclaim_task(schema.id) # ✓ Claimed (无依赖)\ncomplete_task(schema.id) # ✓ Completed → 解锁 endpoints, docs\n\nclaim_task(endpoints.id) # ✓ Claimed (schema 已完成)\ncomplete_task(endpoints.id) # ✓ Completed → 解锁 tests\n\nclaim_task(docs.id) # ✓ Claimed (schema 已完成)\ncomplete_task(docs.id) # ✓ Completed\n\nclaim_task(tests.id) # ✓ Claimed (endpoints 已完成)\ncomplete_task(tests.id) # ✓ Completed\n```\n\n每个 `create_task` 写一个 JSON 文件,每个 `claim_task` / `complete_task` 更新文件。跨会话时,`.tasks/` 目录还在,Agent 读文件就能恢复进度。\n\n---\n\n## 相对 s11 的变更\n\n| 组件 | 之前 (s11) | 之后 (s12) |\n|------|-----------|-----------|\n| 任务管理 | 无 | Task dataclass + 5 个工具 |\n| 新类型 | — | Task(id, subject, description, status, owner, blockedBy) |\n| 存储 | 无持久化 | `.tasks/{id}.json` 跨会话 |\n| 依赖 | 无 | `blockedBy` 图 + `can_start` 检查 |\n| 工具 | bash, read_file, write_file (3) | + create_task, list_tasks, get_task, claim_task, complete_task (8) |\n| 生命周期 | — | pending → in_progress → completed(无 release 回退) |\n\n---\n\n## 试一下\n\n```sh\ncd learn-claude-code\npython s12_task_system/code.py\n```\n\n试试这些 prompt:\n\n1. `Create tasks: setup database schema, create API endpoints (depends on schema), write tests (depends on endpoints), write docs (depends on schema)`\n2. `List all tasks and their statuses`\n3. `Claim the first unblocked task and complete it`\n4. `List tasks again — which ones are now unblocked?`\n\n观察重点:`.tasks/` 目录下是否生成了 JSON 文件?完成任务后,被阻塞的任务是否解锁?\n\n---\n\n## 接下来\n\n任务图有了。但有些任务要跑很久——比如全量测试、部署到服务器。Agent 调 LLM 按量计费,不能干等一个慢操作。\n\ns13 Background Tasks → 慢操作放后台。Agent 继续处理其他任务,后台跑完了通知它。\n\n\n深入 CC 源码
\n\n> 以下基于 CC 源码 `utils/tasks.ts`(862 行)、`tools/TaskCreateTool/TaskCreateTool.ts`(138 行)、`tools/TaskUpdateTool/TaskUpdateTool.ts`(406 行)、`tools/TaskGetTool/TaskGetTool.ts`(128 行)、`tools/TaskListTool/TaskListTool.ts`(116 行)、`hooks/useTaskListWatcher.ts`(221 行)的分析。\n\n### 一、TaskRecord 的完整字段\n\n教学版只讲了 id、subject、status、owner、blockedBy。CC 实际有 9 个字段(`utils/tasks.ts:76-89`):\n\n| 字段 | 类型 | 用途 |\n|------|------|------|\n| `id` | string | 递增整数 ID |\n| `subject` | string | 简短标题 |\n| `description` | string | 自由格式描述 |\n| `activeForm` | string? | 进行时态,in_progress 时在 spinner 显示 |\n| `owner` | string? | 分配的 agent ID |\n| `status` | pending/in_progress/completed | 生命周期 |\n| `blocks` | string[] | 此任务阻塞的任务 ID(下游) |\n| `blockedBy` | string[] | 阻塞此任务的任务 ID(上游) |\n| `metadata` | Record? | 任意扩展键值对 |\n\n存储位置:`~/.claude/tasks/{taskListId}/{id}.json`。每个任务一个文件。\n\n### 二、不是 TodoWrite 的升级,是两个独立系统\n\nCC 中 Task System 和 TodoWrite **同时存在**,通过 `isTodoV2Enabled()` 切换(`utils/tasks.ts:133`)——交互式会话默认启用 Task(V2),非交互式/SDK 默认用 TodoWrite。环境变量 `CLAUDE_CODE_ENABLE_TASKS` 可强制启用 Task。Task 有 TodoWrite 没有的:文件锁并发保护、依赖强制执行、ownership、fs.watch 响应式监听、生命周期 hooks。\n\n### 三、并发认领的锁机制\n\n`claimTask()`(`utils/tasks.ts:541-612`)用双重锁防竞争:\n\n**任务文件锁**:`proper-lockfile` 锁住 `{taskId}.json`(最多重试 30 次,指数退避 5-100ms)。锁内:\n1. 重新读取任务(防 TOCTOU)\n2. 检查已被他人认领 → `already_claimed`\n3. 检查已完成 → `already_resolved`\n4. 检查上游未完成 → `blocked`\n5. 设置 owner\n\n**列表级锁**(agent busy 检查时):`.lock` 文件,原子性扫描所有任务并检查该 agent 是否已有其他 open task。\n\n注意:教学版把 claim 和开始工作合成一步(claim = set owner + in_progress);真实 CC 的 `claimTask` 主要解决 owner 竞争,只设 owner 不改 status,状态更新由 `TaskUpdate` 完成。\n\n### 四、高水位标防 ID 重用\n\n`.highwatermark` 文件记录曾分配过的最高任务 ID。即使任务被删除,ID 也不会被重用。\n\n### 五、四个 Task 工具\n\nCC 的任务系统有四个工具(不是教学版的一个通用 Task 工具):`TaskCreate`、`TaskGet`、`TaskUpdate`、`TaskList`。全部设置 `isConcurrencySafe: true` 和 `shouldDefer: true`(工具 schema 不在初始 prompt 中,需 ToolSearch 后才可见)。\n\n教学版的 `create_task(blockedBy=...)` 在创建时直接声明依赖,是合理简化。真实 CC 的 `TaskCreate` 只接受 subject/description/activeForm/metadata,依赖关系由 `TaskUpdate` 的 `addBlocks/addBlockedBy` 维护。\n\n \n\n\n"
},
{
"version": "s12",
"locale": "ja",
"title": "s12: Task System — 大きな目標を小さなタスクに分割",
- "content": "# s12: Task System — 大きな目標を小さなタスクに分割\n\ns01 → ... → s10 → s11 → `s12` → [s13](/ja/s13) → s14 → ... → s20\n\n> *\"大きな目標を小さなタスクに分け、順序付け、永続化\"* — ファイル永続化タスクグラフ、マルチ Agent 協調の基盤。\n>\n> **Harness 層**: タスク — 永続化された目標、復旧可能な進捗。\n\n---\n\n## 課題\n\nAgent がプロジェクトを受けた:データベース構築、API 実装、テスト追加。s05 の TodoWrite でリストを作り、まず API を書き始め、途中でデータベーステーブルがないことに気づいて戻る。テスト追加時に API インターフェースのシグネチャがまた変わっている...\n\n屋根を先に建てて基礎を後から打つことはできない。タスクには順序がある。タスクの依存関係は有向非巡回グラフ(DAG)を形成すべき;教学版は `blockedBy` チェックのみをデモし、循環検出は実装していない。\n\ns05 の TodoWrite は現在のタスクの実行チェックリストで、セッションメモリに保持される。ここで必要なのは**タスクシステム**:各タスクは JSON ファイル、タスク間に `blockedBy` 依存関係、ディスク上でセッションをまたいで永続化。\n\n---\n\n## ソリューション\n\n\n\n教学版は基本 agent loop を維持し、タスクシステムに集中するため S11 の完全なエラーリカバリ(RecoveryState、バックオフ、エスカレーション、reactive compact、フォールバックモデル)を省略。追加:5 つの新規タスクツール + `.tasks/` ディレクトリによる永続化 + `blockedBy` 依存チェック。タスクシステムとエラーリカバリは独立したレイヤー:CC ソースコードでは `utils/tasks.ts` は CRUD のみ、`query.ts` の with_retry/RecoveryState がエラーリカバリを担当し、互いに非結合。\n\nTodoWrite vs Task System:\n\n| | TodoWrite (s05) | Task System (s12) |\n|---|---|---|\n| 位置づけ | 現在のタスクの実行チェックリスト | 復旧可能なタスクシステム |\n| ストレージ | プロセス内 / セッション状態 | `.tasks/{id}.json` |\n| 依存関係 | なし | `blockedBy` / `blocks` グラフ |\n| ライフサイクル | 現在のセッション / 現在のタスク | セッション横断 |\n| 分担 | タスク認識を扱わない | `owner` / claim |\n| ステータス | pending / in_progress / completed | pending / in_progress / completed |\n| 粒度 | Agent 自身の手順 | 認識・追跡・アンロックできるタスク |\n\n---\n\n## 仕組み\n\n\n\n### Task: データ構造\n\n各タスクは JSON ファイル、`.tasks/` ディレクトリに保存:\n\n```python\n@dataclass\nclass Task:\n id: str\n subject: str\n description: str\n status: str # pending | in_progress | completed\n owner: str | None # Agent 名(マルチ Agent シナリオ)\n blockedBy: list[str] # 依存タスク ID のリスト\n```\n\nID は `timestamp + random hex` で生成、シンプルだが十分。CC は順次 ID + highwatermark ファイルで ID 再利用を防止する、より厳密な設計。\n\n### create_task: タスク作成\n\n```python\ndef create_task(subject: str, description: str = \"\",\n blockedBy: list[str] | None = None) -> Task:\n task = Task(\n id=f\"task_{int(time.time())}_{random_hex(4)}\",\n subject=subject, description=description,\n status=\"pending\", owner=None,\n blockedBy=blockedBy or [],\n )\n save_task(task)\n return task\n```\n\n作成時に自動的に `save_task` で `.tasks/{id}.json` に書き込み。`blockedBy` で依存を宣言、例えば \"API を書く\" の `blockedBy` は `[\"task_schema\"]`。\n\n### can_start: 依存チェック\n\nタスクは `blockedBy` が**すべて completed** になってからでないと開始できない:\n\n```python\ndef can_start(task_id: str) -> bool:\n task = load_task(task_id)\n for dep_id in task.blockedBy:\n if not _task_path(dep_id).exists():\n return False # missing dependency = blocked\n dep = load_task(dep_id)\n if dep.status != \"completed\":\n return False\n return True\n```\n\n`can_start` は `claim_task` の事前チェック:`blockedBy` に一つでも completed でないものがあれば、認識不可。存在しない依存は blocked として扱い、誤った ID 参照時のクラッシュを防ぐ。\n\n### claim_task: タスク認識\n\nAgent がタスクに取り掛かる時、`claim_task` を呼び出し:`owner` を設定、ステータスを `pending` → `in_progress` に変更。`owner` フィールドは誰が作業中かを記録し、マルチ Agent シナリオで重複認識を防止:\n\n```python\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n task = load_task(task_id)\n if task.status != \"pending\":\n return f\"Task {task_id} is {task.status}, cannot claim\"\n if not can_start(task_id):\n deps = [d for d in task.blockedBy\n if load_task(d).status != \"completed\"]\n return f\"Blocked by: {deps}\"\n task.owner = owner\n task.status = \"in_progress\"\n save_task(task)\n return f\"Claimed {task_id} ({task.subject})\"\n```\n\nタスクが既に他者に認識されている(`status != \"pending\"`)、または依存が未完了(`can_start` が False)の場合、認識を拒否。\n\n### complete_task: 完了とアンロック\n\nタスク完了後、`completed` に設定。同時に他の全タスクを走査し、**直前にアンロックされた**下流タスクを特定:\n\n```python\ndef complete_task(task_id: str) -> str:\n task = load_task(task_id)\n task.status = \"completed\"\n save_task(task)\n # アンロックされた下流タスクを検索\n unblocked = [t.subject for t in list_tasks()\n if t.status == \"pending\" and t.blockedBy\n and can_start(t.id)]\n msg = f\"Completed {task_id} ({task.subject})\"\n if unblocked:\n msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n return msg\n```\n\n\"schema\" 完了後、\"endpoints\" と \"docs\" の `can_start` が True を返し、開始可能になる。\n\n### get_task: 完全な詳細を確認\n\n`list_tasks` は 1 行サマリのみ表示。`get_task` は description と依存関係の詳細を含む完全なタスク JSON を返す。セッションをまたいで復旧する際、Agent は完全な説明を読んで作業を継続する必要がある:\n\n```python\ndef get_task(task_id: str) -> str:\n task = load_task(task_id)\n return json.dumps(asdict(task), indent=2)\n```\n\n### 状態マシン: 2 つのアクション、3 つの状態\n\n```\npending ──claim──→ in_progress ──complete──→ completed\n```\n\nここで `claim` / `complete` はアクション、`pending` / `in_progress` / `completed` は状態:\n\n- **claim_task**: `pending` → `in_progress`。owner を設定し、作業を開始。\n- **complete_task**: `in_progress` → `completed`。タスクを完了済みにし、下流をアンロック。\n\nCC には `in_progress → pending` の release パスがない。teammate が終了または shutdown した場合、CC は未完了タスクの owner をクリアし、status を `pending` にリセットし、他の agent が再認識できるようにする。教学版はこの復旧パスを省略。\n\n### 組み合わせて実行\n\n```python\n# 依存関係のあるタスクを作成\nschema = create_task(\"setup database schema\")\nendpoints = create_task(\"create API endpoints\", blockedBy=[schema.id])\ntests = create_task(\"write tests\", blockedBy=[endpoints.id])\ndocs = create_task(\"write docs\", blockedBy=[schema.id])\n\n# Agent が最初に実行可能なタスクを認識\nclaim_task(schema.id) # ✓ Claimed(依存なし)\ncomplete_task(schema.id) # ✓ Completed → endpoints, docs をアンロック\n\nclaim_task(endpoints.id) # ✓ Claimed(schema 完了済み)\ncomplete_task(endpoints.id) # ✓ Completed → tests をアンロック\n\nclaim_task(docs.id) # ✓ Claimed(schema 完了済み)\ncomplete_task(docs.id) # ✓ Completed\n\nclaim_task(tests.id) # ✓ Claimed(endpoints 完了済み)\ncomplete_task(tests.id) # ✓ Completed\n```\n\n各 `create_task` が JSON ファイルを書き込み、各 `claim_task` / `complete_task` がファイルを更新。セッションをまたいでも `.tasks/` ディレクトリが残り、Agent はファイルを読んで進捗を復旧。\n\n---\n\n## s11 からの変更\n\n| コンポーネント | 変更前 (s11) | 変更後 (s12) |\n|--------------|------------|------------|\n| タスク管理 | なし | Task dataclass + 5 ツール |\n| 新規型 | — | Task(id, subject, description, status, owner, blockedBy) |\n| ストレージ | 永続化なし | `.tasks/{id}.json` セッション横断 |\n| 依存関係 | なし | `blockedBy` グラフ + `can_start` チェック |\n| ツール | bash, read_file, write_file (3) | + create_task, list_tasks, get_task, claim_task, complete_task (8) |\n| ライフサイクル | — | pending → in_progress → completed(release ロールバックなし) |\n\n---\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython s12_task_system/code.py\n```\n\n以下のプロンプトを試してください:\n\n1. `Create tasks: setup database schema, create API endpoints (depends on schema), write tests (depends on endpoints), write docs (depends on schema)`\n2. `List all tasks and their statuses`\n3. `Claim the first unblocked task and complete it`\n4. `List tasks again — which ones are now unblocked?`\n\n観察ポイント:`.tasks/` ディレクトリに JSON ファイルが生成されているか?タスク完了後、ブロックされていたタスクがアンロックされているか?\n\n---\n\n## 次の章\n\nタスクグラフができた。しかし、一部のタスクは長時間かかる — 全テスト実行やサーバーデプロイなど。Agent は LLM をトークン課金で呼び出しており、遅い操作を待つ余裕はない。\n\ns13 Background Tasks → 遅い操作はバックグラウンドへ。Agent は他のタスクの処理を続け、バックグラウンドの完了を通知で受け取る。\n\n\nCC ソースコード深掘り
\n\n> 以下は CC ソースコード `utils/tasks.ts`(862 行)、`tools/TaskCreateTool/TaskCreateTool.ts`(138 行)、`tools/TaskUpdateTool/TaskUpdateTool.ts`(406 行)、`tools/TaskGetTool/TaskGetTool.ts`(128 行)、`tools/TaskListTool/TaskListTool.ts`(116 行)、`hooks/useTaskListWatcher.ts`(221 行)の完全分析に基づく。\n\n### 一、TaskRecord の完全フィールド\n\nチュートリアルでは id、subject、status、owner、blockedBy のみ解説。CC は実際に 9 フィールドを持つ(`utils/tasks.ts:76-89`):\n\n| フィールド | 型 | 用途 |\n|------|------|------|\n| `id` | string | 昇順整数 ID |\n| `subject` | string | 短いタイトル |\n| `description` | string | 自由形式の説明 |\n| `activeForm` | string? | 現在進行形、in_progress 時にスピナーに表示 |\n| `owner` | string? | 割り当てられた agent ID |\n| `status` | pending/in_progress/completed | ライフサイクル |\n| `blocks` | string[] | このタスクがブロックするタスク ID(下流) |\n| `blockedBy` | string[] | このタスクをブロックするタスク ID(上流) |\n| `metadata` | Record? | 任意の拡張キーバリューペア |\n\n保存場所:`~/.claude/tasks/{taskListId}/{id}.json`。タスクごとに 1 ファイル。\n\n### 二、TodoWrite のアップグレードではなく、2 つの独立システム\n\nCC では Task System と TodoWrite **は共存**し、`isTodoV2Enabled()` で切り替え(`utils/tasks.ts:133`)— 対話セッションはデフォルトで Task (V2)、非対話/SDK セッションは TodoWrite。環境変数 `CLAUDE_CODE_ENABLE_TASKS` で Task を強制有効化可能。Task は TodoWrite にない機能を持つ:ファイルロック並行保護、依存関係強制、ownership、fs.watch リアクティブ監視、ライフサイクルフック。\n\n### 三、並行認識のロック機構\n\n`claimTask()`(`utils/tasks.ts:541-612`)は二重ロックで競合を防止:\n\n**タスクファイルロック**:`proper-lockfile` で `{taskId}.json` をロック(最大 30 リトライ、指数バックオフ 5-100ms)。ロック内:\n1. タスクを再読込(TOCTOU 防止)\n2. 既に他者が認識済み → `already_claimed`\n3. 既に完了済み → `already_resolved`\n4. 上流が未完了 → `blocked`\n5. owner を設定\n\n**リストレベルロック**(agent busy チェック時):`.lock` ファイル、全タスクを原子的に走査し該当 agent が他の open task を持つか確認。\n\n注意:教学版は認識と作業開始を 1 ステップに統合(claim = owner 設定 + in_progress);実際の CC の `claimTask` は主に owner 競合を解決し、owner のみを設定して status は変更しない。status の更新は `TaskUpdate` が担当。\n\n### 四、高水位標による ID 再利用防止\n\n`.highwatermark` ファイルが過去に割り当てられた最大タスク ID を記録。タスクが削除されても ID は再利用されない。\n\n### 五、4 つの Task ツール\n\nCC のタスクシステムは 4 つのツールを持つ(チュートリアルの汎用 Task ツールとは異なる):`TaskCreate`、`TaskGet`、`TaskUpdate`、`TaskList`。すべて `isConcurrencySafe: true` と `shouldDefer: true` が設定(ツールスキーマは初期プロンプトに含まれず、ToolSearch 後にのみ可視)。\n\n教学版の `create_task(blockedBy=...)` は作成時に直接依存を宣言する合理な簡略化。実際の CC の `TaskCreate` は subject/description/activeForm/metadata のみを受け付け、依存関係は `TaskUpdate` の `addBlocks/addBlockedBy` で管理される。\n\n \n\n\n"
+ "content": "# s12: Task System — 大きな目標を小さなタスクに分割\n\ns01 → ... → s10 → s11 → `s12` → [s13](/ja/s13) → s14 → ... → s20\n\n> *\"大きな目標を小さなタスクに分け、順序付け、永続化\"* — ファイル永続化タスクグラフ、マルチ Agent 協調の基盤。\n>\n> **Harness 層**: タスク — 永続化された目標、復旧可能な進捗。\n\n---\n\n## 課題\n\nAgent がプロジェクトを受けた:データベース構築、API 実装、テスト追加。s05 の TodoWrite でリストを作り、まず API を書き始め、途中でデータベーステーブルがないことに気づいて戻る。テスト追加時に API インターフェースのシグネチャがまた変わっている...\n\n屋根を先に建てて基礎を後から打つことはできない。タスクには順序がある。タスクの依存関係は有向非巡回グラフ(DAG)を形成すべき;教学版は `blockedBy` チェックのみをデモし、循環検出は実装していない。\n\ns05 の TodoWrite は現在のタスクの実行チェックリストで、セッションメモリに保持される。ここで必要なのは**タスクシステム**:各タスクは JSON ファイル、タスク間に `blockedBy` 依存関係、ディスク上でセッションをまたいで永続化。\n\n---\n\n## ソリューション\n\n\n\n教学版は基本 agent loop を維持し、タスクシステムに集中するため S11 の完全なエラーリカバリ(RecoveryState、バックオフ、エスカレーション、reactive compact、フォールバックモデル)を省略。追加:5 つの新規タスクツール + `.tasks/` ディレクトリによる永続化 + `blockedBy` 依存チェック。タスクシステムとエラーリカバリは独立したレイヤー:CC ソースコードでは `utils/tasks.ts` は CRUD のみ、`query.ts` の with_retry/RecoveryState がエラーリカバリを担当し、互いに非結合。\n\nTodoWrite vs Task System:\n\n| | TodoWrite (s05) | Task System (s12) |\n|---|---|---|\n| 位置づけ | 現在のタスクの実行チェックリスト | 復旧可能なタスクシステム |\n| ストレージ | プロセス内 / セッション状態 | `.tasks/{id}.json` |\n| 依存関係 | なし | `blockedBy` / `blocks` グラフ |\n| ライフサイクル | 現在のセッション / 現在のタスク | セッション横断 |\n| 分担 | タスク認識を扱わない | `owner` / claim |\n| ステータス | pending / in_progress / completed | pending / in_progress / completed |\n| 粒度 | Agent 自身の手順 | 認識・追跡・アンロックできるタスク |\n\n---\n\n## 仕組み\n\n\n\n### Task: データ構造\n\n各タスクは JSON ファイル、`.tasks/` ディレクトリに保存:\n\n```python\n@dataclass\nclass Task:\n id: str\n subject: str\n description: str\n status: str # pending | in_progress | completed\n owner: str | None # Agent 名(マルチ Agent シナリオ)\n blockedBy: list[str] # 依存タスク ID のリスト\n```\n\nID は `timestamp + random hex` で生成、シンプルだが十分。CC は順次 ID + highwatermark ファイルで ID 再利用を防止する、より厳密な設計。\n\n### create_task: タスク作成\n\n```python\ndef create_task(subject: str, description: str = \"\",\n blockedBy: list[str] | None = None) -> Task:\n task = Task(\n id=f\"task_{int(time.time())}_{random_hex(4)}\",\n subject=subject, description=description,\n status=\"pending\", owner=None,\n blockedBy=blockedBy or [],\n )\n save_task(task)\n return task\n```\n\n作成時に自動的に `save_task` で `.tasks/{id}.json` に書き込み。`blockedBy` で依存を宣言、例えば \"API を書く\" の `blockedBy` は `[\"task_schema\"]`。\n\n### can_start: 依存チェック\n\nタスクは `blockedBy` が**すべて completed** になってからでないと開始できない:\n\n```python\ndef can_start(task_id: str) -> bool:\n task = load_task(task_id)\n for dep_id in task.blockedBy:\n if not _task_path(dep_id).exists():\n return False # missing dependency = blocked\n dep = load_task(dep_id)\n if dep.status != \"completed\":\n return False\n return True\n```\n\n`can_start` は `claim_task` の事前チェック:`blockedBy` に一つでも completed でないものがあれば、認識不可。存在しない依存は blocked として扱い、誤った ID 参照時のクラッシュを防ぐ。\n\n### claim_task: タスク認識\n\nAgent がタスクに取り掛かる時、`claim_task` を呼び出し:`owner` を設定、ステータスを `pending` → `in_progress` に変更。`owner` フィールドは誰が作業中かを記録し、マルチ Agent シナリオで重複認識を防止:\n\n```python\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n task = load_task(task_id)\n if task.status != \"pending\":\n return f\"Task {task_id} is {task.status}, cannot claim\"\n if not can_start(task_id):\n deps = [d for d in task.blockedBy\n if load_task(d).status != \"completed\"]\n return f\"Blocked by: {deps}\"\n task.owner = owner\n task.status = \"in_progress\"\n save_task(task)\n return f\"Claimed {task_id} ({task.subject})\"\n```\n\nタスクが既に他者に認識されている(`status != \"pending\"`)、または依存が未完了(`can_start` が False)の場合、認識を拒否。\n\n### complete_task: 完了とアンロック\n\nタスク完了後、`completed` に設定。同時に他の全タスクを走査し、**直前にアンロックされた**下流タスクを特定:\n\n```python\ndef complete_task(task_id: str) -> str:\n task = load_task(task_id)\n task.status = \"completed\"\n save_task(task)\n # アンロックされた下流タスクを検索\n unblocked = [t.subject for t in list_tasks()\n if t.status == \"pending\" and t.blockedBy\n and can_start(t.id)]\n msg = f\"Completed {task_id} ({task.subject})\"\n if unblocked:\n msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n return msg\n```\n\n\"schema\" 完了後、\"endpoints\" と \"docs\" の `can_start` が True を返し、開始可能になる。\n\n### get_task: 完全な詳細を確認\n\n`list_tasks` は 1 行サマリのみ表示。`get_task` は description と依存関係の詳細を含む完全なタスク JSON を返す。セッションをまたいで復旧する際、Agent は完全な説明を読んで作業を継続する必要がある:\n\n```python\ndef get_task(task_id: str) -> str:\n task = load_task(task_id)\n return json.dumps(asdict(task), indent=2)\n```\n\n### 状態マシン: 2 つのアクション、3 つの状態\n\n```\npending ──claim──→ in_progress ──complete──→ completed\n```\n\nここで `claim` / `complete` はアクション、`pending` / `in_progress` / `completed` は状態:\n\n- **claim_task**: `pending` → `in_progress`。owner を設定し、作業を開始。\n- **complete_task**: `in_progress` → `completed`。タスクを完了済みにし、下流をアンロック。\n\n教学版には `in_progress → pending` の release パスがない。実際の CC では、teammate が終了または shutdown した場合、CC は未完了タスクの owner をクリアし、status を `pending` にリセットし、他の agent が再認識できるようにする。教学版はこの復旧パスを省略。\n\n### 組み合わせて実行\n\n```python\n# 依存関係のあるタスクを作成\nschema = create_task(\"setup database schema\")\nendpoints = create_task(\"create API endpoints\", blockedBy=[schema.id])\ntests = create_task(\"write tests\", blockedBy=[endpoints.id])\ndocs = create_task(\"write docs\", blockedBy=[schema.id])\n\n# Agent が最初に実行可能なタスクを認識\nclaim_task(schema.id) # ✓ Claimed(依存なし)\ncomplete_task(schema.id) # ✓ Completed → endpoints, docs をアンロック\n\nclaim_task(endpoints.id) # ✓ Claimed(schema 完了済み)\ncomplete_task(endpoints.id) # ✓ Completed → tests をアンロック\n\nclaim_task(docs.id) # ✓ Claimed(schema 完了済み)\ncomplete_task(docs.id) # ✓ Completed\n\nclaim_task(tests.id) # ✓ Claimed(endpoints 完了済み)\ncomplete_task(tests.id) # ✓ Completed\n```\n\n各 `create_task` が JSON ファイルを書き込み、各 `claim_task` / `complete_task` がファイルを更新。セッションをまたいでも `.tasks/` ディレクトリが残り、Agent はファイルを読んで進捗を復旧。\n\n---\n\n## s11 からの変更\n\n| コンポーネント | 変更前 (s11) | 変更後 (s12) |\n|--------------|------------|------------|\n| タスク管理 | なし | Task dataclass + 5 ツール |\n| 新規型 | — | Task(id, subject, description, status, owner, blockedBy) |\n| ストレージ | 永続化なし | `.tasks/{id}.json` セッション横断 |\n| 依存関係 | なし | `blockedBy` グラフ + `can_start` チェック |\n| ツール | bash, read_file, write_file (3) | + create_task, list_tasks, get_task, claim_task, complete_task (8) |\n| ライフサイクル | — | pending → in_progress → completed(release ロールバックなし) |\n\n---\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython s12_task_system/code.py\n```\n\n以下のプロンプトを試してください:\n\n1. `Create tasks: setup database schema, create API endpoints (depends on schema), write tests (depends on endpoints), write docs (depends on schema)`\n2. `List all tasks and their statuses`\n3. `Claim the first unblocked task and complete it`\n4. `List tasks again — which ones are now unblocked?`\n\n観察ポイント:`.tasks/` ディレクトリに JSON ファイルが生成されているか?タスク完了後、ブロックされていたタスクがアンロックされているか?\n\n---\n\n## 次の章\n\nタスクグラフができた。しかし、一部のタスクは長時間かかる — 全テスト実行やサーバーデプロイなど。Agent は LLM をトークン課金で呼び出しており、遅い操作を待つ余裕はない。\n\ns13 Background Tasks → 遅い操作はバックグラウンドへ。Agent は他のタスクの処理を続け、バックグラウンドの完了を通知で受け取る。\n\n\nCC ソースコード深掘り
\n\n> 以下は CC ソースコード `utils/tasks.ts`(862 行)、`tools/TaskCreateTool/TaskCreateTool.ts`(138 行)、`tools/TaskUpdateTool/TaskUpdateTool.ts`(406 行)、`tools/TaskGetTool/TaskGetTool.ts`(128 行)、`tools/TaskListTool/TaskListTool.ts`(116 行)、`hooks/useTaskListWatcher.ts`(221 行)の完全分析に基づく。\n\n### 一、TaskRecord の完全フィールド\n\nチュートリアルでは id、subject、status、owner、blockedBy のみ解説。CC は実際に 9 フィールドを持つ(`utils/tasks.ts:76-89`):\n\n| フィールド | 型 | 用途 |\n|------|------|------|\n| `id` | string | 昇順整数 ID |\n| `subject` | string | 短いタイトル |\n| `description` | string | 自由形式の説明 |\n| `activeForm` | string? | 現在進行形、in_progress 時にスピナーに表示 |\n| `owner` | string? | 割り当てられた agent ID |\n| `status` | pending/in_progress/completed | ライフサイクル |\n| `blocks` | string[] | このタスクがブロックするタスク ID(下流) |\n| `blockedBy` | string[] | このタスクをブロックするタスク ID(上流) |\n| `metadata` | Record? | 任意の拡張キーバリューペア |\n\n保存場所:`~/.claude/tasks/{taskListId}/{id}.json`。タスクごとに 1 ファイル。\n\n### 二、TodoWrite のアップグレードではなく、2 つの独立システム\n\nCC では Task System と TodoWrite **は共存**し、`isTodoV2Enabled()` で切り替え(`utils/tasks.ts:133`)— 対話セッションはデフォルトで Task (V2)、非対話/SDK セッションは TodoWrite。環境変数 `CLAUDE_CODE_ENABLE_TASKS` で Task を強制有効化可能。Task は TodoWrite にない機能を持つ:ファイルロック並行保護、依存関係強制、ownership、fs.watch リアクティブ監視、ライフサイクルフック。\n\n### 三、並行認識のロック機構\n\n`claimTask()`(`utils/tasks.ts:541-612`)は二重ロックで競合を防止:\n\n**タスクファイルロック**:`proper-lockfile` で `{taskId}.json` をロック(最大 30 リトライ、指数バックオフ 5-100ms)。ロック内:\n1. タスクを再読込(TOCTOU 防止)\n2. 既に他者が認識済み → `already_claimed`\n3. 既に完了済み → `already_resolved`\n4. 上流が未完了 → `blocked`\n5. owner を設定\n\n**リストレベルロック**(agent busy チェック時):`.lock` ファイル、全タスクを原子的に走査し該当 agent が他の open task を持つか確認。\n\n注意:教学版は認識と作業開始を 1 ステップに統合(claim = owner 設定 + in_progress);実際の CC の `claimTask` は主に owner 競合を解決し、owner のみを設定して status は変更しない。status の更新は `TaskUpdate` が担当。\n\n### 四、高水位標による ID 再利用防止\n\n`.highwatermark` ファイルが過去に割り当てられた最大タスク ID を記録。タスクが削除されても ID は再利用されない。\n\n### 五、4 つの Task ツール\n\nCC のタスクシステムは 4 つのツールを持つ(チュートリアルの汎用 Task ツールとは異なる):`TaskCreate`、`TaskGet`、`TaskUpdate`、`TaskList`。すべて `isConcurrencySafe: true` と `shouldDefer: true` が設定(ツールスキーマは初期プロンプトに含まれず、ToolSearch 後にのみ可視)。\n\n教学版の `create_task(blockedBy=...)` は作成時に直接依存を宣言する合理な簡略化。実際の CC の `TaskCreate` は subject/description/activeForm/metadata のみを受け付け、依存関係は `TaskUpdate` の `addBlocks/addBlockedBy` で管理される。\n\n \n\n\n"
},
{
"version": "s13",