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

View File

@@ -0,0 +1,280 @@
# s12: Task System — Break Big Goals into Small Tasks
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s10 → s11 → `s12` → [s13](../s13_background_tasks/) → s14 → ... → s20
> *"Break big goals into small tasks, order them, persist"* — File-persisted task graph, the foundation for multi-agent collaboration.
>
> **Harness Layer**: Tasks — Persisted goals, recoverable progress.
---
## The Problem
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.
s05's TodoWrite is a list. No dependencies, no persistence — when the conversation ends, the list is gone. What you need is a **task system**: each task is a JSON file, tasks have `blockedBy` dependencies, and they persist across sessions on disk.
---
## The Solution
![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 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.
TodoWrite vs Task System:
| | TodoWrite (s05) | Task System (s12) |
|---|---|---|
| Storage | In-memory list | `.tasks/` JSON files |
| Dependencies | None | `blockedBy` dependency graph |
| Persistence | Lost when conversation ends | Cross-session |
| Multi-agent | None | `owner` field |
| Status | checked / unchecked | pending → in_progress → completed |
---
## How It Works
![Task DAG](images/task-dag.en.svg)
### Task: Data Structure
Each task is a JSON file, stored in the `.tasks/` directory:
```python
@dataclass
class Task:
id: str
subject: str
description: str
status: str # pending | in_progress | completed
owner: str | None # Agent name (multi-agent scenarios)
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.
### create_task: Create Tasks
```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
```
Automatically calls `save_task` on creation to write `.tasks/{id}.json`. `blockedBy` declares dependencies, for example "write API" has `blockedBy: ["task_schema"]`.
### can_start: Dependency Check
A task can only start after all its `blockedBy` dependencies are **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` 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.
### claim_task: Claim a Task
When 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:
```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})"
```
If the task is already claimed by someone else (`status != "pending"`), or dependencies aren't met (`can_start` returns False), the claim is rejected.
### complete_task: Complete and Unblock
When a task is done, set it to `completed`. Simultaneously scan all other tasks to find downstream tasks that were **just unblocked**:
```python
def complete_task(task_id: str) -> str:
task = load_task(task_id)
task.status = "completed"
save_task(task)
# Find newly unblocked downstream tasks
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
```
After completing "schema", `can_start` returns True for "endpoints" and "docs"; they can begin.
### get_task: View Full Details
`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:
```python
def get_task(task_id: str) -> str:
task = load_task(task_id)
return json.dumps(asdict(task), indent=2)
```
### State Machine: Two Actions, Three States
```
pending ──claim──→ in_progress ──complete──→ completed
```
Here `claim` / `complete` are actions, while `pending` / `in_progress` / `completed` are states:
- **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
# Create tasks with dependencies
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 claims the first available task
claim_task(schema.id) # ✓ Claimed (no dependencies)
complete_task(schema.id) # ✓ Completed → unblocks endpoints, docs
claim_task(endpoints.id) # ✓ Claimed (schema completed)
complete_task(endpoints.id) # ✓ Completed → unblocks tests
claim_task(docs.id) # ✓ Claimed (schema completed)
complete_task(docs.id) # ✓ Completed
claim_task(tests.id) # ✓ Claimed (endpoints completed)
complete_task(tests.id) # ✓ Completed
```
Each `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.
---
## Changes from s11
| Component | Before (s11) | After (s12) |
|-----------|-------------|-------------|
| Task management | None | Task dataclass + 5 tools |
| New types | — | Task (id, subject, description, status, owner, blockedBy) |
| Storage | No persistence | `.tasks/{id}.json` cross-session |
| Dependencies | None | `blockedBy` graph + `can_start` check |
| Tools | bash, read_file, write_file (3) | + create_task, list_tasks, get_task, claim_task, complete_task (8) |
| Lifecycle | — | pending → in_progress → completed (no release rollback) |
---
## Try It
```sh
cd learn-claude-code
python s12_task_system/code.py
```
Try these prompts:
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?`
What to observe: Are JSON files generated in the `.tasks/` directory? After completing a task, are the blocked tasks unblocked?
---
## What's Next
The 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.
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. Not a TodoWrite Upgrade — Two Independent Systems
In 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.
### 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

@@ -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.ja.svg)
教学版は基本 agent loop を維持し、タスクシステムに集中するため S11 の完全なエラーリカバリRecoveryState、バックオフ、エスカレーション、reactive compact、フォールバックモデルを省略。追加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.ja.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` は 1 行サマリのみ表示。`get_task` は description と依存関係の詳細を含む完全なタスク JSON を返す。セッションをまたいで復旧する際、Agent は完全な説明を読んで作業を継続する必要がある:
```python
def get_task(task_id: str) -> str:
task = load_task(task_id)
return json.dumps(asdict(task), indent=2)
```
### 状態マシン: 2 つのアクション、3 つの状態
```
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 が終了または shutdown した場合、CC は未完了タスクの 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) # ✓ Claimedschema 完了済み)
complete_task(endpoints.id) # ✓ Completed → tests をアンロック
claim_task(docs.id) # ✓ Claimedschema 完了済み)
complete_task(docs.id) # ✓ Completed
claim_task(tests.id) # ✓ Claimedendpoints 完了済み)
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 → completedrelease ロールバックなし) |
---
## 試してみる
```sh
cd learn-claude-code
python s12_task_system/code.py
```
以下のプロンプトを試してください:
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 時にスピナーに表示 |
| `owner` | string? | 割り当てられた agent ID |
| `status` | pending/in_progress/completed | ライフサイクル |
| `blocks` | string[] | このタスクがブロックするタスク ID下流 |
| `blockedBy` | string[] | このタスクをブロックするタスク ID上流 |
| `metadata` | Record? | 任意の拡張キーバリューペア |
保存場所:`~/.claude/tasks/{taskListId}/{id}.json`。タスクごとに 1 ファイル。
### 二、TodoWrite のアップグレードではなく、2 つの独立システム
CC では Task System と TodoWrite **は共存**し、`isTodoV2Enabled()` で切り替え(`utils/tasks.ts:133`)— 対話セッションはデフォルトで Task (V2)、非対話/SDK セッションは TodoWrite。環境変数 `CLAUDE_CODE_ENABLE_TASKS` で Task を強制有効化可能。Task は TodoWrite にない機能を持つファイルロック並行保護、依存関係強制、ownership、fs.watch リアクティブ監視、ライフサイクルフック。
### 三、並行認識のロック機構
`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 -->

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 -->

376
s12_task_system/code.py Normal file
View File

@@ -0,0 +1,376 @@
#!/usr/bin/env python3
"""
s12: Task System — file-persisted task graph with blockedBy dependencies.
Run: python s12_task_system/code.py
Need: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY
Changes from s11:
- Task dataclass (id, subject, description, status, owner, blockedBy)
- TASKS_DIR = .tasks/ for persistent JSON storage
- create_task / save_task / load_task / list_tasks / get_task
- can_start: checks blockedBy all completed (missing deps = blocked)
- claim_task: set owner + pending -> in_progress
- 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.
"""
import os, subprocess, json, time, random
from pathlib import Path
from dataclasses import dataclass, asdict
try:
import readline
readline.parse_and_bind('set bind-tty-special-chars off')
except ImportError:
pass
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
WORKDIR = Path.cwd()
MEMORY_DIR = WORKDIR / ".memory"
MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
# ── Task System ──
TASKS_DIR = WORKDIR / ".tasks"
TASKS_DIR.mkdir(exist_ok=True)
@dataclass
class Task:
id: str
subject: str
description: str
status: str # pending | in_progress | completed
owner: str | None # Agent name (multi-agent scenarios)
blockedBy: list[str] # Dependency task IDs
def _task_path(task_id: str) -> Path:
return TASKS_DIR / f"{task_id}.json"
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
task = Task(
id=f"task_{int(time.time())}_{random.randint(0, 9999):04d}",
subject=subject,
description=description,
status="pending",
owner=None,
blockedBy=blockedBy or [],
)
save_task(task)
return task
def save_task(task: Task):
_task_path(task.id).write_text(json.dumps(asdict(task), indent=2))
def load_task(task_id: str) -> Task:
return Task(**json.loads(_task_path(task_id).read_text()))
def list_tasks() -> list[Task]:
return [Task(**json.loads(p.read_text()))
for p in sorted(TASKS_DIR.glob("task_*.json"))]
def get_task(task_id: str) -> str:
"""Return full task details as JSON."""
task = load_task(task_id)
return json.dumps(asdict(task), indent=2)
def can_start(task_id: str) -> bool:
"""Check if all blockedBy dependencies are completed.
Missing dependencies are treated as blocked."""
task = load_task(task_id)
for dep_id in task.blockedBy:
if not _task_path(dep_id).exists():
return False
if load_task(dep_id).status != "completed":
return False
return True
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 not _task_path(d).exists() or load_task(d).status != "completed"]
return f"Blocked by: {deps}"
task.owner = owner
task.status = "in_progress"
save_task(task)
print(f" \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
return f"Claimed {task.id} ({task.subject})"
def complete_task(task_id: str) -> str:
task = load_task(task_id)
if task.status != "in_progress":
return f"Task {task_id} is {task.status}, cannot complete"
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)]
print(f" \033[32m[complete] {task.subject}\033[0m")
msg = f"Completed {task.id} ({task.subject})"
if unblocked:
msg += f"\nUnblocked: {', '.join(unblocked)}"
print(f" \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
return msg
# ── Prompt Assembly (from s10, synced) ──
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file, "
"create_task, list_tasks, get_task, claim_task, complete_task.",
"workspace": f"Working directory: {WORKDIR}",
"memory": "Relevant memories are injected below when available.",
}
def assemble_system_prompt(context: dict) -> str:
sections = [PROMPT_SECTIONS["identity"],
PROMPT_SECTIONS["tools"],
PROMPT_SECTIONS["workspace"]]
memories = context.get("memories", "")
if memories:
sections.append(f"Relevant memories:\n{memories}")
return "\n\n".join(sections)
_last_context_key, _last_prompt = None, None
def get_system_prompt(context: dict) -> str:
global _last_context_key, _last_prompt
key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
if key == _last_context_key and _last_prompt:
return _last_prompt
_last_context_key = key
_last_prompt = assemble_system_prompt(context)
return _last_prompt
# ── Tools ──
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
def run_bash(command: str) -> str:
try:
r = subprocess.run(command, shell=True, cwd=WORKDIR,
capture_output=True, text=True, timeout=120)
out = (r.stdout + r.stderr).strip()
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
def run_read(path: str, limit: int | None = None) -> str:
try:
lines = safe_path(path).read_text().splitlines()
if limit and limit < len(lines):
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
return "\n".join(lines)
except Exception as e:
return f"Error: {e}"
def run_write(path: str, content: str) -> str:
try:
fp = safe_path(path)
fp.parent.mkdir(parents=True, exist_ok=True)
fp.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
except Exception as e:
return f"Error: {e}"
# Task tools
def run_create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> str:
task = create_task(subject, description, blockedBy)
deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else ""
print(f" \033[34m[create] {task.subject}{deps}\033[0m")
return f"Created {task.id}: {task.subject}{deps}"
def run_list_tasks() -> str:
tasks = list_tasks()
if not tasks:
return "No tasks. Use create_task to add some."
lines = []
for t in tasks:
icon = {"pending": "", "in_progress": "",
"completed": ""}.get(t.status, "?")
deps = f" (blockedBy: {', '.join(t.blockedBy)})" if t.blockedBy else ""
owner = f" [{t.owner}]" if t.owner else ""
lines.append(f" {icon} {t.id}: {t.subject} "
f"[{t.status}]{owner}{deps}")
return "\n".join(lines)
def run_get_task(task_id: str) -> str:
try:
return get_task(task_id)
except FileNotFoundError:
return f"Error: Task {task_id} not found"
def run_claim_task(task_id: str) -> str:
return claim_task(task_id, owner="agent")
def run_complete_task(task_id: str) -> str:
return complete_task(task_id)
TOOLS = [
{"name": "bash", "description": "Run a shell command.",
"input_schema": {"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"]}},
{"name": "read_file", "description": "Read file contents.",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"},
"limit": {"type": "integer"}},
"required": ["path"]}},
{"name": "write_file", "description": "Write content to a file.",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"},
"content": {"type": "string"}},
"required": ["path", "content"]}},
{"name": "create_task",
"description": "Create a new task with optional blockedBy dependencies.",
"input_schema": {"type": "object",
"properties": {
"subject": {"type": "string"},
"description": {"type": "string"},
"blockedBy": {"type": "array",
"items": {"type": "string"}}},
"required": ["subject"]}},
{"name": "list_tasks",
"description": "List all tasks with status, owner, and dependencies.",
"input_schema": {"type": "object", "properties": {},
"required": []}},
{"name": "get_task",
"description": "Get full details of a specific task by ID.",
"input_schema": {"type": "object",
"properties": {"task_id": {"type": "string"}},
"required": ["task_id"]}},
{"name": "claim_task",
"description": "Claim a pending task. Sets owner, changes status to in_progress.",
"input_schema": {"type": "object",
"properties": {"task_id": {"type": "string"}},
"required": ["task_id"]}},
{"name": "complete_task",
"description": "Complete an in-progress task. Reports unblocked downstream tasks.",
"input_schema": {"type": "object",
"properties": {"task_id": {"type": "string"}},
"required": ["task_id"]}},
]
TOOL_HANDLERS = {
"bash": run_bash, "read_file": run_read, "write_file": run_write,
"create_task": run_create_task, "list_tasks": run_list_tasks,
"get_task": run_get_task, "claim_task": run_claim_task,
"complete_task": run_complete_task,
}
# ── Context ──
def update_context(context: dict, messages: list) -> dict:
"""Derive context from real state."""
memories = ""
if MEMORY_INDEX.exists():
content = MEMORY_INDEX.read_text().strip()
if content:
memories = content
return {
"enabled_tools": list(TOOL_HANDLERS.keys()),
"workspace": str(WORKDIR),
"memories": memories,
}
# ── Agent Loop (simplified, focused on task system) ──
def agent_loop(messages: list, context: dict):
system = get_system_prompt(context)
while True:
try:
response = client.messages.create(
model=MODEL, system=system, messages=messages,
tools=TOOLS, max_tokens=8000)
except Exception as e:
messages.append({"role": "assistant", "content": [
{"type": "text",
"text": f"[Error] {type(e).__name__}: {e}"}]})
return
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
results = []
for block in response.content:
if block.type != "tool_use":
continue
print(f"\033[36m> {block.name}\033[0m")
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown: {block.name}"
print(str(output)[:300])
results.append({"type": "tool_result",
"tool_use_id": block.id, "content": output})
messages.append({"role": "user", "content": results})
context = update_context(context, messages)
system = get_system_prompt(context)
if __name__ == "__main__":
print("s12: task system")
print("Enter a question, press Enter to send. Type q to quit.\n")
history = []
context = update_context({}, [])
while True:
try:
query = input("\033[36ms12 >> \033[0m")
except (EOFError, KeyboardInterrupt):
break
if query.strip().lower() in ("q", "exit", ""):
break
history.append({"role": "user", "content": query})
agent_loop(history, context)
context = update_context(context, history)
for block in history[-1]["content"]:
if getattr(block, "type", None) == "text":
print(block.text)
print()

View File

@@ -0,0 +1,59 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 400" font-family="system-ui, -apple-system, sans-serif">
<defs>
<marker id="dep" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#94a3b8"/>
</marker>
</defs>
<rect width="760" height="400" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="760" height="44" fill="#0d9488" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="#0d9488"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task DAG — Dependency Example: Database → API → Tests → Deploy</text>
<!-- Row 1: schema (completed) -->
<rect x="295" y="70" width="170" height="48" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="2"/>
<text x="380" y="92" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">✓ schema</text>
<text x="380" y="108" fill="#16a34a" font-size="9" text-anchor="middle">completed</text>
<!-- Arrows: schema → endpoints, schema → docs -->
<path d="M 340 118 L 240 162" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<path d="M 420 118 L 520 162" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<!-- Row 2: endpoints (in_progress), docs (pending) -->
<rect x="115" y="164" width="170" height="48" rx="8" fill="#dbeafe" stroke="#2563eb" stroke-width="2"/>
<text x="200" y="186" fill="#1e40af" font-size="12" font-weight="700" text-anchor="middle">● endpoints</text>
<text x="200" y="202" fill="#2563eb" font-size="9" text-anchor="middle">in_progress · owner: agent-1</text>
<rect x="475" y="164" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
<text x="560" y="186" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ docs</text>
<text x="560" y="202" fill="#94a3b8" font-size="9" text-anchor="middle">pending · blockedBy: schema ✓</text>
<!-- Arrows: endpoints → tests, docs → deploy -->
<path d="M 200 212 L 200 262" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<path d="M 510 212 L 440 262" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<!-- Row 3: tests (pending), deploy (pending) -->
<rect x="115" y="264" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
<text x="200" y="286" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ tests</text>
<text x="200" y="302" fill="#94a3b8" font-size="9" text-anchor="middle">blockedBy: endpoints ●</text>
<!-- Arrow: tests → deploy -->
<path d="M 285 288 L 375 288" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<rect x="375" y="264" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
<text x="460" y="286" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ deploy</text>
<text x="460" y="302" fill="#94a3b8" font-size="9" text-anchor="middle">blockedBy: tests, docs</text>
<!-- Legend -->
<rect x="40" y="338" width="680" height="46" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="60" y="352" width="14" height="12" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="80" y="363" fill="#475569" font-size="10">completed</text>
<rect x="160" y="352" width="14" height="12" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="180" y="363" fill="#475569" font-size="10">in_progress</text>
<rect x="270" y="352" width="14" height="12" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="290" y="363" fill="#475569" font-size="10">pending</text>
<text x="370" y="363" fill="#94a3b8" font-size="10">→ blockedBy (arrows = dependency direction)</text>
<text x="60" y="378" fill="#94a3b8" font-size="9">docs' blockedBy (schema) is completed → can_start returns True, can be claimed</text>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,59 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 400" font-family="system-ui, -apple-system, sans-serif">
<defs>
<marker id="dep" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#94a3b8"/>
</marker>
</defs>
<rect width="760" height="400" fill="#fafbfc" rx="8"/>
<!-- タイトル -->
<rect x="0" y="0" width="760" height="44" fill="#0d9488" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="#0d9488"/>
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Task DAG — 依存関係の例:データベース → API → テスト → デプロイ</text>
<!-- 行 1: schema完了 -->
<rect x="295" y="70" width="170" height="48" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="2"/>
<text x="380" y="92" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">✓ schema</text>
<text x="380" y="108" fill="#16a34a" font-size="9" text-anchor="middle">completed</text>
<!-- 矢印: schema → endpoints, schema → docs -->
<path d="M 340 118 L 240 162" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<path d="M 420 118 L 520 162" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<!-- 行 2: endpoints進行中、docs保留中 -->
<rect x="115" y="164" width="170" height="48" rx="8" fill="#dbeafe" stroke="#2563eb" stroke-width="2"/>
<text x="200" y="186" fill="#1e40af" font-size="12" font-weight="700" text-anchor="middle">● endpoints</text>
<text x="200" y="202" fill="#2563eb" font-size="9" text-anchor="middle">in_progress · owner: agent-1</text>
<rect x="475" y="164" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
<text x="560" y="186" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ docs</text>
<text x="560" y="202" fill="#94a3b8" font-size="9" text-anchor="middle">pending · blockedBy: schema ✓</text>
<!-- 矢印: endpoints → tests, docs → deploy -->
<path d="M 200 212 L 200 262" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<path d="M 510 212 L 440 262" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<!-- 行 3: tests保留中、deploy保留中 -->
<rect x="115" y="264" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
<text x="200" y="286" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ tests</text>
<text x="200" y="302" fill="#94a3b8" font-size="9" text-anchor="middle">blockedBy: endpoints ●</text>
<!-- 矢印: tests → deploy -->
<path d="M 285 288 L 375 288" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<rect x="375" y="264" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
<text x="460" y="286" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ deploy</text>
<text x="460" y="302" fill="#94a3b8" font-size="9" text-anchor="middle">blockedBy: tests, docs</text>
<!-- 凡例 -->
<rect x="40" y="338" width="680" height="46" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="60" y="352" width="14" height="12" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="80" y="363" fill="#475569" font-size="10">completed</text>
<rect x="160" y="352" width="14" height="12" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="180" y="363" fill="#475569" font-size="10">in_progress</text>
<rect x="270" y="352" width="14" height="12" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="290" y="363" fill="#475569" font-size="10">pending</text>
<text x="370" y="363" fill="#94a3b8" font-size="10">→ blockedBy矢印 = 依存方向)</text>
<text x="60" y="378" fill="#94a3b8" font-size="9">docs の blockedBy (schema) は完了済み → can_start が True を返し、claim 可能</text>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,59 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 400" font-family="system-ui, -apple-system, sans-serif">
<defs>
<marker id="dep" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#94a3b8"/>
</marker>
</defs>
<rect width="760" height="400" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="760" height="44" fill="#0d9488" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="#0d9488"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task DAG — 依赖关系示例:搭数据库 → API → 测试 → 部署</text>
<!-- Row 1: schema (completed) -->
<rect x="295" y="70" width="170" height="48" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="2"/>
<text x="380" y="92" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">✓ schema</text>
<text x="380" y="108" fill="#16a34a" font-size="9" text-anchor="middle">completed</text>
<!-- Arrows: schema → endpoints, schema → docs -->
<path d="M 340 118 L 240 162" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<path d="M 420 118 L 520 162" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<!-- Row 2: endpoints (in_progress), docs (pending) -->
<rect x="115" y="164" width="170" height="48" rx="8" fill="#dbeafe" stroke="#2563eb" stroke-width="2"/>
<text x="200" y="186" fill="#1e40af" font-size="12" font-weight="700" text-anchor="middle">● endpoints</text>
<text x="200" y="202" fill="#2563eb" font-size="9" text-anchor="middle">in_progress · owner: agent-1</text>
<rect x="475" y="164" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
<text x="560" y="186" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ docs</text>
<text x="560" y="202" fill="#94a3b8" font-size="9" text-anchor="middle">pending · blockedBy: schema ✓</text>
<!-- Arrows: endpoints → tests, docs → deploy -->
<path d="M 200 212 L 200 262" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<path d="M 510 212 L 440 262" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<!-- Row 3: tests (pending), deploy (pending) -->
<rect x="115" y="264" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
<text x="200" y="286" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ tests</text>
<text x="200" y="302" fill="#94a3b8" font-size="9" text-anchor="middle">blockedBy: endpoints ●</text>
<!-- Arrow: tests → deploy -->
<path d="M 285 288 L 375 288" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#dep)"/>
<rect x="375" y="264" width="170" height="48" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
<text x="460" y="286" fill="#475569" font-size="12" font-weight="700" text-anchor="middle">○ deploy</text>
<text x="460" y="302" fill="#94a3b8" font-size="9" text-anchor="middle">blockedBy: tests, docs</text>
<!-- Legend -->
<rect x="40" y="338" width="680" height="46" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="60" y="352" width="14" height="12" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="80" y="363" fill="#475569" font-size="10">completed</text>
<rect x="160" y="352" width="14" height="12" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="180" y="363" fill="#475569" font-size="10">in_progress</text>
<rect x="270" y="352" width="14" height="12" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="290" y="363" fill="#475569" font-size="10">pending</text>
<text x="370" y="363" fill="#94a3b8" font-size="10">→ blockedBy箭头 = 依赖方向)</text>
<text x="60" y="378" fill="#94a3b8" font-size="9">docs 的 blockedBy (schema) 已完成 → can_start 返回 True可被 claim</text>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,94 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 420" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0d9488"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-teal" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0d9488"/>
</marker>
</defs>
<rect width="760" height="420" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 5 Task Tools + .tasks/ Persistence + blockedBy Dependencies</text>
<!-- Legend -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s11 Preserved</text>
<rect x="160" y="56" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
<text x="178" y="66" fill="#0d9488" font-size="10" font-weight="600">s12 New</text>
<!-- ===== s11 loop (compact) ===== -->
<rect x="30" y="92" width="80" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="70" y="116" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
<line x1="110" y1="112" x2="128" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="131" y="86" width="120" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="191" y="108" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt + compress</text>
<text x="191" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">(s10-s11)</text>
<line x1="251" y1="112" x2="269" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="272" y="86" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="322" y="108" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM (try/except)</text>
<text x="322" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">(s11)</text>
<line x1="372" y1="112" x2="390" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- TOOLS (expanded) -->
<rect x="393" y="80" width="210" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="498" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write</text>
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · list_tasks</text>
<text x="408" y="140" fill="#0d9488" font-size="9" font-weight="600">get_task · claim_task · complete_task</text>
<!-- Loop back -->
<path d="M 603 112 L 640 112 L 640 155 L 70 155 L 70 132" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<!-- ===== .tasks/ directory (teal) ===== -->
<rect x="40" y="185" width="310" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
<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>
<!-- 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)"/>
<text x="320" y="178" fill="#0d9488" font-size="9">create / save / read</text>
<!-- ===== Lifecycle (teal) ===== -->
<rect x="390" y="185" width="330" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
<text x="555" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">Dependency Check + Lifecycle</text>
<text x="408" y="222" fill="#0d9488" font-size="9">can_start: all blockedBy completed?</text>
<text x="408" y="238" fill="#0d9488" font-size="9">claim_task → owner = agent, pending → in_progress</text>
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + unblock downstream</text>
<!-- ===== State machine ===== -->
<rect x="40" y="286" width="680" height="46" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<text x="60" y="310" fill="#1e3a5f" font-size="11" font-weight="600">State Machine:</text>
<rect x="160" y="298" width="56" height="20" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="188" y="312" fill="#475569" font-size="9" text-anchor="middle">pending</text>
<line x1="216" y1="308" x2="288" y2="308" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
<text x="252" y="303" fill="#0d9488" font-size="8" font-weight="600" text-anchor="middle">claim</text>
<rect x="290" y="298" width="76" height="20" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="328" y="312" fill="#1e40af" font-size="9" text-anchor="middle">in_progress</text>
<line x1="366" y1="308" x2="458" y2="308" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
<text x="412" y="303" fill="#16a34a" font-size="8" font-weight="600" text-anchor="middle">complete_task</text>
<rect x="460" y="298" width="72" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="496" y="312" fill="#166534" font-size="9" text-anchor="middle">completed</text>
<text x="548" y="312" fill="#94a3b8" font-size="9">No release rollback; crash → unassign owner</text>
<!-- ===== Bottom notes ===== -->
<rect x="40" y="352" width="680" height="52" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="60" y="366" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="80" y="376" fill="#475569" font-size="10">s11 Preserved: loop, prompt assembly, compression (error recovery independent from task system)</text>
<rect x="60" y="384" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
<text x="80" y="394" fill="#475569" font-size="10">s12 New: Task dataclass + 5 tools + .tasks/ persistence + blockedBy dependency graph</text>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,94 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 420" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0d9488"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-teal" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0d9488"/>
</marker>
</defs>
<rect width="760" height="420" fill="#fafbfc" rx="8"/>
<!-- タイトル -->
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Task System — 5 つのタスクツール + .tasks/ 永続化 + blockedBy 依存</text>
<!-- 凡例 -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s11 保持</text>
<rect x="140" y="56" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
<text x="158" y="66" fill="#0d9488" font-size="10" font-weight="600">s12 新規</text>
<!-- ===== s11 ループ(コンパクト) ===== -->
<rect x="30" y="92" width="80" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="70" y="116" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
<line x1="110" y1="112" x2="128" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="131" y="86" width="120" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="191" y="108" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt + compress</text>
<text x="191" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">(s10-s11)</text>
<line x1="251" y1="112" x2="269" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="272" y="86" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="322" y="108" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM (try/except)</text>
<text x="322" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">(s11)</text>
<line x1="372" y1="112" x2="390" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- ツール(展開) -->
<rect x="393" y="80" width="210" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="498" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write</text>
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · list_tasks</text>
<text x="408" y="140" fill="#0d9488" font-size="9" font-weight="600">get_task · claim_task · complete_task</text>
<!-- ループバック -->
<path d="M 603 112 L 640 112 L 640 155 L 70 155 L 70 132" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<!-- ===== .tasks/ ディレクトリ(ティール) ===== -->
<rect x="40" y="185" width="310" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
<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>
<!-- 矢印: 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)"/>
<text x="320" y="178" fill="#0d9488" font-size="9">create / save / read</text>
<!-- ===== ライフサイクル(ティール) ===== -->
<rect x="390" y="185" width="330" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
<text x="555" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">依存チェック + ライフサイクル</text>
<text x="408" y="222" fill="#0d9488" font-size="9">can_start: blockedBy がすべて completed?</text>
<text x="408" y="238" fill="#0d9488" font-size="9">claim_task → owner = agent, pending → in_progress</text>
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + 下流をアンロック</text>
<!-- ===== 状態マシン ===== -->
<rect x="40" y="286" width="680" height="46" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<text x="60" y="310" fill="#1e3a5f" font-size="11" font-weight="600">状態マシン:</text>
<rect x="150" y="298" width="56" height="20" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="178" y="312" fill="#475569" font-size="9" text-anchor="middle">pending</text>
<line x1="206" y1="308" x2="278" y2="308" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
<text x="242" y="303" fill="#0d9488" font-size="8" font-weight="600" text-anchor="middle">claim</text>
<rect x="280" y="298" width="76" height="20" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="318" y="312" fill="#1e40af" font-size="9" text-anchor="middle">in_progress</text>
<line x1="356" y1="308" x2="448" y2="308" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
<text x="402" y="303" fill="#16a34a" font-size="8" font-weight="600" text-anchor="middle">complete_task</text>
<rect x="450" y="298" width="72" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="486" y="312" fill="#166534" font-size="9" text-anchor="middle">completed</text>
<text x="538" y="312" fill="#94a3b8" font-size="9">release ロールバックなし、クラッシュ時は unassign で owner クリア</text>
<!-- ===== 下部ノート ===== -->
<rect x="40" y="352" width="680" height="52" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="60" y="366" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="80" y="376" fill="#475569" font-size="10">s11 保持:ループ、プロンプト組み立て、圧縮(エラーリカバリとタスクシステムは独立)</text>
<rect x="60" y="384" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
<text x="80" y="394" fill="#475569" font-size="10">s12 新規Task dataclass + 5 ツール + .tasks/ 永続化 + blockedBy 依存グラフ</text>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -0,0 +1,94 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 420" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0d9488"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-teal" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0d9488"/>
</marker>
</defs>
<rect width="760" height="420" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 5 个任务工具 + .tasks/ 持久化 + blockedBy 依赖</text>
<!-- Legend -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s11 保留</text>
<rect x="140" y="56" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
<text x="158" y="66" fill="#0d9488" font-size="10" font-weight="600">s12 新增</text>
<!-- ===== s11 loop (compact) ===== -->
<rect x="30" y="92" width="80" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="70" y="116" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
<line x1="110" y1="112" x2="128" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="131" y="86" width="120" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="191" y="108" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt + compress</text>
<text x="191" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">(s10-s11)</text>
<line x1="251" y1="112" x2="269" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="272" y="86" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="322" y="108" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM (try/except)</text>
<text x="322" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">(s11)</text>
<line x1="372" y1="112" x2="390" y2="112" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- TOOLS (expanded) -->
<rect x="393" y="80" width="210" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="498" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write</text>
<text x="408" y="128" fill="#0d9488" font-size="9" font-weight="600">create_task · list_tasks</text>
<text x="408" y="140" fill="#0d9488" font-size="9" font-weight="600">get_task · claim_task · complete_task</text>
<!-- Loop back -->
<path d="M 603 112 L 640 112 L 640 155 L 70 155 L 70 132" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<!-- ===== .tasks/ directory (teal) ===== -->
<rect x="40" y="185" width="310" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
<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>
<!-- 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)"/>
<text x="320" y="178" fill="#0d9488" font-size="9">create / save / read</text>
<!-- ===== Lifecycle (teal) ===== -->
<rect x="390" y="185" width="330" height="76" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
<text x="555" y="205" fill="#134e4a" font-size="11" font-weight="700" text-anchor="middle">依赖检查 + 生命周期</text>
<text x="408" y="222" fill="#0d9488" font-size="9">can_start: blockedBy 全部 completed?</text>
<text x="408" y="238" fill="#0d9488" font-size="9">claim_task → owner = agent, pending → in_progress</text>
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + 解锁下游</text>
<!-- ===== State machine ===== -->
<rect x="40" y="286" width="680" height="46" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<text x="60" y="310" fill="#1e3a5f" font-size="11" font-weight="600">状态机:</text>
<rect x="120" y="298" width="56" height="20" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="148" y="312" fill="#475569" font-size="9" text-anchor="middle">pending</text>
<line x1="176" y1="308" x2="238" y2="308" stroke="#0d9488" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
<text x="207" y="303" fill="#0d9488" font-size="8" font-weight="600" text-anchor="middle">claim</text>
<rect x="240" y="298" width="76" height="20" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="278" y="312" fill="#1e40af" font-size="9" text-anchor="middle">in_progress</text>
<line x1="316" y1="308" x2="408" y2="308" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-teal)"/>
<text x="362" y="303" fill="#16a34a" font-size="8" font-weight="600" text-anchor="middle">complete_task</text>
<rect x="410" y="298" width="72" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="446" y="312" fill="#166534" font-size="9" text-anchor="middle">completed</text>
<text x="500" y="312" fill="#94a3b8" font-size="9">CC 无 release 回退,崩溃时用 unassign 清 owner</text>
<!-- ===== Bottom notes ===== -->
<rect x="40" y="352" width="680" height="52" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="60" y="366" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="80" y="376" fill="#475569" font-size="10">s11 保留循环、prompt 组装、压缩(错误恢复与任务系统独立)</text>
<rect x="60" y="384" width="12" height="10" rx="2" fill="#f0fdfa" stroke="#0d9488" stroke-width="1"/>
<text x="80" y="394" fill="#475569" font-size="10">s12 新增Task dataclass + 5 个工具 + .tasks/ 持久化 + blockedBy 依赖图</text>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB