feat: T-1.21 + T-1.22 + P1-23 — 打通 dispatch→Worker→merge 全链路

T-1.21 (P1-22): Dispatch → Worker 桥接
- eng_mode: spawn_workers() 为每个 ready 任务生成标准 Skill 调用参数
- commands/eng.md: dispatch 段从意图描述改为可执行5步伪代码
  Agent 不再需要猜测工具名、参数格式、task-text 来源

T-1.22 (P1-24): Merge → TaskGraph 状态同步
- merge_worker_result Phase 6.5: 写回 task-graph.json 节点 status
- merge 后节点状态 DONE,ready_tasks() 过滤已完成任务

_select_ready_tasks 修复:
- DAG ready 为空时不再 fallback 到 todo.md(todo.md 状态陈旧时导致重复派发)

P1-23: commands/eng.md dispatch 指令操作化
- 保留 Eng 工具白名单不变(极端接管需要 Write/Edit)
- Arc/Eng 约束非对称性是刻意的设计决策

63+4 项测试全通过,含端到端全链路测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
AirLongDian
2026-06-11 14:58:04 +08:00
parent 4b8a21aee2
commit dd4aa6b345
3 changed files with 323 additions and 10 deletions

View File

@@ -556,6 +556,16 @@ def merge_worker_result(project_root: Path, result_path: Path) -> dict:
state["lastMergeAt"] = now_iso()
atomic_json_write(paths["state"], state)
# Phase 6.5: 同步 task-graph.json 节点状态P1-24
tg_json = airplan_root(project_root) / "state" / "airarc" / "reviews" / "task-graph.json"
if tg_json.exists():
from air_runtime.modes.arc_mode import _export_task_graph_json
graph = TaskGraph.load(tg_json)
if task_id in graph.nodes:
new_status = "DONE" if status == "done" else status.upper()
graph.nodes[task_id].status = new_status
_export_task_graph_json(graph, tg_json)
log.emit(MERGE_COMPLETED, {
"taskId": task_id,
"status": status,
@@ -586,17 +596,17 @@ def merge_worker_result(project_root: Path, result_path: Path) -> dict:
def _select_ready_tasks(project_root: Path, max_count: int) -> list[str]:
"""优先从 task-graph.json 的 DAG 计算 in-degree 为 0 的 TODO taskfallback parse_tasks。"""
"""优先从 task-graph.json 的 DAG 计算 in-degree 为 0 的 TODO task
DAG 中 ready 为空意味着无任务可派发(全部完成或全部被依赖阻塞),不应 fallback 到 todo.md。"""
task_graph_json = airplan_root(project_root) / "state" / "airarc" / "reviews" / "task-graph.json"
if task_graph_json.exists():
try:
graph = TaskGraph.load(task_graph_json)
ready = graph.ready_tasks()
if ready:
return ready[:max_count]
return ready[:max_count] # 空列表也是正确答案,不 fallback
except Exception:
pass
# fallback
# fallback:仅在 task-graph.json 不存在时使用 todo.md
todo = get_todo_path(project_root)
if not todo.exists():
return []
@@ -604,6 +614,25 @@ def _select_ready_tasks(project_root: Path, max_count: int) -> list[str]:
return [t.task_id for t in tasks if t.status == "TODO"][:max_count]
def spawn_workers(project_root: Path, task_ids: list[str]) -> list[dict]:
"""T-1.21: 为每个 ready 任务准备 spawn 指令,返回 Agent 可直接消费的 Skill 调用参数列表。
不实际启动子进程——启动由 Agent 框架的 Skill 工具完成。
"""
tg_json = airplan_root(project_root) / "state" / "airarc" / "reviews" / "task-graph.json"
graph = TaskGraph.load(tg_json) if tg_json.exists() else TaskGraph()
instructions = []
for tid in task_ids:
node = graph.nodes.get(tid)
task_text = node.task if node else ""
instructions.append({
"skill": "airplan",
"args": f"do --sub enter --task-id {tid} --task-text '{task_text}' --project .",
"taskId": tid,
"taskText": task_text,
})
return instructions
def handle_adr_invalidation(project_root: Path, adr_id: str) -> dict:
"""P1-21: ADR 变更级联失效处理。