43-task-decomposition-granularity.md 18.3 KB

43 · Goal 分解粒度:规划产出该长成文件,还是长成 task/subtask 树

状态: 📝 需求 + 现状调研,尚未设计、未实现 日期: 2026-08-31 触发场景: snakegame-co 公司,goal G-mtfh23mu-bd63("HTML 贪吃蛇游戏"),task T-mtfh2kuf-7c31。 该任务当天被 missing_required_input 死循环卡住(根因与修复见 33 号文档 的复发一例, tools.md 范例与 work-graph-db.js 引擎层已同日修复),排查过程中用户提出了本篇的需求。 格式对齐 27-requirement.md:原话照搬 + 结构化复述,现状调研另起一节。


原始需求(用户原话,一字不改)

就这个任务,我讲讲我理想的状态,应该是 org planner 完成了brd prd sdd 后,在 task 任务看板应该能看到规划出来的 todo list 作为这个 html 贪吃蛇游戏 goal 的 task 和 sub task 展示出来,然后每一个 org coder 都是按照这个 task todo list 在逐个的开发完成,然后 org validator 应该是针对 task todo list 在逐个验收,这个现在是不是实现不了?


结构化复述

用户设想的理想流程,拆成四步:

  1. 规划阶段:org-planner 完成 brd/prd/sdd 之后,不只是产出文档,还应该把这份规划分解成一份 todo 清单。
  2. 展示阶段:这份 todo 清单应该以 task 和 sub-task 的形态出现在任务看板上,挂在"HTML 贪吃蛇游戏"这个 goal 下面(例子:方向键控制、吃食物加分、碰撞结束、重新开始——即当前 acceptance.json 里的 A.1-A.5)。
  3. 开发阶段:org-coder 不是一次性对着一大段任务描述写完整个交付物,而是按 todo 清单逐条开发。
  4. 验收阶段:org-validator 也不是一次性给整个交付物判总分,而是按 todo 清单逐条验收。

用户明确问:现状是不是做不到这个?


现状调研结论(本次会话核实,非最终设计)

结论:部分可以,"做不到"的那部分是当前 agent 设计上的主动选择,不是漏做的功能。四层现状分别是:

1. tasks.parent_task_id——字段在,但服务的是另一条流水线

tasks 表(server/services/work-graph-db.js)确实有 parent_task_id 列,也确实有代码在读写它——但那是 LemonBoss Agent Chat 的人工派单流程(server/routes/agent-chat.js + task-service.js 建任务树),跟本篇讨论的 goal → task → org-graph/work-graph 这条流水线是两套独立机制。

本篇关心的这条流水线里,真正负责"从 goal 生成 task 行"的函数是 work-graph-db.js 的 createTask({goalId, companyId, seq, title, description, executionMode, dependsOn, priority, budgetJson, reportTo, createdBy})——这个函数签名里根本没有 parentTaskId 参数。也就是说,子任务对这条流水线而言是结构性够不着,不是"字段有、没人填"。

2. task-planning 阶段本身有能力拆多任务,但拆分依据不是"检查项"

真正把 goal 拆成 task 的阶段是 task-planning(server/services/task-planning.js + system/agents/task-planning/,落库调用点在 server/routes/company.js:1577 insertTasksForGoal)。这一步确实能一次拆出多条 task(已有测试验证过 2 条以上的场景)。

snakegame-co 这次只拆出 1 条 task,不是能力限制,是 system/agents/task-planning/agent.md 第二步的明确设计判断:拆分依据是"交付物/验收边界",不是"工作阶段"。原文点名"①调研②设计③开发④测试⑤上线"这种按阶段拆是"最常见也最致命的错法"。按这份人设的判断,方向键/得分/碰撞/重开这四条,是同一个交付物的验收清单(正是最终 acceptance.json 里一起被同一份 Playwright 脚本验证的 A.1-A.5),不是四个可以独立排期验收的 task——agent 设计上就会拒绝把它们拆成四条 task。该 agent 人设还硬顶单 goal ≤ 8 条 task,整体偏"合并"不偏"细分"。

3. org-planner 没有"建任务"这个工具

org-planner 在跑的时候(system/agents/org-planner/tools.md)能力边界是:调研 → 阶段流转 → 写 brd/prd/sdd/acceptance.md 这几份文件 → 向 CEO 汇报。没有任何一个工具允许它创建新的 task 行(org-coder / org-validator 同样没有)。规划阶段的产出,结构上只能落成文档,附着在它所属的那一条已经固定的 task 上,没法在跑的过程中反过来生出新的、可独立调度的子任务。

4. 看板 UI 没有树形渲染

web/src/views/TaskBoard.vue、web/src/components/graph-console/GoalTaskPane.vue 目前都没有父子任务的渲染逻辑——即便前三层都打通、数据库里真的出现了带 parentTaskId 的行,看板现在也画不出树形结构,只会当成平铺列表处理(或者干脆不显示子任务)。


设计决策(2026-08-31 定案,三选一各选完)

问题 决定 理由
子任务挂在哪套机制上 goal → task 流水线里新建一套独立父子机制(不复用 Agent Chat 的 parent_task_id) 后者是为「人工派单、多轮对话」设计的语义,跟 org-graph 自动执行的生命周期(谁创建、何时终结)对不上,硬凑容易埋雷
子任务要不要独立开发+验收循环 先只拆验收,开发仍整体完成——org-coder 一次性写完整个交付物,验收侧改成按子任务(验收条目)逐条判,而不是给总分 改动小得多,且先解决「验收颗粒度太粗」这个更痛的点;独立开发循环(每个子任务各起一个 work-graph run)留作后续再评估
什么粒度的 goal 该拆 由 task-planning agent 自己判断,不写死规则 跟它现在「要不要拆多个 task」的判断逻辑同源,交给同一套"交付物/验收边界"标准,不额外发明一套阈值规则

由这三条决定组合出的实现方向:task-planning 判断"值不值得走细粒度验收"是一个粗粒度的开关,真正的子任务清单在 org-planner 写完 acceptance.json 之后才生成——因为验收条目本身就是规划阶段的产出,task-planning 跑的时候 brd/prd/sdd 还不存在,没法预先知道要拆成哪几条。这意味着"判断要不要拆"和"生成子任务"是两个时间点、两个步骤:

  1. task-planning(goal 创建时):新增一个判断维度——这个 task 的验收后续要不要走"逐条子任务"模式,写一个标记(例如 task 行上加 acceptance_mode: 'holistic' | 'itemized',或等价字段),不生成任何子任务行。
  2. org-planner 收尾(acceptance 产物写完之后,org-graph 流程内新增一步):如果上一步标记是 itemized,读验收条目,逐条 createTask({parentTaskId, ...}) 生成子任务行,挂在同一个 goal 下。
  3. coder-validator(work-graph 内既有节点,就是本 session 全程调试的那个 agent):判断逻辑从"给整个 task 一个总分"改成"遍历子任务逐条判",每条子任务各自 success/fail,父任务的终态由子任务汇总(全 success → success,否则看具体子任务)。
  4. 看板 UI:TaskBoard.vue / GoalTaskPane.vue 加父子任务树形渲染,只读展示,不涉及交互改动。

实现细节决策(2026-08-31 定案)

问题 决定 理由
标记字段怎么设计 tasks 表加 acceptance_mode 字段,取值 `'holistic' \ 'itemized',命名风格对齐既有execution_mode`(manual/auto),由 task-planning 生成 task 行时一并写入
部分子任务失败时父任务终态 必须全部子任务 success,父任务才 success;任一子任务 fail → 父任务 fail 跟当前"一次性给总分、不合格就是不合格"的严谨程度保持一致,也呼应 34 号文档 关于验证不能打折的教训;optional 子验收项留作后续需要时再加,先不预留这层复杂度
子任务要不要复用现成的状态语义 复用(pending/ready/running/success/fail/blocked 这套状态词汇) 直接复用能白拿状态流转、看板渲染的既有约定,不用另起一套并行的状态语义

落地时的订正(技术设计阶段核实后发现):上一行"复用现成状态机"字面上写的是 graph_run_nodes 那套,但子任务本质是 tasks 表的行(带 parent_task_id),不是某个 work-graph run 内部的节点实例——它没有自己独立的 run。所以真正落地时复用的是 tasks.status 这套既有枚举(queued/running/blocked/completed/failed/cancelled),只是沿用同一套状态词汇的精神(有限状态、终态幂等、settleNode/settleTaskFromRun 那一类条件更新闸门),不是字面上去 graph_run_nodes 表插行。见下方技术实现计划 Phase 3。

另一处订正:上面第 3 步原来写的是"org-validator",但技术设计阶段查证后发现 org-validator 和 coder-validator 是两个不同目录、不同职责的独立 agent——org-validator 是 org-graph 顶层节点(整体验收),coder-validator 是 org-coder 这一层 work-graph 内部的节点(就是本 session 全程调试的那个,work-validator/v1 输出格式)。真正产出逐项验收证据、需要按子任务逐条判的是 coder-validator,已在上面订正。

至此,43 号文档的方案层面已经全部定案。下面是核实过具体代码形态后的技术实现计划。


技术实现计划(2026-08-31,代码形态已核实,未开工)

Phase 0 · Schema

  • tasks 表加 acceptance_mode 列,CHECK (acceptance_mode IN ('holistic','itemized')) DEFAULT 'holistic'——语法上照抄 execution_mode 已有的 CHECK 写法(work-graph-db.js:533),迁移方式照抄 parent_task_id 自己当年落地时用的 hasColumn 判断 + ALTER TABLE 兜底那一段(work-graph-db.js:626-627),同一个文件里已有两份现成模板可抄,不用发明新的迁移写法。
  • createTask({...}) 签名(work-graph-db.js:1861-1873)加 parentTaskId、acceptanceMode 两个参数,INSERT 列表(work-graph-db.js:1902-1907)跟着加。parent_task_id 列本身早就存在(是 Agent Chat 那条派单流程在用),这里只是第一次让 goal→task 流水线自己也写这一列——顺手确认一下现有读它的 task-service.js:66-81 那段任务树构建逻辑,会不会因为多了一类新来源的父子行而产生非预期的合并/展示(大概率不会,因为按 company_id/goalId 天然分域,但落地前应该跑一次它的既有测试)。
  • 守卫测试:图流水线创建的顶层 task(insertTasksForGoal 路径)parent_task_id 仍是 NULL;子任务的 parent_task_id 非空且指向父任务、acceptance_mode 恒为 'holistic'(子任务不允许再递归拆一层)。

Phase 1 · task-planning 判断 acceptance_mode

task-planning 目前是纯"回复 JSON 代码块"模式(system/agents/task-planning/tools.md:5),产出 4 字段 [{seq, title, description, depends_on}](agent.md:120-142),而且硬规则 5(agent.md:10)明确禁止 LLM 多写字段——"服务端生成,你写了会被当成脏字段打回",mode/priority/budget 这些运营字段都是 company.js:1396-1429 buildTaskPlans() 在服务端按 goal 级默认值统一注入的(company.js:1421-1423),不是 LLM 决定的。

acceptance_mode 要不要拆本质上是一次语义判断(跟"要不要拆多个 task"用的是同一种判断力,这正是当初选"由 agent 自己判断、不写死规则"的理由),没法像 mode/priority/budget 那样简单地由服务端注入默认值——所以这里必须显式在硬规则 5 上开一个例外:

  • agent.md 的 JSON schema 从 4 字段扩到 5 字段,加 acceptance_mode,并在硬规则 5 旁边补一句明确的例外说明(为什么这一个字段跟其它运营字段不同,避免以后被"这些字段都是服务端生成"的惯例带偏、被误当脏字段清掉)。
  • buildTaskPlans()(company.js:1396-1429)改成透传 LLM 给的 acceptance_mode,而不是像其它字段那样用服务端默认值覆盖;LLM 没给这个字段时兜底 'holistic'(保守,不引入新行为,除非规划阶段明确判断要拆)。

Phase 2 · org-planner 收尾生成子任务(⚠️ 有一个真实风险要先处理)

org-planner 的 4 阶段 FSM(system/agents/org-planner/tools.md:19-23,node bin/lemon.js task transition --phase {plan|execute|verify})产出的验收清单叫 acceptance.md(不是 .json)——Markdown 表格 | # | 验收项 | 判定方式 | pass 条件 |(tools.md:44-50),一行一条验收项,是子任务清单最自然的数据源。这不是同名的 R-19-B acceptance.json 契约(goal-contract.js:76 定义的 benchmark|test_data|test_script 三段式,由另外三个独立的 goal-benchmark/goal-test-data/goal-test-script 节点产出)——两者名字像、来源和用途完全不同,写代码时一定要认清楚读的是哪一份。

⚠️ 已知风险,动工前必须先确认:本 session 全程调试的 snakegame-co 这条真实任务,acceptance.md 和 acceptance.json 实际上都没有被真正当成验收依据用——coder-validator 最终判定用的是自己临时写的一套 Playwright 测试(A.1-A.5),goal-contract.js 那份 acceptance.json 甚至报了 note:"goal-contract.js cannot parse index.html as JSON...treat as no task-level contract",被当场判定"这个任务没有 task 级契约"直接跳过。如果子任务清单要建在 acceptance.md 之上,得先跑一次真实回归确认它确实会被稳定产出、格式跟文档描述一致——不能假设它已经可靠,这跟 33 号文档"建立在一个从来没人真正产出过的 kind='log' 之上"是同一类风险,值得先花一次真跑单独验证,而不是直接在上面盖楼。

如果验证通过,新增步骤:org-planner 的 verify 阶段落盘 acceptance.md 之后,插入一步"物化子任务"——仅当父任务 acceptance_mode==='itemized' 时,逐行读表格,为每一条验收项调用 createTask({parentTaskId: 当前 task.id, acceptanceMode:'holistic', title: 该行"验收项"文本, description: 该行"判定方式"+"pass 条件", ...})。

两个待确认的实现细节:

  • 这一步该是给 org-planner 加一个新工具(让它自己触发"物化子任务"),还是挂在服务端某个 hook 上(verify 阶段落盘后自动触发,agent 无感)?倾向后者——子任务生成是纯数据搬运,不需要占用一次 agent 调用去决定"要不要生成"(acceptance_mode 这个语义判断已经在 Phase 1 做完了),机械地转换 Markdown 表格到 task 行不需要 LLM 参与。
  • 本次调研没能确认 org-planner"完成→进入 org-coder"具体由哪个信号触发(verify transition 本身,还是 tools.md:59-65 那个额外的 task/report 调用)——插入新步骤前必须先查清楚,避免插错时机打断现有的阶段流转。

Phase 3 · coder-validator 改成逐条判 + 回写子任务状态

关键发现:coder-validator 现在产出的 checks[]/verdict 结构化 JSON(就是本 session 里 attempt-2.json 那种形状),目前没有任何服务端代码解析它——graph-engine.js 判定这个节点成功与否只看 executor 返回的顶层 status 字段,checks[] 纯粹是给人看的审计文本,goal-contract.js 走的是完全独立的另一条 CLI 路径(自己生成 verdict_id),跟 coder-validator 的输出没有关系。

所以"逐条判"要真正影响子任务状态,不能只是把 agent 提示词写得更细——必须新增一个真实的工具调用:coder-validator 每判完一条验收项,调用一次新工具(例如 report-subtask-result),带上子任务 id + success/fail,服务端据此走一次 tasks 表的条件更新(复用 settleTaskFromRun 同款的"终态幂等闸门"思路:已经是终态的子任务不能被覆盖)。这跟本 repo 一贯的做法一致——状态改变走真实工具调用,不解析自由文本 JSON(checks[] 格式在 org-validator/coder-validator/planner-validator/testdata-validator/benchmark-validator 五个 validator 之间只是"约定俗成、没有强制"的 work-validator/v1,不是哪里都强制校验的 schema,解析它不安全)。

父任务终态:全部子任务 completed → 父任务按现有 settleNode 逻辑判 success;任一子任务 failed → 父任务 fail(Phase 0 就已经决定)。

Phase 4 · 看板树形渲染

  • web/src/components/FileNode.vue 是现成的自递归 <script setup> 组件模板(v-for="child in node.children" + depth*14+6 缩进),照这个结构写一个 TaskNode.vue,不用发明新模式。
  • GoalTaskPane.vue:78-131 现在是纯平铺 <ul class="tasks"><li v-for="t in g.tasks || []">,改成先按 parentTaskId 把 g.tasks 分组建成树,再用 TaskNode 递归渲染。TaskBoard.vue 本身不用改——它是纯外壳,全权委托给 GoalTaskPane.vue(TaskBoard.vue:23-32)。
  • taskView()(company.js:994-1031,GET /:companyId/goals?withTasks=1 等多个路由共用的唯一整形函数)要把 parent_task_id、acceptance_mode 加进输出字段列表——目前两个字段都完全没有出现在响应里,包括未使用的形式都没有。

建议的实施顺序

Phase 0 → 1 → 4 → 2 → 3。

Phase 4(看板渲染)提前到 Phase 2/3 之前:先用手工插入的测试数据验证树形展示对不对,把"看得见"和"能自动生成"两件事解耦,避免一次性大集成、出问题不知道是生成的问题还是展示的问题。

Phase 2 排在 Phase 3 之前,且内部先做"acceptance.md 是否真的可靠"这次独立验证——这是唯一一个建在未经证实的既有产物之上的环节,值得单独花一次真跑确认,再决定要不要照原计划往下建。