Commit 8712aef4e432f3cf6843639a293e54f6ace3bd5f

Authored by bboonnee
1 parent bc0eaa5c

E: cross-Org-Agent dynamic Work Graph design capability

Implements docs/todo/dynamic-work-graph-design-skill-design.md. G already
answered FR-2 (how to mark a parallel-eligible node group): no new field
needed -- topological_layers() infers independence purely from
depends_on/edges, so the new Skill's job is just "express real
dependencies honestly, don't add fake ones."

New Skill skills/design-dynamic-work-graph/SKILL.md: the node-boundary /
zero_token-vs-needs_judgment / per-node self-contained task_prompt
methodology, wired into agent_profile.py's FRONTMATTER for coder and
validator (Planner's own doc-chain path is untouched -- FR-4 only names
Coder/Validator).

work_graph_plan.py: new _GENERIC_PROMPT_TEMPLATE + generic_node_prompt_builder(),
used whenever org_node != 'planner'. Planner keeps its existing fixed
brd/prd/sdd/benchmark/... chain (_PROMPT_TEMPLATE) completely unchanged --
zero regression risk to the one real production planning path.

Scoped per the requirements doc's non-goal #1 ("prove the mechanism works,
don't cover every real scenario yet"): does NOT wire into
e2e_pipeline.py::implement_code()'s existing single-node production path.
New tests prove the mechanism itself: a Coder scenario that naturally
splits into 3 independent modules produces a correctly-shaped
graph_manifest, topological_layers() puts all three in one concurrent
layer (ties E directly to G), and generic_node_prompt_builder drives it
through run_graph() to completion.

Full src suite (580 tests) passes with zero regressions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs/todo/dynamic-work-graph-design-skill-design.md 0 → 100644
  1 +# 跨 Org Agent 的动态 Work Graph 设计能力——详细设计(SDD)
  2 +
  3 +**日期**: 2026-08-06
  4 +**性质**: 详细设计,对应需求文档 `docs/todo/dynamic-work-graph-design-skill-requirements.md`(条目 E,已确认)。回答该需求文档第 8 节的开放问题。
  5 +**依赖**:条目 G(`docs/work-graph-parallel-execution-design.md`)已实现——`topological_layers()`/`run_graph()` 已经在生产代码里。
  6 +**状态**: 设计草案,待评审后实施。
  7 +
  8 +## 1. G 已经回答了 FR-2(并行分组字段格式)
  9 +
  10 +需求文档 FR-2 留空的"哪些节点属于同一个可并行分组"字段格式,G 的实际实现给出了确定答案:**不需要新字段**。`topological_layers()` 完全靠 `depends_on`/`edges` 结构本身推导独立性——没有相互依赖的节点自动落入同一层,`run_graph()` 自动并发执行同层节点。Planning Skill 的方法论职责因此简化为:"如实表达真正的依赖关系,不要为了图好看而人为加上并不存在的 depends_on"(14 步版对照笔记"边是数据契约"这条原则的直接应用),不需要额外发明一个"parallel_group"标记。
  11 +
  12 +## 2. 新 Skill:`design-dynamic-work-graph`
  13 +
  14 +`skills/design-dynamic-work-graph/SKILL.md`,与 `planning-requirement-expansion`、`code-task-development` 等现有 Skill 平级存放。内容方法论(不含代码):
  15 +
  16 +1. 节点边界怎么划:一个节点 = 一次独立的、有明确输入输出契约的 Work Node 执行(一次 `claude -p` 调用)。任务小到一次调用就能完成时不需要拆图(呼应需求文档第 6 节约束)。
  17 +2. `edge_type` 怎么选:`zero_token`(纯聚合/规约,不需要 Agent 推理,比如"把三份判断投票汇总")vs `needs_judgment`(下游需要真正读懂上游内容才能继续,比如"PRD 依赖 BRD 的实际内容")。
  18 +3. 如何识别可并行的候选点:如实列出每对节点之间是否存在真实数据流动——没有数据流动就不要加 `depends_on`,G 会自动把它们并发调度。
  19 +4. 每个节点必须自带 `task_prompt`(该节点执行时使用的完整提示词,自包含,不依赖调用方另外拼接)——这是 Coder/Validator 场景与 Planner 场景的关键区别:Planner 的四个节点类型固定(brd/prd/sdd/benchmark...),下游代码用一个查表 `_DOC_PROMPTS[artifact_type]` 就能拿到提示词;Coder/Validator 的子任务没有固定的小词表,必须由 Claude 自己在规划阶段把每个节点的提示词写出来。
  20 +
  21 +## 3. `work_graph_plan.py` 改造(FR-3)
  22 +
  23 +**不改动 Planner 现有路径**:`_PROMPT_TEMPLATE`(brd/prd/sdd/benchmark 固定链)与 `_normalize_graph_manifest` 的 canonical-chain 强制逻辑完全保留,`org_node == "planner"` 时走原有分支,零回归风险。
  24 +
  25 +新增:
  26 +
  27 +- `_GENERIC_PROMPT_TEMPLATE`:当 `org_node != "planner"` 时使用,要求 Claude 依据 `design-dynamic-work-graph` Skill 的方法论,把 `goal_text` 拆成一个 `graph_manifest`,每个节点除 `node_id`/`depends_on` 外必须包含 `task_prompt`(自包含提示词);输出格式(fenced ```json 块)跟现有约定一致,复用同一套 `_extract_graph_manifest` 解析逻辑。
  28 +- `plan_work_graph()` 内部按 `org_node == "planner"` 二选一模板,函数签名/返回值不变(需求文档非目标 #2)。
  29 +- `generic_node_prompt_builder(graph_manifest)`:新增的公开辅助函数,返回一个 `build_prompt(node, outputs)` 闭包,直接读取 `node["task_prompt"]`(`run_graph()` 的 `build_prompt` 约定不变,仍然是"给定 node + 已完成 outputs,返回这次调用的提示词")——之所以放在 `work_graph_plan.py` 而不是 `e2e_pipeline.py`,是因为它跟生成侧的字段约定(`task_prompt` 这个 key 名)强耦合,两者应该在同一个模块里维护,避免字段改名时两处不同步。
  30 +
  31 +`_normalize_graph_manifest` 不需要改动——canonical-chain 强制分支本来就有 `all(t in by_artifact_type for t in _CANONICAL_DOC_CHAIN)` 前置判断,Coder/Validator 的动态图没有 brd/prd/sdd/benchmark 节点,天然不触发,其余的 `id`→`node_id`/`depends_on` 兜底逻辑对新路径同样适用,不需要区分。
  32 +
  33 +## 4. Skill 接入 Coder/Validator
  34 +
  35 +`src/agent_profile.py::FRONTMATTER["coder"]["skills"]`/`["validator"]["skills"]` 各自追加 `"design-dynamic-work-graph"`。`.claude/agents/{coder,validator}.md` 是从这里渲染出来的(`render_agent_definition`),不需要手改部署产物。
  36 +
  37 +## 5. 验证范围(对应需求文档验收标准,按非目标 #1 收窄)
  38 +
  39 +需求文档 FR-5/验收标准 1 优先级是"应该",且非目标 #1 明确"不要求第一版就覆盖 Coder/Validator 的全部真实工作场景,只需要证明机制本身走得通"。本轮实现范围:
  40 +
  41 +- 证明 `plan_work_graph(conn, "coder", ...)` 能生成一个正确带 `task_prompt`、互不依赖节点的 `graph_manifest`(mock `claude` 子进程返回一个"给三个独立模块各自补单元测试"的真实场景响应)。
  42 +- 证明该 `graph_manifest` 经 `topological_layers()` 分层后,三个节点确实落在同一层(跟 G 的实现直接挂钩,不是空谈)。
  43 +- 证明 `generic_node_prompt_builder` 生成的 `build_prompt` 能驱动 `run_graph()` 端到端跑完(mock `execute_work_graph`)。
  44 +- **不改动** `e2e_pipeline.py::implement_code()` 的现有单节点生产路径——那是更大的、需要独立评估的改动(如何决定"这个 Coder 任务复杂到该规划成图"本身就是一个开放问题,需求文档风险第 3 条已经点出),留给后续任务。
  45 +
  46 +## 6. 回答需求文档第 8 节开放问题
  47 +
  48 +- 新 Skill 命名:`design-dynamic-work-graph`,目录 `skills/design-dynamic-work-graph/`。
  49 +- `_PROMPT_TEMPLATE` 改造方式:按 `org_node` 二选一模板(Planner 固定链 vs 通用动态规划),不是让 Claude 完全自由决定 Planner 要生成哪些节点——Planner 那条链是"架构常量",如 `_normalize_graph_manifest` 自己的注释所说,不应该变成每次都不同。
  50 +- FR-2 并行分组字段格式:不需要,G 靠 `depends_on`/`edges` 结构自动推导(见第 1 节)。
  51 +- Coder/Validator 第一个接入场景:mock 场景"给三个独立模块各自补一个单元测试",用于验证机制可用性,不接入真实生产路径。
... ...
docs/todo/graph-engineering-open-items.md
... ... @@ -76,7 +76,7 @@
76 76  
77 77 ## E. Planning Skill 提示词强化——识别可并行节点 + 通用 Work Graph 设计能力
78 78  
79   -**状态**:✅ 已确认,且范围比原开放问题更大
  79 +**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/todo/dynamic-work-graph-design-skill-requirements.md` + 设计文档 `docs/todo/dynamic-work-graph-design-skill-design.md`
80 80  
81 81 **来源**:综述版对照 §7 开放问题 3 + 14 步版对照 §3 开放问题 2(同一主题:让 Planning 阶段更早识别并行机会)
82 82  
... ... @@ -84,9 +84,7 @@
84 84  
85 85 **确认结果(2026-08-05)**:提示词强化肯定要做——把"对每个箭头问是否真的有数据流动"的提问方式、以及"并行可分解 / 单一上下文装不下 / 收益覆盖成本"这三条尺子写进 Planning 相关 prompt。**但范围比原开放问题更大**:不能只强化 Planner 自己写 BRD/PRD/SDD/Benchmark 那条链的 prompt——架构文档 §5 本来就说"每个 Org Agent 拥有一个 Planning Skill"(Coder 和 Validator 各自也要规划自己的 Work Graph),"如何识别 fan-out/fan-in、设计一张动态 Work Graph"这套方法论应该是**跨 Org Agent 通用**的能力,而不是分别在 Planner/Coder/Validator 三份 Planning Skill 里各写一遍。用户原话:"planner agent 里面要有多个 skill,不仅仅是 brd prd sdd benchmark 还有给其他 org graph 的 node 节点的 agent 做各种动态 work graph 的设计"。
86 86  
87   -**待办**:✅ 需求文档已产出 → `docs/dynamic-work-graph-design-skill-requirements.md`。深挖代码后发现真实缺口比原开放问题更大:`work_graph_plan.py::plan_work_graph()` 的提示词目前**硬编码**成永远只生成 brd/prd/sdd/benchmark 四个节点,且只有 Planner(`e2e_pipeline.py::plan_documents`)真正调用它——**Coder 和 Validator 现在根本没有走"先规划图再执行"这条路径**,走的是更简化的单次 `org_node_trigger.trigger_org_node()` 调用。所以 E 不只是"强化提示词",而是要先把动态 Work Graph 规划能力从 Planner 专属扩展成三个 Org Agent 通用。
88   -
89   -**依赖关系**:这条和「G. Work Graph 节点并行执行」是互补的——G 解决"执行层能不能并发跑",E 解决"规划层会不会生成值得并发的图"。两条需要一起排期,建议先看 G 的需求文档,E 的需求文档要在 G 确认的数据结构(`graph_manifest` 里怎么表达"这组节点可并行")基础上写,避免两边字段对不上。
  87 +**待办**:✅ 已实现(2026-08-06)。G 已经回答了 FR-2(并行分组字段格式):不需要新字段,`topological_layers()` 完全靠 `depends_on`/`edges` 结构本身推导独立性——设计文档第 1 节记录了这个结论。实现内容:新 Skill `skills/design-dynamic-work-graph/SKILL.md`(跨 Org Agent 通用方法论,接入 `agent_profile.py::FRONTMATTER["coder"]["skills"]`/`["validator"]["skills"]`);`work_graph_plan.py` 新增 `_GENERIC_PROMPT_TEMPLATE`(`org_node != "planner"` 时使用,每节点自带 `task_prompt`,Planner 原有固定链路 `_PROMPT_TEMPLATE` 完全不动,零回归)+ `generic_node_prompt_builder()`。按需求文档非目标 #1("不要求第一版就覆盖 Coder/Validator 全部真实工作场景,只需要证明机制走得通")收窄范围:**没有**改动 `e2e_pipeline.py::implement_code()` 的现有单节点生产路径——新增测试证明"给 Coder 一个可拆成 3 个独立模块补测试的场景 → 生成正确的 `graph_manifest` → `topological_layers()` 确认三个节点落在同一层 → `run_graph()` 端到端跑完"这条链路本身成立(`src/test_work_graph_plan.py`:`GenericPlanningPromptTest`/`CoderThreeIndependentModulesScenarioTest`/`GenericNodePromptBuilderTest`/`GenericPathEndToEndViaRunGraphTest`),不是接入真实生产路径。`python3 -m unittest discover -s src`(580 tests)全绿,零回归。
90 88  
91 89 ---
92 90  
... ... @@ -118,20 +116,40 @@
118 116  
119 117 ---
120 118  
121   -## 确认结果汇总(2026-08-05)
  119 +## H. 统一 Role Package 抽象层
  120 +
  121 +**状态**:🟡 需求文档已产出,待评审/排期
  122 +
  123 +**来源**:不属于三份 Graph Engineering 对照笔记,是用户对照 Agentic Enterprise 产品目标("role + context + skills + applications + workflows + permissions + acceptance criteria;all essentially are context for intelligent computing")单独提出的差距检查,2026-08-06。
  124 +
  125 +**背景**:逐维度核对代码发现,七个维度里六个都有对应实现(applications 是真空白),但没有被收敛成一个统一的"role package"格式——理解"Validator 这个角色完整定义是什么"现在得同时读 `agent_profile.py` + `skills/qa-task-validation/SKILL.md` + `.claude/agent-memory/validator/` + `docs/sdd/06-验收与测试锚点.md` 四个地方拼出来。更根本的问题:`role` 本身不是数据驱动的——`ORG_NODES = ("planner", "coder", "validator")` 硬编码元组,`schema.py` 四张表的 `CHECK (org_node IN ('planner','coder','validator'))` 约束——新增角色需要改代码和 schema,不是"包一个新角色进来"。
  126 +
  127 +**待办**:需求文档已产出 → `docs/role-package-abstraction-requirements.md`。核心可验证标准:能不能只靠"写一份新的 Role Package"、不改 `schema.py` 约束、不改 `agent_profile.py` 硬编码字典,就让系统认识一个新角色。已知这条改动面比 B/D/E/F/G 任何一条都大(4 张表的 CHECK 约束、`agent_profile.py` 被多处代码依赖),且 workflows 维度依赖 E 的产出。
  128 +
  129 +---
  130 +
  131 +## 工作量与优先级评估(2026-08-06)
  132 +
  133 +| 项 | 一句话 | 阶段 | 工作量 | 优先级 | 依赖 |
  134 +|---|---|---|---|---|---|
  135 +| **G** 并行执行 | Work Graph 层内并发、层间屏障 | ✅ **已实现**(`bc0eaa5`,575 测试全绿) | M | 已完成 | 无 |
  136 +| **E** 动态 Work Graph 设计能力 | 让 Coder/Validator 也能规划自己的图,Planning 提示词识别可并行节点 | ✅ **已实现**(2026-08-06,580 测试全绿) | L | 已完成 | G(已满足) |
  137 +| **F** 三视角评审团 fan-out | `judge_root_cause` 从 Python 函数改成 Work Graph 菱形拓扑 | 设计已产出 → `docs/todo/verifier-fan-out-jury-design.md`,实现中 | M | P0(G/E 都已落地,正在推进) | 无(G 已满足) |
  138 +| **B** loop-until-dry Skill | Validator 未知规模扫描收敛能力 | 仅需求 | S/M | P2 | 无 |
  139 +| **H** Role Package 抽象层 | 统一 role/context/skills/applications/workflows/permissions/acceptance 打包格式 | 仅需求 | XL | P2(战略级,不阻塞其他项) | workflows 字段依赖 E 的产出 |
  140 +| **D** Time-travel 调试 | 回到历史节点分叉重跑 | 仅需求 | L | P3 | 无强依赖,但价值随图变复杂(E 落地后)才会显现 |
  141 +| C 按节点选模型 | — | ⛔ 已确认不做 | — | — | — |
  142 +| A 术语对照 | — | ✅ 已完成 | — | — | — |
122 143  
123   -| 项 | 结果 | 需求文档 |
124   -|---|---|---|
125   -| A | ✅ 已完成 | 无需求文档,直接改了 `docs/graphlemon_architecture.md` §2 术语表 |
126   -| B | ✅ 已确认,需求文档已产出 | `docs/validator-loop-until-dry-skill-requirements.md` |
127   -| C | ⛔ 暂不做 | 无 |
128   -| D | ✅ 已确认,需求文档已产出 | `docs/work-graph-time-travel-debugging-requirements.md` |
129   -| E | ✅ 已确认,需求文档已产出(范围比原开放问题更大) | `docs/dynamic-work-graph-design-skill-requirements.md` |
130   -| F | ✅ 已确认,需求文档已产出 | `docs/verifier-fan-out-jury-requirements.md` |
131   -| G | ✅ 已实现(2026-08-06) | `docs/work-graph-parallel-execution-requirements.md` + `docs/work-graph-parallel-execution-design.md` |
  144 +**2026-08-06 更新的关键判断**:G 已经被 graphlemon 自己的自举循环实现并验证(不是本轮对话做的——`git log` 显示提交 `bc0eaa5`,`src/work_graph_orchestrate.py` 里 `topological_layers()`/`_LockedConn`/`WorkGraphLayerError` 都是真代码,`python3 -m unittest discover -s src` 跑出 575 测试全绿,跟 G 条目自己记录的实现细节吻合)。这把之前"G 和 E 必须一起排"的判断更新为:**E 现在是唯一的剩余瓶颈**——并行执行引擎已经是真的了,但 `plan_work_graph()` 的提示词依然硬编码只产出一条 BRD→PRD→SDD→Benchmark 直链,没有 E,G 造好的并行能力仍然没有真实的图可以运行。F 也从"强依赖 G,排在后面"变成"G 的执行引擎已经存在,可以直接排",工作量估算不变但不再需要等待。
132 145  
133   -**当前状态**:A 已落地;B/C/D/E/F/G 六项全部完成需求文档(BRD/PRD 层面),均未进入详细设计(SDD 层面)。
  146 +**工作量估算依据**:G(已完成,实际证实是 M 量级——6 步实施计划,改动集中在一个模块);E(L——Coder/Validator 从未真正走过 `plan_work_graph`/`run_graph` 路径,是全新集成,还要防止破坏 Planner 现有生产路径);F(M——判定逻辑已验证过,只是把 Python 函数调用改造成图形状);B(S/M——不碰执行引擎,纯提示词工程);H(XL——自己的需求文档写明"改动面比 B/D/E/F/G 任何一条都大",4 张表 CHECK 约束+多处代码依赖+从零定义 applications);D(L——动一条有连锁风险的唯一约束,且控制台可视化工作量可能比后端还大)。
134 147  
135   -**推进顺序建议**:G 是 E、F 的前置依赖(E 的并行分组字段格式、F 的 fan-out 时间收益都需要 G 先定型),建议优先把 G 推进到详细设计。B、D 相对独立,可以随时单独推进详细设计。E、F 的详细设计需要等 G 的数据结构定下来。
  148 +**当前推进顺序建议**:
  149 +1. **E**(P0)——现在是把 G 的投入变成真实收益的唯一剩余步骤,优先推进详细设计。
  150 +2. **F**(P1)——G 的执行引擎已就绪,可以直接进入详细设计,不必再等。
  151 +3. **B**(P2)——随时可插空,跟其他项无依赖冲突。
  152 +4. **H**(P2,建议独立设计周期)——价值是长期的(系统现在被硬编码死在 3 个角色上),不阻塞任何短期工作。
  153 +5. **D**(P3)——现在的图都还很简单,time-travel 要解决的"大图不好调"痛点要等 E 落地、图真的变复杂后才会显现。
136 154  
137   -下一步要我先把哪一份需求文档推进到详细设计(SDD 层面)?还是先把六份需求文档过一遍评审,确认没有需要调整的地方,再决定设计阶段的排期?
  155 +下一步要我先把 E 推进到详细设计吗?
... ...
skills/design-dynamic-work-graph/SKILL.md 0 → 100644
  1 +---
  2 +name: design-dynamic-work-graph
  3 +description: |
  4 + 跨 Org Agent(Planner/Coder/Validator 共用)的通用方法论:如何把一个目标拆成一张可执行的
  5 + graph_manifest(节点、依赖、每节点自包含提示词),供 work_graph_plan.py::plan_work_graph()
  6 + 的通用规划路径驱动、work_graph_orchestrate.py::run_graph() 执行。
  7 +version: 1.0.0
  8 +tags:
  9 + - planning
  10 + - work-graph
  11 + - orchestration
  12 +metadata:
  13 + origin: graphlemon-native
  14 + related: [G(并行执行)docs/work-graph-parallel-execution-design.md, E docs/todo/dynamic-work-graph-design-skill-design.md]
  15 +platforms: [linux, macos, windows]
  16 +---
  17 +
  18 +# SKILL:design-dynamic-work-graph
  19 +
  20 +## 适用场景
  21 +- 任务复杂到值得先规划一张图、再按图执行,而不是一次单次调用就能完成(不是所有任务都需要上图——任务很小或孤立时,直接一次调用更合适)。
  22 +- 调用方是 Planner 自身以外的 Org Agent(Coder/Validator),需要为自己的一批子任务生成 `graph_manifest`。
  23 +
  24 +## 输出:一个 `graph_manifest`
  25 +```json
  26 +{
  27 + "nodes": [
  28 + {"node_id": "...", "depends_on": ["..."], "task_prompt": "..."}
  29 + ],
  30 + "edges": [
  31 + {"from": "...", "to": "...", "edge_type": "zero_token" | "needs_judgment"}
  32 + ]
  33 +}
  34 +```
  35 +
  36 +每个节点必须自带 `task_prompt`:该节点执行时使用的完整、自包含提示词,不依赖调用方另外拼接上下文。这是跟 Planner 自己的 BRD/PRD/SDD/Benchmark 链的关键区别——Planner 的节点类型固定在一个小词表里,下游代码按 `artifact_type` 查表就能拿到提示词;Coder/Validator 的子任务没有这种固定词表,提示词必须在规划阶段由你自己写清楚。
  37 +
  38 +## 规则(强制)
  39 +
  40 +1. **节点边界**:一个节点 = 一次独立的、有明确输入输出契约的 Work Node 执行(一次 `claude -p` 调用)。不要把一个连贯的思考过程拆成多个人为的小节点。
  41 +2. **如实表达依赖,不要多加**:`depends_on` 只写真正存在数据流动的依赖——下游节点的 `task_prompt` 真的需要引用上游节点的产出内容,才算依赖。**不要为了图看起来更"有序"而人为串行化本来互不依赖的节点**:执行引擎(`topological_layers()`)会把没有依赖关系的节点自动分到同一层并发执行,多余的依赖会白白拖慢总执行时间,还会让"这几个节点该并行"这条信息从图里消失。
  42 +3. **`edge_type` 二选一**:
  43 + - `zero_token`:纯聚合/规约边,下游不需要 Agent 推理就能处理上游输出(比如"把几份判断汇总计数")。这类聚合优先用普通代码完成,不要为它专门生成一个节点。
  44 + - `needs_judgment`:下游真的需要读懂上游内容、做出判断才能继续(比如"根据 SDD 内容写实现")。
  45 +4. **任务小就不要上图**:如果目标本身一次 Work Node 调用就能完成,直接返回一个单节点 `graph_manifest`(或者告知调用方不需要走图规划路径),不要为了"用上这套机制"强行拆分。
  46 +5. **`node_id` 唯一、可读**:用能表达该节点做什么的短标识符(如 `test-module-a`),不要用无意义的序号。
  47 +
  48 +## 分解方法
  49 +
  50 +1. 先列出这批子任务里,每一对任务之间是否存在真实的数据流动("B 的 `task_prompt` 是否真的需要 A 的产出内容")。没有则不连边。
  51 +2. 对确实存在的依赖,判断是 `zero_token` 还是 `needs_judgment`。
  52 +3. 为每个节点写出自包含的 `task_prompt`——假设执行它的 Agent 看不到这次规划的任何上下文,只能看到这一个节点的 `task_prompt` 和它依赖节点的产出。
  53 +4. 检查是否有环(`topological_layers()` 遇到环会报错)——如果两个节点互相需要对方的产出,说明边界划错了,需要合并成一个节点或重新拆分。
  54 +
  55 +## 自审清单
  56 +- [ ] 每个节点都有 `task_prompt`,且不依赖这次规划对话里才有的隐含上下文。
  57 +- [ ] 没有为了让图"看起来有序"而添加的虚假 `depends_on`。
  58 +- [ ] 没有本来可以用普通代码完成的聚合被写成了一个需要 Agent 推理的节点。
  59 +- [ ] 图里没有环。
  60 +- [ ] 如果这批任务本质上一次调用就能做完,没有为了走图机制而强行拆分。
... ...
src/agent_profile.py
... ... @@ -65,13 +65,14 @@ FRONTMATTER = {
65 65 "code-agent-engineering",
66 66 "debugging-toolkit",
67 67 "software-planning-execution",
  68 + "design-dynamic-work-graph",
68 69 ],
69 70 "tools": ["Read", "Write", "Edit", "Bash", "Grep", "Glob"],
70 71 },
71 72 "validator": {
72 73 "memory": "project",
73 74 "model": "claude-sonnet-5",
74   - "skills": ["qa-task-validation", "unattended-agent-operations"],
  75 + "skills": ["qa-task-validation", "unattended-agent-operations", "design-dynamic-work-graph"],
75 76 "tools": ["Read", "Bash", "Grep", "Glob"],
76 77 # 2026-07-31 fix, found for real running U-014's full closed loop:
77 78 # scoped Bash patterns here ("Bash(git commit:*)") don't just block
... ...
src/test_work_graph_plan.py
... ... @@ -12,7 +12,7 @@ import subprocess
12 12 import unittest
13 13 from unittest.mock import patch
14 14  
15   -from src import schema, work_graph_plan
  15 +from src import schema, work_graph_execute, work_graph_orchestrate, work_graph_plan
16 16  
17 17 _MANIFEST = {
18 18 "nodes": [
... ... @@ -41,7 +41,12 @@ def _fenced_result(manifest, wrapper_key=None):
41 41  
42 42 class PlanWorkGraphTestBase(unittest.TestCase):
43 43 def setUp(self):
44   - self.conn = schema.connect(":memory:")
  44 + # check_same_thread=False: GenericPathEndToEndViaRunGraphTest below
  45 + # drives a real multi-node concurrent layer through run_graph()
  46 + # (G), which calls into this conn from ThreadPoolExecutor worker
  47 + # threads via _LockedConn -- see test_work_graph_orchestrate.py's
  48 + # RunGraphTestBase for the same reasoning.
  49 + self.conn = schema.connect(":memory:", check_same_thread=False)
45 50 schema.create_schema(self.conn)
46 51 self.conn.execute(
47 52 "INSERT INTO project (project_id, name, target_folder_path, has_git_repo, status) "
... ... @@ -268,5 +273,105 @@ class FailureTest(PlanWorkGraphTestBase):
268 273 self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM work_graph_run").fetchone()[0], 0)
269 274  
270 275  
  276 +# E (2026-08-06): cross-Org-Agent dynamic planning path -- org_node != 'planner'
  277 +# gets _GENERIC_PROMPT_TEMPLATE instead of the fixed doc-chain prompt, and each
  278 +# node carries its own task_prompt instead of a fixed artifact_type.
  279 +
  280 +_CODER_THREE_MODULE_MANIFEST = {
  281 + "nodes": [
  282 + {"node_id": "test-module-a", "depends_on": [], "task_prompt": "Write unit tests for module_a.py."},
  283 + {"node_id": "test-module-b", "depends_on": [], "task_prompt": "Write unit tests for module_b.py."},
  284 + {"node_id": "test-module-c", "depends_on": [], "task_prompt": "Write unit tests for module_c.py."},
  285 + ],
  286 + "edges": [],
  287 +}
  288 +
  289 +
  290 +class GenericPlanningPromptTest(PlanWorkGraphTestBase):
  291 + def test_planner_still_gets_fixed_doc_chain_template(self):
  292 + with patch("src.work_graph_plan.subprocess.run") as run:
  293 + run.return_value = _completed_process(
  294 + stdout_obj={"is_error": False, "result": _fenced_result(_MANIFEST)}
  295 + )
  296 + work_graph_plan.plan_work_graph(self.conn, "planner", "task-1", "goal", "/tmp/proj-1")
  297 + prompt = run.call_args.args[0][run.call_args.args[0].index("-p") + 1]
  298 + self.assertIn("BRD, PRD, SDD, Benchmark", prompt)
  299 + self.assertNotIn("design-dynamic-work-graph", prompt)
  300 +
  301 + def test_coder_gets_generic_template_referencing_the_skill(self):
  302 + with patch("src.work_graph_plan.subprocess.run") as run:
  303 + run.return_value = _completed_process(
  304 + stdout_obj={"is_error": False, "result": _fenced_result(_CODER_THREE_MODULE_MANIFEST)}
  305 + )
  306 + work_graph_plan.plan_work_graph(self.conn, "coder", "task-1", "goal", "/tmp/proj-1")
  307 + prompt = run.call_args.args[0][run.call_args.args[0].index("-p") + 1]
  308 + self.assertIn("design-dynamic-work-graph", prompt)
  309 + self.assertNotIn("BRD, PRD, SDD, Benchmark", prompt)
  310 + self.assertIn("task_prompt", prompt)
  311 +
  312 +
  313 +class CoderThreeIndependentModulesScenarioTest(PlanWorkGraphTestBase):
  314 + """E acceptance criterion 1: a Coder scenario that naturally splits into
  315 + 2-3 independent sub-tasks produces a graph_manifest via the new generic
  316 + path (not the old single trigger_org_node call), correctly marking those
  317 + nodes as mutually independent."""
  318 +
  319 + def test_three_independent_modules_produce_correctly_shaped_manifest(self):
  320 + with patch("src.work_graph_plan.subprocess.run") as run:
  321 + run.return_value = _completed_process(
  322 + stdout_obj={"is_error": False, "result": _fenced_result(_CODER_THREE_MODULE_MANIFEST)}
  323 + )
  324 + result = work_graph_plan.plan_work_graph(self.conn, "coder", "task-1", "goal", "/tmp/proj-1")
  325 +
  326 + manifest = result["graph_manifest"]
  327 + self.assertEqual(len(manifest["nodes"]), 3)
  328 + for node in manifest["nodes"]:
  329 + self.assertEqual(node["depends_on"], [])
  330 + self.assertIn("task_prompt", node)
  331 +
  332 + # E's whole point ties directly into G: independence is expressed
  333 + # structurally (empty depends_on), so topological_layers() must put
  334 + # all three in ONE concurrent layer, not three serial ones.
  335 + layers = work_graph_orchestrate.topological_layers(manifest)
  336 + self.assertEqual(len(layers), 1)
  337 + self.assertEqual({n["node_id"] for n in layers[0]}, {"test-module-a", "test-module-b", "test-module-c"})
  338 +
  339 +
  340 +class GenericNodePromptBuilderTest(unittest.TestCase):
  341 + def test_returns_the_node_own_task_prompt(self):
  342 + node = {"node_id": "test-module-a", "task_prompt": "Write unit tests for module_a.py."}
  343 + self.assertEqual(
  344 + work_graph_plan.generic_node_prompt_builder(node, {}), "Write unit tests for module_a.py."
  345 + )
  346 +
  347 +
  348 +class GenericPathEndToEndViaRunGraphTest(PlanWorkGraphTestBase):
  349 + """E acceptance criterion (FR-5): the generated graph_manifest, driven by
  350 + generic_node_prompt_builder, actually runs to completion through
  351 + run_graph() -- not just a standalone parsing/shape check."""
  352 +
  353 + def test_three_independent_nodes_execute_and_return_outputs(self):
  354 + with patch("src.work_graph_plan.subprocess.run") as run:
  355 + run.return_value = _completed_process(
  356 + stdout_obj={"is_error": False, "result": _fenced_result(_CODER_THREE_MODULE_MANIFEST)}
  357 + )
  358 + plan_result = work_graph_plan.plan_work_graph(self.conn, "coder", "task-1", "goal", "/tmp/proj-1")
  359 +
  360 + # plan_work_graph() already inserted the work_graph_run row
  361 + # (status='planning') -- run_graph() doesn't require any particular
  362 + # status on it, so nothing more to set up here.
  363 + with patch.object(work_graph_execute, "write_workflow_script"), \
  364 + patch.object(work_graph_execute, "execute_work_graph") as mock_execute:
  365 + mock_execute.side_effect = lambda cwd, name, args, schema, timeout=600: {"content": "done: " + name}
  366 + outputs = work_graph_orchestrate.run_graph(
  367 + self.conn, plan_result["run_id"], "coder", "coder-implementation", "/tmp/proj-1",
  368 + plan_result["graph_manifest"], work_graph_plan.generic_node_prompt_builder,
  369 + {"type": "object", "required": ["node_id"]}, {"type": "object", "required": ["content"]},
  370 + )
  371 +
  372 + self.assertEqual(set(outputs.keys()), {"test-module-a", "test-module-b", "test-module-c"})
  373 + self.assertEqual(mock_execute.call_count, 3)
  374 +
  375 +
271 376 if __name__ == "__main__":
272 377 unittest.main()
... ...
src/work_graph_plan.py
... ... @@ -77,6 +77,34 @@ _PROMPT_TEMPLATE = (
77 77 "shaped exactly like {{\"nodes\": [...], \"edges\": [...]}} -- no extra wrapper key."
78 78 )
79 79  
  80 +# E (2026-08-06): the generic, cross-Org-Agent path -- used for every
  81 +# org_node OTHER than 'planner'. Planner's own doc-writing chain stays on
  82 +# _PROMPT_TEMPLATE above unchanged (zero regression risk, see
  83 +# docs/todo/dynamic-work-graph-design-skill-design.md §3): that chain is a
  84 +# fixed architectural constant (brd->prd->sdd->benchmark->...), not
  85 +# something that should vary call to call, whereas Coder/Validator's
  86 +# sub-tasks have no such fixed small vocabulary of node types. Each node
  87 +# here carries its own self-contained `task_prompt` (see the
  88 +# design-dynamic-work-graph Skill this prompt points at) instead of an
  89 +# `artifact_type` that a fixed lookup table like _DOC_PROMPTS could resolve.
  90 +_GENERIC_PROMPT_TEMPLATE = (
  91 + "Plan (do not execute) a Work Graph for this goal: {goal_text}\n"
  92 + "Follow the design-dynamic-work-graph Skill's methodology: only add a depends_on edge "
  93 + "when a node's task_prompt genuinely needs another node's output content -- nodes with "
  94 + "no real data dependency between them will be scheduled to run CONCURRENTLY, so adding "
  95 + "an edge just to make the graph look ordered actively slows execution down. If this goal "
  96 + "is small enough to finish in one call, output a single-node graph_manifest rather than "
  97 + "forcing a split.\n"
  98 + "Output a graph_manifest whose nodes each have: node_id (a short readable identifier), "
  99 + "depends_on (list of other node_ids), and task_prompt (the FULL, self-contained prompt "
  100 + "for that node's own execution -- assume the agent running it sees nothing except this "
  101 + "task_prompt and its dependencies' outputs, not this planning conversation). Every edge "
  102 + "needs edge_type: 'zero_token' for pure aggregation/reduce steps, 'needs_judgment' for "
  103 + "anything requiring the agent to actually reason about content.\n"
  104 + "End your reply with the graph_manifest as a single fenced ```json code block "
  105 + "shaped exactly like {{\"nodes\": [...], \"edges\": [...]}} -- no extra wrapper key."
  106 +)
  107 +
80 108 _JSON_FENCE_RE = re.compile(r"```(?:json)?\s*(\{.*?\})\s*```", re.DOTALL)
81 109  
82 110  
... ... @@ -180,8 +208,16 @@ def _normalize_graph_manifest(manifest):
180 208  
181 209 def plan_work_graph(conn, org_node, task_id, goal_text, target_folder_path, timeout=300):
182 210 """Returns {"run_id", "graph_manifest"}. Creates one `work_graph_run` row
183   - (status='planning') -- moving it to 'running'/executing it is out of scope."""
184   - prompt = _PROMPT_TEMPLATE.format(goal_text=goal_text)
  211 + (status='planning') -- moving it to 'running'/executing it is out of scope.
  212 +
  213 + E (2026-08-06): `org_node == 'planner'` still gets the fixed
  214 + brd/prd/sdd/benchmark/... chain prompt (_PROMPT_TEMPLATE, unchanged).
  215 + Every other org_node gets _GENERIC_PROMPT_TEMPLATE -- the cross-Org-
  216 + Agent dynamic planning path described in the design-dynamic-work-graph
  217 + Skill, whose nodes carry their own task_prompt instead of a fixed
  218 + artifact_type. Function signature/return shape are unchanged either way."""
  219 + template = _PROMPT_TEMPLATE if org_node == "planner" else _GENERIC_PROMPT_TEMPLATE
  220 + prompt = template.format(goal_text=goal_text)
185 221  
186 222 result = subprocess.run(
187 223 [
... ... @@ -220,3 +256,15 @@ def plan_work_graph(conn, org_node, task_id, goal_text, target_folder_path, time
220 256 )
221 257 conn.commit()
222 258 return {"run_id": run_id, "graph_manifest": graph_manifest}
  259 +
  260 +
  261 +def generic_node_prompt_builder(node, outputs):
  262 + """E (2026-08-06): the `build_prompt(node, outputs)` run_graph() needs
  263 + for a graph_manifest produced via _GENERIC_PROMPT_TEMPLATE -- lives
  264 + here, not in e2e_pipeline.py, because it's tightly coupled to the
  265 + `task_prompt` field convention that prompt asks for (keeping both in
  266 + the same module means a field rename can't silently desync producer
  267 + and consumer). No factory/closure needed: every node is self-contained
  268 + per the design-dynamic-work-graph Skill's contract, so `outputs`
  269 + (already-completed dependency content) isn't needed here."""
  270 + return node["task_prompt"]
... ...