feat: consolidate course into 21 lessons

This commit is contained in:
Haoran
2026-07-31 03:15:58 +08:00
parent 4bc33ec858
commit 2d69019342
200 changed files with 7338 additions and 10829 deletions

View File

@@ -2,7 +2,7 @@
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s14 → ... → s20 → s21 → s22
s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s14 → ... → s20 → s21
> *"大きな目標を小さなタスクに分け、順序付け、永続化"* — ファイル永続化タスクグラフ、マルチ Agent 協調の基盤。
>
@@ -14,7 +14,7 @@ s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s1
Agent がプロジェクトを受けたデータベース構築、API 実装、テスト追加。s05 の TodoWrite でリストを作り、まず API を書き始め、途中でデータベーステーブルがないことに気づいて戻る。テスト追加時に API インターフェースのシグネチャがまた変わっている...
屋根を先に建てて基礎を後から打つことはできない。タスクには順序がある。タスク依存関係は有向非巡回グラフDAGを形成すべき;教学版`blockedBy` チェックのみをデモし、循環検出は実装していない
屋根を先に建てて基礎を後から打つことはできない。タスクには順序がある。タスク間の前提依存関係は有向非巡回グラフDAGとして表現でき、この章で`blockedBy` でそれらを記録する
s05 の TodoWrite は現在のタスクの実行チェックリストで、セッションメモリに保持される。ここで必要なのは**タスクシステム**:各タスクは JSON ファイル、タスク間に `blockedBy` 依存関係、ディスク上でセッションをまたいで永続化。
@@ -24,7 +24,7 @@ s05 の TodoWrite は現在のタスクの実行チェックリストで、セ
![Task System Overview](images/task-system-overview.ja.svg)
教育版は基本 agent loop を維持し、タスクシステムに集中するため S11 の完全なエラーリカバリRecoveryState、バックオフ、エスカレーション、reactive compact、フォールバックモデルを省略。追加5 つの教育用ツール + `.tasks/` ディレクトリによる永続化 + `blockedBy` 依存チェック。タスクシステムとエラーリカバリは独立したレイヤーで、タスクモジュールは状態を、query recovery はモデル呼び出し失敗を扱う
この章では、5 つのタスクツール、`.tasks/` ディレクトリへの永続化、`blockedBy` 依存チェックを追加する
TodoWrite vs Task System
@@ -39,8 +39,6 @@ TodoWrite vs Task System
| 粒度 | Agent 自身の手順 | 認識・追跡・アンロックできるタスク |
| 更新契約 | リスト全体を置換 | 個別レコードを作成・取得・更新・一覧 |
教育用 API は `create_task``list_tasks``get_task``claim_task``complete_task` としてライフサイクルを明示する。Claude Code の製品サーフェスはこれらを `TaskCreate``TaskGet``TaskUpdate``TaskList` の 4 ツールにまとめ、認識と完了は独立した公式ツールではなく更新操作として扱う。
---
## 仕組み
@@ -62,7 +60,7 @@ class Task:
blockedBy: list[str] # 依存タスク ID のリスト
```
ID は `timestamp + random hex` で生成、シンプルだが十分。CC は順次 ID + highwatermark ファイルで ID 再利用を防止する、より厳密な設計
ID は `timestamp + random hex` で生成する
### create_task: タスク作成
@@ -162,8 +160,6 @@ 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 が再認識できるようにする。教学版はこの復旧パスを省略。
### 組み合わせて実行
```python
@@ -228,58 +224,5 @@ python s12_task_system/code.py
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 時にスピナーに表示 |
| `owner` | string? | 割り当てられた agent ID |
| `status` | pending/in_progress/completed | ライフサイクル |
| `blocks` | string[] | このタスクがブロックするタスク ID下流 |
| `blockedBy` | string[] | このタスクをブロックするタスク ID上流 |
| `metadata` | Record? | 任意の拡張キーバリューペア |
保存場所:`~/.claude/tasks/{taskListId}/{id}.json`。タスクごとに 1 ファイル。
### 二、目的は近いが、機構は独立
Task ツールと TodoWrite は共存できるが、同じストレージモデルを共有しない。現在の対話型セッションは構造化 Task ツールを既定で使い、TodoWrite は非対話型や Agent SDK などの互換サーフェスに残る。公開範囲はリリースや設定で変わり得る。Task レコードはファイルロック、依存関係、ownership、リアクティブ監視、ライフサイクルフックを追加し、TodoWrite はリスト全体を置換するセッションチェックリストである。
### 三、並行認識のロック機構
`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 を持つか確認。
注意:教学版は認識と作業開始を 1 ステップに統合claim = owner 設定 + in_progress実際の CC の `claimTask` は主に owner 競合を解決し、owner のみを設定して status は変更しない。status の更新は `TaskUpdate` が担当。
### 四、高水位標による ID 再利用防止
`.highwatermark` ファイルが過去に割り当てられた最大タスク ID を記録。タスクが削除されても ID は再利用されない。
### 五、4 つの Task ツール
CC のタスクシステムは 4 つのツールを持つ(チュートリアルの汎用 Task ツールとは異なる):`TaskCreate``TaskGet``TaskUpdate``TaskList`。すべて `isConcurrencySafe: true``shouldDefer: true` が設定ツールスキーマは初期プロンプトに含まれず、ToolSearch 後にのみ可視)。
教学版の `create_task(blockedBy=...)` は作成時に直接依存を宣言する合理な簡略化。実際の CC の `TaskCreate` は subject/description/activeForm/metadata のみを受け付け、依存関係は `TaskUpdate``addBlocks/addBlockedBy` で管理される。
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

View File

@@ -2,7 +2,7 @@
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s14 → ... → s20 → s21 → s22
s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s14 → ... → s20 → s21
> *"Break big goals into small tasks, order them, persist"* — File-persisted task graph, the foundation for multi-agent collaboration.
>
@@ -14,7 +14,7 @@ s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s1
The 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...
You 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.
You can't build the roof before laying the foundation. Tasks have ordering. Task prerequisites can be represented as a Directed Acyclic Graph (DAG); this chapter records them with `blockedBy`.
s05'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.
@@ -24,7 +24,7 @@ s05's TodoWrite is an execution checklist for the current task, kept in session
![Task System Overview](images/task-system-overview.en.svg)
Teaching 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 teaching tools + `.tasks/` directory for persistence + `blockedBy` dependency checking. The task system and error recovery are independent layers: in CC source, `utils/tasks.ts` handles task state while query recovery handles model-call failures.
This chapter adds 5 task tools, persistence in the `.tasks/` directory, and `blockedBy` dependency checks.
TodoWrite vs Task System:
@@ -39,8 +39,6 @@ TodoWrite vs Task System:
| Granularity | The agent's own steps | Tasks that can be claimed, tracked, and unblocked |
| Update contract | Replace the whole checklist | Create/get/update/list individual records |
The teaching API spells the lifecycle out as `create_task`, `list_tasks`, `get_task`, `claim_task`, and `complete_task`. Claude Code's product surface groups those operations into four tools: `TaskCreate`, `TaskGet`, `TaskUpdate`, and `TaskList`; claiming and completion are updates, not separate official tools.
---
## How It Works
@@ -62,7 +60,7 @@ class Task:
blockedBy: list[str] # List of dependency task IDs
```
IDs 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.
IDs are generated with `timestamp + random hex`.
### create_task: Create Tasks
@@ -162,8 +160,6 @@ 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.
### Putting It Together
```python
@@ -228,58 +224,5 @@ The task graph is in place. But some tasks take a long time — like running ful
s13 Background Tasks → Slow operations go to the background. The agent continues processing other tasks, and gets notified when the background work is done.
<details>
<summary>Deep Dive into CC Source</summary>
> 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).
### 1. TaskRecord's Full Fields
The tutorial only covers id, subject, status, owner, blockedBy. CC actually has 9 fields (`utils/tasks.ts:76-89`):
| Field | Type | Purpose |
|------|------|---------|
| `id` | string | Incrementing integer ID |
| `subject` | string | Short title |
| `description` | string | Free-form description |
| `activeForm` | string? | Present tense form, shown in spinner when in_progress |
| `owner` | string? | Assigned agent ID |
| `status` | pending/in_progress/completed | Lifecycle |
| `blocks` | string[] | Task IDs blocked by this task (downstream) |
| `blockedBy` | string[] | Task IDs blocking this task (upstream) |
| `metadata` | Record? | Arbitrary extension key-value pairs |
Storage location: `~/.claude/tasks/{taskListId}/{id}.json`. One file per task.
### 2. Same Intent, Independent Mechanisms
Task tools and TodoWrite can coexist, but they do not share one storage model. Current interactive sessions default to structured Task tools, while TodoWrite remains on compatibility surfaces such as non-interactive and Agent SDK usage; exact exposure varies by release and configuration. Task records add file-lock concurrency protection, dependency enforcement, ownership, reactive monitoring, and lifecycle hooks. TodoWrite remains a whole-list session checklist.
### 3. Concurrent Claim Locking
`claimTask()` (`utils/tasks.ts:541-612`) uses dual locking to prevent races:
**Task file lock**: `proper-lockfile` locks `{taskId}.json` (up to 30 retries, exponential backoff 5-100ms). Inside the lock:
1. Re-read task (prevent TOCTOU)
2. Check already claimed by another → `already_claimed`
3. Check already completed → `already_resolved`
4. Check upstream not completed → `blocked`
5. Set owner
**List-level lock** (agent busy check): `.lock` file, atomic scan of all tasks to check if the agent already has other open tasks.
Note: 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`.
### 4. High-Water Mark to Prevent ID Reuse
The `.highwatermark` file records the highest task ID ever assigned. Even if a task is deleted, its ID won't be reused.
### 5. Four Task Tools
CC'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).
The 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`.
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

View File

@@ -2,7 +2,7 @@
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s14 → ... → s20 → s21 → s22
s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s14 → ... → s20 → s21
> *"大目标拆成小任务, 排好序, 持久化"* — 文件持久化的任务图, 多 agent 协作的基础。
>
@@ -14,7 +14,7 @@ s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s1
Agent 接到一个项目:搭数据库、写 API、加测试。它用 s05 的 TodoWrite 列了一张清单,然后开始写 API写到一半发现没数据库表回头补加测试时发现 API 接口签名又变了...
盖房子不能先盖屋顶再打地基。任务之间有先后。任务依赖应该形成有向无环图DAG;教学版只演示 `blockedBy` 检查,没有实现环检测
盖房子不能先盖屋顶再打地基。任务之间有先后。任务之间的前置依赖可以表示为有向无环图DAG,本章用 `blockedBy` 记录这些依赖
s05 的 TodoWrite 是当前任务的执行清单,保存在会话内存中。这里需要的是**任务系统**:每个任务是一个 JSON 文件,任务之间有 `blockedBy` 依赖,跨会话持久化在磁盘上。
@@ -24,7 +24,7 @@ s05 的 TodoWrite 是当前任务的执行清单,保存在会话内存中。
![Task System Overview](images/task-system-overview.svg)
教学代码保留基础 agent loop为聚焦任务系统省略了 S11 的完整错误恢复RecoveryState、退避、升级、reactive compact、fallback model。新增 5 个教学工具 + `.tasks/` 目录持久化 + `blockedBy` 依赖检查。任务系统与错误恢复是独立层CC 源码中的任务模块管理任务状态query recovery 管理模型调用失败。
本章新增 5 个任务工具、`.tasks/` 目录持久化 `blockedBy` 依赖检查。
TodoWrite vs Task System
@@ -39,8 +39,6 @@ TodoWrite vs Task System
| 粒度 | Agent 自己的步骤 | 可被认领、追踪、解锁的任务 |
| 更新契约 | 整表替换 | 对单条记录执行创建、读取、更新、列举 |
教学 API 用 `create_task``list_tasks``get_task``claim_task``complete_task` 把生命周期展开。Claude Code 的产品表面把这些操作归入四个工具:`TaskCreate``TaskGet``TaskUpdate``TaskList`;认领与完成属于更新,不是独立的官方工具。
---
## 工作原理
@@ -62,7 +60,7 @@ class Task:
blockedBy: list[str] # 依赖的任务 ID 列表
```
ID 用 `timestamp + random hex` 生成简单但够用。CC 用顺序 ID + highwatermark 文件防止 ID 重用,是更严谨的设计
ID 使`timestamp + random hex` 生成。
### create_task: 创建任务
@@ -162,8 +160,6 @@ pending ──claim──→ in_progress ──complete──→ 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
@@ -228,58 +224,5 @@ python s12_task_system/code.py
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`。每个任务一个文件。
### 二、目标相近,机制独立
Task 工具与 TodoWrite 可以同时存在,但不共享一套存储模型。当前交互式会话默认使用结构化 Task 工具TodoWrite 仍位于非交互式、Agent SDK 等兼容表面具体暴露方式会随版本和配置变化。Task 记录增加了文件锁并发保护、依赖强制执行、ownership、响应式监听和生命周期 hooksTodoWrite 仍是整表替换的会话清单。
### 三、并发认领的锁机制
`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@v1, ja@v1 -->

View File

@@ -14,10 +14,8 @@ Changes from s11:
- complete_task: set completed + report unblocked downstream
- 5 new tools: create_task, list_tasks, get_task, claim_task, complete_task
Note: Teaching code keeps a basic agent loop to stay focused on the task
system. S11's full error recovery (RecoveryState, backoff, escalation,
reactive compact, fallback model) is omitted — in real CC, tasks.ts and
withRetry are independent layers that compose naturally.
This chapter keeps the agent loop focused on the task system. Error recovery
remains the independent layer introduced in s11.
"""
import os, subprocess, json, time, random

View File

@@ -57,7 +57,7 @@
<text x="195" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">.tasks/ — Cross-session Persistence</text>
<text x="60" y="222" fill="#0d9488" font-size="9">task_xxx.json · task_yyy.json · task_zzz.json</text>
<text x="60" y="238" fill="#6b7280" font-size="8">{id, subject, description, status, owner, blockedBy}</text>
<text x="60" y="252" fill="#6b7280" font-size="8">Tutorial ID: timestamp + random | CC: sequential ID + highwatermark</text>
<text x="60" y="252" fill="#6b7280" font-size="8">ID: timestamp + random</text>
<!-- Arrow: tools → .tasks/ -->
<path d="M 440 144 L 440 165 L 250 165 L 250 185" fill="none" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -57,7 +57,7 @@
<text x="195" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">.tasks/ — セッション横断永続化</text>
<text x="60" y="222" fill="#0d9488" font-size="9">task_xxx.json · task_yyy.json · task_zzz.json</text>
<text x="60" y="238" fill="#6b7280" font-size="8">{id, subject, description, status, owner, blockedBy}</text>
<text x="60" y="252" fill="#6b7280" font-size="8">チュートリアル ID: timestamp + random | CC: 順次 ID + highwatermark</text>
<text x="60" y="252" fill="#6b7280" font-size="8">ID: timestamp + random</text>
<!-- 矢印: tools → .tasks/ -->
<path d="M 440 144 L 440 165 L 250 165 L 250 185" fill="none" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -57,7 +57,7 @@
<text x="195" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">.tasks/ — 跨会话持久化</text>
<text x="60" y="222" fill="#0d9488" font-size="9">task_xxx.json · task_yyy.json · task_zzz.json</text>
<text x="60" y="238" fill="#6b7280" font-size="8">{id, subject, description, status, owner, blockedBy}</text>
<text x="60" y="252" fill="#6b7280" font-size="8">教学版 ID: timestamp + random | CC: 顺序 ID + highwatermark</text>
<text x="60" y="252" fill="#6b7280" font-size="8">ID: timestamp + random</text>
<!-- Arrow: tools → .tasks/ -->
<path d="M 440 144 L 440 165 L 250 165 L 250 185" fill="none" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB