Follow up PR #265: refine chapters, diagrams, and add S20 (#283)

* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience

Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building
incrementally on the previous. Key fixes across chapters:

- s01-s04: agent loop, tool dispatch, permission pipeline, hooks
- s05-s08: todo write, subagent, skill loading, context compact
- s09-s11: memory system, system prompt assembly, error recovery
- s12-s14: task graph, background tasks, cron scheduler

All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS,
json.dumps cache, real-state context, can_start dep protection, etc.).

* feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools

Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform
chapters. Each chapter inherits all previous fixes and adds one mechanism:

- s15: agent teams (TeamCreate, teammate threads, shared task list)
- s16: team protocols (plan approval, shutdown handshake, consume_inbox)
- s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox)
- s18: worktree isolation (git worktree, bind_task, cwd switching, safety)
- s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache)

All appendix source code references verified against CC source. Config priority
corrected: claude.ai < plugin < user < project < local.

* fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash

- s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02)
- s06-s08: todo_write validates content/status required fields (inherited from s05)
- s09: extract_memories uses pre-compression snapshot instead of compacted messages
- s16: submit_plan docstring clarifies protocol-only (not code-level gate)
- s17-s19: match_response restores type mismatch validation (from s16)
- s17-s19: claim_task deps list handles missing dep files without crashing

* fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation

- s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task,
  non-interactive/SDK defaults to TodoWrite. Fix env var name to
  CLAUDE_CODE_ENABLE_TASKS (not TODO_V2).
- s14/s15: add _validate_cron_field with per-field range checks (minute 0-59,
  hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi.
  Replace old try/except validation that only caught exceptions.
- s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree,
  not just create_worktree.

* fix: align s16-s19 teaching tool consistency

* fix pr265 chapter diagrams

* Add comprehensive s20 harness chapter

* Fix chapter smoke test regressions

* Clarify README tutorial track transition

---------

Co-authored-by: Haoran <bill-billion@outlook.com>
This commit is contained in:
gui-yue
2026-05-20 21:45:38 +08:00
committed by GitHub
parent c354cf7721
commit 1baf1aca5a
174 changed files with 35833 additions and 353 deletions

280
s12_task_system/README.md Normal file
View File

@@ -0,0 +1,280 @@
# s12: Task System — 目标太大,拆成小任务
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s14 → ... → s20
> *"大目标拆成小任务, 排好序, 持久化"* — 文件持久化的任务图, 多 agent 协作的基础。
>
> **Harness 层**: 任务 — 持久化的目标, 可恢复的进度。
---
## 问题
Agent 接到一个项目:搭数据库、写 API、加测试。它用 s05 的 TodoWrite 列了一张清单,然后开始写 API写到一半发现没数据库表回头补加测试时发现 API 接口签名又变了...
盖房子不能先盖屋顶再打地基。任务之间有先后。任务依赖应该形成有向无环图DAG教学版只演示 `blockedBy` 检查,没有实现环检测。
s05 的 TodoWrite 是一个列表。没有依赖关系、没有持久化、对话结束列表就没了。你需要的是**任务系统**:每个任务是一个 JSON 文件,任务之间有 `blockedBy` 依赖,跨会话持久化在磁盘上。
---
## 解决方案
![Task System Overview](images/task-system-overview.svg)
教学代码保留基础 agent loop为聚焦任务系统省略了 S11 的完整错误恢复RecoveryState、退避、升级、reactive compact、fallback model。新增 5 个任务工具 + `.tasks/` 目录持久化 + `blockedBy` 依赖检查。任务系统与错误恢复是独立层CC 源码中 `utils/tasks.ts` 只管 CRUD`query.ts` 的 with_retry/RecoveryState 管错误恢复,互不耦合。
TodoWrite vs Task System
| | TodoWrite (s05) | Task System (s12) |
|---|---|---|
| 存储 | 内存列表 | `.tasks/` JSON 文件 |
| 依赖 | 无 | `blockedBy` 依赖图 |
| 持久性 | 对话结束即丢 | 跨会话 |
| 多 Agent | 无 | `owner` 字段 |
| 状态 | checked / unchecked | pending → in_progress → completed |
---
## 工作原理
![Task DAG](images/task-dag.svg)
### Task: 数据结构
每个任务是一个 JSON 文件,存于 `.tasks/` 目录:
```python
@dataclass
class Task:
id: str
subject: str
description: str
status: str # pending | in_progress | completed
owner: str | None # Agent 名(多 Agent 场景)
blockedBy: list[str] # 依赖的任务 ID 列表
```
ID 用 `timestamp + random hex` 生成简单但够用。CC 用顺序 ID + highwatermark 文件防止 ID 重用,是更严谨的设计。
### create_task: 创建任务
```python
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
task = Task(
id=f"task_{int(time.time())}_{random_hex(4)}",
subject=subject, description=description,
status="pending", owner=None,
blockedBy=blockedBy or [],
)
save_task(task)
return task
```
创建时自动 `save_task``.tasks/{id}.json``blockedBy` 声明依赖,比如 "写 API" 的 `blockedBy``["task_schema"]`
### can_start: 依赖检查
一个任务只能在它的 `blockedBy` **全部 completed** 之后才能开始:
```python
def can_start(task_id: str) -> bool:
task = load_task(task_id)
for dep_id in task.blockedBy:
if not _task_path(dep_id).exists():
return False # missing dependency = blocked
dep = load_task(dep_id)
if dep.status != "completed":
return False
return True
```
`can_start``claim_task` 的前置检查:`blockedBy` 里有任何一个不是 completed就不能认领。不存在的依赖视为 blocked避免引用错误 ID 时崩溃。
### claim_task: 认领任务
Agent 开始做一个任务时,调用 `claim_task`:设置 `owner`,状态从 `pending``in_progress``owner` 字段记录谁在做这个任务,多 Agent 场景下防止重复认领:
```python
def claim_task(task_id: str, owner: str = "agent") -> str:
task = load_task(task_id)
if task.status != "pending":
return f"Task {task_id} is {task.status}, cannot claim"
if not can_start(task_id):
deps = [d for d in task.blockedBy
if load_task(d).status != "completed"]
return f"Blocked by: {deps}"
task.owner = owner
task.status = "in_progress"
save_task(task)
return f"Claimed {task_id} ({task.subject})"
```
如果任务已被别人认领(`status != "pending"`),或者依赖没完成(`can_start` 返回 False拒绝认领。
### complete_task: 完成与解锁
任务做完后,设为 `completed`。同时扫描所有其他任务,找出**刚刚被解锁**的下游任务:
```python
def complete_task(task_id: str) -> str:
task = load_task(task_id)
task.status = "completed"
save_task(task)
# 找出被解锁的下游任务
unblocked = [t.subject for t in list_tasks()
if t.status == "pending" and t.blockedBy
and can_start(t.id)]
msg = f"Completed {task_id} ({task.subject})"
if unblocked:
msg += f"\nUnblocked: {', '.join(unblocked)}"
return msg
```
完成 "schema" 后,"endpoints" 和 "docs" 的 `can_start` 返回 True它们可以开始。
### get_task: 查看完整细节
`list_tasks` 只显示一行摘要。`get_task` 返回完整的任务 JSON包括 description 和依赖细节。跨会话恢复时Agent 需要读取完整描述才能继续工作:
```python
def get_task(task_id: str) -> str:
task = load_task(task_id)
return json.dumps(asdict(task), indent=2)
```
### 状态机: 两个动作,三个状态
```
pending ──claim──→ in_progress ──complete──→ completed
```
这里的 `claim` / `complete` 是动作,`pending` / `in_progress` / `completed` 是状态:
- **claim_task**: `pending``in_progress`。设置 owner开始工作。
- **complete_task**: `in_progress``completed`。把任务标记为完成,并解锁下游。
CC 没有 `in_progress → pending` 的 release 路径。如果 teammate 终止或 shutdownCC 会把它未完成的任务 unassign清除 owner并将 status 重置为 `pending`,方便其他 agent 重新认领。教学版省略了这一恢复路径。
### 合起来跑
```python
# 创建有依赖的任务
schema = create_task("setup database schema")
endpoints = create_task("create API endpoints", blockedBy=[schema.id])
tests = create_task("write tests", blockedBy=[endpoints.id])
docs = create_task("write docs", blockedBy=[schema.id])
# Agent 认领第一个可做的任务
claim_task(schema.id) # ✓ Claimed (无依赖)
complete_task(schema.id) # ✓ Completed → 解锁 endpoints, docs
claim_task(endpoints.id) # ✓ Claimed (schema 已完成)
complete_task(endpoints.id) # ✓ Completed → 解锁 tests
claim_task(docs.id) # ✓ Claimed (schema 已完成)
complete_task(docs.id) # ✓ Completed
claim_task(tests.id) # ✓ Claimed (endpoints 已完成)
complete_task(tests.id) # ✓ Completed
```
每个 `create_task` 写一个 JSON 文件,每个 `claim_task` / `complete_task` 更新文件。跨会话时,`.tasks/` 目录还在Agent 读文件就能恢复进度。
---
## 相对 s11 的变更
| 组件 | 之前 (s11) | 之后 (s12) |
|------|-----------|-----------|
| 任务管理 | 无 | Task dataclass + 5 个工具 |
| 新类型 | — | Taskid, subject, description, status, owner, blockedBy |
| 存储 | 无持久化 | `.tasks/{id}.json` 跨会话 |
| 依赖 | 无 | `blockedBy` 图 + `can_start` 检查 |
| 工具 | bash, read_file, write_file (3) | + create_task, list_tasks, get_task, claim_task, complete_task (8) |
| 生命周期 | — | pending → in_progress → completed无 release 回退) |
---
## 试一下
```sh
cd learn-claude-code
python s12_task_system/code.py
```
试试这些 prompt
1. `Create tasks: setup database schema, create API endpoints (depends on schema), write tests (depends on endpoints), write docs (depends on schema)`
2. `List all tasks and their statuses`
3. `Claim the first unblocked task and complete it`
4. `List tasks again — which ones are now unblocked?`
观察重点:`.tasks/` 目录下是否生成了 JSON 文件?完成任务后,被阻塞的任务是否解锁?
---
## 接下来
任务图有了。但有些任务要跑很久——比如全量测试、部署到服务器。Agent 调 LLM 按量计费,不能干等一个慢操作。
s13 Background Tasks → 慢操作放后台。Agent 继续处理其他任务,后台跑完了通知它。
<details>
<summary>深入 CC 源码</summary>
> 以下基于 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 行)的分析。
### 一、TaskRecord 的完整字段
教学版只讲了 id、subject、status、owner、blockedBy。CC 实际有 9 个字段(`utils/tasks.ts:76-89`
| 字段 | 类型 | 用途 |
|------|------|------|
| `id` | string | 递增整数 ID |
| `subject` | string | 简短标题 |
| `description` | string | 自由格式描述 |
| `activeForm` | string? | 进行时态in_progress 时在 spinner 显示 |
| `owner` | string? | 分配的 agent ID |
| `status` | pending/in_progress/completed | 生命周期 |
| `blocks` | string[] | 此任务阻塞的任务 ID下游 |
| `blockedBy` | string[] | 阻塞此任务的任务 ID上游 |
| `metadata` | Record? | 任意扩展键值对 |
存储位置:`~/.claude/tasks/{taskListId}/{id}.json`。每个任务一个文件。
### 二、不是 TodoWrite 的升级,是两个独立系统
CC 中 Task System 和 TodoWrite **同时存在**,通过 `isTodoV2Enabled()` 切换(`utils/tasks.ts:133`)——交互式会话默认启用 TaskV2非交互式/SDK 默认用 TodoWrite。环境变量 `CLAUDE_CODE_ENABLE_TASKS` 可强制启用 Task。Task 有 TodoWrite 没有的文件锁并发保护、依赖强制执行、ownership、fs.watch 响应式监听、生命周期 hooks。
### 三、并发认领的锁机制
`claimTask()``utils/tasks.ts:541-612`)用双重锁防竞争:
**任务文件锁**`proper-lockfile` 锁住 `{taskId}.json`(最多重试 30 次,指数退避 5-100ms。锁内
1. 重新读取任务(防 TOCTOU
2. 检查已被他人认领 → `already_claimed`
3. 检查已完成 → `already_resolved`
4. 检查上游未完成 → `blocked`
5. 设置 owner
**列表级锁**agent busy 检查时):`.lock` 文件,原子性扫描所有任务并检查该 agent 是否已有其他 open task。
注意:教学版把 claim 和开始工作合成一步claim = set owner + in_progress真实 CC 的 `claimTask` 主要解决 owner 竞争,只设 owner 不改 status状态更新由 `TaskUpdate` 完成。
### 四、高水位标防 ID 重用
`.highwatermark` 文件记录曾分配过的最高任务 ID。即使任务被删除ID 也不会被重用。
### 五、四个 Task 工具
CC 的任务系统有四个工具(不是教学版的一个通用 Task 工具):`TaskCreate``TaskGet``TaskUpdate``TaskList`。全部设置 `isConcurrencySafe: true``shouldDefer: true`(工具 schema 不在初始 prompt 中,需 ToolSearch 后才可见)。
教学版的 `create_task(blockedBy=...)` 在创建时直接声明依赖,是合理简化。真实 CC 的 `TaskCreate` 只接受 subject/description/activeForm/metadata依赖关系由 `TaskUpdate``addBlocks/addBlockedBy` 维护。
</details>
<!-- translation-sync: zh@v1, en@v0, ja@v0 -->