Commit a4b248939f37cfdbfdce2354f531b6de6d7dbc9e
1 parent
ddda1ac7
fix(graph-engine): 44 号写共享区产出闸门永假 + 41 号 infra_flake 忽视 target_node(两处真 bug)
44 号:writesArtifacts 节点产出闸门永远打不过 - goal-benchmark 按设计把产物写进 root run 共享 artifacts/ 目录(三个节点合成同一份 acceptance.json,天然不可能落在某节点私有目录),但 unsatisfiedRequiredProduces 的 R-19 逃生口 readDeclaredProduces 的 allowedRoots 只认节点私有目录 + 项目根,够不到 共享区——这条契约在当前实现下无论 agent 表现多好都无法通过。 - 修法(44 号文档方案 1,改动面最小):allowedRoots 加入 runRootDir(run)/artifacts/; goal-benchmark 的 agent.md/tools.md 补上"写完共享产物后在自己节点私有目录声明一份 contract.json"这一步。 - 回归:graph-files-index.test.js 新增用例 ⑭。 41 号:routeFailure 的 infra_flake 分支完全无视 target_node - 30 号文档记录过一次真实死循环:节点自称 infra_flake 并点名 target_node,引擎原来 一律原地重跑该节点自己,从不看 target_node。 - 修法(最保守方案,用户确认):新增 infraFlakeTargetEdge——target_node 只有在图上 真有一条从本节点出发的 backward fail 边指向那个节点时才生效,命中就改走正常的 backward 边路由(计入 backward_count/双层熔断),不再享受"原地重跑、只受 infra_flake_count 约束"的免账本待遇;没命中维持原有行为。 - 重要澄清:这条修复堵不住 30 号文档那次真实事故——事故里点名的目标是下游节点, 而 org-graph-service.js 规则 4(backward-labeled)从图结构上就不允许 backward 边 指向非祖先,那次事故本来就不是"有边不认",是"诊断内容在打图结构不允许表达的 主意",已经靠 INFRA_FLAKE_HARD_CAP 熔断收敛,不在这条修复范围内。 - 回归:graph-engine.test.js 的 infra_flake describe 块新增两条用例。 44 号文档移入 done/,41 号清单项相应更新,43 号文档的交叉引用一并修正。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CwGcvMfUcgCcweN5vjhTkn
Showing
9 changed files
with
240 additions
and
13 deletions
docs/README.md
| @@ -37,6 +37,7 @@ | @@ -37,6 +37,7 @@ | ||
| 37 | | [system-design/40-agent-chat-streaming-interrupt.md](./done/system-design/40-agent-chat-streaming-interrupt.md) | ✅ **已实现**(commit `4d23f50`,2026-08-30)。回应"前端刷新不及时/没有打字机效果,要不要把 SSE 换成 WebSocket"——排查后结论是传输层不是瓶颈(对照 hermes-webui/hermes-agent/deepseek-harness 三个参考实现,全部走 SSE,不用 WebSocket)。三个独立缺口全部补上:① 打字机不流畅→后端按字符数切片 + 前端 `createTypewriterQueue` 匀速吐出;② 执行中无法打断并接上新消息→`stop` 端点改轮询确认锁真正释放,前端据此直接发送/强制发送兜底,顺带修了服务端对同 convId 并发缺闸门这个真实 bug;③ 串台加固→`handleEvent` 抽成纯函数并补齐 `9b1c3f1` 那条不变式的回归测试。已用真实 Claude CLI 会话在浏览器验证 | | 37 | | [system-design/40-agent-chat-streaming-interrupt.md](./done/system-design/40-agent-chat-streaming-interrupt.md) | ✅ **已实现**(commit `4d23f50`,2026-08-30)。回应"前端刷新不及时/没有打字机效果,要不要把 SSE 换成 WebSocket"——排查后结论是传输层不是瓶颈(对照 hermes-webui/hermes-agent/deepseek-harness 三个参考实现,全部走 SSE,不用 WebSocket)。三个独立缺口全部补上:① 打字机不流畅→后端按字符数切片 + 前端 `createTypewriterQueue` 匀速吐出;② 执行中无法打断并接上新消息→`stop` 端点改轮询确认锁真正释放,前端据此直接发送/强制发送兜底,顺带修了服务端对同 convId 并发缺闸门这个真实 bug;③ 串台加固→`handleEvent` 抽成纯函数并补齐 `9b1c3f1` 那条不变式的回归测试。已用真实 Claude CLI 会话在浏览器验证 | |
| 38 | | [system-design/39-boss-agent-task-supervision.md](./done/system-design/39-boss-agent-task-supervision.md) | ✅ **P1-P5 全部已实现**(2026-08-30,`a00590a`/`5108d67`/`d967ac0`/`14cbaf5`)。boss agent(复用 `ceo`)监督 goal/task 看板闭环:draft task 回写、事件驱动+低频兜底唤醒、独立任务看板页、公司级 `target_dir`。**真实端到端测试**(建公司→下 goal→跑出一个 Playwright 5/5 验证通过的贪吃蛇游戏)撞出并记录了三个单测没覆盖的真问题,见文内「七、真跑复盘」;沉淀的待办已在 [42-org-work-graph-reliability-followups.md](./done/system-design/42-org-work-graph-reliability-followups.md) 全部收口 | | 38 | | [system-design/39-boss-agent-task-supervision.md](./done/system-design/39-boss-agent-task-supervision.md) | ✅ **P1-P5 全部已实现**(2026-08-30,`a00590a`/`5108d67`/`d967ac0`/`14cbaf5`)。boss agent(复用 `ceo`)监督 goal/task 看板闭环:draft task 回写、事件驱动+低频兜底唤醒、独立任务看板页、公司级 `target_dir`。**真实端到端测试**(建公司→下 goal→跑出一个 Playwright 5/5 验证通过的贪吃蛇游戏)撞出并记录了三个单测没覆盖的真问题,见文内「七、真跑复盘」;沉淀的待办已在 [42-org-work-graph-reliability-followups.md](./done/system-design/42-org-work-graph-reliability-followups.md) 全部收口 | |
| 39 | | [system-design/42-org-work-graph-reliability-followups.md](./done/system-design/42-org-work-graph-reliability-followups.md) | ✅ **Phase 1-4 全部已实现**(2026-08-30,`6ae1f76`/`2a60fe1`/`ae7650e`/`2a0ffb8`)。39 号文档真跑复盘沉淀的四 phase 可靠性收尾:① 卡住恢复闭环(服务器重启僵尸节点检测 + 强制判失败端点)② `ceo` 自建 task 的闸门穿透性回归测试 ③ `target_dir` 的 API/UI 配置入口(Phase 3 前端缺一次真实浏览器点击验证,见该节说明,是唯一已知缺口)④ `timeline.ndjson` 承接 planning/reflection 轨迹——`--artifact` 路径本身编码 `rootRunId`/`nodeRunId`,不用改任何 agent.md 或塞新环境变量 | | 39 | | [system-design/42-org-work-graph-reliability-followups.md](./done/system-design/42-org-work-graph-reliability-followups.md) | ✅ **Phase 1-4 全部已实现**(2026-08-30,`6ae1f76`/`2a60fe1`/`ae7650e`/`2a0ffb8`)。39 号文档真跑复盘沉淀的四 phase 可靠性收尾:① 卡住恢复闭环(服务器重启僵尸节点检测 + 强制判失败端点)② `ceo` 自建 task 的闸门穿透性回归测试 ③ `target_dir` 的 API/UI 配置入口(Phase 3 前端缺一次真实浏览器点击验证,见该节说明,是唯一已知缺口)④ `timeline.ndjson` 承接 planning/reflection 轨迹——`--artifact` 路径本身编码 `rootRunId`/`nodeRunId`,不用改任何 agent.md 或塞新环境变量 | |
| 40 | +| [system-design/44-writes-artifacts-produces-gate-mismatch.md](./done/system-design/44-writes-artifacts-produces-gate-mismatch.md) | ✅ **已修复**(2026-08-31)。43 号文档真实回归中发现:`writesArtifacts:true` 节点(`goal-benchmark`)写进共享 `artifacts/` 目录的产物,产出闸门只查节点私有目录,永远判「没产出」。修法:`readDeclaredProduces` 的 `allowedRoots` 纳入共享区 + `goal-benchmark` 补一份 `contract.json` 显式声明 | | ||
| 40 | 41 | ||
| 41 | ## todo/(当前主线) | 42 | ## todo/(当前主线) |
| 42 | 43 | ||
| @@ -56,11 +57,10 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | @@ -56,11 +57,10 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | ||
| 56 | | [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(设计见 29 号,已实现,见下)、`source_kind`(✅已删)/`legacy_path`(不能删)删列、三项「已实施未验证」。任务态清理(✅已实现,见下)。§五 是**已立起来的守卫清单**(别重复造,也别删) | | 57 | | [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(设计见 29 号,已实现,见下)、`source_kind`(✅已删)/`legacy_path`(不能删)删列、三项「已实施未验证」。任务态清理(✅已实现,见下)。§五 是**已立起来的守卫清单**(别重复造,也别删) | |
| 57 | | [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据。2026-08-23 追加真实实例并已修:`infra_flake` 原地重跑没有熔断线,真撞上过一次确定性死循环(节点把诊断结论误判成自己的执行结果,反复重跑却从不路由到诊断建议的目标节点);已加 `infra_flake_count` 列 + 硬顶 3 次熔断,连续判 infra_flake 超过硬顶就改判 blocked 交给人工 | | 58 | | [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据。2026-08-23 追加真实实例并已修:`infra_flake` 原地重跑没有熔断线,真撞上过一次确定性死循环(节点把诊断结论误判成自己的执行结果,反复重跑却从不路由到诊断建议的目标节点);已加 `infra_flake_count` 列 + 硬顶 3 次熔断,连续判 infra_flake 超过硬顶就改判 blocked 交给人工 | |
| 58 | | [31-work-graph-fail-fallback-coverage.md](./todo/system-design/31-work-graph-fail-fallback-coverage.md) | work graph 兜底边覆盖不全 — ✅ 已完成(除入口节点外强制兜底边,`planner.json`/`executor-fanout.json` 补 6 条边),978/978 单测通过;2026-08-21 又用真实 LLM 起了一条新任务做端到端回归验证,`org-planner-sdd` 自判失败后自动打回 `org-planner-prd`(3 次,零人工介入),修复确认生效 | | 59 | | [31-work-graph-fail-fallback-coverage.md](./todo/system-design/31-work-graph-fail-fallback-coverage.md) | work graph 兜底边覆盖不全 — ✅ 已完成(除入口节点外强制兜底边,`planner.json`/`executor-fanout.json` 补 6 条边),978/978 单测通过;2026-08-21 又用真实 LLM 起了一条新任务做端到端回归验证,`org-planner-sdd` 自判失败后自动打回 `org-planner-prd`(3 次,零人工介入),修复确认生效 | |
| 59 | -| [41-graph-vs-fde-reliability-discussion.md](./todo/system-design/41-graph-vs-fde-reliability-discussion.md) | 📝 **讨论记录 + 落地实测,非设计方案**。org/work graph 与 master-Innovator-FDE 两套工作流的稳定性对比:根因是"拓扑运行时生成 vs 写死"。问题 7 是真实执行记录——新建 `org-hunt/digest/evolve`(graph 原生协议,方法论抄 innovator-hunt/digest/evolver)跑 Hunt→Digest→Evolve,11 轮真跑中挖出并修复 5 个独立缺陷(建公司自动规划竞态、work-graph 模板库缺内容型交付物骨架、验收节点任务描述越权、两个规划 agent 的路径口径写错、R-19 产出索引 `indexNodeOutputs()` 不覆盖项目根——最后一条改了 `graph-engine.js` 核心逻辑,加 `contract.json` 的 `produces` 显式声明机制,全局生效,补了 2 条守卫测试)。最终产出与原生 FDE(`prd-methodology-trial`)对比:内容骨架可迁移、质量不打折,但过程严谨度不如原生 FDE,可靠性差距悬殊(原生一次跑通,graph 版 11 轮才稳定)。`routeFailure` 的 `infra_flake` 分支绕过跨层寻址(`target_node`)仍是唯一确认还没补的口子 | | 60 | +| [41-graph-vs-fde-reliability-discussion.md](./todo/system-design/41-graph-vs-fde-reliability-discussion.md) | 📝 **讨论记录 + 落地实测,非设计方案**。org/work graph 与 master-Innovator-FDE 两套工作流的稳定性对比:根因是"拓扑运行时生成 vs 写死"。问题 7 是真实执行记录——新建 `org-hunt/digest/evolve`(graph 原生协议,方法论抄 innovator-hunt/digest/evolver)跑 Hunt→Digest→Evolve,11 轮真跑中挖出并修复 5 个独立缺陷(建公司自动规划竞态、work-graph 模板库缺内容型交付物骨架、验收节点任务描述越权、两个规划 agent 的路径口径写错、R-19 产出索引 `indexNodeOutputs()` 不覆盖项目根——最后一条改了 `graph-engine.js` 核心逻辑,加 `contract.json` 的 `produces` 显式声明机制,全局生效,补了 2 条守卫测试)。最终产出与原生 FDE(`prd-methodology-trial`)对比:内容骨架可迁移、质量不打折,但过程严谨度不如原生 FDE,可靠性差距悬殊(原生一次跑通,graph 版 11 轮才稳定)。`routeFailure` 的 `infra_flake` 分支绕过跨层寻址(`target_node`)这个口子已按最保守方案补上(2026-08-31,见文内清单)——但堵不住 30 号文档记录的那次真实事故,那次事故点名的目标是下游节点,图模型本身就不允许 backward 边指向非祖先,属于修复范围之外 | |
| 60 | | [37-org-graph-department-scoping.md](./todo/system-design/37-org-graph-department-scoping.md) | 📝 **设计讨论,调用点扫描已完成,尚未立项实现**。现状 org graph 按 `company_id` 单例(`27-P2` 标题原文即"公司级"),塞不下 HR/财务/市场这类跟"交付流水线"无关的公司常设职能——`system/agents/hr/`("招·留·管·退")本体已存在却从没被装进任何图,全靠 fire-and-forget chat 招聘、无图执行审计轨迹。提议把 org graph 作用域从 `company_id` 升级为 `(company_id, department_id)`,一家公司可装配多张部门图,每张仍保持人工固定/慢变/可审计。全仓库调用点扫描结论:改造面是最初估算的 4 倍——真正的单点真相在 `work-graph-db.js:readDefGraph` 的裸 SQL 不变式(不在服务层),还牵涉两个易混淆服务文件间必须字节级一致的重复唯一索引、独立的 `graph-seed-service.js` 对账子系统、`company-hire-org-layer.test.js` 专属测试、`orggraph-planning`/`task-planning` 两份 agent 人设文档的自我认知改写 | | 61 | | [37-org-graph-department-scoping.md](./todo/system-design/37-org-graph-department-scoping.md) | 📝 **设计讨论,调用点扫描已完成,尚未立项实现**。现状 org graph 按 `company_id` 单例(`27-P2` 标题原文即"公司级"),塞不下 HR/财务/市场这类跟"交付流水线"无关的公司常设职能——`system/agents/hr/`("招·留·管·退")本体已存在却从没被装进任何图,全靠 fire-and-forget chat 招聘、无图执行审计轨迹。提议把 org graph 作用域从 `company_id` 升级为 `(company_id, department_id)`,一家公司可装配多张部门图,每张仍保持人工固定/慢变/可审计。全仓库调用点扫描结论:改造面是最初估算的 4 倍——真正的单点真相在 `work-graph-db.js:readDefGraph` 的裸 SQL 不变式(不在服务层),还牵涉两个易混淆服务文件间必须字节级一致的重复唯一索引、独立的 `graph-seed-service.js` 对账子系统、`company-hire-org-layer.test.js` 专属测试、`orggraph-planning`/`task-planning` 两份 agent 人设文档的自我认知改写 | |
| 61 | | [36-prd-agent-methodology-training-feasibility.md](./todo/system-design/36-prd-agent-methodology-training-feasibility.md) | 📝 **调研 + 阶段一/二均已完成**。回应"落地页太单薄是不是该用 Innovator 先训练专业 agent、再用 Dream 持续迭代"——认同方向。**阶段一**:`graph-engine.js` 结算节点后把 `success`/`fail:<reason>` 写进本体 `memory.md`(此前只有人类对话路径会写,Dream 天天空转)。**阶段二**:新建独立试验节点 `prd-methodology-trial` 走真实 Hunt→Digest→Evolve,2 轮收敛(96/97 分,真实基线 50 分),三个真实历史项目反推验证方法论有效;过程中连带修复同一个 2026-08-16 改名批次遗留的 5 处 `innovator`→`org-innovator` 死链(后端两处硬编码、前端"重新训练"按钮、`/api/agent/list` 展示 id 来源的四个 `config.json`),以及 `AgentScaffold` 默认模板一处裸相对路径缺陷。结论:FDE 对方法论类角色确实管用,但收敛速度/验收严谨度依赖任务书显式写明的自纠正要求,非免人工黑盒。环节 3(训练产物 → 雇佣本体转换)尚未开工 | | 62 | | [36-prd-agent-methodology-training-feasibility.md](./todo/system-design/36-prd-agent-methodology-training-feasibility.md) | 📝 **调研 + 阶段一/二均已完成**。回应"落地页太单薄是不是该用 Innovator 先训练专业 agent、再用 Dream 持续迭代"——认同方向。**阶段一**:`graph-engine.js` 结算节点后把 `success`/`fail:<reason>` 写进本体 `memory.md`(此前只有人类对话路径会写,Dream 天天空转)。**阶段二**:新建独立试验节点 `prd-methodology-trial` 走真实 Hunt→Digest→Evolve,2 轮收敛(96/97 分,真实基线 50 分),三个真实历史项目反推验证方法论有效;过程中连带修复同一个 2026-08-16 改名批次遗留的 5 处 `innovator`→`org-innovator` 死链(后端两处硬编码、前端"重新训练"按钮、`/api/agent/list` 展示 id 来源的四个 `config.json`),以及 `AgentScaffold` 默认模板一处裸相对路径缺陷。结论:FDE 对方法论类角色确实管用,但收敛速度/验收严谨度依赖任务书显式写明的自纠正要求,非免人工黑盒。环节 3(训练产物 → 雇佣本体转换)尚未开工 | |
| 62 | | [34-validator-scored-without-real-deliverable.md](./todo/system-design/34-validator-scored-without-real-deliverable.md) | 📝 **调研记录,checklist 已查完**。最初以为 `org-coder-impl` 从未产出任何 HTML/CSS,后来发现真实网站其实存在于 `graph_files` 索引不覆盖的「项目根」目录(已顺手修:控制台「产物」面板现在会展示项目根)。追加查清 validator 有没有真的做浏览器级检查——中途自我订正一次:不是"没工具做不到"(`bash` 能走到仓库自带的 Playwright,另一条对照任务证明可行),是**没人教它这么做**;抽查另外两个项目根后发现**验证严谨度完全不稳定**(同类任务有的做了真实 Playwright 级验证、有的只做静态正则、有的没验证就被取消),根因很可能是新招聘的 `unit-tester` 本体撞上了 [32 号文档](./todo/system-design/32-hire-body-fill-reliability.md) 那类"装配没填充完"的真实样本,不是平台工具限制 | | 63 | | [34-validator-scored-without-real-deliverable.md](./todo/system-design/34-validator-scored-without-real-deliverable.md) | 📝 **调研记录,checklist 已查完**。最初以为 `org-coder-impl` 从未产出任何 HTML/CSS,后来发现真实网站其实存在于 `graph_files` 索引不覆盖的「项目根」目录(已顺手修:控制台「产物」面板现在会展示项目根)。追加查清 validator 有没有真的做浏览器级检查——中途自我订正一次:不是"没工具做不到"(`bash` 能走到仓库自带的 Playwright,另一条对照任务证明可行),是**没人教它这么做**;抽查另外两个项目根后发现**验证严谨度完全不稳定**(同类任务有的做了真实 Playwright 级验证、有的只做静态正则、有的没验证就被取消),根因很可能是新招聘的 `unit-tester` 本体撞上了 [32 号文档](./todo/system-design/32-hire-body-fill-reliability.md) 那类"装配没填充完"的真实样本,不是平台工具限制 | |
| 63 | -| [44-writes-artifacts-produces-gate-mismatch.md](./todo/system-design/44-writes-artifacts-produces-gate-mismatch.md) | 📝 **真实回归中发现的缺陷记录,未修复**。43 号文档 Phase 2 真实验证时撞见:`goal-benchmark`(`planner.json` 里标 `writesArtifacts:true`)按文档正确把产物写进 root run 共享 `artifacts/` 目录,但产出闸门 `unsatisfiedRequiredProduces`(`graph-engine.js:4430`)只查节点私有目录(`nodeArtifactDir`),`writesArtifacts` 这个字段全仓库零代码读取——这条契约在当前实现下**永远无法通过**,不是偶发。真实证据:`artifacts/goal-benchmark.json` 磁盘上确认存在,闸门仍判「没找到」,强制 fail 沿兜底边回退。很可能正是 snakegame-co 参考任务里 `goal-benchmark`/`planner-validator` 显示 `skipped` 的真实原因。已有的 R-19 `contract.json` 显式声明逃生口(41 号文档修的那个)`allowedRoots` 也够不到共享 `artifacts/` 目录,一并够不到。留了两条候选修法,未实现 | | ||
| 64 | | [43-task-decomposition-granularity.md](./todo/system-design/43-task-decomposition-granularity.md) | ✅ **Phase 0-4 全部已实现**(2026-08-31)。回应"org-planner 出 brd/prd/sdd 后,任务看板能不能展示成 task/sub-task 树,coder-validator 按清单逐条开发验收"。`tasks` 加 `acceptance_mode`/`parent_task_id`(后者复用既有列);`task-planning` 判断要不要拆(硬规则 5 开例外让 LLM 写这一个字段);org-planner 收尾把 `acceptance.md` 物化成子任务;`coder-validator` 新增 `task subtask-result` 工具逐条上报,`settleNode()` 新增闸门强制核对子任务全 `completed` 才认 success;看板加子任务展示。**落地时订正了一处原计划想岔的架构假设**:org-planner 在 org 层永远是 `subgraph` 节点(不是 agent_call),真跑数据显示它当前展开成的 `planner.json` 5 节点子图里,`goal-benchmark`/`planner-validator` 两节点常被 skip,`acceptance.md` 未必真的产出——钩子已改成扫子 run 全节点、找不到就优雅降级为 holistic,不阻塞主产出,但"子任务一定会长出来"这件事目前是尽力而为不是保证,见文档"实现记录"一节的遗留风险说明。全量单测 1120/1123 通过(3 个失败与本次改动无关,本次改动前即存在) | | 64 | | [43-task-decomposition-granularity.md](./todo/system-design/43-task-decomposition-granularity.md) | ✅ **Phase 0-4 全部已实现**(2026-08-31)。回应"org-planner 出 brd/prd/sdd 后,任务看板能不能展示成 task/sub-task 树,coder-validator 按清单逐条开发验收"。`tasks` 加 `acceptance_mode`/`parent_task_id`(后者复用既有列);`task-planning` 判断要不要拆(硬规则 5 开例外让 LLM 写这一个字段);org-planner 收尾把 `acceptance.md` 物化成子任务;`coder-validator` 新增 `task subtask-result` 工具逐条上报,`settleNode()` 新增闸门强制核对子任务全 `completed` 才认 success;看板加子任务展示。**落地时订正了一处原计划想岔的架构假设**:org-planner 在 org 层永远是 `subgraph` 节点(不是 agent_call),真跑数据显示它当前展开成的 `planner.json` 5 节点子图里,`goal-benchmark`/`planner-validator` 两节点常被 skip,`acceptance.md` 未必真的产出——钩子已改成扫子 run 全节点、找不到就优雅降级为 holistic,不阻塞主产出,但"子任务一定会长出来"这件事目前是尽力而为不是保证,见文档"实现记录"一节的遗留风险说明。全量单测 1120/1123 通过(3 个失败与本次改动无关,本次改动前即存在) | |
| 65 | 65 | ||
| 66 | ## todo/future/(暂缓,非当前重心) | 66 | ## todo/future/(暂缓,非当前重心) |
docs/todo/system-design/44-writes-artifacts-produces-gate-mismatch.md renamed to docs/done/system-design/44-writes-artifacts-produces-gate-mismatch.md
| 1 | # 44 · `writesArtifacts` 节点的产出闸门永远打不过:写到共享区,闸门查的是私有目录 | 1 | # 44 · `writesArtifacts` 节点的产出闸门永远打不过:写到共享区,闸门查的是私有目录 |
| 2 | 2 | ||
| 3 | -> **状态**: 📝 真实回归中发现的缺陷记录,已诊断到根因,**未修复**(本文档只记录,不含实现) | 3 | +> **状态**: ✅ **已修复**(2026-08-31)。按下面「四、建议修法」的方案 1 实现:`readDeclaredProduces` |
| 4 | +> 的 `allowedRoots` 扩到 `runRootDir(run)/artifacts/` 共享区(`graph-engine.js`),`goal-benchmark` | ||
| 5 | +> 的 `agent.md`/`tools.md` 补上"写完共享产物后也在自己节点私有目录声明一份 `contract.json`" | ||
| 6 | +> 这一步。回归测试见 `graph-files-index.test.js` 的用例 ⑭ | ||
| 4 | > **日期**: 2026-08-31 | 7 | > **日期**: 2026-08-31 |
| 5 | -> **发现场景**:[43 号文档](./43-task-decomposition-granularity.md) Phase 2 的真实端到端验证——新建 `task-decomp-verify-co` 公司,走一条真实 `itemized` 任务,org-planner 展开成 `system/work-graph-templates/planner.json` 那张 5 节点子图(真实 Claude CLI,非 mock) | 8 | +> **发现场景**:[43 号文档](../../todo/system-design/43-task-decomposition-granularity.md) Phase 2 的真实端到端验证——新建 `task-decomp-verify-co` 公司,走一条真实 `itemized` 任务,org-planner 展开成 `system/work-graph-templates/planner.json` 那张 5 节点子图(真实 Claude CLI,非 mock) |
| 6 | > **与 43 号文档的关系**:**完全独立的缺陷**,跟 goal 分解粒度那件事没有任何关系,只是恰好在验证 43 号文档时撞见。43 号文档"实现记录"一节提到的"acceptance.md 未必可靠",这份文档是那句话背后更具体、更确定的根因之一 | 9 | > **与 43 号文档的关系**:**完全独立的缺陷**,跟 goal 分解粒度那件事没有任何关系,只是恰好在验证 43 号文档时撞见。43 号文档"实现记录"一节提到的"acceptance.md 未必可靠",这份文档是那句话背后更具体、更确定的根因之一 |
| 7 | 10 | ||
| 8 | --- | 11 | --- |
| @@ -81,7 +84,7 @@ system/work-graph-templates/planner.json:11: { ..., "writesArtifacts": true } | @@ -81,7 +84,7 @@ system/work-graph-templates/planner.json:11: { ..., "writesArtifacts": true } | ||
| 81 | 84 | ||
| 82 | ### 2.4 已有的逃生口(R-19 产出索引的显式声明机制)够不到共享区 | 85 | ### 2.4 已有的逃生口(R-19 产出索引的显式声明机制)够不到共享区 |
| 83 | 86 | ||
| 84 | -`graph-engine.js` 已经有一条为**同一类**问题准备的机制——`readDeclaredProduces`(`graph-engine.js:4377-4406`):节点可以在自己私有目录写一份 `contract.json`,里面显式声明 `produces: {kind: ["<绝对路径>"]}`,闸门会认这份声明而不是死磕目录扫描。这正是 [41 号文档](./41-graph-vs-fde-reliability-discussion.md) 提到的"R-19 产出索引不覆盖项目根,加 `contract.json` 的 `produces` 显式声明机制"那次修复的产物。 | 87 | +`graph-engine.js` 已经有一条为**同一类**问题准备的机制——`readDeclaredProduces`(`graph-engine.js:4377-4406`):节点可以在自己私有目录写一份 `contract.json`,里面显式声明 `produces: {kind: ["<绝对路径>"]}`,闸门会认这份声明而不是死磕目录扫描。这正是 [41 号文档](../../todo/system-design/41-graph-vs-fde-reliability-discussion.md) 提到的"R-19 产出索引不覆盖项目根,加 `contract.json` 的 `produces` 显式声明机制"那次修复的产物。 |
| 85 | 88 | ||
| 86 | 但这条逃生口本身也够不到共享 `artifacts/` 目录——`inBounds()` 校验的 `allowedRoots` 只有 `[节点私有目录, 项目根]`(`graph-engine.js:4384`),不包含 `runRootDir(run)/artifacts/`。即使 `goal-benchmark` 主动在自己目录写一份 `contract.json` 声明 `produces.acceptance = ["/abs/path/to/artifacts/goal-benchmark.json"]`,这条声明也会因为路径不在 `allowedRoots` 里被判 `bad=true`,等于没声明。 | 89 | 但这条逃生口本身也够不到共享 `artifacts/` 目录——`inBounds()` 校验的 `allowedRoots` 只有 `[节点私有目录, 项目根]`(`graph-engine.js:4384`),不包含 `runRootDir(run)/artifacts/`。即使 `goal-benchmark` 主动在自己目录写一份 `contract.json` 声明 `produces.acceptance = ["/abs/path/to/artifacts/goal-benchmark.json"]`,这条声明也会因为路径不在 `allowedRoots` 里被判 `bad=true`,等于没声明。 |
| 87 | 90 | ||
| @@ -91,7 +94,7 @@ system/work-graph-templates/planner.json:11: { ..., "writesArtifacts": true } | @@ -91,7 +94,7 @@ system/work-graph-templates/planner.json:11: { ..., "writesArtifacts": true } | ||
| 91 | 94 | ||
| 92 | **任何一家公司只要 org-planner 走 `planner.json` 骨架(当前的常态,见 43 号文档 Phase 2 的真实数据核实),`org-planner-benchmark` 这一步几乎注定会撞这堵墙。** 每次撞墙都沿兜底回退边退回 `org-planner-sdd` 重来一轮(烧一次 sdd-planning 的真实调用 + 一次 goal-benchmark 的真实调用),直到 `loop_count` 撞到 `planner.json` 兜底边的硬顶(`LOOP_HARD_CAP_RUN` 相关,全树 `backward_count` 硬顶 5),最终大概率被强制判 `blocked` 或者 `planner-validator` 跟着一起被跳过。 | 95 | **任何一家公司只要 org-planner 走 `planner.json` 骨架(当前的常态,见 43 号文档 Phase 2 的真实数据核实),`org-planner-benchmark` 这一步几乎注定会撞这堵墙。** 每次撞墙都沿兜底回退边退回 `org-planner-sdd` 重来一轮(烧一次 sdd-planning 的真实调用 + 一次 goal-benchmark 的真实调用),直到 `loop_count` 撞到 `planner.json` 兜底边的硬顶(`LOOP_HARD_CAP_RUN` 相关,全树 `backward_count` 硬顶 5),最终大概率被强制判 `blocked` 或者 `planner-validator` 跟着一起被跳过。 |
| 93 | 96 | ||
| 94 | -**这很可能正是本 session 全程调试的参考任务(snakegame-co,[43 号文档](./43-task-decomposition-granularity.md) 的触发场景)里 `goal-benchmark`/`planner-validator` 两个节点显示 `skipped` 的真实原因**——不是没跑到,是跑了、正确地写了产物,却被这道闸门反复判定「没写」,最终在某一轮被放弃跳过。 | 97 | +**这很可能正是本 session 全程调试的参考任务(snakegame-co,[43 号文档](../../todo/system-design/43-task-decomposition-granularity.md) 的触发场景)里 `goal-benchmark`/`planner-validator` 两个节点显示 `skipped` 的真实原因**——不是没跑到,是跑了、正确地写了产物,却被这道闸门反复判定「没写」,最终在某一轮被放弃跳过。 |
| 95 | 98 | ||
| 96 | 连带后果: | 99 | 连带后果: |
| 97 | 100 |
docs/todo/system-design/41-graph-vs-fde-reliability-discussion.md
| @@ -351,7 +351,18 @@ FDE 无关,纯粹为了让 org-hunt 做真调研)反复 `rerun` 同一个 ta | @@ -351,7 +351,18 @@ FDE 无关,纯粹为了让 org-hunt 做真调研)反复 `rerun` 同一个 ta | ||
| 351 | **已被另一并行会话实现**,见 [39-boss-agent-task-supervision.md](./39-boss-agent-task-supervision.md) | 351 | **已被另一并行会话实现**,见 [39-boss-agent-task-supervision.md](./39-boss-agent-task-supervision.md) |
| 352 | (P1-P5,commit `a00590a`/`5108d67`/`d967ac0`),本文档这条 TODO 作废,后续跟进请去那份 | 352 | (P1-P5,commit `a00590a`/`5108d67`/`d967ac0`),本文档这条 TODO 作废,后续跟进请去那份 |
| 353 | 文档的「六、已知遗留」一节。 | 353 | 文档的「六、已知遗留」一节。 |
| 354 | -- [ ] `routeFailure` 的 `infra_flake` 分支补上 `target_node` 路由(30 号文档遗留的"刻意没做②")。 | 354 | +- [x] ~~`routeFailure` 的 `infra_flake` 分支补上 `target_node` 路由(30 号文档遗留的"刻意没做②")~~—— |
| 355 | + **已按最保守的方案实现**(2026-08-31,`graph-engine.js` 的 `infraFlakeTargetEdge`): | ||
| 356 | + `target_node` 只有在图上真有一条从本节点出发的 **backward** fail 边指向那个节点时才 | ||
| 357 | + 生效,命中就改走正常的 backward 边路由(计入 backward_count/双层熔断),不再享受 | ||
| 358 | + "原地重跑、只受 infra_flake_count 约束"的免账本待遇;没命中(含没声明)维持原有的 | ||
| 359 | + 原地重跑行为不变。**这条修复堵不住 30 号文档记录的那次真实事故**——事故里 | ||
| 360 | + `org-coder-test` 点名的 `org-coder-validator` 是它的**下游**,而 `org-graph-service.js` | ||
| 361 | + 规则 4(`backward-labeled`)从图结构上就不允许 backward 边指向非祖先节点,那次事故 | ||
| 362 | + 本来就不是"图上有边但引擎不认",是"诊断内容在打一个图结构不允许表达的主意"—— | ||
| 363 | + 已经靠 `INFRA_FLAKE_HARD_CAP` 熔断收敛,属于本条修复范围之外。这条修复堵的是更窄的 | ||
| 364 | + 另一种情形:诊断点名的目标恰好是本节点自己真实存在的祖先时,不该绕开正常的回退 | ||
| 365 | + 记账。回归测试见 `graph-engine.test.js` 的 `infra_flake` describe 块新增两条用例。 | ||
| 355 | - [ ] `27-p4-parallel-and-artifacts.md` 并发 fan-out 设计推进。 | 366 | - [ ] `27-p4-parallel-and-artifacts.md` 并发 fan-out 设计推进。 |
| 356 | - [ ] 若要落地部门级 org graph(37 号文档方向),仍是"设计讨论,尚未立项"状态。 | 367 | - [ ] 若要落地部门级 org graph(37 号文档方向),仍是"设计讨论,尚未立项"状态。 |
| 357 | - [ ] **可选文档补丁**:`org-hunt/digest/evolve` 是"元 agent 不能直接入图,要建原生协议独立副本" | 368 | - [ ] **可选文档补丁**:`org-hunt/digest/evolve` 是"元 agent 不能直接入图,要建原生协议独立副本" |
docs/todo/system-design/43-task-decomposition-granularity.md
| @@ -162,4 +162,4 @@ Phase 2 排在 Phase 3 之前,且内部先做"acceptance.md 是否真的可靠 | @@ -162,4 +162,4 @@ Phase 2 排在 Phase 3 之前,且内部先做"acceptance.md 是否真的可靠 | ||
| 162 | 162 | ||
| 163 | **遗留风险**(订正后依然成立,写实话):`acceptance.md` 会不会被稳定产出,取决于 `workgraph-planning` 会不会给 org-planner 挑一张真正跑完 `goal-benchmark`/`planner-validator` 的图、以及那张图会不会真的产出这份文件——这两件事都不在本次改动范围内,`maybeMaterializeItemizedSubtasks()` 目前是**优雅降级**(找不到就跳过,不报错),不是**保证生效**。换句话说:`acceptance_mode='itemized'` 现在是"如果规划链路配合,就能自动长出子任务",还没做到"一定会长出子任务"。 | 163 | **遗留风险**(订正后依然成立,写实话):`acceptance.md` 会不会被稳定产出,取决于 `workgraph-planning` 会不会给 org-planner 挑一张真正跑完 `goal-benchmark`/`planner-validator` 的图、以及那张图会不会真的产出这份文件——这两件事都不在本次改动范围内,`maybeMaterializeItemizedSubtasks()` 目前是**优雅降级**(找不到就跳过,不报错),不是**保证生效**。换句话说:`acceptance_mode='itemized'` 现在是"如果规划链路配合,就能自动长出子任务",还没做到"一定会长出子任务"。 |
| 164 | 164 | ||
| 165 | -**2026-08-31 真实回归验证坐实了这条风险**,而且找到了比"未必可靠"更具体的根因:见 [44 号文档](./44-writes-artifacts-produces-gate-mismatch.md)——`goal-benchmark` 按设计把产物写进 root run 共享 `artifacts/` 目录,但产出闸门只查节点私有目录,`writesArtifacts:true` 这个字段全仓库零代码读取,这条契约在当前实现下**结构性地**打不过,不是运气不好。这很可能正是本文档触发场景(snakegame-co)里 `goal-benchmark`/`planner-validator` 显示 `skipped` 的真实原因。44 号文档只记录诊断,未修复;修复它会提高 Phase 2 的实际命中率,但不在 43 号文档范围内,不在这里展开。 | 165 | +**2026-08-31 真实回归验证坐实了这条风险**,而且找到了比"未必可靠"更具体的根因:见 [44 号文档](../../done/system-design/44-writes-artifacts-produces-gate-mismatch.md)——`goal-benchmark` 按设计把产物写进 root run 共享 `artifacts/` 目录,但产出闸门只查节点私有目录,`writesArtifacts:true` 这个字段全仓库零代码读取,这条契约在当前实现下**结构性地**打不过,不是运气不好。这很可能正是本文档触发场景(snakegame-co)里 `goal-benchmark`/`planner-validator` 显示 `skipped` 的真实原因。44 号文档已修复(同日),会提高 Phase 2 的实际命中率,但验证不在 43 号文档范围内,不在这里展开。 |
server/services/graph-engine.js
| @@ -2016,11 +2016,55 @@ function selectFailEdge(def, defNodeId, failReason) { | @@ -2016,11 +2016,55 @@ function selectFailEdge(def, defNodeId, failReason) { | ||
| 2016 | return candidates.find((e) => e.failReason === null || e.failReason === undefined || e.failReason === '') || null; | 2016 | return candidates.find((e) => e.failReason === null || e.failReason === undefined || e.failReason === '') || null; |
| 2017 | } | 2017 | } |
| 2018 | 2018 | ||
| 2019 | +/** | ||
| 2020 | + * 41 号文档修复:infra_flake 诊断里带的 `target_node`(`fail_node_ref` 解出的 `workNode`), | ||
| 2021 | + * 只有在图上真有一条从本节点出发的 **backward** fail 边指向那个节点时才算数——边赢, | ||
| 2022 | + * 声明只是主张(同 `resolveCrossLayerTarget` 的设计哲学:地址与路由打架时以图上真实 | ||
| 2023 | + * 存在的边为准)。 | ||
| 2024 | + * | ||
| 2025 | + * 只认 `backward` 边有两层原因: | ||
| 2026 | + * 1. 只有 backward 边享有"计入 backward_count、受双层硬顶约束"这份安全网——forward | ||
| 2027 | + * 的 fail 边(比如"失败转人工审核")走的是 `markReady` 直接点亮,没有熔断线; | ||
| 2028 | + * 认 forward 边等于又开一条不受任何计数约束的重定向通道,跟这条修复本来要堵的 | ||
| 2029 | + * 口子是同一类风险。 | ||
| 2030 | + * 2. `org-graph-service.js` 规则 4(`backward-labeled`)从图结构上禁止声明"目标不是 | ||
| 2031 | + * 源节点祖先"的 backward 边(校验判据是"去掉这条边后目标是否仍能沿 forward 边 | ||
| 2032 | + * 回到源节点")——也就是说,backward 边的目标**必然是**当前节点的祖先,因此 | ||
| 2033 | + * `handleBackwardEdge` 第③步"重置目标 + 它的 forward 下游"必然会把当前这个 | ||
| 2034 | + * 诊断节点自己也一并重置回 pending(它就在自己声称的祖先的下游),不会有节点 | ||
| 2035 | + * 卡死在 fail 里出不来。 | ||
| 2036 | + * | ||
| 2037 | + * **这条修复堵不住 30 号文档那次真实事故**:`org-coder-test` 当时点名的 | ||
| 2038 | + * `org-coder-validator` 是它的**下游**(`executor.json` 里 test→validator 是 forward | ||
| 2039 | + * 边),根据上面第 2 点,图模型里根本不可能存在一条合法的 backward 边指向下游节点—— | ||
| 2040 | + * 这次事故本来就不属于"图上有边但引擎不认",而是"诊断内容本身在打一个图结构不允许 | ||
| 2041 | + * 表达的主意"。那次事故已经靠 `INFRA_FLAKE_HARD_CAP` 熔断收敛(见上面两条 §六 9c | ||
| 2042 | + * 用例),这条修复堵的是另一种更窄的情形:诊断点名的目标恰好是本节点自己真实存在 | ||
| 2043 | + * 的祖先时,不再享受"原地重跑、只受 infra_flake_count 约束"的免账本待遇,改走跟别的 | ||
| 2044 | + * 失败一样受 backward_count/双层硬顶约束的正常路由。 | ||
| 2045 | + * | ||
| 2046 | + * @returns {object|null} 命中的边,或 null(没声明 / 声明的节点没有对应边) | ||
| 2047 | + */ | ||
| 2048 | +function infraFlakeTargetEdge(def, nodeRun) { | ||
| 2049 | + const ref = parseNodeRef(nodeRun.fail_node_ref); | ||
| 2050 | + if (!ref || !ref.workNode) return null; | ||
| 2051 | + return def.edges.find((e) => { | ||
| 2052 | + if (e.from !== nodeRun.def_node_id || e.condition !== 'fail' || e.edgeKind !== 'backward') return false; | ||
| 2053 | + const target = def.nodes.get(e.to); | ||
| 2054 | + return !!target && target.name === ref.workNode; | ||
| 2055 | + }) || null; | ||
| 2056 | +} | ||
| 2057 | + | ||
| 2019 | function routeFailure(runId, def, nodeRun, out, remediationNote = null) { | 2058 | function routeFailure(runId, def, nodeRun, out, remediationNote = null) { |
| 2059 | + const infraFlakeEdge = nodeRun.fail_reason === INFRA_FLAKE_REASON | ||
| 2060 | + ? infraFlakeTargetEdge(def, nodeRun) : null; | ||
| 2061 | + | ||
| 2020 | // infra_flake 特例(§四.D.2.1 末段):基础设施抖动直接**原地重跑**,不选边、不写 | 2062 | // infra_flake 特例(§四.D.2.1 末段):基础设施抖动直接**原地重跑**,不选边、不写 |
| 2021 | // loop record、不加 backward_count、不扣该节点的 retry 预算(attempt 也不加一)。 | 2063 | // loop record、不加 backward_count、不扣该节点的 retry 预算(attempt 也不加一)。 |
| 2022 | // 终止性由 §四.A.3 的任务级总预算兜底——每次重跑仍算一次 agent 调用,池子空了整棵树挂起。 | 2064 | // 终止性由 §四.A.3 的任务级总预算兜底——每次重跑仍算一次 agent 调用,池子空了整棵树挂起。 |
| 2023 | - if (nodeRun.fail_reason === INFRA_FLAKE_REASON) { | 2065 | + // 41 号文档修复:这条快捷路径只在「没有可信的 target_node」时才走——诊断点名的目标 |
| 2066 | + // 若真有对应的 fail 边(infraFlakeEdge 非空),改走下面的正常路由,不再原地重跑自己。 | ||
| 2067 | + if (nodeRun.fail_reason === INFRA_FLAKE_REASON && !infraFlakeEdge) { | ||
| 2024 | const d = def.nodes.get(nodeRun.def_node_id) || {}; | 2068 | const d = def.nodes.get(nodeRun.def_node_id) || {}; |
| 2025 | const name = d.name || nodeRun.def_node_id; | 2069 | const name = d.name || nodeRun.def_node_id; |
| 2026 | 2070 | ||
| @@ -2058,7 +2102,20 @@ function routeFailure(runId, def, nodeRun, out, remediationNote = null) { | @@ -2058,7 +2102,20 @@ function routeFailure(runId, def, nodeRun, out, remediationNote = null) { | ||
| 2058 | return; | 2102 | return; |
| 2059 | } | 2103 | } |
| 2060 | 2104 | ||
| 2061 | - const edge = selectFailEdge(def, nodeRun.def_node_id, nodeRun.fail_reason); | 2105 | + if (infraFlakeEdge) { |
| 2106 | + const d = def.nodes.get(nodeRun.def_node_id) || {}; | ||
| 2107 | + const targetName = (def.nodes.get(infraFlakeEdge.to) || {}).name || infraFlakeEdge.to; | ||
| 2108 | + sse.emitEvent(runId, 'node.triage', | ||
| 2109 | + `infra_flake 诊断指名的目标 ${targetName} 在图上有对应 fail 边,按正常回退路由处理(计入 backward_count/双层熔断),不再原地重跑 ${d.name || nodeRun.def_node_id} 自己`, | ||
| 2110 | + { node_run_id: nodeRun.id, node_name: d.name || nodeRun.def_node_id, | ||
| 2111 | + target_node_ref: nodeRun.fail_node_ref, edge_id: infraFlakeEdge.id }, | ||
| 2112 | + { ref: nodeRun.id }); | ||
| 2113 | + } | ||
| 2114 | + | ||
| 2115 | + // infraFlakeEdge 命中时直接用它,不再交给 selectFailEdge 按 failReason 重新挑一遍—— | ||
| 2116 | + // 已经验证过这条边的目标就是诊断点名的那个节点,不该再被"有没有精确匹配 infra_flake | ||
| 2117 | + // 这个 failReason 字符串"的兜底规则挑到另一条边去。 | ||
| 2118 | + const edge = infraFlakeEdge || selectFailEdge(def, nodeRun.def_node_id, nodeRun.fail_reason); | ||
| 2062 | 2119 | ||
| 2063 | // 死路:没有可走的 fail 边 → 节点 blocked,等人工介入(区别于人工主动「停止」)。 | 2120 | // 死路:没有可走的 fail 边 → 节点 blocked,等人工介入(区别于人工主动「停止」)。 |
| 2064 | // 判词的收件人是**它自己**:人工「重试」把它打回 ready 之后(retryNode 不动这一列), | 2121 | // 判词的收件人是**它自己**:人工「重试」把它打回 ready 之后(retryNode 不动这一列), |
| @@ -4367,10 +4424,17 @@ const { companyRootDir, taskDirRel } = require('./company-paths'); | @@ -4367,10 +4424,17 @@ const { companyRootDir, taskDirRel } = require('./company-paths'); | ||
| 4367 | * 采信一份可能指错地方/指向不存在文件的声明(39 号文档确认的设计取向:静默放过比 | 4424 | * 采信一份可能指错地方/指向不存在文件的声明(39 号文档确认的设计取向:静默放过比 |
| 4368 | * 显式报错更难排查)。 | 4425 | * 显式报错更难排查)。 |
| 4369 | * 1. 路径必须是绝对路径 | 4426 | * 1. 路径必须是绝对路径 |
| 4370 | - * 2. 路径必须落在「项目根」或「本节点私有目录」之内——不能是引擎仓库根,也不能是 | ||
| 4371 | - * 别的节点私有目录(那是真正的越界读写,不该被一份自称的声明放行) | 4427 | + * 2. 路径必须落在「项目根」「本节点私有目录」或「root run 共享 artifacts/ 目录」之内 |
| 4428 | + * ——不能是引擎仓库根,也不能是别的节点私有目录(那是真正的越界读写,不该被 | ||
| 4429 | + * 一份自称的声明放行) | ||
| 4372 | * 3. 文件必须真实存在且是一个普通文件 | 4430 | * 3. 文件必须真实存在且是一个普通文件 |
| 4373 | * | 4431 | * |
| 4432 | + * 44 号文档修复:`writesArtifacts: true` 的节点(如 goal-benchmark)按设计把产物写进 | ||
| 4433 | + * `runRootDir(run)/artifacts/` 共享区(多个节点合成同一份 acceptance.json 的地方,天然 | ||
| 4434 | + * 不可能落在某个节点自己的私有目录),此前这里的 allowedRoots 够不到那个目录,导致这类 | ||
| 4435 | + * 节点即使自己在私有目录写了合法的 contract.json 声明,也会因为声明的路径「越界」被判 | ||
| 4436 | + * 「没产出」——闸门在当前实现下永远打不过,不是 agent 表现不好。 | ||
| 4437 | + * | ||
| 4374 | * @returns {Map<string, string[]|null>} kind -> 校验通过的绝对路径数组; | 4438 | * @returns {Map<string, string[]|null>} kind -> 校验通过的绝对路径数组; |
| 4375 | * 校验不通过的 kind 映到 `null` | 4439 | * 校验不通过的 kind 映到 `null` |
| 4376 | */ | 4440 | */ |
| @@ -4381,7 +4445,7 @@ function readDeclaredProduces(run, dir) { | @@ -4381,7 +4445,7 @@ function readDeclaredProduces(run, dir) { | ||
| 4381 | if (!produces || typeof produces !== 'object') return out; | 4445 | if (!produces || typeof produces !== 'object') return out; |
| 4382 | 4446 | ||
| 4383 | const projectRoot = readProjectRootRecord(run); | 4447 | const projectRoot = readProjectRootRecord(run); |
| 4384 | - const allowedRoots = [dir, projectRoot && projectRoot.root_path] | 4448 | + const allowedRoots = [dir, projectRoot && projectRoot.root_path, path.join(runRootDir(run), 'artifacts')] |
| 4385 | .filter(Boolean) | 4449 | .filter(Boolean) |
| 4386 | .map((p) => path.resolve(p)); | 4450 | .map((p) => path.resolve(p)); |
| 4387 | const inBounds = (abs) => allowedRoots.some((root) => abs === root || abs.startsWith(root + path.sep)); | 4451 | const inBounds = (abs) => allowedRoots.some((root) => abs === root || abs.startsWith(root + path.sep)); |
server/tests/unit/graph-engine.test.js
| @@ -943,6 +943,85 @@ describe('graph-engine', () => { | @@ -943,6 +943,85 @@ describe('graph-engine', () => { | ||
| 943 | expect(wdb.getRun(runId).backward_count).toBe(0); | 943 | expect(wdb.getRun(runId).backward_count).toBe(0); |
| 944 | expect(wdb.getRun(runId).run_status).toBe('blocked'); | 944 | expect(wdb.getRun(runId).run_status).toBe('blocked'); |
| 945 | }); | 945 | }); |
| 946 | + | ||
| 947 | + // 41 号文档修复:target_node 只有在图上真有一条从本节点出发的 backward fail 边指向 | ||
| 948 | + // 那个节点时才生效——边赢,声明只是主张。 | ||
| 949 | + // | ||
| 950 | + // ⚠️ 图校验的"backward-labeled"规则(org-graph-service.js 规则 4)要求:backward 边 | ||
| 951 | + // 必须真的构成回路,也就是**目标必须是源节点的祖先**(去掉这条边后,目标仍能沿 forward | ||
| 952 | + // 边回到源节点)。这意味着 30 号文档那次真实事故——org-coder-test 点名 target_node= | ||
| 953 | + // org-coder-validator(validator 是 test 的**下游**,不是祖先)——在这套图模型里**永远 | ||
| 954 | + // 不可能**存在一条合法的 backward 边指向它,不管这条修复怎么写都堵不住/也不需要堵, | ||
| 955 | + // 因为图本身就不允许声明这种边(那次事故按原有行为处理,即上面两条 §六 9c 用例: | ||
| 956 | + // 靠 infra_flake_count 硬顶收敛,不是本条修复要覆盖的场景)。 | ||
| 957 | + // | ||
| 958 | + // 本条修复真正堵的是**另一种**情形:target_node 点名的节点恰好是本节点自己真实存在的 | ||
| 959 | + // 祖先(比如 test 对着它本来就有的 test→impl 退回边,点名 target_node=impl)——命中时 | ||
| 960 | + // 不再走"原地重跑诊断节点自己、只受 infra_flake_count 约束"这条免账本快捷路径,改走 | ||
| 961 | + // 正常的 backward 边路由(计入 backward_count、受双层硬顶约束),跟别的失败一视同仁。 | ||
| 962 | + it('target_node 点名的是自己真实存在的祖先(不是描述里那种指向下游的情形):' | ||
| 963 | + + '命中对应 backward 边,不再原地重跑诊断节点自己,改走正常回退路由(计入 backward_count)', async () => { | ||
| 964 | + const companyId = nextCompany(); | ||
| 965 | + const { runId } = buildWorkRun({ | ||
| 966 | + companyId, | ||
| 967 | + nodes: ['impl', 'test', 'validator'], | ||
| 968 | + edges: [ | ||
| 969 | + { from: 'impl', to: 'test' }, | ||
| 970 | + { from: 'test', to: 'validator' }, | ||
| 971 | + { from: 'test', to: 'impl', condition: 'fail', edgeKind: 'backward' }, // 真实的 executor.json 拓扑 | ||
| 972 | + ], | ||
| 973 | + mode: 'auto', | ||
| 974 | + }); | ||
| 975 | + const calls = fakeExecutor({ | ||
| 976 | + impl: (nth) => (nth === 1 ? { status: 'success' } : { status: 'success' }), | ||
| 977 | + // 诊断点名的正是 test 自己那条已有 backward 边的目标:impl | ||
| 978 | + test: (nth) => (nth === 1 | ||
| 979 | + ? { status: 'fail', failReason: 'infra_flake', failNodeRef: { workNode: 'impl' } } | ||
| 980 | + : { status: 'success' }), | ||
| 981 | + validator: { status: 'success' }, | ||
| 982 | + }); | ||
| 983 | + | ||
| 984 | + await engine.startRun(runId, { mode: 'auto' }).driver; | ||
| 985 | + | ||
| 986 | + // 不是原地重跑 test 自己——test 的 forward 下游(含它自己)被 impl 那条回退重置, | ||
| 987 | + // impl 重新跑一遍带出新一轮 test/validator,test 总共被真实调用两次(不是"原地重跑") | ||
| 988 | + expect(calls.filter((c) => c.node === 'impl')).toHaveLength(2); | ||
| 989 | + expect(nodeByName(runId, 'validator').status).toBe('success'); | ||
| 990 | + // 走的是正常回退路由,不是 infra_flake 的免账本快捷路径 | ||
| 991 | + expect(wdb.listLoopRecords(runId)).toHaveLength(1); | ||
| 992 | + expect(wdb.getRun(runId).backward_count).toBe(1); | ||
| 993 | + }); | ||
| 994 | + | ||
| 995 | + it('target_node 没命中任何 backward fail 边(比如指向的节点是自己的下游而非祖先——' | ||
| 996 | + + '图模型本身就不允许这种边存在):维持原地重跑诊断节点自己,不静默改道', async () => { | ||
| 997 | + const companyId = nextCompany(); | ||
| 998 | + const { runId } = buildWorkRun({ | ||
| 999 | + companyId, | ||
| 1000 | + nodes: ['impl', 'test', 'validator'], | ||
| 1001 | + edges: [ | ||
| 1002 | + { from: 'impl', to: 'test' }, | ||
| 1003 | + { from: 'test', to: 'validator' }, | ||
| 1004 | + { from: 'test', to: 'impl', condition: 'fail', edgeKind: 'backward' }, // 真实的 executor.json 拓扑 | ||
| 1005 | + ], | ||
| 1006 | + mode: 'auto', | ||
| 1007 | + }); | ||
| 1008 | + const calls = fakeExecutor({ | ||
| 1009 | + impl: { status: 'success' }, | ||
| 1010 | + // 诊断点名 validator,但图上 test 的 fail 边只通向 impl——地址够不到(也永远不可能够到) | ||
| 1011 | + test: (nth) => (nth === 1 | ||
| 1012 | + ? { status: 'fail', failReason: 'infra_flake', failNodeRef: { workNode: 'validator' } } | ||
| 1013 | + : { status: 'success' }), | ||
| 1014 | + validator: { status: 'success' }, | ||
| 1015 | + }); | ||
| 1016 | + | ||
| 1017 | + await engine.startRun(runId, { mode: 'auto' }).driver; | ||
| 1018 | + | ||
| 1019 | + // 原地重跑了 test 自己(今天已有的行为,target_node 够不到就当没声明) | ||
| 1020 | + expect(calls.filter((c) => c.node === 'test')).toHaveLength(2); | ||
| 1021 | + expect(calls.filter((c) => c.node === 'validator')).toHaveLength(1); | ||
| 1022 | + expect(wdb.listLoopRecords(runId)).toHaveLength(0); | ||
| 1023 | + expect(wdb.getRun(runId).backward_count).toBe(0); | ||
| 1024 | + }); | ||
| 946 | }); | 1025 | }); |
| 947 | 1026 | ||
| 948 | // ────────────────────────────────────────────────────────────── | 1027 | // ────────────────────────────────────────────────────────────── |
server/tests/unit/graph-files-index.test.js
| @@ -678,4 +678,47 @@ describe('R-19 产物索引:graph_files / graph_edge_contracts', () => { | @@ -678,4 +678,47 @@ describe('R-19 产物索引:graph_files / graph_edge_contracts', () => { | ||
| 678 | 678 | ||
| 679 | try { fs.rmSync(outsideDir, { recursive: true, force: true }); } catch {} | 679 | try { fs.rmSync(outsideDir, { recursive: true, force: true }); } catch {} |
| 680 | }); | 680 | }); |
| 681 | + | ||
| 682 | + it('⑭ 44 号文档修复:声明的 produces 指向 root run 共享 artifacts/ 目录(不是节点私有目录/项目根)' | ||
| 683 | + + '——不再判越界,节点按 success 结算', async () => { | ||
| 684 | + const companyId = 'co-files-shared-artifacts'; | ||
| 685 | + const taskId = 'task-shared-artifacts'; | ||
| 686 | + const b = buildRun({ | ||
| 687 | + companyId, taskId, | ||
| 688 | + nodes: [{ nodeName: 'w-benchmark' }, { nodeName: 'w-validator' }], | ||
| 689 | + edges: [{ from: 'w-benchmark', to: 'w-validator', condition: 'success' }], | ||
| 690 | + }); | ||
| 691 | + wdb.declareEdgeContract({ | ||
| 692 | + edgeLayer: 'work', edgeId: b.edgeId('w-benchmark', 'w-validator'), kind: 'acceptance', required: true, | ||
| 693 | + }); | ||
| 694 | + | ||
| 695 | + engine.setAgentExecutor(async (ctx) => { | ||
| 696 | + if (ctx.nodeName !== 'w-benchmark') return { status: 'success' }; | ||
| 697 | + // 产物写进共享区,不是节点私有目录——goal-benchmark 真实的写法 | ||
| 698 | + const sharedDir = path.join(runsDir, ctx.run.root_run_id, 'artifacts'); | ||
| 699 | + fs.mkdirSync(sharedDir, { recursive: true }); | ||
| 700 | + const sharedPath = path.join(sharedDir, 'goal-benchmark.json'); | ||
| 701 | + fs.writeFileSync(sharedPath, JSON.stringify({ dimensions: [] })); | ||
| 702 | + | ||
| 703 | + const dir = nodeDir(ctx.run.root_run_id, ctx.nodeRunId); | ||
| 704 | + fs.mkdirSync(dir, { recursive: true }); | ||
| 705 | + // 声明指向共享区绝对路径——44 号文档修复前,这条声明会被判「越界」 | ||
| 706 | + fs.writeFileSync(path.join(dir, 'contract.json'), JSON.stringify({ | ||
| 707 | + produces: { acceptance: sharedPath }, | ||
| 708 | + })); | ||
| 709 | + return { status: 'success', outputRef: dir }; | ||
| 710 | + }); | ||
| 711 | + try { | ||
| 712 | + await engine.advanceOne(b.runId); | ||
| 713 | + await engine.advanceOne(b.runId); | ||
| 714 | + } finally { | ||
| 715 | + engine.setAgentExecutor(null); | ||
| 716 | + } | ||
| 717 | + | ||
| 718 | + const state = wdb.getRunState(b.runId); | ||
| 719 | + const benchmark = state.nodes.find((n) => n.name === 'w-benchmark'); | ||
| 720 | + expect(benchmark.failReason).toBeFalsy(); | ||
| 721 | + expect(benchmark.status).toBe('success'); | ||
| 722 | + expect(state.nodes.find((n) => n.name === 'w-validator').status).toBe('success'); | ||
| 723 | + }); | ||
| 681 | }); | 724 | }); |
system/agents/goal-benchmark/agent.md
| @@ -156,6 +156,23 @@ node server/services/goal-contract.js validate --run-dir $R # 必须退出 0 | @@ -156,6 +156,23 @@ node server/services/goal-contract.js validate --run-dir $R # 必须退出 0 | ||
| 156 | `--ref` 指的是 `artifacts/` 下的相对路径(大块内容外置,契约里存 `{"$ref": "goal-benchmark.json"}`)。 | 156 | `--ref` 指的是 `artifacts/` 下的相对路径(大块内容外置,契约里存 `{"$ref": "goal-benchmark.json"}`)。 |
| 157 | `validate` 退出码:`0` 合法 / `1` 契约非法(照它打印的 `[rule] detail` 改)/ `2` 压根没契约或参数错。 | 157 | `validate` 退出码:`0` 合法 / `1` 契约非法(照它打印的 `[rule] detail` 改)/ `2` 压根没契约或参数错。 |
| 158 | 158 | ||
| 159 | +### 2.5) 图执行侧的产出闸门:自己节点目录也要写一份 `contract.json`(44 号文档修复) | ||
| 160 | + | ||
| 161 | +上面第 2 步挂进的是**验收契约**(`acceptance.json`,给 `benchmark-validator` 判卷用);这一步 | ||
| 162 | +是**引擎的产出闸门**(`graph-engine.js` 的 `unsatisfiedRequiredProduces`),两回事,缺一不可。 | ||
| 163 | + | ||
| 164 | +你写进共享区的 `artifacts/goal-benchmark.json` 天生不在你自己的节点私有目录里(三个节点合成 | ||
| 165 | +同一份 `acceptance.json`,只能落共享区)——引擎的产出闸门默认只扫节点私有目录,扫不到就会把 | ||
| 166 | +这次明明成功的执行强制判 `fail_reason: produces_missing`,沿兜底边打回 `org-planner-sdd` 重来。 | ||
| 167 | +唯一的解法是显式声明:在 `runs/{rootRunId}/nodes/{你自己的 nodeRunId}/contract.json` 写 | ||
| 168 | + | ||
| 169 | +```json | ||
| 170 | +{ "produces": { "acceptance": "<runs/{rootRunId}/artifacts/goal-benchmark.json 的绝对路径>" } } | ||
| 171 | +``` | ||
| 172 | + | ||
| 173 | +必须是绝对路径,且必须在第 1 步真写完那份文件**之后**再写这份声明(引擎会去校验文件是否 | ||
| 174 | +真实存在)。漏了这一步,哪怕维度表内容完全正确,这个节点也永远过不了闸门。 | ||
| 175 | + | ||
| 159 | ### 3) 自己的节点目录:`attempt-{n}.json`(引擎读它判本节点成功/失败) | 176 | ### 3) 自己的节点目录:`attempt-{n}.json`(引擎读它判本节点成功/失败) |
| 160 | 177 | ||
| 161 | ```jsonc | 178 | ```jsonc |
system/agents/goal-benchmark/tools.md
| @@ -25,6 +25,16 @@ ls $R/artifacts/ # 别人已ç»å†™äº†å“ªå‡ | @@ -25,6 +25,16 @@ ls $R/artifacts/ # 别人已ç»å†™äº†å“ªå‡ | ||
| 25 | 用文件写入工具写 `$R/artifacts/goal-benchmark.json`ï¼ˆå½¢çŠ¶è§ `agent.md`)。 | 25 | 用文件写入工具写 `$R/artifacts/goal-benchmark.json`ï¼ˆå½¢çŠ¶è§ `agent.md`)。 |
| 26 | **è¿™æ˜¯ä½ å”¯ä¸€æœ‰æƒå†™è¿›å…±äº«åŒºçš„æ–‡ä»¶**(`writesArtifacts: true` åªç»™äº†ä½ è¿™ä¸€æ ¼ç‰¹æƒï¼‰ã€‚ | 26 | **è¿™æ˜¯ä½ å”¯ä¸€æœ‰æƒå†™è¿›å…±äº«åŒºçš„æ–‡ä»¶**(`writesArtifacts: true` åªç»™äº†ä½ è¿™ä¸€æ ¼ç‰¹æƒï¼‰ã€‚ |
| 27 | 27 | ||
| 28 | +写完**立刻**åœ¨ä½ è‡ªå·±çš„èŠ‚ç‚¹ç§æœ‰ç›®å½•å†å†™ä¸€ä»½ `contract.json`(44 å·æ–‡æ¡£ä¿®å¤çš„一æ¥ï¼Œåˆ«æ¼ï¼‰ï¼š | ||
| 29 | + | ||
| 30 | +```json | ||
| 31 | +{ "produces": { "acceptance": "<$R/artifacts/goal-benchmark.json çš„ç»å¯¹è·¯å¾„>" } } | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +写到 `$R/nodes/<ä½ çš„ nodeRunId>/contract.json`。**必须是ç»å¯¹è·¯å¾„**——引擎按这份声明去共享区 | ||
| 35 | +找文件,ä¸å£°æ˜Žæˆ–声明æˆç›¸å¯¹è·¯å¾„ï¼Œä½ è¿™ä¸€æ¥ä¼šè¢«åˆ¤ã€Œæ²¡äº§å‡ºã€å¼ºåˆ¶å›žé€€é‡æ¥ï¼ˆå“ªæ€•ç»´åº¦è¡¨å·²ç» | ||
| 36 | +æ£ç¡®å†™è¿›äº†å…±äº«åŒºï¼‰ã€‚ | ||
| 37 | + | ||
| 28 | ## 3. 挂进契约 + æ ¡éªŒï¼ˆ`server/services/goal-contract.js` çš„ CLI) | 38 | ## 3. 挂进契约 + æ ¡éªŒï¼ˆ`server/services/goal-contract.js` çš„ CLI) |
| 29 | 39 | ||
| 30 | ```bash | 40 | ```bash |