Merge pull request #533 from Bill-Billion/fix/task-dependency-two-phase
Some checks failed
CI / build (push) Has been cancelled
Test / python-smoke (push) Has been cancelled
Test / web-build (push) Has been cancelled

fix: build task dependencies in two phases
This commit is contained in:
Yang Haoran 2026-08-19 01:42:28 +08:00 committed by GitHub
commit f9e8b280f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1102 additions and 443 deletions

View file

@ -26,7 +26,7 @@ TodoWrite は、こうした依存関係や担当を記録しない。「API を
![Task System Overview](images/task-system-overview.ja.svg)
コードは S04 の 5 つの基本ツール、Permission、Hooks、共通の `execute_tool` を保ち、そこへ 5 つのタスクツール、`.tasks/` ディレクトリへの永続化、`blockedBy` の依存チェックを追加する。
コードは S04 の 5 つの基本ツール、Permission、Hooks、共通の `execute_tool` を保ち、そこへ 6 つのタスクツール、`.tasks/` ディレクトリへの永続化、`blockedBy` の依存チェックを追加する。
TodoWrite vs Task System
@ -69,12 +69,22 @@ ID は `task_` と 8 桁のランダムな 16 進文字で生成する。ファ
### create_task: タスク作成
```python
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
return TASKS.create(subject, description, blockedBy)
def create_task(subject: str, description: str = "") -> Task:
return TASKS.create(subject, description)
```
`TaskStore.create` は subject と依存 ID を確認し、`.tasks/{id}.json` に書き込む。`blockedBy` で依存を宣言し、例えば「API を書く」タスクはデータベースタスクの ID を参照できる。
`TaskStore.create` は subject を確認し、ランダム ID を割り当てて `.tasks/{id}.json` に書き込む。新しいタスクの `blockedBy` は常に空で、ツール結果が実行時に生成された ID をモデルへ返す。
### update_task: 返された ID で依存を追加
```python
def update_task(task_id: str, addBlockedBy: list[str]) -> Task:
return TASKS.update_dependencies(task_id, addBlockedBy)
```
タスクグラフは 2 段階で構築する。まず全ノードを作成し、その後 `create_task` が返した ID を使って `update_task` で辺を追加する。モデルが 1 回の応答で複数のツール呼び出しを出す場合、同じ階層の呼び出しはツール結果が返る前にすべて確定するため、ある `create_task` は別の呼び出しで生成されたばかりの ID を利用できない。
`update_task` は変更全体を検証してから保存する。対象と依存タスクは存在し、対象は pending かつ未所有でなければならず、自己依存や循環も禁止する。既存の辺を再度追加しても重複しない。
### can_start: 依存チェック
@ -159,11 +169,16 @@ pending ──claim──→ in_progress ──complete──→ completed
### 組み合わせて実行
```python
# 依存関係のあるタスクを作成
# 第 1 段階:全ノードを作成して実行時 ID を受け取る
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])
endpoints = create_task("create API endpoints")
tests = create_task("write tests")
docs = create_task("write docs")
# 第 2 段階:返された ID で依存の辺を追加する
update_task(endpoints.id, addBlockedBy=[schema.id])
update_task(tests.id, addBlockedBy=[endpoints.id])
update_task(docs.id, addBlockedBy=[schema.id])
# Agent が最初に実行可能なタスクを引き受ける
claim_task(schema.id) # ✓ Claimed依存なし
@ -179,7 +194,7 @@ claim_task(tests.id) # ✓ Claimedendpoints 完了済み)
complete_task(tests.id) # ✓ Completed
```
`create_task` が JSON ファイルを書き込み、`claim_task` / `complete_task` がファイルを更新。セッションをまたいでも `.tasks/` ディレクトリが残り、Agent はファイルを読んで進捗を復旧。
`create_task` が JSON ファイルを書き込み、`update_task``claim_task``complete_task` がファイルを更新する。セッションをまたいでも `.tasks/` ディレクトリが残り、Agent はファイルを読んで進捗を復旧できる
---
@ -208,4 +223,4 @@ python s10_task_system/code.py
s11 Background Tasks → 遅い操作をバックグラウンドで実行する。Agent は他のタスクの処理を続け、バックグラウンド処理の完了後に通知を受け取る。
<!-- translation-sync: zh@v4, en@v4, ja@v4 -->
<!-- translation-sync: zh@v5, en@v5, ja@v5 -->

View file

@ -26,7 +26,7 @@ This chapter adds a Task System. Each task has its own ID and status; `blockedBy
![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.
The code keeps S04's five base tools, Permission, Hooks, and shared `execute_tool`, then adds 6 task tools, persistence in the `.tasks/` directory, and `blockedBy` dependency checks.
TodoWrite vs Task System:
@ -69,12 +69,22 @@ IDs use the `task_` prefix followed by 8 random hexadecimal characters. Files ar
### create_task: Create Tasks
```python
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
return TASKS.create(subject, description, blockedBy)
def create_task(subject: str, description: str = "") -> Task:
return TASKS.create(subject, description)
```
`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.
`TaskStore.create` checks the subject, allocates a random ID, and writes `.tasks/{id}.json`. A new task always starts with an empty `blockedBy` list. The tool result returns the runtime-generated ID to the model.
### update_task: Add Dependencies with Returned IDs
```python
def update_task(task_id: str, addBlockedBy: list[str]) -> Task:
return TASKS.update_dependencies(task_id, addBlockedBy)
```
Task graph construction uses two phases: create every node first, then call `update_task` with the IDs returned by `create_task` to add edges. This matters when the model emits several tool calls in one response: sibling calls are formed before any tool result exists, so one `create_task` call cannot consume another call's newly generated ID.
`update_task` validates the entire change before saving it. The target and dependencies must exist, the target must still be pending and unowned, and the new edges must not introduce self-dependencies or cycles. Repeating an existing edge is safe and does not duplicate it.
### can_start: Dependency Check
@ -159,11 +169,16 @@ Here `claim` / `complete` are actions, while `pending` / `in_progress` / `comple
### Putting It Together
```python
# Create tasks with dependencies
# Phase 1: create every node and receive its runtime ID
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])
endpoints = create_task("create API endpoints")
tests = create_task("write tests")
docs = create_task("write docs")
# Phase 2: add edges using those returned IDs
update_task(endpoints.id, addBlockedBy=[schema.id])
update_task(tests.id, addBlockedBy=[endpoints.id])
update_task(docs.id, addBlockedBy=[schema.id])
# Agent claims the first available task
claim_task(schema.id) # ✓ Claimed (no dependencies)
@ -179,7 +194,7 @@ 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.
Each `create_task` writes a JSON file; `update_task`, `claim_task`, and `complete_task` update it. Across sessions, the `.tasks/` directory persists — the agent reads the files to recover progress.
---
@ -208,4 +223,4 @@ The task graph is in place, but full test suites, dependency installation, and d
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 -->
<!-- translation-sync: zh@v5, en@v5, ja@v5 -->

View file

@ -26,7 +26,7 @@ TodoWrite 没有记录这些依赖和分工。它可以显示“编写 API”仍
![Task System Overview](images/task-system-overview.svg)
代码保留 S04 的五个基础工具、Permission、Hooks 和统一 `execute_tool`,再加入 5 个任务工具、`.tasks/` 目录持久化和 `blockedBy` 依赖检查。
代码保留 S04 的五个基础工具、Permission、Hooks 和统一 `execute_tool`,再加入 6 个任务工具、`.tasks/` 目录持久化和 `blockedBy` 依赖检查。
TodoWrite vs Task System
@ -69,12 +69,22 @@ ID 使用 `task_` 加 8 位随机十六进制字符生成。创建文件时使
### create_task: 创建任务
```python
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
return TASKS.create(subject, description, blockedBy)
def create_task(subject: str, description: str = "") -> Task:
return TASKS.create(subject, description)
```
`TaskStore.create` 检查 subject 和依赖 ID再把任务写入 `.tasks/{id}.json``blockedBy` 声明依赖,比如“写 API”的 `blockedBy` 可以指向数据库任务的 ID。
`TaskStore.create` 检查 subject分配随机 ID再把任务写入 `.tasks/{id}.json`。新任务的 `blockedBy` 固定为空,工具结果会把运行时生成的 ID 返回给模型。
### update_task: 使用返回的 ID 添加依赖
```python
def update_task(task_id: str, addBlockedBy: list[str]) -> Task:
return TASKS.update_dependencies(task_id, addBlockedBy)
```
任务图采用两阶段构建:先创建所有节点,再使用 `create_task` 返回的 ID 调用 `update_task` 添加边。模型可能在一条回复里同时发出多个工具调用,而这些同级调用在任何工具结果产生前就已经确定,因此某个 `create_task` 无法直接使用另一个调用刚生成的 ID。
`update_task` 会先校验整次修改,再统一保存。目标任务和依赖必须存在,目标必须仍为 pending 且无人认领,并且不能形成自依赖或环。重复添加已有依赖是安全的,不会产生重复边。
### can_start: 依赖检查
@ -159,11 +169,16 @@ pending ──claim──→ in_progress ──complete──→ completed
### 合起来跑
```python
# 创建有依赖的任务
# 第一阶段:创建所有节点并取得运行时 ID
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])
endpoints = create_task("create API endpoints")
tests = create_task("write tests")
docs = create_task("write docs")
# 第二阶段:使用返回的 ID 建立依赖边
update_task(endpoints.id, addBlockedBy=[schema.id])
update_task(tests.id, addBlockedBy=[endpoints.id])
update_task(docs.id, addBlockedBy=[schema.id])
# Agent 认领第一个可做的任务
claim_task(schema.id) # ✓ Claimed (无依赖)
@ -179,7 +194,7 @@ claim_task(tests.id) # ✓ Claimed (endpoints 已完成)
complete_task(tests.id) # ✓ Completed
```
每个 `create_task` 写一个 JSON 文件,每个 `claim_task` / `complete_task` 更新文件。跨会话时,`.tasks/` 目录还在Agent 读文件就能恢复进度。
每个 `create_task` 写一个 JSON 文件,`update_task``claim_task` `complete_task` 更新文件。跨会话时,`.tasks/` 目录还在Agent 读文件就能恢复进度。
---
@ -208,4 +223,4 @@ python s10_task_system/code.py
s11 Background Tasks → 把慢操作放到后台。Agent 可以继续处理其他任务,后台执行完成后再接收通知。
<!-- translation-sync: zh@v4, en@v4, ja@v4 -->
<!-- translation-sync: zh@v5, en@v5, ja@v5 -->

View file

@ -53,7 +53,9 @@ MODEL = os.environ["MODEL_ID"]
SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
"Use task tools to track dependencies and progress."
"Use task tools to track dependencies and progress. Create all task nodes "
"first. After create_task returns runtime-generated IDs, use update_task "
"with those exact IDs to add dependencies."
)
@ -97,17 +99,11 @@ class TaskStore:
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:
def create(self, subject: str, description: str = "") -> 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(
@ -116,7 +112,7 @@ class TaskStore:
description=description,
status="pending",
owner=None,
blockedBy=dependencies,
blockedBy=[],
)
try:
with self._path(task.id, create_root=True).open(
@ -128,6 +124,52 @@ class TaskStore:
continue
raise RuntimeError("Could not allocate a unique task ID")
def _depends_on(self, task_id: str, target_id: str) -> bool:
"""Return whether task_id transitively depends on target_id."""
pending = [task_id]
visited = set()
while pending:
current = pending.pop()
if current == target_id:
return True
if current in visited:
continue
visited.add(current)
pending.extend(self.load(current).blockedBy)
return False
def update_dependencies(self, task_id: str,
add_blocked_by: list[str]) -> Task:
if not isinstance(add_blocked_by, list):
raise ValueError("addBlockedBy must be a list of task IDs")
task = self.load(task_id)
if task.status != "pending" or task.owner is not None:
raise ValueError(
f"Task {task_id} dependencies can only be updated while "
"pending and unowned"
)
dependencies = list(dict.fromkeys(add_blocked_by))
for dependency in dependencies:
if dependency == task_id:
raise ValueError("Task cannot depend on itself")
if not self.exists(dependency):
raise ValueError(f"Dependency not found: {dependency}")
if dependency not in task.blockedBy and self._depends_on(
dependency, task_id
):
raise ValueError(
f"Dependency cycle detected: {task_id} -> {dependency}"
)
task.blockedBy.extend(
dependency for dependency in dependencies
if dependency not in task.blockedBy
)
self.save(task)
return task
def save(self, task: Task) -> None:
self._path(task.id, create_root=True).write_text(
json.dumps(asdict(task), indent=2),
@ -154,9 +196,12 @@ class TaskStore:
TASKS = TaskStore(TASKS_DIR)
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
return TASKS.create(subject, description, blockedBy)
def create_task(subject: str, description: str = "") -> Task:
return TASKS.create(subject, description)
def update_task(task_id: str, addBlockedBy: list[str]) -> Task:
return TASKS.update_dependencies(task_id, addBlockedBy)
def load_task(task_id: str) -> Task:
@ -290,15 +335,17 @@ def run_glob(pattern: str) -> str:
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_create_task(subject: str, description: str = "") -> str:
task = create_task(subject, description)
print(f" [create] {task.subject}")
return f"Created {task.id}: {task.subject}"
def run_update_task(task_id: str, addBlockedBy: list[str]) -> str:
task = update_task(task_id, addBlockedBy)
dependencies = ", ".join(task.blockedBy) or "(none)"
print(f" [update] {task.subject} blockedBy: {dependencies}")
return f"Updated {task.id} blockedBy: {dependencies}"
def run_list_tasks() -> str:
@ -347,8 +394,10 @@ TOOLS = [
"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": "create_task", "description": "Create a task and return its runtime-generated ID.",
"input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}}, "required": ["subject"], "additionalProperties": False}},
{"name": "update_task", "description": "Add dependencies using IDs returned by create_task.",
"input_schema": {"type": "object", "properties": {"task_id": {"type": "string", "pattern": "^task_[0-9a-f]{8}$"}, "addBlockedBy": {"type": "array", "items": {"type": "string", "pattern": "^task_[0-9a-f]{8}$"}, "minItems": 1}}, "required": ["task_id", "addBlockedBy"], "additionalProperties": False}},
{"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.",
@ -366,6 +415,7 @@ TOOL_HANDLERS = {
"edit_file": run_edit,
"glob": run_glob,
"create_task": run_create_task,
"update_task": run_update_task,
"list_tasks": run_list_tasks,
"get_task": run_get_task,
"claim_task": run_claim_task,

View file

@ -16,7 +16,7 @@
<!-- 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>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 6 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"/>
@ -46,7 +46,7 @@
<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="128" fill="#0d9488" font-size="9" font-weight="600">create_task · update_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 -->
@ -61,13 +61,13 @@
<!-- 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>
<text x="300" y="178" fill="#0d9488" font-size="9">create → ID / update edges / 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="222" fill="#0d9488" font-size="9">create_task → runtime ID; update_task → blockedBy</text>
<text x="408" y="238" fill="#0d9488" font-size="9">can_start + claim: all dependencies completed?</text>
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + unblock downstream</text>
<!-- ===== State machine ===== -->
@ -90,5 +90,5 @@
<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>
<text x="80" y="394" fill="#475569" font-size="10">s10 New: Task dataclass + 6 tools + .tasks/ persistence + blockedBy dependency graph</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Before After
Before After

View file

@ -16,7 +16,7 @@
<!-- タイトル -->
<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>
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Task System — 6 つのタスクツール + .tasks/ 永続化 + blockedBy 依存</text>
<!-- 凡例 -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
@ -46,7 +46,7 @@
<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="128" fill="#0d9488" font-size="9" font-weight="600">create_task · update_task · list_tasks</text>
<text x="408" y="140" fill="#0d9488" font-size="9" font-weight="600">get_task · claim_task · complete_task</text>
<!-- ループバック -->
@ -61,13 +61,13 @@
<!-- 矢印: 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>
<text x="300" y="178" fill="#0d9488" font-size="9">create → ID / update edges / 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="222" fill="#0d9488" font-size="9">create_task → 実行時 IDupdate_task → blockedBy</text>
<text x="408" y="238" fill="#0d9488" font-size="9">can_start + claim依存がすべて completed?</text>
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + 下流をアンロック</text>
<!-- ===== 状態マシン ===== -->
@ -90,5 +90,5 @@
<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>
<text x="80" y="394" fill="#475569" font-size="10">s10 新規Task dataclass + 6 ツール + .tasks/ 永続化 + blockedBy 依存グラフ</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before After
Before After

View file

@ -16,7 +16,7 @@
<!-- 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>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 6 个任务工具 + .tasks/ 持久化 + blockedBy 依赖</text>
<!-- Legend -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
@ -46,7 +46,7 @@
<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="128" fill="#0d9488" font-size="9" font-weight="600">create_task · update_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 -->
@ -61,13 +61,13 @@
<!-- 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>
<text x="300" y="178" fill="#0d9488" font-size="9">create → ID / update edges / 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="222" fill="#0d9488" font-size="9">create_task → 运行时 IDupdate_task → blockedBy</text>
<text x="408" y="238" fill="#0d9488" font-size="9">can_start + claim依赖全部 completed?</text>
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + 解锁下游</text>
<!-- ===== State machine ===== -->
@ -90,5 +90,5 @@
<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>
<text x="80" y="394" fill="#475569" font-size="10">s10 新增Task dataclass + 6 个工具 + .tasks/ 持久化 + blockedBy 依赖图</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Before After
Before After

View file

@ -47,6 +47,8 @@ s13 は s10 の基本ツール、Hooks、Permission、Task System を再利用
- **任意の worktree** は、必要なタスクだけを別の作業ディレクトリへ紐付ける。紐付けのないタスクは通常のリポジトリディレクトリを使う。
- **型付きプロトコルと計画ゲート** は shutdown と承認状態を明示し、必要な計画が承認されるまで変更系ツールを止める。
タスクグラフの作成は s10 の 2 段階契約を維持する。Lead はまず全ノードに `create_task` を呼び、返された実行時 ID で `update_task(addBlockedBy=...)` を実行してから ready task を割り当てる。`update_task` を使えるのは Lead だけであり、チームメイトは一覧・Claim・完了はできるが、チーム実行中にグラフ構造を変更できない。
s11 の background task と s12 の scheduled task は本章へ持ち込まない。どちらも teammate communication、task claim、plan approval には必要ない。
これらはすべて Team Harness レイヤーの一部である。タスク発見のために別の Agent Loop は要らず、worktree が別種の Agent を作るわけでもない。
@ -447,4 +449,4 @@ Lead と teammate が呼び出せるのは、`code.py` に直接定義したツ
s14 MCP Tools → 共通の発見・呼び出しプロトコルで実行時に外部サービスへ接続し、そのツールを tool pool に追加する。
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->
<!-- translation-sync: zh@v12, en@v12, ja@v12 -->

View file

@ -47,6 +47,8 @@ s13 reuses s10's base tools, hooks, permission checks, and Task System, then add
- **Optional worktrees** bind a task to another working directory when the work needs it. Unbound tasks use the normal repository directory.
- **Typed protocols and a plan gate** make shutdown and approval state explicit and block mutating tools until a required plan is approved.
Task graph authoring keeps s10's two-phase contract. The Lead first calls `create_task` for every node, then uses the returned runtime IDs with `update_task(addBlockedBy=...)` before assigning ready work. Only the Lead receives `update_task`; teammates can list, claim, and complete tasks but cannot rewrite graph structure while the team is running.
s11 background tasks and s12 scheduled tasks are not carried into this chapter. Neither mechanism is required for teammate communication, task claiming, or plan approval.
These are all parts of the Team harness layer. Teammates do not need a separate loop for task discovery, and a worktree does not create a new kind of agent.
@ -448,4 +450,4 @@ The Lead and its teammates can only call tools defined directly in `code.py`. Co
s14 MCP Tools → Connect external services at runtime through one discovery and invocation protocol, then add their tools to the tool pool.
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->
<!-- translation-sync: zh@v12, en@v12, ja@v12 -->

View file

@ -46,6 +46,8 @@ s13 复用 s10 的基础工具、Hooks、Permission 和 Task System并增加
- **可选 worktree** 在需要时把任务绑定到另一个工作目录;未绑定任务仍使用仓库目录。
- **类型化协议和计划闸门** 显式记录关机与审批状态,并在计划获批前阻止修改型工具。
任务图继续采用 s10 的两阶段契约。Lead 先为所有节点调用 `create_task`,再使用返回的运行时 ID 调用 `update_task(addBlockedBy=...)`,最后才分配 ready task。只有 Lead 能使用 `update_task`;队友只能列举、认领和完成任务,团队运行期间不能改写任务图结构。
s11 的后台任务和 s12 的定时任务没有被带入本章。它们不参与队友通信、任务认领或计划审批。
这些机制都属于 Team 这一层。任务发现不需要另一套 Agent Loopworktree 也不会产生另一种 Agent。
@ -443,4 +445,4 @@ Lead 和队友目前只能调用直接写在 `code.py` 里的工具。接入 Jir
s14 MCP Tools → 通过统一的发现与调用协议,在运行时连接外部服务并把它们的工具加入工具池。
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->
<!-- translation-sync: zh@v12, en@v12, ja@v12 -->

View file

@ -130,16 +130,11 @@ def _task_path(task_id: str) -> Path:
return path
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
def create_task(subject: str, description: str = "") -> Task:
subject = subject.strip()
if not subject:
raise ValueError("Task subject cannot be empty")
dependencies = list(dict.fromkeys(blockedBy or []))
with task_store_lock():
for dependency in dependencies:
if not _task_path(dependency).is_file():
raise ValueError(f"Dependency not found: {dependency}")
for _ in range(100):
task = Task(
id=f"task_{secrets.token_hex(4)}",
@ -147,7 +142,7 @@ def create_task(subject: str, description: str = "",
description=description,
status="pending",
owner=None,
blockedBy=dependencies,
blockedBy=[],
)
try:
with _task_path(task.id).open("x", encoding="utf-8") as handle:
@ -158,6 +153,55 @@ def create_task(subject: str, description: str = "",
raise RuntimeError("Could not allocate a unique task ID")
def _task_depends_on(task_id: str, target_id: str) -> bool:
"""Return whether task_id transitively depends on target_id."""
pending = [task_id]
visited = set()
while pending:
current = pending.pop()
if current == target_id:
return True
if current in visited:
continue
visited.add(current)
pending.extend(load_task(current).blockedBy)
return False
def update_task(task_id: str, addBlockedBy: list[str]) -> Task:
"""Add dependency edges after create_task has returned real task IDs."""
if not isinstance(addBlockedBy, list):
raise ValueError("addBlockedBy must be a list of task IDs")
with task_store_lock():
task = load_task(task_id)
if task.status != "pending" or task.owner is not None:
raise ValueError(
f"Task {task_id} dependencies can only be updated while "
"pending and unowned"
)
dependencies = list(dict.fromkeys(addBlockedBy))
for dependency in dependencies:
if dependency == task_id:
raise ValueError("Task cannot depend on itself")
if not _task_path(dependency).is_file():
raise ValueError(f"Dependency not found: {dependency}")
if dependency not in task.blockedBy and _task_depends_on(
dependency, task_id
):
raise ValueError(
f"Dependency cycle detected: {task_id} -> {dependency}"
)
task.blockedBy.extend(
dependency for dependency in dependencies
if dependency not in task.blockedBy
)
save_task(task)
return task
def save_task(task: Task):
with task_store_lock():
path = _task_path(task.id)
@ -579,9 +623,15 @@ def remove_worktree(name: str, discard_changes: bool = False) -> str:
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file, edit_file, glob, "
"get_task, create_task, list_tasks, claim_task, complete_task, "
"create_task, update_task, list_tasks, get_task, claim_task, "
"complete_task, "
"spawn_teammate, list_teammates, send_message, request_shutdown, "
"request_plan, review_plan, create_worktree.",
"tasks": (
"Create all task nodes first. Only after create_task returns "
"runtime-generated IDs, use update_task with those exact IDs to add "
"dependencies. Only the Lead changes task dependencies."
),
"teams": (
"When parallel work would help, first propose a small team with clear "
"responsibilities and wait for the user's confirmation. Do not call "
@ -716,12 +766,22 @@ def run_agent_glob(pattern: str) -> str:
# -- 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_create_task(subject: str, description: str = "") -> str:
task = create_task(subject, description)
print(f" \033[34m[create] {task.subject}\033[0m")
return f"Created {task.id}: {task.subject}"
def run_update_task(task_id: str, addBlockedBy: list[str]) -> str:
try:
task = update_task(task_id, addBlockedBy)
except ValueError as exc:
return f"Error: {exc}"
except FileNotFoundError:
return f"Error: Task {task_id} not found"
dependencies = ", ".join(task.blockedBy) or "(none)"
print(f" \033[34m[update] {task.subject} blockedBy: {dependencies}\033[0m")
return f"Updated {task.id} blockedBy: {dependencies}"
def run_list_tasks() -> str:
@ -1467,14 +1527,26 @@ BASE_TOOLS = [
TASK_TOOLS = [
{"name": "create_task",
"description": "Create a task with optional dependencies.",
"description": "Create a task and return its runtime-generated ID.",
"input_schema": {"type": "object",
"properties": {
"subject": {"type": "string"},
"description": {"type": "string"},
"blockedBy": {"type": "array",
"items": {"type": "string"}}},
"required": ["subject"]}},
"description": {"type": "string"}},
"required": ["subject"],
"additionalProperties": False}},
{"name": "update_task",
"description": "Add dependencies using IDs returned by create_task.",
"input_schema": {"type": "object",
"properties": {
"task_id": {"type": "string",
"pattern": "^task_[0-9a-f]{8}$"},
"addBlockedBy": {
"type": "array",
"items": {"type": "string",
"pattern": "^task_[0-9a-f]{8}$"},
"minItems": 1}},
"required": ["task_id", "addBlockedBy"],
"additionalProperties": False}},
{"name": "list_tasks", "description": "List shared tasks.",
"input_schema": {"type": "object", "properties": {}}},
{"name": "get_task", "description": "Get one task by ID.",
@ -1569,6 +1641,7 @@ TOOL_HANDLERS = {
"edit_file": run_agent_edit,
"glob": run_agent_glob,
"create_task": run_create_task,
"update_task": run_update_task,
"list_tasks": run_list_tasks,
"get_task": run_get_task,
"claim_task": run_claim_task,

View file

@ -79,12 +79,12 @@ loop 自体は同じ構造のままだ。model を呼び、response に `tool_us
### Tools と Dispatch
built-in tool pool には 25 個の tool がある:
built-in tool pool には 26 個の tool がある:
```text
bash, read_file, write_file, edit_file, glob
todo_write, task, load_skill, compact
create_task, list_tasks, get_task, claim_task, complete_task
create_task, update_task, list_tasks, get_task, claim_task, complete_task
schedule_cron, list_crons, cancel_cron
spawn_teammate, list_teammates, send_message
request_shutdown, request_plan, review_plan
@ -127,6 +127,8 @@ S15 には 2 層の plan がある:
目的は近いが実装は別である。`todo_write` は現在のセッションのチェックリスト全体を置き換え、task record は安定 ID と個別のライフサイクル更新を持つ。次節の独立した `task` ツールは「隔離 subagent を一度派遣する」意味であり、Task System ではない。
統合 host でもタスクグラフは 2 段階で構築する。Lead はまず全タスクノードを作成し、`create_task` が返した実行時 ID で `update_task` を呼ぶ。チームメイトが使えるのは一覧・Claim・完了だけなので、依存構造は仕事を配る前に Lead が確定する。
### Subagent と Team
S15 には 2 種類の delegation がある:
@ -238,4 +240,4 @@ python s15_integrated_harness/code.py
[s16 Workflow Runtime](../s16_workflow_runtime/) は、この host に `Workflow` tool を追加する。Workflow は固定された orchestration path を code に置き、進行状況を記録して同じ run を再開できるようにする。
<!-- translation-sync: zh@v13, en@v13, ja@v13 -->
<!-- translation-sync: zh@v14, en@v14, ja@v14 -->

View file

@ -79,12 +79,12 @@ The loop keeps the same structure: call the model, check whether the response co
### Tools and Dispatch
The built-in tool pool contains 25 tools:
The built-in tool pool contains 26 tools:
```text
bash, read_file, write_file, edit_file, glob
todo_write, task, load_skill, compact
create_task, list_tasks, get_task, claim_task, complete_task
create_task, update_task, list_tasks, get_task, claim_task, complete_task
schedule_cron, list_crons, cancel_cron
spawn_teammate, list_teammates, send_message
request_shutdown, request_plan, review_plan
@ -127,6 +127,8 @@ The first keeps a single agent from drifting. The second supports team coordinat
They share an intent, not an implementation: `todo_write` replaces one session checklist, while task records have stable IDs and individual lifecycle updates. The separate `task` tool below means "dispatch one isolated subagent"; it is not the Task System.
Task graph construction remains two-phase in the integrated host: the Lead creates all task nodes first, then calls `update_task` with the runtime IDs returned by `create_task`. Teammates receive only list, claim, and complete operations, so dependency structure is fixed by the Lead before work is distributed.
### Subagents and Teams
S15 has two kinds of delegation:
@ -238,4 +240,4 @@ Watch for:
[s16 Workflow Runtime](../s16_workflow_runtime/) adds a `Workflow` tool to this host. A workflow keeps a fixed orchestration path in code and records progress so the same run can resume.
<!-- translation-sync: zh@v13, en@v13, ja@v13 -->
<!-- translation-sync: zh@v14, en@v14, ja@v14 -->

View file

@ -79,12 +79,12 @@ S15 不再引入新机制,而是把前面各章的组件集成到同一个 har
### 工具与分发
内置工具池包含 25 个工具:
内置工具池包含 26 个工具:
```text
bash, read_file, write_file, edit_file, glob
todo_write, task, load_skill, compact
create_task, list_tasks, get_task, claim_task, complete_task
create_task, update_task, list_tasks, get_task, claim_task, complete_task
schedule_cron, list_crons, cancel_cron
spawn_teammate, list_teammates, send_message
request_shutdown, request_plan, review_plan
@ -127,6 +127,8 @@ S15 同时保留两层计划:
两者目标相近,但实现不同:`todo_write` 整表替换当前会话清单task record 则有稳定 ID 和单条生命周期更新。下面单独出现的 `task` 工具表示“一次性派发隔离 subagent”不是 Task System。
集成宿主中的任务图仍采用两阶段构建Lead 先创建所有任务节点,再使用 `create_task` 返回的运行时 ID 调用 `update_task`。队友只能列举、认领和完成任务,因此依赖结构由 Lead 在分发工作前确定。
### 子 agent 与团队
S15 有两种 delegation
@ -238,4 +240,4 @@ python s15_integrated_harness/code.py
[s16 Workflow Runtime](../s16_workflow_runtime/) 会在这个 host 中加入 `Workflow` 工具。Workflow 把固定的编排路径写在代码中,并记录运行进度,使同一次运行可以继续执行。
<!-- translation-sync: zh@v13, en@v13, ja@v13 -->
<!-- translation-sync: zh@v14, en@v14, ja@v14 -->

View file

@ -205,16 +205,11 @@ def _task_path(task_id: str) -> Path:
return path
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
def create_task(subject: str, description: str = "") -> Task:
subject = subject.strip()
if not subject:
raise ValueError("Task subject cannot be empty")
dependencies = list(dict.fromkeys(blockedBy or []))
with task_store_lock():
for dependency in dependencies:
if not _task_path(dependency).is_file():
raise ValueError(f"Dependency not found: {dependency}")
for _ in range(100):
task = Task(
id=f"task_{secrets.token_hex(4)}",
@ -222,7 +217,7 @@ def create_task(subject: str, description: str = "",
description=description,
status="pending",
owner=None,
blockedBy=dependencies,
blockedBy=[],
)
try:
with _task_path(task.id).open("x", encoding="utf-8") as handle:
@ -233,6 +228,55 @@ def create_task(subject: str, description: str = "",
raise RuntimeError("Could not allocate a unique task ID")
def _task_depends_on(task_id: str, target_id: str) -> bool:
"""Return whether task_id transitively depends on target_id."""
pending = [task_id]
visited = set()
while pending:
current = pending.pop()
if current == target_id:
return True
if current in visited:
continue
visited.add(current)
pending.extend(load_task(current).blockedBy)
return False
def update_task(task_id: str, addBlockedBy: list[str]) -> Task:
"""Add dependency edges after create_task has returned real task IDs."""
if not isinstance(addBlockedBy, list):
raise ValueError("addBlockedBy must be a list of task IDs")
with task_store_lock():
task = load_task(task_id)
if task.status != "pending" or task.owner is not None:
raise ValueError(
f"Task {task_id} dependencies can only be updated while "
"pending and unowned"
)
dependencies = list(dict.fromkeys(addBlockedBy))
for dependency in dependencies:
if dependency == task_id:
raise ValueError("Task cannot depend on itself")
if not _task_path(dependency).is_file():
raise ValueError(f"Dependency not found: {dependency}")
if dependency not in task.blockedBy and _task_depends_on(
dependency, task_id
):
raise ValueError(
f"Dependency cycle detected: {task_id} -> {dependency}"
)
task.blockedBy.extend(
dependency for dependency in dependencies
if dependency not in task.blockedBy
)
save_task(task)
return task
def save_task(task: Task):
with task_store_lock():
path = _task_path(task.id)
@ -737,12 +781,18 @@ PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file, edit_file, glob, "
"todo_write, task, load_skill, compact, "
"create_task, list_tasks, get_task, claim_task, complete_task, "
"create_task, update_task, list_tasks, get_task, claim_task, "
"complete_task, "
"schedule_cron, list_crons, cancel_cron, "
"spawn_teammate, list_teammates, send_message, "
"request_shutdown, request_plan, review_plan, "
"create_worktree, "
"connect_mcp. MCP tools are prefixed mcp__{server}__{tool}.",
"tasks": (
"Create all task nodes first. Only after create_task returns "
"runtime-generated IDs, use update_task with those exact IDs to add "
"dependencies. Only the Lead changes task dependencies."
),
"teams": (
"When parallel work would help, first propose a small team with clear "
"responsibilities and wait for the user's confirmation. Do not call "
@ -777,6 +827,7 @@ def assemble_system_prompt(context: dict) -> str:
# memory, skill catalog, MCP state, and active teammates become visible.
sections = [PROMPT_SECTIONS["identity"],
PROMPT_SECTIONS["tools"],
PROMPT_SECTIONS["tasks"],
PROMPT_SECTIONS["teams"],
PROMPT_SECTIONS["workspace"],
PROMPT_SECTIONS["memory"],
@ -2620,12 +2671,22 @@ def run_create_worktree(name: str, task_id: str) -> str:
# -- Basic Tool Handlers --
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_create_task(subject: str, description: str = "") -> str:
task = create_task(subject, description)
print(f" \033[34m[create] {task.subject}\033[0m")
return f"Created {task.id}: {task.subject}"
def run_update_task(task_id: str, addBlockedBy: list[str]) -> str:
try:
task = update_task(task_id, addBlockedBy)
except ValueError as exc:
return f"Error: {exc}"
except FileNotFoundError:
return f"Error: Task {task_id} not found"
dependencies = ", ".join(task.blockedBy) or "(none)"
print(f" \033[34m[update] {task.subject} blockedBy: {dependencies}\033[0m")
return f"Updated {task.id} blockedBy: {dependencies}"
def run_list_tasks() -> str:
@ -2745,13 +2806,26 @@ BUILTIN_TOOLS = [
"input_schema": {"type": "object",
"properties": {"focus": {"type": "string"}},
"required": []}},
{"name": "create_task", "description": "Create a task.",
{"name": "create_task",
"description": "Create a task and return its runtime-generated ID.",
"input_schema": {"type": "object",
"properties": {"subject": {"type": "string"},
"description": {"type": "string"},
"blockedBy": {"type": "array",
"items": {"type": "string"}}},
"required": ["subject"]}},
"description": {"type": "string"}},
"required": ["subject"],
"additionalProperties": False}},
{"name": "update_task",
"description": "Add dependencies using IDs returned by create_task.",
"input_schema": {"type": "object",
"properties": {
"task_id": {"type": "string",
"pattern": "^task_[0-9a-f]{8}$"},
"addBlockedBy": {
"type": "array",
"items": {"type": "string",
"pattern": "^task_[0-9a-f]{8}$"},
"minItems": 1}},
"required": ["task_id", "addBlockedBy"],
"additionalProperties": False}},
{"name": "list_tasks", "description": "List all tasks.",
"input_schema": {"type": "object", "properties": {}, "required": []}},
{"name": "get_task", "description": "Get full task details.",
@ -2848,7 +2922,8 @@ BUILTIN_HANDLERS = {
"glob": run_agent_glob,
"todo_write": run_todo_write, "task": spawn_subagent,
"load_skill": load_skill,
"create_task": run_create_task, "list_tasks": run_list_tasks,
"create_task": run_create_task, "update_task": run_update_task,
"list_tasks": run_list_tasks,
"get_task": run_get_task,
"claim_task": run_claim_task, "complete_task": run_complete_task,
"schedule_cron": run_schedule_cron,

View file

@ -74,7 +74,7 @@
<text x="718" y="407" fill="#0f766e" font-size="9">s14 MCP tools</text>
<path d="M 790 318 L 790 188" fill="none" stroke="#0d9488" stroke-width="1.3" marker-end="url(#arrow-green)" stroke-dasharray="5,3"/>
<rect x="70" y="462" width="780" height="112" rx="8" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.2"/>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 25 builtins + dynamic mcp__server__tool</text>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 26 builtins + dynamic mcp__server__tool</text>
<text x="95" y="512" fill="#334155" font-size="9">file/shell: bash · read · write · edit · glob</text>
<text x="95" y="530" fill="#334155" font-size="9">single-agent: todo_write · task · load_skill · compact</text>
<text x="95" y="548" fill="#334155" font-size="9">durable work: task tools · cron tools</text>

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Before After
Before After

View file

@ -74,7 +74,7 @@
<text x="718" y="407" fill="#0f766e" font-size="9">s14 MCP tools</text>
<path d="M 790 318 L 790 188" fill="none" stroke="#0d9488" stroke-width="1.3" marker-end="url(#arrow-green)" stroke-dasharray="5,3"/>
<rect x="70" y="462" width="780" height="112" rx="8" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.2"/>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 25 builtins + dynamic mcp__server__tool</text>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 26 builtins + dynamic mcp__server__tool</text>
<text x="95" y="512" fill="#334155" font-size="9">file/shell: bash · read · write · edit · glob</text>
<text x="95" y="530" fill="#334155" font-size="9">single-agent: todo_write · task · load_skill · compact</text>
<text x="95" y="548" fill="#334155" font-size="9">durable work: task tools · cron tools</text>

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Before After
Before After

View file

@ -94,10 +94,10 @@
<!-- Tool pool -->
<rect x="70" y="462" width="780" height="112" rx="8" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.2"/>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 25 builtins + dynamic mcp__server__tool</text>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 26 builtins + dynamic mcp__server__tool</text>
<text x="95" y="512" fill="#334155" font-size="9">file/shell: bash · read · write · edit · glob</text>
<text x="95" y="530" fill="#334155" font-size="9">single-agent: todo_write · task · load_skill · compact</text>
<text x="95" y="548" fill="#334155" font-size="9">durable work: create/list/get/claim/complete_task · schedule/list/cancel_cron</text>
<text x="95" y="548" fill="#334155" font-size="9">durable work: create/update/list/get/claim/complete_task · schedule/list/cancel_cron</text>
<text x="510" y="512" fill="#334155" font-size="9">team: spawn_teammate · send_message · typed protocols</text>
<text x="510" y="530" fill="#334155" font-size="9">protocol: request_shutdown · request_plan · review_plan</text>
<text x="510" y="548" fill="#334155" font-size="9">workdir/plugin: create_worktree · connect_mcp</text>

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

View file

@ -123,6 +123,13 @@ def claim_in_child(lesson_path: str, root: str, task_id: str, owner: str,
results.put(lesson.claim_task(task_id, owner=owner))
def update_in_child(lesson_path: str, root: str, task_id: str,
dependency_id: str, barrier, results):
lesson = load_lesson(Path(root), Path(lesson_path))
barrier.wait()
results.put(lesson.run_update_task(task_id, [dependency_id]))
class AgentTeamsRuntimeTests(unittest.TestCase):
def test_downstream_lessons_execute_the_merged_runtime_contract(self):
for lesson_path in RUNTIME_LESSONS:
@ -140,6 +147,54 @@ class AgentTeamsRuntimeTests(unittest.TestCase):
self.assertTrue(lesson.release_completed_assignment("alice"))
self.assertNotIn("alice", lesson.teammate_assignments)
def test_task_dependencies_use_runtime_ids_and_are_lead_only(self):
for lesson_path in RUNTIME_LESSONS:
with self.subTest(lesson=lesson_path.parent.name):
with tempfile.TemporaryDirectory() as tmp:
lesson = load_lesson(Path(tmp), lesson_path)
tool_defs = getattr(lesson, "TOOLS", None)
if tool_defs is None:
tool_defs = lesson.BUILTIN_TOOLS
tools = {tool["name"]: tool for tool in tool_defs}
self.assertIn("update_task", tools)
self.assertNotIn(
"blockedBy",
tools["create_task"]["input_schema"]["properties"],
)
self.assertIn(
"runtime-generated IDs",
lesson.PROMPT_SECTIONS["tasks"],
)
dependency = lesson.create_task("Create schema")
target = lesson.create_task("Write API")
self.assertIn(
"Updated",
lesson.run_update_task(target.id, [dependency.id]),
)
self.assertEqual(
lesson.load_task(target.id).blockedBy, [dependency.id]
)
captured_tools = []
def stop_after_capture(**kwargs):
captured_tools.extend(
tool["name"] for tool in kwargs["tools"]
)
raise RuntimeError("stop after capturing teammate tools")
lesson.client.messages.create = stop_after_capture
lesson.spawn_teammate_thread(
"tool-auditor", "reviewer", "Inspect the task board."
)
self.assertTrue(wait_until(
lambda: "tool-auditor" not in lesson.active_teammates
))
self.assertIn("claim_task", captured_tools)
self.assertNotIn("update_task", captured_tools)
def test_inbox_delivery_is_runtime_owned(self):
with tempfile.TemporaryDirectory() as tmp:
lesson = load_lesson(Path(tmp))
@ -669,6 +724,9 @@ class AgentTeamsRuntimeTests(unittest.TestCase):
with self.subTest(tool=tool_name, task_id=task_id):
result = getattr(lesson, tool_name)(task_id)
self.assertIn("Error:", result)
self.assertIn(
"Error:", lesson.run_update_task(task_id, [])
)
def test_plan_gate_blocks_mutating_tools_until_approval(self):
with tempfile.TemporaryDirectory() as tmp:
@ -1621,6 +1679,55 @@ class AgentTeamsRuntimeTests(unittest.TestCase):
self.assertEqual(persisted.status, "in_progress")
self.assertIn(persisted.owner, {"alice", "bob"})
def test_dependency_updates_are_atomic_across_processes(self):
context = multiprocessing.get_context("spawn")
for lesson_path in RUNTIME_LESSONS:
with self.subTest(lesson=lesson_path.parent.name):
with tempfile.TemporaryDirectory() as tmp:
lesson = load_lesson(Path(tmp), lesson_path)
first = lesson.create_task("First")
second = lesson.create_task("Second")
barrier = context.Barrier(3)
results = context.Queue()
workers = [
context.Process(
target=update_in_child,
args=(
str(lesson_path), tmp, task_id, dependency_id,
barrier, results,
),
)
for task_id, dependency_id in (
(first.id, second.id),
(second.id, first.id),
)
]
for worker in workers:
worker.start()
barrier.wait()
for worker in workers:
worker.join(5)
self.assertEqual(worker.exitcode, 0)
outcomes = [results.get(timeout=1) for _ in workers]
self.assertEqual(
sum(outcome.startswith("Updated ") for outcome in outcomes),
1,
)
self.assertEqual(
sum("Dependency cycle detected" in outcome
for outcome in outcomes),
1,
)
persisted = {
first.id: lesson.load_task(first.id).blockedBy,
second.id: lesson.load_task(second.id).blockedBy,
}
self.assertEqual(
sum(bool(value) for value in persisted.values()), 1
)
def test_plan_approval_cannot_cross_assignment_boundary(self):
for lesson_path in RUNTIME_LESSONS:
with self.subTest(lesson=lesson_path.parent.name):

View file

@ -73,6 +73,7 @@ def test_s10_keeps_the_s04_kernel_and_adds_task_tools() -> None:
"edit_file",
"glob",
"create_task",
"update_task",
"list_tasks",
"get_task",
"claim_task",
@ -83,6 +84,13 @@ def test_s10_keeps_the_s04_kernel_and_adds_task_tools() -> None:
assert not hasattr(lesson, "MEMORY_DIR")
assert not (workdir / ".tasks").exists()
tools = {tool["name"]: tool for tool in lesson.TOOLS}
create_schema = tools["create_task"]["input_schema"]
update_schema = tools["update_task"]["input_schema"]
assert "blockedBy" not in create_schema["properties"]
assert create_schema["additionalProperties"] is False
assert update_schema["required"] == ["task_id", "addBlockedBy"]
def test_dependencies_gate_claim_and_completion_checks_owner() -> None:
with tempfile.TemporaryDirectory() as tmp:
@ -90,7 +98,8 @@ def test_dependencies_gate_claim_and_completion_checks_owner() -> None:
lesson = load_lesson(workdir)
schema = lesson.create_task("create schema")
api = lesson.create_task("write API", blockedBy=[schema.id])
api = lesson.create_task("write API")
lesson.update_task(api.id, [schema.id])
assert lesson.claim_task(api.id) == f"Blocked by: ['{schema.id}']"
assert "Claimed" in lesson.claim_task(schema.id)
@ -103,6 +112,41 @@ def test_dependencies_gate_claim_and_completion_checks_owner() -> None:
assert lesson.load_task(api.id).status == "completed"
def test_dependencies_are_added_after_create_returns_runtime_ids() -> None:
with tempfile.TemporaryDirectory() as tmp:
lesson = load_lesson(Path(tmp))
create_results = [
lesson.execute_tool(tool_call("create_task", subject=subject))
for subject in (
"create schema",
"write API",
"write tests",
"write docs",
)
]
task_ids = [result.split()[1].rstrip(":") for result in create_results]
schema_id, api_id, tests_id, docs_id = task_ids
update_results = [
lesson.execute_tool(tool_call(
"update_task", task_id=api_id, addBlockedBy=[schema_id]
)),
lesson.execute_tool(tool_call(
"update_task", task_id=tests_id, addBlockedBy=[api_id]
)),
lesson.execute_tool(tool_call(
"update_task", task_id=docs_id, addBlockedBy=[schema_id]
)),
]
assert all(not result.startswith("Error:") for result in update_results)
assert lesson.load_task(schema_id).blockedBy == []
assert lesson.load_task(api_id).blockedBy == [schema_id]
assert lesson.load_task(tests_id).blockedBy == [api_id]
assert lesson.load_task(docs_id).blockedBy == [schema_id]
def test_invalid_and_missing_task_ids_become_tool_results() -> None:
with tempfile.TemporaryDirectory() as tmp:
lesson = load_lesson(Path(tmp))
@ -132,17 +176,49 @@ def test_create_retries_instead_of_overwriting_an_existing_id(
assert [task.subject for task in lesson.list_tasks()] == ["second", "first"]
def test_create_rejects_unknown_dependencies() -> None:
def test_update_rejects_invalid_graph_changes_without_partial_mutation() -> None:
with tempfile.TemporaryDirectory() as tmp:
lesson = load_lesson(Path(tmp))
dependency = lesson.create_task("create schema")
target = lesson.create_task("write API")
output = lesson.execute_tool(tool_call(
"create_task",
subject="write API",
blockedBy=["task_00000000"],
missing = lesson.execute_tool(tool_call(
"update_task",
task_id=target.id,
addBlockedBy=[dependency.id, "task_00000000"],
))
self_dependency = lesson.execute_tool(tool_call(
"update_task", task_id=target.id, addBlockedBy=[target.id]
))
assert output == "Error: Dependency not found: task_00000000"
assert missing == "Error: Dependency not found: task_00000000"
assert self_dependency == "Error: Task cannot depend on itself"
assert lesson.load_task(target.id).blockedBy == []
def test_update_is_idempotent_and_rejects_cycles_or_started_tasks() -> None:
with tempfile.TemporaryDirectory() as tmp:
lesson = load_lesson(Path(tmp))
first = lesson.create_task("first")
second = lesson.create_task("second")
third = lesson.create_task("third")
lesson.update_task(second.id, [first.id, first.id])
lesson.update_task(second.id, [first.id])
lesson.update_task(third.id, [second.id])
cycle = lesson.execute_tool(tool_call(
"update_task", task_id=first.id, addBlockedBy=[third.id]
))
assert cycle.startswith("Error: Dependency cycle detected")
assert lesson.load_task(first.id).blockedBy == []
assert lesson.load_task(second.id).blockedBy == [first.id]
assert "Claimed" in lesson.claim_task(first.id)
started = lesson.execute_tool(tool_call(
"update_task", task_id=first.id, addBlockedBy=[second.id]
))
assert "only be updated while pending and unowned" in started
def test_task_store_rejects_a_symlink_outside_the_workspace() -> None:

View file

@ -25,6 +25,47 @@ def load_lesson(name: str, script: Path):
return module
def test_s10_scenario_builds_the_task_graph_in_two_phases() -> None:
steps = load_scenario("s10")["steps"]
create_calls = [
(index, json.loads(step["content"]))
for index, step in enumerate(steps)
if step.get("toolName") == "create_task"
and step["type"] == "tool_call"
]
create_results = [
(index, step["content"])
for index, step in enumerate(steps)
if step.get("toolName") == "create_task"
and step["type"] == "tool_result"
]
update_index = next(
index for index, step in enumerate(steps)
if step.get("toolName") == "update_task"
and step["type"] == "tool_call"
)
update = json.loads(steps[update_index]["content"])
task_ids = [
re.fullmatch(r"Created (task_[0-9a-f]{8}): .+", content).group(1)
for _, content in create_results
]
assert len(create_calls) == len(create_results) == 2
assert all("blockedBy" not in content for _, content in create_calls)
assert max(index for index, _ in create_results) < update_index
assert update == {
"task_id": task_ids[1],
"addBlockedBy": [task_ids[0]],
}
claim_inputs = [
json.loads(step["content"])
for step in steps
if step.get("toolName") == "claim_task"
and step["type"] == "tool_call"
]
assert all(set(claim_input) == {"task_id"} for claim_input in claim_inputs)
def test_s13_scenario_uses_the_real_plan_protocol() -> None:
steps = load_scenario("s13")["steps"]
spawn = next(
@ -48,6 +89,21 @@ def test_s13_scenario_uses_the_real_plan_protocol() -> None:
index for index, step in enumerate(steps)
if "plan_approval_response" in step.get("content", "")
)
create_indices = [
index for index, step in enumerate(steps)
if step.get("toolName") == "create_task"
and step["type"] == "tool_call"
]
update_index = next(
index for index, step in enumerate(steps)
if step.get("toolName") == "update_task"
and step["type"] == "tool_call"
)
first_spawn_index = next(
index for index, step in enumerate(steps)
if step.get("toolName") == "spawn_teammate"
and step["type"] == "tool_call"
)
review = json.loads(steps[review_index]["content"])
spawn_input = json.loads(spawn["content"])
@ -58,6 +114,15 @@ def test_s13_scenario_uses_the_real_plan_protocol() -> None:
assert re.fullmatch(r"req_\d{6}", review["request_id"])
assert review["approve"] is True
assert "approved" not in review
assert all(
"blockedBy" not in json.loads(steps[index]["content"])
for index in create_indices
)
assert max(create_indices) < update_index < first_spawn_index
assert json.loads(steps[update_index]["content"]) == {
"task_id": "task_5e6f7a8b",
"addBlockedBy": ["task_1a2b3c4d"],
}
def test_s15_scenario_calls_the_discovered_mcp_tool() -> None:

View file

@ -473,8 +473,9 @@ def test_workflow_default_entry_extends_the_real_s15_host(
tools, handlers = host.assemble_tool_pool()
names = [tool["name"] for tool in tools]
assert len(host.BUILTIN_TOOLS) == 25
assert len(host.BUILTIN_TOOLS) == 26
assert names[:-1] == [tool["name"] for tool in host.BUILTIN_TOOLS]
assert "update_task" in names
assert names[-1] == "Workflow"
assert handlers["Workflow"] is workflow.run_workflow_sync
assert handlers["Workflow"](name="missing") == (

View file

@ -16,7 +16,7 @@
<!-- 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>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 6 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"/>
@ -46,7 +46,7 @@
<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="128" fill="#0d9488" font-size="9" font-weight="600">create_task · update_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 -->
@ -61,13 +61,13 @@
<!-- 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>
<text x="300" y="178" fill="#0d9488" font-size="9">create → ID / update edges / 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="222" fill="#0d9488" font-size="9">create_task → runtime ID; update_task → blockedBy</text>
<text x="408" y="238" fill="#0d9488" font-size="9">can_start + claim: all dependencies completed?</text>
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + unblock downstream</text>
<!-- ===== State machine ===== -->
@ -90,5 +90,5 @@
<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>
<text x="80" y="394" fill="#475569" font-size="10">s10 New: Task dataclass + 6 tools + .tasks/ persistence + blockedBy dependency graph</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Before After
Before After

View file

@ -16,7 +16,7 @@
<!-- タイトル -->
<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>
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Task System — 6 つのタスクツール + .tasks/ 永続化 + blockedBy 依存</text>
<!-- 凡例 -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
@ -46,7 +46,7 @@
<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="128" fill="#0d9488" font-size="9" font-weight="600">create_task · update_task · list_tasks</text>
<text x="408" y="140" fill="#0d9488" font-size="9" font-weight="600">get_task · claim_task · complete_task</text>
<!-- ループバック -->
@ -61,13 +61,13 @@
<!-- 矢印: 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>
<text x="300" y="178" fill="#0d9488" font-size="9">create → ID / update edges / 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="222" fill="#0d9488" font-size="9">create_task → 実行時 IDupdate_task → blockedBy</text>
<text x="408" y="238" fill="#0d9488" font-size="9">can_start + claim依存がすべて completed?</text>
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + 下流をアンロック</text>
<!-- ===== 状態マシン ===== -->
@ -90,5 +90,5 @@
<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>
<text x="80" y="394" fill="#475569" font-size="10">s10 新規Task dataclass + 6 ツール + .tasks/ 永続化 + blockedBy 依存グラフ</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before After
Before After

View file

@ -16,7 +16,7 @@
<!-- 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>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Task System — 6 个任务工具 + .tasks/ 持久化 + blockedBy 依赖</text>
<!-- Legend -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
@ -46,7 +46,7 @@
<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="128" fill="#0d9488" font-size="9" font-weight="600">create_task · update_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 -->
@ -61,13 +61,13 @@
<!-- 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>
<text x="300" y="178" fill="#0d9488" font-size="9">create → ID / update edges / 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="222" fill="#0d9488" font-size="9">create_task → 运行时 IDupdate_task → blockedBy</text>
<text x="408" y="238" fill="#0d9488" font-size="9">can_start + claim依赖全部 completed?</text>
<text x="408" y="252" fill="#0d9488" font-size="9">complete_task → completed + 解锁下游</text>
<!-- ===== State machine ===== -->
@ -90,5 +90,5 @@
<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>
<text x="80" y="394" fill="#475569" font-size="10">s10 新增Task dataclass + 6 个工具 + .tasks/ 持久化 + blockedBy 依赖图</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Before After
Before After

View file

@ -74,7 +74,7 @@
<text x="718" y="407" fill="#0f766e" font-size="9">s14 MCP tools</text>
<path d="M 790 318 L 790 188" fill="none" stroke="#0d9488" stroke-width="1.3" marker-end="url(#arrow-green)" stroke-dasharray="5,3"/>
<rect x="70" y="462" width="780" height="112" rx="8" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.2"/>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 25 builtins + dynamic mcp__server__tool</text>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 26 builtins + dynamic mcp__server__tool</text>
<text x="95" y="512" fill="#334155" font-size="9">file/shell: bash · read · write · edit · glob</text>
<text x="95" y="530" fill="#334155" font-size="9">single-agent: todo_write · task · load_skill · compact</text>
<text x="95" y="548" fill="#334155" font-size="9">durable work: task tools · cron tools</text>

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Before After
Before After

View file

@ -74,7 +74,7 @@
<text x="718" y="407" fill="#0f766e" font-size="9">s14 MCP tools</text>
<path d="M 790 318 L 790 188" fill="none" stroke="#0d9488" stroke-width="1.3" marker-end="url(#arrow-green)" stroke-dasharray="5,3"/>
<rect x="70" y="462" width="780" height="112" rx="8" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.2"/>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 25 builtins + dynamic mcp__server__tool</text>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 26 builtins + dynamic mcp__server__tool</text>
<text x="95" y="512" fill="#334155" font-size="9">file/shell: bash · read · write · edit · glob</text>
<text x="95" y="530" fill="#334155" font-size="9">single-agent: todo_write · task · load_skill · compact</text>
<text x="95" y="548" fill="#334155" font-size="9">durable work: task tools · cron tools</text>

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Before After
Before After

View file

@ -94,10 +94,10 @@
<!-- Tool pool -->
<rect x="70" y="462" width="780" height="112" rx="8" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.2"/>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 25 builtins + dynamic mcp__server__tool</text>
<text x="460" y="487" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="700">TOOL POOL: 26 builtins + dynamic mcp__server__tool</text>
<text x="95" y="512" fill="#334155" font-size="9">file/shell: bash · read · write · edit · glob</text>
<text x="95" y="530" fill="#334155" font-size="9">single-agent: todo_write · task · load_skill · compact</text>
<text x="95" y="548" fill="#334155" font-size="9">durable work: create/list/get/claim/complete_task · schedule/list/cancel_cron</text>
<text x="95" y="548" fill="#334155" font-size="9">durable work: create/update/list/get/claim/complete_task · schedule/list/cancel_cron</text>
<text x="510" y="512" fill="#334155" font-size="9">team: spawn_teammate · send_message · typed protocols</text>
<text x="510" y="530" fill="#334155" font-size="9">protocol: request_shutdown · request_plan · review_plan</text>
<text x="510" y="548" fill="#334155" font-size="9">workdir/plugin: create_worktree · connect_mcp</text>

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

View file

@ -15,6 +15,20 @@
"description": "各タスクは .tasks/ に id、subject、description、status、owner、blockedBy を持って保存されます。タスクボードはコンテキスト圧縮や再起動を越えて残ります。"
}
},
{
"id": "runtime-owned-task-ids",
"title": "Create Nodes Before Adding Edges",
"description": "create_task returns a host-generated ID. After all nodes exist, update_task uses those returned IDs to add blockedBy edges and rejects invalid or cyclic changes.",
"alternatives": "Asking the model to invent persistent IDs can produce collisions or references that the host never created.",
"zh": {
"title": "先创建节点,再添加边",
"description": "create_task 返回宿主生成的 ID。所有节点创建后update_task 使用这些返回的 ID 添加 blockedBy 边,并拒绝无效或成环的修改。"
},
"ja": {
"title": "ノードを作成してから辺を追加する",
"description": "create_task は host が生成した ID を返します。全ードの作成後、update_task は返された ID で blockedBy の辺を追加し、無効な変更や循環を拒否します。"
}
},
{
"id": "blockedby-dependencies",
"title": "blockedBy Encodes Ordering",

View file

@ -440,21 +440,23 @@ const CURRENT_FLOW_OVERRIDES: Record<string, FlowDefinition> = {
s10: {
nodes: [
{ id: "start", label: "User Goal", type: "start", x: COL_CENTER, y: 30 },
{ id: "create", label: "create_task", type: "subprocess", x: COL_CENTER, y: 120 },
{ id: "save", label: "Persist JSON\n.tasks/", type: "process", x: COL_CENTER, y: 210 },
{ id: "list", label: "list / get", type: "subprocess", x: COL_RIGHT, y: 300 },
{ id: "deps", label: "blockedBy\ncomplete?", type: "decision", x: COL_CENTER, y: 390 },
{ id: "blocked", label: "Remain Pending", type: "end", x: COL_RIGHT, y: 490 },
{ id: "claim", label: "claim_task\nowner + in_progress", type: "subprocess", x: COL_LEFT, y: 490 },
{ id: "complete", label: "complete_task", type: "subprocess", x: COL_LEFT, y: 590 },
{ id: "unblock", label: "Report\nUnblocked", type: "process", x: COL_CENTER, y: 690 },
{ id: "append", label: "Append Result", type: "process", x: COL_CENTER, y: 780 },
{ id: "create", label: "create_task\n(all nodes)", type: "subprocess", x: COL_CENTER, y: 120 },
{ id: "save", label: "Persist Nodes\nReturn IDs", type: "process", x: COL_CENTER, y: 210 },
{ id: "update", label: "update_task\n(addBlockedBy)", type: "subprocess", x: COL_CENTER, y: 300 },
{ id: "list", label: "list / get", type: "subprocess", x: COL_RIGHT, y: 390 },
{ id: "deps", label: "blockedBy\ncomplete?", type: "decision", x: COL_CENTER, y: 480 },
{ id: "blocked", label: "Remain Pending", type: "end", x: COL_RIGHT, y: 580 },
{ id: "claim", label: "claim_task\nowner + in_progress", type: "subprocess", x: COL_LEFT, y: 580 },
{ id: "complete", label: "complete_task", type: "subprocess", x: COL_LEFT, y: 680 },
{ id: "unblock", label: "Report\nUnblocked", type: "process", x: COL_CENTER, y: 780 },
{ id: "append", label: "Append Result", type: "process", x: COL_CENTER, y: 870 },
],
edges: [
{ from: "start", to: "create" },
{ from: "create", to: "save" },
{ from: "save", to: "list" },
{ from: "save", to: "deps" },
{ from: "save", to: "update" },
{ from: "update", to: "list" },
{ from: "update", to: "deps" },
{ from: "deps", to: "blocked", label: "no" },
{ from: "deps", to: "claim", label: "yes" },
{ from: "claim", to: "complete" },

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -12,36 +12,72 @@
"type": "tool_call",
"toolName": "create_task",
"content": "{\"subject\":\"Run tests\"}",
"annotation": "Tasks are created as JSON files under .tasks/."
"annotation": "The first phase creates task nodes without dependencies."
},
{
"type": "tool_result",
"toolName": "create_task",
"content": "Created task_a1b2c3d4: Run tests",
"annotation": "The host returns the runtime-generated ID after the tool executes."
},
{
"type": "tool_call",
"toolName": "create_task",
"content": "{\"subject\":\"Deploy release\",\"blockedBy\":[\"task_tests\"]}",
"annotation": "blockedBy encodes dependency ordering."
"content": "{\"subject\":\"Deploy release\"}",
"annotation": "Sibling create calls do not guess or predeclare IDs."
},
{
"type": "tool_result",
"toolName": "create_task",
"content": "Created task_5e6f7a8b: Deploy release",
"annotation": "The second node receives a different host-generated ID."
},
{
"type": "tool_call",
"toolName": "update_task",
"content": "{\"task_id\":\"task_5e6f7a8b\",\"addBlockedBy\":[\"task_a1b2c3d4\"]}",
"annotation": "The second phase adds an edge using IDs returned by create_task."
},
{
"type": "tool_result",
"toolName": "update_task",
"content": "Updated task_5e6f7a8b blockedBy: task_a1b2c3d4",
"annotation": "The runtime validates and persists the dependency graph."
},
{
"type": "tool_call",
"toolName": "claim_task",
"content": "{\"task_id\":\"task_deploy\",\"owner\":\"agent\"}",
"content": "{\"task_id\":\"task_5e6f7a8b\"}",
"annotation": "The claim fails until dependencies are complete."
},
{
"type": "tool_result",
"toolName": "claim_task",
"content": "Blocked by: [\"task_tests\"]",
"content": "Blocked by: [\"task_a1b2c3d4\"]",
"annotation": "The task graph prevents premature work."
},
{
"type": "tool_call",
"toolName": "claim_task",
"content": "{\"task_id\":\"task_a1b2c3d4\"}",
"annotation": "The unblocked prerequisite is claimed before work begins."
},
{
"type": "tool_result",
"toolName": "claim_task",
"content": "Claimed task_a1b2c3d4 (Run tests)",
"annotation": "Claim moves the task from pending to in_progress."
},
{
"type": "tool_call",
"toolName": "complete_task",
"content": "{\"task_id\":\"task_tests\"}",
"content": "{\"task_id\":\"task_a1b2c3d4\"}",
"annotation": "Completing a dependency can unblock downstream tasks."
},
{
"type": "tool_result",
"toolName": "complete_task",
"content": "Completed task_tests\nUnblocked: Deploy release",
"content": "Completed task_a1b2c3d4 (Run tests)\nUnblocked: Deploy release",
"annotation": "The harness reports newly available work."
}
]

View file

@ -30,24 +30,36 @@
"content": "Created task_1a2b3c4d: Refactor authentication",
"annotation": "The runtime-generated task ID is carried into every later operation on this task."
},
{
"type": "tool_call",
"toolName": "create_worktree",
"content": "{\"name\":\"auth-refactor\",\"task_id\":\"task_1a2b3c4d\"}",
"annotation": "The worktree directory is recorded on the task instead of managed as a separate workflow or security sandbox."
},
{
"type": "tool_call",
"toolName": "create_task",
"content": "{\"subject\":\"Update authentication tests\",\"blockedBy\":[\"task_1a2b3c4d\"]}",
"annotation": "The task graph keeps dependent work from starting early."
"content": "{\"subject\":\"Update authentication tests\"}",
"annotation": "The Lead creates every task node before adding graph edges."
},
{
"type": "tool_result",
"toolName": "create_task",
"content": "Created task_5e6f7a8b: Update authentication tests (blockedBy: task_1a2b3c4d)",
"content": "Created task_5e6f7a8b: Update authentication tests",
"annotation": "The second generated ID names the dependent task that the test teammate will later claim."
},
{
"type": "tool_call",
"toolName": "update_task",
"content": "{\"task_id\":\"task_5e6f7a8b\",\"addBlockedBy\":[\"task_1a2b3c4d\"]}",
"annotation": "Only the Lead links tasks using IDs already returned by the runtime."
},
{
"type": "tool_result",
"toolName": "update_task",
"content": "Updated task_5e6f7a8b blockedBy: task_1a2b3c4d",
"annotation": "The dependency is fixed before teammates begin claiming work."
},
{
"type": "tool_call",
"toolName": "create_worktree",
"content": "{\"name\":\"auth-refactor\",\"task_id\":\"task_1a2b3c4d\"}",
"annotation": "The worktree directory is recorded on the task instead of managed as a separate workflow or security sandbox."
},
{
"type": "tool_call",
"toolName": "spawn_teammate",