48 · Task/Subtask 验收契约 1:1 强绑定
状态: 📝 讨论收敛,未实现。 日期: 2026-09-02 触发场景: 46 号文档 Phase 3 v2 真实回归时,
org-validator在轻量子任务图(org-coder实现 →org-validator验证,跳过公司完整 org graph 的org-planner规划仪式)里卡死——goal-contract.js validate --run-dir报input_underspecified(没有acceptance.json),org-validator按它自己的硬指令"退org-planner重跑",但这张轻量图里压根没有org-planner节点,退无可退。追问下去牵出一个更根本的问题:"验收契约由谁产出"这件事目前完全绑死在"公司完整 org graph 一定会跑到org-planner"这个假设上,任何不满足这个假设的执行路径都会撞上同一类死锁。
讨论纪要(结构化复述,不是单条原话,是几轮讨论收敛出的结论)
用户最初的观察:"org validator 因为没 org planner 的产出就卡死了?而不是按照 task 的要求去 validator?" ——直觉是 validator 应该"以 task 为主",文件只是参考物,不该是硬性评判标准。
讨论过程中的几个关键转折:
- 查证后发现"卡死"是设计使然,不是疏漏——
org-validator/tools.md第 45 行原文:"跑不出判决:没契约/契约非法/参数错 →input_underspecified,退org-planner。不是「那就跳过这条」"。这条硬约束背后的理由是这个系统这几个月反复验证过的教训(43/44/47 号文档):让 agent 拿自然语言 task 描述自由判断"算不算做完"不可靠,所以验收必须走结构化契约,不能自由心证。这条纪律本身不该改。 - 真正该改的是"契约从哪来"——用户提炼:"硬约束的产生应该是自由灵活的,不至于被卡死"。也就是说:消费端(validator 只认结构化契约、不自由判断)该保持硬约束;产出端(谁来写这份契约)不该被锁死在单一角色(
org-planner)身上。 - 讨论过一个"运行时兜底级联"方案(用户提出):org-planner 写 → 没有则 subtask-planning 写 → 都没有则 code 节点自己写 → 都没有则 validator 自己补一份再判、有问题就把 bug + 自己写的验收标准一起打回给上游重做。这个逻辑本身站得住,但有两个问题:
- 对
decomposed_work这条链路,第 3/4 层永远不会被触发——decomposed_work子任务只有一个产生入口(materializeDecomposedSubtasks,只在subtask-planning成功产出 steps 之后才会被调用),所以"没有 planner、也没有 subtask-planning"这个前提在这条链路上不可能成立。给一条不可达的路径写实现,是在为不存在的场景预先建模。 - 更本质的问题:同一个 agent 既写验收标准、又拿着自己刚写的标准去判自己/别人刚做完的东西,跟"先出考卷再考试"比,可信度天然打折——即便格式上仍是结构化契约,也更容易"照着已经做出来的答案凑标准"。运行时兜底能保证流程不会永久卡死,但不能同等程度地保证补出来的东西和事先写好的一样可信。
- 对
- 收敛到的方案:创建时强绑定(预防性,主力机制)+ 运行时兜底(纠正性,安全网,仅记录原则,本轮不实现)。具体:"每一个 task/subtask 天然带着自己的验收契约,这在创建那一刻就必须有,写在
tasks表的字段上,1:1 强绑定"——用户原话:"这样是不是更简单?"。确认:是,比"在某个特定图形状里临时补数据"更彻底,因为约束点提到了createTask这个所有创建路径的公共入口,不用每条路径各自记得要不要造契约。
现状调研(本次讨论核实的代码事实)
acceptance.json / acceptance.md / goal-benchmark.json 是三个不同的东西(讨论中容易混用,需要先分清)
| 产物 | 落点 | 产出方 | 消费方 | 格式 |
|---|---|---|---|---|
acceptance.json(R19-B 契约) |
tasks/{taskId}/acceptance.json,goal-contract.js |
goal-benchmark 节点(唯一真正调 put 的 agent) |
org-validator/coder-validator/benchmark-validator/testdata-validator/planner-validator/validator-triager |
结构化 JSON,{benchmark, test_data, test_script} 三段 |
acceptance.md |
org-planner 私有目录 | org-planner/agent.md 硬约束要求的四份产物之一 |
43 号文档的 maybeMaterializeItemizedSubtasks(首选数据源) |
Markdown 验收表格 |
goal-benchmark.json |
runs/{rootRunId}/artifacts/(共享区) |
goal-benchmark 节点 |
goal-contract.js(被 $ref 指到)+ maybeMaterializeItemizedSubtasks 兜底数据源(47 号文档新增) |
结构化 JSON,dimensions[]/traceability[] |
本文档只关心第一个(acceptance.json,org-validator 真正用来判卷的那份)。跟 43/47 号文档的"子任务物化"是两回事,不要混。
契约产出链是 workgraph-planning 现场展开出的子图节点,不是独立体系
org-planner 执行时,workgraph-planning 依据 planner.json 骨架(brd → prd → sdd → benchmark → validator)+ 一套裁剪判据现场规划出子图。benchmark 节点被标注"不可裁"——理由是它产的 acceptance 被另一张图(org-validator 宿主的 validator.json)跨图消费,裁了就是跨图断供。也就是说,现有机制里"契约一定会被产出"这件事,本身建立在"workgraph-planning 一定会把 goal-benchmark 节点规划进图里"这个假设上——这个假设只在完整 org graph 路径里成立,46 号文档的轻量子任务图完全绕开了 workgraph-planning,这个假设自然不成立。
写入 API 已经是现成的、可直接程序化调用的函数
server/services/goal-contract.js 导出 writeContract(runDir, contract) 和 writeContractSection(runDir, {section, content, goalId, createdBy})——后者是"产出侧唯一该用的入口"(文件内注释原话),CLI 的 put 子命令只是它的一层包装。两者都是纯函数、不依赖 CLI 进程,可以直接从 enqueueTaskRun(Node.js 代码)里调用,不需要 shell 出去跑 goal-contract.js put。
test_script 段的形状要求:{runner: 'node', path: <artifacts/ 内相对路径>, args?, timeout_ms?}——runner 白名单目前只有 node。
设计决策
主力机制:tasks 表新增 1:1 字段,createTask 统一校验(预防性)
新增列(暂定名 acceptance_contract,TEXT,JSON blob,形状复用 goal-contract.js 的 {benchmark, test_data, test_script},走应用层校验、不新造格式——跟 acceptance_mode/subtask_kind 同一套迁移写法,ALTER TABLE ADD COLUMN,新建库有 CHECK、存量库应用层守)。createTask 用 goal-contract.js 已经导出的 validateContract 做形状校验,不重写一遍规则。
本轮范围只收在 decomposed_work 这条链路(subtask-planning → materializeDecomposedSubtasks → enqueueTaskRun 的轻量图分支),不动 task-planning、org-planner 的 acceptance_item 物化、人类/CEO 快速派单(POST /:companyId/tasks)这几条创建路径——task-planning 在 46 号文档 Phase 1 设计时就明确保护过"职责不变、极简、3 轮内、不碰磁盘",把"必须交结构化验收契约"这个要求推给它,值得单独起一轮讨论,不该这次顺手带上。
安全网(仅记原则,本轮不写代码):发现缺失时就地补一份,但不与判卷同一次完成
如果以后出现另一种既没有 subtask-planning 也没有 org-planner 的图形状,撞上契约缺失时的兜底原则:发现者退一步,先独立于已完成的产出补一份契约,再判——不要在同一次调用里"边写标准边照着标准判",尽量保住"先立标准、后判"这个顺序。这条原则不需要现在写成代码,decomposed_work 这条链路走主力机制就已经完全覆盖,留给以后真的出现新图形状时再决定怎么接。
与既有机制的边界
org-coder、org-validator、goal-benchmark等所有既有 agent 的agent.md/tools.md不需要改一行——它们不关心契约从哪来,enqueueTaskRun把契约落到它们熟悉的artifacts/acceptance.json位置,对它们来说和平时公司完整 org graph 产出的契约没有区别。- 43/47 号文档的物化机制(
acceptance.md/goal-benchmark.json→acceptance_item子任务)完全不受影响,是另一套东西。
开发规划:需要改动的 agent 和代码(盘点)
Agent(提示词层面,只有一个要改)
| Agent | 改动 | 为什么 |
|---|---|---|
subtask-planning |
agent.md 输出契约新增字段——每个 step 除了 title/why/depends_on_hint,还要交一份最小验收标准(形状对齐 goal-contract.js 的某个 section,具体哪个见下"待定")。这需要真的理解"这一步做完该长什么样",不能用代码从 why 字段机械转写(转出来的东西大概率是"尽量/基本可用"这类不可度量措辞,goal-contract.js/org-validator 明确拒收) |
这是本轮唯一需要改提示词的 agent |
org-coder、org-validator、goal-benchmark、coder-validator 等 |
不改 | 见上"与既有机制的边界" |
代码
| 文件 | 改动 |
|---|---|
server/services/work-graph-db.js |
tasks 表加 acceptance_contract 列(批 7 迁移);createTask 新增同名参数 + 用 goal-contract.validateContract 校验;TASK_FROZEN_FIELDS 收进这个字段(排队后不许再改,跟 budgetJson 那几个字段同待遇) |
server/services/subtask-planning.js |
解析 LLM 输出里新增的验收字段,materializeDecomposedSubtasks 建每条 decomposed_work 行时把它写进 acceptance_contract |
server/routes/company.js(enqueueTaskRun) |
decomposed_work 分支:起 run 之前,读 taskRow.acceptance_contract,调 goal-contract.writeContractSection(或 writeContract),把它落进这条新 run 目录的 artifacts/acceptance.json,登记进 graph_files(writeContractSection 内部已经做,不用额外重复登记,需确认) |
server/services/goal-contract.js |
大概率不用改——writeContract/writeContractSection/validateContract/SECTIONS 已经导出,直接复用 |
| 测试 | createTask 校验(新增/非法值拒绝);materializeDecomposedSubtasks 正确落 acceptance_contract 字段;enqueueTaskRun 正确把它写进新 run 的 artifacts/acceptance.json(可以直接读文件断言);一条端到端回归——真的走一遍轻量图,确认 org-validator 这次能读到契约、不再 input_underspecified |
一处待定,写实现前要先定案
subtask-planning 该交哪个 section(benchmark/test_data/test_script)取决于怎么落地最自然:test_script 形状是 {runner:'node', path, args?, timeout_ms?}——直觉是指向 org-coder 会写的测试文件(这次真实回归里 org-coder 确实主动写了 test/convert.test.js),但 subtask-planning 在 org-coder 动手之前就要交契约,那时候还不知道 org-coder 最终会把测试文件放在哪个路径——需要要么给 org-coder 约定一个固定的测试文件路径(比如统一要求 test/<task 相关名>.test.js),要么改用 benchmark 段(不依赖具体文件路径,走"维度+及格线"这套已经验证过的格式,org-validator 自己想办法判)。这个选择直接决定 org-coder 的 tools.md 要不要跟着补一条"测试文件固定放哪"的约定——如果选 benchmark 段,org-coder 也可以维持"不改一行"。建议选 benchmark 段,避免牵连 org-coder,但最终定案留到真正实现这一步时再拍板。
明确不做(本轮范围外)
task-planning、org-planner的acceptance_item物化、人类/CEO 快速派单三条创建路径的契约强制——留到以后单独讨论,不在这次改动里。- "发现者就地补一份"的运行时兜底代码——只记原则,
decomposed_work链路走主力机制已完全覆盖,不为不可达的场景写代码。 - 不改
org-coder/org-validator/goal-benchmark等既有 agent 的提示词(除非"待定"那条最终选了test_script段,才需要给org-coder补一条测试文件路径约定)。
验收标准(后续实现完要检查的)
createTask不给acceptance_contract直接创建decomposed_work子任务——应该被拒绝(如果按主力机制彻底做,decomposed_work这个类型应该强制要求这个字段非空)。- 真实回归:重复 46 号文档 Phase 3 v2 那次卡死的场景(单位换算任务的轻量图),这次
org-validator应该能读到artifacts/acceptance.json、真正跑一次goal-contract.js validate+run,不再是input_underspecified。 - 确认
org-coder/org-validator全程不需要感知这份契约是"这次由 subtask-planning 供的"还是"平时 org-planner 供的"——两条路径对它们完全透明。