43 · Goal 分解粒度:规划产出该长成文件,还是长成 task/subtask 树
状态: ✅ Phase 0-4 全部已实现(2026-08-31)。全量单测 1120/1123 通过(3 个失败与本次改动无关,改动前即存在) 日期: 2026-08-31 触发场景: snakegame-co 公司,goal
G-mtfh23mu-bd63("HTML 贪吃蛇游戏"),taskT-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 在逐个验收,这个现在是不是实现不了?
结构化复述
用户设想的理想流程,拆成四步:
- 规划阶段:org-planner 完成 brd/prd/sdd 之后,不只是产出文档,还应该把这份规划分解成一份 todo 清单。
- 展示阶段:这份 todo 清单应该以 task 和 sub-task 的形态出现在任务看板上,挂在"HTML 贪吃蛇游戏"这个 goal 下面(例子:方向键控制、吃食物加分、碰撞结束、重新开始——即当前
acceptance.json里的 A.1-A.5)。 - 开发阶段:org-coder 不是一次性对着一大段任务描述写完整个交付物,而是按 todo 清单逐条开发。
- 验收阶段: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 还不存在,没法预先知道要拆成哪几条。这意味着"判断要不要拆"和"生成子任务"是两个时间点、两个步骤:
- task-planning(goal 创建时):新增一个判断维度——这个 task 的验收后续要不要走"逐条子任务"模式,写一个标记(例如 task 行上加
acceptance_mode: 'holistic' | 'itemized',或等价字段),不生成任何子任务行。 - org-planner 收尾(acceptance 产物写完之后,org-graph 流程内新增一步):如果上一步标记是
itemized,读验收条目,逐条createTask({parentTaskId, ...})生成子任务行,挂在同一个 goal 下。 coder-validator(work-graph 内既有节点,就是本 session 全程调试的那个 agent):判断逻辑从"给整个 task 一个总分"改成"遍历子任务逐条判",每条子任务各自 success/fail,父任务的终态由子任务汇总(全 success → success,否则看具体子任务)。- 看板 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"具体由哪个信号触发(
verifytransition 本身,还是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 是否真的可靠"这次独立验证——这是唯一一个建在未经证实的既有产物之上的环节,值得单独花一次真跑确认,再决定要不要照原计划往下建。
实现记录(2026-08-31,Phase 0-4 全部完成)
按 Phase 0→1→4→2→3 顺序实现,每个 Phase 落地时跑一次相关单测 + 全量回归,全程零静默失败。 以下记两处跟原计划不一样的地方——都是动手时发现原计划想岔了,不是简单照抄执行。
落地清单
| Phase | 改了什么 | 测试 |
|---|---|---|
| 0(schema) | tasks 加 acceptance_mode 列(CHECK IN ('holistic','itemized'),迁移套用 parent_task_id 当年的 hasColumn/ALTER TABLE 模板);createTask() 加 parentTaskId/acceptanceMode,子任务禁止递归声明 itemized;taskView() 补上这两个字段的输出 |
task-acceptance-mode.test.js(6 条) |
| 1(task-planning 判断) | task-planning 的 JSON schema 从 4 字段扩到 5 字段(acceptance_mode 可选),在硬规则 5 上明确开了一条例外(这个字段需要语义判断,不能像 mode/priority/budget 那样服务端注入默认值);agent.md 加 Step 2bis + 例子;buildTaskPlans() 透传 |
company-goal-task-routes.test.js 补 2 条断言 |
| 4(看板渲染) | GoalTaskPane.vue 按 parentTaskId 分层渲染(子任务只读、无操作菜单,因为它们没有独立 run);新出现的 TaskTable.vue(同一天另一个 session 并发重写了 TaskBoard.vue)用一行"↳ 子任务·属于「…」"指回父任务,不强行画树——它是可排序表格,硬套树会打架 |
用假数据 harness 截图人工验证(渲染逻辑本身足够简单,没再加单测) |
| 2(org-planner 物化子任务) | 见下方"计划外的架构订正" | task-decomposition-materialize.test.js(3 条,真的驱动了 org-planner 展开子图 + 子 run 收敛回传的完整路径) |
| 3(coder-validator 逐条上报) | 新增 wdb.settleSubtaskResult()(终态幂等条件更新,同 settleTaskFromRun 一个模子);新路由 POST /:companyId/tasks/:taskId/subtask-result;listCompanyTasks/GET /:companyId/tasks 加 parentTaskId 过滤;bin/lemon.js 加 task subtasks / task subtask-result 两个子命令;settleNode() 新增闸门——coder-validator 判 success 前核一遍它验的 task 若有子任务,是否全部 completed,没全的话强制改判 fail(新 ENGINE_FAIL_REASONS.SUBTASKS_INCOMPLETE);coder-validator/agent.md 加 Step 4bis |
task-decomposition-validator-gate.test.js(6 条);顺带修了 graph-engine-fail-reason.test.js 的取值域快照(六→七个引擎取值)和 template-verdicts.test.js(agent.md 里一句话的措辞会被误判成"教 agent 写一个引擎不认的 fail_reason",改了措辞不改语义) |
计划外的架构订正:org-planner 不是 agent_call,是 subgraph
原计划假设 system/agents/org-planner/ 是一个单体 agent,走 4 阶段 FSM 直接产出 brd.md/prd.md/sdd.md/acceptance.md,Phase 2 打算在它自己的 settleNode() 成功分支挂一个钩子。这个假设是错的——org 层节点在 readDefGraph 里全部被标记成 nodeKind='subgraph'(无一例外),真正核实 snakegame-co 那条参考任务的生产数据后发现,org-planner 当前总是靠 workgraph-planning 展开成 planner.json 那张 5 节点子图(brd-planning → prd-planning → sdd-planning → goal-benchmark → planner-validator),而这条参考任务的 goal-benchmark/planner-validator 两个节点当时都是 skipped——acceptance.md(连带 R-19-B 的 acceptance.json 契约)从未被真正产出过。这正是文档最初标记的"⚠️ 已知风险",真跑数据把它坐实了。
修法:钩子从"org-planner 这个 agent_call 节点自己成功时触发"改成"org-planner 这个 subgraph 节点(不管它是直接执行还是展开成子图)真正结算 success 时触发"——两种形态统一在同一个 maybeMaterializeItemizedSubtasks() 里判:agent_call 就在它自己的私有目录找 acceptance.md,subgraph 就去它的子 run 里全节点扫(不假设固定是哪个节点写的,因为 workgraph-planning 现场选模板,节点名不固定)。找不到就留一条 timeline note、按 holistic 收尾,不阻塞、不拖累 org-planner 这一轮真正有价值的产出——这与文档最初的"软失败"设计初衷一致,只是触发时机改对了。
遗留风险(订正后依然成立,写实话):acceptance.md 会不会被稳定产出,取决于 workgraph-planning 会不会给 org-planner 挑一张真正跑完 goal-benchmark/planner-validator 的图、以及那张图会不会真的产出这份文件——这两件事都不在本次改动范围内,maybeMaterializeItemizedSubtasks() 目前是优雅降级(找不到就跳过,不报错),不是保证生效。换句话说:acceptance_mode='itemized' 现在是"如果规划链路配合,就能自动长出子任务",还没做到"一定会长出子任务"。
2026-08-31 真实回归验证坐实了这条风险,而且找到了比"未必可靠"更具体的根因:见 44 号文档——goal-benchmark 按设计把产物写进 root run 共享 artifacts/ 目录,但产出闸门只查节点私有目录,writesArtifacts:true 这个字段全仓库零代码读取,这条契约在当前实现下结构性地打不过,不是运气不好。这很可能正是本文档触发场景(snakegame-co)里 goal-benchmark/planner-validator 显示 skipped 的真实原因。44 号文档已修复(同日),会提高 Phase 2 的实际命中率,但验证不在 43 号文档范围内,不在这里展开。