# 43 · Goal 分解粒度:规划产出该长成文件,还是长成 task/subtask 树 > **状态**: ✅ Phase 0-4 全部已实现(2026-08-31)。全量单测 1120/1123 通过(3 个失败与本次改动无关,改动前即存在) > **日期**: 2026-08-31 > **触发场景**: snakegame-co 公司,goal `G-mtfh23mu-bd63`("HTML 贪吃蛇游戏"),task `T-mtfh2kuf-7c31`。 > 该任务当天被 `missing_required_input` 死循环卡住(根因与修复见 [33 号文档](../../done/system-design/33-needs-gate-role-filter-bug.md) 的复发一例, > `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 行时一并写入 | 不额外发明隐式推导规则,创建时就能在库里直接看到这条 task 走哪种验收模式,调试/展示都直观 | | 部分子任务失败时父任务终态 | **必须全部子任务 success,父任务才 success**;任一子任务 fail → 父任务 fail | 跟当前"一次性给总分、不合格就是不合格"的严谨程度保持一致,也呼应 [34 号文档](./34-validator-scored-without-real-deliverable.md) 关于验证不能打折的教训;`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` 是现成的自递归 `