43 · Goal 分解粒度:规划产出该长成文件,还是长成 task/subtask 树
状态: 📝 需求 + 现状调研,尚未设计、未实现 日期: 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 是否真的可靠"这次独立验证——这是唯一一个建在未经证实的既有产物之上的环节,值得单独花一次真跑确认,再决定要不要照原计划往下建。