# 39. Boss Agent 任务看板监督 + 公司目标代码目录 — 需求评估与设计草案 > **状态**:✅ P1-P5 已实现(2026-08-30,commit `a00590a` P1/P3/P4 后端、`5108d67` P2 前端、`d967ac0` P5、`14cbaf5` graph-run.js 鉴权补丁)。本文档是落地前的设计草案,实现细节以代码为准,仅存档决策过程。**已知未覆盖的后续工作见「六、已知遗留」,真实端到端跑测的复盘见「七、真跑复盘」,复盘沉淀的待办已拆成独立文档 [42-org-work-graph-reliability-followups.md](./42-org-work-graph-reliability-followups.md)(按 phase 排开工顺序)**。 > **日期**:2026-08-30 > **背景**:承接对话讨论——goal/task 两级模型([27-P3C](../../done/system-design/27-p3c-goal-task-model.md))已落地,但推进它的是纯确定性代码(`task-scheduler.js` + `transitionRun` 钩子),没有任何 agent 在持续监督这块看板、处理异常、把执行中发现的新问题回写成新任务。用户提出五阶段方案补上这一层,并追加公司级目标代码目录需求。 > **读者须知**:本文档大量引用现有代码路径核实"已有 vs 缺失",结论以文内给出的具体函数/文件为准;这些引用会随代码演进过期,落地前请重新核实。 --- ## 一、先说结论 用户的五阶段里,**P2(任务看板)本质已完成,P1/P3 的基础设施已有大半,P4 是真正的新增核心,P5 的机制已经实现了 70%,只是没有暴露到公司层面。** 真正从零开始的工作量比五个"phase"暗示的要小得多,但顺序建议大改:**P4(回写闭环)应该提到最前面**,因为它才是这次讨论最初的诉求("自己发现问题、自己重新规划"),P1/P3 的 boss agent 只有在 P4 的工具/数据通路建好之后才有事可做。 | 用户的 phase | 名义工作量 | 实际现状 | 建议 | |---|---|---|---| | P1 雇佣通用 boss agent | 新建一个角色 + 训练 | 心跳基础设施、`ceo` 本体、Innovator 训练方法论**全部已有** | 复用 `ceo`,不新建角色;工作量收窄成"写 heartbeat.md + 工具" | | P2 任务看板独立页面 | 一个新系统 | goal/task 两级看板**已实现**(`GoalTaskPane.vue`),只是嵌在控制台里 | 降级成 UI 改造,非必须,可排最后 | | P3 心跳挨个决定下一个 task | 新的调度逻辑 | 挨个执行**已经是** `task-scheduler.js` 在做,且做得更好(事件驱动、零成本、已测试) | **不要重建调度**;心跳的职责收窄为"异常判断",且应事件触发而非纯轮询 | | P4 bug/新任务回写看板 | 一条新闭环 | **不存在**——没有任何 agent 可读写 goal/task 板的工具,四分类失败归因(30 号文档)目前只导向"重试或人工介入",不导向"开新任务" | **真正的核心工作,优先做** | | P5 公司目标代码目录 | 新能力 | DevLoop 的 `path-resolver.js` 已实现 `sandbox` / `linked-new` / `linked-existing` 三种模式,**`linked-existing`(接管已有目录)已经是完整实现**,只是没有公司级默认值,也没打通到 org/work graph(非 DevLoop)路径 | 补一个公司级默认配置 + 核实非 DevLoop 路径是否需要同样机制 | --- ## 二、逐 phase 现状核实 ### P1:每个公司雇佣 boss agent,system agent 层训练通用版本 **已有的基础设施**(全部实地核实过): 1. **通用心跳机制**:`server/services/heartbeat-service.js`。状态落在 `system/agents/{aid}/{cid}/heartbeat.json`(公司态目录,不是共享本体——这条边界是踩过坑之后加的,见文件头注释),本体 `config.json.heartbeat` 只是只读默认建议间隔。`_triggerHeartbeat()` 按 `setInterval` 定时给 agent 发一条 `[SYSTEM HEARTBEAT]` 系统消息(走正常的 `/api/agent/{agentId}/chat`),要求 agent "**根据你的身份和 heartbeat.md 规则**……如果一切正常,无事发生,请直接回复『心跳正常』",带 30 分钟会话互斥防重叠。**这套机制现成可用,不用重建。** - ✅ **修正一处之前的误判**:原以为 prompt 里"扫描全局 OKR 目标状态和节点"那句硬编码需要改代码才能适配新 goal/task 模型。查了 `system/agents/` 下已有 **35 个**本体带 `heartbeat.md`(如 `org-innovator/heartbeat.md`),内容五花八门、完全跟 OKR 无关,说明触发消息里那句"扫描 OKR"从来就只是**默认示例文案**,各 agent 早就按自己 `heartbeat.md` 里写的规则各干各的。**结论:`heartbeat-service.js` 大概率零改动**,只需要新建 `system/agents/ceo/heartbeat.md`,写清楚"去查 goal/task 板、按 §四.9 的规则判断",agent 读到自己的 `heartbeat.md` 就会照着做,不需要动服务端代码。落地时找一个现有 `heartbeat.md` 生效的真实案例复核一次这个推断,再动手写 `ceo` 的那份。 2. **本体已存在**:`system/agents/ceo/`(`type: "cxo"`,"Chief Executive Officer",`heartbeat.enabled: false` 默认关)。同时存在 `system/agents/master/`(`type: "master"`,**全局唯一**,`/api/master/chat`,负责建公司/招聘等跨公司操作)。两者定位不同:`master` 是平台层面的单例助手,`ceo` 才是"每个公司一个"的角色。 3. **训练方法论已验证**:`36-prd-agent-methodology-training-feasibility.md` 已经用 Innovator(Hunt→Digest→Evolve)训练出过一个方法论型 agent(`prd-methodology-trial`),2 轮收敛、96/97 分,方法论对这类"判断型"角色确实管用。可以照搬这条路径训练 `ceo` 的监督方法论,而不是手写一份 prompt 拍脑袋。 **结论**:不建议新建"boss"角色。**复用 `system/agents/ceo/`**——它的岗位定义(CEO)本来就该管"任务有没有卡住、要不要重新分解",另起一个语义重叠的角色只会重犯这个仓库反复踩过的"名字歧义"坑(`27-naming-conventions.md` 通篇在防这个)。P1 的实际工作量是:① 给 `ceo` 补一份 `heartbeat.md`(监督规则);② 给它挂上 P4 需要的工具(见下);③ 默认 `heartbeat.enabled: true`(但见 P3 的成本讨论,建议先不无脑轮询);④ 可选:用 Innovator 训练它的监督方法论。 **缺口 A**:目前**没有任何 agent 工具能读写 `goals`/`tasks` 表或操作 graph run**(搜了 `server/src/tools/` 全目录,零命中)。`ceo` 就算被唤醒也无事可做——这是 P1 能不能生效的真正前提,见 P4。 **缺口 B(新发现,之前漏了):`ceo` 现在没有被雇进任何公司,`hire()` 也没有任何自动触发点会调它**。查了 `hire(companyId, roleName)`(`company-service.js:422`)——它只查本体目录 + `company_agents` 表,**不查 org graph**,机制上雇一个不在图里的角色完全没问题(数据模型层面,"雇了"和"进图"是两件独立的事)。但目前**没有任何代码路径会主动调 `hire(companyId, 'ceo')`**——它不在三份 org-graph-templates 里,也没有类似 `orggraph-planning` 那样的自动招聘钩子替它兜底。`37-org-graph-department-scoping.md` 里记录过一模一样的处境:`hr/` 本体"已经真实存在……只是从没被装进任何图……今天完全是编外人员"——`ceo` 现在是同款处境,**不是有先例可抄,是同一个坑还没人填**。P1 必须显式加一步:① 公司创建流程里(不管选哪个 org-graph-template)固定 `hire(companyId, 'ceo')` 一次;② 存量公司需要一次性回填脚本,遍历现有公司补雇。这条不做,前面说的"复用 ceo"就是一句空话——压根没有 `ceo` 实例在跑。 --- ### P2:任务看板独立页面 **已实现**([27-P3C](../../done/system-design/27-p3c-goal-task-model.md),✅ 2026-08-20):`web/src/components/graph-console/GoalTaskPane.vue` + `goal-task-model.js`,挂在 `/:companyId/graph-console` 左栏。goal/task 两级状态、优先级排队位次、颜色语义全部已有,REST 是 `server/routes/company.js` 的 `GET/POST /api/company/:cid/goals` 系列。 **用户要的"独立页面"** 目前是控制台的一个侧栏,不是路由级独立页面。这是纯 UI 改造,**不涉及数据模型或后端**,随时可做,风险最低。 **"subtask" 层**:当前**没有独立的 subtask 表**,`task` 就是看板能管理的最细颗粒度。task 再往下是 `graph_runs`(work graph 节点,可能含嵌套子图),这层在中间的执行画布(`WorkGraphCanvas`)里看,不在左栏。 > ⚠️ **不建议新增一张 subtask 表**去对应 work graph 节点。这个仓库已经因为"两套任务体系不统一"吃过至少两次苦头(P3C §十二"批 5"整章在处理这个债,`27-p3c-batch3-handover.md` 也记过一次事故)。如果要在看板上展示"这个 task 内部具体在跑哪几步",应该是**只读地把该 task 当前 `current_run_id` 对应的 `graph_run_nodes` 渲染成一份"子步骤列表"**,复用现有表和现有执行画布的数据源,不新建写路径。 #### P2 决定:与 OKR 页面的关系(已拍板,2026-08-30) 排查发现现状比预期乱:**OKR 相关 UI 现在有两份几乎逐行相同的重复实现**——`web/src/views/OkrDashboard.vue`(路由 `/okr`,`requireAdmin`,零导航入口的孤儿页面)和 `CompanyDashboard.vue` 里挂着 "goals" 名字、实际渲染 "Goals (Full OKR Tree)" 的 tab——两者都是旧 OKR 树(`O-xxx`,owner + progress%),是"OKR 正名"(`27-naming-conventions.md` §六)前端没跟着改完的遗留物,前端两处都调 `/api/okr/*`,和 P3C 的新 goal/task 模型完全不是一回事。 **已拍板**:**不合并**,OKR 这套前端 UI**直接废弃**,新任务看板**另起一个独立页面**。具体动作: 1. **新增路由**:仿照 `GraphConsole` 已经立好的先例(`/:id/graph-console` 是独立路由,不挤在 `CompanyDetail` 的 `:tab(...)` 白名单里),新增 `/:id/task-board`(命名待定)→ 新建 `web/src/views/TaskBoard.vue`。**不要塞进 `:tab(...)` 正则**,理由与 `graph-console` 当年的注释一致:任务看板信息密度高,值得一个整页。 2. **复用现有逻辑,不重写数据层**:`GoalTaskPane.vue` + `goal-task-model.js` 现在已经封装了 goal/task 的状态映射、排序、颜色语义(且有专门的单测 `goal-task-model.test.js` 兜底)。新页面应该是把这个组件从 `GraphConsole` 的侧栏布局里"拔出来"放大铺满整页,**不是重新实现一遍**。`GraphConsole` 侧栏可以保留一份精简版(点某个 task 仍需跳转去看它的执行图),也可以改成跳转链接指向新页面——两种都行,实现时再定。 3. **废弃 OKR 前端**:删 `/okr` 路由 + `OkrDashboard.vue`;删 `CompanyDashboard.vue` 里那个挂着 "goals" 名字实为 OKR 树的 tab;路由 `:tab(...)` 白名单里的 `goals`/`okr` 两个值一并摘掉,避免留死链接。 4. **导航入口**:新页面必须挂真实导航入口(`AppNav` 或公司详情页的显眼位置),不能重蹈 `/okr` 那种"孤儿页面"的覆辙。 5. **鉴权**:`okr.js` 当年挂 `requireAdmin` 是因为它是孤儿页面、没做归属校验的权宜之计(`38-auth-multi-tenant-isolation.md`),**不是"任务看板天生该管理员专属"**。新路由要按 `company.js` 等其他公司级路由的标准做法,走 `canAccessCompany` 归属校验,不能照抄 `requireAdmin`。 **范围之外、本次不动**(留作独立的后续决定,不在这次 P2 改造里顺手做):`/api/okr/*` 后端路由、`okr` 相关 SQLite 表、`goals.okr_node_id` 这个可选外键字段——前端 UI 废弃不等于后端子系统也要一起拆,那是一次单独的、影响面更大的退役决定(牵涉数据迁移/存量数据处理),需要单独评估,这里先只处理前端。 --- ### P3:心跳挨个决定下一个 task **这条按用户原话实现是在重建一个已经存在、已经测试、且更优的系统。** `task-scheduler.js` 的队列排序口径是 `priority → seq → created_at`(`ORDER BY priority, seq, created_at`),出队靠 `transitionRun` 这个唯一漏斗事件触发,不是轮询;v1 约束 `max_parallel_tasks`(`company.json`,缺省 1)决定"挨个"还是"并发几个",`server/tests/unit/task-slots-parallel.test.js` 覆盖了 >1 的情况。这套机制零 LLM 成本、毫秒级触发、已经过多轮真实回归验证(`31-work-graph-fail-fallback-coverage.md` 的真实 LLM 端到端案例)。 **成本账**:`heartbeat-service.js` 现在的实现是**每一次 tick 都真的发一次 LLM 调用**,只是 prompt 要求"没事就回复 `[心跳正常]`"——省不了那次调用的 token/延迟。如果 N 家公司、每家每 5 分钟一次心跳,24 小时就是 N×288 次 LLM 调用,绝大多数只会换回一句"心跳正常"。 **修订建议**:boss agent 心跳**只负责调度器处理不了的判断题**,而且触发方式改成"事件优先、轮询兜底",不是纯定时轮询: 1. 判断题清单(调度器管不了、需要 LLM 判断的): - task 长时间 `blocked` 或 `failed`(预算耗尽/依赖死锁)——要不要重新分解、要不要升级给人类; - 同一 goal 下多个 task 出现优先级冲突(业务变化导致原定顺序不再合理)——要不要调整 `priority`; - `task-planning` 3 轮降级成单任务(P3C §四.9 的降级路径)——要不要人工介入重新审视这条 goal 的描述。 2. **优先事件驱动**:在 `transitionRun`/`settleGoal` 这类既有钩子里,遇到上述判断题触发的状态变化(比如 task 转 `blocked`)时,**主动**唤醒 `ceo` 处理这一条,而不是让它等下一次心跳才发现。心跳退化成**低频兜底**(比如 30 分钟一次,只扫"有没有钩子漏掉的异常"),不是主决策路径——这与现有 `task-scheduler.js` 头部注释里"兜底扫描周期"的设计哲学完全一致,照抄这个模式即可。 --- ### P4:org/work graph 产生的新 bug / 新 task 回写看板,形成闭环 **这是五个 phase 里唯一真正从零开始、也是这次讨论最初诉求("自己发现问题、自己重新规划")真正要落地的地方。** **现有的失败归因机制**(`30-task-delegation-reliability-gaps.md`):`BugTriager`/`validator-triager` 类节点已经在做四分类——`contract_mismatch` / `impl_bug` / `spec_ambiguous` / `infra_flake`——但**四条路径的终点目前都是"原地重试"或"预算耗尽后 blocked 等人工"**,没有一条走向"生成一条新 task/goal"。 **需要新增的两个通路**: 1. **确定性通路(不需要 agent 判断)**:把四分类里"显然该开新任务"的情形直接在 `routeFailure`(`graph-engine.js`)里接一条规则,而不是等 boss agent 心跳才处理——比如 `spec_ambiguous` 在 goal 级预算内重试超过阈值,直接生成一条 `status='draft'`、`created_by='system'`、挂在同一 `goal_id` 下的新 task,附上失败节点的诊断产物作为新 task 的 `description` 素材,人工/`ceo` 再审。这条不需要 LLM 判断"要不要开新任务",只需要判断"开一条什么样的"——后者才是可选的 agent 介入点。 2. **主观通路(需要 agent 判断,这是 P1 的 `ceo`/工具的用武之地)**:agent 在执行任务过程中发现的、**不在当前 task 描述范围内**的问题(比如 coder 顺手发现一个无关的历史 bug),需要一个**工具**让它"提一条新任务"而不是把不相关的修复硬塞进当前 task 的产物里。这是当前完全没有的能力。 **必须先建的地基**——查完代码,发现比"包一层工具"更底层的东西还没有,得先排清楚三件事,顺序不能反: #### 地基 0(阻塞项):`ceo` 现在物理上调不通这套 REST API `server/src/tools/tool-registry.js` 开头写得明明白白——**这是"Master Agent 工具注册中心"**,`[TOOL_CALL: xxx]` 这套 JSON 文本协议是 Master 专属的自定义解析层,`server/src/tools/devloop/` 下另有一套 DevLoop 专属的 tool-registry。`ceo` 这类普通 org node agent(走标准 Claude CLI 子进程)**两套都不属于**——它能用的只有 Claude CLI 原生的 Bash/curl,具体调用哪个接口靠 `tools.md` 里的**大白话 + curl 示例**(`task-planning/tools.md` 就是这个写法的范本)。所以"task-board-tools.js" 这个提法本身**不成立**——不需要新写一个 JS 工具模块,需要写的是一份 `system/agents/ceo/tools.md`(现在是空文件)。 但更麻烦的是:`server/routes/company.js` 顶部 `router.use(requireAuth)`(14 行),**goals/tasks 全系列端点只认人类 JWT**,没有走 `agent-chat.js` 那样的 `requireAuthOrInternal`。也就是说,就算 `ceo` 的 `tools.md` 写得再clear,它发出的每一条 curl **现在都会静默 401**——这正是最近几个 commit(`d48425a` selfChat 自环漏带内部令牌、`9e58656` bin/lemon.js task report/dispatch 401)反复修的那一类坑,company.js 这次还没打这个补丁。**这是全部 P4 工作的前提阻塞项**,必须先给 goals/tasks 这组路由加上 `requireAuthOrInternal`(或者更收紧一点,只给这几条挂内部令牌白名单),agent 才有物理上调得通的可能。 #### 地基 1:`POST /:companyId/tasks` 没有"草稿"模式,agent 提的任务会立刻烧预算 现有 `POST /:companyId/tasks`(`company.js:2077`)建完 task 行**无条件紧接着调 `enqueueTaskRun` + `startNextIfSlotFree`**——语义是"下达即执行",服务的是"人类/外部 agent 派工"这个存量场景(代码注释原话:"存量调用方的心智是下达即执行")。这条路径不适合 `ceo` 自主提的新任务:`task-planning` 拆出来的 task 尚且要先落 `draft`、人点"全部下达"才转 `queued`(P3C §四.9 的既定哲学——"一次 LLM 猜测直接烧 5 份预算"是要避免的事),`ceo` 一次判断失误直接把新任务怼进队列烧钱,风险比 task-planning 更高(task-planning 只在 goal 刚建时跑一次,`ceo` 是持续在线判断)。 **需要的改动很小**:给 `POST /:companyId/tasks` 加一个 `draft: true`(或 `status: 'draft'`)入参,为真时只调 `wdb.createTask()` 落库,跳过 `enqueueTaskRun`/`markGoalDispatched`/`startNextIfSlotFree` 三步,返回 `status: 'draft'`。已有的 `POST /:companyId/tasks/:taskId/enqueue`(`company.js:2275`,`draft → queued`)原样复用,不用新建。默认行为(不传这个参数)完全不变,纯加法,不影响存量调用方。 #### 地基 2:`ceo` 的 `tools.md` 写什么 在地基 0/1 落地后,照抄 `task-planning/tools.md` 的写法(先说"大概率不需要工具"、给出何时才用、给出 curl 示例、专辟一节"你不该用的工具"防止越权)给 `ceo` 写一份,覆盖: - **读**:`GET /:cid/goals?status=active`(扫哪些 goal 在跑)、`GET /:cid/tasks?status=blocked` / `?status=failed`(找卡住的)、`GET /:cid/goals/:goalId`(看某 goal 下全部 task + run 摘要) - **写**:`PATCH /:cid/tasks/:taskId`(改 `priority`)、`POST /:cid/tasks`(带 `draft:true`,提新任务)、`POST /:cid/tasks/:taskId/enqueue`(把自己或别人提的草稿正式下达——这一步要不要也留给人类点,是个开放问题,见 §四) - **明确不该用**:`org-graph/replan`、招聘类端点(这些是 HR/`orggraph-planning` 的事,`ceo` 判断"缺角色"之后应该是发通知/提 task,不是自己动手改组织结构) #### 确定性通路(routeFailure 里那条规则)不走这套 REST 30 号文档四分类里"显然该开新任务"的情形(比如 `spec_ambiguous` 重试超阈值),是 `graph-engine.js` 服务端内部代码,**直接调 `wdb.createTask()`**(同进程函数调用),不必经过 HTTP、不必带内部令牌,落库同样是 `draft`。这条通路和 `ceo` 的 agent 通路是并行的两条腿,共享同一个"落 draft、不自动下达"的规则,但触发方式不同(一个是引擎钩子里的确定性规则,一个是 agent 主观判断)。 **先做地基 0(鉴权)→ 地基 1(draft 模式)→ 地基 2(`tools.md`),P1/P3 的 boss agent 才有事可干**——三步不做完,`ceo` 被心跳唤醒了也只能干瞪眼,或者干瞪眼都算好的,更可能是每次尝试都吃一个静默 401。 --- ### P5:公司级目标代码目录("接管已有工作目录") **核实结论:这个能力在 DevLoop 层面已经完整实现,只是没有公司级默认值,也没有确认是否覆盖非 DevLoop 的执行路径。** `server/services/devloop/path-resolver.js` 的 `allocateProjectRoot()` 已支持三种模式(`server/routes/devloop.js` 建项目时传 `mode`/`target_path`): | mode | 语义 | 现状 | |---|---|---| | `sandbox` | 自动分配到 `data/companies/{cid}/projects/{pid}/`(lemonboss 内部) | ✅ 已实现,默认值 | | `linked-new` | 用户指定一个**空/新建**的绝对路径作为项目根,登记进 `~/.devloop/linked-projects.json` | ✅ 已实现 | | `linked-existing` | 用户指定一个**已存在**的目录,接管为项目根 | ✅ **已实现,不是文件头注释说的"预留接口拒收"**——代码实测有完整校验:路径必须存在且是目录、不能已经是别的 DevLoop 项目(`project.json` 冲突检测)、若有 `.git` 必须 working tree 干净(防止 agent 的 `git add -A` 把用户脏改动卷进自动 commit)。文件头那句注释是过期文档,**建议顺手修正**。 | 这正是用户要的"像 Claude Code 选工作目录一样接管一个已有目录",**核心机制已经有**,且安全校验(黑名单敏感路径、git 干净树)已经比较完善。 **org/work graph(非 DevLoop)路径已核实**,结论比预期更好也更麻烦,分开说: **好消息:不是两套系统**。`graph-engine.js` 有自己的 `ensureProjectRoot(ctx)`(3476 行),文件头注释写明"位置的分配**复用 DevLoop 的 `path-resolver.allocateProjectRoot`(sandbox 模式)**,不新造一套"——`allocateRunProjectRoot()`(3458 行)内部真的就是调 `require('./devloop/path-resolver').allocateProjectRoot({ company_id, mode: 'sandbox' })`。org-coder 类节点的交付物落点、`graph_files` 索引、`34号文档` 提到的"项目根",指的都是这同一套机制,不是 `locator.taskDir`(那是 agent 自己的运行 cwd,`_applyRuntimeScope` 分配的,两者不是一回事——agent 在 `locator.taskDir` 里跑,但写产物是写去 `ctx.projectRoot`,这个区分在 `graph-engine.js` 3736 行的"越界写入"检测里能看到:写到项目根之外会被判越界)。**结论:P5 只需要改一处入口(`allocateRunProjectRoot`),不需要在 org graph 侧另建一套。** **坏消息:现在的分配粒度是"每个 root run 一份全新 sandbox",不是"每个公司一份持久目录"**。`allocateRunProjectRoot()` 硬编码 `mode: 'sandbox'`,且**没有任何"复用已有项目根"的入参**——`ensureProjectRoot()` 的幂等只在**同一个 root run 内**成立(先读 `project-root.json` 再决定要不要新分配),跨 run(也就是跨 task)**永远分配一个新 `pid`**,哪怕是同一个公司、同一个 goal 下的两个 task,也会落进两个互不相干的空目录。这比"没法接管外部目录"更根本——现在**同一家公司自己的两个任务都不共享代码**,遑论接管一个外部已有仓库。 **P5 真正要做的三件事**(比原先设想的"补个配置项"要深一层): 1. **公司级配置**:`company.json` 加 `target_dir`(绝对路径,可空)+ `target_mode`(`linked-new` / `linked-existing`,配了 `target_dir` 才有意义)。 2. **改 `allocateRunProjectRoot(run)`**:读 `run.company_id` 对应的 `company.json.target_dir`。**配了就不再"每 run 分配新 pid",而是所有该公司的 run 一律解析到同一个 `root_path`**(跳过 `path-resolver` 的 pid/registry 那一整套,直接返回固定路径,首次访问时按 `target_mode` 做一次 `linked-existing`/`linked-new` 的校验——现成的校验逻辑,`path-resolver.js` 261-310 行原样复用,只是只做一次,不是每个 run 各做一次);没配 `target_dir` 就走现在的 `sandbox` 老路径,行为不变。 3. **⚠️ 并发风险,必须在设计里显式处理**:一旦多个 task 共享同一个 `target_dir`,就与前面讨论过的"跨 task 并发"(`max_parallel_tasks > 1`)正面相撞——两个 coder agent 同时写同一个 git 仓库会冲突,`linked-existing` 现有的"进入时 working tree 必须干净"只是一次性校验,不是运行期互斥锁。**建议 v1 做法**:`company.json.target_dir` 非空时,该公司的 `max_parallel_tasks` 强制钉死为 1(在 `maxActiveRootRuns()` 里加一条:读到 `target_dir` 就忽略/覆盖用户配的 `max_parallel_tasks`),从根上排除并发写冲突,比引入 git worktree/分支合并那一整套复杂度便宜得多。`ponytail`: 这是刻意的简化,天花板是"一家公司同一时间只能有一个任务真正动代码";真要多任务并发写同一个仓库,需要 git worktree + 完成时合并这一层,属于独立工作量,不在这次范围内。 **建议**:P5 独立于 P1-P4 推进(数据面完全不相关),优先级可以更高——但工作量比最初评估要大一档,不是"公司级配置 + 一次核实"就完,是"公司级配置 + 改分配粒度 + 并发约束联动"三件套。 --- ## 三、修订后的建议顺序 用户原始顺序是 P1→P2→P3→P4,本文档建议倒过来做地基: ``` 第一步:P4 地基 —— 建 task-board-tools(读写 goal/task 的 agent 工具) + routeFailure 里接确定性的「四分类 → 自动开新 task」规则 第二步:P1 收窄版 —— 给 ceo 配 heartbeat.md(监督规则,认新 goal/task 模型不是旧 OKR) + 挂上第一步的工具 第三步:P3 收窄版 —— 心跳只做异常判断,事件触发优先、轮询兜底、绝不重做调度 第四步:P4 完整版 —— 把「主观判断要不要开新 task」的部分交给已经装好工具的 ceo 第五步:P2(UI) —— 独立任务看板页面,随时可插入,风险最低 并行轨:P5 —— 公司级默认 target dir 配置 + 核实 org graph 交付物落点是否需要同样机制 ``` 理由:P1/P3 的 boss agent 在 P4 地基建好之前没有可操作的对象;P2 是纯 UI,不卡任何人;P5 数据面独立,可以完全并行推进。 --- ## 四、开放问题(已拍板,2026-08-30) 1. ~~`ceo` 会不会和已有流水线角色冲突~~ **已核实排除**:`system/org-graph-templates/{dev,content,ops}.json` 三份模板全文搜索 `ceo`,零命中——`ceo` 现在没有出现在任何一张图里,是完全没被使用的孤儿本体。给它加监督职责没有冲突。 2. **P4 确定性通路新开 task 的 `report_to`:继承原 goal 的 `report_to`**。`okr_node_id` 不用单独决定——它按 P3C §四.7 既定规则只挂在 `goals` 表,新 task 挂在同一个 `goal_id` 下自动沿用该 goal 的 OKR 归属,不存在"新开一份"的问题。 3. **心跳成本策略:事件触发为主 + 低频兜底轮询**(不加"异常持续够久才唤醒"的抖动过滤,先简单落地)。主路径挂在 `transitionRun` 等既有钩子上,异常状态一出现就唤醒;另加一个量级在 30 分钟左右的低频轮询兜底,防钩子覆盖不到的漏网情况——直接照抄 `task-scheduler.js` 头部注释里"兜底扫描周期"那个既有设计哲学,不重新发明。 4. **`ceo` 可以全权自主 `enqueue` 自己提的 draft task,不需要等人类点**——这是五个选项里最激进的一档,明确高于 `task-planning`(§四.9 定案:产出永远先落 draft,人点"全部下达")的自主权限。**这个选择意味着安全网必须落在别处,不能靠"人工审核"这一层**:落地时要确保 `max_parallel_tasks`(防止一次性并发太多)、`DEFAULT_TASK_BUDGET`(防止单任务无限烧钱)、goal 级 `org-validator` 收敛验收(防止"完成"了但没真正达成)这几道既有闸门都乘上了 `ceo` 自主创建的 task,不能因为创建路径变了就绕开——这几道闸门本来就是数据层/调度器层强制的(不挂在"谁创建的"这个字段上),照理天然覆盖,但落地时要专门写一条回归测试钉死这一点。 5. **P5 org graph 交付物落点调研:已做完**,结论见 §二 P5 小节——不是两套系统(复用同一个 `path-resolver`),但分配粒度是"每个 task 一份全新 sandbox",同公司任务之间都不共享代码,比预想的工作量更大,且与"跨 task 并发"直接联动,需要一条并发约束兜底。 6. **P4 确定性通路的实际挂载点:已实现并确认(2026-08-30)**。文档原先设想挂在 DevLoop 的四分类(`spec_ambiguous`/`contract_mismatch`)上——但那是 DevLoop 专属词汇,`graph-engine.js` 的 `routeFailure` 用的是另一套(`impl_bug`/`input_underspecified`/`skill_defect`/`infra_flake`),根本没有这两个取值。实现时改挂在 `handleBackwardEdge` 的双层硬顶熔断(单边 `loop_count>3` 或 run 级 `backward_count>5` → `force_terminated`)上——**任何**原因导致的反复回退不收敛都触发,不按 `fail_reason` 精筛。**已确认维持这个实现,不额外收窄**。副作用是良性的:`infra_flake` 走的是完全独立的原地重跑代码路径(不经过 `handleBackwardEdge`),结构上天然不会触发这条自动开新任务的规则,不需要专门加排除逻辑。落点函数:`graph-engine.js` 的 `maybeProposeReplanTask()`,挂在 `handleBackwardEdge` 熔断分支里(约 2131-2133 行)。 --- ## 六、已知遗留(实现后核实,2026-08-30) 代码落地后回查,发现两个当初设计里提过、但没有专门验证/没做完的点,记在这里而不是假装文档已经全部兑现: 1. **§四.4 定案的"回归测试钉死闸门"没有单独落地**。`ceo` 自主 `draft` + `enqueue` 走的是和人类调用完全相同的 `POST /:cid/tasks` → `POST /:cid/tasks/:taskId/enqueue` 两个端点(`max_parallel_tasks`/`DEFAULT_TASK_BUDGET`/goal 级 `org-validator` 这几道闸门都挂在数据层/调度器层,不认"谁创建的",理论上自动覆盖),但没有一条测试专门断言"ceo 创建的 task 一样会被这几道闸门拦下"——现有测试(`task-entry-draft-mode.test.js`、`task-blocked-ceo-wake.test.js`)验的是 draft 模式本身和唤醒机制,不是闸门穿透性。理论上没有绕过的口子,但没有测试钉死就不能说"已确认"。 2. **`company.json.target_dir`/`target_mode` 没有任何 API/UI 入口去设置**——现在只能手工编辑公司态目录下的 `company.json` 文件才能配置。P5 落地的是"读取并生效"这一半,"人怎么把这两个字段填进去"这一半还没做(哪怕只是一个 `PATCH /api/company/:cid` 端点)。 3. **`server/routes/graph-run.js` 漏了同一处鉴权修复,已在真实跑动中撞出来并修复(2026-08-30)**。地基 0 当时只改了 `company.js`(goals/tasks 端点),`graph-run.js`(run 的 start/stop/retry/budget 等控制端点)还是纯 `requireAuth`。真实端到端测试里 `ceo` 被唤醒后能读写 goal/task 板,但调 `/api/graph-runs/*` 处理卡住的 run 时精确撞上 401,自己在对话记录里诊断出这一点并写出了人工需要跑的 curl 命令。已修:`router`/`companyRouter` 两个挂载点都改成 `requireAuthOrInternal`,两处 `:companyId`/`:id` 的 `param` 中间件加了与 `company.js` 一致的 `req.user?.internal` 旁路,`route-auth-gate.test.js` 同步更新断言,全量测试零回归(1469/1473,剩余 4 个是核实过的 pre-existing 失败)。**这次的教训**:地基 0 的"这一步做完了"判据不能只看 `company.js` 一个文件——`ceo` 需要触达的控制面横跨两个路由文件,遗漏了没有靠代码审查发现,是靠真实跑一遍才暴露的。 --- ## 七、真跑复盘(2026-08-30,`snakegame-co` 端到端测试) P1-P5 落地后,用它做了一次真实端到端测试:建公司 → 手工配 `target_dir` 指向一个真实的空目录 → 下一个"做 HTML 版贪吃蛇游戏"的 goal → `task-planning` 真实拆分 → 全程真实 Claude CLI 调用把 org-planner(PRD/SDD/验收标准)→ org-coder(RESEARCH/PLAN/EXECUTE/VERIFY,含真实的 test/validator/impl 修正迭代)跑完,最终 `goal status: done`、`task status: completed`,产出的 `index.html` 用独立跑的 Playwright 测试验证 5/5 通过(一个可以在浏览器里直接玩的贪吃蛇游戏)。 **核心结论:1469 个单测全绿,这一次真实端到端跑动却测出了至少三个单测完全没覆盖到的真问题。** "全部测试通过"不能当"系统能用"的证据——这次最有价值的发现,没有一个是单测挑出来的,全是真实跑一遍撞出来的。 ### 7.1 真跑撞出的技术缺口 按发现顺序: 1. **服务器重启会让 in-flight 的 graph 节点变僵尸,没有任何机制能自动发现**。为了让 `graph-run.js` 鉴权修复生效重启了一次服务器,正在跑 `org-planner-sdd` 的真实 Claude CLI 子进程被连带杀掉,但 `graph_run_nodes.status` 永远停在 `running`——`ceo` 的两条唤醒路径(事件驱动 + 30 分钟兜底扫描)判据都只认 `status='blocked'`,对"运行中但背后进程已死"这个状态完全不可见。查过现成的"重启后清理残留"机制(`server/index.js` 的 `resumeIncompleteTasks()`),发现它只管 ArenaSK 的 Skill 训练任务,跟 org/work graph 毫无关系;`shutdown()` 也只关插件管理器,不碰 `graph_run_nodes`。这是这次代价最大的一个坑,恢复时手写了一段脚本直接调 `graph-run-sse.transitionNode` 强制把节点判成 `fail`(走的是引擎自己的转换机制,不是绕过系统直接改数据库),才能接上标准的 `retry` 端点继续推进——但这条恢复路径本身也暴露了:**"这个节点看着卡住了,强制判失败以便恢复"这个动作现在没有任何 REST 入口**,只能改代码。 2. **`graph-run.js` 的鉴权缺口**(见「六、已知遗留」第 3 条,已修复推送)。 3. **规划/行动/反思没有统一的可追溯位置**。`org-coder` 的四段式阶段流转(RESEARCH→PLAN→EXECUTE→VERIFY)靠调 `bin/lemon.js task transition`,打到 `agent-chat.js` 的 `/task/transition` 端点——这个端点靠自己的 `sessions` Map 找"当前活跃会话",但 `org-coder` 作为 work graph 节点跑的时候走的是 `graph-engine.js` → `AgentManager` → `session-pool.js`,是 CLAUDE.md 里早就记录过的"两个互不相干的会话池"那个坑的又一个受害者:阶段流转的业务逻辑(P0 审批拦截、OKR 子状态更新)在这条路径上**静默跳过**,`_plan.md` 这类中间产物既不进 `graph_files` 契约(这个是刻意的——validator 判定不该依赖"coder 计划写得像不像样"),也不进任何跨节点共享的轨迹,人类/`ceo` 事后想知道"这一步当时怎么想的",只能去读原始的 agent transcript。 这三条已经拆成具体的开发计划:[42-org-work-graph-reliability-followups.md](./42-org-work-graph-reliability-followups.md)。 ### 7.2 过程方法上的教训 1. **三个并行 fork 分工做 P1-P5 这个模式本身是对的**——文件边界分得干净,`git diff` 核实过没有意外重叠,最后能精确按 hunk(不是整文件)拆成三个独立 commit。值得作为以后类似规模改动的默认工作方式。 2. **"全部测试通过"不能替代真实跑一遍**——见上,这是本次复盘最核心的教训,直接决定了 41 号文档 Phase 1-2 优先做"让已经在跑的东西更可靠"而不是急着加新功能。 3. **"这一步做完了"的判据容易只验证了子集**——`graph-run.js` 的鉴权缺口就是典型:地基 0 当时的验证只覆盖了 `company.js`,`ceo` 实际需要触达的控制面横跨两个路由文件。以后给 agent 开权限类的工作,落地前要先列一份"这个角色实际会调哪些端点"的清单,逐条核对,不能默认"改了一个入口就等于都改了"。 4. **重启共享的开发服务器要谨慎**——为了让代码改动生效重启了一次服务器,没意识到这会杀掉正在跑的真实 Claude CLI 子进程,浪费了一整轮真实 LLM 调用(真金白银)。以后类似场景应该先确认有没有 in-flight 的真实执行,能等就等它到一个安全点再重启。 5. **"为什么不能自动恢复"这类追问是复盘里最值钱的部分**——第一反应往往是"手动修一下继续跑",是持续追问"这本该由谁负责、为什么没发生"才把表面的一次性意外,看清楚成系统性盲区(僵尸节点检测缺失)。这次几个最高价值的发现,都来自这种不满足于表面解释、继续往下问一层的模式,值得作为以后排查问题的默认姿势,不只是这一次的运气。