add worktree & up task、teammate etc

This commit is contained in:
CrazyBoyM 2026-02-24 01:44:44 +08:00
parent c6a27ef1d7
commit aea8844bac
54 changed files with 2404 additions and 210 deletions

View File

@ -1,4 +1,4 @@
# Learn Claude Code -- AI Agent をゼロから構築する # Learn Claude Code -- 0 から 1 へ構築する nano Claude Code-like agent
[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) [English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md)
@ -17,16 +17,16 @@
loop back -----------------> messages[] loop back -----------------> messages[]
これだけだ。すべての AI コーディングエージェントはこのループ これは最小ループだ。すべての AI コーディングエージェントに必要な土台になる
他はすべて改良に過ぎない 本番のエージェントには、ポリシー・権限・ライフサイクル層が追加される
``` ```
**11 の段階的セッション、シンプルなループから完全な自律チームまで。** **12 の段階的セッション、シンプルなループから分離された自律実行まで。**
**各セッションは1つのメカニズムを追加する。各メカニズムには1つのモットーがある。** **各セッションは1つのメカニズムを追加する。各メカニズムには1つのモットーがある。**
> **s01**   *"Bash があれば十分"* — 1つのツール + 1つのループ = エージェント > **s01**   *"Bash があれば十分"* — 1つのツール + 1つのループ = エージェント
> >
> **s02**   *"ループは変わらない"* — ツール追加はハンドラー追加であり、ロジック追加ではない > **s02**   *"ループは変わらない"* — ツール追加はハンドラー追加であり、ループの作り直しではない
> >
> **s03**   *"行動する前に計画せよ"* — 可視化された計画がタスク完了率を向上させる > **s03**   *"行動する前に計画せよ"* — 可視化された計画がタスク完了率を向上させる
> >
@ -45,6 +45,8 @@
> **s10**   *"同じ request_id、2つのプロトコル"* — 1つの FSM パターンでシャットダウン + プラン承認 > **s10**   *"同じ request_id、2つのプロトコル"* — 1つの FSM パターンでシャットダウン + プラン承認
> >
> **s11**   *"ポーリング、クレーム、作業、繰り返し"* — コーディネーター不要、エージェントが自己組織化 > **s11**   *"ポーリング、クレーム、作業、繰り返し"* — コーディネーター不要、エージェントが自己組織化
>
> **s12**   *"ディレクトリで分離し、タスクIDで調整する"* — タスクボード + 必要時の worktree レーン
--- ---
@ -77,6 +79,19 @@ def agent_loop(messages):
各セッションはこのループの上に1つのメカニズムを重ねる -- ループ自体は変わらない。 各セッションはこのループの上に1つのメカニズムを重ねる -- ループ自体は変わらない。
## スコープ (重要)
このリポジトリは、nano Claude Code-like agent を 0->1 で構築・学習するための教材プロジェクトです。
学習を優先するため、以下の本番メカニズムは意図的に簡略化または省略しています。
- 完全なイベント / Hook バス (例: PreToolUse, SessionStart/End, ConfigChange)。
s12 では教材用に最小の追記型ライフサイクルイベントのみ実装している。
- ルールベースの権限ガバナンスと信頼フロー
- セッションライフサイクル制御 (resume/fork) と高度な worktree ライフサイクル制御
- MCP ランタイムの詳細 (transport/OAuth/リソース購読/ポーリング)
このリポジトリの JSONL メールボックス方式は教材用の実装であり、特定の本番内部実装を主張するものではありません。
## クイックスタート ## クイックスタート
```sh ```sh
@ -87,6 +102,7 @@ cp .env.example .env # .env を編集して ANTHROPIC_API_KEY を入力
python agents/s01_agent_loop.py # ここから開始 python agents/s01_agent_loop.py # ここから開始
python agents/s11_autonomous_agents.py # 完全自律チーム python agents/s11_autonomous_agents.py # 完全自律チーム
python agents/s12_worktree_task_isolation.py # Task 対応の worktree 分離
``` ```
### Web プラットフォーム ### Web プラットフォーム
@ -124,6 +140,9 @@ s08 バックグラウンドタスク [6] s10 チームプロトコル
| |
s11 自律エージェント [14] s11 自律エージェント [14]
アイドルサイクル + 自動クレーム アイドルサイクル + 自動クレーム
|
s12 Worktree 分離 [16]
タスク調整 + 必要時の分離実行レーン
[N] = ツール数 [N] = ツール数
``` ```
@ -133,7 +152,7 @@ s08 バックグラウンドタスク [6] s10 チームプロトコル
``` ```
learn-claude-code/ learn-claude-code/
| |
|-- agents/ # Python リファレンス実装 (s01-s11 + 完全版) |-- agents/ # Python リファレンス実装 (s01-s12 + 完全版)
|-- docs/{en,zh,ja}/ # メンタルモデル優先のドキュメント (3言語) |-- docs/{en,zh,ja}/ # メンタルモデル優先のドキュメント (3言語)
|-- web/ # インタラクティブ学習プラットフォーム (Next.js) |-- web/ # インタラクティブ学習プラットフォーム (Next.js)
|-- skills/ # s05 の Skill ファイル |-- skills/ # s05 の Skill ファイル
@ -158,6 +177,7 @@ learn-claude-code/
| [s09](./docs/ja/s09-agent-teams.md) | エージェントチーム | *追記で送信、排出で読取* | | [s09](./docs/ja/s09-agent-teams.md) | エージェントチーム | *追記で送信、排出で読取* |
| [s10](./docs/ja/s10-team-protocols.md) | チームプロトコル | *同じ request_id、2つのプロトコル* | | [s10](./docs/ja/s10-team-protocols.md) | チームプロトコル | *同じ request_id、2つのプロトコル* |
| [s11](./docs/ja/s11-autonomous-agents.md) | 自律エージェント | *ポーリング、クレーム、作業、繰り返し* | | [s11](./docs/ja/s11-autonomous-agents.md) | 自律エージェント | *ポーリング、クレーム、作業、繰り返し* |
| [s12](./docs/ja/s12-worktree-task-isolation.md) | Worktree + タスク分離 | *ディレクトリで分離し、タスクIDで調整する* |
## ライセンス ## ライセンス
@ -165,4 +185,4 @@ MIT
--- ---
**モデルがエージェントだ。私たちの仕事はツールを渡して、邪魔をしないこと。** **モデルがエージェントだ。私たちの仕事はツールを与えて邪魔しないこと。**

View File

@ -1,4 +1,4 @@
# Learn Claude Code -- 从零构建 AI Agent # Learn Claude Code -- 从 0 到 1 构建 nano Claude Code-like agent
[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) [English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md)
@ -17,16 +17,16 @@
loop back -----------------> messages[] loop back -----------------> messages[]
就这些。每个 AI 编程 Agent 都是这个循环。 这是最小循环。每个 AI 编程 Agent 都需要这个循环。
其他一切都是优化 生产级 Agent 还会叠加策略、权限与生命周期层
``` ```
**11 个递进式课程, 从简单循环到完整的自治团队。** **12 个递进式课程, 从简单循环到隔离化的自治执行。**
**每个课程添加一个机制。每个机制有一句格言。** **每个课程添加一个机制。每个机制有一句格言。**
> **s01**   *"Bash 就够了"* — 一个工具 + 一个循环 = 一个智能体 > **s01**   *"Bash 就够了"* — 一个工具 + 一个循环 = 一个智能体
> >
> **s02**   *"循环没有变"* — 加工具就是加 handler, 不是加逻辑 > **s02**   *"循环没有变"* — 加工具就是加 handler, 不是重写循环
> >
> **s03**   *"先计划再行动"* — 可见的计划提升任务完成率 > **s03**   *"先计划再行动"* — 可见的计划提升任务完成率
> >
@ -45,6 +45,8 @@
> **s10**   *"同一个 request_id, 两个协议"* — 一个 FSM 模式驱动关机 + 计划审批 > **s10**   *"同一个 request_id, 两个协议"* — 一个 FSM 模式驱动关机 + 计划审批
> >
> **s11**   *"轮询, 认领, 工作, 重复"* — 无需协调者, 智能体自组织 > **s11**   *"轮询, 认领, 工作, 重复"* — 无需协调者, 智能体自组织
>
> **s12**   *"目录隔离, 任务 ID 协调"* — 任务板协调 + 按需 worktree 隔离通道
--- ---
@ -77,6 +79,19 @@ def agent_loop(messages):
每个课程在这个循环之上叠加一个机制 -- 循环本身始终不变。 每个课程在这个循环之上叠加一个机制 -- 循环本身始终不变。
## 范围说明 (重要)
本仓库是一个 0->1 的学习型项目,用于从零构建 nano Claude Code-like agent。
为保证学习路径清晰,仓库有意简化或省略了部分生产机制:
- 完整事件 / Hook 总线 (例如 PreToolUse、SessionStart/End、ConfigChange)。
s12 仅提供教学用途的最小 append-only 生命周期事件流。
- 基于规则的权限治理与信任流程
- 会话生命周期控制 (resume/fork) 与更完整的 worktree 生命周期控制
- 完整 MCP 运行时细节 (transport/OAuth/资源订阅/轮询)
仓库中的团队 JSONL 邮箱协议是教学实现,不是对任何特定生产内部实现的声明。
## 快速开始 ## 快速开始
```sh ```sh
@ -87,6 +102,7 @@ cp .env.example .env # 编辑 .env 填入你的 ANTHROPIC_API_KEY
python agents/s01_agent_loop.py # 从这里开始 python agents/s01_agent_loop.py # 从这里开始
python agents/s11_autonomous_agents.py # 完整自治团队 python agents/s11_autonomous_agents.py # 完整自治团队
python agents/s12_worktree_task_isolation.py # Task 感知的 worktree 隔离
``` ```
### Web 平台 ### Web 平台
@ -124,6 +140,9 @@ s08 后台任务 [6] s10 团队协议 [12]
| |
s11 自治智能体 [14] s11 自治智能体 [14]
空闲轮询 + 自动认领 空闲轮询 + 自动认领
|
s12 Worktree 隔离 [16]
任务协调 + 按需隔离执行通道
[N] = 工具数量 [N] = 工具数量
``` ```
@ -133,7 +152,7 @@ s08 后台任务 [6] s10 团队协议 [12]
``` ```
learn-claude-code/ learn-claude-code/
| |
|-- agents/ # Python 参考实现 (s01-s11 + 完整版) |-- agents/ # Python 参考实现 (s01-s12 + 完整版)
|-- docs/{en,zh,ja}/ # 心智模型优先的文档 (3 种语言) |-- docs/{en,zh,ja}/ # 心智模型优先的文档 (3 种语言)
|-- web/ # 交互式学习平台 (Next.js) |-- web/ # 交互式学习平台 (Next.js)
|-- skills/ # s05 的 Skill 文件 |-- skills/ # s05 的 Skill 文件
@ -158,6 +177,7 @@ learn-claude-code/
| [s09](./docs/zh/s09-agent-teams.md) | 智能体团队 | *追加即发送, 排空即读取* | | [s09](./docs/zh/s09-agent-teams.md) | 智能体团队 | *追加即发送, 排空即读取* |
| [s10](./docs/zh/s10-team-protocols.md) | 团队协议 | *同一个 request_id, 两个协议* | | [s10](./docs/zh/s10-team-protocols.md) | 团队协议 | *同一个 request_id, 两个协议* |
| [s11](./docs/zh/s11-autonomous-agents.md) | 自治智能体 | *轮询, 认领, 工作, 重复* | | [s11](./docs/zh/s11-autonomous-agents.md) | 自治智能体 | *轮询, 认领, 工作, 重复* |
| [s12](./docs/zh/s12-worktree-task-isolation.md) | Worktree + 任务隔离 | *目录隔离, 任务 ID 协调* |
## 许可证 ## 许可证
@ -165,4 +185,4 @@ MIT
--- ---
**模型就是智能体。我们的工作是给它工具, 然后让开。** **模型就是智能体。我们的工作是给它工具, 然后让开。**

View File

@ -1,4 +1,4 @@
# Learn Claude Code -- Build an AI Agent From Scratch # Learn Claude Code -- A nano Claude Code-like agent, built from 0 to 1
[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) [English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md)
@ -17,16 +17,16 @@
loop back -----------------> messages[] loop back -----------------> messages[]
That's it. Every AI coding agent is this loop. That's the minimal loop. Every AI coding agent needs this loop.
Everything else is refinement. Production agents add policy, permissions, and lifecycle layers.
``` ```
**11 progressive sessions, from a simple loop to full autonomous teams.** **12 progressive sessions, from a simple loop to isolated autonomous execution.**
**Each session adds one mechanism. Each mechanism has one motto.** **Each session adds one mechanism. Each mechanism has one motto.**
> **s01**   *"Bash is all you need"* — one tool + one loop = an agent > **s01**   *"Bash is all you need"* — one tool + one loop = an agent
> >
> **s02**   *"The loop didn't change"* — adding tools means adding handlers, not logic > **s02**   *"The loop didn't change"* — adding tools means adding handlers, not rewriting the loop
> >
> **s03**   *"Plan before you act"* — visible plans improve task completion > **s03**   *"Plan before you act"* — visible plans improve task completion
> >
@ -45,6 +45,8 @@
> **s10**   *"Same request_id, two protocols"* — one FSM pattern powers shutdown + plan approval > **s10**   *"Same request_id, two protocols"* — one FSM pattern powers shutdown + plan approval
> >
> **s11**   *"Poll, claim, work, repeat"* — no coordinator needed, agents self-organize > **s11**   *"Poll, claim, work, repeat"* — no coordinator needed, agents self-organize
>
> **s12**   *"Isolate by directory, coordinate by task ID"* — task board + optional worktree lanes
--- ---
@ -77,6 +79,19 @@ def agent_loop(messages):
Every session layers one mechanism on top of this loop -- without changing the loop itself. Every session layers one mechanism on top of this loop -- without changing the loop itself.
## Scope (Important)
This repository is a 0->1 learning project for building a nano Claude Code-like agent.
It intentionally simplifies or omits several production mechanisms:
- Full event/hook buses (for example PreToolUse, SessionStart/End, ConfigChange).
s12 includes only a minimal append-only lifecycle event stream for teaching.
- Rule-based permission governance and trust workflows
- Session lifecycle controls (resume/fork) and advanced worktree lifecycle controls
- Full MCP runtime details (transport/OAuth/resource subscribe/polling)
Treat the team JSONL mailbox protocol in this repo as a teaching implementation, not a claim about any specific production internals.
## Quick Start ## Quick Start
```sh ```sh
@ -87,6 +102,7 @@ cp .env.example .env # Edit .env with your ANTHROPIC_API_KEY
python agents/s01_agent_loop.py # Start here python agents/s01_agent_loop.py # Start here
python agents/s11_autonomous_agents.py # Full autonomous team python agents/s11_autonomous_agents.py # Full autonomous team
python agents/s12_worktree_task_isolation.py # Task-aware worktree isolation
``` ```
### Web Platform ### Web Platform
@ -124,6 +140,9 @@ s08 Background Tasks [6] s10 Team Protocols [12]
| |
s11 Autonomous Agents [14] s11 Autonomous Agents [14]
idle cycle + auto-claim idle cycle + auto-claim
|
s12 Worktree Isolation [16]
task coordination + optional isolated execution lanes
[N] = number of tools [N] = number of tools
``` ```
@ -133,7 +152,7 @@ s08 Background Tasks [6] s10 Team Protocols [12]
``` ```
learn-claude-code/ learn-claude-code/
| |
|-- agents/ # Python reference implementations (s01-s11 + full) |-- agents/ # Python reference implementations (s01-s12 + full)
|-- docs/{en,zh,ja}/ # Mental-model-first documentation (3 languages) |-- docs/{en,zh,ja}/ # Mental-model-first documentation (3 languages)
|-- web/ # Interactive learning platform (Next.js) |-- web/ # Interactive learning platform (Next.js)
|-- skills/ # Skill files for s05 |-- skills/ # Skill files for s05
@ -158,6 +177,7 @@ Available in [English](./docs/en/) | [中文](./docs/zh/) | [日本語](./docs/j
| [s09](./docs/en/s09-agent-teams.md) | Agent Teams | *Append to send, drain to read* | | [s09](./docs/en/s09-agent-teams.md) | Agent Teams | *Append to send, drain to read* |
| [s10](./docs/en/s10-team-protocols.md) | Team Protocols | *Same request_id, two protocols* | | [s10](./docs/en/s10-team-protocols.md) | Team Protocols | *Same request_id, two protocols* |
| [s11](./docs/en/s11-autonomous-agents.md) | Autonomous Agents | *Poll, claim, work, repeat* | | [s11](./docs/en/s11-autonomous-agents.md) | Autonomous Agents | *Poll, claim, work, repeat* |
| [s12](./docs/en/s12-worktree-task-isolation.md) | Worktree + Task Isolation | *Isolate by directory, coordinate by task ID* |
## License ## License

View File

@ -1,2 +1,2 @@
# agents/ - Python teaching agents (s01-s11) + reference agent (s_full) # agents/ - Python teaching agents (s01-s12) + reference agent (s_full)
# Each file is self-contained and runnable: python agents/s01_agent_loop.py # Each file is self-contained and runnable: python agents/s01_agent_loop.py

View File

@ -2,7 +2,7 @@
""" """
s01_agent_loop.py - The Agent Loop s01_agent_loop.py - The Agent Loop
The entire secret of coding agents in one pattern: The entire secret of an AI coding agent in one pattern:
while stop_reason == "tool_use": while stop_reason == "tool_use":
response = LLM(messages, tools) response = LLM(messages, tools)
@ -18,8 +18,9 @@ The entire secret of coding agents in one pattern:
+---------------+ +---------------+
(loop continues) (loop continues)
That's it. The ENTIRE agent is a while loop that feeds tool This is the core loop: feed tool results back to the model
results back to the model until the model decides to stop. until the model decides to stop. Production agents layer
policy, hooks, and lifecycle controls on top.
""" """
import os import os

View File

@ -0,0 +1,775 @@
#!/usr/bin/env python3
"""
s12_worktree_task_isolation.py - Worktree + Task Isolation
Directory-level isolation for parallel task execution.
Tasks are the control plane and worktrees are the execution plane.
.tasks/task_12.json
{
"id": 12,
"subject": "Implement auth refactor",
"status": "in_progress",
"worktree": "auth-refactor"
}
.worktrees/index.json
{
"worktrees": [
{
"name": "auth-refactor",
"path": ".../.worktrees/auth-refactor",
"branch": "wt/auth-refactor",
"task_id": 12,
"status": "active"
}
]
}
Key insight: "Isolate by directory, coordinate by task ID."
"""
import json
import os
import re
import subprocess
import time
from pathlib import Path
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
WORKDIR = Path.cwd()
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
def detect_repo_root(cwd: Path) -> Path | None:
"""Return git repo root if cwd is inside a repo, else None."""
try:
r = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
cwd=cwd,
capture_output=True,
text=True,
timeout=10,
)
if r.returncode != 0:
return None
root = Path(r.stdout.strip())
return root if root.exists() else None
except Exception:
return None
REPO_ROOT = detect_repo_root(WORKDIR) or WORKDIR
SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
"Use task + worktree tools for multi-task work. "
"For parallel or risky changes: create tasks, allocate worktree lanes, "
"run commands in those lanes, then choose keep/remove for closeout. "
"Use worktree_events when you need lifecycle visibility."
)
# -- EventBus: append-only lifecycle events for observability --
class EventBus:
def __init__(self, event_log_path: Path):
self.path = event_log_path
self.path.parent.mkdir(parents=True, exist_ok=True)
if not self.path.exists():
self.path.write_text("")
def emit(
self,
event: str,
task: dict | None = None,
worktree: dict | None = None,
error: str | None = None,
):
payload = {
"event": event,
"ts": time.time(),
"task": task or {},
"worktree": worktree or {},
}
if error:
payload["error"] = error
with self.path.open("a", encoding="utf-8") as f:
f.write(json.dumps(payload) + "\n")
def list_recent(self, limit: int = 20) -> str:
n = max(1, min(int(limit or 20), 200))
lines = self.path.read_text(encoding="utf-8").splitlines()
recent = lines[-n:]
items = []
for line in recent:
try:
items.append(json.loads(line))
except Exception:
items.append({"event": "parse_error", "raw": line})
return json.dumps(items, indent=2)
# -- TaskManager: persistent task board with optional worktree binding --
class TaskManager:
def __init__(self, tasks_dir: Path):
self.dir = tasks_dir
self.dir.mkdir(parents=True, exist_ok=True)
self._next_id = self._max_id() + 1
def _max_id(self) -> int:
ids = []
for f in self.dir.glob("task_*.json"):
try:
ids.append(int(f.stem.split("_")[1]))
except Exception:
pass
return max(ids) if ids else 0
def _path(self, task_id: int) -> Path:
return self.dir / f"task_{task_id}.json"
def _load(self, task_id: int) -> dict:
path = self._path(task_id)
if not path.exists():
raise ValueError(f"Task {task_id} not found")
return json.loads(path.read_text())
def _save(self, task: dict):
self._path(task["id"]).write_text(json.dumps(task, indent=2))
def create(self, subject: str, description: str = "") -> str:
task = {
"id": self._next_id,
"subject": subject,
"description": description,
"status": "pending",
"owner": "",
"worktree": "",
"blockedBy": [],
"created_at": time.time(),
"updated_at": time.time(),
}
self._save(task)
self._next_id += 1
return json.dumps(task, indent=2)
def get(self, task_id: int) -> str:
return json.dumps(self._load(task_id), indent=2)
def exists(self, task_id: int) -> bool:
return self._path(task_id).exists()
def update(self, task_id: int, status: str = None, owner: str = None) -> str:
task = self._load(task_id)
if status:
if status not in ("pending", "in_progress", "completed"):
raise ValueError(f"Invalid status: {status}")
task["status"] = status
if owner is not None:
task["owner"] = owner
task["updated_at"] = time.time()
self._save(task)
return json.dumps(task, indent=2)
def bind_worktree(self, task_id: int, worktree: str, owner: str = "") -> str:
task = self._load(task_id)
task["worktree"] = worktree
if owner:
task["owner"] = owner
if task["status"] == "pending":
task["status"] = "in_progress"
task["updated_at"] = time.time()
self._save(task)
return json.dumps(task, indent=2)
def unbind_worktree(self, task_id: int) -> str:
task = self._load(task_id)
task["worktree"] = ""
task["updated_at"] = time.time()
self._save(task)
return json.dumps(task, indent=2)
def list_all(self) -> str:
tasks = []
for f in sorted(self.dir.glob("task_*.json")):
tasks.append(json.loads(f.read_text()))
if not tasks:
return "No tasks."
lines = []
for t in tasks:
marker = {
"pending": "[ ]",
"in_progress": "[>]",
"completed": "[x]",
}.get(t["status"], "[?]")
owner = f" owner={t['owner']}" if t.get("owner") else ""
wt = f" wt={t['worktree']}" if t.get("worktree") else ""
lines.append(f"{marker} #{t['id']}: {t['subject']}{owner}{wt}")
return "\n".join(lines)
TASKS = TaskManager(REPO_ROOT / ".tasks")
EVENTS = EventBus(REPO_ROOT / ".worktrees" / "events.jsonl")
# -- WorktreeManager: create/list/run/remove git worktrees + lifecycle index --
class WorktreeManager:
def __init__(self, repo_root: Path, tasks: TaskManager, events: EventBus):
self.repo_root = repo_root
self.tasks = tasks
self.events = events
self.dir = repo_root / ".worktrees"
self.dir.mkdir(parents=True, exist_ok=True)
self.index_path = self.dir / "index.json"
if not self.index_path.exists():
self.index_path.write_text(json.dumps({"worktrees": []}, indent=2))
self.git_available = self._is_git_repo()
def _is_git_repo(self) -> bool:
try:
r = subprocess.run(
["git", "rev-parse", "--is-inside-work-tree"],
cwd=self.repo_root,
capture_output=True,
text=True,
timeout=10,
)
return r.returncode == 0
except Exception:
return False
def _run_git(self, args: list[str]) -> str:
if not self.git_available:
raise RuntimeError("Not in a git repository. worktree tools require git.")
r = subprocess.run(
["git", *args],
cwd=self.repo_root,
capture_output=True,
text=True,
timeout=120,
)
if r.returncode != 0:
msg = (r.stdout + r.stderr).strip()
raise RuntimeError(msg or f"git {' '.join(args)} failed")
return (r.stdout + r.stderr).strip() or "(no output)"
def _load_index(self) -> dict:
return json.loads(self.index_path.read_text())
def _save_index(self, data: dict):
self.index_path.write_text(json.dumps(data, indent=2))
def _find(self, name: str) -> dict | None:
idx = self._load_index()
for wt in idx.get("worktrees", []):
if wt.get("name") == name:
return wt
return None
def _validate_name(self, name: str):
if not re.fullmatch(r"[A-Za-z0-9._-]{1,40}", name or ""):
raise ValueError(
"Invalid worktree name. Use 1-40 chars: letters, numbers, ., _, -"
)
def create(self, name: str, task_id: int = None, base_ref: str = "HEAD") -> str:
self._validate_name(name)
if self._find(name):
raise ValueError(f"Worktree '{name}' already exists in index")
if task_id is not None and not self.tasks.exists(task_id):
raise ValueError(f"Task {task_id} not found")
path = self.dir / name
branch = f"wt/{name}"
self.events.emit(
"worktree.create.before",
task={"id": task_id} if task_id is not None else {},
worktree={"name": name, "base_ref": base_ref},
)
try:
self._run_git(["worktree", "add", "-b", branch, str(path), base_ref])
entry = {
"name": name,
"path": str(path),
"branch": branch,
"task_id": task_id,
"status": "active",
"created_at": time.time(),
}
idx = self._load_index()
idx["worktrees"].append(entry)
self._save_index(idx)
if task_id is not None:
self.tasks.bind_worktree(task_id, name)
self.events.emit(
"worktree.create.after",
task={"id": task_id} if task_id is not None else {},
worktree={
"name": name,
"path": str(path),
"branch": branch,
"status": "active",
},
)
return json.dumps(entry, indent=2)
except Exception as e:
self.events.emit(
"worktree.create.failed",
task={"id": task_id} if task_id is not None else {},
worktree={"name": name, "base_ref": base_ref},
error=str(e),
)
raise
def list_all(self) -> str:
idx = self._load_index()
wts = idx.get("worktrees", [])
if not wts:
return "No worktrees in index."
lines = []
for wt in wts:
suffix = f" task={wt['task_id']}" if wt.get("task_id") else ""
lines.append(
f"[{wt.get('status', 'unknown')}] {wt['name']} -> "
f"{wt['path']} ({wt.get('branch', '-')}){suffix}"
)
return "\n".join(lines)
def status(self, name: str) -> str:
wt = self._find(name)
if not wt:
return f"Error: Unknown worktree '{name}'"
path = Path(wt["path"])
if not path.exists():
return f"Error: Worktree path missing: {path}"
r = subprocess.run(
["git", "status", "--short", "--branch"],
cwd=path,
capture_output=True,
text=True,
timeout=60,
)
text = (r.stdout + r.stderr).strip()
return text or "Clean worktree"
def run(self, name: str, command: str) -> str:
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
if any(d in command for d in dangerous):
return "Error: Dangerous command blocked"
wt = self._find(name)
if not wt:
return f"Error: Unknown worktree '{name}'"
path = Path(wt["path"])
if not path.exists():
return f"Error: Worktree path missing: {path}"
try:
r = subprocess.run(
command,
shell=True,
cwd=path,
capture_output=True,
text=True,
timeout=300,
)
out = (r.stdout + r.stderr).strip()
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (300s)"
def remove(self, name: str, force: bool = False, complete_task: bool = False) -> str:
wt = self._find(name)
if not wt:
return f"Error: Unknown worktree '{name}'"
self.events.emit(
"worktree.remove.before",
task={"id": wt.get("task_id")} if wt.get("task_id") is not None else {},
worktree={"name": name, "path": wt.get("path")},
)
try:
args = ["worktree", "remove"]
if force:
args.append("--force")
args.append(wt["path"])
self._run_git(args)
if complete_task and wt.get("task_id") is not None:
task_id = wt["task_id"]
before = json.loads(self.tasks.get(task_id))
self.tasks.update(task_id, status="completed")
self.tasks.unbind_worktree(task_id)
self.events.emit(
"task.completed",
task={
"id": task_id,
"subject": before.get("subject", ""),
"status": "completed",
},
worktree={"name": name},
)
idx = self._load_index()
for item in idx.get("worktrees", []):
if item.get("name") == name:
item["status"] = "removed"
item["removed_at"] = time.time()
self._save_index(idx)
self.events.emit(
"worktree.remove.after",
task={"id": wt.get("task_id")} if wt.get("task_id") is not None else {},
worktree={"name": name, "path": wt.get("path"), "status": "removed"},
)
return f"Removed worktree '{name}'"
except Exception as e:
self.events.emit(
"worktree.remove.failed",
task={"id": wt.get("task_id")} if wt.get("task_id") is not None else {},
worktree={"name": name, "path": wt.get("path")},
error=str(e),
)
raise
def keep(self, name: str) -> str:
wt = self._find(name)
if not wt:
return f"Error: Unknown worktree '{name}'"
idx = self._load_index()
kept = None
for item in idx.get("worktrees", []):
if item.get("name") == name:
item["status"] = "kept"
item["kept_at"] = time.time()
kept = item
self._save_index(idx)
self.events.emit(
"worktree.keep",
task={"id": wt.get("task_id")} if wt.get("task_id") is not None else {},
worktree={
"name": name,
"path": wt.get("path"),
"status": "kept",
},
)
return json.dumps(kept, indent=2) if kept else f"Error: Unknown worktree '{name}'"
WORKTREES = WorktreeManager(REPO_ROOT, TASKS, EVENTS)
# -- Base tools (kept minimal, same style as previous sessions) --
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
def run_bash(command: str) -> str:
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
if any(d in command for d in dangerous):
return "Error: Dangerous command blocked"
try:
r = subprocess.run(
command,
shell=True,
cwd=WORKDIR,
capture_output=True,
text=True,
timeout=120,
)
out = (r.stdout + r.stderr).strip()
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
def run_read(path: str, limit: int = None) -> str:
try:
lines = safe_path(path).read_text().splitlines()
if limit and limit < len(lines):
lines = lines[:limit] + [f"... ({len(lines) - limit} more)"]
return "\n".join(lines)[:50000]
except Exception as e:
return f"Error: {e}"
def run_write(path: str, content: str) -> str:
try:
fp = safe_path(path)
fp.parent.mkdir(parents=True, exist_ok=True)
fp.write_text(content)
return f"Wrote {len(content)} bytes"
except Exception as e:
return f"Error: {e}"
def run_edit(path: str, old_text: str, new_text: str) -> str:
try:
fp = safe_path(path)
c = fp.read_text()
if old_text not in c:
return f"Error: Text not found in {path}"
fp.write_text(c.replace(old_text, new_text, 1))
return f"Edited {path}"
except Exception as e:
return f"Error: {e}"
TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
"task_create": lambda **kw: TASKS.create(kw["subject"], kw.get("description", "")),
"task_list": lambda **kw: TASKS.list_all(),
"task_get": lambda **kw: TASKS.get(kw["task_id"]),
"task_update": lambda **kw: TASKS.update(kw["task_id"], kw.get("status"), kw.get("owner")),
"task_bind_worktree": lambda **kw: TASKS.bind_worktree(kw["task_id"], kw["worktree"], kw.get("owner", "")),
"worktree_create": lambda **kw: WORKTREES.create(kw["name"], kw.get("task_id"), kw.get("base_ref", "HEAD")),
"worktree_list": lambda **kw: WORKTREES.list_all(),
"worktree_status": lambda **kw: WORKTREES.status(kw["name"]),
"worktree_run": lambda **kw: WORKTREES.run(kw["name"], kw["command"]),
"worktree_keep": lambda **kw: WORKTREES.keep(kw["name"]),
"worktree_remove": lambda **kw: WORKTREES.remove(kw["name"], kw.get("force", False), kw.get("complete_task", False)),
"worktree_events": lambda **kw: EVENTS.list_recent(kw.get("limit", 20)),
}
TOOLS = [
{
"name": "bash",
"description": "Run a shell command in the current workspace (blocking).",
"input_schema": {
"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"],
},
},
{
"name": "read_file",
"description": "Read file contents.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"limit": {"type": "integer"},
},
"required": ["path"],
},
},
{
"name": "write_file",
"description": "Write content to file.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"},
},
"required": ["path", "content"],
},
},
{
"name": "edit_file",
"description": "Replace exact text in file.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"old_text": {"type": "string"},
"new_text": {"type": "string"},
},
"required": ["path", "old_text", "new_text"],
},
},
{
"name": "task_create",
"description": "Create a new task on the shared task board.",
"input_schema": {
"type": "object",
"properties": {
"subject": {"type": "string"},
"description": {"type": "string"},
},
"required": ["subject"],
},
},
{
"name": "task_list",
"description": "List all tasks with status, owner, and worktree binding.",
"input_schema": {"type": "object", "properties": {}},
},
{
"name": "task_get",
"description": "Get task details by ID.",
"input_schema": {
"type": "object",
"properties": {"task_id": {"type": "integer"}},
"required": ["task_id"],
},
},
{
"name": "task_update",
"description": "Update task status or owner.",
"input_schema": {
"type": "object",
"properties": {
"task_id": {"type": "integer"},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"],
},
"owner": {"type": "string"},
},
"required": ["task_id"],
},
},
{
"name": "task_bind_worktree",
"description": "Bind a task to a worktree name.",
"input_schema": {
"type": "object",
"properties": {
"task_id": {"type": "integer"},
"worktree": {"type": "string"},
"owner": {"type": "string"},
},
"required": ["task_id", "worktree"],
},
},
{
"name": "worktree_create",
"description": "Create a git worktree and optionally bind it to a task.",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"task_id": {"type": "integer"},
"base_ref": {"type": "string"},
},
"required": ["name"],
},
},
{
"name": "worktree_list",
"description": "List worktrees tracked in .worktrees/index.json.",
"input_schema": {"type": "object", "properties": {}},
},
{
"name": "worktree_status",
"description": "Show git status for one worktree.",
"input_schema": {
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"],
},
},
{
"name": "worktree_run",
"description": "Run a shell command in a named worktree directory.",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"command": {"type": "string"},
},
"required": ["name", "command"],
},
},
{
"name": "worktree_remove",
"description": "Remove a worktree and optionally mark its bound task completed.",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"force": {"type": "boolean"},
"complete_task": {"type": "boolean"},
},
"required": ["name"],
},
},
{
"name": "worktree_keep",
"description": "Mark a worktree as kept in lifecycle state without removing it.",
"input_schema": {
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"],
},
},
{
"name": "worktree_events",
"description": "List recent worktree/task lifecycle events from .worktrees/events.jsonl.",
"input_schema": {
"type": "object",
"properties": {"limit": {"type": "integer"}},
},
},
]
def agent_loop(messages: list):
while True:
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=messages,
tools=TOOLS,
max_tokens=8000,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
results = []
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS.get(block.name)
try:
output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
except Exception as e:
output = f"Error: {e}"
print(f"> {block.name}: {str(output)[:200]}")
results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(output),
}
)
messages.append({"role": "user", "content": results})
if __name__ == "__main__":
print(f"Repo root for s12: {REPO_ROOT}")
if not WORKTREES.git_available:
print("Note: Not in a git repo. worktree_* tools will return errors.")
history = []
while True:
try:
query = input("\033[36ms12 >> \033[0m")
except (EOFError, KeyboardInterrupt):
break
if query.strip().lower() in ("q", "exit", ""):
break
history.append({"role": "user", "content": query})
agent_loop(history)
print()

View File

@ -3,12 +3,13 @@
s_full.py - Full Reference Agent s_full.py - Full Reference Agent
Capstone implementation combining every mechanism from s01-s11. Capstone implementation combining every mechanism from s01-s11.
Session s12 (task-aware worktree isolation) is taught separately.
NOT a teaching session -- this is the "put it all together" reference. NOT a teaching session -- this is the "put it all together" reference.
+------------------------------------------------------------------+ +------------------------------------------------------------------+
| FULL AGENT | | FULL AGENT |
| | | |
| System prompt (s05 skills, s03 todo nag) | | System prompt (s05 skills, task-first + optional todo nag) |
| | | |
| Before each LLM call: | | Before each LLM call: |
| +--------------------+ +------------------+ +--------------+ | | +--------------------+ +------------------+ +--------------+ |
@ -150,6 +151,9 @@ class TodoManager:
lines.append(f"\n({done}/{len(self.items)} completed)") lines.append(f"\n({done}/{len(self.items)} completed)")
return "\n".join(lines) return "\n".join(lines)
def has_open_items(self) -> bool:
return any(item.get("status") != "completed" for item in self.items)
# === SECTION: subagent (s04) === # === SECTION: subagent (s04) ===
def run_subagent(prompt: str, agent_type: str = "Explore") -> str: def run_subagent(prompt: str, agent_type: str = "Explore") -> str:
@ -545,12 +549,10 @@ BUS = MessageBus()
TEAM = TeammateManager(BUS, TASK_MGR) TEAM = TeammateManager(BUS, TASK_MGR)
# === SECTION: system_prompt === # === SECTION: system_prompt ===
SYSTEM = f"""You are a coding agent at {WORKDIR}. SYSTEM = f"""You are a coding agent at {WORKDIR}. Use tools to solve tasks.
Use tools to solve tasks. Use TodoWrite for multi-step work. Prefer task_create/task_update/task_list for multi-step work. Use TodoWrite for short checklists.
Use task for subagent delegation. Use load_skill for specialized knowledge. Use task for subagent delegation. Use load_skill for specialized knowledge.
Skills: {SKILLS.descriptions()}"""
Skills available:
{SKILLS.descriptions()}"""
# === SECTION: shutdown_protocol (s10) === # === SECTION: shutdown_protocol (s10) ===
@ -692,9 +694,9 @@ def agent_loop(messages: list):
results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)}) results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)})
if block.name == "TodoWrite": if block.name == "TodoWrite":
used_todo = True used_todo = True
# s03: nag reminder # s03: nag reminder (only when todo workflow is active)
rounds_without_todo = 0 if used_todo else rounds_without_todo + 1 rounds_without_todo = 0 if used_todo else rounds_without_todo + 1
if rounds_without_todo >= 3: if TODO.has_open_items() and rounds_without_todo >= 3:
results.insert(0, {"type": "text", "text": "<reminder>Update your todos.</reminder>"}) results.insert(0, {"type": "text", "text": "<reminder>Update your todos.</reminder>"})
messages.append({"role": "user", "content": results}) messages.append({"role": "user", "content": results})
# s06: manual compress # s06: manual compress

View File

@ -1,6 +1,6 @@
# s01: The Agent Loop # s01: The Agent Loop
> The entire secret of AI coding agents is a while loop that feeds tool results back to the model until the model decides to stop. > The core of a coding agent is a while loop that feeds tool results back to the model until the model decides to stop.
## The Problem ## The Problem
@ -59,7 +59,8 @@ messages.append({"role": "assistant", "content": response.content})
``` ```
4. We check the stop reason. If the model did not call a tool, the loop 4. We check the stop reason. If the model did not call a tool, the loop
ends. This is the only exit condition. ends. In this minimal lesson implementation, this is the only loop exit
condition.
```python ```python
if response.stop_reason != "tool_use": if response.stop_reason != "tool_use":
@ -126,7 +127,7 @@ This is session 1 -- the starting point. There is no prior session.
## Design Rationale ## Design Rationale
This loop is the universal foundation of all LLM-based agents. Production implementations add error handling, token counting, streaming, and retry logic, but the fundamental structure is unchanged. The simplicity is the point: one exit condition (`stop_reason != "tool_use"`) controls the entire flow. Everything else in this course -- tools, planning, compression, teams -- layers on top of this loop without modifying it. Understanding this loop means understanding every agent. This loop is the foundation of LLM-based agents. Production implementations add error handling, token counting, streaming, retry logic, permission policy, and lifecycle orchestration, but the core interaction pattern still starts here. The simplicity is the point for this session: in this minimal implementation, one exit condition (`stop_reason != "tool_use"`) controls the flow we need to learn first. Everything else in this course layers on top of this loop. Understanding this loop gives you the base model, not the full production architecture.
## Try It ## Try It

View File

@ -1,6 +1,6 @@
# s02: Tools # s02: Tools
> A dispatch map routes tool calls to handler functions -- the loop itself does not change at all. > A dispatch map routes tool calls to handler functions. The loop stays identical.
## The Problem ## The Problem
@ -133,7 +133,7 @@ def agent_loop(messages: list):
## Design Rationale ## Design Rationale
The dispatch map pattern scales linearly -- adding a tool means adding one handler and one schema entry. The loop never changes. This separation of concerns (loop vs handlers) is why agent frameworks can support dozens of tools without increasing control flow complexity. The pattern also enables independent testing of each handler in isolation, since handlers are pure functions with no coupling to the loop. Any agent that outgrows a dispatch map has a design problem, not a scaling problem. The dispatch map scales linearly: add a tool, add a handler, add a schema entry. The loop never changes. Handlers are pure functions, so they test in isolation. Any agent that outgrows a dispatch map has a design problem, not a scaling problem.
## Try It ## Try It

View File

@ -19,9 +19,7 @@ explicitly. The model creates a plan, marks items in_progress as it works,
and marks them completed when done. A nag reminder injects a nudge if the and marks them completed when done. A nag reminder injects a nudge if the
model goes 3+ rounds without updating its todos. model goes 3+ rounds without updating its todos.
Teaching simplification: the nag threshold of 3 rounds is set low for Note: the nag threshold of 3 rounds is low for visibility. Production systems tune higher. From s07, this course switches to the Task board for durable multi-step work; TodoWrite remains available for quick checklists.
teaching visibility. Production agents typically use a higher threshold
around 10 to avoid excessive prompting.
## The Solution ## The Solution

View File

@ -14,7 +14,7 @@ does this project use?" might require reading 5 files, but the parent
agent does not need all 5 file contents in its history -- it just needs agent does not need all 5 file contents in its history -- it just needs
the answer: "pytest with conftest.py configuration." the answer: "pytest with conftest.py configuration."
The solution is process isolation: spawn a child agent with `messages=[]`. In this course, a practical solution is fresh-context isolation: spawn a child agent with `messages=[]`.
The child explores, reads files, runs commands. When it finishes, only its The child explores, reads files, runs commands. When it finishes, only its
final text response returns to the parent. The child's entire message final text response returns to the parent. The child's entire message
history is discarded. history is discarded.
@ -137,11 +137,10 @@ def run_subagent(prompt: str) -> str:
| Context | Single shared | Parent + child isolation | | Context | Single shared | Parent + child isolation |
| Subagent | None | `run_subagent()` function | | Subagent | None | `run_subagent()` function |
| Return value | N/A | Summary text only | | Return value | N/A | Summary text only |
| Todo system | TodoManager | Removed (not needed here) |
## Design Rationale ## Design Rationale
Process isolation gives context isolation for free. A fresh `messages[]` means the subagent cannot be confused by the parent's conversation history. The tradeoff is communication overhead -- results must be compressed back to the parent, losing detail. This is the same tradeoff as OS process isolation: safety and cleanliness in exchange for serialization cost. Limiting subagent depth (no recursive spawning) prevents unbounded resource consumption, and a max iteration count ensures runaway children terminate. Fresh-context isolation is a practical way to approximate context isolation in this session. A fresh `messages[]` means the subagent starts without the parent's conversation history. The tradeoff is communication overhead -- results must be compressed back to the parent, losing detail. This is a message-history isolation strategy, not OS process isolation. Limiting subagent depth (no recursive spawning) prevents unbounded resource consumption, and a max iteration count ensures runaway children terminate.
## Try It ## Try It

View File

@ -144,7 +144,6 @@ class SkillLoader:
| System prompt | Static string | + skill descriptions | | System prompt | Static string | + skill descriptions |
| Knowledge | None | .skills/*.md files | | Knowledge | None | .skills/*.md files |
| Injection | None | Two-layer (system + result)| | Injection | None | Two-layer (system + result)|
| Subagent | `run_subagent()` | Removed (different focus) |
## Design Rationale ## Design Rationale

View File

@ -162,7 +162,6 @@ def agent_loop(messages):
| Auto-compact | None | Token threshold trigger | | Auto-compact | None | Token threshold trigger |
| Manual compact | None | `compact` tool | | Manual compact | None | `compact` tool |
| Transcripts | None | Saved to .transcripts/ | | Transcripts | None | Saved to .transcripts/ |
| Skills | load_skill | Removed (different focus) |
## Design Rationale ## Design Rationale

View File

@ -1,28 +1,31 @@
# s07: Tasks # s07: Tasks
> Tasks persist as JSON files on the filesystem with a dependency graph, so they survive context compression and can be shared across agents. > Tasks are persisted as JSON files with a dependency graph, so state survives context compression and can be shared across agents.
## The Problem ## Problem
In-memory state like TodoManager (s03) is lost when the context is In-memory state (for example the TodoManager from s03) is fragile under compression (s06). Once earlier turns are compacted into summaries, in-memory todo state is gone.
compressed (s06). After auto_compact replaces messages with a summary,
the todo list is gone. The agent has to reconstruct it from the summary
text, which is lossy and error-prone.
This is the critical s06-to-s07 bridge: TodoManager items die with s06 -> s07 is the key transition:
compression; file-based tasks don't. Moving state to the filesystem
makes it compression-proof.
More fundamentally, in-memory state is invisible to other agents. 1. Todo list state in memory is conversational and lossy.
When we eventually build teams (s09+), teammates need a shared task 2. Task board state on disk is durable and recoverable.
board. In-memory data structures are process-local.
The solution is to persist tasks as JSON files in `.tasks/`. Each task A second issue is visibility: in-memory structures are process-local, so teammates cannot reliably share that state.
is a separate file with an ID, subject, status, and dependency graph.
Completing task 1 automatically unblocks task 2 if task 2 has
`blockedBy: [1]`. The file system becomes the source of truth.
## The Solution ## When to Use Task vs Todo
From s07 onward, Task is the default. Todo remains for short linear checklists.
## Quick Decision Matrix
| Situation | Prefer | Why |
|---|---|---|
| Short, single-session checklist | Todo | Lowest ceremony, fastest capture |
| Cross-session work, dependencies, or teammates | Task | Durable state, dependency graph, shared visibility |
| Unsure which one to use | Task | Easier to simplify later than migrate mid-run |
## Solution
``` ```
.tasks/ .tasks/
@ -42,7 +45,7 @@ Dependency resolution:
## How It Works ## How It Works
1. The TaskManager provides CRUD operations. Each task is a JSON file. 1. TaskManager provides CRUD with one JSON file per task.
```python ```python
class TaskManager: class TaskManager:
@ -61,8 +64,7 @@ class TaskManager:
return json.dumps(task, indent=2) return json.dumps(task, indent=2)
``` ```
2. When a task is marked completed, `_clear_dependency` removes its ID 2. Completing a task clears that dependency from other tasks.
from all other tasks' `blockedBy` lists.
```python ```python
def _clear_dependency(self, completed_id: int): def _clear_dependency(self, completed_id: int):
@ -73,8 +75,7 @@ def _clear_dependency(self, completed_id: int):
self._save(task) self._save(task)
``` ```
3. The `update` method handles status changes and bidirectional dependency 3. `update` handles status transitions and dependency wiring.
wiring.
```python ```python
def update(self, task_id, status=None, def update(self, task_id, status=None,
@ -94,7 +95,7 @@ def update(self, task_id, status=None,
self._save(task) self._save(task)
``` ```
4. Four task tools are added to the dispatch map. 4. Task tools are added to the dispatch map.
```python ```python
TOOL_HANDLERS = { TOOL_HANDLERS = {
@ -109,8 +110,7 @@ TOOL_HANDLERS = {
## Key Code ## Key Code
The TaskManager with dependency graph (from `agents/s07_task_system.py`, TaskManager with dependency graph (from `agents/s07_task_system.py`, lines 46-123):
lines 46-123):
```python ```python
class TaskManager: class TaskManager:
@ -145,17 +145,20 @@ class TaskManager:
## What Changed From s06 ## What Changed From s06
| Component | Before (s06) | After (s07) | | Component | Before (s06) | After (s07) |
|----------------|------------------|----------------------------| |---|---|---|
| Tools | 5 | 8 (+task_create/update/list/get)| | Tools | 5 | 8 (`task_create/update/list/get`) |
| State storage | In-memory only | JSON files in .tasks/ | | State storage | In-memory only | JSON files in `.tasks/` |
| Dependencies | None | blockedBy + blocks graph | | Dependencies | None | `blockedBy + blocks` graph |
| Compression | Three-layer | Removed (different focus) | | Persistence | Lost on compact | Survives compression |
| Persistence | Lost on compact | Survives compression |
## Design Rationale ## Design Rationale
File-based state survives context compression. When the agent's conversation is compacted, in-memory state is lost, but tasks written to disk persist. The dependency graph ensures correct execution order even after context loss. This is the bridge between ephemeral conversation and persistent work -- the agent can forget conversation details but always has the task board to remind it what needs doing. The filesystem as source of truth also enables future multi-agent sharing, since any process can read the same JSON files. File-based state survives compaction and process restarts. The dependency graph preserves execution order even when conversation details are forgotten. This turns transient chat context into durable work state.
Durability still needs a write discipline: reload task JSON before each write, validate expected `status/blockedBy`, then persist atomically. Otherwise concurrent writers can overwrite each other.
Course-level implication: s07+ defaults to Task because it better matches long-running and collaborative engineering workflows.
## Try It ## Try It
@ -164,7 +167,7 @@ cd learn-claude-code
python agents/s07_task_system.py python agents/s07_task_system.py
``` ```
Example prompts to try: Suggested prompts:
1. `Create 3 tasks: "Setup project", "Write code", "Write tests". Make them depend on each other in order.` 1. `Create 3 tasks: "Setup project", "Write code", "Write tests". Make them depend on each other in order.`
2. `List all tasks and show the dependency graph` 2. `List all tasks and show the dependency graph`

View File

@ -168,7 +168,6 @@ class BackgroundManager:
| Execution | Blocking only | Blocking + background threads| | Execution | Blocking only | Blocking + background threads|
| Notification | None | Queue drained per loop | | Notification | None | Queue drained per loop |
| Concurrency | None | Daemon threads | | Concurrency | None | Daemon threads |
| Task system | File-based CRUD | Removed (different focus) |
## Design Rationale ## Design Rationale

View File

@ -1,6 +1,6 @@
# s09: Agent Teams # s09: Agent Teams
> Persistent teammates with JSONL inboxes turn isolated agents into a communicating team -- spawn, message, broadcast, and drain. > Persistent teammates with JSONL inboxes are one teaching protocol for turning isolated agents into a communicating team -- spawn, message, broadcast, and drain.
## The Problem ## The Problem
@ -215,7 +215,7 @@ pattern used here is safe for the teaching scenario.
## Design Rationale ## Design Rationale
File-based mailboxes (append-only JSONL) provide concurrency-safe inter-agent communication. Append is atomic on most filesystems, avoiding lock contention. The "drain on read" pattern (read all, truncate) gives batch delivery. This is simpler and more robust than shared memory or socket-based IPC for agent coordination. The tradeoff is latency -- messages are only seen at the next poll -- but for LLM-driven agents where each turn takes seconds, polling latency is negligible compared to inference time. File-based mailboxes (append-only JSONL) are easy to inspect and reason about in a teaching codebase. The "drain on read" pattern (read all, truncate) gives batch delivery with very little machinery. The tradeoff is latency -- messages are only seen at the next poll -- but for LLM-driven agents where each turn takes seconds, polling latency is acceptable for this course.
## Try It ## Try It

View File

@ -20,10 +20,7 @@ original system prompt identity ("you are alice, role: coder") fades.
Identity re-injection solves this by inserting an identity block at the Identity re-injection solves this by inserting an identity block at the
start of compressed contexts. start of compressed contexts.
Teaching simplification: the token estimation used here is rough Note: token estimation here uses characters/4 (rough). The nag threshold of 3 rounds is low for teaching visibility.
(characters / 4). Production systems use proper tokenizer libraries.
The nag threshold of 3 rounds (from s03) is set low for teaching
visibility; production agents typically use a higher threshold around 10.
## The Solution ## The Solution

View File

@ -0,0 +1,250 @@
# s12: Worktree + Task Isolation
> Isolate by directory, coordinate by task ID -- tasks are the control plane, worktrees are the execution plane, and an event stream makes every lifecycle step observable.
## The Problem
By s11, agents can claim and complete tasks autonomously. But every task runs in one shared directory. Ask two agents to refactor different modules at the same time and you hit three failure modes:
Agent A edits `auth.py`. Agent B edits `auth.py`. Neither knows the other touched it. Unstaged changes collide, task status says "in_progress" but the directory is a mess, and when something breaks there is no way to roll back one agent's work without destroying the other's. The task board tracks _what to do_ but has no opinion about _where to do it_.
The fix is to separate the two concerns. Tasks manage goals. Worktrees manage execution context. Bind them by task ID, and each agent gets its own directory, its own branch, and a clean teardown path.
## The Solution
```
Control Plane (.tasks/) Execution Plane (.worktrees/)
+---------------------------+ +---------------------------+
| task_1.json | | index.json |
| id: 1 | | name: "auth-refactor" |
| subject: "Auth refactor"| bind | path: ".worktrees/..." |
| status: "in_progress" | <----> | branch: "wt/auth-..." |
| worktree: "auth-refactor"| | task_id: 1 |
+---------------------------+ | status: "active" |
+---------------------------+
| task_2.json | | |
| id: 2 | bind | name: "ui-login" |
| subject: "Login page" | <----> | task_id: 2 |
| worktree: "ui-login" | | status: "active" |
+---------------------------+ +---------------------------+
|
+---------------------------+
| events.jsonl (append-only)|
| worktree.create.before |
| worktree.create.after |
| worktree.remove.after |
| task.completed |
+---------------------------+
```
Three state layers make this work:
1. **Control plane** (`.tasks/task_*.json`) -- what is assigned, in progress, or done. Key fields: `id`, `subject`, `status`, `owner`, `worktree`.
2. **Execution plane** (`.worktrees/index.json`) -- where commands run and whether the workspace is still valid. Key fields: `name`, `path`, `branch`, `task_id`, `status`.
3. **Runtime state** (in-memory) -- per-turn execution continuity: `current_task`, `current_worktree`, `tool_result`, `error`.
## How It Works
The lifecycle has five steps. Each step is a tool call.
1. **Create a task.** Persist the goal first. The task starts as `pending` with an empty `worktree` field.
```python
task = {
"id": self._next_id,
"subject": subject,
"status": "pending",
"owner": "",
"worktree": "",
}
self._save(task)
```
2. **Create a worktree.** Allocate an isolated directory and branch. If you pass `task_id`, the task auto-advances to `in_progress` and the binding is written to both sides.
```python
self._run_git(["worktree", "add", "-b", branch, str(path), base_ref])
entry = {
"name": name,
"path": str(path),
"branch": branch,
"task_id": task_id,
"status": "active",
}
idx["worktrees"].append(entry)
self._save_index(idx)
if task_id is not None:
self.tasks.bind_worktree(task_id, name)
```
3. **Run commands in the worktree.** `worktree_run` sets `cwd` to the worktree path. Edits happen in the isolated directory, not the shared workspace.
```python
r = subprocess.run(
command,
shell=True,
cwd=path,
capture_output=True,
text=True,
timeout=300,
)
```
4. **Observe.** `worktree_status` shows git state inside the isolated context. `worktree_events` queries the append-only event stream.
5. **Close out.** Two choices:
- `worktree_keep(name)` -- preserve the directory, mark lifecycle as `kept`.
- `worktree_remove(name, complete_task=True)` -- remove the directory, complete the bound task, unbind, and emit `task.completed`. This is the closeout pattern: one call handles teardown and task completion together.
## State Machines
```
Task: pending -------> in_progress -------> completed
(worktree_create (worktree_remove
with task_id) with complete_task=true)
Worktree: absent --------> active -----------> removed | kept
(worktree_create) (worktree_remove | worktree_keep)
```
## Key Code
The closeout pattern -- teardown + task completion in one operation (from `agents/s12_worktree_task_isolation.py`):
```python
def remove(self, name: str, force: bool = False, complete_task: bool = False) -> str:
wt = self._find(name)
if not wt:
return f"Error: Unknown worktree '{name}'"
self.events.emit(
"worktree.remove.before",
task={"id": wt.get("task_id")} if wt.get("task_id") is not None else {},
worktree={"name": name, "path": wt.get("path")},
)
try:
args = ["worktree", "remove"]
if force:
args.append("--force")
args.append(wt["path"])
self._run_git(args)
if complete_task and wt.get("task_id") is not None:
task_id = wt["task_id"]
self.tasks.update(task_id, status="completed")
self.tasks.unbind_worktree(task_id)
self.events.emit("task.completed", task={
"id": task_id, "status": "completed",
}, worktree={"name": name})
idx = self._load_index()
for item in idx.get("worktrees", []):
if item.get("name") == name:
item["status"] = "removed"
item["removed_at"] = time.time()
self._save_index(idx)
self.events.emit(
"worktree.remove.after",
task={"id": wt.get("task_id")} if wt.get("task_id") is not None else {},
worktree={"name": name, "path": wt.get("path"), "status": "removed"},
)
return f"Removed worktree '{name}'"
except Exception as e:
self.events.emit(
"worktree.remove.failed",
worktree={"name": name},
error=str(e),
)
raise
```
The task-side binding (from `agents/s12_worktree_task_isolation.py`):
```python
def bind_worktree(self, task_id: int, worktree: str, owner: str = "") -> str:
task = self._load(task_id)
task["worktree"] = worktree
if task["status"] == "pending":
task["status"] = "in_progress"
task["updated_at"] = time.time()
self._save(task)
```
The dispatch map wiring all tools together:
```python
TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
"task_create": lambda **kw: TASKS.create(kw["subject"], kw.get("description", "")),
"task_list": lambda **kw: TASKS.list_all(),
"task_get": lambda **kw: TASKS.get(kw["task_id"]),
"task_update": lambda **kw: TASKS.update(kw["task_id"], kw.get("status"), kw.get("owner")),
"task_bind_worktree": lambda **kw: TASKS.bind_worktree(kw["task_id"], kw["worktree"]),
"worktree_create": lambda **kw: WORKTREES.create(kw["name"], kw.get("task_id")),
"worktree_list": lambda **kw: WORKTREES.list_all(),
"worktree_status": lambda **kw: WORKTREES.status(kw["name"]),
"worktree_run": lambda **kw: WORKTREES.run(kw["name"], kw["command"]),
"worktree_keep": lambda **kw: WORKTREES.keep(kw["name"]),
"worktree_remove": lambda **kw: WORKTREES.remove(kw["name"], kw.get("force", False), kw.get("complete_task", False)),
"worktree_events": lambda **kw: EVENTS.list_recent(kw.get("limit", 20)),
}
```
## Event Stream
Every lifecycle transition emits a before/after/failed triplet to `.worktrees/events.jsonl`. This is an append-only log, not a replacement for task/worktree state files.
Events emitted:
- `worktree.create.before` / `worktree.create.after` / `worktree.create.failed`
- `worktree.remove.before` / `worktree.remove.after` / `worktree.remove.failed`
- `worktree.keep`
- `task.completed` (when `complete_task=true` succeeds)
Payload shape:
```json
{
"event": "worktree.remove.after",
"task": {"id": 7, "status": "completed"},
"worktree": {"name": "auth-refactor", "path": "...", "status": "removed"},
"ts": 1730000000
}
```
This gives you three things: policy decoupling (audit and notifications stay outside the core flow), failure compensation (`*.failed` records mark partial transitions), and queryability (`worktree_events` tool reads the log directly).
## What Changed From s11
| Component | Before (s11) | After (s12) |
|--------------------|----------------------------|----------------------------------------------|
| Coordination state | Task board (`owner/status`) | Task board + explicit `worktree` binding |
| Execution scope | Shared directory | Task-scoped isolated directory |
| Recoverability | Task status only | Task status + worktree index |
| Teardown semantics | Task completion | Task completion + explicit keep/remove |
| Lifecycle visibility | Implicit in logs | Explicit events in `.worktrees/events.jsonl` |
## Design Rationale
Separating control plane from execution plane means you can reason about _what to do_ and _where to do it_ independently. A task can exist without a worktree (planning phase). A worktree can exist without a task (ad-hoc exploration). Binding them is an explicit action that writes state to both sides. This composability is the point -- it keeps the system recoverable after crashes. After an interruption, state reconstructs from `.tasks/` + `.worktrees/index.json` on disk. Volatile in-memory session state downgrades into explicit, durable file state. The event stream adds observability without coupling side effects into the critical path: auditing, notifications, and quota checks consume events rather than intercepting state writes.
## Try It
```sh
cd learn-claude-code
python agents/s12_worktree_task_isolation.py
```
Example prompts to try:
1. `Create tasks for backend auth and frontend login page, then list tasks.`
2. `Create worktree "auth-refactor" for task 1, create worktree "ui-login", then bind task 2 to "ui-login".`
3. `Run "git status --short" in worktree "auth-refactor".`
4. `Keep worktree "ui-login", then list worktrees and inspect worktree events.`
5. `Remove worktree "auth-refactor" with complete_task=true, then list tasks/worktrees/events.`

View File

@ -1,6 +1,6 @@
# s01: The Agent Loop # s01: The Agent Loop
> AIコーディングエージェントの秘密はすべて、モデルが「終了」と判断するまでツール結果をモデルにフィードバックし続けるwhileループにある。 > AIコーディングエージェントの中核は、モデルが「終了」と判断するまでツール結果をモデルにフィードバックし続ける while ループにある。
## 問題 ## 問題
@ -49,7 +49,7 @@ response = client.messages.create(
messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "assistant", "content": response.content})
``` ```
4. stop reasonを確認する。モデルがツールを呼び出さなかった場合、ループは終了する。これが唯一の終了条件だ。 4. stop reasonを確認する。モデルがツールを呼び出さなかった場合、ループは終了する。この最小実装では、これが唯一のループ終了条件だ。
```python ```python
if response.stop_reason != "tool_use": if response.stop_reason != "tool_use":
@ -115,7 +115,7 @@ def agent_loop(messages: list):
## 設計原理 ## 設計原理
このループはすべてのLLMベースエージェントの普遍的な基盤だ。本番実装ではエラーハンドリング、トークンカウント、ストリーミング、リトライロジックが追加されるが、根本的な構造は変わらない。シンプルさこそがポイントだ: 1つの終了条件(`stop_reason != "tool_use"`)がフロー全体を制御する。本コースの他のすべて -- ツール、計画、圧縮、チーム -- はこのループの上に積み重なるが、ループ自体は変更しない。このループを理解することは、すべてのエージェントを理解することだ このループは LLM ベースエージェントの土台だ。本番実装ではエラーハンドリング、トークン計測、ストリーミング、リトライに加え、権限ポリシーやライフサイクル編成が追加されるが、コアの相互作用パターンはここから始まる。シンプルさこそこの章の狙いであり、この最小実装では 1 つの終了条件(`stop_reason != "tool_use"`)で学習に必要な制御を示す。本コースの他の要素はこのループに積み重なる。つまり、このループの理解は基礎であって、本番アーキテクチャ全体そのものではない
## 試してみる ## 試してみる

View File

@ -10,7 +10,7 @@
解決策は構造化された状態管理だ: モデルが明示的に書き込むTodoManager。モデルは計画を作成し、作業中のアイテムをin_progressとしてマークし、完了時にcompletedとマークする。nagリマインダーは、モデルが3ラウンド以上todoを更新しなかった場合にナッジを注入する。 解決策は構造化された状態管理だ: モデルが明示的に書き込むTodoManager。モデルは計画を作成し、作業中のアイテムをin_progressとしてマークし、完了時にcompletedとマークする。nagリマインダーは、モデルが3ラウンド以上todoを更新しなかった場合にナッジを注入する。
教育上の簡略化: nagの閾値3ラウンドは教育目的の可視化のために低く設定されている。本番のエージェントでは過剰なプロンプトを避けるため閾値は約10に設定されている 注: nag 閾値 3 ラウンドは可視化のために低く設定。本番ではより高い値に調整される。s07 以降は永続的なマルチステップ作業に Task ボードを使用。TodoWrite は軽量チェックリストとして引き続き利用可能
## 解決策 ## 解決策

View File

@ -8,7 +8,7 @@
これは探索的タスクで特に深刻だ。「このプロジェクトはどのテストフレームワークを使っているか」という質問には5つのファイルを読む必要があるかもしれないが、親エージェントには5つのファイルの内容すべては不要だ -- 「pytest with conftest.py configuration」という回答だけが必要なのだ。 これは探索的タスクで特に深刻だ。「このプロジェクトはどのテストフレームワークを使っているか」という質問には5つのファイルを読む必要があるかもしれないが、親エージェントには5つのファイルの内容すべては不要だ -- 「pytest with conftest.py configuration」という回答だけが必要なのだ。
解決策はプロセスの分離だ: `messages=[]`で子エージェントを生成する。子は探索し、ファイルを読み、コマンドを実行する。終了時には最終的なテキストレスポンスだけが親に返される。子のメッセージ履歴全体は破棄される。 このコースでの実用的な解決策は fresh `messages[]` 分離だ: `messages=[]`で子エージェントを生成する。子は探索し、ファイルを読み、コマンドを実行する。終了時には最終的なテキストレスポンスだけが親に返される。子のメッセージ履歴全体は破棄される。
## 解決策 ## 解決策
@ -124,11 +124,10 @@ def run_subagent(prompt: str) -> str:
| Context | Single shared | Parent + child isolation | | Context | Single shared | Parent + child isolation |
| Subagent | None | `run_subagent()` function | | Subagent | None | `run_subagent()` function |
| Return value | N/A | Summary text only | | Return value | N/A | Summary text only |
| Todo system | TodoManager | Removed (not needed here) |
## 設計原理 ## 設計原理
プロセス分離はコンテキスト分離を無料で提供する。新しい`messages[]`は、サブエージェントが親の会話履歴に混乱させられないことを意味する。トレードオフは通信オーバーヘッドだ -- 結果は親に圧縮して返す必要があり、詳細が失われる。これはOSのプロセス分離と同じトレードオフだ: シリアライゼーションコストと引き換えに安全性とクリーンさを得る。サブエージェントの深さ制限(再帰的なスポーンは不可)は無制限のリソース消費を防ぎ、最大反復回数は暴走した子プロセスの終了を保証する。 このセッションでは、fresh `messages[]` 分離はコンテキスト分離を近似する実用手段だ。新しい`messages[]`により、サブエージェントは親の会話履歴を持たずに開始する。トレードオフは通信オーバーヘッドで、結果を親へ圧縮して返すため詳細が失われる。これはメッセージ履歴の分離戦略であり、OSのプロセス分離そのものではない。サブエージェントの深さ制限(再帰スポーン不可)は無制限のリソース消費を防ぎ、最大反復回数は暴走した子処理の終了を保証する。
## 試してみる ## 試してみる

View File

@ -132,7 +132,6 @@ class SkillLoader:
| System prompt | Static string | + skill descriptions | | System prompt | Static string | + skill descriptions |
| Knowledge | None | .skills/*.md files | | Knowledge | None | .skills/*.md files |
| Injection | None | Two-layer (system + result)| | Injection | None | Two-layer (system + result)|
| Subagent | `run_subagent()` | Removed (different focus) |
## 設計原理 ## 設計原理

View File

@ -149,7 +149,6 @@ def agent_loop(messages):
| Auto-compact | None | Token threshold trigger | | Auto-compact | None | Token threshold trigger |
| Manual compact | None | `compact` tool | | Manual compact | None | `compact` tool |
| Transcripts | None | Saved to .transcripts/ | | Transcripts | None | Saved to .transcripts/ |
| Skills | load_skill | Removed (different focus) |
## 設計原理 ## 設計原理

View File

@ -1,16 +1,29 @@
# s07: Tasks # s07: Tasks
> タスクはファイルシステム上にJSON形式で依存グラフ付きで永続化され、コンテキスト圧縮後も生き残り、複数エージェント間で共有できる。 > タスクを依存グラフ付き JSON として永続化し、コンテキスト圧縮後も状態を保持し、複数エージェントで共有できるようにする。
## 問題 ## 問題
インメモリの状態であるTodoManager(s03)は、コンテキストが圧縮(s06)されると失われる。auto_compactがメッセージを要約で置換した後、todoリストは消える。エージェントは要約テキストからそれを再構成しなければならないが、これは不正確でエラーが起きやすい。 インメモリ状態s03 の TodoManager などは、s06 の圧縮後に失われやすい。古いターンが要約化されると、Todo 状態は会話の外に残らない。
これがs06からs07への重要な橋渡しだ: TodoManagerのアイテムは圧縮と共に死ぬが、ファイルベースのタスクは死なない。状態をファイルシステムに移すことで、圧縮に対する耐性が得られる。 s06 -> s07 の本質は次の切替:
さらに根本的な問題として、インメモリの状態は他のエージェントからは見えない。最終的にチーム(s09以降)を構築する際、チームメイトには共有のタスクボードが必要だ。インメモリのデータ構造はプロセスローカルだ。 1. メモリ上 Todo は会話依存で失われやすい。
2. ディスク上 Task は永続で復元しやすい。
解決策はタスクを`.tasks/`にJSON形式で永続化すること。各タスクはID、件名、ステータス、依存グラフを持つ個別のファイルだ。タスク1を完了すると、タスク2が`blockedBy: [1]`を持つ場合、自動的にタスク2のブロックが解除される。ファイルシステムが信頼できる情報源となる。 さらに可視性の問題がある。インメモリ構造はプロセスローカルであり、チームメイト間の共有が不安定になる。
## Task vs Todo: 使い分け
s07 以降は Task がデフォルト。Todo は短い直線的チェックリスト用に残る。
## クイック判定マトリクス
| 状況 | 優先 | 理由 |
|---|---|---|
| 短時間・単一セッション・直線的チェック | Todo | 儀式が最小で記録が速い |
| セッション跨ぎ・依存関係・複数担当 | Task | 永続性、依存表現、協調可視性が必要 |
| 迷う場合 | Task | 後で簡略化する方が、途中移行より低コスト |
## 解決策 ## 解決策
@ -32,7 +45,7 @@ Dependency resolution:
## 仕組み ## 仕組み
1. TaskManagerがCRUD操作を提供する。各タスクは1つのJSONファイル 1. TaskManager はタスクごとに1 JSON ファイルで CRUD を提供する
```python ```python
class TaskManager: class TaskManager:
@ -51,7 +64,7 @@ class TaskManager:
return json.dumps(task, indent=2) return json.dumps(task, indent=2)
``` ```
2. タスクが完了とマークされると、`_clear_dependency`がそのIDを他のすべてのタスクの`blockedBy`リストから除去する。 2. タスク完了時、他タスクの依存を解除する。
```python ```python
def _clear_dependency(self, completed_id: int): def _clear_dependency(self, completed_id: int):
@ -62,7 +75,7 @@ def _clear_dependency(self, completed_id: int):
self._save(task) self._save(task)
``` ```
3. `update`メソッドがステータス変更と双方向の依存関係の結線を処理する 3. `update` が状態遷移と依存配線を担う
```python ```python
def update(self, task_id, status=None, def update(self, task_id, status=None,
@ -82,7 +95,7 @@ def update(self, task_id, status=None,
self._save(task) self._save(task)
``` ```
4. 4つのタスクツールがディスパッチマップに追加される。 4. タスクツール群をディスパッチへ追加する。
```python ```python
TOOL_HANDLERS = { TOOL_HANDLERS = {
@ -97,7 +110,7 @@ TOOL_HANDLERS = {
## 主要コード ## 主要コード
依存グラフ付きTaskManager(`agents/s07_task_system.py` 46-123行目): 依存グラフ付き TaskManager`agents/s07_task_system.py` 46-123行:
```python ```python
class TaskManager: class TaskManager:
@ -130,19 +143,22 @@ class TaskManager:
self._save(task) self._save(task)
``` ```
## s06からの変更 ## s06 からの変更
| Component | Before (s06) | After (s07) | | 項目 | Before (s06) | After (s07) |
|----------------|------------------|----------------------------| |---|---|---|
| Tools | 5 | 8 (+task_create/update/list/get)| | Tools | 5 | 8 (`task_create/update/list/get`) |
| State storage | In-memory only | JSON files in .tasks/ | | 状態保存 | メモリのみ | `.tasks/` の JSON |
| Dependencies | None | blockedBy + blocks graph | | 依存関係 | なし | `blockedBy + blocks` グラフ |
| Compression | Three-layer | Removed (different focus) | | 永続性 | compact で消失 | compact 後も維持 |
| Persistence | Lost on compact | Survives compression |
## 設計原理 ## 設計原理
ファイルベースの状態はコンテキスト圧縮を生き延びる。エージェントの会話が圧縮されるとメモリ内の状態は失われるが、ディスクに書き込まれたタスクは永続する。依存グラフにより、コンテキストが失われた後でも正しい順序で実行される。これは一時的な会話と永続的な作業の橋渡しだ -- エージェントは会話の詳細を忘れても、タスクボードが常に何をすべきかを思い出させてくれる。ファイルシステムを信頼できる情報源とすることで、将来のマルチエージェント共有も可能になる。任意のプロセスが同じJSONファイルを読み取れるからだ。 ファイルベース状態は compaction や再起動に強い。依存グラフにより、会話詳細を忘れても実行順序を保てる。これにより、会話中心の状態を作業中心の永続状態へ移せる。
ただし耐久性には運用前提がある。書き込みのたびに task JSON を再読込し、`status/blockedBy` が期待通りか確認してから原子的に保存しないと、並行更新で状態を上書きしやすい。
コース設計上、s07 以降で Task を主線に置くのは、長時間・協調開発の実態に近いから。
## 試してみる ## 試してみる
@ -151,7 +167,7 @@ cd learn-claude-code
python agents/s07_task_system.py python agents/s07_task_system.py
``` ```
試せるプロンプト例: 例:
1. `Create 3 tasks: "Setup project", "Write code", "Write tests". Make them depend on each other in order.` 1. `Create 3 tasks: "Setup project", "Write code", "Write tests". Make them depend on each other in order.`
2. `List all tasks and show the dependency graph` 2. `List all tasks and show the dependency graph`

View File

@ -157,7 +157,6 @@ class BackgroundManager:
| Execution | Blocking only | Blocking + background threads| | Execution | Blocking only | Blocking + background threads|
| Notification | None | Queue drained per loop | | Notification | None | Queue drained per loop |
| Concurrency | None | Daemon threads | | Concurrency | None | Daemon threads |
| Task system | File-based CRUD | Removed (different focus) |
## 設計原理 ## 設計原理

View File

@ -1,6 +1,6 @@
# s09: Agent Teams # s09: Agent Teams
> JSONL形式のインボックスを持つ永続的なチームメイトが、孤立したエージェントをコミュニケーションするチームに変える -- spawn、message、broadcast、drain。 > JSONL 形式のインボックスを持つ永続的なチームメイトは、孤立したエージェントを連携可能なチームへ変えるための教材プロトコルの一つだ -- spawn、message、broadcast、drain。
## 問題 ## 問題
@ -8,7 +8,7 @@
本物のチームワークには3つのものが必要だ: (1)単一のプロンプトを超えて存続する永続的なエージェント、(2)アイデンティティとライフサイクル管理、(3)エージェント間の通信チャネル。メッセージングがなければ、永続的なチームメイトでさえ聾唖だ -- 並列に作業できるが協調することはない。 本物のチームワークには3つのものが必要だ: (1)単一のプロンプトを超えて存続する永続的なエージェント、(2)アイデンティティとライフサイクル管理、(3)エージェント間の通信チャネル。メッセージングがなければ、永続的なチームメイトでさえ聾唖だ -- 並列に作業できるが協調することはない。
解決策は、名前付きの永続的エージェントを生成するTeammateManagerと、JONSLインボックスファイルを使うMessageBusの組み合わせだ。各チームメイトは自身のagent loopをスレッドで実行し、各LLM呼び出しの前にインボックスを確認し、他のチームメイトやリーダーにメッセージを送れる。 解決策は、名前付きの永続的エージェントを生成するTeammateManagerと、JSONL インボックスファイルを使うMessageBusの組み合わせだ。各チームメイトは自身のagent loopをスレッドで実行し、各LLM呼び出しの前にインボックスを確認し、他のチームメイトやリーダーにメッセージを送れる。
s06からs07への橋渡しについての注記: s03のTodoManagerアイテムは圧縮(s06)と共に死ぬ。ファイルベースのタスク(s07)はディスク上に存在するため圧縮後も生き残る。チームも同じ原則の上に構築されている -- config.jsonとインボックスファイルはコンテキストウィンドウの外に永続化される。 s06からs07への橋渡しについての注記: s03のTodoManagerアイテムは圧縮(s06)と共に死ぬ。ファイルベースのタスク(s07)はディスク上に存在するため圧縮後も生き残る。チームも同じ原則の上に構築されている -- config.jsonとインボックスファイルはコンテキストウィンドウの外に永続化される。
@ -194,7 +194,7 @@ class MessageBus:
## 設計原理 ## 設計原理
ファイルベースのメールボックス(追記専用JSONL)は並行性安全なエージェント間通信を提供する。追記はほとんどのファイルシステムでアトミックであり、ロック競合を回避する。「読み取り時にドレイン」パターン(全読み取り、切り詰め)はバッチ配信を提供する。これは共有メモリやソケットベースのIPCよりもシンプルで堅牢だ。トレードオフはレイテンシだ -- メッセージは次のポーリングまで見えない -- しかし各ターンに数秒の推論時間がかかるLLM駆動エージェントにとって、ポーリングレイテンシは推論時間に比べて無視できる。 ファイルベースのメールボックス(追記専用 JSONL)は、教材コードとして観察しやすく理解しやすい。「読み取り時にドレイン」パターン(全読み取り、切り詰め)は、少ない仕組みでバッチ配信を実現できる。トレードオフはレイテンシで、メッセージは次のポーリングまで見えない。ただし本コースでは、各ターンに数秒かかる LLM 推論を前提にすると、この遅延は許容範囲である。
## 試してみる ## 試してみる

View File

@ -10,7 +10,7 @@ s09-s10では、チームメイトは明示的に指示された時のみ作業
しかし自律エージェントには微妙な問題がある: コンテキスト圧縮後に、エージェントが自分が誰かを忘れる可能性がある。メッセージが要約されると、元のシステムプロンプトのアイデンティティ(「あなたはalice、役割はcoder」)が薄れる。アイデンティティの再注入は、圧縮されたコンテキストの先頭にアイデンティティブロックを挿入することでこれを解決する。 しかし自律エージェントには微妙な問題がある: コンテキスト圧縮後に、エージェントが自分が誰かを忘れる可能性がある。メッセージが要約されると、元のシステムプロンプトのアイデンティティ(「あなたはalice、役割はcoder」)が薄れる。アイデンティティの再注入は、圧縮されたコンテキストの先頭にアイデンティティブロックを挿入することでこれを解決する。
教育上の簡略化: ここで使用するトークン推定は大まかなもの(文字数 / 4)だ。本番システムでは適切なトークナイザーライブラリを使用する。nagの閾値3ラウンド(s03から)は教育目的の可視化のために低く設定されている。本番のエージェントでは閾値は約10 注: トークン推定は文字数/4大まか。nag 閾値 3 ラウンドは可視化のために低く設定
## 解決策 ## 解決策

View File

@ -0,0 +1,226 @@
# s12: Worktree + Task Isolation
> ディレクトリで分離し、タスクIDで調整する -- タスクボード(制御面)と worktree(実行面)の組み合わせで、並行編集を衝突しやすい状態から追跡可能・復元可能・後片付け可能な状態に変える。
## 問題
s11 でエージェントはタスクを自律的に処理できるようになった。だが全タスクが同じ作業ディレクトリで走ると、3つの障害が現れる。
あるエージェントが認証リファクタリングに取り組みながら、別のエージェントがログインページを作っている。両者が `src/auth.py` を編集する。未コミットの変更が混ざり合い、`git diff` は2つのタスクの差分が入り混じった結果を返す。どちらのエージェントの変更かを後から特定するのは困難になり、片方のタスクを巻き戻すと他方の編集も消える。
1. 変更汚染: 未コミット変更が相互に干渉する。
2. 責務の曖昧化: タスク状態とファイル変更がずれる。
3. 終了処理の難化: 実行コンテキストを残すか削除するかの判断が曖昧になる。
解決の核は「何をやるか」と「どこでやるか」の分離だ。
## 解決策
```
Control Plane (.tasks/) Execution Plane (.worktrees/)
+---------------------+ +------------------------+
| task_1.json | | auth-refactor/ |
| status: in_progress| bind | branch: wt/auth-ref |
| worktree: auth-ref|-------->| cwd for commands |
+---------------------+ +------------------------+
| task_2.json | | ui-login/ |
| status: pending | bind | branch: wt/ui-login |
| worktree: ui-login|-------->| cwd for commands |
+---------------------+ +------------------------+
| |
v v
"what to do" "where to execute"
Events (.worktrees/events.jsonl)
worktree.create.before -> worktree.create.after
worktree.remove.before -> worktree.remove.after
task.completed
```
## 仕組み
1. 状態は3つの層に分かれる。制御面はタスクの目標と担当を管理し、実行面は worktree のパスとブランチを管理し、実行時状態はメモリ上の1ターン情報を保持する。
```text
制御面 (.tasks/task_*.json) -> id/subject/status/owner/worktree
実行面 (.worktrees/index.json) -> name/path/branch/task_id/status
実行時状態 (メモリ) -> current_task/current_worktree/error
```
2. Task と worktree はそれぞれ独立した状態機械を持つ。
```text
Task: pending -> in_progress -> completed
Worktree: absent -> active -> removed | kept
```
3. `task_create` でまず目標を永続化する。worktree はまだ不要だ。
```python
task = {
"id": self._next_id,
"subject": subject,
"status": "pending",
"owner": "",
"worktree": "",
"created_at": time.time(),
"updated_at": time.time(),
}
self._save(task)
```
4. `worktree_create(name, task_id?)` で分離ディレクトリとブランチを作る。`task_id` を渡すと、タスクが `pending` なら自動的に `in_progress` に遷移する。
```python
entry = {
"name": name,
"path": str(path),
"branch": branch,
"task_id": task_id,
"status": "active",
"created_at": time.time(),
}
idx["worktrees"].append(entry)
self._save_index(idx)
if task_id is not None:
self.tasks.bind_worktree(task_id, name)
```
5. `worktree_run(name, command)` で分離ディレクトリ内のコマンドを実行する。`cwd=worktree_path` が実質的な「enter」だ。
```python
r = subprocess.run(
command,
shell=True,
cwd=path,
capture_output=True,
text=True,
timeout=300,
)
```
6. 終了処理では `keep``remove` を明示的に選ぶ。`worktree_remove(name, complete_task=true)` はディレクトリ削除とタスク完了を一度に行う。
```python
def remove(self, name: str, force: bool = False, complete_task: bool = False) -> str:
self._run_git(["worktree", "remove", wt["path"]])
if complete_task and wt.get("task_id") is not None:
self.tasks.update(wt["task_id"], status="completed")
self.tasks.unbind_worktree(wt["task_id"])
self.events.emit("task.completed", ...)
```
7. `.worktrees/events.jsonl` にライフサイクルイベントが append-only で記録される。重要な遷移には `before / after / failed` の三段イベントが出力される。
```json
{
"event": "worktree.remove.after",
"task": {"id": 7, "status": "completed"},
"worktree": {"name": "auth-refactor", "path": "...", "status": "removed"},
"ts": 1730000000
}
```
イベントは可観測性のサイドチャネルであり、task/worktree の主状態機械の書き込みを置き換えるものではない。監査・通知・ポリシーチェックはイベント購読側で処理する。
## 主要コード
タスクの worktree バインドと状態遷移(`agents/s12_worktree_task_isolation.py` 182-191行目):
```python
def bind_worktree(self, task_id: int, worktree: str, owner: str = "") -> str:
task = self._load(task_id)
task["worktree"] = worktree
if owner:
task["owner"] = owner
if task["status"] == "pending":
task["status"] = "in_progress"
task["updated_at"] = time.time()
self._save(task)
return json.dumps(task, indent=2)
```
Worktree の作成とイベント発火(`agents/s12_worktree_task_isolation.py` 283-334行目):
```python
def create(self, name: str, task_id: int = None, base_ref: str = "HEAD") -> str:
self._validate_name(name)
if self._find(name):
raise ValueError(f"Worktree '{name}' already exists in index")
path = self.dir / name
branch = f"wt/{name}"
self.events.emit("worktree.create.before",
task={"id": task_id} if task_id is not None else {},
worktree={"name": name, "base_ref": base_ref})
try:
self._run_git(["worktree", "add", "-b", branch, str(path), base_ref])
entry = {
"name": name, "path": str(path), "branch": branch,
"task_id": task_id, "status": "active",
"created_at": time.time(),
}
idx = self._load_index()
idx["worktrees"].append(entry)
self._save_index(idx)
if task_id is not None:
self.tasks.bind_worktree(task_id, name)
self.events.emit("worktree.create.after", ...)
return json.dumps(entry, indent=2)
except Exception as e:
self.events.emit("worktree.create.failed", ..., error=str(e))
raise
```
ツールディスパッチマップ(`agents/s12_worktree_task_isolation.py` 535-552行目):
```python
TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
"task_create": lambda **kw: TASKS.create(kw["subject"], kw.get("description", "")),
"task_list": lambda **kw: TASKS.list_all(),
"task_get": lambda **kw: TASKS.get(kw["task_id"]),
"task_update": lambda **kw: TASKS.update(kw["task_id"], kw.get("status"), kw.get("owner")),
"task_bind_worktree": lambda **kw: TASKS.bind_worktree(kw["task_id"], kw["worktree"], kw.get("owner", "")),
"worktree_create": lambda **kw: WORKTREES.create(kw["name"], kw.get("task_id"), kw.get("base_ref", "HEAD")),
"worktree_list": lambda **kw: WORKTREES.list_all(),
"worktree_status": lambda **kw: WORKTREES.status(kw["name"]),
"worktree_run": lambda **kw: WORKTREES.run(kw["name"], kw["command"]),
"worktree_keep": lambda **kw: WORKTREES.keep(kw["name"]),
"worktree_remove": lambda **kw: WORKTREES.remove(kw["name"], kw.get("force", False), kw.get("complete_task", False)),
"worktree_events": lambda **kw: EVENTS.list_recent(kw.get("limit", 20)),
}
```
## s11 からの変更
| 観点 | s11 | s12 |
|---|---|---|
| 調整状態 | Task board (`owner/status`) | Task board + `worktree` 明示バインド |
| 実行スコープ | 共有ディレクトリ | タスク単位の分離ディレクトリ |
| 復元性 | タスク状態のみ | タスク状態 + worktree index |
| 終了意味論 | タスク完了のみ | タスク完了 + 明示的 keep/remove 判断 |
| ライフサイクル可視性 | 暗黙的なログ | `.worktrees/events.jsonl` の明示イベント |
## 設計原理
制御面と実行面の分離が中核だ。タスクは「何をやるか」を記述し、worktree は「どこでやるか」を提供する。両者は組み合わせ可能だが、強結合ではない。状態遷移は暗黙の自動掃除ではなく、`worktree_keep` / `worktree_remove` という明示的なツール操作として表現する。イベントストリームは `before / after / failed` の三段構造で重要な遷移を記録し、監査や通知をコアロジックから分離する。中断後でも `.tasks/` + `.worktrees/index.json` から状態を再構築できる。揮発的な会話状態を明示的なディスク状態に落とすことが、復元可能性の鍵だ。
## 試してみる
```sh
cd learn-claude-code
python agents/s12_worktree_task_isolation.py
```
試せるプロンプト例:
1. `Create tasks for backend auth and frontend login page, then list tasks.`
2. `Create worktree "auth-refactor" for task 1, create worktree "ui-login", then bind task 2 to "ui-login".`
3. `Run "git status --short" in worktree "auth-refactor".`
4. `Keep worktree "ui-login", then list worktrees and inspect worktree events.`
5. `Remove worktree "auth-refactor" with complete_task=true, then list tasks/worktrees/events.`

View File

@ -1,6 +1,6 @@
# s01: Agent Loop (智能体循环) # s01: Agent Loop (智能体循环)
> AI 编程智能体的全部秘密就是一个 while 循环 -- 把工具执行结果反馈给模型, 直到模型决定停止。 > AI 编程智能体的核心是一个 while 循环 -- 把工具执行结果反馈给模型, 直到模型决定停止。
## 问题 ## 问题
@ -49,7 +49,7 @@ response = client.messages.create(
messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "assistant", "content": response.content})
``` ```
4. 检查 stop_reason。如果模型没有调用工具, 循环结束。这是唯一的退出条件。 4. 检查 stop_reason。如果模型没有调用工具, 循环结束。在本节最小实现里, 这是唯一的循环退出条件。
```python ```python
if response.stop_reason != "tool_use": if response.stop_reason != "tool_use":
@ -115,7 +115,7 @@ def agent_loop(messages: list):
## 设计原理 ## 设计原理
这个循环是所有基于 LLM 的智能体的通用基础。生产实现会增加错误处理、token 计数、流式输出和重试逻辑, 但基本结构不变。简洁性就是重点: 一个退出条件 (`stop_reason != "tool_use"`) 控制整个流程。本课程中的所有其他内容 -- 工具、规划、压缩、团队 -- 都是在这个循环之上叠加, 而不修改它。理解这个循环就是理解所有智能体 这个循环是所有基于 LLM 的智能体基础。生产实现会增加错误处理、token 计数、流式输出、重试、权限策略与生命周期编排, 但核心交互模式仍从这里开始。本节强调简洁性: 在本节最小实现里, 一个退出条件 (`stop_reason != "tool_use"`) 就能支撑我们先学会主流程。本课程中的其他内容都在这个循环上叠加。理解这个循环是建立基础心智模型, 不是完整的生产架构
## 试一试 ## 试一试

View File

@ -6,7 +6,7 @@
只有 `bash` 时, 智能体所有操作都通过 shell: 读文件、写文件、编辑文件。这能用但很脆弱。`cat` 的输出会被不可预测地截断。`sed` 替换遇到特殊字符就会失败。模型浪费大量 token 构造 shell 管道, 而一个直接的函数调用会简单得多。 只有 `bash` 时, 智能体所有操作都通过 shell: 读文件、写文件、编辑文件。这能用但很脆弱。`cat` 的输出会被不可预测地截断。`sed` 替换遇到特殊字符就会失败。模型浪费大量 token 构造 shell 管道, 而一个直接的函数调用会简单得多。
更重要的是, bash 是一个安全攻击面。每次 bash 调用都能做 shell 能做的一切。有了专用工具如 `read_file``write_file`, 你可以在工具层面强制路径沙箱化, 阻止危险模式, 而不是寄希望于模型自觉回避。 更重要的是, bash 存在安全风险。每次 bash 调用都能做 shell 能做的一切。有了专用工具如 `read_file``write_file`, 你可以在工具层面强制路径沙箱化, 阻止危险模式, 而不是寄希望于模型自觉回避。
关键洞察: 添加工具不需要修改循环。s01 的循环保持不变。你只需在工具数组中添加条目, 编写处理函数, 然后通过 dispatch map 把它们关联起来。 关键洞察: 添加工具不需要修改循环。s01 的循环保持不变。你只需在工具数组中添加条目, 编写处理函数, 然后通过 dispatch map 把它们关联起来。

View File

@ -10,7 +10,7 @@
解决方案是结构化状态: 一个模型显式写入的 TodoManager。模型创建计划, 工作时将项目标记为 in_progress, 完成后标记为 completed。nag reminder 机制在模型连续 3 轮以上不更新待办时注入提醒。 解决方案是结构化状态: 一个模型显式写入的 TodoManager。模型创建计划, 工作时将项目标记为 in_progress, 完成后标记为 completed。nag reminder 机制在模型连续 3 轮以上不更新待办时注入提醒。
教学简化说明: 这里 nag 阈值设为 3 轮是为了教学可见性。生产环境的智能体通常使用约 10 轮的阈值以避免过度提醒 注: nag 阈值 3 轮是为教学可见性设的低值, 生产环境通常更高。从 s07 起, 课程转向 Task 看板处理持久化多步工作; TodoWrite 仍可用于轻量清单
## 解决方案 ## 解决方案

View File

@ -8,7 +8,7 @@
这对探索性任务尤其糟糕。"这个项目用了什么测试框架?" 可能需要读取 5 个文件, 但父智能体的历史中并不需要这 5 个文件的全部内容 -- 它只需要答案: "pytest, 使用 conftest.py 配置。" 这对探索性任务尤其糟糕。"这个项目用了什么测试框架?" 可能需要读取 5 个文件, 但父智能体的历史中并不需要这 5 个文件的全部内容 -- 它只需要答案: "pytest, 使用 conftest.py 配置。"
解决方案是进程隔离: 以 `messages=[]` 启动一个子智能体。子智能体进行探索、读取文件、运行命令。完成后, 只有最终的文本响应返回给父智能体。子智能体的全部消息历史被丢弃。 在本课程里, 一个实用解法是 fresh `messages[]` 隔离: 以 `messages=[]` 启动一个子智能体。子智能体进行探索、读取文件、运行命令。完成后, 只有最终的文本响应返回给父智能体。子智能体的全部消息历史被丢弃。
## 解决方案 ## 解决方案
@ -124,11 +124,10 @@ def run_subagent(prompt: str) -> str:
| 上下文 | 单一共享 | 父 + 子隔离 | | 上下文 | 单一共享 | 父 + 子隔离 |
| Subagent | 无 | `run_subagent()` 函数 | | Subagent | 无 | `run_subagent()` 函数 |
| 返回值 | 不适用 | 仅摘要文本 | | 返回值 | 不适用 | 仅摘要文本 |
| Todo 系统 | TodoManager | 已移除 (非本节重点) |
## 设计原理 ## 设计原理
进程隔离免费提供了上下文隔离。全新的 `messages[]` 意味着子智能体不会被父级的对话历史干扰。代价是通信开销 -- 结果必须压缩回父级, 丢失细节。这与操作系统进程隔离的权衡相同: 用序列化成本换取安全性和整洁性。限制子智能体深度 (不允许递归生成) 防止无限资源消耗, 最大迭代次数确保失控的子进程能终止。 在本节中, fresh `messages[]` 隔离是一个近似实现上下文隔离的实用办法。全新的 `messages[]` 意味着子智能体从不携带父级历史开始。代价是通信开销 -- 结果必须压缩回父级, 丢失细节。这是消息历史隔离策略, 不是操作系统进程隔离本身。限制子智能体深度 (不允许递归生成) 防止无限资源消耗, 最大迭代次数确保失控的子任务能终止。
## 试一试 ## 试一试

View File

@ -4,7 +4,7 @@
## 问题 ## 问题
你希望智能体针对不同领域遵循特定的工作流: git 约定、测试模式、代码审查清单。简单粗暴的做法是把所有内容都塞进系统提示。但系统提示的有效注意力是有限的 -- 文本太多, 模型就会开始忽略其中一部分。 智能体需要针对不同领域遵循特定的工作流: git 约定、测试模式、代码审查清单。简单粗暴的做法是把所有内容都塞进系统提示。但系统提示的有效注意力是有限的 -- 文本太多, 模型就会开始忽略其中一部分。
如果你有 10 个技能, 每个 2000 token, 那就是 20,000 token 的系统提示。模型关注开头和结尾, 但会略过中间部分。更糟糕的是, 这些技能中大部分与当前任务无关。文件编辑任务不需要 git 工作流说明。 如果你有 10 个技能, 每个 2000 token, 那就是 20,000 token 的系统提示。模型关注开头和结尾, 但会略过中间部分。更糟糕的是, 这些技能中大部分与当前任务无关。文件编辑任务不需要 git 工作流说明。
@ -132,7 +132,6 @@ class SkillLoader:
| 系统提示 | 静态字符串 | + 技能描述列表 | | 系统提示 | 静态字符串 | + 技能描述列表 |
| 知识库 | 无 | .skills/*.md 文件 | | 知识库 | 无 | .skills/*.md 文件 |
| 注入方式 | 无 | 两层 (系统提示 + result) | | 注入方式 | 无 | 两层 (系统提示 + result) |
| Subagent | `run_subagent()` | 已移除 (非本节重点) |
## 设计原理 ## 设计原理

View File

@ -149,7 +149,6 @@ def agent_loop(messages):
| Auto-compact | 无 | token 阈值触发 | | Auto-compact | 无 | token 阈值触发 |
| Manual compact | 无 | `compact` 工具 | | Manual compact | 无 | `compact` 工具 |
| Transcripts | 无 | 保存到 .transcripts/ | | Transcripts | 无 | 保存到 .transcripts/ |
| Skills | load_skill | 已移除 (非本节重点) |
## 设计原理 ## 设计原理

View File

@ -10,7 +10,19 @@
更根本地说, 内存中的状态对其他智能体不可见。当我们最终构建团队 (s09+) 时, 队友需要一个共享的任务看板。内存中的数据结构是进程局部的。 更根本地说, 内存中的状态对其他智能体不可见。当我们最终构建团队 (s09+) 时, 队友需要一个共享的任务看板。内存中的数据结构是进程局部的。
解决方案是将任务作为 JSON 文件持久化在 `.tasks/` 目录中。每个任务是一个单独的文件, 包含 ID、主题、状态和依赖图。完成任务 1 会自动解除任务 2 的阻塞 (如果任务 2 有 `blockedBy: [1]`)。文件系统成为唯一的真实来源。 解决方案是将任务作为 JSON 文件持久化在 `.tasks/` 目录中。每个任务是一个单独的文件, 包含 ID、主题、状态和依赖图。完成任务 1 会自动解除任务 2 的阻塞 (如果任务 2 有 `blockedBy: [1]`)。在本教学实现里, 文件系统是任务状态的真实来源。
## Task vs Todo: 何时用哪个
从 s07 起, Task 是默认主线。Todo 仍可用于短期线性清单。
## 快速判定矩阵
| 场景 | 优先选择 | 原因 |
|---|---|---|
| 短时、单会话、线性清单 | Todo | 心智负担最低,记录最快 |
| 跨会话、存在依赖、多人协作 | Task | 状态可持久、依赖可表达、协作可见 |
| 一时拿不准 | Task | 后续降级更容易,半途迁移成本更低 |
## 解决方案 ## 解决方案
@ -132,17 +144,20 @@ class TaskManager:
## 相对 s06 的变更 ## 相对 s06 的变更
| 组件 | 之前 (s06) | 之后 (s07) | | 组件 | 之前 (s06) | 之后 (s07) |
|----------------|------------------|----------------------------------| |---|---|---|
| Tools | 5 | 8 (+task_create/update/list/get) | | Tools | 5 | 8 (`task_create/update/list/get`) |
| 状态存储 | 仅内存 | .tasks/ 中的 JSON 文件 | | 状态存储 | 仅内存 | `.tasks/` 中的 JSON 文件 |
| 依赖关系 | 无 | blockedBy + blocks 图 | | 依赖关系 | 无 | `blockedBy + blocks` 图 |
| 压缩机制 | 三层 | 已移除 (非本节重点) | | 持久化 | 压缩后丢失 | 压缩后存活 |
| 持久化 | 压缩后丢失 | 压缩后存活 |
## 设计原理 ## 设计原理
基于文件的状态能在上下文压缩中存活。当智能体的对话被压缩时, 内存中的状态会丢失, 但写入磁盘的任务会持久保存。依赖图确保即使在上下文丢失后也能按正确顺序执行。这是临时对话与持久工作之间的桥梁 -- 智能体可以忘记对话细节, 但始终有任务看板来提醒它还需要做什么。文件系统作为唯一真实来源也为未来的多智能体共享提供了基础, 因为任何进程都可以读取相同的 JSON 文件。 基于文件的状态能在上下文压缩中存活。当智能体的对话被压缩时, 内存中的状态会丢失, 但写入磁盘的任务会持久保存。依赖图确保即使在上下文丢失后也能按正确顺序执行。这是临时对话与持久工作之间的桥梁 -- 智能体可以忘记对话细节, 但始终有任务看板来提醒它还需要做什么。在本教学实现里, 文件系统作为任务状态真实来源也为未来的多智能体共享提供了基础, 因为任何进程都可以读取相同的 JSON 文件。
但“持久化”成立有前提:每次写入前都要重新读取任务文件,确认 `status/blockedBy` 与预期一致,再原子写回。否则并发写入很容易互相覆盖状态。
从课程设计上看, 这也是为什么 s07 之后我们默认采用 Task 而不是 Todo: 它更接近真实工程中的长期执行与协作需求。
## 试一试 ## 试一试

View File

@ -157,7 +157,6 @@ class BackgroundManager:
| 执行方式 | 仅阻塞 | 阻塞 + 后台线程 | | 执行方式 | 仅阻塞 | 阻塞 + 后台线程 |
| 通知机制 | 无 | 每轮排空的队列 | | 通知机制 | 无 | 每轮排空的队列 |
| 并发 | 无 | 守护线程 | | 并发 | 无 | 守护线程 |
| 任务系统 | 基于文件的 CRUD | 已移除 (非本节重点) |
## 设计原理 ## 设计原理

View File

@ -1,6 +1,6 @@
# s09: Agent Teams (智能体团队) # s09: Agent Teams (智能体团队)
> 持久化的队友通过 JSONL 收件箱将孤立的智能体转变为可通信的团队 -- spawn、message、broadcast 和 drain。 > 持久化的队友通过 JSONL 收件箱提供了一种教学协议, 将孤立的智能体转变为可通信的团队 -- spawn、message、broadcast 和 drain。
## 问题 ## 问题
@ -194,7 +194,7 @@ class MessageBus:
## 设计原理 ## 设计原理
基于文件的邮箱 (追加式 JSONL) 提供了并发安全的智能体间通信。追加操作在大多数文件系统上是原子的, 避免了锁竞争。"读取时排空" 模式 (读取全部, 截断) 提供批量传递。这比共享内存或基于 socket 的 IPC 更简单、更健壮。代价是延迟 -- 消息只在下一次轮询时才被看到 -- 但对于每轮需要数秒推理时间的 LLM 驱动智能体来说, 轮询延迟相比推理时间可以忽略不计 基于文件的邮箱 (追加式 JSONL) 在教学代码中具有可观察、易理解的优势。"读取时排空" 模式 (读取全部, 截断) 用很少的机制就能实现批量传递。代价是延迟 -- 消息只在下一次轮询时才被看到 -- 但对于每轮需要数秒推理时间的 LLM 驱动智能体来说, 本课程中该延迟是可接受的
## 试一试 ## 试一试

View File

@ -10,7 +10,7 @@
但自治智能体面临一个微妙问题: 上下文压缩后, 智能体可能忘记自己是谁。如果消息被摘要化, 原始系统提示中的身份 ("你是 alice, 角色: coder") 就会淡化。身份重注入通过在压缩后的上下文开头插入身份块来解决这个问题。 但自治智能体面临一个微妙问题: 上下文压缩后, 智能体可能忘记自己是谁。如果消息被摘要化, 原始系统提示中的身份 ("你是 alice, 角色: coder") 就会淡化。身份重注入通过在压缩后的上下文开头插入身份块来解决这个问题。
教学简化说明: 这里的 token 估算比较粗糙 (字符数 / 4)。生产系统使用专业的 tokenizer 库。s03 中的 nag 阈值 3 轮是为教学可见性设的低值; 生产环境的智能体通常使用约 10 轮的阈值。 注: token 估算使用字符数/4 (粗略)。nag 阈值 3 轮是为教学可见性设的低值。
## 解决方案 ## 解决方案

View File

@ -0,0 +1,193 @@
# s12: Worktree + 任务隔离
> 目录隔离, 任务 ID 协调 -- 用"任务板 (控制面) + worktree (执行面)"把并行改动从互相污染变成可追踪、可恢复、可收尾。
## 问题
s11 时, agent 已经能认领任务并协同推进。但所有任务共享同一个工作目录。两个 agent 同时改同一棵文件树时, 未提交的变更互相干扰, 任务状态和实际改动对不上, 收尾时也无法判断该保留还是清理哪些文件。
考虑一个具体场景: agent A 在做 auth 重构, agent B 在做登录页。两者都修改了 `config.py`。A 的半成品改动被 B 的 `git status` 看到, B 以为是自己的遗留, 尝试提交 -- 结果两个任务都坏了。
根因是"做什么"和"在哪里做"没有分开。任务板管目标, 但执行上下文是共享的。解决方案: 给每个任务分配独立的 git worktree 目录, 用任务 ID 把两边关联起来。
## 解决方案
```
控制面 (.tasks/) 执行面 (.worktrees/)
+------------------+ +------------------------+
| task_1.json | | auth-refactor/ |
| status: in_progress <----> branch: wt/auth-refactor
| worktree: "auth-refactor" | task_id: 1 |
+------------------+ +------------------------+
| task_2.json | | ui-login/ |
| status: pending <----> branch: wt/ui-login
| worktree: "ui-login" | task_id: 2 |
+------------------+ +------------------------+
|
index.json (worktree registry)
events.jsonl (lifecycle log)
```
三层状态:
1. 控制面 (What): `.tasks/task_*.json` -- 任务目标、责任归属、完成状态
2. 执行面 (Where): `.worktrees/index.json` -- 隔离目录路径、分支、存活状态
3. 运行态 (Now): 单轮内存上下文 -- 当前任务、当前 worktree、工具结果
状态机:
```text
Task: pending -> in_progress -> completed
Worktree: absent -> active -> removed | kept
```
## 工作原理
1. 创建任务, 把目标写入任务板。
```python
TASKS.create("Implement auth refactor")
# -> .tasks/task_1.json status=pending worktree=""
```
2. 创建 worktree 并绑定任务。传入 `task_id` 时自动把任务推进到 `in_progress`
```python
WORKTREES.create("auth-refactor", task_id=1)
# -> git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD
# -> index.json 追加 entry, task_1.json 绑定 worktree="auth-refactor"
```
3. 在隔离目录中执行命令。`cwd` 指向 worktree 路径, 主目录不受影响。
```python
WORKTREES.run("auth-refactor", "git status --short")
# -> subprocess.run(command, cwd=".worktrees/auth-refactor", ...)
```
4. 观测和回写。`worktree_status` 查看 git 状态, `task_update` 维护进度。
```python
WORKTREES.status("auth-refactor") # git status inside worktree
TASKS.update(1, owner="agent-A") # update task metadata
```
5. 收尾: 选择 keep 或 remove。`remove` 配合 `complete_task=true` 会同时完成任务并解绑 worktree。
```python
WORKTREES.remove("auth-refactor", complete_task=True)
# -> git worktree remove
# -> task_1.json status=completed, worktree=""
# -> index.json status=removed
# -> events.jsonl 写入 task.completed + worktree.remove.after
```
6. 进程中断后, 从 `.tasks/` + `.worktrees/index.json` 重建现场。会话记忆是易失的, 磁盘状态是持久的。
## 核心代码
事件流 -- append-only 生命周期日志 (来自 `agents/s12_worktree_task_isolation.py`):
```python
class EventBus:
def emit(self, event, task=None, worktree=None, error=None):
payload = {
"event": event,
"ts": time.time(),
"task": task or {},
"worktree": worktree or {},
}
if error:
payload["error"] = error
with self.path.open("a", encoding="utf-8") as f:
f.write(json.dumps(payload) + "\n")
```
事件流写入 `.worktrees/events.jsonl`, 每个关键操作发出三段式事件:
- `worktree.create.before / after / failed`
- `worktree.remove.before / after / failed`
- `task.completed` (当 `complete_task=true` 成功时)
事件负载形状:
```json
{
"event": "worktree.remove.after",
"task": {"id": 7, "status": "completed"},
"worktree": {"name": "auth-refactor", "path": "...", "status": "removed"},
"ts": 1730000000
}
```
任务绑定 -- Task 侧持有 worktree 名称:
```python
def bind_worktree(self, task_id: int, worktree: str, owner: str = "") -> str:
task = self._load(task_id)
task["worktree"] = worktree
if task["status"] == "pending":
task["status"] = "in_progress"
self._save(task)
```
隔离执行 -- cwd 路由到 worktree 目录:
```python
r = subprocess.run(
command,
shell=True,
cwd=path,
capture_output=True,
text=True,
timeout=300,
)
```
收尾联动 -- remove 同时完成任务:
```python
def remove(self, name, force=False, complete_task=False):
self._run_git(["worktree", "remove", wt["path"]])
if complete_task and wt.get("task_id") is not None:
self.tasks.update(wt["task_id"], status="completed")
self.tasks.unbind_worktree(wt["task_id"])
self.events.emit("task.completed", ...)
```
生命周期工具注册:
```python
"worktree_keep": lambda **kw: WORKTREES.keep(kw["name"]),
"worktree_events": lambda **kw: EVENTS.list_recent(kw.get("limit", 20)),
```
## 相对 s11 的变更
| 组件 | 之前 (s11) | 之后 (s12) |
|----------------|----------------------------|-----------------------------------------|
| 协调状态 | 任务板 (owner/status) | 任务板 + `worktree` 显式绑定 |
| 执行上下文 | 共享目录 | 每个任务可分配独立 worktree 目录 |
| 可恢复性 | 依赖任务状态 | 任务状态 + worktree 索引双重恢复 |
| 收尾语义 | 任务完成 | 任务完成 + worktree 显式 keep/remove |
| 生命周期可见性 | 隐式日志 | `.worktrees/events.jsonl` 显式事件流 |
## 设计原理
控制面/执行面分离是这一章的核心模式。Task 管"做什么", worktree 管"在哪做", 两者通过 task ID 关联但不强耦合。这意味着一个任务可以先不绑定 worktree (纯规划阶段), 也可以在多个 worktree 之间迁移。
显式状态机让每次迁移都可审计、可恢复。进程崩溃后, 从 `.tasks/``.worktrees/index.json` 两个文件就能重建全部现场, 不依赖会话内存。
事件流是旁路可观测层, 不替代主状态机写入。审计、通知、配额控制等副作用放在事件消费者中处理, 核心流程保持最小。`keep/remove` 作为显式收尾动作存在, 而不是隐式清理 -- agent 必须做出决策, 这个决策本身被记录。
## 试一试
```sh
cd learn-claude-code
python agents/s12_worktree_task_isolation.py
```
可以尝试的提示:
1. `Create tasks for backend auth and frontend login page, then list tasks.`
2. `Create worktree "auth-refactor" for task 1, create worktree "ui-login", then bind task 2 to "ui-login".`
3. `Run "git status --short" in worktree "auth-refactor".`
4. `Keep worktree "ui-login", then list worktrees and inspect worktree events.`
5. `Remove worktree "auth-refactor" with complete_task=true, then list tasks/worktrees/events.`

View File

@ -17,6 +17,7 @@ import s08Annotations from "@/data/annotations/s08.json";
import s09Annotations from "@/data/annotations/s09.json"; import s09Annotations from "@/data/annotations/s09.json";
import s10Annotations from "@/data/annotations/s10.json"; import s10Annotations from "@/data/annotations/s10.json";
import s11Annotations from "@/data/annotations/s11.json"; import s11Annotations from "@/data/annotations/s11.json";
import s12Annotations from "@/data/annotations/s12.json";
interface Decision { interface Decision {
id: string; id: string;
@ -44,6 +45,7 @@ const ANNOTATIONS: Record<string, AnnotationFile> = {
s09: s09Annotations as AnnotationFile, s09: s09Annotations as AnnotationFile,
s10: s10Annotations as AnnotationFile, s10: s10Annotations as AnnotationFile,
s11: s11Annotations as AnnotationFile, s11: s11Annotations as AnnotationFile,
s12: s12Annotations as AnnotationFile,
}; };
interface DesignDecisionsProps { interface DesignDecisionsProps {

View File

@ -20,6 +20,7 @@ const scenarioModules: Record<string, () => Promise<{ default: Scenario }>> = {
s09: () => import("@/data/scenarios/s09.json") as Promise<{ default: Scenario }>, s09: () => import("@/data/scenarios/s09.json") as Promise<{ default: Scenario }>,
s10: () => import("@/data/scenarios/s10.json") as Promise<{ default: Scenario }>, s10: () => import("@/data/scenarios/s10.json") as Promise<{ default: Scenario }>,
s11: () => import("@/data/scenarios/s11.json") as Promise<{ default: Scenario }>, s11: () => import("@/data/scenarios/s11.json") as Promise<{ default: Scenario }>,
s12: () => import("@/data/scenarios/s12.json") as Promise<{ default: Scenario }>,
}; };
interface AgentLoopSimulatorProps { interface AgentLoopSimulatorProps {

View File

@ -18,6 +18,7 @@ const visualizations: Record<
s09: lazy(() => import("./s09-agent-teams")), s09: lazy(() => import("./s09-agent-teams")),
s10: lazy(() => import("./s10-team-protocols")), s10: lazy(() => import("./s10-team-protocols")),
s11: lazy(() => import("./s11-autonomous-agents")), s11: lazy(() => import("./s11-autonomous-agents")),
s12: lazy(() => import("./s12-worktree-task-isolation")),
}; };
export function SessionVisualization({ version }: { version: string }) { export function SessionVisualization({ version }: { version: string }) {

View File

@ -62,7 +62,7 @@ const STEPS = [
{ {
title: "Clean Context", title: "Clean Context",
description: description:
"The parent gets a clean summary without context bloat. This is process isolation for LLMs.", "The parent gets a clean summary without context bloat. This is fresh-context isolation via messages[].",
}, },
]; ];

View File

@ -0,0 +1,278 @@
"use client";
import { motion } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
type TaskStatus = "pending" | "in_progress" | "completed";
interface TaskRow {
id: number;
subject: string;
status: TaskStatus;
worktree: string;
}
interface WorktreeRow {
name: string;
branch: string;
task: string;
state: "none" | "active" | "kept" | "removed";
}
interface Lane {
name: string;
files: string[];
highlight?: boolean;
}
interface StepState {
title: string;
desc: string;
tasks: TaskRow[];
worktrees: WorktreeRow[];
lanes: Lane[];
op: string;
}
const STEPS: StepState[] = [
{
title: "Single Workspace Pain",
desc: "Two tasks are active, but both edits would hit one directory and collide.",
op: "task_create x2",
tasks: [
{ id: 1, subject: "Auth refactor", status: "in_progress", worktree: "" },
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "" },
],
worktrees: [],
lanes: [
{ name: "main", files: ["auth/service.py", "ui/Login.tsx"], highlight: true },
{ name: "wt/auth-refactor", files: [] },
{ name: "wt/ui-login", files: [] },
],
},
{
title: "Allocate Lane for Task 1",
desc: "Create a worktree lane and associate it with task 1 for clear ownership.",
op: "worktree_create(name='auth-refactor', task_id=1)",
tasks: [
{ id: 1, subject: "Auth refactor", status: "in_progress", worktree: "auth-refactor" },
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "" },
],
worktrees: [
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "active" },
],
lanes: [
{ name: "main", files: ["ui/Login.tsx"] },
{ name: "wt/auth-refactor", files: ["auth/service.py"], highlight: true },
{ name: "wt/ui-login", files: [] },
],
},
{
title: "Allocate Lane for Task 2",
desc: "Lane creation and task association can be separate. Here task 2 binds after lane creation.",
op: "worktree_create(name='ui-login')\ntask_bind_worktree(task_id=2, worktree='ui-login')",
tasks: [
{ id: 1, subject: "Auth refactor", status: "in_progress", worktree: "auth-refactor" },
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "ui-login" },
],
worktrees: [
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "active" },
{ name: "ui-login", branch: "wt/ui-login", task: "#2", state: "active" },
],
lanes: [
{ name: "main", files: [] },
{ name: "wt/auth-refactor", files: ["auth/service.py"] },
{ name: "wt/ui-login", files: ["ui/Login.tsx"], highlight: true },
],
},
{
title: "Run Commands in Isolated Lanes",
desc: "Each command routes by selected lane directory, not by the shared root.",
op: "worktree_run('auth-refactor', 'pytest tests/auth -q')",
tasks: [
{ id: 1, subject: "Auth refactor", status: "in_progress", worktree: "auth-refactor" },
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "ui-login" },
],
worktrees: [
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "active" },
{ name: "ui-login", branch: "wt/ui-login", task: "#2", state: "active" },
],
lanes: [
{ name: "main", files: [] },
{ name: "wt/auth-refactor", files: ["auth/service.py", "tests/auth/test_login.py"], highlight: true },
{ name: "wt/ui-login", files: ["ui/Login.tsx", "ui/Login.css"] },
],
},
{
title: "Keep One Lane, Close Another",
desc: "Closeout can mix decisions: keep ui-login active for follow-up, remove auth-refactor and complete task 1.",
op: "worktree_keep('ui-login')\nworktree_remove('auth-refactor', complete_task=true)\nworktree_events(limit=10)",
tasks: [
{ id: 1, subject: "Auth refactor", status: "completed", worktree: "" },
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "ui-login" },
],
worktrees: [
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "removed" },
{ name: "ui-login", branch: "wt/ui-login", task: "#2", state: "kept" },
],
lanes: [
{ name: "main", files: [] },
{ name: "wt/auth-refactor", files: [] },
{ name: "wt/ui-login", files: ["ui/Login.tsx"], highlight: true },
],
},
{
title: "Isolation + Coordination + Events",
desc: "The board tracks shared truth, worktree lanes isolate execution, and events provide auditable side-channel traces.",
op: "task_list + worktree_list + worktree_events",
tasks: [
{ id: 1, subject: "Auth refactor", status: "completed", worktree: "" },
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "ui-login" },
],
worktrees: [
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "removed" },
{ name: "ui-login", branch: "wt/ui-login", task: "#2", state: "kept" },
],
lanes: [
{ name: "main", files: [] },
{ name: "wt/auth-refactor", files: [] },
{ name: "wt/ui-login", files: ["ui/Login.tsx"], highlight: true },
],
},
];
function statusClass(status: TaskStatus): string {
if (status === "completed") return "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300";
if (status === "in_progress") return "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300";
return "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300";
}
function worktreeClass(state: WorktreeRow["state"]): string {
if (state === "active") return "border-emerald-300 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-900/20";
if (state === "kept") return "border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20";
if (state === "removed") return "border-zinc-200 bg-zinc-100 opacity-70 dark:border-zinc-700 dark:bg-zinc-800";
return "border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900";
}
export default function WorktreeTaskIsolation({ title }: { title?: string }) {
const vis = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2600 });
const step = STEPS[vis.currentStep];
return (
<section className="min-h-[500px] space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "Worktree Task Isolation"}
</h2>
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<div className="mb-3 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 font-mono text-xs text-blue-700 dark:border-blue-900 dark:bg-blue-950/30 dark:text-blue-300">
{step.op}
</div>
<div className="grid gap-3 lg:grid-cols-3">
<div className="rounded-md border border-zinc-200 dark:border-zinc-700">
<div className="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
Task Board (.tasks)
</div>
<div className="space-y-2 p-2">
{step.tasks.map((task) => (
<motion.div
key={`${task.id}-${task.status}-${task.worktree}`}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
className="rounded border border-zinc-200 p-2 text-xs dark:border-zinc-700"
>
<div className="flex items-center justify-between gap-2">
<span className="font-mono text-zinc-500 dark:text-zinc-400">#{task.id}</span>
<span className={`rounded px-1.5 py-0.5 text-[10px] font-semibold ${statusClass(task.status)}`}>
{task.status}
</span>
</div>
<div className="mt-1 font-medium text-zinc-800 dark:text-zinc-100">{task.subject}</div>
<div className="mt-1 font-mono text-[10px] text-zinc-500 dark:text-zinc-400">
worktree: {task.worktree || "-"}
</div>
</motion.div>
))}
</div>
</div>
<div className="rounded-md border border-zinc-200 dark:border-zinc-700">
<div className="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
Worktree Index (.worktrees/index.json)
</div>
<div className="space-y-2 p-2">
{step.worktrees.length === 0 && (
<div className="rounded border border-dashed border-zinc-300 px-3 py-4 text-center text-xs text-zinc-500 dark:border-zinc-700 dark:text-zinc-400">
no worktrees yet
</div>
)}
{step.worktrees.map((wt) => (
<motion.div
key={`${wt.name}-${wt.state}`}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
className={`rounded border p-2 text-xs ${worktreeClass(wt.state)}`}
>
<div className="font-mono text-[11px] font-semibold text-zinc-800 dark:text-zinc-100">{wt.name}</div>
<div className="font-mono text-[10px] text-zinc-500 dark:text-zinc-400">{wt.branch}</div>
<div className="mt-1 text-[10px] text-zinc-600 dark:text-zinc-300">task: {wt.task}</div>
</motion.div>
))}
</div>
</div>
<div className="rounded-md border border-zinc-200 dark:border-zinc-700">
<div className="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
Execution Lanes
</div>
<div className="space-y-2 p-2">
{step.lanes.map((lane) => (
<motion.div
key={`${lane.name}-${lane.files.join(",")}`}
initial={{ opacity: 0, x: -4 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.25 }}
className={`rounded border p-2 text-xs ${
lane.highlight
? "border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20"
: "border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"
}`}
>
<div className="font-mono text-[11px] font-semibold text-zinc-800 dark:text-zinc-100">{lane.name}</div>
<div className="mt-1 space-y-1 font-mono text-[10px] text-zinc-500 dark:text-zinc-400">
{lane.files.length === 0 ? (
<div>(no changes)</div>
) : (
lane.files.map((f) => <div key={f}>{f}</div>)
)}
</div>
</motion.div>
))}
</div>
</div>
</div>
<div className="mt-4 rounded-md border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm dark:border-zinc-700 dark:bg-zinc-800/60">
<div className="font-medium text-zinc-800 dark:text-zinc-100">{step.title}</div>
<div className="text-zinc-600 dark:text-zinc-300">{step.desc}</div>
</div>
</div>
<StepControls
currentStep={vis.currentStep}
totalSteps={vis.totalSteps}
onPrev={vis.prev}
onNext={vis.next}
onReset={vis.reset}
isPlaying={vis.isPlaying}
onToggleAutoPlay={vis.toggleAutoPlay}
stepTitle={step.title}
stepDescription={step.desc}
/>
</section>
);
}

View File

@ -30,17 +30,59 @@
} }
}, },
{ {
"id": "task-replaces-todo", "id": "task-default-todo-coexistence",
"title": "TaskManager Replaces TodoWrite", "title": "Task as Course Default, Todo Still Useful",
"description": "TaskManager is the multi-agent evolution of TodoWrite. Same core concept (a list of items with statuses) but with critical additions: file persistence (survives crashes), dependency tracking (blocks/blockedBy), ownership (which agent is working on what), and multi-process safety. TodoWrite was designed for a single agent tracking its own work in memory. TaskManager is designed for a team of agents coordinating through the filesystem. The API is intentionally similar so the conceptual upgrade path is clear.", "description": "TaskManager extends the Todo mental model and becomes the default workflow from s07 onward in this course. This 'default' is a course sequencing choice, not a universal runtime default claim. Both track work items with statuses, but TaskManager adds file persistence (survives crashes), dependency tracking (blocks/blockedBy), ownership fields, and multi-process coordination. Todo remains useful for short, linear, one-shot tracking where heavyweight coordination is unnecessary.",
"alternatives": "Keeping TodoWrite for single-agent use and adding TaskManager only for multi-agent scenarios would avoid breaking the single-agent experience. But maintaining two systems with overlapping functionality increases complexity. TaskManager is a strict superset of TodoWrite -- a single agent using TaskManager just ignores the multi-agent features.", "alternatives": "Using only Todo keeps the model minimal but weak for long-running or collaborative work. Using only Task everywhere maximizes consistency but can feel heavy for tiny one-off tasks. Reminder signals are omission-reduction hints, not implicit mode switches; Task/Todo choice should still come from workflow intent and available tools.",
"zh": { "zh": {
"title": "TaskManager 取代 TodoWrite", "title": "Task 为课程主线Todo 仍有适用场景",
"description": "TaskManager 是 TodoWrite 的多代理进化版。核心概念相同带状态的项目列表但增加了关键能力文件持久化崩溃后存活、依赖追踪blocks/blockedBy、所有权哪个 agent 在处理什么、以及多进程安全。TodoWrite 为单 agent 在内存中追踪自身工作而设计。TaskManager 为代理团队通过文件系统协调而设计。API 刻意保持相似,使概念升级路径清晰。" "description": "TaskManager 延续了 Todo 的心智模型,并在本课程 s07 之后成为默认主线。两者都管理带状态的任务项,但 TaskManager 增加了文件持久化崩溃后可恢复、依赖追踪blocks/blockedBy、owner 字段与多进程协作能力。Todo 仍适合短、线性、一次性的轻量跟踪。"
}, },
"ja": { "ja": {
"title": "TaskManager が TodoWrite を置き換え", "title": "Task を主線にしつつ Todo も併存",
"description": "TaskManager は TodoWrite のマルチエージェント進化版です。コア概念は同じステータス付きの項目リストですが、重要な追加がありますファイル永続化クラッシュ後も存続、依存関係追跡blocks/blockedBy、所有権どのエージェントが何を担当しているか、マルチプロセス安全性。TodoWrite は単一エージェントがメモリ内で自身の作業を追跡するために設計されました。TaskManager はエージェントチームがファイルシステムを通じて連携するために設計されています。API は意図的に類似させ、概念的なアップグレードパスを明確にしています。" "description": "TaskManager は Todo のメンタルモデルを拡張し、本コースでは s07 以降のデフォルトになる。どちらもステータス付き作業項目を扱うが、TaskManager にはファイル永続化クラッシュ耐性、依存関係追跡blocks/blockedBy、owner、マルチプロセス協調がある。Todo は短く直線的な単発作業では引き続き有効。"
}
},
{
"id": "task-write-discipline",
"title": "Durability Needs Write Discipline",
"description": "File persistence reduces context loss, but it does not remove concurrent-write risks by itself. Before writing task state, reload the JSON, validate expected status/dependency fields, and then save atomically. This prevents one agent from silently overwriting another agent's transition.",
"alternatives": "Blind overwrite writes are simpler but can corrupt coordination state under parallel execution. A database with optimistic locking would enforce stronger safety, but the course keeps file-based state for zero-dependency teaching.",
"zh": {
"title": "持久化仍需要写入纪律",
"description": "文件持久化能降低上下文丢失,但不会自动消除并发写入风险。写任务状态前应先重读 JSON、校验 `status/blockedBy` 是否符合预期,再原子写回,避免不同 agent 悄悄覆盖彼此状态。"
},
"ja": {
"title": "耐久性には書き込み規律が必要",
"description": "ファイル永続化だけでは並行書き込み競合は防げない。更新前に JSON を再読込し、`status/blockedBy` を検証して原子的に保存することで、他エージェントの遷移上書きを防ぐ。"
}
},
{
"id": "reminder-advisory-not-switch",
"title": "Reminder Is Advisory, Not a Mode Switch",
"description": "Reminder signals should be treated as omission-reduction hints, not as control-plane switches. Choosing Task vs Todo should come from workflow intent and currently available tools, while reminders only nudge usage when tracking appears stale.",
"alternatives": "Treating reminders as implicit mode selectors looks convenient, but it hides decision boundaries and makes behavior harder to reason about during long sessions.",
"zh": {
"title": "Reminder 是提示,不是模式开关",
"description": "Reminder 信号用于降低遗漏不应当被当作控制面的模式切换器。Task/Todo 的选择应由工作流意图与可用工具决定,提醒只在追踪滞后时提供轻量提示。"
},
"ja": {
"title": "Reminder は助言でありモード切替ではない",
"description": "Reminder は取りこぼしを減らすための助言であり、制御面のモード切替として扱わない。Task/Todo の選択はワークフロー意図と利用可能ツールで決め、Reminder は追跡が滞ったときに軽く促す。"
}
},
{
"id": "todo-task-fast-matrix",
"title": "Todo/Task Fast Decision Matrix",
"description": "Use Todo for short one-session linear checklists. Use Task for cross-session work, dependencies, or teammate coordination. If uncertain, start with Task because downscoping is cheaper than migrating state mid-run.",
"alternatives": "Always using Todo keeps the model minimal but breaks durability and collaboration. Always using Task maximizes consistency but may feel heavy for tiny one-shot notes.",
"zh": {
"title": "Todo/Task 快速判定矩阵",
"description": "短时单会话线性清单用 Todo跨会话、依赖、多人协作用 Task拿不准时先用 Task因为后续降级比半途迁移状态更便宜。"
},
"ja": {
"title": "Todo/Task クイック判定マトリクス",
"description": "短い単一セッションの直線タスクは Todo、セッション跨ぎや依存・協調がある作業は Task。迷うなら Task 開始が安全で、後で簡略化する方が途中移行より低コスト。"
} }
} }
] ]

View File

@ -0,0 +1,103 @@
{
"version": "s12",
"decisions": [
{
"id": "shared-board-isolated-lanes",
"title": "Shared Task Board + Isolated Execution Lanes",
"description": "The task board remains shared and centralized in `.tasks/`, while file edits happen in per-task worktree directories. This separation preserves global visibility (who owns what, what is done) without forcing everyone to edit inside one mutable directory. Coordination stays simple because there is one board, and execution stays safe because each lane is isolated.",
"alternatives": "A single shared workspace is simpler but causes edit collisions and mixed git state. Fully independent task stores per lane avoid collisions but lose team-level visibility and make planning harder.",
"zh": {
"title": "共享任务板 + 隔离执行通道",
"description": "任务板继续集中在 `.tasks/`,而文件改动发生在按任务划分的 worktree 目录中。这样既保留了全局可见性(谁在做什么、完成到哪),又避免所有人同时写同一目录导致冲突。协调层简单(一个任务板),执行层安全(多条隔离通道)。"
},
"ja": {
"title": "共有タスクボード + 分離実行レーン",
"description": "タスクボードは `.tasks/` に集約しつつ、実際の編集はタスクごとの worktree ディレクトリで行う。これにより全体の可視性担当と進捗を維持しながら、単一ディレクトリでの衝突を回避できる。調整は1つのボードで単純化され、実行はレーン分離で安全になる。"
}
},
{
"id": "index-file-lifecycle",
"title": "Explicit Worktree Lifecycle Index",
"description": "`.worktrees/index.json` records each worktree's name, path, branch, task_id, and status. This makes lifecycle state inspectable and recoverable even after context compression or process restarts. The index also provides a deterministic source for list/status/remove operations.",
"alternatives": "Relying only on `git worktree list` removes local bookkeeping but loses task binding metadata and custom lifecycle states. Keeping all state only in memory is simpler in code but breaks recoverability.",
"zh": {
"title": "显式 worktree 生命周期索引",
"description": "`.worktrees/index.json` 记录每个 worktree 的名称、路径、分支、task_id 与状态。即使上下文压缩或进程重启,这些生命周期状态仍可检查和恢复。它也为 list/status/remove 提供了确定性的本地数据源。"
},
"ja": {
"title": "明示的な worktree ライフサイクル索引",
"description": "`.worktrees/index.json` に name/path/branch/task_id/status を記録することで、コンテキスト圧縮やプロセス再起動後も状態を追跡できる。list/status/remove の挙動もこの索引を基準に決定できる。"
}
},
{
"id": "lane-cwd-routing-and-reentry-guard",
"title": "Lane-Scoped CWD Routing + Re-entry Guard",
"description": "This course runtime uses lane-scoped cwd routing (`worktree_run(name, command)`). Other runtimes may choose session-level cwd switches. The design goal is predictable lane context with a re-entry guard when already inside an active worktree context.",
"alternatives": "Global cwd mutation is easy to implement but can leak context across parallel work. Allowing silent re-entry makes lifecycle ownership ambiguous and complicates teardown behavior.",
"zh": {
"title": "按通道 cwd 路由 + 禁止重入",
"description": "本课程运行时采用按通道 `cwd` 路由(`worktree_run(name, command)`)。其他运行时也可能选择会话级 cwd 切换。设计目标是让并行通道可预测,并在已处于 active worktree 上下文时通过重入保护避免二次进入。"
},
"ja": {
"title": "レーン単位 cwd ルーティング + 再入防止",
"description": "本コース実装では `worktree_run(name, command)` によるレーン単位 cwd ルーティングを採用する。実装によってはセッション単位で cwd を切り替える場合もある。狙いは並列レーンの予測可能性を保ち、active な worktree 文脈での再入を防ぐこと。"
}
},
{
"id": "event-stream-observability",
"title": "Append-Only Lifecycle Event Stream",
"description": "Lifecycle events are appended to `.worktrees/events.jsonl` (`worktree.create.*`, `worktree.remove.*`, `task.completed`). This turns hidden transitions into queryable records and makes failures explicit (`*.failed`) instead of silent.",
"alternatives": "Relying only on console logs is lighter but fragile during long sessions and hard to audit. A full event bus infrastructure is powerful but heavier than needed for this teaching baseline.",
"zh": {
"title": "追加式生命周期事件流",
"description": "生命周期事件写入 `.worktrees/events.jsonl`(如 `worktree.create.*`、`worktree.remove.*`、`task.completed`)。这样状态迁移可查询、可追踪,失败也会以 `*.failed` 显式暴露,而不是静默丢失。"
},
"ja": {
"title": "追記型ライフサイクルイベント",
"description": "ライフサイクルイベントを `.worktrees/events.jsonl` に追記する(`worktree.create.*`、`worktree.remove.*`、`task.completed` など)。遷移が可観測になり、失敗も `*.failed` として明示できる。"
}
},
{
"id": "hook-style-extension",
"title": "Hook-Style Extensions via Event Triplets",
"description": "Treat `before/after/failed` lifecycle emissions as extension points. Keep source-of-truth state writes in task/worktree files, and run side effects (audit, notification, policy checks) in event consumers.",
"alternatives": "Embedding every side effect directly in create/remove logic couples concerns tightly and makes failure handling harder. Moving source-of-truth to event replay is also risky without strict idempotency/repair semantics.",
"zh": {
"title": "通过三段事件实现 Hook 风格扩展",
"description": "把 `before/after/failed` 生命周期事件当作扩展插槽。真实状态写入仍留在 task/worktree 文件,审计、通知、策略检查等副作用交给事件消费者。"
},
"ja": {
"title": "三段イベントによる Hook 風拡張",
"description": "`before/after/failed` ライフサイクルイベントを拡張ポイントとして使う。正準状態は task/worktree ファイルに残し、副作用(監査・通知・ポリシーチェック)はイベント購読側で処理する。"
}
},
{
"id": "task-worktree-closeout",
"title": "Close Task and Workspace Together",
"description": "`worktree_remove(..., complete_task=true)` allows a single closeout step: remove the isolated directory and mark the bound task completed. In this course model, closeout remains an explicit tool-driven transition (`worktree_keep` / `worktree_remove`) rather than hidden automatic cleanup. This reduces dangling state where a task says done but its temporary lane remains active (or the reverse).",
"alternatives": "Keeping closeout fully manual gives flexibility but increases operational drift. Fully automatic removal on every completion risks deleting a workspace before final review.",
"zh": {
"title": "任务与工作区一起收尾",
"description": "`worktree_remove(..., complete_task=true)` 允许在一个动作里完成收尾:删除隔离目录并把绑定任务标记为 completed。在本课程模型里收尾保持为显式工具驱动迁移`worktree_keep` / `worktree_remove`),而不是隐藏的自动清理。这样可减少状态悬挂(任务已完成但临时工作区仍活跃,或反过来)。"
},
"ja": {
"title": "タスクとワークスペースを同時にクローズ",
"description": "`worktree_remove(..., complete_task=true)` により、分離ディレクトリ削除とタスク完了更新を1ステップで実行できる。本コースのモデルでは、クローズ処理は `worktree_keep` / `worktree_remove` の明示ツール遷移として扱い、暗黙の自動清掃にはしない。完了済みタスクに未回収レーンが残る、といったズレを減らせる。"
}
},
{
"id": "event-stream-side-channel",
"title": "Event Stream Is Observability Side-Channel",
"description": "Lifecycle events improve auditability, but the source of truth remains task/worktree state files. Events should be read as transition traces, not as a replacement state machine.",
"alternatives": "Using logs alone hides structured transitions; using events as the only state source risks drift when replay/repair semantics are undefined.",
"zh": {
"title": "事件流是观测旁路,不是状态机替身",
"description": "生命周期事件提升可审计性,但真实状态源仍是任务/工作区状态文件。事件更适合做迁移轨迹,而不是替代主状态机。"
},
"ja": {
"title": "イベントは観測サイドチャネルであり状態機械の代替ではない",
"description": "ライフサイクルイベントは監査性を高めるが、真の状態源は task/worktree 状態ファイルのまま。イベントは遷移トレースとして扱い、主状態機械の代替にしない。"
}
}
]
}

View File

@ -271,6 +271,43 @@ export const EXECUTION_FLOWS: Record<string, FlowDefinition> = {
{ from: "poll", to: "inbox" }, { from: "poll", to: "inbox" },
], ],
}, },
s12: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "llm", label: "LLM Call", type: "process", x: COL_CENTER, y: 110 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 190 },
{ id: "is_wt", label: "worktree tool?", type: "decision", x: COL_LEFT, y: 280 },
{ id: "task", label: "Task Board\\n(.tasks)", type: "process", x: 60, y: 360 },
{ id: "wt_create", label: "Allocate / Enter\\nWorktree", type: "subprocess", x: 60, y: 440 },
{ id: "wt_run", label: "Run in\\nIsolated Dir", type: "subprocess", x: COL_LEFT + 80, y: 360 },
{ id: "wt_close", label: "Closeout:\\nworktree_keep / remove", type: "process", x: COL_LEFT + 80, y: 440 },
{ id: "events", label: "Emit Lifecycle Events\\n(side-channel)", type: "process", x: COL_RIGHT, y: 420 },
{ id: "events_read", label: "Optional Read\\nworktree_events", type: "subprocess", x: COL_RIGHT, y: 520 },
{ id: "append", label: "Append Result", type: "process", x: COL_CENTER, y: 530 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 280 },
],
edges: [
{ from: "start", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "is_wt", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "is_wt", to: "task", label: "task ops" },
{ from: "is_wt", to: "wt_create", label: "create/bind" },
{ from: "is_wt", to: "wt_run", label: "run/status" },
{ from: "task", to: "wt_create", label: "allocate lane" },
{ from: "wt_create", to: "wt_run" },
{ from: "task", to: "append", label: "task result" },
{ from: "wt_create", to: "events", label: "emit create" },
{ from: "wt_create", to: "append", label: "create result" },
{ from: "wt_run", to: "wt_close" },
{ from: "wt_run", to: "append", label: "run/status result" },
{ from: "wt_close", to: "events", label: "emit closeout" },
{ from: "wt_close", to: "append", label: "closeout result" },
{ from: "events", to: "events_read", label: "optional query" },
{ from: "events_read", to: "append", label: "events result" },
{ from: "append", to: "llm" },
],
},
}; };
export function getFlowForVersion(version: string): FlowDefinition | null { export function getFlowForVersion(version: string): FlowDefinition | null {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,51 @@
{
"version": "s12",
"title": "Worktree + Task Isolation",
"description": "Use a shared task board with optional worktree lanes for clean parallel execution",
"steps": [
{
"type": "user_message",
"content": "Implement auth refactor and login UI updates in parallel",
"annotation": "Two active tasks in one workspace would collide"
},
{
"type": "tool_call",
"content": "task_create(subject: \"Auth refactor\")\ntask_create(subject: \"Login UI polish\")",
"toolName": "task_manager",
"annotation": "Shared board remains the coordination source of truth"
},
{
"type": "tool_call",
"content": "worktree_create(name: \"auth-refactor\", task_id: 1)\nworktree_create(name: \"ui-login\")\ntask_bind_worktree(task_id: 2, worktree: \"ui-login\")",
"toolName": "worktree_manager",
"annotation": "Lane allocation and task association are composable; task 2 binds after lane creation"
},
{
"type": "system_event",
"content": "worktree.create.before/after emitted\n.tasks/task_1.json -> { status: \"in_progress\", worktree: \"auth-refactor\" }\n.tasks/task_2.json -> { status: \"in_progress\", worktree: \"ui-login\" }\n.worktrees/index.json updated",
"annotation": "Control-plane state remains canonical; hook-style consumers can react to lifecycle events without owning canonical state writes"
},
{
"type": "tool_call",
"content": "worktree_run(name: \"auth-refactor\", command: \"pytest tests/auth -q\")\nworktree_run(name: \"ui-login\", command: \"npm test -- login\")",
"toolName": "worktree_run",
"annotation": "In this teaching runtime, commands route by lane-scoped cwd; other runtimes may use session-level directory switches. The invariant is explicit execution context."
},
{
"type": "tool_call",
"content": "worktree_keep(name: \"ui-login\")\nworktree_remove(name: \"auth-refactor\", complete_task: true)\nworktree_events(limit: 10)",
"toolName": "worktree_manager",
"annotation": "Closeout is explicit tool-driven state transition: mix keep/remove decisions and query lifecycle events in one pass"
},
{
"type": "system_event",
"content": "worktree.keep emitted for ui-login\nworktree.remove.before/after emitted for auth-refactor\ntask.completed emitted for #1\n.worktrees/events.jsonl appended",
"annotation": "Lifecycle transitions become explicit records while task/worktree files remain source-of-truth"
},
{
"type": "assistant_text",
"content": "Task board handles coordination, worktrees handle isolation. Parallel tracks stay clean and auditable.",
"annotation": "Coordinate in one board, isolate by lane only where needed, and run optional policy/audit side effects from lifecycle events"
}
]
}

View File

@ -1,10 +1,10 @@
{ {
"meta": { "title": "Learn Claude Code", "description": "Build an AI coding agent from scratch, one concept at a time" }, "meta": { "title": "Learn Claude Code", "description": "Build a nano Claude Code-like agent from 0 to 1, one mechanism at a time" },
"nav": { "home": "Home", "timeline": "Timeline", "compare": "Compare", "layers": "Layers", "github": "GitHub" }, "nav": { "home": "Home", "timeline": "Timeline", "compare": "Compare", "layers": "Layers", "github": "GitHub" },
"home": { "hero_title": "Learn Claude Code", "hero_subtitle": "Build an AI coding agent from scratch, one concept at a time", "start": "Start Learning", "core_pattern": "The Core Pattern", "core_pattern_desc": "Every AI coding agent shares the same loop: call the model, execute tools, feed results back. Everything else is details.", "learning_path": "Learning Path", "learning_path_desc": "11 progressive sessions, from a simple loop to full autonomous teams", "layers_title": "Architectural Layers", "layers_desc": "Five orthogonal concerns that compose into a complete agent", "loc": "LOC", "learn_more": "Learn More", "versions_in_layer": "versions", "message_flow": "Message Growth", "message_flow_desc": "Watch the messages array grow as the agent loop executes" }, "home": { "hero_title": "Learn Claude Code", "hero_subtitle": "Build a nano Claude Code-like agent from 0 to 1, one mechanism at a time", "start": "Start Learning", "core_pattern": "The Core Pattern", "core_pattern_desc": "Every AI coding agent shares the same loop: call the model, execute tools, feed results back. Production systems add policy, permissions, and lifecycle layers on top.", "learning_path": "Learning Path", "learning_path_desc": "12 progressive sessions, from a simple loop to isolated autonomous execution", "layers_title": "Architectural Layers", "layers_desc": "Five orthogonal concerns that compose into a complete agent", "loc": "LOC", "learn_more": "Learn More", "versions_in_layer": "versions", "message_flow": "Message Growth", "message_flow_desc": "Watch the messages array grow as the agent loop executes" },
"version": { "loc": "lines of code", "tools": "tools", "new": "New", "prev": "Previous", "next": "Next", "view_source": "View Source", "view_diff": "View Diff", "design_decisions": "Design Decisions", "whats_new": "What's New", "tutorial": "Tutorial", "simulator": "Agent Loop Simulator", "execution_flow": "Execution Flow", "architecture": "Architecture", "concept_viz": "Concept Visualization", "alternatives": "Alternatives Considered", "tab_learn": "Learn", "tab_simulate": "Simulate", "tab_code": "Code", "tab_deep_dive": "Deep Dive" }, "version": { "loc": "lines of code", "tools": "tools", "new": "New", "prev": "Previous", "next": "Next", "view_source": "View Source", "view_diff": "View Diff", "design_decisions": "Design Decisions", "whats_new": "What's New", "tutorial": "Tutorial", "simulator": "Agent Loop Simulator", "execution_flow": "Execution Flow", "architecture": "Architecture", "concept_viz": "Concept Visualization", "alternatives": "Alternatives Considered", "tab_learn": "Learn", "tab_simulate": "Simulate", "tab_code": "Code", "tab_deep_dive": "Deep Dive" },
"sim": { "play": "Play", "pause": "Pause", "step": "Step", "reset": "Reset", "speed": "Speed", "step_of": "of" }, "sim": { "play": "Play", "pause": "Pause", "step": "Step", "reset": "Reset", "speed": "Speed", "step_of": "of" },
"timeline": { "title": "Learning Path", "subtitle": "s01 to s11: Progressive Agent Design", "layer_legend": "Layer Legend", "loc_growth": "LOC Growth", "learn_more": "Learn More" }, "timeline": { "title": "Learning Path", "subtitle": "s01 to s12: Progressive Agent Design", "layer_legend": "Layer Legend", "loc_growth": "LOC Growth", "learn_more": "Learn More" },
"layers": { "layers": {
"title": "Architectural Layers", "title": "Architectural Layers",
"subtitle": "Five orthogonal concerns that compose into a complete agent", "subtitle": "Five orthogonal concerns that compose into a complete agent",
@ -49,7 +49,8 @@
"s08": "Background Tasks", "s08": "Background Tasks",
"s09": "Agent Teams", "s09": "Agent Teams",
"s10": "Team Protocols", "s10": "Team Protocols",
"s11": "Autonomous Agents" "s11": "Autonomous Agents",
"s12": "Worktree + Task Isolation"
}, },
"layer_labels": { "layer_labels": {
"tools": "Tools & Execution", "tools": "Tools & Execution",
@ -69,6 +70,7 @@
"s08": "Background Task Lanes", "s08": "Background Task Lanes",
"s09": "Agent Team Mailboxes", "s09": "Agent Team Mailboxes",
"s10": "FSM Team Protocols", "s10": "FSM Team Protocols",
"s11": "Autonomous Agent Cycle" "s11": "Autonomous Agent Cycle",
"s12": "Worktree Task Isolation"
} }
} }

View File

@ -1,10 +1,10 @@
{ {
"meta": { "title": "Learn Claude Code", "description": "AIコーディングエージェントをゼロから構築、一つずつ概念を追加" }, "meta": { "title": "Learn Claude Code", "description": "0 から 1 へ nano Claude Code-like agent を構築し、毎回 1 つの仕組みを追加" },
"nav": { "home": "ホーム", "timeline": "学習パス", "compare": "バージョン比較", "layers": "アーキテクチャ層", "github": "GitHub" }, "nav": { "home": "ホーム", "timeline": "学習パス", "compare": "バージョン比較", "layers": "アーキテクチャ層", "github": "GitHub" },
"home": { "hero_title": "Learn Claude Code", "hero_subtitle": "AIコーディングエージェントをゼロから構築、一つずつ概念を追加", "start": "学習を始める", "core_pattern": "コアパターン", "core_pattern_desc": "全てのAIコーディングエージェントは同じループを共有:モデルを呼び出し、ツールを実行し、結果をフィードバック。他は全て詳細。", "learning_path": "学習パス", "learning_path_desc": "11の段階的セッション、シンプルなループから完全自律チームまで", "layers_title": "アーキテクチャ層", "layers_desc": "5つの直交する関心事が完全なエージェントを構成", "loc": "行", "learn_more": "詳細を見る", "versions_in_layer": "バージョン", "message_flow": "メッセージの増加", "message_flow_desc": "エージェントループ実行時のメッセージ配列の成長を観察" }, "home": { "hero_title": "Learn Claude Code", "hero_subtitle": "0 から 1 へ nano Claude Code-like agent を構築し、毎回 1 つの仕組みを追加", "start": "学習を始める", "core_pattern": "コアパターン", "core_pattern_desc": "すべての AI コーディングエージェントは同じループを共有する:モデルを呼び出し、ツールを実行し、結果を返す。実運用ではこの上にポリシー、権限、ライフサイクル層が重なる。", "learning_path": "学習パス", "learning_path_desc": "12の段階的セッション、シンプルなループから分離された自律実行まで", "layers_title": "アーキテクチャ層", "layers_desc": "5つの直交する関心事が完全なエージェントを構成", "loc": "行", "learn_more": "詳細を見る", "versions_in_layer": "バージョン", "message_flow": "メッセージの増加", "message_flow_desc": "エージェントループ実行時のメッセージ配列の成長を観察" },
"version": { "loc": "行のコード", "tools": "ツール", "new": "新規", "prev": "前のバージョン", "next": "次のバージョン", "view_source": "ソースを見る", "view_diff": "差分を見る", "design_decisions": "設計判断", "whats_new": "新機能", "tutorial": "チュートリアル", "simulator": "エージェントループシミュレーター", "execution_flow": "実行フロー", "architecture": "アーキテクチャ", "concept_viz": "コンセプト可視化", "alternatives": "検討された代替案", "tab_learn": "学習", "tab_simulate": "シミュレーション", "tab_code": "ソースコード", "tab_deep_dive": "詳細分析" }, "version": { "loc": "行のコード", "tools": "ツール", "new": "新規", "prev": "前のバージョン", "next": "次のバージョン", "view_source": "ソースを見る", "view_diff": "差分を見る", "design_decisions": "設計判断", "whats_new": "新機能", "tutorial": "チュートリアル", "simulator": "エージェントループシミュレーター", "execution_flow": "実行フロー", "architecture": "アーキテクチャ", "concept_viz": "コンセプト可視化", "alternatives": "検討された代替案", "tab_learn": "学習", "tab_simulate": "シミュレーション", "tab_code": "ソースコード", "tab_deep_dive": "詳細分析" },
"sim": { "play": "再生", "pause": "一時停止", "step": "ステップ", "reset": "リセット", "speed": "速度", "step_of": "/" }, "sim": { "play": "再生", "pause": "一時停止", "step": "ステップ", "reset": "リセット", "speed": "速度", "step_of": "/" },
"timeline": { "title": "学習パス", "subtitle": "s01からs11へ:段階的エージェント設計", "layer_legend": "レイヤー凡例", "loc_growth": "コード量の推移", "learn_more": "詳細を見る" }, "timeline": { "title": "学習パス", "subtitle": "s01からs12へ:段階的エージェント設計", "layer_legend": "レイヤー凡例", "loc_growth": "コード量の推移", "learn_more": "詳細を見る" },
"layers": { "layers": {
"title": "アーキテクチャ層", "title": "アーキテクチャ層",
"subtitle": "5つの直交する関心事が完全なエージェントを構成", "subtitle": "5つの直交する関心事が完全なエージェントを構成",
@ -49,7 +49,8 @@
"s08": "バックグラウンドタスク", "s08": "バックグラウンドタスク",
"s09": "エージェントチーム", "s09": "エージェントチーム",
"s10": "チームプロトコル", "s10": "チームプロトコル",
"s11": "自律エージェント" "s11": "自律エージェント",
"s12": "Worktree + タスク分離"
}, },
"layer_labels": { "layer_labels": {
"tools": "ツールと実行", "tools": "ツールと実行",
@ -69,6 +70,7 @@
"s08": "バックグラウンドタスクレーン", "s08": "バックグラウンドタスクレーン",
"s09": "エージェントチーム メールボックス", "s09": "エージェントチーム メールボックス",
"s10": "FSM チームプロトコル", "s10": "FSM チームプロトコル",
"s11": "自律エージェントサイクル" "s11": "自律エージェントサイクル",
"s12": "Worktree タスク分離"
} }
} }

View File

@ -1,10 +1,10 @@
{ {
"meta": { "title": "Learn Claude Code", "description": "从零构建 AI 编程 Agent每次只加一个概念" }, "meta": { "title": "Learn Claude Code", "description": "从 0 到 1 构建 nano Claude Code-like agent每次只加一个机制" },
"nav": { "home": "首页", "timeline": "学习路径", "compare": "版本对比", "layers": "架构层", "github": "GitHub" }, "nav": { "home": "首页", "timeline": "学习路径", "compare": "版本对比", "layers": "架构层", "github": "GitHub" },
"home": { "hero_title": "Learn Claude Code", "hero_subtitle": "从零构建 AI 编程 Agent每次只加一个概念", "start": "开始学习", "core_pattern": "核心模式", "core_pattern_desc": "所有 AI 编程 Agent 共享同一个循环:调用模型、执行工具、回传结果。其他都是细节。", "learning_path": "学习路径", "learning_path_desc": "11 个渐进式课程,从简单循环到完整自主团队", "layers_title": "架构层次", "layers_desc": "五个正交关注点组合成完整的 Agent", "loc": "行", "learn_more": "了解更多", "versions_in_layer": "个版本", "message_flow": "消息增长", "message_flow_desc": "观察 Agent 循环执行时消息数组的增长" }, "home": { "hero_title": "Learn Claude Code", "hero_subtitle": "从 0 到 1 构建 nano Claude Code-like agent每次只加一个机制", "start": "开始学习", "core_pattern": "核心模式", "core_pattern_desc": "所有 AI 编程 Agent 共享同一个循环:调用模型、执行工具、回传结果。生产级系统会在其上叠加策略、权限和生命周期层。", "learning_path": "学习路径", "learning_path_desc": "12 个渐进式课程,从简单循环到隔离化自治执行", "layers_title": "架构层次", "layers_desc": "五个正交关注点组合成完整的 Agent", "loc": "行", "learn_more": "了解更多", "versions_in_layer": "个版本", "message_flow": "消息增长", "message_flow_desc": "观察 Agent 循环执行时消息数组的增长" },
"version": { "loc": "行代码", "tools": "个工具", "new": "新增", "prev": "上一版", "next": "下一版", "view_source": "查看源码", "view_diff": "查看变更", "design_decisions": "设计决策", "whats_new": "新增内容", "tutorial": "教程", "simulator": "Agent 循环模拟器", "execution_flow": "执行流程", "architecture": "架构", "concept_viz": "概念可视化", "alternatives": "替代方案", "tab_learn": "学习", "tab_simulate": "模拟", "tab_code": "源码", "tab_deep_dive": "深入探索" }, "version": { "loc": "行代码", "tools": "个工具", "new": "新增", "prev": "上一版", "next": "下一版", "view_source": "查看源码", "view_diff": "查看变更", "design_decisions": "设计决策", "whats_new": "新增内容", "tutorial": "教程", "simulator": "Agent 循环模拟器", "execution_flow": "执行流程", "architecture": "架构", "concept_viz": "概念可视化", "alternatives": "替代方案", "tab_learn": "学习", "tab_simulate": "模拟", "tab_code": "源码", "tab_deep_dive": "深入探索" },
"sim": { "play": "播放", "pause": "暂停", "step": "单步", "reset": "重置", "speed": "速度", "step_of": "/" }, "sim": { "play": "播放", "pause": "暂停", "step": "单步", "reset": "重置", "speed": "速度", "step_of": "/" },
"timeline": { "title": "学习路径", "subtitle": "s01 到 s11:渐进式 Agent 设计", "layer_legend": "层次图例", "loc_growth": "代码量增长", "learn_more": "了解更多" }, "timeline": { "title": "学习路径", "subtitle": "s01 到 s12:渐进式 Agent 设计", "layer_legend": "层次图例", "loc_growth": "代码量增长", "learn_more": "了解更多" },
"layers": { "layers": {
"title": "架构层次", "title": "架构层次",
"subtitle": "五个正交关注点组合成完整的 Agent", "subtitle": "五个正交关注点组合成完整的 Agent",
@ -49,7 +49,8 @@
"s08": "后台任务", "s08": "后台任务",
"s09": "Agent 团队", "s09": "Agent 团队",
"s10": "团队协议", "s10": "团队协议",
"s11": "自主 Agent" "s11": "自主 Agent",
"s12": "Worktree + 任务隔离"
}, },
"layer_labels": { "layer_labels": {
"tools": "工具与执行", "tools": "工具与执行",
@ -69,6 +70,7 @@
"s08": "后台任务通道", "s08": "后台任务通道",
"s09": "Agent 团队邮箱", "s09": "Agent 团队邮箱",
"s10": "FSM 团队协议", "s10": "FSM 团队协议",
"s11": "自主 Agent 循环" "s11": "自主 Agent 循环",
"s12": "Worktree 任务隔离"
} }
} }

View File

@ -1,5 +1,5 @@
export const VERSION_ORDER = [ export const VERSION_ORDER = [
"s01", "s02", "s03", "s04", "s05", "s06", "s07", "s08", "s09", "s10", "s11" "s01", "s02", "s03", "s04", "s05", "s06", "s07", "s08", "s09", "s10", "s11", "s12"
] as const; ] as const;
export const LEARNING_PATH = VERSION_ORDER; export const LEARNING_PATH = VERSION_ORDER;
@ -14,10 +14,10 @@ export const VERSION_META: Record<string, {
layer: "tools" | "planning" | "memory" | "concurrency" | "collaboration"; layer: "tools" | "planning" | "memory" | "concurrency" | "collaboration";
prevVersion: string | null; prevVersion: string | null;
}> = { }> = {
s01: { title: "The Agent Loop", subtitle: "Bash is All You Need", coreAddition: "Single-tool agent loop", keyInsight: "The entire agent is a while loop + one tool", layer: "tools", prevVersion: null }, s01: { title: "The Agent Loop", subtitle: "Bash is All You Need", coreAddition: "Single-tool agent loop", keyInsight: "The minimal agent kernel is a while loop + one tool", layer: "tools", prevVersion: null },
s02: { title: "Tools", subtitle: "The Loop Didn't Change", coreAddition: "Tool dispatch map", keyInsight: "Adding tools means adding handlers, the loop stays the same", layer: "tools", prevVersion: "s01" }, s02: { title: "Tools", subtitle: "The Loop Didn't Change", coreAddition: "Tool dispatch map", keyInsight: "Adding tools means adding handlers, not rewriting the loop", layer: "tools", prevVersion: "s01" },
s03: { title: "TodoWrite", subtitle: "Plan Before You Act", coreAddition: "TodoManager + nag reminder", keyInsight: "Visible plans improve task completion and accountability", layer: "planning", prevVersion: "s02" }, s03: { title: "TodoWrite", subtitle: "Plan Before You Act", coreAddition: "TodoManager + nag reminder", keyInsight: "Visible plans improve task completion and accountability", layer: "planning", prevVersion: "s02" },
s04: { title: "Subagents", subtitle: "Fresh Context via Task Tool", coreAddition: "Subagent spawn with isolated messages[]", keyInsight: "Process isolation = context isolation", layer: "planning", prevVersion: "s03" }, s04: { title: "Subagents", subtitle: "Process Isolation = Context Isolation", coreAddition: "Subagent spawn with isolated messages[]", keyInsight: "Process isolation gives context isolation for free", layer: "planning", prevVersion: "s03" },
s05: { title: "Skills", subtitle: "SKILL.md + tool_result Injection", coreAddition: "SkillLoader + two-layer injection", keyInsight: "Skills inject via tool_result, not system prompt", layer: "planning", prevVersion: "s04" }, s05: { title: "Skills", subtitle: "SKILL.md + tool_result Injection", coreAddition: "SkillLoader + two-layer injection", keyInsight: "Skills inject via tool_result, not system prompt", layer: "planning", prevVersion: "s04" },
s06: { title: "Compact", subtitle: "Strategic Forgetting", coreAddition: "micro-compact + auto-compact + archival", keyInsight: "Forgetting old context enables infinite-length sessions", layer: "memory", prevVersion: "s05" }, s06: { title: "Compact", subtitle: "Strategic Forgetting", coreAddition: "micro-compact + auto-compact + archival", keyInsight: "Forgetting old context enables infinite-length sessions", layer: "memory", prevVersion: "s05" },
s07: { title: "Tasks", subtitle: "Persistent CRUD with Dependencies", coreAddition: "TaskManager with file-based state + dependency graph", keyInsight: "File-based state survives context compression", layer: "planning", prevVersion: "s06" }, s07: { title: "Tasks", subtitle: "Persistent CRUD with Dependencies", coreAddition: "TaskManager with file-based state + dependency graph", keyInsight: "File-based state survives context compression", layer: "planning", prevVersion: "s06" },
@ -25,6 +25,7 @@ export const VERSION_META: Record<string, {
s09: { title: "Agent Teams", subtitle: "Teammates + Mailboxes", coreAddition: "TeammateManager + file-based mailbox", keyInsight: "Persistent teammates with async mailbox inboxes", layer: "collaboration", prevVersion: "s08" }, s09: { title: "Agent Teams", subtitle: "Teammates + Mailboxes", coreAddition: "TeammateManager + file-based mailbox", keyInsight: "Persistent teammates with async mailbox inboxes", layer: "collaboration", prevVersion: "s08" },
s10: { title: "Team Protocols", subtitle: "Shutdown + Plan Approval", coreAddition: "request_id correlation for two protocols", keyInsight: "Same request-response pattern, two applications", layer: "collaboration", prevVersion: "s09" }, s10: { title: "Team Protocols", subtitle: "Shutdown + Plan Approval", coreAddition: "request_id correlation for two protocols", keyInsight: "Same request-response pattern, two applications", layer: "collaboration", prevVersion: "s09" },
s11: { title: "Autonomous Agents", subtitle: "Idle Cycle + Auto-Claim", coreAddition: "Task board polling + timeout-based self-governance", keyInsight: "Polling + timeout makes teammates self-organizing", layer: "collaboration", prevVersion: "s10" }, s11: { title: "Autonomous Agents", subtitle: "Idle Cycle + Auto-Claim", coreAddition: "Task board polling + timeout-based self-governance", keyInsight: "Polling + timeout makes teammates self-organizing", layer: "collaboration", prevVersion: "s10" },
s12: { title: "Worktree + Task Isolation", subtitle: "Isolate by Directory", coreAddition: "Composable worktree lifecycle + event stream over a shared task board", keyInsight: "Task board coordinates ownership, worktrees isolate execution, and events make lifecycle auditable", layer: "collaboration", prevVersion: "s11" },
}; };
export const LAYERS = [ export const LAYERS = [
@ -32,5 +33,5 @@ export const LAYERS = [
{ id: "planning" as const, label: "Planning & Coordination", color: "#10B981", versions: ["s03", "s04", "s05", "s07"] }, { id: "planning" as const, label: "Planning & Coordination", color: "#10B981", versions: ["s03", "s04", "s05", "s07"] },
{ id: "memory" as const, label: "Memory Management", color: "#8B5CF6", versions: ["s06"] }, { id: "memory" as const, label: "Memory Management", color: "#8B5CF6", versions: ["s06"] },
{ id: "concurrency" as const, label: "Concurrency", color: "#F59E0B", versions: ["s08"] }, { id: "concurrency" as const, label: "Concurrency", color: "#F59E0B", versions: ["s08"] },
{ id: "collaboration" as const, label: "Collaboration", color: "#EF4444", versions: ["s09", "s10", "s11"] }, { id: "collaboration" as const, label: "Collaboration", color: "#EF4444", versions: ["s09", "s10", "s11", "s12"] },
] as const; ] as const;