refactor: streamline the course to 17 lessons

This commit is contained in:
Haoran
2026-08-12 03:02:42 +08:00
parent ab35e59672
commit 7e2f2fd99b
250 changed files with 12179 additions and 18653 deletions

View File

@@ -0,0 +1,211 @@
# s10: Task System — 実行チェックリストから協調できるタスク状態へ
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → ... → s08 → s09 → `s10` → [s11](../s11_background_tasks/) → s12 → ... → s16 → s17
> *"大きな目標を小さなタスクに分け、順序付け、永続化"* — ファイル永続化タスクグラフ、マルチ Agent 協調の基盤。
>
> **Harness 層**: タスク — 永続化された目標、復旧可能な進捗。
---
## 課題
s05 の TodoWrite は、Agent が現在のタスクの実行手順を記録するためのものだ。各項目には内容と状態があり、次に何をするべきかを確認できる。
プロジェクトをデータベーステーブルの作成、API の実装、テストの追加という 3 つのタスクに分ける場合、Harness はそれらの関係も把握する必要がある。API はデータベーステーブルの完成を待ち、テストは API の仕様が確定するまで待たなければならない。各タスクの担当者も記録する必要がある。
TodoWrite は、こうした依存関係や担当を記録しない。「API を実装する」が未完了であることは示せても、そのタスクを開始できるかどうかを Harness が判断することはできない。
この章では Task System を追加する。各タスクは個別の ID と状態を持ち、`blockedBy` が前提タスクを、`owner` が担当する Agent を記録する。
---
## ソリューション
![Task System Overview](images/task-system-overview.ja.svg)
コードは S04 の 5 つの基本ツール、Permission、Hooks、共通の `execute_tool` を保ち、そこへ 5 つのタスクツール、`.tasks/` ディレクトリへの永続化、`blockedBy` の依存チェックを追加する。
TodoWrite vs Task System
| | TodoWrite (s05) | Task System (s10) |
|---|---|---|
| 位置づけ | 現在のタスクの実行チェックリスト | 復旧可能なタスクシステム |
| ストレージ | プロセス内 / セッション状態 | `.tasks/{id}.json` |
| 依存関係 | なし | `blockedBy` 依存グラフ |
| ライフサイクル | 現在のセッション / 現在のタスク | セッション横断 |
| 分担 | タスクの引き受けなし | `owner` / claim |
| ステータス | pending / in_progress / completed | pending / in_progress / completed |
| 粒度 | Agent 自身の手順 | 引き受け・追跡・アンロックできるタスク |
| 更新契約 | リスト全体を置換 | 個別レコードを作成・取得・更新・一覧 |
---
## 仕組み
![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
blockedBy: list[str] # 依存タスク ID のリスト
```
ID は `task_` と 8 桁のランダムな 16 進文字で生成する。ファイルは排他的に作成し、同じ ID が存在する場合は生成し直す。
`TaskStore` はタスク ID を検証し、JSON ファイルを読み書きする。`TASKS = TaskStore(TASKS_DIR)` がこの章で使うタスクストアである。
### create_task: タスク作成
```python
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
return TASKS.create(subject, description, blockedBy)
```
`TaskStore.create` は subject と依存 ID を確認し、`.tasks/{id}.json` に書き込む。`blockedBy` で依存を宣言し、例えば「API を書く」タスクはデータベースタスクの ID を参照できる。
### can_start: 依存チェック
タスクは `blockedBy` が**すべて completed** になってからでないと開始できない:
```python
def can_start(task_id: str) -> bool:
return not incomplete_dependencies(load_task(task_id))
```
`incomplete_dependencies` は各前提タスクを読み込む。completed でないタスクや、ファイルが存在しないタスクが一つでもあれば引き受けられない。
### claim_task: タスクを引き受ける
Agent がタスクに取り掛かる時、`claim_task` を呼び出し、`owner` を設定してステータスを `pending``in_progress` に変更する。`owner` フィールドは誰がタスクを引き受けたかを記録する:
```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"
dependencies = incomplete_dependencies(task)
if dependencies:
return f"Blocked by: {dependencies}"
task.owner = owner
task.status = "in_progress"
TASKS.save(task)
return f"Claimed {task_id} ({task.subject})"
```
タスクが pending でない場合や、依存が未完了の場合は引き受けを拒否する。S10 はタスクの状態を順番に更新する。
### complete_task: 完了とアンロック
タスク完了後、`completed` に設定。同時に他の全タスクを走査し、**直前にアンロックされた**下流タスクを特定:
```python
def complete_task(task_id: str, owner: str = "agent") -> str:
task = load_task(task_id)
if task.status != "in_progress":
return f"Task {task_id} is {task.status}, cannot complete"
if task.owner != owner:
return f"Task {task_id} is owned by {task.owner}, not {owner}"
ready_before = {t.id for t in list_tasks()
if t.status == "pending" and t.blockedBy
and can_start(t.id)}
task.status = "completed"
TASKS.save(task)
unblocked = [t.subject for t in list_tasks()
if t.status == "pending" and t.blockedBy
and t.id not in ready_before
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`。タスクを完了済みにし、下流をアンロック。
### 組み合わせて実行
```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 はファイルを読んで進捗を復旧。
---
## 試してみる
```sh
cd learn-claude-code
python s10_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 Loop は現在のツール呼び出しでブロックされ、コマンドが終了するまで他の処理を続けられない。
s11 Background Tasks → 遅い操作をバックグラウンドで実行する。Agent は他のタスクの処理を続け、バックグラウンド処理の完了後に通知を受け取る。
<!-- translation-sync: zh@v4, en@v4, ja@v4 -->

211
s10_task_system/README.md Normal file
View File

@@ -0,0 +1,211 @@
# s10: Task System — From an Execution Checklist to Coordinated Task State
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → ... → s08 → s09 → `s10` → [s11](../s11_background_tasks/) → s12 → ... → s16 → s17
> *"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
s05's TodoWrite lets an agent record the steps of its current task. Each checklist item has content and a status, helping the agent keep track of what remains.
When a project is split into three tasks—creating database tables, writing an API, and adding tests—the Harness also needs to know how they relate: the API must wait for the database tables, and the tests must wait for a stable API. It also needs to record who is responsible for each task.
TodoWrite does not record these dependencies or assignments. It can show that "write the API" is unfinished, but the Harness cannot use that information to decide whether the task is ready to start.
This chapter adds a Task System. Each task has its own ID and status; `blockedBy` records prerequisites, and `owner` records the agent responsible for the task.
---
## The Solution
![Task System Overview](images/task-system-overview.en.svg)
The code keeps S04's five base tools, Permission, Hooks, and shared `execute_tool`, then adds 5 task tools, persistence in the `.tasks/` directory, and `blockedBy` dependency checks.
TodoWrite vs Task System:
| | TodoWrite (s05) | Task System (s10) |
|---|---|---|
| Role | Execution checklist for the current task | Recoverable task system |
| Storage | In-process / session state | `.tasks/{id}.json` |
| Dependencies | None | `blockedBy` dependency graph |
| Lifecycle | Current session / current task | Cross-session |
| Coordination | No task claiming | `owner` / claim |
| Status | pending / in_progress / completed | pending / in_progress / completed |
| 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 |
---
## 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 responsible for this task
blockedBy: list[str] # List of dependency task IDs
```
IDs use the `task_` prefix followed by 8 random hexadecimal characters. Files are created exclusively; an existing ID is discarded and regenerated.
`TaskStore` validates task IDs and reads and writes the JSON files. `TASKS = TaskStore(TASKS_DIR)` is the store used by this chapter.
### create_task: Create Tasks
```python
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
return TASKS.create(subject, description, blockedBy)
```
`TaskStore.create` checks the subject and dependency IDs, then writes `.tasks/{id}.json`. `blockedBy` declares dependencies; for example, "write API" can reference the database task's ID.
### can_start: Dependency Check
A task can only start after all its `blockedBy` dependencies are **completed**:
```python
def can_start(task_id: str) -> bool:
return not incomplete_dependencies(load_task(task_id))
```
`incomplete_dependencies` loads each prerequisite. A task cannot be claimed if any prerequisite is not completed or its file no longer exists.
### 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 claimed the task:
```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"
dependencies = incomplete_dependencies(task)
if dependencies:
return f"Blocked by: {dependencies}"
task.owner = owner
task.status = "in_progress"
TASKS.save(task)
return f"Claimed {task_id} ({task.subject})"
```
The claim is rejected if the task is not pending or its dependencies are incomplete. S10 only updates task state sequentially.
### 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, owner: str = "agent") -> str:
task = load_task(task_id)
if task.status != "in_progress":
return f"Task {task_id} is {task.status}, cannot complete"
if task.owner != owner:
return f"Task {task_id} is owned by {task.owner}, not {owner}"
ready_before = {t.id for t in list_tasks()
if t.status == "pending" and t.blockedBy
and can_start(t.id)}
task.status = "completed"
TASKS.save(task)
unblocked = [t.subject for t in list_tasks()
if t.status == "pending" and t.blockedBy
and t.id not in ready_before
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.
### 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.
---
## Try It
```sh
cd learn-claude-code
python s10_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 full test suites, dependency installation, and deployment commands can take a long time. When these commands run synchronously, the Agent Loop remains blocked in the current tool call and cannot continue until the command finishes.
s11 Background Tasks → Slow operations run in the background. The Agent Loop can continue processing other tasks and receives a notification when the background work finishes.
<!-- translation-sync: zh@v4, en@v4, ja@v4 -->

View File

@@ -0,0 +1,211 @@
# s10: Task System — 从执行清单到可协调的任务状态
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → ... → s08 → s09 → `s10` → [s11](../s11_background_tasks/) → s12 → ... → s16 → s17
> *"大目标拆成小任务, 排好序, 持久化"* — 文件持久化的任务图, 多 agent 协作的基础。
>
> **Harness 层**: 任务 — 持久化的目标, 可恢复的进度。
---
## 问题
s05 的 TodoWrite 让 Agent 记录当前任务的执行步骤。清单中的每一项只有内容和状态,用来提醒 Agent 接下来还要做什么。
当项目被拆成创建数据库表、编写 API 和添加测试三个任务时Harness 还需要知道它们之间的关系:数据库表完成后才能编写 APIAPI 接口确定后才能添加测试。每个任务还要记录由谁负责。
TodoWrite 没有记录这些依赖和分工。它可以显示“编写 API”仍未完成但 Harness 无法据此判断这个任务是否可以开始。
本章加入 Task System。每个任务都有独立的 ID 和状态,`blockedBy` 记录前置任务,`owner` 记录负责执行的 Agent。
---
## 解决方案
![Task System Overview](images/task-system-overview.svg)
代码保留 S04 的五个基础工具、Permission、Hooks 和统一 `execute_tool`,再加入 5 个任务工具、`.tasks/` 目录持久化和 `blockedBy` 依赖检查。
TodoWrite vs Task System
| | TodoWrite (s05) | Task System (s10) |
|---|---|---|
| 定位 | 当前任务的执行清单 | 可恢复的任务系统 |
| 存储 | 进程内 / 会话状态 | `.tasks/{id}.json` |
| 依赖 | 无 | `blockedBy` 依赖图 |
| 生命周期 | 当前会话 / 当前任务 | 跨会话保留 |
| 分工 | 不负责任务认领 | `owner` / claim |
| 状态 | pending / in_progress / completed | pending / in_progress / completed |
| 粒度 | Agent 自己的步骤 | 可被认领、追踪、解锁的任务 |
| 更新契约 | 整表替换 | 对单条记录执行创建、读取、更新、列举 |
---
## 工作原理
![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
blockedBy: list[str] # 依赖的任务 ID 列表
```
ID 使用 `task_` 加 8 位随机十六进制字符生成。创建文件时使用排他写入;如果 ID 已存在,就重新生成。
`TaskStore` 负责校验任务 ID 和读写 JSON 文件,`TASKS = TaskStore(TASKS_DIR)` 是本章使用的任务存储。
### create_task: 创建任务
```python
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
return TASKS.create(subject, description, blockedBy)
```
`TaskStore.create` 检查 subject 和依赖 ID再把任务写入 `.tasks/{id}.json``blockedBy` 声明依赖,比如“写 API”的 `blockedBy` 可以指向数据库任务的 ID。
### can_start: 依赖检查
一个任务只能在它的 `blockedBy` **全部 completed** 之后才能开始:
```python
def can_start(task_id: str) -> bool:
return not incomplete_dependencies(load_task(task_id))
```
`incomplete_dependencies` 读取每个前置任务。只要有一个不是 completed或者对应文件已经不存在任务就不能认领。
### claim_task: 认领任务
Agent 开始做一个任务时,调用 `claim_task`:设置 `owner`,状态从 `pending``in_progress``owner` 字段记录谁认领了这个任务:
```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"
dependencies = incomplete_dependencies(task)
if dependencies:
return f"Blocked by: {dependencies}"
task.owner = owner
task.status = "in_progress"
TASKS.save(task)
return f"Claimed {task_id} ({task.subject})"
```
如果任务不是 pending或者依赖没有完成就拒绝认领。S10 只处理顺序执行的状态更新。
### complete_task: 完成与解锁
任务做完后,设为 `completed`。同时扫描所有其他任务,找出**刚刚被解锁**的下游任务:
```python
def complete_task(task_id: str, owner: str = "agent") -> str:
task = load_task(task_id)
if task.status != "in_progress":
return f"Task {task_id} is {task.status}, cannot complete"
if task.owner != owner:
return f"Task {task_id} is owned by {task.owner}, not {owner}"
ready_before = {t.id for t in list_tasks()
if t.status == "pending" and t.blockedBy
and can_start(t.id)}
task.status = "completed"
TASKS.save(task)
unblocked = [t.subject for t in list_tasks()
if t.status == "pending" and t.blockedBy
and t.id not in ready_before
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`。把任务标记为完成,并解锁下游。
### 合起来跑
```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 读文件就能恢复进度。
---
## 试一下
```sh
cd learn-claude-code
python s10_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 Loop 会一直停在当前工具调用上,只有命令结束后才能继续处理其他工作。
s11 Background Tasks → 把慢操作放到后台。Agent 可以继续处理其他任务,后台执行完成后再接收通知。
<!-- translation-sync: zh@v4, en@v4, ja@v4 -->

530
s10_task_system/code.py Normal file
View File

@@ -0,0 +1,530 @@
#!/usr/bin/env python3
"""
s10_task_system.py - Task System
.tasks/
task_a1b2c3d4.json {status: completed, blockedBy: []}
task_e5f6a7b8.json {status: pending, blockedBy: [task_a1b2c3d4]}
task_11223344.json {status: pending, blockedBy: [task_e5f6a7b8]}
Dependency graph:
+-----------+ +-----------+ +-----------+
| schema | ---> | API | ---> | tests |
| completed | | pending | | pending |
+-----------+ +-----------+ +-----------+
can_start(API) is true because schema is completed.
Task lifecycle:
pending --claim_task--> in_progress --complete_task--> completed
"""
import glob
import json
import os
import re
import secrets
import subprocess
from dataclasses import asdict, dataclass
from pathlib import Path
try:
import readline
readline.parse_and_bind("set bind-tty-special-chars off")
readline.parse_and_bind("set input-meta on")
readline.parse_and_bind("set output-meta on")
readline.parse_and_bind("set convert-meta 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()
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
"Use task tools to track dependencies and progress."
)
# -- New in s10: persistent task records --
TASKS_DIR = WORKDIR / ".tasks"
TASK_ID_PATTERN = re.compile(r"^task_[0-9a-f]{8}$")
@dataclass
class Task:
id: str
subject: str
description: str
status: str
owner: str | None
blockedBy: list[str]
class TaskStore:
def __init__(self, directory: Path):
self.directory = directory
def _root(self, create: bool = False) -> Path:
if create:
self.directory.mkdir(parents=True, exist_ok=True)
root = self.directory.resolve()
if not root.is_relative_to(WORKDIR.resolve()):
raise ValueError("Task store escapes the workspace")
return root
def _path(self, task_id: str, create_root: bool = False) -> Path:
if not isinstance(task_id, str) or not TASK_ID_PATTERN.fullmatch(task_id):
raise ValueError(f"Invalid task ID: {task_id!r}")
root = self._root(create=create_root)
path = (root / f"{task_id}.json").resolve()
if not path.is_relative_to(root):
raise ValueError(f"Invalid task ID: {task_id!r}")
return path
def exists(self, task_id: str) -> bool:
return self._path(task_id).is_file()
def create(self, subject: str, description: str = "",
blocked_by: list[str] | None = None) -> Task:
subject = subject.strip()
if not subject:
raise ValueError("Task subject cannot be empty")
dependencies = list(dict.fromkeys(blocked_by or []))
for dependency in dependencies:
if not self.exists(dependency):
raise ValueError(f"Dependency not found: {dependency}")
self._root(create=True)
for _ in range(100):
task = Task(
id=f"task_{secrets.token_hex(4)}",
subject=subject,
description=description,
status="pending",
owner=None,
blockedBy=dependencies,
)
try:
with self._path(task.id, create_root=True).open(
"x", encoding="utf-8"
) as handle:
json.dump(asdict(task), handle, indent=2)
return task
except FileExistsError:
continue
raise RuntimeError("Could not allocate a unique task ID")
def save(self, task: Task) -> None:
self._path(task.id, create_root=True).write_text(
json.dumps(asdict(task), indent=2),
encoding="utf-8",
)
def load(self, task_id: str) -> Task:
data = json.loads(self._path(task_id).read_text(encoding="utf-8"))
task = Task(**data)
if task.id != task_id:
raise ValueError(f"Task file ID does not match {task_id}")
if task.status not in ("pending", "in_progress", "completed"):
raise ValueError(f"Invalid task status: {task.status}")
return task
def list(self) -> list[Task]:
if not self.directory.exists():
return []
root = self._root()
return [self.load(path.stem)
for path in sorted(root.glob("task_*.json"))]
TASKS = TaskStore(TASKS_DIR)
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
return TASKS.create(subject, description, blockedBy)
def load_task(task_id: str) -> Task:
return TASKS.load(task_id)
def list_tasks() -> list[Task]:
return TASKS.list()
def get_task(task_id: str) -> str:
return json.dumps(asdict(load_task(task_id)), indent=2)
def incomplete_dependencies(task: Task) -> list[str]:
incomplete = []
for dependency in task.blockedBy:
try:
if load_task(dependency).status != "completed":
incomplete.append(dependency)
except (FileNotFoundError, ValueError):
incomplete.append(dependency)
return incomplete
def can_start(task_id: str) -> bool:
return not incomplete_dependencies(load_task(task_id))
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"
dependencies = incomplete_dependencies(task)
if dependencies:
return f"Blocked by: {dependencies}"
task.owner = owner
task.status = "in_progress"
TASKS.save(task)
print(f" [claim] {task.subject} -> in_progress (owner: {owner})")
return f"Claimed {task.id} ({task.subject})"
def complete_task(task_id: str, owner: str = "agent") -> str:
task = load_task(task_id)
if task.status != "in_progress":
return f"Task {task_id} is {task.status}, cannot complete"
if task.owner != owner:
return f"Task {task_id} is owned by {task.owner}, not {owner}"
ready_before = {
candidate.id
for candidate in list_tasks()
if candidate.status == "pending"
and candidate.blockedBy
and can_start(candidate.id)
}
task.status = "completed"
TASKS.save(task)
unblocked = [candidate.subject for candidate in list_tasks()
if candidate.status == "pending"
and candidate.blockedBy
and candidate.id not in ready_before
and can_start(candidate.id)]
print(f" [complete] {task.subject}")
message = f"Completed {task.id} ({task.subject})"
if unblocked:
message += f"\nUnblocked: {', '.join(unblocked)}"
print(f" [unblocked] {', '.join(unblocked)}")
return message
# -- From s04: tool implementations --
def run_bash(command: str) -> str:
try:
result = subprocess.run(
command,
shell=True,
cwd=WORKDIR,
capture_output=True,
text=True,
timeout=120,
)
output = (result.stdout + result.stderr).strip()
return output[:50000] if output else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
def run_read(path: str, limit: int | None = None) -> str:
try:
lines = (WORKDIR / path).resolve().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 error:
return f"Error: {error}"
def run_write(path: str, content: str) -> str:
try:
file_path = (WORKDIR / path).resolve()
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
except Exception as error:
return f"Error: {error}"
def run_edit(path: str, old_text: str, new_text: str) -> str:
try:
file_path = (WORKDIR / path).resolve()
text = file_path.read_text()
if old_text not in text:
return f"Error: text not found in {path}"
file_path.write_text(text.replace(old_text, new_text, 1))
return f"Edited {path}"
except Exception as error:
return f"Error: {error}"
def run_glob(pattern: str) -> str:
try:
matches = [
match
for match in glob.glob(pattern, root_dir=WORKDIR)
if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
]
return "\n".join(matches) if matches else "(no matches)"
except Exception as error:
return f"Error: {error}"
def run_create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> str:
task = create_task(subject, description, blockedBy)
dependencies = (
f" (blockedBy: {', '.join(task.blockedBy)})"
if task.blockedBy else ""
)
print(f" [create] {task.subject}{dependencies}")
return f"Created {task.id}: {task.subject}{dependencies}"
def run_list_tasks() -> str:
tasks = list_tasks()
if not tasks:
return "No tasks. Use create_task to add some."
lines = []
for task in tasks:
marker = {
"pending": "[ ]",
"in_progress": "[>]",
"completed": "[x]",
}.get(task.status, "[?]")
dependencies = (
f" (blockedBy: {', '.join(task.blockedBy)})"
if task.blockedBy else ""
)
owner = f" [{task.owner}]" if task.owner else ""
lines.append(
f"{marker} {task.id}: {task.subject} "
f"[{task.status}]{owner}{dependencies}"
)
return "\n".join(lines)
def run_get_task(task_id: str) -> str:
return get_task(task_id)
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, owner="agent")
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": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
{"name": "create_task", "description": "Create a task with optional 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 tasks with status, owner, and dependencies.",
"input_schema": {"type": "object", "properties": {}}},
{"name": "get_task", "description": "Get a task by ID.",
"input_schema": {"type": "object", "properties": {"task_id": {"type": "string"}}, "required": ["task_id"]}},
{"name": "claim_task", "description": "Claim a pending task whose dependencies are complete.",
"input_schema": {"type": "object", "properties": {"task_id": {"type": "string"}}, "required": ["task_id"]}},
{"name": "complete_task", "description": "Complete the task claimed by this agent.",
"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,
"edit_file": run_edit,
"glob": run_glob,
"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,
}
# -- From s04: hooks and permission checks --
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
def register_hook(event: str, callback):
HOOKS[event].append(callback)
def trigger_hooks(event: str, *args):
for callback in HOOKS[event]:
result = callback(*args)
if result is not None:
return result
return None
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def permission_hook(block):
if block.name == "bash":
command = block.input.get("command", "")
for pattern in DENY_LIST:
if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
return "Permission denied by deny list"
if any(keyword in command for keyword in DESTRUCTIVE):
print("\n\033[33m[permission] Potentially destructive command\033[0m")
print(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
if block.name in ("read_file", "write_file", "edit_file"):
path = block.input.get("path", "")
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):
print("\n\033[33m[permission] Access outside workspace\033[0m")
print(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
return None
def log_hook(block):
preview = str(list(block.input.values())[:2])[:60]
print(f"\033[90m[HOOK] {block.name}({preview})\033[0m")
return None
def large_output_hook(block, output):
if len(str(output)) > 100000:
print(
f"\033[33m[HOOK] Large output from {block.name}: "
f"{len(str(output))} chars\033[0m"
)
return None
def context_hook(query: str):
print(f"\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\033[0m")
return None
def summary_hook(messages: list):
tool_count = sum(
1
for message in messages
for block in (
message.get("content")
if isinstance(message.get("content"), list)
else []
)
if isinstance(block, dict) and block.get("type") == "tool_result"
)
print(f"\033[90m[HOOK] Stop: session used {tool_count} tool calls\033[0m")
return None
register_hook("UserPromptSubmit", context_hook)
register_hook("PreToolUse", permission_hook)
register_hook("PreToolUse", log_hook)
register_hook("PostToolUse", large_output_hook)
register_hook("Stop", summary_hook)
def execute_tool(block) -> str:
blocked = trigger_hooks("PreToolUse", block)
if blocked:
return str(blocked)
handler = TOOL_HANDLERS.get(block.name)
try:
output = handler(**block.input) if handler else f"Unknown: {block.name}"
except Exception as error:
output = f"Error: {error}"
trigger_hooks("PostToolUse", block, output)
return str(output)
# -- Agent loop --
def agent_loop(messages: list):
while True:
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=messages,
tools=TOOLS,
max_tokens=8000,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
force = trigger_hooks("Stop", messages)
if force:
messages.append({"role": "user", "content": force})
continue
return
results = []
for block in response.content:
if block.type != "tool_use":
continue
output = execute_tool(block)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
messages.append({"role": "user", "content": results})
if __name__ == "__main__":
print("s10: Task System - dependencies and task state")
print("Enter a question, press Enter to send. Type q to quit.\n")
history = []
while True:
try:
query = input("\033[36ms10 >> \033[0m")
except (EOFError, KeyboardInterrupt):
break
if query.strip().lower() in ("q", "exit", ""):
break
trigger_hooks("UserPromptSubmit", query)
history.append({"role": "user", "content": query})
agent_loop(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">Base Loop</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">s10 New</text>
<!-- ===== base 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">system prompt</text>
<text x="191" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">fixed instructions</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 call</text>
<text x="322" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">model request</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">Hooks + TOOL_HANDLERS</text>
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write · edit · glob</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">ID: task_ + 8 random hex characters</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">complete_task checks status and 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">Base loop: model call + Permission/Hooks + tool dispatch + tool_result</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">s10 New: Task dataclass + 5 tools + .tasks/ persistence + blockedBy dependency graph</text>
</svg>

After

Width:  |  Height:  |  Size: 6.7 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">基本ループ</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">s10 新規</text>
<!-- ===== 基本ループ(コンパクト) ===== -->
<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">system prompt</text>
<text x="191" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">fixed instructions</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 call</text>
<text x="322" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">model request</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">Hooks + TOOL_HANDLERS</text>
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write · edit · glob</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: task_ + 8 桁のランダムな 16 進文字</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">complete_task は status と 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">基本ループ:モデル呼び出し + Permission/Hooks + ツール分配 + tool_result</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">s10 新規Task dataclass + 5 ツール + .tasks/ 永続化 + blockedBy 依存グラフ</text>
</svg>

After

Width:  |  Height:  |  Size: 6.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 个任务工具 + .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">基础循环</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">s10 新增</text>
<!-- ===== base 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">system prompt</text>
<text x="191" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">fixed instructions</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 call</text>
<text x="322" y="122" fill="#94a3b8" font-size="8" text-anchor="middle">model request</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">Hooks + TOOL_HANDLERS</text>
<text x="408" y="114" fill="#2563eb" font-size="9">bash · read · write · edit · glob</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: task_ + 8 位随机十六进制字符</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">complete_task 检查 status 和 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">基础循环:模型调用 + Permission/Hooks + 工具分发 + tool_result</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">s10 新增Task dataclass + 5 个工具 + .tasks/ 持久化 + blockedBy 依赖图</text>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB