# system/work-graph-templates/ Work Graph(**org node 级** workflow)的**骨架模板库**。与 `system/org-graph-templates/`(公司级)同构, 只是低一层:那边一家公司一张图、活整个公司生命周期;这边**一个 org node 接到一次任务规划一张图,用完即归档**。 每个 `*.json` 是一种 org node 职能的执行流图起点。org graph 上的节点被执行到时,引擎调 `workgraph-planning`(全局通用本体,27-REQ R-15.2),它会 `ls` 本目录、逐个读取、对照各自的 `whenToUse` 选型,再结合宿主 org node 的角色定义、本次任务描述、可读的 `artifacts/`、上一轮 `remediation_note`, 裁剪 / 替换 / 组合出这次真正要跑的 work graph。 > 设计依据:[27-P3/P4](../../docs/todo/system-design/27-p3-p4-work-graph-console.md) §四.D.0 / §四.D.2.2 / §四.D.3 / §九bis > 命名规范:[27-NAMING](../../docs/todo/system-design/27-naming-conventions.md) §二 / §三 **这是起点集合,不是穷举枚举。** 加一种起手式只需往本目录扔一个 JSON 文件,**不改任何代码**。 模板里的 `systemAgentId` 对 `workgraph-planning` 是*建议*——它可以替换成更合适的本体, 但**替换后的那个也必须真实存在**:`work_graph_nodes.system_agent_id` 有外键,引用不存在的本体 落库时直接 `FOREIGN KEY constraint failed`。模板里**写着的**那些则不是建议而是承诺, 由下面那道 CI 防线保证它们始终指得到真东西。 ## 当前模板 | 文件 | id | 宿主 org node | 节点链 | |---|---|---|---| | `planner.json` | `planner` | 规划类(`org-planner`) | `brd → prd → sdd → benchmark → validator` | | `executor.json` | `executor` | 执行类(`org-coder` / `org-writer` / `org-monitor`),单交付物 | `impl → test → validator` | | `executor-fanout.json` | `executor-fanout` | 执行类,可并行拆分 | `split → (impl/test)×N → merge → integration → validator` | | `validator.json` | `validator` | **`org-validator` 专用** | `validator-triager → benchmark-validator → testdata-validator` | 四张图共享一个形状:**做 → 自检 → 本层验收,验收失败沿 `backward` 边退回重做**。差异只在中段。 ### 🛡️ 本目录的 `systemAgentId` 有一道 CI 防线 `server/tests/unit/template-refs.test.js` 对本目录每个 JSON 做三件事: 1. 每个非占位符 `systemAgentId` 在 `system/templates/` 下**必须有目录**; 2. 同一个 id 在 `system/templates/_registry.json` 里**必须有条目**; 3. 真的建一次库、把**不含占位符**的骨架全部 `createWorkGraph` 落一遍。 为什么第 1 条查的是目录:`system_agents` 的数据源头是**目录扫描** (`graph-seed-service.seedTemplates()` 读 `system/templates/*`),`_registry.json` 只用来补 `benchmarkScore`。而 `work_graph_nodes.system_agent_id` 有外键指向 `system_agents(id)`—— **目录不在 = 落库时 `FOREIGN KEY constraint failed`**,`createWorkGraphSafely` 会把它翻成 `rule=reference` 打回 agent,连打三轮后抛错,节点按无 `fail_reason` 的 fail 走兜底边。 这正是 R6-F1 实际踩到的形态(当时 `validator.json` + `planner.json` 一共 6 个空引用)。 **改本目录的任何 JSON 后跑一次**: ```bash npx vitest run server/tests/unit/template-refs.test.js ``` ### `{{占位符}}` 必须替换 `executor*.json` 里的 `{{orgNode}}` / `{{implAgent}}` / `{{testAgent}}` / `{{validatorAgent}}` / `{{splitAgent}}` / `{{mergeAgent}}` / `{{integrationAgent}}` 是**占位符,不是合法值**, 提交前必须全部替换成真实的节点名与本体名,否则落库出来的是一张引用不存在 agent 的废图。 - `{{orgNode}}` ← 宿主 org node 的**全名**(如 `org-coder`)。 - `{{xxxAgent}}` ← 一个**真实存在**的 system agent 本体 id。核实方式见下一节。 `planner.json` / `validator.json` 没有占位符——因为 `org-planner` 与 `org-validator` 在三个 org graph 模板里**首尾同名**(27-NAMING §3.1),它们的 work graph 节点名可以写死。 ### 节点名 ≠ 本体名(27-NAMING §3.2,最容易搞错的一条) | | 规则 | 例 | |---|---|---| | `nodeName` | **图内唯一**,带宿主 org 节点前缀 | `org-coder-validator` / `org-planner-benchmark` | | `systemAgentId` | 跨图复用的**本体**,**不带**领域前缀 | `coder-validator` / `goal-benchmark` | 本体若被迫带前缀,`coder-validator` 会复制成多份,与 P1「同一份本体、多处引用、各自私有语境」直接冲突。 > ⚠️ 唯一的例外是 `validator.json` 里的三个节点:`validator-triager` / `benchmark-validator` / > `testdata-validator` 按 §3.1 字面规则本应写成 `org-validator-triager` 等,但 27-P3/P4 全文用的是 > 父节点职能的简写。**前缀取全名还是取职能这一处仍未定案**(27-NAMING §3.5 第 1 行), > **定案前一律按简写书写**,不要以任一方去改另一方。 > 至于职能词,`reflection` → `triager` 已由 27-REQ **R-17.2 定案**,不再是待定项。 ### `validator.json` 为什么归因排第一位 `org-validator` 判定任务未达成时该退回 `org-planner`(规划有问题)还是 `org-coder`(实现有问题)? 这个判断**不由失败节点顺手吐出**,而是由 `org-validator` 自己 work graph 里排第一位的专职归因节点 `validator-triager` 做出(27-REQ R-16)。它的产出 `fail_reason` + `remediation_note` 随子 run 收敛 透传回 org 层(§四.D.0 三值透传),成为 org graph 选边的唯一依据。 三个 fail 出边的走向由此确定: - `benchmark-validator` / `testdata-validator` 验收不过 → **兜底 backward 边退回 `validator-triager`** 归因; - `validator-triager` **故意不留 fail 出边**——它归因完发现「这层修不了」时以 fail 收尾, 引擎判为死路 → 节点 `blocked` → 子 run `blocked` → 父节点拿到 `fail` + `fail_reason` + `remediation_note`, 回到 org 层按 §四.D.2.2 选边。这是设计好的收敛出口,不是漏画的边。 反复归因不会失控:`backward` 边有单边 `loop_count ≤ 3` 与 run 级 `backward_count ≤ 5` 的双层熔断 (§四.D.2.1),撞顶直接 `force_terminated`,纯整数比较,不依赖任何 LLM 判断。 ### 后两个节点各自只认一格(两个都是 `goal-contract.js` 的生产消费方) | 节点 | 它做的 | `server/services/goal-contract.js` 做的 | |---|---|---| | `benchmark-validator` | 逐维度找证据给分 → 写 `attempt-{n}.scores.json` | 加权求和 + 阈值比较 + 判决落盘 | | `testdata-validator` | 逐 case 真的喂一遍 → 如实记实际输出到 `attempt-{n}.results.json` | `deep-equal` / `subset` 逐条比对 + 判决落盘 | 切法是「**LLM 只给分 / 只记实际输出,算术与阈值交给不起 LLM 的执行器**」—— 同一个 agent 既给分又判过没过,很容易把想要的结论倒推成分数。 两个文件名后缀是刻意选的:引擎认判词只认严格的 `attempt-{n}.json`(`graph-engine.js` 的 `ATTEMPT_FILE_RE`),不会误认这两份;而 `goal-contract.js` 的 `deriveTargetIds` 又能从同样的 文件名推出 `nodeRunId` 与 `attempt`,判决四元组不缺环。 ⚠️ 两处**已知边界,不是漏画**: 1. **`test_data` 段常常根本不存在**。三件套(`goal-benchmark` / `goal-test-data` / `goal-test-script`) 按 27-E2E §十一 是**动态选入**的,本目录只建了 `goal-benchmark` 一个产出本体; 另两段由图上任何带 `writesArtifacts: true` 的节点用 `goal-contract.js put --section …` 写, **不另设专用本体**。契约里没有 `test_data` 段时 `testdata-validator` 走 `verdict: "skipped"` 放行—— 这是常态,不是它没干活。 2. **两个节点都只读自己那一格**(`sections.benchmark` / `sections.test_data`), 不看 `run` 返回的顶层 `ok`。`run` 会把契约里所有非空 section 一起跑,顶层是三段的合取; 拿它当自己的结论 = 同一个失败被两个节点各算一遍 = 回退次数翻倍、熔断提前撞顶。 ## 文件格式 ```jsonc { "id": "planner", // 必填,与文件名同名(不含 .json) "label": "规划型 org node 的 work graph", // 必填,给人看的中文名 "whenToUse": "……", // 必填,LLM 选型的唯一依据:适用场景 + 典型信号词 + 与其他模板的区别 "ownerOrgNode": "org-planner", // 必填,这张图挂在哪类 org node 下;占位符写 {{orgNode}} "nodes": [ { "nodeName": "org-planner-brd", "systemAgentId": "brd-planning" }, { "nodeName": "org-planner-benchmark", "systemAgentId": "goal-benchmark", "writesArtifacts": true }, // 可选,声明它有权写 root run 的共享 artifacts/ { "nodeName": "benchmark-validator", "systemAgentId": "benchmark-validator", "readsArtifacts": ["goal-benchmark.json"] } // 可选,声明它读哪些共享产物 ], "dataContractNotes": "……", // 可选,给规划 agent 看的取舍说明:这张图为什么这么声明数据契约 "edges": [ { "from": "org-planner-brd", "to": "org-planner-prd", "condition": "success", "produces": ["brd"] }, // 数据契约:上游产出什么,交给下游 { "from": "org-planner-sdd", "to": "org-planner-benchmark", "condition": "success", "produces": ["sdd"], "needs": ["prd"] }, // needs = 下游要吃、但产出方不是紧邻上游 { "from": "org-planner-validator", "to": "org-planner-prd", "condition": "fail", "failReason": "input_underspecified", "edgeKind": "backward" }, { "from": "org-planner-validator", "to": "org-planner-sdd", "condition": "fail", "edgeKind": "backward" } // 无 failReason = 兜底边;backward 边一律不声明契约 ] } ``` - `condition`:`"success"` | `"fail"`,边的语义是**执行流**(上游出这个结果时流向下游)。 - `edgeKind`:`"forward"`(默认,可省略)| `"backward"`。**指向拓扑序更早节点的边必须显式标 `backward`**。 - `failReason`:仅 `condition: "fail"` 的边可带,是失败的细分路由键(对应 `work_graph_edges.fail_reason` 列)。 省略即为该节点的**兜底边**。取值域**不由引擎约束**,推荐取值见 27-P3/P4 §四.D.2.2 (`impl_bug` / `input_underspecified` / `skill_defect` / `infra_flake`)。 - `nodeKind`:`"agent_call"`(默认,可省略)| `"subgraph"`。work graph 节点默认是一次 agent 调用; 只有确实要再下钻一层图的节点才写 `subgraph`。 - `writesArtifacts` / `readsArtifacts`:可选声明,对应 §四.A.5 的共享产物区读写权。 写 `artifacts/` 是特权动作,只有被显式标记的节点才有,防止任意节点往共享区乱写。 - `produces` / `needs`:**边上的数据契约**,见下一节。四张骨架的每条 forward 边都必须有其中之一。 ## `produces` / `needs`:边只带控制流的话,图就不传数据 `condition` / `edgeKind` / `failReason` 表达的全是**执行流**。下游节点的提示词里能不能出现 「上游产物在这个绝对路径」,只取决于边上的数据契约声明——第四次真 CLI 冒烟里 `org-planner-prd` 的归因原话是「不是上游没产出,是它的产出结构上到不了我手上」, 根因就是这条边上什么都没写。 这个声明有**两个**读回方,都在 `server/services/graph-engine.js`: | 环 | 谁读 | 读到之后干什么 | |---|---|---| | 输入侧 | `describeInputs` → `inboundContracts('work', def.edges, defNodeId)` | 按声明的 kind 去产物索引查实例,把**绝对路径**拼进下游节点的提示词;声明了却查不到就显式标 ⚠️ | | 产出侧 | `indexNodeOutputs` → 读本节点**出边**上的契约 | 用声明的 kind 给这个节点刚落盘的文件定 kind(文件名认不出来时靠声明一对一认领),否则一律记成 `artifact`,下游按 kind 就查不到 | 写法与规则(权威说明在 `system/templates/workgraph-planning/tools.md` §6bis,这里只记要点): - kind 取值域只有 7 个:`brd` / `prd` / `sdd` / `acceptance` / `contract` / `artifact` / `log`, 自创取值会被 `rule=schema` 打回。 - `produces` 写在**产出方的出边**上(绝大多数情况只用它);`needs` 写在**消费方的入边**上, 仅当产出方**不是紧邻上游**(跨节点中继)时才用。 - **`backward` 边一律不声明**(`rule=schema`):引擎解析输入只看 forward 入边, 写在回退边上等于没写,而「看起来写了」比「没写」更难排查。 - **规则 6 `data-contract`**:声明要吃的 kind(`needs`,`required` 不为 false) 在图上必须有产出方,否则整张图被拒。产出方确实不在本图(上一层 org graph 或 `artifacts/` 给的)时, 写成 `"needs":[{"kind":"artifact","required":false}]` —— 这是唯一的逃生口, `validator.json` 用的就是它。 - **任务级共享的 `acceptance` / `contract` 不要沿边声明**:引擎按 task + kind 自动递给每个节点 (`graph-engine` 的 `TASK_SHARED_KINDS`),不受拓扑相邻性限制。只有当**本图里某个节点自己产出**它、 再交给本图的下游时才写 `produces`(`planner.json` 的 `acceptance` 就是这种情形)。 🛡️ **防线**:`server/tests/unit/work-graph-skeletons.test.js` 逐张骨架查四件事—— 过 `validateWorkGraph`(含规则 6)、**每条** forward 边都有非空声明、 `createWorkGraph` 落库后 `graph_edge_contracts` 的 kind/role/required 与 JSON 逐条一致、 每个非入口节点的 `inboundContracts` 非空。改本目录任何 JSON 后跑: ```bash npx vitest run server/tests/unit/work-graph-skeletons.test.js ``` ## 加一个新模板 1. 新建 `system/work-graph-templates/{id}.json`,按上面的格式填。 2. **节点名遵循 `{宿主 org 节点全名}-{职能}`,本体名不带前缀**(27-NAMING §3.1 / §3.2,见上文对照表)。 3. **避开这 6 个命名冲突 id**:`agent-creator` / `digester` / `evolver` / `evolver-youtube-crawler` / `hunter` / `innovator`。它们在 `system/agents/` 与 `system/templates/` 下同名但内容可能不同步。 4. `workgraph-planning` 自己**不要**出现在图里——它规划图,但不是图上的节点。 5. **不要把 `RESEARCH → PLAN → EXECUTE → VERIFY` 四段式画进图里,也不要指望节点内部去跑它** (27-P3/P4 §九bis)。work graph 本身就是那张图的泛化版,两层并存会让两层 Context Flush 互相清对方的上下文。详见下一节。 6. 人工验算五项校验(写入 API 也会校验,但模板先自查能省一轮 LLM 重试): | # | 规则 | 怎么自查 | |---|---|---| | 1 | **无环**(只看 `forward` 边) | 把所有 `backward` 边划掉,剩下的边必须能排出拓扑序 | | 2 | **恰好一个入口** | 只数 `forward` 边的入度,入度为 0 的节点必须**恰好一个** | | 3 | **全可达** | 从入口沿 forward 边 BFS,必须覆盖 `nodes` 里每一个节点 | | 4 | **回退边显式标注** | 任何 `to` 在拓扑序上早于 `from` 的边,必须带 `"edgeKind": "backward"` | | 5 | **fail 兜底边** | 有 `fail` 出边的节点,其中必须至少有一条不带 `failReason` | | 6 | **声明要吃的 kind 有产出方** | 每条 `needs`(未标 `required:false`)的 kind,图上必须有某条出边 `produces` 它 | 7. 自验:直接拿引擎真正用的那套校验跑一遍本目录全部模板(占位符不影响校验,它只做名字匹配): ```bash node -e " const fs=require('fs'),p=require('path'),{validateWorkGraph}=require('./server/services/work-graph-db.js'); const d='system/work-graph-templates';let bad=0; for(const f of fs.readdirSync(d).filter(f=>f.endsWith('.json'))){ const t=JSON.parse(fs.readFileSync(p.join(d,f),'utf-8')); const r=validateWorkGraph(t.nodes,t.edges); console.log(f,r.ok?'OK':'FAIL',r.ok?'':JSON.stringify(r.errors)); if(!r.ok)bad++; } process.exit(bad?1:0);" ``` ## 🚫 不要在节点内部再套四段式(27-P3/P4 §九bis) `system/templates/base/` 的基座员工被要求跑 `RESEARCH → PLAN → EXECUTE → VERIFY`, 每次流转靠 `node bin/lemon.js task transition` **清空上下文**(Context Flush)。 work graph 的节点本身就是这套东西的泛化版——**同一次执行里两层都跑,两层 Context Flush 会互相清对方的上下文**。 定案:**图是唯一的流转层。** 阶段顺序由边定义提供,阶段间产物由 `output_ref` 传递, 失败回退由 `backward` 边 + 熔断提供,不需要节点内部再来一遍。 | ❌ 不要 | ✅ 要 | |---|---| | 一个节点的 prompt 里写「先 research 再 plan 再 execute 再 verify」 | 一个节点只做一件能一句话说清「做完什么算成功」的事 | | 让节点自己调 `task transition` 换阶段 | 让引擎按边推进到下一个节点 | | 把四段式画成图里的四个节点,再让每个节点内部也跑四段式 | 要拆调研/规划就直接拆成图上的相邻节点,只拆一层 | 把四段式改写成引擎上的一张标准图模板是 P3 **之后**的待办,本目录不预先实现它。