mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-20 12:13:38 +08:00
fix: build task dependencies in two phases
This commit is contained in:
@@ -47,6 +47,8 @@ s13 は s10 の基本ツール、Hooks、Permission、Task System を再利用
|
||||
- **任意の worktree** は、必要なタスクだけを別の作業ディレクトリへ紐付ける。紐付けのないタスクは通常のリポジトリディレクトリを使う。
|
||||
- **型付きプロトコルと計画ゲート** は shutdown と承認状態を明示し、必要な計画が承認されるまで変更系ツールを止める。
|
||||
|
||||
タスクグラフの作成は s10 の 2 段階契約を維持する。Lead はまず全ノードに `create_task` を呼び、返された実行時 ID で `update_task(addBlockedBy=...)` を実行してから ready task を割り当てる。`update_task` を使えるのは Lead だけであり、チームメイトは一覧・Claim・完了はできるが、チーム実行中にグラフ構造を変更できない。
|
||||
|
||||
s11 の background task と s12 の scheduled task は本章へ持ち込まない。どちらも teammate communication、task claim、plan approval には必要ない。
|
||||
|
||||
これらはすべて Team Harness レイヤーの一部である。タスク発見のために別の Agent Loop は要らず、worktree が別種の Agent を作るわけでもない。
|
||||
@@ -447,4 +449,4 @@ Lead と teammate が呼び出せるのは、`code.py` に直接定義したツ
|
||||
|
||||
s14 MCP Tools → 共通の発見・呼び出しプロトコルで実行時に外部サービスへ接続し、そのツールを tool pool に追加する。
|
||||
|
||||
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->
|
||||
<!-- translation-sync: zh@v12, en@v12, ja@v12 -->
|
||||
|
||||
@@ -47,6 +47,8 @@ s13 reuses s10's base tools, hooks, permission checks, and Task System, then add
|
||||
- **Optional worktrees** bind a task to another working directory when the work needs it. Unbound tasks use the normal repository directory.
|
||||
- **Typed protocols and a plan gate** make shutdown and approval state explicit and block mutating tools until a required plan is approved.
|
||||
|
||||
Task graph authoring keeps s10's two-phase contract. The Lead first calls `create_task` for every node, then uses the returned runtime IDs with `update_task(addBlockedBy=...)` before assigning ready work. Only the Lead receives `update_task`; teammates can list, claim, and complete tasks but cannot rewrite graph structure while the team is running.
|
||||
|
||||
s11 background tasks and s12 scheduled tasks are not carried into this chapter. Neither mechanism is required for teammate communication, task claiming, or plan approval.
|
||||
|
||||
These are all parts of the Team harness layer. Teammates do not need a separate loop for task discovery, and a worktree does not create a new kind of agent.
|
||||
@@ -448,4 +450,4 @@ The Lead and its teammates can only call tools defined directly in `code.py`. Co
|
||||
|
||||
s14 MCP Tools → Connect external services at runtime through one discovery and invocation protocol, then add their tools to the tool pool.
|
||||
|
||||
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->
|
||||
<!-- translation-sync: zh@v12, en@v12, ja@v12 -->
|
||||
|
||||
@@ -46,6 +46,8 @@ s13 复用 s10 的基础工具、Hooks、Permission 和 Task System,并增加
|
||||
- **可选 worktree** 在需要时把任务绑定到另一个工作目录;未绑定任务仍使用仓库目录。
|
||||
- **类型化协议和计划闸门** 显式记录关机与审批状态,并在计划获批前阻止修改型工具。
|
||||
|
||||
任务图继续采用 s10 的两阶段契约。Lead 先为所有节点调用 `create_task`,再使用返回的运行时 ID 调用 `update_task(addBlockedBy=...)`,最后才分配 ready task。只有 Lead 能使用 `update_task`;队友只能列举、认领和完成任务,团队运行期间不能改写任务图结构。
|
||||
|
||||
s11 的后台任务和 s12 的定时任务没有被带入本章。它们不参与队友通信、任务认领或计划审批。
|
||||
|
||||
这些机制都属于 Team 这一层。任务发现不需要另一套 Agent Loop,worktree 也不会产生另一种 Agent。
|
||||
@@ -443,4 +445,4 @@ Lead 和队友目前只能调用直接写在 `code.py` 里的工具。接入 Jir
|
||||
|
||||
s14 MCP Tools → 通过统一的发现与调用协议,在运行时连接外部服务并把它们的工具加入工具池。
|
||||
|
||||
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->
|
||||
<!-- translation-sync: zh@v12, en@v12, ja@v12 -->
|
||||
|
||||
@@ -130,16 +130,11 @@ def _task_path(task_id: str) -> Path:
|
||||
return path
|
||||
|
||||
|
||||
def create_task(subject: str, description: str = "",
|
||||
blockedBy: list[str] | None = None) -> Task:
|
||||
def create_task(subject: str, description: str = "") -> Task:
|
||||
subject = subject.strip()
|
||||
if not subject:
|
||||
raise ValueError("Task subject cannot be empty")
|
||||
dependencies = list(dict.fromkeys(blockedBy or []))
|
||||
with task_store_lock():
|
||||
for dependency in dependencies:
|
||||
if not _task_path(dependency).is_file():
|
||||
raise ValueError(f"Dependency not found: {dependency}")
|
||||
for _ in range(100):
|
||||
task = Task(
|
||||
id=f"task_{secrets.token_hex(4)}",
|
||||
@@ -147,7 +142,7 @@ def create_task(subject: str, description: str = "",
|
||||
description=description,
|
||||
status="pending",
|
||||
owner=None,
|
||||
blockedBy=dependencies,
|
||||
blockedBy=[],
|
||||
)
|
||||
try:
|
||||
with _task_path(task.id).open("x", encoding="utf-8") as handle:
|
||||
@@ -158,6 +153,55 @@ def create_task(subject: str, description: str = "",
|
||||
raise RuntimeError("Could not allocate a unique task ID")
|
||||
|
||||
|
||||
def _task_depends_on(task_id: str, target_id: str) -> bool:
|
||||
"""Return whether task_id transitively depends on target_id."""
|
||||
pending = [task_id]
|
||||
visited = set()
|
||||
while pending:
|
||||
current = pending.pop()
|
||||
if current == target_id:
|
||||
return True
|
||||
if current in visited:
|
||||
continue
|
||||
visited.add(current)
|
||||
pending.extend(load_task(current).blockedBy)
|
||||
return False
|
||||
|
||||
|
||||
def update_task(task_id: str, addBlockedBy: list[str]) -> Task:
|
||||
"""Add dependency edges after create_task has returned real task IDs."""
|
||||
if not isinstance(addBlockedBy, list):
|
||||
raise ValueError("addBlockedBy must be a list of task IDs")
|
||||
|
||||
with task_store_lock():
|
||||
task = load_task(task_id)
|
||||
if task.status != "pending" or task.owner is not None:
|
||||
raise ValueError(
|
||||
f"Task {task_id} dependencies can only be updated while "
|
||||
"pending and unowned"
|
||||
)
|
||||
|
||||
dependencies = list(dict.fromkeys(addBlockedBy))
|
||||
for dependency in dependencies:
|
||||
if dependency == task_id:
|
||||
raise ValueError("Task cannot depend on itself")
|
||||
if not _task_path(dependency).is_file():
|
||||
raise ValueError(f"Dependency not found: {dependency}")
|
||||
if dependency not in task.blockedBy and _task_depends_on(
|
||||
dependency, task_id
|
||||
):
|
||||
raise ValueError(
|
||||
f"Dependency cycle detected: {task_id} -> {dependency}"
|
||||
)
|
||||
|
||||
task.blockedBy.extend(
|
||||
dependency for dependency in dependencies
|
||||
if dependency not in task.blockedBy
|
||||
)
|
||||
save_task(task)
|
||||
return task
|
||||
|
||||
|
||||
def save_task(task: Task):
|
||||
with task_store_lock():
|
||||
path = _task_path(task.id)
|
||||
@@ -579,9 +623,15 @@ def remove_worktree(name: str, discard_changes: bool = False) -> str:
|
||||
PROMPT_SECTIONS = {
|
||||
"identity": "You are a coding agent. Act, don't explain.",
|
||||
"tools": "Available tools: bash, read_file, write_file, edit_file, glob, "
|
||||
"get_task, create_task, list_tasks, claim_task, complete_task, "
|
||||
"create_task, update_task, list_tasks, get_task, claim_task, "
|
||||
"complete_task, "
|
||||
"spawn_teammate, list_teammates, send_message, request_shutdown, "
|
||||
"request_plan, review_plan, create_worktree.",
|
||||
"tasks": (
|
||||
"Create all task nodes first. Only after create_task returns "
|
||||
"runtime-generated IDs, use update_task with those exact IDs to add "
|
||||
"dependencies. Only the Lead changes task dependencies."
|
||||
),
|
||||
"teams": (
|
||||
"When parallel work would help, first propose a small team with clear "
|
||||
"responsibilities and wait for the user's confirmation. Do not call "
|
||||
@@ -716,12 +766,22 @@ def run_agent_glob(pattern: str) -> str:
|
||||
|
||||
# -- Task Tools --
|
||||
|
||||
def run_create_task(subject: str, description: str = "",
|
||||
blockedBy: list[str] | None = None) -> str:
|
||||
task = create_task(subject, description, blockedBy)
|
||||
deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else ""
|
||||
print(f" \033[34m[create] {task.subject}{deps}\033[0m")
|
||||
return f"Created {task.id}: {task.subject}{deps}"
|
||||
def run_create_task(subject: str, description: str = "") -> str:
|
||||
task = create_task(subject, description)
|
||||
print(f" \033[34m[create] {task.subject}\033[0m")
|
||||
return f"Created {task.id}: {task.subject}"
|
||||
|
||||
|
||||
def run_update_task(task_id: str, addBlockedBy: list[str]) -> str:
|
||||
try:
|
||||
task = update_task(task_id, addBlockedBy)
|
||||
except ValueError as exc:
|
||||
return f"Error: {exc}"
|
||||
except FileNotFoundError:
|
||||
return f"Error: Task {task_id} not found"
|
||||
dependencies = ", ".join(task.blockedBy) or "(none)"
|
||||
print(f" \033[34m[update] {task.subject} blockedBy: {dependencies}\033[0m")
|
||||
return f"Updated {task.id} blockedBy: {dependencies}"
|
||||
|
||||
|
||||
def run_list_tasks() -> str:
|
||||
@@ -1467,14 +1527,26 @@ BASE_TOOLS = [
|
||||
|
||||
TASK_TOOLS = [
|
||||
{"name": "create_task",
|
||||
"description": "Create a task with optional dependencies.",
|
||||
"description": "Create a task and return its runtime-generated ID.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {
|
||||
"subject": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"blockedBy": {"type": "array",
|
||||
"items": {"type": "string"}}},
|
||||
"required": ["subject"]}},
|
||||
"description": {"type": "string"}},
|
||||
"required": ["subject"],
|
||||
"additionalProperties": False}},
|
||||
{"name": "update_task",
|
||||
"description": "Add dependencies using IDs returned by create_task.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {
|
||||
"task_id": {"type": "string",
|
||||
"pattern": "^task_[0-9a-f]{8}$"},
|
||||
"addBlockedBy": {
|
||||
"type": "array",
|
||||
"items": {"type": "string",
|
||||
"pattern": "^task_[0-9a-f]{8}$"},
|
||||
"minItems": 1}},
|
||||
"required": ["task_id", "addBlockedBy"],
|
||||
"additionalProperties": False}},
|
||||
{"name": "list_tasks", "description": "List shared tasks.",
|
||||
"input_schema": {"type": "object", "properties": {}}},
|
||||
{"name": "get_task", "description": "Get one task by ID.",
|
||||
@@ -1569,6 +1641,7 @@ TOOL_HANDLERS = {
|
||||
"edit_file": run_agent_edit,
|
||||
"glob": run_agent_glob,
|
||||
"create_task": run_create_task,
|
||||
"update_task": run_update_task,
|
||||
"list_tasks": run_list_tasks,
|
||||
"get_task": run_get_task,
|
||||
"claim_task": run_claim_task,
|
||||
|
||||
Reference in New Issue
Block a user