Commit 8d8eaa7407bd8bf897e1ccfa30d3476ce8593016
1 parent
b96e6895
docs: Phase 2 SDD - universal self-planning + merged graph + dynamic loop_back
Design for FR-2/FR-3/FR-4: extending self-planning to Coder/Validator,
per-node subagent/skill/tools binding, and node-level precise loop_back
replacing org_level_loop.py's coarse fast_lane/full_replan routing.
Three findings during design research that revise the requirements doc's
original FR-2 assumption ("just swap the call site"):
- _GENERIC_PROMPT_TEMPLATE never solicits judgment/on_fail today -- Coder/
Validator switching to plan_work_graph() as-is would produce graphs with
no judgment/loop-back capability at all.
- run_graph()'s subagent_type/skill_ref are run-level constants passed to
every node identically, not node-level despite work_node_execution
already having columns for it -- needs a real (bounded) engine extension.
- Today's on_fail.action="loop_back" target is statically declared at
planning time via a single on_failure edge; it cannot express "a
terminal validator dynamically diagnoses which of several ancestor
nodes is the true root cause." New on_fail.action="dynamic_loop_back"
designed for this, reusing root_cause_trace.py's jury/majority-vote
structure with the vote target changed from a category to a node_id.
Also incorporates three requirements-doc §7 decisions that were made
(apparently concurrently) while this was being designed and overrule two
of this doc's initial drafts: work_graph_run.org_node is removed entirely
(not kept/sunk to node level) with concrete work_graph_run_query.py/
work_graph_fork.py rework; Coder/Validator get a suggested node-type
catalog (not fully free-form); the append interface is a parameter on
the existing plan_work_graph() (not a new function).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
2 changed files
with
424 additions
and
5 deletions
docs/todo/org-graph-unification-and-universal-self-planning-requirements.md
| @@ -104,12 +104,13 @@ | @@ -104,12 +104,13 @@ | ||
| 104 | - **验收**:能在(已真实复现过的)"benchmark 输出叙述文字冒充产出"场景下,在 Coder 动手之前拦住任务;崩溃后 resume 不再假设 Planner artifact 一定存在;fresh-run 也不再假设 prd/benchmark 一定存在。 | 104 | - **验收**:能在(已真实复现过的)"benchmark 输出叙述文字冒充产出"场景下,在 Coder 动手之前拦住任务;崩溃后 resume 不再假设 Planner artifact 一定存在;fresh-run 也不再假设 prd/benchmark 一定存在。 |
| 105 | 105 | ||
| 106 | ### Phase 2 — 全员自规划 + 合图(方案 A)+ 节点级验证 skill | 106 | ### Phase 2 — 全员自规划 + 合图(方案 A)+ 节点级验证 skill |
| 107 | +- **详细设计**:`docs/todo/org-graph-unification-phase2-universal-self-planning-design.md`(2026-08-08,按 §7 四条拍板结果改写)。设计阶段发现范围比原设想更大——`_GENERIC_PROMPT_TEMPLATE` 从未征集 judgment/on_fail、`run_graph()` 的 subagent_type/skill_ref 是 run 级常量不是节点级、现有 `loop_back` 是规划阶段静态声明的单一目标不支持终审失败时动态选择根因节点(新增 `on_fail.action="dynamic_loop_back"` 解决,复用 `root_cause_trace.py` 陪审团结构)——详见该文档 §1/§5。 | ||
| 107 | - **范围**:FR-2 + FR-3 + FR-4,按 §4/§4.1 已拍板的合图路径实现。 | 108 | - **范围**:FR-2 + FR-3 + FR-4,按 §4/§4.1 已拍板的合图路径实现。 |
| 108 | -- **产出**: | ||
| 109 | - 1. `work_graph_run.org_node` 从 run 级单值列下沉为节点级字段(与 FR-3 的 subagent/skill/tools/data 绑定字段合并设计),`ux_work_graph_run_active` 唯一约束按合图后的新语义调整(schema 迁移)。 | ||
| 110 | - 2. Coder/Validator 接入 `plan_work_graph()`,但不再各自开新 `run_id`——改为把自己规划出的节点**增量追加**进 Phase 1 已经建好的同一个跨阶段 `work_graph_run`(§4.1 第 2 点的追加机制)。 | ||
| 111 | - 3. 新增节点级验证 skill,`org_level_loop.py` 的粗粒度 `fast_lane`/`full_replan` 路由被替换为图内精确 `loop_back`(因为合图后失败节点和目标节点天然在同一个 `run_id`,不再需要跨图路由层)。 | ||
| 112 | -- **前置条件**:Phase 1 落地的"复核"阶段先用 `org_level_loop.py` 粗粒度路由兜底上线;Phase 2 把这段兜底逻辑替换成图内精确 loop_back,同时完成 §4.1 的三个迁移点。 | 109 | +- **产出**(按拍板后的最终形状): |
| 110 | + 1. `work_graph_run.org_node` 列彻底删除(不是下沉/保留),`work_graph_run_query.py`/`work_graph_fork.py` 改造为从 `work_node_execution.subagent_type` 聚合/逐节点判断;`ux_work_graph_run_active` 收窄为 `UNIQUE(task_id)`。 | ||
| 111 | + 2. Coder/Validator 接入 `plan_work_graph()`,新增 `append_to_run_id` 参数(不是独立函数)实现增量追加进 Phase 1 建好的跨阶段 `work_graph_run`;新增 `_CODER_NODE_CATALOG`/`_VALIDATOR_NODE_CATALOG` 建议节点清单(不是完全自由规划)。 | ||
| 112 | + 3. `run_graph()`/`_run_one_node()` 扩展为读取节点自己声明的 `subagent_type`/`skill_ref`(缺省退回图级默认),新增节点级验证 skill `work-graph-node-review`,`org_level_loop.py` 的粗粒度 `fast_lane`/`full_replan` 路由被 `dynamic_loop_back` 取代(不删除该模块,其他调用方仍在用)。 | ||
| 113 | +- **前置条件**:Phase 1 落地的"复核"阶段先用 `org_level_loop.py` 粗粒度路由兜底上线;Phase 2 把这段兜底逻辑替换成图内精确 loop_back。 | ||
| 113 | - **风险**:Coder 开始自规划、产出内容型节点后,`validator-test-script-narrative-output-issue.md` 记录的叙述文字复发风险需要在本 phase 内验证是否触发,必要时补加机械拦截网(参考 `check_script_compiles()` 的先例),不是事后打补丁。 | 114 | - **风险**:Coder 开始自规划、产出内容型节点后,`validator-test-script-narrative-output-issue.md` 记录的叙述文字复发风险需要在本 phase 内验证是否触发,必要时补加机械拦截网(参考 `check_script_compiles()` 的先例),不是事后打补丁。 |
| 114 | - **建议前置(非强制)**:`docs/todo/work-graph-node-execution-deblackbox-requirements.md`(2026-08-08,单节点执行层去黑盒化,正交但相关)——Phase 2 会大量新增 `judgment.kind="llm"` 节点级判断调用,如果去黑盒化工作先完成,Phase 2 可以直接基于新的 `trigger_org_node_structured()` 设计,不需要在原生 Workflow 和新机制之间选一套来复用。 | 115 | - **建议前置(非强制)**:`docs/todo/work-graph-node-execution-deblackbox-requirements.md`(2026-08-08,单节点执行层去黑盒化,正交但相关)——Phase 2 会大量新增 `judgment.kind="llm"` 节点级判断调用,如果去黑盒化工作先完成,Phase 2 可以直接基于新的 `trigger_org_node_structured()` 设计,不需要在原生 Workflow 和新机制之间选一套来复用。 |
| 115 | 116 |
docs/todo/org-graph-unification-phase2-universal-self-planning-design.md
0 → 100644
| 1 | +# Phase 2 — 全员自规划 + 合图 + 节点级精确 loop_back——详细设计(SDD) | ||
| 2 | + | ||
| 3 | +**日期**: 2026-08-08 | ||
| 4 | +**性质**: 详细设计,对应 `docs/todo/org-graph-unification-and-universal-self-planning-requirements.md` 的 Phase 2(FR-2/FR-3/FR-4),按 §4/§4.1 已拍板的合图(方案 A)路径设计。 | ||
| 5 | +**依赖**: 硬依赖 Phase 1(`docs/todo/org-graph-unification-phase1-review-gate-design.md`)先落地——Phase 2 是在 Phase 1 建立的 `planner_reviewer` 阶段基础上继续合图,Phase 1 未完成前本文档的调用点改动无法接入。Phase 1 目前由另一个进程在实施中,本文档的具体代码位置引用以 Phase 1 SDD 的设计为准,Phase 1 真正落地后需要一次快速核对(见 §9)。 | ||
| 6 | +**状态**: 设计草案。**2026-08-08 修订**:写作过程中,需求文档 §7 的四个开放问题已被用户直接拍板(详见该文档 §7 对应条目),其中两条推翻了本文档最初的设计判断——Coder/Validator 自规划改为"建议节点清单"而非完全自由(§3),`work_graph_run.org_node` 改为彻底移除而非保留(§2.1)——本版本已按拍板结果改写,不是原始草稿。 | ||
| 7 | + | ||
| 8 | +--- | ||
| 9 | + | ||
| 10 | +## 1. 一个必须先说清楚的修正:这比需求文档原来设想的更大 | ||
| 11 | + | ||
| 12 | +`org-graph-unification-and-universal-self-planning-requirements.md` 里 FR-2 原话是"函数本身已通用,只需替换调用点"——**这个判断经本次设计调研核实是不准确的**,具体差距见下面三点。本文档按修正后的真实范围设计,不是简单的调用点替换。 | ||
| 13 | + | ||
| 14 | +### 1.1 `_GENERIC_PROMPT_TEMPLATE` 从未征集 `judgment`/`on_fail` | ||
| 15 | + | ||
| 16 | +`work_graph_plan.py:171-187` 的 `_GENERIC_PROMPT_TEMPLATE`(Coder/Validator 走 `plan_work_graph()` 时会用到的模板)只要求 LLM 给每个节点 `node_id`/`depends_on`/`task_prompt`,**完全没有要求 `judgment`/`on_fail` 字段**——跟 Planner 专用的 `_PROMPT_TEMPLATE`(`work_graph_plan.py:136-159`,明确征集 judgment/on_fail/on_failure 边)不是同一份提示词。如果只是把 `implement_code()`/`validate_implementation_with_own_graph()` 的硬编码 manifest 换成调用 `plan_work_graph(conn, "coder", ...)`,产出的图**不会带任何判断/回路能力**——FR-4 想要的"每个节点验证+精确 loop_back"根本无从谈起。**必须先扩展 `_GENERIC_PROMPT_TEMPLATE`**,见 §3。 | ||
| 17 | + | ||
| 18 | +### 1.2 `run_graph()`/`work_node_execution` 的 `subagent_type`/`skill_ref` 是 run 级常量,不是节点级 | ||
| 19 | + | ||
| 20 | +`work_node_execution` 表(`schema.py:156-176`)已经有 `subagent_type`/`skill_ref` 两列——但 `_run_one_node()`(`work_graph_orchestrate.py:837-903`)调用 `_ensure_node_row()` 时,传的是 `run_graph()` 整个调用唯一的 `org_node`/`skill_id` 参数(`work_graph_orchestrate.py:856-857`),对图里**每一个节点都相同**,不是节点自己声明的值。FR-3 要的"每个节点声明用哪个 subagent + skill 实现"这个字段今天在数据库层已经有位置放,但执行链路上从来没有真正做到"节点级差异化"。**这是需要真正扩展的执行引擎逻辑**,不是数据已经在只是没读——见 §4。 | ||
| 21 | + | ||
| 22 | +### 1.3 现有 `on_fail.action="loop_back"` 是静态预声明的单一目标,不是动态根因判断 | ||
| 23 | + | ||
| 24 | +这是本次调研发现的**最关键的架构缺口**:今天的 loop_back 机制要求节点在**规划阶段**就通过 `edge_type="on_failure"` 边声明"我失败了就跳回哪一个节点"(`work_graph_orchestrate.py:315-326` `_on_failure_target`,`work_graph_plan.py:247-283` `_validate_loop_back_targets` 在规划完成时就校验这条边指向合法上游节点)——**目标节点是写死的,不是失败发生时才动态判断的**。 | ||
| 25 | + | ||
| 26 | +但 FR-4 想要的是"终审 Validator 判断失败后,根因判断具体是哪个更早的节点出问题,精确跳回那个节点"——终审失败时,真正的根因可能是 SDD 节点、Benchmark 节点、或某个 Coder 实现节点中的任意一个,**这个"该跳回哪"的判断只能在失败发生的那一刻、看到具体的失败症状后才能做出,不可能在规划阶段预先声明一条固定的边**。现有机制完全没有覆盖这种"下游失败,动态诊断该回退到众多上游候选节点里的哪一个"的场景——它只覆盖"这个节点自己的产出不合格,固定退回同一个预先约定的地方"。 | ||
| 27 | + | ||
| 28 | +**这意味着 FR-4 需要给引擎新增一种 `on_fail.action`(本文档称之为 `dynamic_loop_back`),不是简单复用现有 `loop_back`**——见 §5。这确实修改了 `work_graph_orchestrate.py` 的判断/回路逻辑本身,跟 Phase 1 需求文档非目标 §5.3"不重写 judgment/on_fail/loop_back 引擎"的表述有出入;本文档认为这是必要的、有边界的**扩展**(新增一个 action 分支,不改动任何现有 action 的行为),不是"重写",Phase 1 那条非目标应理解为"不推倒重来"而非"一行都不能动",但这个偏差需要明确记录,不能悄悄做。 | ||
| 29 | + | ||
| 30 | +--- | ||
| 31 | + | ||
| 32 | +## 2. Schema 变更 | ||
| 33 | + | ||
| 34 | +### 2.1 `work_graph_run.org_node` 彻底移除(用户拍板,2026-08-08) | ||
| 35 | + | ||
| 36 | +本节最初的设计判断是"不需要改,保留作'run 由谁发起'的元数据"——**已被用户推翻**:合图后一个 task 全程只有一个 `work_graph_run` 行,节点分属不同 org_node(`planner`/`planner_reviewer`/`coder`/`validator`),继续在 run 级保留单值 `org_node` 列既不准确(一个 run 里真实混着 4 种 org_node 的节点)也没有实际用途(真正的归属信息在 §1.2 已经确认属于 `work_node_execution.subagent_type`)——决策是彻底删除这一列,不是留作冗余字段。 | ||
| 37 | + | ||
| 38 | +**Schema 迁移**:`work_graph_run` 表删除 `org_node` 列(SQLite 无 `DROP COLUMN` 的老版本需要 create-copy-drop-rename,跟本文档 §2.2 同一个迁移函数里一起做,或者用现代 SQLite 的 `ALTER TABLE ... DROP COLUMN` 直接支持——取决于实施时 SQLite 版本,若支持则用后者,更简单)。 | ||
| 39 | + | ||
| 40 | +**下游改造点**(决策本身点名的两处,本节给出具体定义): | ||
| 41 | + | ||
| 42 | +#### 2.1.1 `work_graph_run_query.py:54-67`(`?org_node=` 过滤 + 结果字段) | ||
| 43 | + | ||
| 44 | +```python | ||
| 45 | +query = ( | ||
| 46 | + "SELECT wgr.run_id, wgr.task_id, wgr.status, wgr.started_at, wgr.completed_at, " | ||
| 47 | + "(SELECT COUNT(*) FROM work_node_execution wne WHERE wne.run_id = wgr.run_id) AS node_count, " | ||
| 48 | + "(SELECT GROUP_CONCAT(DISTINCT wne.subagent_type) FROM work_node_execution wne " | ||
| 49 | + " WHERE wne.run_id = wgr.run_id) AS org_nodes_involved, " | ||
| 50 | + "wgr.parent_run_id, wgr.fork_of_node_exec_id " | ||
| 51 | + "FROM work_graph_run wgr JOIN task t ON t.task_id = wgr.task_id WHERE t.project_id = ?" | ||
| 52 | +) | ||
| 53 | +params = [project_id] | ||
| 54 | +if org_node: | ||
| 55 | + query += ( | ||
| 56 | + " AND EXISTS (SELECT 1 FROM work_node_execution wne " | ||
| 57 | + "WHERE wne.run_id = wgr.run_id AND wne.subagent_type = ?)" | ||
| 58 | + ) | ||
| 59 | + params.append(org_node) | ||
| 60 | +``` | ||
| 61 | + | ||
| 62 | +返回形状里原来的单值 `"org_node": r[2]` 换成 `"org_nodes_involved": r[...].split(",") if r[...] else []`(GROUP_CONCAT 返回逗号拼接字符串或 NULL,app 层拆成列表)——调用方(`task_query.py`/frontend)原来读单值 `org_node` 字段的地方,改成读这个列表;Phase 3 的 Org 级元图(FR-5a)本来就需要知道"这个 run 横跨了哪几个 org_node 阶段",这个字段直接服务那个需求,不是额外负担。 | ||
| 63 | + | ||
| 64 | +#### 2.1.2 `work_graph_fork.py` 的 `org_node` 用法(三处,逐一替换) | ||
| 65 | + | ||
| 66 | +1. **`create_fork()` 第 125-137 行**:SELECT 语句去掉 `wgr.org_node`;原来解出的 `org_node` 变量直接用同一行已经在读的 `wne.subagent_type`(源节点自己的执行身份)替代——这个值本来就在查询结果里,不需要新增查询。 | ||
| 67 | +2. **`_reconstruct_build_prompt(conn, task_id, org_node, graph_manifest)`(第 94-113 行)**:**去掉 `org_node` 参数**,dispatch 逻辑从"整张图统一按 `org_node=='planner'` 且全部节点有 `artifact_type`"改成**逐节点判断**——这本来就是合图后必须解决的问题(一张合并图会同时包含 Planner 的 `artifact_type` 节点和 Coder/Validator 的 `task_prompt` 节点,§2.1 决策只是让这个改动提前到现在做,不是额外增加的范围): | ||
| 68 | + | ||
| 69 | + ```python | ||
| 70 | + def _reconstruct_build_prompt(conn, task_id, graph_manifest): | ||
| 71 | + goal_text = conn.execute("SELECT goal_text FROM task WHERE task_id = ?", (task_id,)).fetchone()[0] | ||
| 72 | + node_artifact_types = { | ||
| 73 | + n["node_id"]: n["artifact_type"] for n in graph_manifest["nodes"] if "artifact_type" in n | ||
| 74 | + } | ||
| 75 | + planner_builder = e2e_pipeline._planner_prompt_builder(goal_text, node_artifact_types) | ||
| 76 | + | ||
| 77 | + def build_prompt(node, outputs): | ||
| 78 | + if "artifact_type" in node: | ||
| 79 | + return planner_builder(node, outputs) | ||
| 80 | + if "task_prompt" in node: | ||
| 81 | + return work_graph_plan.generic_node_prompt_builder(node, outputs) | ||
| 82 | + raise ForkGraphShapeUnsupportedError( | ||
| 83 | + "node {} has neither 'artifact_type' nor 'task_prompt'".format(node["node_id"]) | ||
| 84 | + ) | ||
| 85 | + return build_prompt | ||
| 86 | + ``` | ||
| 87 | + | ||
| 88 | +3. **`INSERT INTO work_graph_run (...)`(第 164-168 行)**、**`fork_info`/`execute_fork()` 里的 `org_node`(第 189/208 行)**:INSERT 语句去掉 `org_node` 列;`execute_fork()` 调用 `run_graph()` 时原来传 `fork_info["org_node"]` 的位置,改传 `fork_info["subagent_type"]`(源节点自己的执行身份,作为 `run_graph()` 新签名里"没有节点级覆盖时的默认值",见 §4.1)。 | ||
| 89 | + | ||
| 90 | +### 2.2 `ux_work_graph_run_active` 语义变化(需要迁移) | ||
| 91 | + | ||
| 92 | +`schema.py:151-152`:`CREATE UNIQUE INDEX ux_work_graph_run_active ON work_graph_run(task_id, org_node)`(覆盖非终态 status)——语义是"同一 task 同一 org_node 同一时刻只有一个活跃 run"。合图后一个 task 全程只有一个活跃 run(不再按 org_node 切分),这条唯一索引的粒度要从 `(task_id, org_node)` 收紧为 `(task_id)`: | ||
| 93 | + | ||
| 94 | +```sql | ||
| 95 | +DROP INDEX ux_work_graph_run_active; | ||
| 96 | +CREATE UNIQUE INDEX ux_work_graph_run_active ON work_graph_run(task_id) | ||
| 97 | + WHERE status NOT IN ('done', 'failed'); | ||
| 98 | +``` | ||
| 99 | + | ||
| 100 | +(SQLite 的部分索引语法用 `WHERE` 子句表达"仅非终态",比原来在应用层散落判断更直接——可以借这次迁移顺手把语义从"应用层约定"变成"数据库约束",原索引其实也是全表扫描等价的部分索引,这里是等价改写,不是新引入部分索引这个概念。) | ||
| 101 | + | ||
| 102 | +新增迁移函数 `migrate_narrow_work_graph_run_active_index(conn)`,照抄 §2.3 的思路:先检查现有索引定义,已经是新语义就跳过(幂等)。 | ||
| 103 | + | ||
| 104 | +### 2.3 `artifact.artifact_type` CHECK 枚举 vs 自规划节点的任意 `node_id` | ||
| 105 | + | ||
| 106 | +**决策**:自规划出的 Coder/Validator 节点**不是每一个都要落成 `artifact` 行**。沿用 `implement_code()` 今天的既有设计——只有"最终交付物"(`code_diff`/`test_report`)才落 `artifact`,中间探索/推理节点的产出只落 `work_node_execution.output_payload`(已经是持久化的,断点续跑不受影响),不经过 `_write_completed_node_artifacts()` 那条路径。 | ||
| 107 | + | ||
| 108 | +具体做法:`_GENERIC_PROMPT_TEMPLATE` 扩展后(§3)要求节点声明一个新的可选字段 `promotes_to_artifact`(`{"artifact_type": "code_diff", ...}` 形状,缺省表示"这个节点的产出不落 artifact,只是图内部的中间步骤");`_write_completed_node_artifacts()`(Planner/Validator 现在共用的落盘函数)改成只处理声明了 `promotes_to_artifact` 的节点,不再对图里每个节点都无脑落一行——这本来就是 Planner 自己的 `_PLANNER_NODE_CATALOG` 節点隐含的假设(每个 Planner 节点都是"最终交付物"),Coder/Validator 的自由节点不能沿用同一假设。 | ||
| 109 | + | ||
| 110 | +`artifact.artifact_type` 的 CHECK 枚举**不需要扩表**——`promotes_to_artifact.artifact_type` 只允许落在已有的枚举值里(`code_diff`/`test_report`/`validator_benchmark`/`validator_test_dataset`/`validator_test_script` 等),由 Coder/Validator 的 prompt 明确约束"你的最终交付物只能声明这几种 artifact_type 之一",不开放任意值——避免把这个本来就有明确边界的表变成"什么都能塞"的自由字段。 | ||
| 111 | + | ||
| 112 | +--- | ||
| 113 | + | ||
| 114 | +## 3. `_GENERIC_PROMPT_TEMPLATE` 扩展(对应 §1.1)+ Coder/Validator 建议节点清单(用户拍板,2026-08-08) | ||
| 115 | + | ||
| 116 | +原设计判断"Coder/Validator 保持完全自由规划,不设清单"——**已被推翻**:`work_graph_plan.py:161-170` 2026-08-06 的注释原话"Coder/Validator's sub-tasks have no such fixed small vocabulary of node types"被用户拍板否决,Phase 2 要求给 Coder/Validator 各自定义一份类似 `_PLANNER_NODE_CATALOG` 的"建议节点类型清单"——建议不强制(跟 Planner 目录同样的措辞和约束力度,不是 Planner 文档链那种固定顺序的强约束),每项可以带 `suggested_judgment`。 | ||
| 117 | + | ||
| 118 | +### 3.1 新增两份建议目录 | ||
| 119 | + | ||
| 120 | +```python | ||
| 121 | +_CODER_NODE_CATALOG = [ | ||
| 122 | + {"node_type": "impl", "description": "Actual code changes implementing the SDD."}, | ||
| 123 | + { | ||
| 124 | + "node_type": "unit_test", | ||
| 125 | + "description": "New/updated unit tests covering the change -- same repo convention as existing tests.", | ||
| 126 | + "suggested_judgment": {"kind": "llm", "judge_prompt": _NODE_REVIEW_JUDGE_PROMPT}, # 见 §7.1 新 skill | ||
| 127 | + }, | ||
| 128 | + { | ||
| 129 | + "node_type": "integration_check", | ||
| 130 | + "description": "Actually runs the real test suite/benchmark script against the change and reports pass/fail -- not a description of doing so.", | ||
| 131 | + "suggested_judgment": {"kind": "llm", "judge_prompt": _NODE_REVIEW_JUDGE_PROMPT}, | ||
| 132 | + }, | ||
| 133 | +] | ||
| 134 | +_VALIDATOR_NODE_CATALOG = [ | ||
| 135 | + { | ||
| 136 | + "node_type": "independent_benchmark", | ||
| 137 | + "description": "Validator's own held-out acceptance scenarios -- NOT reused from Planner's/Coder's benchmark, a genuine second opinion.", | ||
| 138 | + }, | ||
| 139 | + {"node_type": "test_dataset", "description": "Concrete test data, if this goal has a natural one."}, | ||
| 140 | + { | ||
| 141 | + "node_type": "test_script", | ||
| 142 | + "description": "A real, runnable script mechanically checking the independent benchmark's scenarios.", | ||
| 143 | + "suggested_judgment": {"kind": "mechanical", "check_ref": "check_script_compiles"}, | ||
| 144 | + }, | ||
| 145 | +] | ||
| 146 | +_GENERIC_NODE_CATALOGS = {"coder": _CODER_NODE_CATALOG, "validator": _VALIDATOR_NODE_CATALOG} | ||
| 147 | +``` | ||
| 148 | + | ||
| 149 | +`node_type` 是目录里的建议标签,不是节点 schema 的新必填字段——Coder/Validator 的节点形状仍然是 `node_id`/`depends_on`/`task_prompt`(跟今天一样,`artifact_type` 是 Planner 专属的字段,不引入到这条路径),目录只是喂给 LLM 参考"这类目标通常需要哪几种节点",最终 `node_id`/`task_prompt` 怎么写仍由 LLM 自己决定,不强制照抄目录里的 `node_type` 字面值。 | ||
| 150 | + | ||
| 151 | +### 3.2 `_GENERIC_PROMPT_TEMPLATE` 改为按 `org_node` 参数化,注入对应目录 | ||
| 152 | + | ||
| 153 | +原模板是一段固定字符串,只用 `.format(goal_text=goal_text)` 填一个占位符(`work_graph_plan.py:351` `plan_work_graph()`里的调用)。现在要注入目录 JSON(自带花括号),复用 `_PROMPT_TEMPLATE` 已经验证过的"拼接 + `.replace()`"写法,不用 `.format()`(同一个转义坑,`_normalize_graph_manifest` 的 docstring 已经记录过一次真实事故,不要在这里重蹈): | ||
| 154 | + | ||
| 155 | +```python | ||
| 156 | +def _generic_prompt_template(org_node): | ||
| 157 | + catalog = _GENERIC_NODE_CATALOGS.get(org_node, []) | ||
| 158 | + catalog_section = ( | ||
| 159 | + "Suggested node types for this goal (use any subset, in any order, invent your own if " | ||
| 160 | + "none fit -- these are starting points, not requirements):\n" + json.dumps(catalog) + "\n" | ||
| 161 | + if catalog else "" | ||
| 162 | + ) | ||
| 163 | + return ( | ||
| 164 | + "Plan (do not execute) a Work Graph for this goal: {goal_text}\n" | ||
| 165 | + + catalog_section + | ||
| 166 | + "Follow the design-dynamic-work-graph Skill's methodology: only add a depends_on edge " | ||
| 167 | + "when a node's task_prompt genuinely needs another node's output content ...\n" | ||
| 168 | + "Output a graph_manifest whose nodes each have: node_id, depends_on, task_prompt, plus:\n" | ||
| 169 | + " - judgment: how to tell if this node's output is acceptable -- {\"kind\": \"none\"} if " | ||
| 170 | + " nothing needs checking, {\"kind\": \"llm\", \"judge_prompt\": \"...{content}...\"} for " | ||
| 171 | + " your own judgment prompt with a {content} placeholder.\n" | ||
| 172 | + " - on_fail (only if judgment.kind != \"none\"): {\"action\": \"retry_self\"}, " | ||
| 173 | + " {\"action\": \"hard_fail\"}, or {\"action\": \"loop_back\"} with a matching on_failure " | ||
| 174 | + " edge to a genuine upstream dependency.\n" | ||
| 175 | + " - promotes_to_artifact (optional): {\"artifact_type\": \"code_diff\"|\"test_report\"|...} " | ||
| 176 | + " ONLY for nodes producing this task's actual final deliverable.\n" | ||
| 177 | + "Every edge needs edge_type: 'zero_token' for pure aggregation/reduce steps, 'needs_judgment' " | ||
| 178 | + "for anything requiring the agent to actually reason about content.\n" | ||
| 179 | + "End your reply with the graph_manifest as a single fenced ```json code block ..." | ||
| 180 | + ) | ||
| 181 | +``` | ||
| 182 | + | ||
| 183 | +`plan_work_graph()` 里 `else` 分支(`work_graph_plan.py:350-351`)从 `prompt = _GENERIC_PROMPT_TEMPLATE.format(goal_text=goal_text)` 改成 `prompt = _generic_prompt_template(org_node).replace("{goal_text}", goal_text)`。 | ||
| 184 | + | ||
| 185 | +`GRAPH_MANIFEST_SCHEMA`(`work_graph_plan.py:33-86`)同步补充 `promotes_to_artifact` 的文档性字段(跟 `judgment`/`on_fail` 一样,仅供文档参考,不做 jsonschema 强校验,原因见模块自己的 `--agent`+`--json-schema` 限制说明)。 | ||
| 186 | + | ||
| 187 | +`_normalize_graph_manifest()`(`work_graph_plan.py:286-333`)不需要改——它的 `_validate_loop_back_targets` 校验逻辑已经是通用的,不区分 Planner 的固定链还是 Coder/Validator 的目录建议图。 | ||
| 188 | + | ||
| 189 | +--- | ||
| 190 | + | ||
| 191 | +## 4. 节点级 `subagent_type`/`skill_ref`/`tools` 绑定(对应 §1.2,FR-3 的"用什么实现") | ||
| 192 | + | ||
| 193 | +### 4.1 `run_graph()`/`_run_one_node()` 签名扩展 | ||
| 194 | + | ||
| 195 | +`run_graph(conn, run_id, org_node, skill_id, target_folder_path, graph_manifest, ...)` 的 `org_node`/`skill_id` 两个参数保留(作为"没有节点级覆盖时的默认值"),`_run_one_node()` 内部改成: | ||
| 196 | + | ||
| 197 | +```python | ||
| 198 | +def _run_one_node(conn, run_id, org_node, skill_id, target_folder_path, node, outputs, graph_manifest, ...): | ||
| 199 | + node_org_node = node.get("subagent_type", org_node) # 节点自己声明的执行身份,缺省用图级默认 | ||
| 200 | + node_skill_id = node.get("skill_ref", skill_id) | ||
| 201 | + ... | ||
| 202 | + node_exec_id = _ensure_node_row( | ||
| 203 | + conn, run_id, exec_node_id_str, node_org_node, node_skill_id, ... | ||
| 204 | + ) | ||
| 205 | + ... | ||
| 206 | + run_fn = _make_run_fn(conn, target_folder_path, node_exec_id, node_org_node, prompt, output_schema, timeout) | ||
| 207 | +``` | ||
| 208 | + | ||
| 209 | +这不是把 `run_graph()` 整个重写——只是把原来"直接用闭包里的 `org_node`/`skill_id`"改成"先看节点自己有没有声明,没有就退回闭包默认值",两行改动,向后兼容(没有声明 `subagent_type` 的节点行为完全不变,Planner 的固定链、Coder 现有的合成单节点图都不受影响)。 | ||
| 210 | + | ||
| 211 | +`_resolve_loop_limit()`(`work_graph_orchestrate.py:512-528`)、`_llm_judgment_verify_fn()`(`work_graph_orchestrate.py:420-467`)里同样用到的 `org_node` 参数,同步改成接收 `node_org_node`(调用方在 `_run_one_node` 里已经解析好,直接传下去,不需要这两个函数自己再判断一次)。 | ||
| 212 | + | ||
| 213 | +### 4.2 "tools" 绑定:复用 `docs/todo/work-graph-node-execution-deblackbox-requirements.md` 的 `trigger_org_node_structured()` | ||
| 214 | + | ||
| 215 | +节点声明 `tools`(如 `{"disallowed_tools": ["Bash"]}`,只做限制、不做放宽——不能让一个自规划节点给自己开出角色本身 `role.json` 没有的权限)时,去黑盒化文档设计的 `trigger_org_node_structured(org_node, prompt, target_folder_path, output_schema, model=None, timeout=300)` 需要再加一个可选的 `disallowed_tools` 参数,透传成底层 `claude` 调用的 `--disallowedTools` CLI 参数(05 §5.4.1.2 已经确认 CLI 顶层参数支持按工具/子命令限定,跟 subagent frontmatter 不支持是两条不同的解析路径)。 | ||
| 216 | + | ||
| 217 | +节点声明的 `disallowed_tools` 与角色 `role.json` 的 `permissions.disallowed_tools` 取并集(只加限制不减限制),在 `_run_one_node` 里合并后传给 `trigger_org_node_structured`。 | ||
| 218 | + | ||
| 219 | +**"data" 绑定**:即"这个节点的输入来自哪些上游节点",`depends_on` + `build_prompt(node, outputs)` 已经完整覆盖这件事(`outputs` 字典就是已完成上游节点的产出),不需要新机制——用户诉求里的"data"就是这一层,只是没有用现有术语描述。 | ||
| 220 | + | ||
| 221 | +--- | ||
| 222 | + | ||
| 223 | +## 5. 新增 `on_fail.action = "dynamic_loop_back"`(对应 §1.3,FR-4 的核心) | ||
| 224 | + | ||
| 225 | +### 5.1 语义 | ||
| 226 | + | ||
| 227 | +跟现有 `loop_back`(跳回一个规划阶段就写死的固定目标)不同,`dynamic_loop_back` 表示"这个节点失败时,由一次专门的根因判断调用,从这个节点的全部上游祖先节点里动态选出真正该回退的目标"——只用于合适放在**图末端的终审性判断节点**(比如 Phase 1 引入的终审 Validator 节点),不要求每个节点都用这个更贵的机制。 | ||
| 228 | + | ||
| 229 | +### 5.2 graph_manifest 字段 | ||
| 230 | + | ||
| 231 | +```jsonc | ||
| 232 | +{ | ||
| 233 | + "node_id": "final_review", | ||
| 234 | + "judgment": {"kind": "llm", "judge_prompt": "..."}, | ||
| 235 | + "on_fail": {"action": "dynamic_loop_back", "max_attempts": 3} | ||
| 236 | + // 不需要对应的 on_failure 边——目标不是静态声明的 | ||
| 237 | +} | ||
| 238 | +``` | ||
| 239 | + | ||
| 240 | +### 5.3 执行逻辑(`work_graph_orchestrate.py` 新增分支) | ||
| 241 | + | ||
| 242 | +`_run_one_node()` 里 `action = (node.get("on_fail") or {}).get("action", "retry_self")` 判断分支新增一支: | ||
| 243 | + | ||
| 244 | +```python | ||
| 245 | +if action == "dynamic_loop_back": | ||
| 246 | + ancestors = sorted(_transitive_ancestors(graph_manifest, node_id) - {node_id}) | ||
| 247 | + diagnosis = _judge_root_cause_node( | ||
| 248 | + conn, node_id, feedback, ancestors, org_node, target_folder_path | ||
| 249 | + ) # 见 §5.4,新函数 | ||
| 250 | + target = diagnosis["target_node_id"] | ||
| 251 | + if target not in ancestors: # 运行时防御,见下方"安全校验" | ||
| 252 | + raise ValueError( | ||
| 253 | + "dynamic_loop_back judge picked {!r}, not a real ancestor of {}".format(target, node_id) | ||
| 254 | + ) | ||
| 255 | + raise _LoopBackRequested(source_node_id=node_id, target_node_id=target, feedback=feedback) | ||
| 256 | +``` | ||
| 257 | + | ||
| 258 | +`_LoopBackRequested`/`run_graph()` 主循环(`_run_layers` 里 `loop_back_signal` 的处理,`work_graph_orchestrate.py:811-830`)**完全不需要改**——它已经是"给一个 `(source_node_id, target_node_id)` 就重新执行 target 及其全部下游"的通用机制,不关心这个 target 是静态声明来的还是动态判断来的,`_transitive_dependents`/`_loop_history_prompt_suffix`/`_resolve_loop_limit` 全部原样复用。**这印证了这确实是"新增一个 action 分支"而不是"重写引擎"**——回路的执行部分完全通用,只有"怎么决定 target 是谁"这一步是新的。 | ||
| 259 | + | ||
| 260 | +### 5.4 `_judge_root_cause_node`:复用 `root_cause_trace.py` 的陪审团结构,换判断目标 | ||
| 261 | + | ||
| 262 | +`root_cause_trace.judge_root_cause_via_work_graph`(`docs/done/verifier-fan-out-jury-design.md`)已经是"多视角 Work Graph fan-out + `_tally_perspectives` 多数票仲裁"的现成机制,只是今天判断的是三选一的粗粒度类别(`planning`/`coder`/`skill_defect`)。新函数复用同一个骨架,把"判断类别"换成"判断具体节点": | ||
| 263 | + | ||
| 264 | +```python | ||
| 265 | +def _judge_root_cause_node(conn, failed_node_id, feedback, candidate_node_ids, org_node, target_folder_path, timeout=180): | ||
| 266 | + """跟 root_cause_trace.judge_root_cause_via_work_graph 同构:每个视角 | ||
| 267 | + 是一次独立的 Work Node fan-out 判断调用,答案从 candidate_node_ids | ||
| 268 | + 里选一个(不是自由文本),_tally_perspectives 同款 >=2/3 多数票仲裁; | ||
| 269 | + < 2/3 一致时不像 root_cause_trace 那样抛 InconclusiveEscalateError 升级 | ||
| 270 | + 人工——见 §5.5,这里退化成"回到最近的上游节点"而不是直接升级人工, | ||
| 271 | + 因为这个判断本身就是可以再来一轮的(下一次 dynamic_loop_back 还有机会 | ||
| 272 | + 重新判断),不像根因分类升级人工那样是终局判断。 | ||
| 273 | + """ | ||
| 274 | +``` | ||
| 275 | + | ||
| 276 | +三个视角建议:①"哪个节点的产出内容本身有实质缺陷"(对着候选节点逐个看 output_payload)、②"如果都没有实质缺陷,是不是节点之间的依赖/衔接有问题"、③固定复用一次原始 `feedback`(终审判断本身给出的失败原因)里能不能直接抽取出候选节点名字。三个视角都在同一批 `candidate_node_ids` 里选一个,用 `_tally_perspectives` 的既有多数票逻辑仲裁。 | ||
| 277 | + | ||
| 278 | +### 5.5 安全边界 | ||
| 279 | + | ||
| 280 | +1. **运行时校验目标必须是真实祖先**(§5.3 已展示)——即使判断调用本身犯错选了一个不存在或非祖先的 node_id,直接报错而不是静默产生一次无意义的回路。 | ||
| 281 | +2. **判断未达成 2/3 一致时的退化策略**:不像 `root_cause_trace` 那样升级人工(那是跨 Org Agent 边界的终局判断,这里只是图内部一次重试的一部分),退化为"回退到 `candidate_node_ids` 里离失败节点最近的直接上游"(`_transitive_ancestors` 结果里 `depends_on` 直接列出的那些,不含更远的间接祖先)——一个保守但总能往前推进的默认值,不会卡死整个循环。 | ||
| 282 | +3. **`_resolve_loop_limit` 的计数维度不变**:`_loop_attempt_count_from_events` 是按 `(target_node_id, source_node_id)` 这一对计数的(`work_graph_orchestrate.py:560-567`),`dynamic_loop_back` 每次可能选出不同的 `target_node_id`,意味着"同一个 source 连续 3 次动态判断到 3 个不同的 target"不会被现有计数逻辑拦住(各自计数都是 1)——**这是一个需要显式接受的简化**:额外加一个"同一个 source_node_id 累计触发 dynamic_loop_back 的总次数"上限(不分 target,配置沿用 `work_graph_loop_limit_config` 同一张表,`config_id` 用 `"{org_node}:{source_node_id}:total"` 这个新的 key 形状),防止"每次判断出不同目标"绕过限流、无限循环。 | ||
| 283 | + | ||
| 284 | +--- | ||
| 285 | + | ||
| 286 | +## 6. Coder/Validator 接入自规划:`implement_code()`/`validate_implementation_with_own_graph()` 改造 | ||
| 287 | + | ||
| 288 | +### 6.1 增量追加接口(用户拍板,2026-08-08:`plan_work_graph()` 新增参数,不是新函数) | ||
| 289 | + | ||
| 290 | +原设计判断是新增一个独立函数 `plan_work_graph_append()`——**已被推翻**:用户拍板要求合并/冲突检测逻辑集中在 `plan_work_graph()` 一个函数里,新增一个可选参数 `append_to_run_id`,不是平行的第二个函数。理由(用户原话):`work_graph_plan.py` 本来就是 `_normalize_graph_manifest`/`_validate_loop_back_targets` 等 `graph_manifest` 结构性操作的归属地,避免 Coder、Validator 两个调用点各自维护一份合并逻辑、未来出现两处不一致的风险。 | ||
| 291 | + | ||
| 292 | +```python | ||
| 293 | +def plan_work_graph(conn, org_node, task_id, goal_text, target_folder_path, timeout=300, append_to_run_id=None): | ||
| 294 | + """append_to_run_id=None(默认):跟今天完全一样,INSERT 一个新 | ||
| 295 | + work_graph_run 行。append_to_run_id=<既有 run_id>:不 INSERT 新行, | ||
| 296 | + 改为读出该 run_id 现有的 graph_manifest,把这次新规划出的节点/边 | ||
| 297 | + 合并进去(node_id 冲突时报错,不静默覆盖或改名),UPDATE 回同一行。 | ||
| 298 | + 追加模式下会把已有节点的 node_id 清单(不含内容)注入 prompt 上下文, | ||
| 299 | + 让新规划的节点能声明 depends_on 指向已有(可能是别的 org_node 产出的) | ||
| 300 | + 节点。返回形状不变:{"run_id", "graph_manifest"}(追加模式下 run_id | ||
| 301 | + 就是传入的 append_to_run_id,graph_manifest 是合并后的完整图)。 | ||
| 302 | + """ | ||
| 303 | + if org_node == "planner": | ||
| 304 | + prompt = _PROMPT_TEMPLATE.replace("{goal_text}", goal_text) | ||
| 305 | + else: | ||
| 306 | + prompt = _generic_prompt_template(org_node).replace("{goal_text}", goal_text) | ||
| 307 | + | ||
| 308 | + if append_to_run_id: | ||
| 309 | + existing = conn.execute( | ||
| 310 | + "SELECT graph_manifest FROM work_graph_run WHERE run_id = ?", (append_to_run_id,) | ||
| 311 | + ).fetchone() | ||
| 312 | + existing_manifest = json.loads(existing[0]) | ||
| 313 | + existing_ids = [n["node_id"] for n in existing_manifest["nodes"]] | ||
| 314 | + prompt = prompt.replace( | ||
| 315 | + "Plan (do not execute)", | ||
| 316 | + "This graph already has these nodes (from earlier stages): {}. Plan (do not execute) " | ||
| 317 | + "ADDITIONAL nodes only -- do not redeclare or duplicate the ones above.".format(existing_ids), | ||
| 318 | + ) | ||
| 319 | + | ||
| 320 | + # ... 真实 subprocess 调用 + _extract_graph_manifest + _normalize_graph_manifest,跟今天一样 ... | ||
| 321 | + new_manifest = _normalize_graph_manifest(_extract_graph_manifest(response_text)) | ||
| 322 | + | ||
| 323 | + if append_to_run_id: | ||
| 324 | + collision = set(n["node_id"] for n in new_manifest["nodes"]) & set(existing_ids) | ||
| 325 | + if collision: | ||
| 326 | + raise PlanningFailedError("append_to_run_id={} node_id collision: {}".format(append_to_run_id, collision)) | ||
| 327 | + merged = { | ||
| 328 | + "nodes": existing_manifest["nodes"] + new_manifest["nodes"], | ||
| 329 | + "edges": existing_manifest.get("edges", []) + new_manifest.get("edges", []), | ||
| 330 | + } | ||
| 331 | + conn.execute("UPDATE work_graph_run SET graph_manifest = ? WHERE run_id = ?", | ||
| 332 | + (json.dumps(merged), append_to_run_id)) | ||
| 333 | + conn.commit() | ||
| 334 | + return {"run_id": append_to_run_id, "graph_manifest": merged} | ||
| 335 | + | ||
| 336 | + run_id = str(uuid.uuid4()) | ||
| 337 | + conn.execute("INSERT INTO work_graph_run (run_id, task_id, graph_manifest, status) VALUES (?, ?, ?, 'planning')", | ||
| 338 | + (run_id, task_id, json.dumps(new_manifest))) | ||
| 339 | + conn.commit() | ||
| 340 | + return {"run_id": run_id, "graph_manifest": new_manifest} | ||
| 341 | +``` | ||
| 342 | + | ||
| 343 | +(`INSERT INTO work_graph_run` 不再写 `org_node` 列,见 §2.1 的删除决策。) | ||
| 344 | + | ||
| 345 | +调用方(`e2e_pipeline.implement_code()`)改造: | ||
| 346 | + | ||
| 347 | +```python | ||
| 348 | +def implement_code(conn, task_id, run_id, sdd_content, goal_text, target_folder_path, ...): | ||
| 349 | + # run_id 现在由调用方(run_task_lifecycle)传入 -- Phase 1 建立的那个 | ||
| 350 | + # 跨阶段 work_graph_run,不再自己创建新 run | ||
| 351 | + graph_manifest = work_graph_plan.plan_work_graph( | ||
| 352 | + conn, "coder", task_id, goal_text, target_folder_path, append_to_run_id=run_id, | ||
| 353 | + )["graph_manifest"] | ||
| 354 | + outputs = work_graph_orchestrate.run_graph( | ||
| 355 | + conn, run_id, "coder", "coder-implementation", target_folder_path, graph_manifest, | ||
| 356 | + work_graph_plan.generic_node_prompt_builder, _CODE_INPUT_SCHEMA, _CODE_OUTPUT_SCHEMA, | ||
| 357 | + needs_worktree=True, timeout=timeout, | ||
| 358 | + ) | ||
| 359 | + ... | ||
| 360 | +``` | ||
| 361 | + | ||
| 362 | +`run_graph()` 的 `graph_manifest` 参数现在是**合并后的完整图**(既有节点 + 新增节点),但 `run_work_node()` 的既有断点续跑短路(已 `done` 的节点直接返回持久化输出)保证已有节点不会被重新执行——这跟 §5.3 提到的"再次调用 `run_graph()` 时自然跳过已完成节点"是同一个机制,不需要额外处理。 | ||
| 363 | + | ||
| 364 | +不再用 `{"nodes": [{"node_id": "implement", "depends_on": []}], "edges": []}` 这个合成单节点 manifest(`e2e_pipeline.py:637` 现状)——真正让 Coder 自己规划实现步骤。`validate_implementation_with_own_graph()` 同理,`_VALIDATOR_MANIFEST` 硬编码字面量被 `plan_work_graph(conn, "validator", ..., append_to_run_id=run_id)` 取代。 | ||
| 365 | + | ||
| 366 | +### 6.2 `run_id` 的传递链路 | ||
| 367 | + | ||
| 368 | +Phase 1 SDD 设计的 `plan_and_review_documents()`(新增函数,Planner 阶段用)已经创建了这次任务唯一的 `work_graph_run` 行;Phase 2 要求这个 `run_id` 一路透传给 `implement_code()`/`validate_implementation_with_own_graph()`,不能让它们各自继续 `INSERT INTO work_graph_run`。`run_task_lifecycle()`/`run_or_resume_task_lifecycle()`(Phase 1 已经改造过的两个函数)需要把 `plan_and_review_documents()` 返回值里的 `run_id` 一路带下去——这是 Phase 2 对 Phase 1 产出的一个新依赖点,Phase 1 落地时如果 `plan_and_review_documents()` 的返回形状没有 `run_id`,需要回头补一个字段(轻量修改,不影响 Phase 1 已完成的核心逻辑)。 | ||
| 369 | + | ||
| 370 | +--- | ||
| 371 | + | ||
| 372 | +## 7. 新增节点级验证 skill + 退休 `org_level_loop.py` 粗粒度路由 | ||
| 373 | + | ||
| 374 | +### 7.1 新 skill:`work-graph-node-review` | ||
| 375 | + | ||
| 376 | +新增 `skills/work-graph-node-review/SKILL.md`,`roles/planner_reviewer`(Phase 1 新增)、`roles/coder`、`roles/validator` 的 `role.json` 都引用它——org-agent-无关,是 `judgment.kind="llm"` 时 `judge_prompt` 的方法论参考(不是新的 Python 机制,`judgment.kind="llm"` 本身已经是引擎特性,这个 skill 提供的是"怎么写一个好的 judge_prompt/怎么识别叙述文字冒充结构化产出"的方法论沉淀,直接把 `validator-test-script-narrative-output-issue.md` §6 的根因分析和 `_NARRATION_RETRY_HINT` 的措辞模式吸收进去,成为所有自规划节点复用的判断标准,不是每个角色各写一份)。这份 skill 的方法论提炼出一个可复用的 `_NODE_REVIEW_JUDGE_PROMPT` 常量(放在 `work_graph_plan.py`,跟 §3.1 的建议目录同一个模块,因为目录本身就要引用它作为 `suggested_judgment.judge_prompt` 的默认值),不是每个目录条目各写一段相似的 judge_prompt。 | ||
| 377 | + | ||
| 378 | +不改造 `qa-task-validation`——那个 skill 判断的是"整体实现是否验收通过"(任务级),职责不同,两者并存,不合并。 | ||
| 379 | + | ||
| 380 | +### 7.2 `org_level_loop.py`/`org_loop_route.py` 的粗粒度路由退休 | ||
| 381 | + | ||
| 382 | +Phase 1 阶段"复核失败先用现有 `org_level_loop.py` 粗粒度路由兜底"(Phase 1 SDD 的过渡方案)在 Phase 2 落地后被替换:终审 Validator 节点声明 `on_fail.action="dynamic_loop_back"`(§5),失败时直接在**同一个** `work_graph_run` 内精确回退,不再需要 `route_org_loop()` 那种"改 `task.status`、换 `assigned_org_node`、期待调用方重新触发另一个函数"的跨阶段路由。 | ||
| 383 | + | ||
| 384 | +`org_level_loop.py::run_fix_loop`/`org_loop_route.py::route_org_loop`/`_NEW_STATUS`/`_NEW_ORG_NODE`/`task.loop_round` 字段——**不删除**(`root_cause_trace.judge_root_cause`/`trace_root_cause_human_reported` 等其他调用方,以及 bug_record 驱动的人工报告场景仍然用得到这套粗粒度分类),但新的合图任务流程不再经过它们;`scheduler.py::finish_task_lifecycle` 需要判断"这是走新合图流程的任务,还是走旧三段式流程的任务"(新流程的失败已经在 `run_graph()` 内部处理完,不会再走到 `finish_task_lifecycle` 的 FAIL/INCONCLUSIVE 分支去调用 `run_fix_loop`)——具体判断依据留给实施阶段(比如看 `work_graph_run.graph_manifest` 里节点是不是都带 `judgment`/`on_fail` 字段,或者更简单,直接看 task 是不是 Phase 1/2 上线之后创建的)。 | ||
| 385 | + | ||
| 386 | +--- | ||
| 387 | + | ||
| 388 | +## 8. 明确的不确定性(比 Phase 1 更大,如实标注) | ||
| 389 | + | ||
| 390 | +1. **`_judge_root_cause_node`(§5.4)的判断质量未经真实验证**——三视角判断"哪个上游节点该负责"是全新设计,没有 A/B 数据支撑,上线后大概率需要根据真实误判案例调整 prompt/视角设计,不是一次写对。 | ||
| 391 | +2. **`promotes_to_artifact`(§2.3)依赖 Coder/Validator 老实遵守"只有最终交付物才声明"这条约束**——是 prompt 层面的软约束,不是机械强制,理论上 LLM 可能给每个节点都声明 `promotes_to_artifact`(虽然 artifact_type 枚举本身会挡住不合法值,但不会挡住"过度声明"这种滥用),需要在真实运行中观察是否需要加机械检查。 | ||
| 392 | +3. **`dynamic_loop_back` 的"同一 source 总次数上限"(§5.5 第 3 点)需要新的 `work_graph_loop_limit_config` key 形状**——`"{org_node}:{source_node_id}:total"` 这个具体格式没有先例,实施时需要重新过一遍 `settings_config.py` 的查询/写入接口是否兼容这种三段式 key,还是需要专门扩展。 | ||
| 393 | +4. **`org_level_loop.py` 新旧流程共存期的判断依据**(§7.2 结尾)还没有定论,是留给实施阶段解决的开放问题,不是本文档的疏漏——但这个问题不解决,Phase 2 上线当天就会有"旧任务卡在新代码路径里"的真实风险,实施顺序上应该早排。 | ||
| 394 | + | ||
| 395 | +--- | ||
| 396 | + | ||
| 397 | +## 9. 跟正在实施的 Phase 1 的接口核对清单(Phase 1 落地后立即执行) | ||
| 398 | + | ||
| 399 | +- [ ] `plan_and_review_documents()` 的返回形状是否包含 `run_id`(§6.2 依赖)。 | ||
| 400 | +- [ ] Phase 1 实际引入的新 `task.status` 值是不是本文档假设的名字(Phase 1 SDD 定的是 `reviewing_planner_output`,Phase 1 由另一进程实施,实际落地值需要核对一次)。 | ||
| 401 | +- [ ] `roles/planner_reviewer/role.json` 的 `skills` 列表是否已经预留位置给 §7.1 新增的 `work-graph-node-review`(如果 Phase 1 实施时已经写了别的占位 skill 名字,需要统一)。 | ||
| 402 | + | ||
| 403 | +--- | ||
| 404 | + | ||
| 405 | +## 10. 实施顺序(拆成三个可独立验收的子阶段) | ||
| 406 | + | ||
| 407 | +### Phase 2a — 引擎扩展(地基,不改任何 org 流程) | ||
| 408 | +- §2.2(`ux_work_graph_run_active` 迁移)+ §4(节点级 subagent/skill/tools 绑定)+ §5(`dynamic_loop_back` 新分支)+ 对应单测。 | ||
| 409 | +- 验收:一个手写的合成 graph_manifest(不经过真实 LLM 规划),节点声明不同的 `subagent_type`,运行后 `work_node_execution.subagent_type` 确实按节点区分;另一个手写 manifest 触发 `dynamic_loop_back`,验证真的能跳到候选节点之一且计数正确。 | ||
| 410 | + | ||
| 411 | +### Phase 2b — Coder 接入自规划 | ||
| 412 | +- §3(`_GENERIC_PROMPT_TEMPLATE` 扩展)+ §2.3(`promotes_to_artifact`)+ §6(`implement_code()` 改造)。 | ||
| 413 | +- 验收:一个真实 goal,Coder 自规划出多节点图(不是合成单节点),`code_diff`/`test_report` 正确落 artifact,中间节点不落。 | ||
| 414 | +- 风险:`validator-test-script-narrative-output-issue.md` 记录的叙述文字复发风险在这一步首次真实暴露给 Coder,按需求文档非目标 §5.2 的要求专门验证。 | ||
| 415 | + | ||
| 416 | +### Phase 2c — Validator 接入自规划 + 精确 loop_back 收口 | ||
| 417 | +- Validator 侧的 §6 改造 + §7(新 skill + 退休粗粒度路由)+ §9 的核对清单。 | ||
| 418 | +- 验收:真实构造一次"终审失败,根因在 SDD 节点"的场景,验证 `dynamic_loop_back` 精确跳回 SDD 节点重新执行,SDD 的下游(Benchmark/Coder 实现/…)全部正确重跑,不需要人工介入,也不需要经过 `org_level_loop.py`。 |