# WorkGraph Planning 工具箱 你的全部工作靠五类工具完成:**读骨架 → 读本体清单 → 落盘规划记录 → 招聘 → 交回图**。 按 `agent.md` 的八步工作流顺序使用。你**不执行图、不辞人、不改 org graph**。 > 🚨 **交回图(§6)没有端点可调。** 落库是引擎的活:它读你回复的正文取图、自己校验、自己写库。 > 你只负责把图放到它认得的位置(§6)。 > 🚨 **招聘(§5)必须排在交回图(§6)前面。** 反了会外键挂 + 节点跑起来找不到人,详见 `agent.md` Step 6。 > 🧭 **路径口径**:CLI 进程的 cwd 是**你自己的 agent 目录** > (`data/companies/<公司ID>/agents/workgraph-planning`),不是引擎仓库根。 > 本页所有仓库相对路径(`system/work-graph-templates/`、`bin/lemon.js`、`data/companies/...`) > 都要自己拼上引擎仓库根的绝对路径再用——那个绝对路径在首轮消息的「写入边界」段里。读允许,写禁止。 > 🚨 **发 JSON 的铁律(先读这条,否则你会陷入死循环)** > 本环境的工具调用在解析带多重双引号的 bash 命令时会崩溃,导致你反复"修格式"直到进程被杀。 > **任何带 JSON body 的请求(现在只剩招聘那一条),一律先把紧凑单行 JSON 写进 `/tmp/*.json` 文件, > 再用 `curl -d @文件` 发送。** JSON 里不许出现真实换行符,不许内联在 `command` 参数里。 --- ## 1. 读候选骨架(Step 2) ```bash ls system/work-graph-templates/ for f in system/work-graph-templates/*.json; do echo "=== $f ==="; cat "$f"; done ``` 永远以 `ls` 的实际结果为准逐个读,目录里可能有比下表更多的文件。选型只看每份的 **`whenToUse`**。 | 文件 | 宿主 org node | 节点链 | |---|---|---| | `planner.json` | 规划类(`org-planner`) | `brd → prd → sdd → benchmark → validator` | | `executor.json` | 执行类,单交付物 | `impl → test → validator` | | `executor-fanout.json` | 执行类,可并行拆分 | `split → (impl/test)×N → merge → integration → validator` | | `validator.json` | **`org-validator` 专用(强制)** | `validator-triager → benchmark-validator → testdata-validator` | `system/work-graph-templates/README.md` 有完整的字段说明与占位符清单,规划前值得读一遍。 ## 2. 读可引用的 system agent 本体(Step 3) ```bash node bin/lemon.js company templates curl -s http://localhost:3010/api/company/<公司ID>/agents ``` 前者是**「能雇到哪些」**(全局 agent 模板,`• — ` 每行一个), 后者是**「这家公司已经雇了哪些」**。两份都要读,但别混: **不在后者里 ≠ 不能用**,只意味着 §5 要真去雇它一次。 骨架 JSON 里写的 `systemAgentId` 只是建议,`{{implAgent}}` 这类占位符更是**必须替换**的空位。 找不到理想本体的三条出路见 `agent.md` Step 3。**你不亲手写 agent 定义**,那是 `agent-creator` 的活; 但**把本体雇进公司是你的活**(§5)。 ## 3. 读上游留下的共享产物(Step 1) ```bash ls -R data/companies/<公司ID>/runs//artifacts/ ``` `artifacts/` 是 root run 级的**任务级共享产物区**,跨 org node 只读可见—— `goal-benchmark.json` / `goal-test-data/` / `goal-test-script/` 这些验收契约就在这里。 | | 读 | 写 | |---|---|---| | `artifacts/` | ✅ 任意层级只读 | ⚠️ 只有你在图里显式标了 `writesArtifacts` 的节点才有权写 | | 别的节点的 `nodes/{其他}/` | ❌ 禁止 | ❌ | 「下游只能看到上游的契约,看不到实现」——**别让你画的节点去重造 `artifacts/` 里已经有的产物**。 ## 4. 落盘规划记录(Step 5,别忘) 用文件写入工具写 `<项目根>/workgraph-plan-.md`(**绝对路径**,项目根由首轮消息的 「写入边界」段给出;没分到项目根就一个文件都不落,把记录写进回复正文)。 七节结构见 `agent.md` Step 5(第 7 节「招聘结果」在 §5 招完人之后回填)。 **图 + 理由 + 五项校验自查**,三样缺一不可——这是人类事后复核你判断力的唯一线索。 ⚠️ 别写相对路径 `workspace/...`:它会落进引擎源码仓库,正是边界段禁止写的位置。 ## 5. 招聘(Step 6)—— **必须在交回图之前跑完** 对图里 `nodes[].systemAgentId` 的**全集去重**,逐个执行: ```bash node bin/lemon.js company hire --id <公司ID> --role <本体名> --type executor --name "<中文岗位名>" ``` | 参数 | 说明 | |---|---| | `--id` | 公司 ID,首轮消息会给 | | `--role` | **本体名**(`systemAgentId`),不是节点名。必填 | | `--type` | 一律 `executor`(work 节点是执行流上的节点,不是 cxo)。省略时 CLI 默认 `cxo`,**别省** | | `--name` | 中文岗位名,省略时默认等于 `--role` | | `--desc` | 可选,一句话职责。新造的本体(模板库里没有)建议写上——它会被转给 `agent-creator` 当写定义的依据 | 这条命令等价于 `POST /api/company/<公司ID>/hire`,body `{"role":...,"type":...,"name":...}`。 CLI 起不来时可以走 curl 降级(**照铁律先写文件**,`/tmp/hire-<本体名>.json` 单行紧凑): ```bash curl -s -X POST http://localhost:3010/api/company/<公司ID>/hire \ -H "Content-Type: application/json" -d @/tmp/hire-<本体名>.json ``` 返回体形如 `{"status":"hired_from_template","agentId":"coder","agentDir":"..."}`。 **`status` 的四档判读、出错的三级处理(重试 / 换掉 / 停)、以及幂等规则,全在 `agent.md` Step 6**, 不要凭感觉处理——尤其 `already_exists` 是**正常结果**,不是错误。 > ⚠️ **CLI 把 HTTP 4xx/5xx 包成一段文本,不是 JSON。** `bin/lemon.js` 在状态码 ≥400 时打印的是 > `❌ [CRITICAL ERROR]: API returned 400.\nDetails: {"error":"role 为必填项"}` 这种字符串 > (还附一段催你重试的话)。**照 `agent.md` Step 6 的分级表处理,别被那段催促语带着无脑重发同一条命令**—— > 真正的原因在 `Details:` 后面那个 JSON 里,先读它。 **`--layer` 这个参数不存在,别自己发明。** work 层招聘该不该写进 org graph 由服务端判定 (27-P3B §四 W7),不是你的入参;你只管把人雇进来。 招完核对(引擎判定本体在不在,用的就是这个目录): ```bash ls data/companies/<公司ID>/agents/ ``` ## 6. 交回图(Step 7)—— **没有端点,引擎读你的回复** **没有 POST,没有 curl,没有 sqlite。** 把图**贴在这一轮回复正文的一个 json 围栏块里**(三个反引号 + `json`) (或写成一个 `.json` 文件、在回复里给出**绝对路径**),引擎自己取出来校验 + 落库。 ```json {"nodes":[{"nodeName":"org-coder-impl","systemAgentId":"coder"},{"nodeName":"org-coder-test","systemAgentId":"unit-tester"},{"nodeName":"org-coder-validator","systemAgentId":"coder-validator"}],"edges":[{"from":"org-coder-impl","to":"org-coder-test","condition":"success","produces":["artifact"]},{"from":"org-coder-test","to":"org-coder-validator","condition":"success","produces":["log"],"needs":["artifact"]},{"from":"org-coder-test","to":"org-coder-impl","condition":"fail","edgeKind":"backward"},{"from":"org-coder-validator","to":"org-coder-impl","condition":"fail","edgeKind":"backward"}]} ``` > `produces` / `needs` 就是 §6bis 那两个键。**forward 边上少了它们,下游节点的提示词里 > 就没有上游产物的路径** —— 先读 §6bis 再写这份 JSON。 引擎取图的死规矩(完整版在 `agent.md` Step 7):**严格 JSON**(注释 / 尾逗号 / 省略号一律解析失败, 整块当没写)· 顶层要有 **`nodes` 数组** · 文件路径必须**绝对** · 回复里**不许出现 `wg_` + 12 位十六进制**的字符串(会被当成"你已落库的图 id",查不到就白烧一轮)。 字段约定: | 字段 | 说明 | |---|---| | `nodes` / `edges` | **引擎只读这两个键**。`ownerOrgNodeId` / `goalId` / `taskId` / `okrNodeId` 由引擎从 run 上下文自己填,写在 JSON 里不生效(要留痕写进规划记录第 4 节) | | `nodes[].nodeName` | 必填,**图内唯一**,带宿主 org node 全名前缀 | | `nodes[].systemAgentId` | 引用的**本体**,**不带**前缀;省略时默认等于 `nodeName`(几乎总该显式写) | | `nodes[].nodeKind` | `"agent_call"`(默认可省略)\| `"subgraph"`(要再下钻一层图时才用) | | `edges[].from` / `to` | **用节点名**,不是 id(图还没落库,你手里没有 id) | | `edges[].condition` | `"success"` \| `"fail"`,必填 | | `edges[].failReason` | 仅 `fail` 边可带;**省略即为兜底边** | | `edges[].edgeKind` | `"forward"`(默认可省略)\| `"backward"` | | `edges[].produces` | **数据契约**:这条边的**上游**会产出哪些 kind,交给下游。见 §6bis,**forward 边基本都要写** | | `edges[].needs` | **数据契约**:这条边的**下游**要吃哪些 kind,而产出方**不是**紧邻上游(跨节点中继)。见 §6bis | **图没过校验时引擎把 `errors` 贴进下一轮提示词**(不是 HTTP 400,没有响应体可读)。 `errors[].rule` 取值为 **`output`**(压根没找到图)/ `acyclic` / `single-entry` / `reachable` / `backward-labeled` / **`fail-fallback-edge`** / **`data-contract`** / `schema` / `reference`, 逐条怎么修见 `agent.md` Step 7 的规则对照表。读懂 → 改图 → 重交,**引擎只给 3 轮**。 **交回图不豁免 §5 的招聘**——引擎在你这一轮回复结束的那一刻就落库,人没雇齐照样外键挂, 而那时你已经下班了。 ## 6bis. 边上的数据契约(`produces` / `needs`)—— **不写就等于下游拿不到输入** 🚨 ### 为什么必须写(这不是可选装饰) 第四次真 CLI 冒烟里 `org-planner-prd` 失败,它自己的归因原话是: > **「不是上游没产出,是它的产出结构上到不了我手上。」** 核实的结果:上游 brd 节点的 `brd.md` 确实写出来了,但它躺在**那个节点的私有目录**里, 而 `brd --success--> prd` 这条边**只携带控制流**(`condition` / `edgeKind` / `failReason`), 不携带任何文件引用。于是 prd 节点的提示词里**一条上游产物路径都没有**, 而跨节点私有目录是禁止读的 —— 它只能失败。 引擎现在会**替下游把输入递到手上**:节点执行前,按「该节点**入边**上声明的 kind」 去产物索引查真实文件,把**绝对路径**拼进提示词。这条链的起点就是你在边上写的声明。 **你不声明 → 索引查不到该给谁 → 下游提示词里没有路径 → 它重演上面那句话。** 这是你画的图能不能跑过第一个节点的分水岭,不是锦上添花。 ### 声明的是「契约」,不是文件名 规划时节点还没跑,**谁也不知道文件叫什么**(`brd.md`?`brd-v2.md`?)。 所以你声明的是**类型**:「这条边传一份 `brd`」。运行时引擎自己把落盘的实例登记进索引, 按 kind 配对。**不要在图里写文件名、不要编路径。** ### kind 的取值域(写错会被 `rule=schema` 打回) | kind | 是什么 | |---|---| | `brd` | 业务需求文档 | | `prd` | 产品需求文档 | | `sdd` | 系统设计文档 | | `acceptance` | 验收契约 | | `contract` | 节点间接口契约 | | `artifact` | 上面都不算的一般交付物(代码、数据、报告…) | | `log` | 运行/测试日志类产物 | 大小写会被归一(`"PRD"` == `"prd"`),但**别自创取值**(`blueprint` / `design-doc` 这类一律被拒)。 ### 两个键的分工 | 键 | 写在哪条边上 | 语义 | |---|---|---| | `produces` | 产出方的**出边** | 「这条边的上游节点会产出这些 kind,交给下游」 —— 绝大多数情况只用它 | | `needs` | 消费方的**入边** | 「下游要吃这些 kind,但产出方不是紧邻上游」 —— 跨节点中继才用 | ```jsonc // brd → prd → sdd:sdd 既要 prd 也要 brd,而 brd 的产出方隔了一个节点 {"from":"p-brd","to":"p-prd","condition":"success","produces":["brd"]} {"from":"p-prd","to":"p-sdd","condition":"success","produces":["prd"],"needs":["brd"]} ``` 三种写法都收:`"produces":"brd"` / `"produces":["brd","prd"]` / `"produces":[{"kind":"brd","required":false}]`。 `required:false` 的唯一正当用途:**产出方根本不在这张图里**(例如 `acceptance` 是上一层 org graph 或 `artifacts/` 给的)。它同时是逃生口——见下面第六项校验。 ### 两条硬规则 1. **`backward` 回退边上不许声明**(`rule=schema`)。引擎解析输入只看 forward 入边, 写在回退边上等于没写,而"看起来写了"比"没写"更难排查。 2. **第六项校验 `data-contract`**:你声明某个节点要吃 kind=X(`needs`), 但**图上没有任何节点的出边声明产出 X** → 整张图被 400 拒。 修法二选一:给真正的产出方那条出边补 `"produces":["X"]`; 或者产出方确实不在本图,改成 `"needs":[{"kind":"X","required":false}]`。 ### 不用声明的那一类:任务级共享产物 `acceptance` / `contract` 这类挂在 task 上的共享产物,引擎**自动**按 task + kind 递给节点, **不受拓扑相邻性限制**(R-9.3 的「planner 产验收契约、validator 执行」中间隔着 coder)。 它们已经在 `artifacts/` 里时你不必也不该沿边声明;只有当**本图里某个节点自己产出**它、 再交给下游时才写 `produces`。 ## 7. 交回前自查五项校验(可选但强烈建议,引擎只给 3 轮) 引擎用的就是这个函数,你可以先在本地跑一遍(`<引擎仓库根>` 用「写入边界」段给的那个绝对路径, `require` 也要用绝对路径 —— 你的 cwd 不在仓库根,写 `./server/...` 会 `Cannot find module`): ```bash node -e " const {validateWorkGraph}=require('<引擎仓库根>/server/services/work-graph-db.js'); const t=JSON.parse(require('fs').readFileSync('<项目根>/workgraph-.json','utf-8')); console.log(JSON.stringify(validateWorkGraph(t.nodes,t.edges),null,1));" ``` `{"ok":true,"errors":[]}` 才交回。这个函数**连第六项 `data-contract` 一起查** (`produces` / `needs` 的取值域、有没有写到 backward 边上、`needs` 的 kind 有没有产出方), 所以 §6bis 那几条也在这一步就能自查出来。 **但它不检查占位符有没有替换干净**——`{{implAgent}}` 在校验器眼里是个合法字符串, 它照样返回 ok。占位符靠你自己看。 --- ## 你**不该**用的工具 > ⚠️ **`company hire` 已经不在这张表里了**(R-18.2 定案)。以前这里写着「你不招人也不辞人」, > 那条**已作废**:招人现在是你的活(§5),只有**辞人**还不是。看到旧提示词里的老话以本节为准。 | 别用 | 为什么 | |---|---| | `POST /api/company/:id/work-graphs` | **NO-SUCH-ENDPOINT — 这个端点不存在,从来没有实现过,不要调用。** 从前这份模板教过它,实测每次真跑都吃一个 404、白烧一轮调用。落库归引擎(§6),谁也不用 POST 图 | | `POST/PATCH /api/company/:id/org-graph` | org graph 是上一层,归 `orggraph-planning` / HR。你只画 work graph | | `node bin/lemon.js company fire` | 你只招不辞。图用完即归档,但雇进来的本体留给后续任务复用(下次 `hire` 会返回 `already_exists`) | | `node bin/lemon.js template create` / `template train` | 造本体、训本体是 `agent-creator` / `innovator` 的活。你只 `hire`,模板库里没有的角色 `hire` 会自动去继承+触发它们 | | 手工 `mkdir` / `cp` 出 `data/companies/<公司ID>/agents/<本体名>/` | 看着像招聘,其实**不写 `system_agents` 表**,引擎落库时照样 `rule=reference` 被打回。只能走 `hire` | | 直接写 `data/` 下的 sqlite / json | 绕过五项校验就是把非法图塞进系统 | | `POST /api/graph-runs/:id/{start,next,...}` | 执行是引擎的活。你把图交出去(§6)就下班 | | 对已落库的 work graph 做任何修改 | work graph 生命周期是**一次性**的,要改就重新规划一张,没有增量补丁 | | `task transition` | **绝对不要**。它是 base 四段式的换阶段命令,与图流转是两层状态机(`agent.md` 约束四) |