Commit c9b96374ab7faf1e3be5c0837cf235c0df75b3d1
1 parent
9a16416a
fix(workgraph-planning): 规划记录文件名带宿主 org node,补 memory.md 桥接原料通道
两个独立的小修复,都是这次 e2e 回归 + 用户讨论"workgraph-planning 该怎么训练"时发现的: 1. workgraph-plan-<taskId>.md 撞名:一个任务里 org-planner/org-coder/org-validator 各自调 workgraph-planning 一次、各自的规划记录却共用同一个文件名,落在同一个项目根下, 后写的原样覆盖先写的——真实撞见过(org-planner 当时的规划记录被 org-validator 覆盖, 事后一个字都找不回来)。改成 workgraph-plan-<taskId>-<宿主orgNode节点名>.md。 2. subgraph 节点结算 → workgraph-planning 自己的 memory.md 桥接:阶段一的 recordNodeOutcomeMemory 明确排除了 subgraph 节点(那是给 agent_call 记的),导致 workgraph-planning 每次规划出的图最终跑得好不好,完全没有可积累的历史——这正是它 "允许裁剪却没有具体判据"这个问题一直无法通过真实反馈自我改进的原因之一。新增 recordWorkgraphPlanningMemory,在 subgraph 节点结算时把结果记进 workgraph-planning (全局唯一规划本体,不是宿主 org node 自己的身份)的 memory.md,只在真实规划 (planWorkGraph===defaultPlanWorkGraph)时记,避免测试夹具污染真实目录。 新增 3 条回归测试,graph-engine 相关测试文件全绿。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
3 changed files
with
210 additions
and
4 deletions
server/services/graph-engine.js
| @@ -1632,6 +1632,34 @@ function recordNodeOutcomeMemory(ctx, nodeSnapshot) { | @@ -1632,6 +1632,34 @@ function recordNodeOutcomeMemory(ctx, nodeSnapshot) { | ||
| 1632 | } | 1632 | } |
| 1633 | 1633 | ||
| 1634 | /** | 1634 | /** |
| 1635 | + * subgraph 节点(org 层)结算时,把「这次规划出的图最终跑得怎么样」记进 | ||
| 1636 | + * `workgraph-planning` 自己的 memory.md——36 号文档追加发现:它的图形状裁剪判据 | ||
| 1637 | + * 现在只有"允许裁剪"没有"具体判据"(真实撞见:单页静态网站任务原样照抄 | ||
| 1638 | + * planner.json 全部 5 节点,一个都没砍),而它自己完全没有可积累的历史——这是补上 | ||
| 1639 | + * 这个原料通道的桥接点,跟 recordNodeOutcomeMemory 记 agent_call 节点是同一个精神。 | ||
| 1640 | + * | ||
| 1641 | + * 不能直接复用 recordNodeOutcomeMemory:subgraph 节点的 `ctx.systemAgentId` 是**宿主 | ||
| 1642 | + * org node 自己的身份**(如 "org-coder"),不是真正执行规划的那个 agent——规划本体 | ||
| 1643 | + * 全局唯一,恒为 `PLANNING_AGENT_ID`(R-15.2)。 | ||
| 1644 | + * | ||
| 1645 | + * 只在真实规划(`planWorkGraph === defaultPlanWorkGraph`)时记:测试夹具几乎总会注入 | ||
| 1646 | + * 假 planWorkGraph(defaultPlanWorkGraph 会真的起 CLI 调 workgraph-planning),这道闸门 | ||
| 1647 | + * 挡住的正是"把假地址写进真实 system/agents/workgraph-planning/ 目录"这类污染 | ||
| 1648 | + * (与 runClaimedNode 里 agentExecutor === defaultAgentExecutor 同一个判据)。 | ||
| 1649 | + */ | ||
| 1650 | +function recordWorkgraphPlanningMemory(companyId, nodeName, status, failReason) { | ||
| 1651 | + if (planWorkGraph !== defaultPlanWorkGraph) return; | ||
| 1652 | + try { | ||
| 1653 | + const addr = resolveAgentName(companyId, PLANNING_AGENT_ID); | ||
| 1654 | + const task = `为 ${nodeName} 规划的 work graph`; | ||
| 1655 | + const result = status === 'success' ? 'success' : `fail${failReason ? `: ${failReason}` : ''}`; | ||
| 1656 | + memoryService.appendToMemoryMd(addr, task, result, { stats: { toolCallCount: 1 } }); | ||
| 1657 | + } catch (err) { | ||
| 1658 | + console.error('[graph-engine] workgraph-planning 结果写入 memory.md 失败:', err.message); | ||
| 1659 | + } | ||
| 1660 | +} | ||
| 1661 | + | ||
| 1662 | +/** | ||
| 1635 | * 批量推进(27-P4 T4/T6):一次拾取至多 `maxConcurrency` 个 ready 节点,并发执行。 | 1663 | * 批量推进(27-P4 T4/T6):一次拾取至多 `maxConcurrency` 个 ready 节点,并发执行。 |
| 1636 | * | 1664 | * |
| 1637 | * `maxConcurrency <= 1` 时**直接委托给 `advanceOne`**,不走下面的批量路径——零行为差异, | 1665 | * `maxConcurrency <= 1` 时**直接委托给 `advanceOne`**,不走下面的批量路径——零行为差异, |
| @@ -1847,6 +1875,13 @@ function settleNode(runId, nodeRunId, outcome = {}) { | @@ -1847,6 +1875,13 @@ function settleNode(runId, nodeRunId, outcome = {}) { | ||
| 1847 | reset: [], infraRerun: false, | 1875 | reset: [], infraRerun: false, |
| 1848 | }; | 1876 | }; |
| 1849 | 1877 | ||
| 1878 | + // 36 号文档:subgraph 节点结算 = 「这次规划出的图最终跑得怎么样」这个信号第一次齐全的时刻 | ||
| 1879 | + // (outcome 是 deriveChildOutcome 派生的,代表子 work graph run 的真实收敛结果)。 | ||
| 1880 | + const settledDefNode = def.nodes.get(nodeRun.def_node_id); | ||
| 1881 | + if (settledDefNode && settledDefNode.nodeKind === 'subgraph') { | ||
| 1882 | + recordWorkgraphPlanningMemory(run.company_id, settledDefNode.name || nodeRun.def_node_id, to, failReason); | ||
| 1883 | + } | ||
| 1884 | + | ||
| 1850 | if (to === 'success') { | 1885 | if (to === 'success') { |
| 1851 | // R-19 产出侧:把这次落盘的文件登记进产物索引,下游节点靠它拿到真实路径。 | 1886 | // R-19 产出侧:把这次落盘的文件登记进产物索引,下游节点靠它拿到真实路径。 |
| 1852 | // 只在 success 时记 —— 失败节点的半成品不该被下游当输入读。 | 1887 | // 只在 success 时记 —— 失败节点的半成品不该被下游当输入读。 |
| @@ -4605,6 +4640,10 @@ module.exports = { | @@ -4605,6 +4640,10 @@ module.exports = { | ||
| 4605 | // 导出只为让"success 那一行怎么格式化"不必靠真起 CLI 触发 isRealDispatch 才能验 | 4640 | // 导出只为让"success 那一行怎么格式化"不必靠真起 CLI 触发 isRealDispatch 才能验 |
| 4606 | // (fail 分支可以借道 body_not_ready 走端到端,success 分支目前只有这条直调路径)。 | 4641 | // (fail 分支可以借道 body_not_ready 走端到端,success 分支目前只有这条直调路径)。 |
| 4607 | recordNodeOutcomeMemory, | 4642 | recordNodeOutcomeMemory, |
| 4643 | + // 36 号文档追加:subgraph 节点(workgraph-planning 规划出的图)结算 → memory.md 桥接。 | ||
| 4644 | + // 真实调用方是 settleNode;导出只为不必真起 CLI 触发 planWorkGraph===defaultPlanWorkGraph | ||
| 4645 | + // 就能验证格式化与闸门逻辑(同 recordNodeOutcomeMemory 的导出理由)。 | ||
| 4646 | + recordWorkgraphPlanningMemory, | ||
| 4608 | 4647 | ||
| 4609 | // 选边 / 熔断 / 透传 | 4648 | // 选边 / 熔断 / 透传 |
| 4610 | selectFailEdge, | 4649 | selectFailEdge, |
server/tests/unit/graph-engine-workgraph-planning-memory.test.js
0 → 100644
| 1 | +/** | ||
| 2 | + * Unit Tests — subgraph 节点结算 → workgraph-planning 自己的 memory.md 桥接(36 号文档追加) | ||
| 3 | + * | ||
| 4 | + * 背景:36 号文档真实回归发现 `workgraph-planning` 的图形状裁剪判据现在只有"允许裁剪" | ||
| 5 | + * 没有"具体判据"(单页静态网站任务原样照抄 planner.json 全部 5 节点,一个都没砍),而它 | ||
| 6 | + * 自己完全没有可积累的历史——阶段一的 `recordNodeOutcomeMemory` 桥接明确排除了 subgraph | ||
| 7 | + * 节点(那是给 agent_call 节点记的,subgraph 节点没有对应的"真实执行"概念)。 | ||
| 8 | + * | ||
| 9 | + * 本文件锁的是补上的那半:subgraph 节点结算(= 它规划出的子 work graph 最终收敛)时, | ||
| 10 | + * 把"这次规划的图跑得怎么样"记进 `workgraph-planning` 自己的 memory.md(不是宿主 org node | ||
| 11 | + * 的 `ctx.systemAgentId`,那是宿主自己的身份,不是真正执行规划的 agent——规划本体全局唯一)。 | ||
| 12 | + * | ||
| 13 | + * ① 真实规划(`planWorkGraph === defaultPlanWorkGraph`)+ success → 记 "success" | ||
| 14 | + * ② 真实规划 + fail(reason) → 记 "fail: <reason>" | ||
| 15 | + * ③ 注入假 planWorkGraph(全仓库现有 subgraph 测试的常态,见 graph-engine-revive-parent.test.js | ||
| 16 | + * 的 buildParentWithFailingChild)→ 不记,不能因为这次改动就把假地址写进真实 | ||
| 17 | + * system/agents/workgraph-planning/ 目录 | ||
| 18 | + * | ||
| 19 | + * ①②借道直调(不必真起 CLI 触发 planWorkGraph===defaultPlanWorkGraph 才能验证格式化), | ||
| 20 | + * ③走完整的 subgraph 结算流程(复用 graph-engine-revive-parent.test.js 的建图手法), | ||
| 21 | + * 确认现有"全仓库 subgraph 测试都注入假 planWorkGraph"这条约定下这个新桥接确实按兵不动。 | ||
| 22 | + */ | ||
| 23 | +import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; | ||
| 24 | +import fs from 'fs'; | ||
| 25 | +import os from 'os'; | ||
| 26 | +import path from 'path'; | ||
| 27 | + | ||
| 28 | +describe('subgraph 节点结算桥接进 workgraph-planning 的 memory.md(36 号文档)', () => { | ||
| 29 | + let wdb; | ||
| 30 | + let engine; | ||
| 31 | + let memoryService; | ||
| 32 | + let runsDir; | ||
| 33 | + const TEST_DB_PATH = '/tmp/test-graph-engine-workgraph-planning-memory.sqlite'; | ||
| 34 | + let origAppendToMemoryMd; | ||
| 35 | + let calls; | ||
| 36 | + | ||
| 37 | + beforeAll(() => { | ||
| 38 | + for (const f of [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`]) { | ||
| 39 | + try { fs.unlinkSync(f); } catch {} | ||
| 40 | + } | ||
| 41 | + process.env.GRAPH_DB_PATH = TEST_DB_PATH; | ||
| 42 | + runsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'graph-wgplan-memory-runs-')); | ||
| 43 | + process.env.GRAPH_RUNS_DIR = runsDir; | ||
| 44 | + for (const m of [ | ||
| 45 | + '../../services/graph-db', | ||
| 46 | + '../../services/graph-service', | ||
| 47 | + '../../services/org-graph-service', | ||
| 48 | + '../../services/work-graph-db', | ||
| 49 | + '../../services/graph-run-sse', | ||
| 50 | + '../../services/graph-engine', | ||
| 51 | + '../../services/memory-service', | ||
| 52 | + ]) { | ||
| 53 | + delete require.cache[require.resolve(m)]; | ||
| 54 | + } | ||
| 55 | + wdb = require('../../services/work-graph-db'); | ||
| 56 | + engine = require('../../services/graph-engine'); | ||
| 57 | + memoryService = require('../../services/memory-service'); | ||
| 58 | + origAppendToMemoryMd = memoryService.appendToMemoryMd; | ||
| 59 | + }); | ||
| 60 | + | ||
| 61 | + afterAll(() => { | ||
| 62 | + for (const f of [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`]) { | ||
| 63 | + try { fs.unlinkSync(f); } catch {} | ||
| 64 | + } | ||
| 65 | + delete process.env.GRAPH_DB_PATH; | ||
| 66 | + try { fs.rmSync(runsDir, { recursive: true, force: true }); } catch {} | ||
| 67 | + delete process.env.GRAPH_RUNS_DIR; | ||
| 68 | + }); | ||
| 69 | + | ||
| 70 | + beforeEach(() => { | ||
| 71 | + const db = wdb.getDb(); | ||
| 72 | + db.pragma('foreign_keys = OFF'); | ||
| 73 | + db.exec(` | ||
| 74 | + DELETE FROM graph_run_loop_records; | ||
| 75 | + DELETE FROM graph_run_nodes; | ||
| 76 | + DELETE FROM graph_runs; | ||
| 77 | + DELETE FROM work_graph_edges; | ||
| 78 | + DELETE FROM work_graph_nodes; | ||
| 79 | + DELETE FROM work_graphs; | ||
| 80 | + DELETE FROM system_agents; | ||
| 81 | + `); | ||
| 82 | + db.pragma('foreign_keys = ON'); | ||
| 83 | + engine.setAgentExecutor(null); | ||
| 84 | + engine.setPlanWorkGraph(null); | ||
| 85 | + memoryService.appendToMemoryMd = origAppendToMemoryMd; | ||
| 86 | + }); | ||
| 87 | + | ||
| 88 | + function spyMemory() { | ||
| 89 | + calls = []; | ||
| 90 | + memoryService.appendToMemoryMd = (...args) => { calls.push(args); }; | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + it('① 真实规划(直调,不必起真 CLI)+ success → 记一行 "success"', () => { | ||
| 94 | + spyMemory(); | ||
| 95 | + engine.recordWorkgraphPlanningMemory('co-wgplan-1', 'org-coder', 'success', null); | ||
| 96 | + | ||
| 97 | + expect(calls.length).toBe(1); | ||
| 98 | + const [addr, task, result] = calls[0]; | ||
| 99 | + expect(addr).toBe('co-wgplan-1:workgraph-planning'); | ||
| 100 | + expect(task).toContain('org-coder'); | ||
| 101 | + expect(result).toBe('success'); | ||
| 102 | + }); | ||
| 103 | + | ||
| 104 | + it('② 真实规划(直调)+ fail(reason) → 记一行 "fail: <reason>"', () => { | ||
| 105 | + spyMemory(); | ||
| 106 | + engine.recordWorkgraphPlanningMemory('co-wgplan-2', 'org-validator', 'fail', 'impl_bug'); | ||
| 107 | + | ||
| 108 | + expect(calls.length).toBe(1); | ||
| 109 | + const [addr, , result] = calls[0]; | ||
| 110 | + expect(addr).toBe('co-wgplan-2:workgraph-planning'); | ||
| 111 | + expect(result).toBe('fail: impl_bug'); | ||
| 112 | + }); | ||
| 113 | + | ||
| 114 | + // ③:复用 graph-engine-revive-parent.test.js 的建图手法——父 run 一个 subgraph 节点 | ||
| 115 | + // (宿主 org node)+ 一张会失败/成功的子图,全程注入假 setPlanWorkGraph(全仓库 subgraph | ||
| 116 | + // 测试的常态:defaultPlanWorkGraph 会真的起 CLI 调 workgraph-planning)。 | ||
| 117 | + function ensureAgents(ids) { | ||
| 118 | + const db = wdb.getDb(); | ||
| 119 | + const now = new Date().toISOString(); | ||
| 120 | + const ins = db.prepare(` | ||
| 121 | + INSERT OR IGNORE INTO system_agents | ||
| 122 | + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at) | ||
| 123 | + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?) | ||
| 124 | + `); | ||
| 125 | + for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now); | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + it('③ 注入假 planWorkGraph(全仓库现有 subgraph 测试的常态)→ 不记,不污染真实目录', async () => { | ||
| 129 | + spyMemory(); | ||
| 130 | + const companyId = 'co-wgplan-3'; | ||
| 131 | + ensureAgents(['org-coder', 'wg-only']); | ||
| 132 | + | ||
| 133 | + const childG = wdb.createWorkGraph({ companyId, nodes: ['wg-only'], edges: [] }); | ||
| 134 | + expect(childG.errors || []).toEqual([]); | ||
| 135 | + | ||
| 136 | + const parentG = wdb.createWorkGraph({ | ||
| 137 | + companyId, | ||
| 138 | + nodes: [{ nodeName: 'org-coder', systemAgentId: 'org-coder', nodeKind: 'subgraph' }], | ||
| 139 | + edges: [], | ||
| 140 | + }); | ||
| 141 | + expect(parentG.errors || []).toEqual([]); | ||
| 142 | + | ||
| 143 | + const parentRun = wdb.createRun({ graphKind: 'work', graphId: parentG.id, companyId, executionMode: 'manual' }); | ||
| 144 | + engine.markRunReady(parentRun.id); | ||
| 145 | + engine.startRun(parentRun.id, { mode: 'manual' }); | ||
| 146 | + | ||
| 147 | + engine.setPlanWorkGraph(() => ({ workGraphId: childG.id })); | ||
| 148 | + engine.setAgentExecutor(async () => ({ status: 'success', output: '子节点成功' })); | ||
| 149 | + | ||
| 150 | + for (let i = 0; i < 12; i += 1) { | ||
| 151 | + const r = await engine.advanceTree(parentRun.id); | ||
| 152 | + if (!r || r.ok === false) break; | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + expect(wdb.getRunState(parentRun.id).run.run_status).toBe('completed'); | ||
| 156 | + expect(calls.length).toBe(0); | ||
| 157 | + }); | ||
| 158 | +}); |
system/agents/workgraph-planning/agent.md
| @@ -149,13 +149,22 @@ curl -s http://localhost:3010/api/company/<公司ID>/agents | @@ -149,13 +149,22 @@ curl -s http://localhost:3010/api/company/<公司ID>/agents | ||
| 149 | 149 | ||
| 150 | ### Step 5 · 落盘规划记录 | 150 | ### Step 5 · 落盘规划记录 |
| 151 | 151 | ||
| 152 | -把图和**为什么这么设计的理由**写成一份 `workgraph-plan-<taskId>.md`,供事后人工审计。 | 152 | +把图和**为什么这么设计的理由**写成一份 `workgraph-plan-<taskId>-<宿主orgNode节点名>.md`, |
| 153 | +供事后人工审计。 | ||
| 154 | + | ||
| 155 | +> ⚠️ **文件名必须带宿主 org node 名字,不能只用 `<taskId>`。** 一个任务里 `org-planner` / | ||
| 156 | +> `org-coder` / `org-validator` 各自都会调你一次、各自规划一张图,都落在**同一个项目根**下—— | ||
| 157 | +> 三次写入如果共用 `workgraph-plan-<taskId>.md` 这一个文件名,后写的会把先写的**原样覆盖**, | ||
| 158 | +> 事后审计只剩最后一个 org node 的记录(2026-08-23 真实撞见过:`org-planner` 当时的规划记录 | ||
| 159 | +> 被后跑的 `org-validator` 覆盖,事后一个字都找不回来)。你的 `nodeName` | ||
| 160 | +> (首轮消息「① 宿主 org node 的角色定义」段里的「节点名(org graph 内)」)就是这里要用的 | ||
| 161 | +> `<宿主orgNode节点名>`,例如 `workgraph-plan-T-mt5d3mi9-57cf-org-coder.md`。 | ||
| 153 | 162 | ||
| 154 | **落点看首轮消息的「写入边界」段**,不要凭习惯写相对路径: | 163 | **落点看首轮消息的「写入边界」段**,不要凭习惯写相对路径: |
| 155 | 164 | ||
| 156 | | 边界段说了什么 | 你写哪儿 | | 165 | | 边界段说了什么 | 你写哪儿 | |
| 157 | |---|---| | 166 | |---|---| |
| 158 | -| 给了项目根 | `<项目根>/workgraph-plan-<taskId>.md`,**用绝对路径** | | 167 | +| 给了项目根 | `<项目根>/workgraph-plan-<taskId>-<宿主orgNode节点名>.md`,**用绝对路径** | |
| 159 | | 明说没分到项目根 | **一个文件都不落**,把这份记录直接写进回复正文 | | 168 | | 明说没分到项目根 | **一个文件都不落**,把这份记录直接写进回复正文 | |
| 160 | 169 | ||
| 161 | ⚠️ 相对路径(`workspace/...`)会落到**引擎源码仓库**里——那正是边界段禁止写的位置。 | 170 | ⚠️ 相对路径(`workspace/...`)会落到**引擎源码仓库**里——那正是边界段禁止写的位置。 |
| @@ -376,7 +385,7 @@ ls data/companies/<公司ID>/agents/ | @@ -376,7 +385,7 @@ ls data/companies/<公司ID>/agents/ | ||
| 376 | 1. **不要放弃**——被打回一轮是设计里预期的交互,不是失败。 | 385 | 1. **不要放弃**——被打回一轮是设计里预期的交互,不是失败。 |
| 377 | 2. **不要原样重交**——不许把同一份图再贴一遍指望它这次能过。 | 386 | 2. **不要原样重交**——不许把同一份图再贴一遍指望它这次能过。 |
| 378 | 3. **不要绕过校验**——不许直接写数据库、不许把违规节点删光来"通过"。 | 387 | 3. **不要绕过校验**——不许直接写数据库、不许把违规节点删光来"通过"。 |
| 379 | -4. 每次重交前,**先在规划记录 `workgraph-plan-<taskId>.md` 里追加一行**:撞了哪条规则、怎么改的。 | 388 | +4. 每次重交前,**先在规划记录 `workgraph-plan-<taskId>-<宿主orgNode节点名>.md` 里追加一行**:撞了哪条规则、怎么改的。 |
| 380 | 5. **引擎只给你 3 轮**(`PLAN_MAX_ROUNDS`,比这份模板从前写的 5 轮更紧——每一轮都是一次真花钱的调用)。 | 389 | 5. **引擎只给你 3 轮**(`PLAN_MAX_ROUNDS`,比这份模板从前写的 5 轮更紧——每一轮都是一次真花钱的调用)。 |
| 381 | 第 3 轮还不过,这个节点就按 fail 结算走兜底边。所以**第一轮就把约束九和五项校验自查做完**, | 390 | 第 3 轮还不过,这个节点就按 fail 结算走兜底边。所以**第一轮就把约束九和五项校验自查做完**, |
| 382 | 不要把校验当成免费的往返。 | 391 | 不要把校验当成免费的往返。 |
| @@ -390,7 +399,7 @@ ls data/companies/<公司ID>/agents/ | @@ -390,7 +399,7 @@ ls data/companies/<公司ID>/agents/ | ||
| 390 | - 节点数 / 边数,其中 backward 边几条; | 399 | - 节点数 / 边数,其中 backward 边几条; |
| 391 | - 引用了哪些本体,其中哪些**清单里不存在**(Step 3 情形 3); | 400 | - 引用了哪些本体,其中哪些**清单里不存在**(Step 3 情形 3); |
| 392 | - **招聘结果**:雇了几个,各是什么 `status`,其中几个走了 `hired_from_base`,**有没有没雇上的**(有就逐个点名 + 原始错误); | 401 | - **招聘结果**:雇了几个,各是什么 `status`,其中几个走了 `hired_from_base`,**有没有没雇上的**(有就逐个点名 + 原始错误); |
| 393 | -- 规划记录的绝对路径(`<项目根>/workgraph-plan-<taskId>.md`;没分到项目根就把记录写在正文里)。 | 402 | +- 规划记录的绝对路径(`<项目根>/workgraph-plan-<taskId>-<宿主orgNode节点名>.md`;没分到项目根就把记录写在正文里)。 |
| 394 | 403 | ||
| 395 | **不要报 `workGraphId`**——图 id 是引擎落库时才生成的,你手上没有,编一个只会让引擎去查一个不存在的图, | 404 | **不要报 `workGraphId`**——图 id 是引擎落库时才生成的,你手上没有,编一个只会让引擎去查一个不存在的图, |
| 396 | 白烧一轮(见 Step 7 那张表最后一行)。 | 405 | 白烧一轮(见 Step 7 那张表最后一行)。 |