48-task-acceptance-contract-binding.md 27.8 KB

48 · Task/Subtask 验收契约 1:1 强绑定

状态: ✅ 已实现并完成真实端到端回归(decomposed_work 链路,含真实 org-coder/org-validator CLI 全程跑通,org-validator 判词 fail_reason=null,不再 input_underspecified)。本文档已收口:安全网仍只是原则(不为不可达场景写代码,见「实现记录」);另外三条创建路径(task-planning/acceptance_item 物化/人类快速派单)经调研确认天然不受本文档标题所述死锁影响,不需要补强绑定(见「三条剩余路径调研结论」)。 日期: 2026-09-02(补充真实回归:2026-09-03;三条剩余路径调研收口:2026-09-03) 触发场景: 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 为主",文件只是参考物,不该是硬性评判标准。

讨论过程中的几个关键转折:

  1. 查证后发现"卡死"是设计使然,不是疏漏——org-validator/tools.md 第 45 行原文:"跑不出判决:没契约/契约非法/参数错 → input_underspecified,退 org-planner。不是「那就跳过这条」"。这条硬约束背后的理由是这个系统这几个月反复验证过的教训(43/44/47 号文档):让 agent 拿自然语言 task 描述自由判断"算不算做完"不可靠,所以验收必须走结构化契约,不能自由心证。这条纪律本身不该改。
  2. 真正该改的是"契约从哪来"——用户提炼:"硬约束的产生应该是自由灵活的,不至于被卡死"。也就是说:消费端(validator 只认结构化契约、不自由判断)该保持硬约束;产出端(谁来写这份契约)不该被锁死在单一角色(org-planner)身上。
  3. 讨论过一个"运行时兜底级联"方案(用户提出):org-planner 写 → 没有则 subtask-planning 写 → 都没有则 code 节点自己写 → 都没有则 validator 自己补一份再判、有问题就把 bug + 自己写的验收标准一起打回给上游重做。这个逻辑本身站得住,但有两个问题:
    • 对 decomposed_work 这条链路,第 3/4 层永远不会被触发——decomposed_work 子任务只有一个产生入口(materializeDecomposedSubtasks,只在 subtask-planning 成功产出 steps 之后才会被调用),所以"没有 planner、也没有 subtask-planning"这个前提在这条链路上不可能成立。给一条不可达的路径写实现,是在为不存在的场景预先建模。
    • 更本质的问题:同一个 agent 既写验收标准、又拿着自己刚写的标准去判自己/别人刚做完的东西,跟"先出考卷再考试"比,可信度天然打折——即便格式上仍是结构化契约,也更容易"照着已经做出来的答案凑标准"。运行时兜底能保证流程不会永久卡死,但不能同等程度地保证补出来的东西和事先写好的一样可信。
  4. 收敛到的方案:创建时强绑定(预防性,主力机制)+ 运行时兜底(纠正性,安全网,仅记录原则,本轮不实现)。具体:"每一个 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 做形状校验,不重写一遍规则。

⚠️ 这段的具体形状被真实回归推翻了一半,见下方「实现记录」§「与原设计的出入」:最终落地的 acceptance_contract 存的是 benchmark 段本身({dimensions, overall_pass_threshold?}),不是这里写的三段式 wrapper——校验/写入时才现套一层。本段留着不改是为了如实记录设计过程,别照这段的字面形状抄代码。

本轮范围只收在 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

一处待定,写实现前要先定案 —— ✅ 已定案:benchmark 段

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,但最终定案留到真正实现这一步时再拍板。

按建议定案:选 benchmark 段,org-coder 全程未改一行(见「实现记录」)。


明确不做(本轮范围外)

  • task-planning、org-planner 的 acceptance_item 物化、人类/CEO 快速派单三条创建路径的契约强制——留到以后单独讨论,不在这次改动里。
  • "发现者就地补一份"的运行时兜底代码——只记原则,decomposed_work 链路走主力机制已完全覆盖,不为不可达的场景写代码。
  • 不改 org-coder/org-validator/goal-benchmark 等既有 agent 的提示词(除非"待定"那条最终选了 test_script 段,才需要给 org-coder 补一条测试文件路径约定)。

验收标准(后续实现完要检查的)

  1. ✅ createTask 不给 acceptance_contract 直接创建 decomposed_work 子任务——应该被拒绝(如果按主力机制彻底做,decomposed_work 这个类型应该强制要求这个字段非空)。单测覆盖,见「实现记录」。
  2. ✅ 真实回归:重复 46 号文档 Phase 3 v2 那次卡死的场景,这次 org-validator 真的读到了 artifacts/acceptance.json、真正执行了判分逻辑,fail_reason=null,不再是 input_underspecified。见「真实端到端回归补跑(2026-09-03)」。
  3. ✅ 确认 org-coder/org-validator 全程不需要感知这份契约是"这次由 subtask-planning 供的"还是"平时 org-planner 供的"——两条路径对它们完全透明;org-validator 判词自己也确认了这一点("非本节点自行发明标准——维度/权重/及格线均来自契约原文")。

实现记录(2026-09-02)

落地清单

文件 改动
server/services/work-graph-db.js tasks 表加 acceptance_contract 列(批 7 迁移,ALTER TABLE ADD COLUMN);createTask 新增 acceptanceContract 参数,subtaskKind==='decomposed_work' 时强制非空,套一层 {version, goal_id, benchmark: acceptanceContract} 走 goal-contract.validateContract 校验(不重写规则,见下方「与原设计的出入」);acceptanceContract 收进 TASK_FROZEN_FIELDS(queued_at 非空后不许再改,同 budgetJson 待遇);materializeDecomposedSubtasks 把 step.acceptance 原样递给 createTask,单步校验失败(try/catch)只跳过那一步、不作废整批,延续既有"宽松解析"哲学
system/agents/subtask-planning/agent.md 硬约束新增第 10 条:每个 step 必须带 acceptance(benchmark 段本身),不许从 why 机械转写;输出形状(约束 1)、字段数(约束 5)同步改;补一节说明"轻量派发链路上 acceptance 会被直接拿去判卷"
server/services/subtask-planning.js normalizeSteps 原样透传 acceptance(不在这一层做形状校验,深层校验留给 createTask);buildSubtaskPlanningPrompt 的交回格式示例、字段说明同步加 acceptance
server/routes/company.js(enqueueTaskRun) isDecomposedSubtask 分支:writeTaskGoalArtifact 之后(goal-contract 靠 task-goal.json 推 {companyId,taskId}),把 taskRow.acceptance_contract 套一层 {benchmark: ...} 通过 goal-contract.writeContract 落进新 run 的 artifacts/acceptance.json;best-effort(写失败只 console.error,不阻塞入队——失败时行为退回改动前的 input_underspecified,不比原来更差)
server/services/goal-contract.js 未改一行——writeContract/validateContract/CONTRACT_VERSION/resolveRunDir 全部复用既有导出
测试 task-decomposition-dispatch.test.js 新增 8 个用例(createTask 四种校验分支、materializeDecomposedSubtasks 两种单步失败场景、路由层两个契约落盘/存量缺口场景);task-decomposition-validator-gate.test.js 补 1 处 acceptanceContract(存量夹具需要跟着改)

与原设计的出入(重要,别照着「设计决策」一节里的形状抄)

设计决策里写的是"形状复用 goal-contract.js 的 {benchmark, test_data, test_script}"——这句话在写文档时想的是"契约的三段式 wrapper"。真实起 subtask-planning agent 验证后发现这是错的:agent.md 教的提示词是"acceptance 对齐 benchmark 段",真实 LLM(不是注入桩)老老实实交回的是 benchmark 段内容本身({dimensions:[...], overall_pass_threshold?}),不会自己再包一层 benchmark 键——这符合直觉:没有人会在被要求"对齐 X 的格式"时,自己又发明一层 {X: ...} 包装。

第一版实现是照着设计决策的字面意思写的(acceptanceContract 直接当三段式契约展开),过了单测(因为单测的假数据也是我自己按错误理解造的),但拿真实 agent 输出一测就在 validateContract 那步报错(unknown top-level field: dimensions)——这正是本轮反复强调"真实回归优于只信单测"的又一次验证。修法:acceptance_contract 列存 benchmark 段本身,createTask/enqueueTaskRun 校验/落盘时现套一层给 goal-contract.validateContract/writeContract 用。agent.md/buildSubtaskPlanningPrompt 的提示词从一开始写的就是裸段格式,不用改。

真实验证(不是只信单测)

  1. 单测:npx vitest run server/tests/unit,1198/1200 通过(2 个失败是与本次改动无关的既有失败:env-example-secrets.test.js/test-id-stability.test.js,均未涉及本次改动的文件);ps aux | grep claude 确认零残留 CLI 子进程。
  2. 真实起 p2verify0902 公司里已雇佣的 subtask-planning agent(真实 Claude CLI 调用,不是注入桩),给一条真实 task("实现摄氏度转华氏度的转换函数"),验证 agent 真的会按新 agent.md 交出 acceptance 字段——交了,且格式如上一节所述(暴露了设计文档与真实产出的形状分歧,已修正)。
  3. 用真实 LLM 那次调用的原始产出(未修改一个字)跑一遍完整链路:materializeDecomposedSubtasks → createTask(校验+落库)→ 模拟 enqueueTaskRun 的写入段 → goal-contract.readContract + validateContract,结果 {"ok":true,"errors":[]}——这就是"org-validator 不会再撞 input_underspecified"这件事的直接证据(验收标准 2 的核心机制)。

真实端到端回归补跑(2026-09-03):org-validator 真的判过了

用户明确要求补一次更干净的真实回归,跑通了完整链路,验收标准第 2 条现在已兑现:

  1. 真实建一条全新任务("实现字符串反转函数")、真实起 subtask-planning agent(真实 CLI,不是注入桩)分解出 1 条 decomposed_work 子任务,acceptance 真实产出为 {"dimensions":[{"name":"普通字符串反转正确","weight":0.6,...},{"name":"空字符串正确处理(返回空字符串,不抛异常)","weight":0.4,...}],"overall_pass_threshold":80}。
  2. 真实调用 POST /tasks/:taskId/enqueue(走 CEO 真实用的 x-internal-token 认证路径),起了真实的轻量图(org-coder → org-validator,两个真实 agent_call 节点)。
  3. 过程中撞见两个纯属本次验证方法论本身的坑,均与 48 号文档代码无关:① 第一次没起真正的 API 服务(只在脚本进程里跑图引擎),org-coder 完工后调 bin/lemon.js task transition/report_task 够不着 localhost:3010,连续 4 次 infra_flake 撞熔断硬顶、卡在 blocked;② 补起服务后单独重试,org-coder 子进程作为另一个独立进程(我的重试脚本)的子进程,没能继承真实服务进程自己生成的 LEMON_INTERNAL_TOKEN(该 token 是每进程随机生成、只有其自身子进程才认得),打 API 仍是 401。两次都被真实 agent 自己的诊断日志准确点破根因(org-validator 后来的判词原话也复述了这段"记账问题",并明确判定"不在本节点验收范围内")。最终解法:让驱动脚本 require('server/index.js') 在同一个进程里把真实服务和图引擎重试跑在一起,org-coder 子进程这才拿到匹配的 token。
  4. 修好拓扑问题后重跑,org-coder 真实实现完成,org-validator 真实执行——读到了 acceptance.json、真的走了 goal-contract 的判分逻辑,两条维度都打了分(100/100),fail_reason 为空,不是 input_underspecified。org-validator 的判词原文还独立追加了 7 组自己写的测试用例复核交付物,附带一句准确的旁注:"本次 acceptance.json 仅含 benchmark 段(created_by=subtask-planning,48 号文档轻量子任务图路径:org-coder 不产出自报分,org-validator 自行取证判分,非本节点自行发明标准——维度/权重/及格线均来自契约原文,未改动)"。
  5. 最终态:graph_run_nodes 两个节点均 success,graph_runs.run_status = completed,task status = completed。

验证方法论本身留下的经验(写下来免得下次重犯):真实驱动图执行必须让 agent 子进程与提供 API 的服务进程同源(同一个 Node 进程内 require('server/index.js')),否则 LEMON_INTERNAL_TOKEN 天然对不上、agent 侧的 CLI 工具调用会全部 401——这不是本次代码的缺陷,是"拿脚本模拟真实拓扑"这件事本身的一个通用坑,下次做类似真实回归可以直接照这个模式起。

仍未做的(遗留,按「明确不做」的既有边界)

  • 安全网("发现缺失时就地补一份")仍然只是原则,未写代码——decomposed_work 链路走主力机制已完全覆盖,原设计判断不变。
  • task-planning/org-planner 的 acceptance_item 物化/人类快速派单三条创建路径的契约强制——本轮范围外,未动。2026-09-03 已调研并收口,见下方「三条剩余路径调研结论」——不是补做了忘记补的东西,是查完发现不该做。

三条剩余路径调研结论(2026-09-03,收口,不写代码)

调研目的:本文档标题所指的死锁(org-validator 判卷时没契约、退无可退)根源是轻量子任务图跳过了 org-planner。核实剩下三条创建路径是否也有同类风险,若有则补同款强绑定;若没有,记录原因收口,不为不存在的风险写代码。

结论:三条路径都不在这类死锁风险范围内,强行加 acceptance_contract 反而会跟已有设计边界冲突。逐条:

task-planning 拆出的顶层 task

调用链:runGoalSplit(server/routes/company.js:1581)→ insertTasksForGoal(company.js:1517)→ wdb.createTask(...)(company.js:1518,不传 subtaskKind/acceptanceContract)。人点"下达"后经 enqueueTaskRun 起图,taskRow.subtask_kind 为 null → isDecomposedSubtask=false → 走完整 org graph,不是轻量图。

完整 org graph 里 org-planner 触发 workgraph-planning 现场展开 planner.json 骨架(brd→prd→sdd→benchmark→validator),benchmark 节点在裁剪判据表里标注"不可裁"(system/agents/workgraph-planning/agent.md:216:"产的 acceptance 被另一张图跨图消费,裁了是跨图断供")——只要走标准骨架,goal-benchmark 节点必然产出 acceptance.json,org-validator 判卷天然有契约可读,不依赖 tasks.acceptance_contract 这个字段。

若强行加:会撞上 46 号文档专门保护的 task-planning 设计边界——"职责不变,仍是粗粒度判断,3 轮内、不碰磁盘"(docs/todo/system-design/46-*.md:93,代码对应 server/services/task-planning.js:37 PLAN_MAX_ROUNDS=3、system/agents/task-planning/tools.md:1-9 "全程不碰磁盘、不发请求")。要求它额外产出结构化 benchmark 段,与这条职责边界直接冲突。

结论:不做。 天然已被 workgraph-planning 的"benchmark 不可裁"约定覆盖,且强行做会破坏 46 号文档定的边界。

org-planner 的 acceptance_item 物化

这是与 48 号 acceptance_contract 不同的另一套机制(43/47 号文档):maybeMaterializeItemizedSubtasks(server/services/graph-engine.js:2428)是运行时批量把 acceptance.md/goal-benchmark.json 的条目拆成多条 subtask_kind='acceptance_item' 的子任务行,供 coder-validator 逐条经 /subtask-result 端点上报完成状态——这些子任务从不经过 enqueueTaskRun,根本不起图执行(graph-engine.js:1874-1898 注释明确写了 acceptance_item 与图执行判定"互不干扰")。acceptance_contract 字段目前唯一的消费方是 enqueueTaskRun 起轻量图时的落盘逻辑(company.js:1161),这条路径不会走到那一步,加了字段也没有消费者会读。

结论:不适用,不做。 不是"漏做了",是这条路径跟"图执行时读取验收契约"这件事在机制上没有交集。

人类/CEO 快速派单 POST /:companyId/tasks

company.js:2243 调 wdb.createTask(...),同样不传 subtaskKind/acceptanceContract,isDecomposedSubtask 恒为 false → 走完整 org graph,与 task-planning 路径同理享有"benchmark 节点不可裁"的天然保护。

若强行加:这条端点明确定位"外部 agent 自主派单、零 LLM 调用"(company.js:1105-1106 注释,且有守卫测试 goal-task-acceptance.test.js 验收 22 盯着"零 LLM 调用"这条),创建路径上没有任何环节能现场生成一份符合 goal-contract.js benchmark 段形状的结构化契约——要求调用方(通常是别的 agent 顺手发的一句话指令)自己拼出这份契约,对使用场景是不合理的负担。且 enqueueTaskRun 目前落盘 acceptance_contract 的分支硬绑 isDecomposedSubtask(company.js:1161),这条路径永远不会命中,需要新开一条落盘分支才能接上,投入产出不成比例。

结论:不做。 天然已受保护,且没有 LLM 环节可以生产契约,强行要求调用方手写不合理。

真正的残留风险(不在本文档范围内)

上面三条路径的"天然保护"全部建立在同一个前提上:公司的 org graph 一定含 org-planner 节点、且 workgraph-planning 现场展开时选中含 benchmark 的标准 planner.json 骨架。这只是 agent 提示词层面的约定(orggraph-planning 规划公司 org graph 时给出的是示例而非强制),不是代码强制的不变式——理论上一家公司可以被规划出不含 org-planner 的非标准拓扑,届时这三条路径会重演本文档标题所述的同一类死锁,只是触发条件从"轻量子任务图"换成了"非标准公司拓扑"。

这是 org graph 拓扑保证问题,跟"task 创建时该不该强绑契约"是不同维度,本文档不处理。留待以后真的出现非标准拓扑撞上这个问题时再讨论(可能与 37 号文档"org graph 按部门拆分"那类拓扑改造放在一起考虑),不为假设性场景预先写代码。