mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-06-20 20:23:36 +08:00
fix: align TodoWrite memory model with task system docs
This commit is contained in:
@@ -32,19 +32,21 @@ The dispatch mechanism is unchanged; the new tool is still routed through `TOOL_
|
|||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
**The todo_write tool**, accepts a list with statuses, persists to `.tasks/current_todos.json` (teaching version writes to disk for observability), and displays progress in the terminal:
|
**The todo_write tool** accepts a list with statuses, keeps it in the current process memory, and displays progress in the terminal:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
CURRENT_TODOS: list[dict] = []
|
||||||
|
|
||||||
def run_todo_write(todos: list) -> str:
|
def run_todo_write(todos: list) -> str:
|
||||||
tasks_file = TASKS_DIR / "current_todos.json"
|
global CURRENT_TODOS
|
||||||
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
|
CURRENT_TODOS = todos
|
||||||
|
|
||||||
lines = ["\n## Current Tasks"]
|
lines = ["\n## Current Tasks"]
|
||||||
for t in todos:
|
for t in CURRENT_TODOS:
|
||||||
icon = {"pending": " ", "in_progress": "▸", "completed": "✓"}[t["status"]]
|
icon = {"pending": " ", "in_progress": "▸", "completed": "✓"}[t["status"]]
|
||||||
lines.append(f" [{icon}] {t['content']}")
|
lines.append(f" [{icon}] {t['content']}")
|
||||||
print("\n".join(lines))
|
print("\n".join(lines))
|
||||||
return f"Updated {len(todos)} tasks"
|
return f"Updated {len(CURRENT_TODOS)} tasks"
|
||||||
```
|
```
|
||||||
|
|
||||||
The tool definition joins the other 5 in the dispatch map:
|
The tool definition joins the other 5 in the dispatch map:
|
||||||
@@ -135,7 +137,7 @@ The Agent can plan now. But if a task is too large, say "refactor the entire aut
|
|||||||
|
|
||||||
CC has two task systems coexisting (`tasks.ts:133-139`):
|
CC has two task systems coexisting (`tasks.ts:133-139`):
|
||||||
|
|
||||||
- **TodoWrite (V1)**: A simple list tool, data maintained in memory AppState (`TodoWriteTool.ts:65-103`). The teaching version writes to `.tasks/current_todos.json` for observability; the real V1 does not write to disk.
|
- **TodoWrite (V1)**: A simple list tool, data maintained in memory AppState (`TodoWriteTool.ts:65-103`). The teaching version also keeps it in process memory and clears it on exit.
|
||||||
- **Task System (V2 = s12)**: File-persisted, dependency graph, concurrency locks, ownership.
|
- **Task System (V2 = s12)**: File-persisted, dependency graph, concurrency locks, ownership.
|
||||||
|
|
||||||
The switch is controlled by `isTodoV2Enabled()`. In the current source: V2 is enabled by default in interactive sessions, V1 in non-interactive (SDK) sessions; setting `CLAUDE_CODE_ENABLE_TASKS` forces V2 regardless. Note the source comment "Force-enable tasks in non-interactive mode" describes the env var path's purpose, not the default branch's return semantics.
|
The switch is controlled by `isTodoV2Enabled()`. In the current source: V2 is enabled by default in interactive sessions, V1 in non-interactive (SDK) sessions; setting `CLAUDE_CODE_ENABLE_TASKS` forces V2 regardless. Note the source comment "Force-enable tasks in non-interactive mode" describes the env var path's purpose, not the default branch's return semantics.
|
||||||
|
|||||||
@@ -32,19 +32,21 @@ Agent は作業を開始する。3 つのファイルをリネーム、テスト
|
|||||||
|
|
||||||
## 仕組み
|
## 仕組み
|
||||||
|
|
||||||
**todo_write ツール**、ステータス付きのリストを受け取り、`.tasks/current_todos.json` に永続化(教育版は観察用にディスクに書き込む)、端末に進捗を表示する:
|
**todo_write ツール**は、ステータス付きのリストを受け取り、現在のプロセスメモリに保持し、端末に進捗を表示する:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
CURRENT_TODOS: list[dict] = []
|
||||||
|
|
||||||
def run_todo_write(todos: list) -> str:
|
def run_todo_write(todos: list) -> str:
|
||||||
tasks_file = TASKS_DIR / "current_todos.json"
|
global CURRENT_TODOS
|
||||||
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
|
CURRENT_TODOS = todos
|
||||||
|
|
||||||
lines = ["\n## Current Tasks"]
|
lines = ["\n## Current Tasks"]
|
||||||
for t in todos:
|
for t in CURRENT_TODOS:
|
||||||
icon = {"pending": " ", "in_progress": "▸", "completed": "✓"}[t["status"]]
|
icon = {"pending": " ", "in_progress": "▸", "completed": "✓"}[t["status"]]
|
||||||
lines.append(f" [{icon}] {t['content']}")
|
lines.append(f" [{icon}] {t['content']}")
|
||||||
print("\n".join(lines))
|
print("\n".join(lines))
|
||||||
return f"Updated {len(todos)} tasks"
|
return f"Updated {len(CURRENT_TODOS)} tasks"
|
||||||
```
|
```
|
||||||
|
|
||||||
ツール定義は他の 5 つと一緒にディスパッチマップに追加される:
|
ツール定義は他の 5 つと一緒にディスパッチマップに追加される:
|
||||||
@@ -135,7 +137,7 @@ Agent は計画できるようになった。しかしタスクが大きすぎ
|
|||||||
|
|
||||||
CC には二つのタスクシステムが共存している(`tasks.ts:133-139`):
|
CC には二つのタスクシステムが共存している(`tasks.ts:133-139`):
|
||||||
|
|
||||||
- **TodoWrite(V1)**:シンプルなリストツール、データはメモリ AppState で管理(`TodoWriteTool.ts:65-103`)。教育版は観察用に `.tasks/current_todos.json` に書き込むが、実際の V1 はディスクに書き込まない
|
- **TodoWrite(V1)**:シンプルなリストツール、データはメモリ AppState で管理(`TodoWriteTool.ts:65-103`)。教育版もプロセスメモリに保持し、終了時に消える
|
||||||
- **Task System(V2 = s12)**:ファイル永続化、依存グラフ、並行ロック、ownership
|
- **Task System(V2 = s12)**:ファイル永続化、依存グラフ、並行ロック、ownership
|
||||||
|
|
||||||
切り替えは `isTodoV2Enabled()` で制御される。現在のソースコードの実装:対話型セッションでは V2 がデフォルトで有効、非対話型セッション(SDK)では V1 がデフォルトで有効。`CLAUDE_CODE_ENABLE_TASKS` 環境変数を設定するとセッション種別に関わらず V2 が強制有効になる。ソースコメント「Force-enable tasks in non-interactive mode」は環境変数パスの用途を説明しており、デフォルト分岐の戻り値のセマンティクスとは異なるため注意。
|
切り替えは `isTodoV2Enabled()` で制御される。現在のソースコードの実装:対話型セッションでは V2 がデフォルトで有効、非対話型セッション(SDK)では V1 がデフォルトで有効。`CLAUDE_CODE_ENABLE_TASKS` 環境変数を設定するとセッション種別に関わらず V2 が強制有効になる。ソースコメント「Force-enable tasks in non-interactive mode」は環境変数パスの用途を説明しており、デフォルト分岐の戻り値のセマンティクスとは異なるため注意。
|
||||||
|
|||||||
@@ -32,19 +32,21 @@ dispatch 机制不变,新工具仍然走 `TOOL_HANDLERS[block.name]` 分发。
|
|||||||
|
|
||||||
## 工作原理
|
## 工作原理
|
||||||
|
|
||||||
**todo_write 工具**,接收一个带状态的列表,持久化到 `.tasks/current_todos.json`(教学版写盘以便观察),同时在终端显示进度:
|
**todo_write 工具**,接收一个带状态的列表,保存在当前进程内存中,同时在终端显示进度:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
CURRENT_TODOS: list[dict] = []
|
||||||
|
|
||||||
def run_todo_write(todos: list) -> str:
|
def run_todo_write(todos: list) -> str:
|
||||||
tasks_file = TASKS_DIR / "current_todos.json"
|
global CURRENT_TODOS
|
||||||
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
|
CURRENT_TODOS = todos
|
||||||
|
|
||||||
lines = ["\n## Current Tasks"]
|
lines = ["\n## Current Tasks"]
|
||||||
for t in todos:
|
for t in CURRENT_TODOS:
|
||||||
icon = {"pending": " ", "in_progress": "▸", "completed": "✓"}[t["status"]]
|
icon = {"pending": " ", "in_progress": "▸", "completed": "✓"}[t["status"]]
|
||||||
lines.append(f" [{icon}] {t['content']}")
|
lines.append(f" [{icon}] {t['content']}")
|
||||||
print("\n".join(lines))
|
print("\n".join(lines))
|
||||||
return f"Updated {len(todos)} tasks"
|
return f"Updated {len(CURRENT_TODOS)} tasks"
|
||||||
```
|
```
|
||||||
|
|
||||||
工具定义和其他 5 个工具一起加入 dispatch map:
|
工具定义和其他 5 个工具一起加入 dispatch map:
|
||||||
@@ -135,7 +137,7 @@ s06 Subagent → 把大任务拆成子任务,每个子任务派一个独立的
|
|||||||
|
|
||||||
CC 中有两套任务系统并存(`tasks.ts:133-139`):
|
CC 中有两套任务系统并存(`tasks.ts:133-139`):
|
||||||
|
|
||||||
- **TodoWrite(V1)**:一个简单的列表工具,数据在内存 AppState 中维护(`TodoWriteTool.ts:65-103`)。教学版写盘到 `.tasks/current_todos.json` 是为了可观察性,真实 V1 不写盘
|
- **TodoWrite(V1)**:一个简单的列表工具,数据在内存 AppState 中维护(`TodoWriteTool.ts:65-103`)。教学版也保存在进程内存里,退出后清空
|
||||||
- **Task System(V2 = s12)**:文件持久化、依赖图、并发锁、ownership
|
- **Task System(V2 = s12)**:文件持久化、依赖图、并发锁、ownership
|
||||||
|
|
||||||
切换由 `isTodoV2Enabled()` 控制。当前源码的实现逻辑:交互式会话中 V2 默认启用,非交互式会话(SDK)中 V1 默认启用;设置 `CLAUDE_CODE_ENABLE_TASKS` 环境变量可强制启用 V2。注意源码注释 "Force-enable tasks in non-interactive mode" 描述的是 env var 路径的用途,和默认分支的返回值语义不同,阅读时需区分。
|
切换由 `isTodoV2Enabled()` 控制。当前源码的实现逻辑:交互式会话中 V2 默认启用,非交互式会话(SDK)中 V1 默认启用;设置 `CLAUDE_CODE_ENABLE_TASKS` 环境变量可强制启用 V2。注意源码注释 "Force-enable tasks in non-interactive mode" 描述的是 env var 路径的用途,和默认分支的返回值语义不同,阅读时需区分。
|
||||||
@@ -153,4 +155,4 @@ Task System 相比 TodoWrite 的核心增量:
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<!-- translation-sync: zh@v1, en@v0, ja@v0 -->
|
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ s05: TodoWrite — add a planning tool on top of s04 hooks.
|
|||||||
todo_write ← NEW
|
todo_write ← NEW
|
||||||
+------------------+
|
+------------------+
|
||||||
|
|
|
|
||||||
.tasks/current_todos.json
|
in-memory current_todos
|
||||||
|
|
|
|
||||||
if rounds_since_todo >= 3:
|
if rounds_since_todo >= 3:
|
||||||
inject <reminder>
|
inject <reminder>
|
||||||
@@ -28,7 +28,7 @@ Run: python s05_todo_write/code.py
|
|||||||
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
|
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os, subprocess, json
|
import os, subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -45,9 +45,9 @@ if os.getenv("ANTHROPIC_BASE_URL"):
|
|||||||
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
|
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
|
||||||
|
|
||||||
WORKDIR = Path.cwd()
|
WORKDIR = Path.cwd()
|
||||||
TASKS_DIR = WORKDIR / ".tasks"; TASKS_DIR.mkdir(exist_ok=True)
|
|
||||||
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
||||||
MODEL = os.environ["MODEL_ID"]
|
MODEL = os.environ["MODEL_ID"]
|
||||||
|
CURRENT_TODOS: list[dict] = []
|
||||||
|
|
||||||
# s05 change: SYSTEM prompt adds planning guidance
|
# s05 change: SYSTEM prompt adds planning guidance
|
||||||
SYSTEM = (
|
SYSTEM = (
|
||||||
@@ -122,20 +122,20 @@ def run_glob(pattern: str) -> str:
|
|||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def run_todo_write(todos: list) -> str:
|
def run_todo_write(todos: list) -> str:
|
||||||
|
global CURRENT_TODOS
|
||||||
# validate required fields
|
# validate required fields
|
||||||
for i, t in enumerate(todos):
|
for i, t in enumerate(todos):
|
||||||
if "content" not in t or "status" not in t:
|
if "content" not in t or "status" not in t:
|
||||||
return f"Error: todos[{i}] missing 'content' or 'status'"
|
return f"Error: todos[{i}] missing 'content' or 'status'"
|
||||||
if t["status"] not in ("pending", "in_progress", "completed"):
|
if t["status"] not in ("pending", "in_progress", "completed"):
|
||||||
return f"Error: todos[{i}] has invalid status '{t['status']}'"
|
return f"Error: todos[{i}] has invalid status '{t['status']}'"
|
||||||
tasks_file = TASKS_DIR / "current_todos.json"
|
CURRENT_TODOS = todos
|
||||||
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
|
|
||||||
lines = ["\n\033[33m## Current Tasks\033[0m"]
|
lines = ["\n\033[33m## Current Tasks\033[0m"]
|
||||||
for t in todos:
|
for t in CURRENT_TODOS:
|
||||||
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
|
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
|
||||||
lines.append(f" [{icon}] {t['content']}")
|
lines.append(f" [{icon}] {t['content']}")
|
||||||
print("\n".join(lines))
|
print("\n".join(lines))
|
||||||
return f"Updated {len(todos)} tasks"
|
return f"Updated {len(CURRENT_TODOS)} tasks"
|
||||||
|
|
||||||
TOOLS = [
|
TOOLS = [
|
||||||
{"name": "bash", "description": "Run a shell command.",
|
{"name": "bash", "description": "Run a shell command.",
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
<text x="560" y="173" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">todo_write</text>
|
<text x="560" y="173" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">todo_write</text>
|
||||||
<text x="560" y="232" fill="#16a34a" font-size="11" font-weight="600" text-anchor="middle">s05 New</text>
|
<text x="560" y="232" fill="#16a34a" font-size="11" font-weight="600" text-anchor="middle">s05 New</text>
|
||||||
|
|
||||||
<text x="560" y="196" fill="#64748b" font-size="8" text-anchor="middle">→ .tasks/current_todos.json</text>
|
<text x="560" y="196" fill="#64748b" font-size="8" text-anchor="middle">→ in-memory TODO list</text>
|
||||||
|
|
||||||
<!-- Loop back -->
|
<!-- Loop back -->
|
||||||
<path d="M 620 112 L 660 112 L 660 260 L 90 260 L 90 134" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
<path d="M 620 112 L 660 112 L 660 260 L 90 260 L 90 134" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -73,7 +73,7 @@
|
|||||||
<text x="560" y="173" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">todo_write</text>
|
<text x="560" y="173" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">todo_write</text>
|
||||||
<text x="560" y="232" fill="#16a34a" font-size="11" font-weight="600" text-anchor="middle">s05 新規</text>
|
<text x="560" y="232" fill="#16a34a" font-size="11" font-weight="600" text-anchor="middle">s05 新規</text>
|
||||||
|
|
||||||
<text x="560" y="196" fill="#64748b" font-size="8" text-anchor="middle">→ .tasks/current_todos.json</text>
|
<text x="560" y="196" fill="#64748b" font-size="8" text-anchor="middle">→ メモリ内 TODO リスト</text>
|
||||||
|
|
||||||
<!-- ループバック -->
|
<!-- ループバック -->
|
||||||
<path d="M 620 112 L 660 112 L 660 260 L 90 260 L 90 134" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
<path d="M 620 112 L 660 112 L 660 260 L 90 260 L 90 134" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
@@ -73,7 +73,7 @@
|
|||||||
<text x="560" y="173" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">todo_write</text>
|
<text x="560" y="173" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">todo_write</text>
|
||||||
<text x="560" y="232" fill="#16a34a" font-size="11" font-weight="600" text-anchor="middle">s05 新增</text>
|
<text x="560" y="232" fill="#16a34a" font-size="11" font-weight="600" text-anchor="middle">s05 新增</text>
|
||||||
|
|
||||||
<text x="560" y="196" fill="#64748b" font-size="8" text-anchor="middle">→ .tasks/current_todos.json</text>
|
<text x="560" y="196" fill="#64748b" font-size="8" text-anchor="middle">→ 进程内 TODO 列表</text>
|
||||||
|
|
||||||
<!-- 回环 -->
|
<!-- 回环 -->
|
||||||
<path d="M 620 112 L 660 112 L 660 260 L 90 260 L 90 134" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
<path d="M 620 112 L 660 112 L 660 260 L 90 260 L 90 134" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -28,7 +28,7 @@ Run: python s06_subagent/code.py
|
|||||||
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
|
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os, subprocess, json
|
import os, subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -45,9 +45,9 @@ if os.getenv("ANTHROPIC_BASE_URL"):
|
|||||||
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
|
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
|
||||||
|
|
||||||
WORKDIR = Path.cwd()
|
WORKDIR = Path.cwd()
|
||||||
TASKS_DIR = WORKDIR / ".tasks"; TASKS_DIR.mkdir(exist_ok=True)
|
|
||||||
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
||||||
MODEL = os.environ["MODEL_ID"]
|
MODEL = os.environ["MODEL_ID"]
|
||||||
|
CURRENT_TODOS: list[dict] = []
|
||||||
|
|
||||||
SYSTEM = (
|
SYSTEM = (
|
||||||
f"You are a coding agent at {WORKDIR}. "
|
f"You are a coding agent at {WORKDIR}. "
|
||||||
@@ -122,19 +122,19 @@ def run_glob(pattern: str) -> str:
|
|||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
|
||||||
def run_todo_write(todos: list) -> str:
|
def run_todo_write(todos: list) -> str:
|
||||||
|
global CURRENT_TODOS
|
||||||
for i, t in enumerate(todos):
|
for i, t in enumerate(todos):
|
||||||
if "content" not in t or "status" not in t:
|
if "content" not in t or "status" not in t:
|
||||||
return f"Error: todos[{i}] missing 'content' or 'status'"
|
return f"Error: todos[{i}] missing 'content' or 'status'"
|
||||||
if t["status"] not in ("pending", "in_progress", "completed"):
|
if t["status"] not in ("pending", "in_progress", "completed"):
|
||||||
return f"Error: todos[{i}] has invalid status '{t['status']}'"
|
return f"Error: todos[{i}] has invalid status '{t['status']}'"
|
||||||
tasks_file = TASKS_DIR / "current_todos.json"
|
CURRENT_TODOS = todos
|
||||||
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
|
|
||||||
lines = ["\n\033[33m## Current Tasks\033[0m"]
|
lines = ["\n\033[33m## Current Tasks\033[0m"]
|
||||||
for t in todos:
|
for t in CURRENT_TODOS:
|
||||||
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
|
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
|
||||||
lines.append(f" [{icon}] {t['content']}")
|
lines.append(f" [{icon}] {t['content']}")
|
||||||
print("\n".join(lines))
|
print("\n".join(lines))
|
||||||
return f"Updated {len(todos)} tasks"
|
return f"Updated {len(CURRENT_TODOS)} tasks"
|
||||||
|
|
||||||
def extract_text(content) -> str:
|
def extract_text(content) -> str:
|
||||||
"""Extract text from message content blocks."""
|
"""Extract text from message content blocks."""
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Run: python s07_skill_loading/code.py
|
|||||||
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
|
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os, subprocess, json
|
import os, subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -44,9 +44,9 @@ if os.getenv("ANTHROPIC_BASE_URL"):
|
|||||||
|
|
||||||
WORKDIR = Path.cwd()
|
WORKDIR = Path.cwd()
|
||||||
SKILLS_DIR = WORKDIR / "skills"
|
SKILLS_DIR = WORKDIR / "skills"
|
||||||
TASKS_DIR = WORKDIR / ".tasks"; TASKS_DIR.mkdir(exist_ok=True)
|
|
||||||
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
||||||
MODEL = os.environ["MODEL_ID"]
|
MODEL = os.environ["MODEL_ID"]
|
||||||
|
CURRENT_TODOS: list[dict] = []
|
||||||
|
|
||||||
# s07: Skill catalog scan (used by build_system below)
|
# s07: Skill catalog scan (used by build_system below)
|
||||||
def _parse_frontmatter(text: str) -> tuple[dict, str]:
|
def _parse_frontmatter(text: str) -> tuple[dict, str]:
|
||||||
@@ -169,19 +169,19 @@ def run_glob(pattern: str) -> str:
|
|||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
|
||||||
def run_todo_write(todos: list) -> str:
|
def run_todo_write(todos: list) -> str:
|
||||||
|
global CURRENT_TODOS
|
||||||
for i, t in enumerate(todos):
|
for i, t in enumerate(todos):
|
||||||
if "content" not in t or "status" not in t:
|
if "content" not in t or "status" not in t:
|
||||||
return f"Error: todos[{i}] missing 'content' or 'status'"
|
return f"Error: todos[{i}] missing 'content' or 'status'"
|
||||||
if t["status"] not in ("pending", "in_progress", "completed"):
|
if t["status"] not in ("pending", "in_progress", "completed"):
|
||||||
return f"Error: todos[{i}] has invalid status '{t['status']}'"
|
return f"Error: todos[{i}] has invalid status '{t['status']}'"
|
||||||
tasks_file = TASKS_DIR / "current_todos.json"
|
CURRENT_TODOS = todos
|
||||||
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
|
|
||||||
lines = ["\n\033[33m## Current Tasks\033[0m"]
|
lines = ["\n\033[33m## Current Tasks\033[0m"]
|
||||||
for t in todos:
|
for t in CURRENT_TODOS:
|
||||||
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
|
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
|
||||||
lines.append(f" [{icon}] {t['content']}")
|
lines.append(f" [{icon}] {t['content']}")
|
||||||
print("\n".join(lines))
|
print("\n".join(lines))
|
||||||
return f"Updated {len(todos)} tasks"
|
return f"Updated {len(CURRENT_TODOS)} tasks"
|
||||||
|
|
||||||
def extract_text(content) -> str:
|
def extract_text(content) -> str:
|
||||||
if not isinstance(content, list):
|
if not isinstance(content, list):
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ WORKDIR = Path.cwd()
|
|||||||
SKILLS_DIR = WORKDIR / "skills"
|
SKILLS_DIR = WORKDIR / "skills"
|
||||||
TRANSCRIPT_DIR = WORKDIR / ".transcripts"
|
TRANSCRIPT_DIR = WORKDIR / ".transcripts"
|
||||||
TOOL_RESULTS_DIR = WORKDIR / ".task_outputs" / "tool-results"
|
TOOL_RESULTS_DIR = WORKDIR / ".task_outputs" / "tool-results"
|
||||||
TASKS_DIR = WORKDIR / ".tasks"; TASKS_DIR.mkdir(exist_ok=True)
|
|
||||||
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
||||||
MODEL = os.environ["MODEL_ID"]
|
MODEL = os.environ["MODEL_ID"]
|
||||||
|
CURRENT_TODOS: list[dict] = []
|
||||||
|
|
||||||
# s07: Skill catalog scan (inherited from s07)
|
# s07: Skill catalog scan (inherited from s07)
|
||||||
def _parse_frontmatter(text: str) -> tuple[dict, str]:
|
def _parse_frontmatter(text: str) -> tuple[dict, str]:
|
||||||
@@ -166,19 +166,19 @@ def run_glob(pattern: str) -> str:
|
|||||||
except Exception as e: return f"Error: {e}"
|
except Exception as e: return f"Error: {e}"
|
||||||
|
|
||||||
def run_todo_write(todos: list) -> str:
|
def run_todo_write(todos: list) -> str:
|
||||||
|
global CURRENT_TODOS
|
||||||
for i, t in enumerate(todos):
|
for i, t in enumerate(todos):
|
||||||
if "content" not in t or "status" not in t:
|
if "content" not in t or "status" not in t:
|
||||||
return f"Error: todos[{i}] missing 'content' or 'status'"
|
return f"Error: todos[{i}] missing 'content' or 'status'"
|
||||||
if t["status"] not in ("pending", "in_progress", "completed"):
|
if t["status"] not in ("pending", "in_progress", "completed"):
|
||||||
return f"Error: todos[{i}] has invalid status '{t['status']}'"
|
return f"Error: todos[{i}] has invalid status '{t['status']}'"
|
||||||
tasks_file = TASKS_DIR / "current_todos.json"
|
CURRENT_TODOS = todos
|
||||||
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
|
|
||||||
lines = ["\n\033[33m## Current Tasks\033[0m"]
|
lines = ["\n\033[33m## Current Tasks\033[0m"]
|
||||||
for t in todos:
|
for t in CURRENT_TODOS:
|
||||||
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
|
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
|
||||||
lines.append(f" [{icon}] {t['content']}")
|
lines.append(f" [{icon}] {t['content']}")
|
||||||
print("\n".join(lines))
|
print("\n".join(lines))
|
||||||
return f"Updated {len(todos)} tasks"
|
return f"Updated {len(CURRENT_TODOS)} tasks"
|
||||||
|
|
||||||
def extract_text(content) -> str:
|
def extract_text(content) -> str:
|
||||||
if not isinstance(content, list): return str(content)
|
if not isinstance(content, list): return str(content)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ The agent receives a project: set up a database, write APIs, add tests. It uses
|
|||||||
|
|
||||||
You can't build the roof before laying the foundation. Tasks have ordering. Task dependencies should form a Directed Acyclic Graph (DAG); the teaching version only demonstrates `blockedBy` checking, without cycle detection.
|
You can't build the roof before laying the foundation. Tasks have ordering. Task dependencies should form a Directed Acyclic Graph (DAG); the teaching version only demonstrates `blockedBy` checking, without cycle detection.
|
||||||
|
|
||||||
s05's TodoWrite is a list. No dependencies, no persistence — when the conversation ends, the list is gone. What you need is a **task system**: each task is a JSON file, tasks have `blockedBy` dependencies, and they persist across sessions on disk.
|
s05's TodoWrite is an execution checklist for the current task, kept in session memory. What you need here is a **task system**: each task is a JSON file, tasks have `blockedBy` dependencies, and they persist across sessions on disk.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,11 +30,13 @@ TodoWrite vs Task System:
|
|||||||
|
|
||||||
| | TodoWrite (s05) | Task System (s12) |
|
| | TodoWrite (s05) | Task System (s12) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Storage | In-memory list | `.tasks/` JSON files |
|
| Role | Execution checklist for the current task | Recoverable task system |
|
||||||
| Dependencies | None | `blockedBy` dependency graph |
|
| Storage | In-process / session state | `.tasks/{id}.json` |
|
||||||
| Persistence | Lost when conversation ends | Cross-session |
|
| Dependencies | None | `blockedBy` / `blocks` graph |
|
||||||
| Multi-agent | None | `owner` field |
|
| Lifecycle | Current session / current task | Cross-session |
|
||||||
| Status | checked / unchecked | pending → in_progress → completed |
|
| 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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Agent がプロジェクトを受けた:データベース構築、API 実装
|
|||||||
|
|
||||||
屋根を先に建てて基礎を後から打つことはできない。タスクには順序がある。タスクの依存関係は有向非巡回グラフ(DAG)を形成すべき;教学版は `blockedBy` チェックのみをデモし、循環検出は実装していない。
|
屋根を先に建てて基礎を後から打つことはできない。タスクには順序がある。タスクの依存関係は有向非巡回グラフ(DAG)を形成すべき;教学版は `blockedBy` チェックのみをデモし、循環検出は実装していない。
|
||||||
|
|
||||||
s05 の TodoWrite はリスト。依存関係も永続化もなく、会話が終わればリストも消える。必要なのは**タスクシステム**:各タスクは JSON ファイル、タスク間に `blockedBy` 依存関係、ディスク上でセッションをまたいで永続化。
|
s05 の TodoWrite は現在のタスクの実行チェックリストで、セッションメモリに保持される。ここで必要なのは**タスクシステム**:各タスクは JSON ファイル、タスク間に `blockedBy` 依存関係、ディスク上でセッションをまたいで永続化。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,11 +30,13 @@ TodoWrite vs Task System:
|
|||||||
|
|
||||||
| | TodoWrite (s05) | Task System (s12) |
|
| | TodoWrite (s05) | Task System (s12) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| ストレージ | メモリ内リスト | `.tasks/` JSON ファイル |
|
| 位置づけ | 現在のタスクの実行チェックリスト | 復旧可能なタスクシステム |
|
||||||
| 依存関係 | なし | `blockedBy` 依存グラフ |
|
| ストレージ | プロセス内 / セッション状態 | `.tasks/{id}.json` |
|
||||||
| 永続性 | 会話終了で消失 | セッション横断 |
|
| 依存関係 | なし | `blockedBy` / `blocks` グラフ |
|
||||||
| マルチ Agent | なし | `owner` フィールド |
|
| ライフサイクル | 現在のセッション / 現在のタスク | セッション横断 |
|
||||||
| ステータス | checked / unchecked | pending → in_progress → completed |
|
| 分担 | タスク認識を扱わない | `owner` / claim |
|
||||||
|
| ステータス | pending / in_progress / completed | pending / in_progress / completed |
|
||||||
|
| 粒度 | Agent 自身の手順 | 認識・追跡・アンロックできるタスク |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Agent 接到一个项目:搭数据库、写 API、加测试。它用 s05 的 T
|
|||||||
|
|
||||||
盖房子不能先盖屋顶再打地基。任务之间有先后。任务依赖应该形成有向无环图(DAG);教学版只演示 `blockedBy` 检查,没有实现环检测。
|
盖房子不能先盖屋顶再打地基。任务之间有先后。任务依赖应该形成有向无环图(DAG);教学版只演示 `blockedBy` 检查,没有实现环检测。
|
||||||
|
|
||||||
s05 的 TodoWrite 是一个列表。没有依赖关系、没有持久化、对话结束列表就没了。你需要的是**任务系统**:每个任务是一个 JSON 文件,任务之间有 `blockedBy` 依赖,跨会话持久化在磁盘上。
|
s05 的 TodoWrite 是当前任务的执行清单,保存在会话内存中。这里需要的是**任务系统**:每个任务是一个 JSON 文件,任务之间有 `blockedBy` 依赖,跨会话持久化在磁盘上。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,11 +30,13 @@ TodoWrite vs Task System:
|
|||||||
|
|
||||||
| | TodoWrite (s05) | Task System (s12) |
|
| | TodoWrite (s05) | Task System (s12) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 存储 | 内存列表 | `.tasks/` JSON 文件 |
|
| 定位 | 当前任务的执行清单 | 可恢复的任务系统 |
|
||||||
| 依赖 | 无 | `blockedBy` 依赖图 |
|
| 存储 | 进程内 / 会话状态 | `.tasks/{id}.json` |
|
||||||
| 持久性 | 对话结束即丢 | 跨会话 |
|
| 依赖 | 无 | `blockedBy` / `blocks` 依赖图 |
|
||||||
| 多 Agent | 无 | `owner` 字段 |
|
| 生命周期 | 当前会话 / 当前任务 | 跨会话保留 |
|
||||||
| 状态 | checked / unchecked | pending → in_progress → completed |
|
| 分工 | 不负责任务认领 | `owner` / claim |
|
||||||
|
| 状态 | pending / in_progress / completed | pending / in_progress / completed |
|
||||||
|
| 粒度 | Agent 自己的步骤 | 可被认领、追踪、解锁的任务 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -277,4 +279,4 @@ CC 的任务系统有四个工具(不是教学版的一个通用 Task 工具
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<!-- translation-sync: zh@v1, en@v0, ja@v0 -->
|
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ That means permission, logging, and audit logic all attach to the same hook poin
|
|||||||
|
|
||||||
S20 keeps two planning layers:
|
S20 keeps two planning layers:
|
||||||
|
|
||||||
- `todo_write`: lightweight plan for the current session, written to `.tasks/current_todos.json`
|
- `todo_write`: lightweight plan for the current session, kept in memory
|
||||||
- task graph: cross-session, dependency-aware, claimable task files under `.tasks/task_*.json`
|
- task graph: cross-session, dependency-aware, claimable task files under `.tasks/task_*.json`
|
||||||
|
|
||||||
The first keeps a single agent from drifting. The second supports team coordination.
|
The first keeps a single agent from drifting. The second supports team coordination.
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ if blocked:
|
|||||||
|
|
||||||
S20 には 2 層の plan がある:
|
S20 には 2 層の plan がある:
|
||||||
|
|
||||||
- `todo_write`: current session 用の軽量 plan。`.tasks/current_todos.json` に保存。
|
- `todo_write`: current session 用の軽量 plan。メモリに保持。
|
||||||
- task graph: cross-session、dependency-aware、claimable な task file。`.tasks/task_*.json` に保存。
|
- task graph: cross-session、dependency-aware、claimable な task file。`.tasks/task_*.json` に保存。
|
||||||
|
|
||||||
前者は単独 agent の drift を防ぐ。後者は team coordination の土台になる。
|
前者は単独 agent の drift を防ぐ。後者は team coordination の土台になる。
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ if blocked:
|
|||||||
|
|
||||||
S20 同时保留两层计划:
|
S20 同时保留两层计划:
|
||||||
|
|
||||||
- `todo_write`:当前会话内的轻量计划,写入 `.tasks/current_todos.json`
|
- `todo_write`:当前会话内的轻量计划,保存在内存中
|
||||||
- task graph:跨会话、可依赖、可认领的任务文件,写入 `.tasks/task_*.json`
|
- task graph:跨会话、可依赖、可认领的任务文件,写入 `.tasks/task_*.json`
|
||||||
|
|
||||||
前者帮助单个 Agent 不漂移;后者支撑团队协作。
|
前者帮助单个 Agent 不漂移;后者支撑团队协作。
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ def terminal_print(text: str):
|
|||||||
# worktrees, and teammates on top of this same file-backed state.
|
# worktrees, and teammates on top of this same file-backed state.
|
||||||
TASKS_DIR = WORKDIR / ".tasks"
|
TASKS_DIR = WORKDIR / ".tasks"
|
||||||
TASKS_DIR.mkdir(exist_ok=True)
|
TASKS_DIR.mkdir(exist_ok=True)
|
||||||
|
CURRENT_TODOS: list[dict] = []
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -457,15 +458,15 @@ def call_tool_handler(handler, args: dict, name: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def run_todo_write(todos: list) -> str:
|
def run_todo_write(todos: list) -> str:
|
||||||
|
global CURRENT_TODOS
|
||||||
for i, todo in enumerate(todos):
|
for i, todo in enumerate(todos):
|
||||||
if "content" not in todo or "status" not in todo:
|
if "content" not in todo or "status" not in todo:
|
||||||
return f"Error: todos[{i}] missing 'content' or 'status'"
|
return f"Error: todos[{i}] missing 'content' or 'status'"
|
||||||
if todo["status"] not in ("pending", "in_progress", "completed"):
|
if todo["status"] not in ("pending", "in_progress", "completed"):
|
||||||
return f"Error: todos[{i}] has invalid status '{todo['status']}'"
|
return f"Error: todos[{i}] has invalid status '{todo['status']}'"
|
||||||
path = TASKS_DIR / "current_todos.json"
|
CURRENT_TODOS = todos
|
||||||
path.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
|
print(f" \033[33m[todo] updated {len(CURRENT_TODOS)} item(s)\033[0m")
|
||||||
print(f" \033[33m[todo] updated {len(todos)} item(s)\033[0m")
|
return f"Updated {len(CURRENT_TODOS)} todos"
|
||||||
return f"Updated {len(todos)} todos"
|
|
||||||
|
|
||||||
|
|
||||||
# ── MessageBus ──
|
# ── MessageBus ──
|
||||||
|
|||||||
Reference in New Issue
Block a user