Commit b36f1c159afdc394dd86881f53ad3b5bfbd2d47b

Authored by yibo
1 parent 14cbaf53

docs: 39 号文档补真跑复盘,41 号文档拆分四 phase 可靠性待办

P1-P5 落地后做了一次真实端到端测试(建公司→下 goal→ceo 监督→真实跑出
一个 Playwright 验证 5/5 通过的贪吃蛇游戏)。1469 个单测全绿,这次真跑
却测出至少三个单测没覆盖到的真问题:服务器重启会让 in-flight 节点变
僵尸且无人能自动发现、graph-run.js 鉴权缺口(已在另一个 commit 修过)、
规划/反思轨迹没有统一可追溯位置。

39 号文档新增「七、真跑复盘」记录技术缺口和过程教训;41 号文档把复盘
沉淀的待办拆成四个 phase 的开发计划(卡住恢复闭环 → 闸门穿透性回归
测试 → target_dir 配置入口 → timeline 承接规划轨迹),按优先级排开工
顺序。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwG4b7Nq1NLR9eeE3M9Us9
docs/todo/system-design/39-boss-agent-task-supervision.md
1 1 # 39. Boss Agent 任务看板监督 + 公司目标代码目录 — 需求评估与设计草案
2 2  
3   -> **状态**:✅ P1-P5 已实现(2026-08-30,commit `a00590a` P1/P3/P4 后端、`5108d67` P2 前端、`d967ac0` P5)。本文档是落地前的设计草案,实现细节以代码为准,本文档不再更新,仅存档决策过程。**已知未覆盖的后续工作见文末新增的「六、已知遗留」**。
  3 +> **状态**:✅ P1-P5 已实现(2026-08-30,commit `a00590a` P1/P3/P4 后端、`5108d67` P2 前端、`d967ac0` P5、`14cbaf5` graph-run.js 鉴权补丁)。本文档是落地前的设计草案,实现细节以代码为准,仅存档决策过程。**已知未覆盖的后续工作见「六、已知遗留」,真实端到端跑测的复盘见「七、真跑复盘」,复盘沉淀的待办已拆成独立文档 [41-org-work-graph-reliability-followups.md](./41-org-work-graph-reliability-followups.md)(按 phase 排开工顺序)**。
4 4 > **日期**:2026-08-30
5 5 > **背景**:承接对话讨论——goal/task 两级模型([27-P3C](../../done/system-design/27-p3c-goal-task-model.md))已落地,但推进它的是纯确定性代码(`task-scheduler.js` + `transitionRun` 钩子),没有任何 agent 在持续监督这块看板、处理异常、把执行中发现的新问题回写成新任务。用户提出五阶段方案补上这一层,并追加公司级目标代码目录需求。
6 6 > **读者须知**:本文档大量引用现有代码路径核实"已有 vs 缺失",结论以文内给出的具体函数/文件为准;这些引用会随代码演进过期,落地前请重新核实。
... ... @@ -190,3 +190,29 @@
190 190 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 模式本身和唤醒机制,不是闸门穿透性。理论上没有绕过的口子,但没有测试钉死就不能说"已确认"。
191 191 2. **`company.json.target_dir`/`target_mode` 没有任何 API/UI 入口去设置**——现在只能手工编辑公司态目录下的 `company.json` 文件才能配置。P5 落地的是"读取并生效"这一半,"人怎么把这两个字段填进去"这一半还没做(哪怕只是一个 `PATCH /api/company/:cid` 端点)。
192 192 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` 需要触达的控制面横跨两个路由文件,遗漏了没有靠代码审查发现,是靠真实跑一遍才暴露的。
  193 +
  194 +---
  195 +
  196 +## 七、真跑复盘(2026-08-30,`snakegame-co` 端到端测试)
  197 +
  198 +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 通过(一个可以在浏览器里直接玩的贪吃蛇游戏)。
  199 +
  200 +**核心结论:1469 个单测全绿,这一次真实端到端跑动却测出了至少三个单测完全没覆盖到的真问题。** "全部测试通过"不能当"系统能用"的证据——这次最有价值的发现,没有一个是单测挑出来的,全是真实跑一遍撞出来的。
  201 +
  202 +### 7.1 真跑撞出的技术缺口
  203 +
  204 +按发现顺序:
  205 +
  206 +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 入口**,只能改代码。
  207 +2. **`graph-run.js` 的鉴权缺口**(见「六、已知遗留」第 3 条,已修复推送)。
  208 +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。
  209 +
  210 +这三条已经拆成具体的开发计划:[41-org-work-graph-reliability-followups.md](./41-org-work-graph-reliability-followups.md)。
  211 +
  212 +### 7.2 过程方法上的教训
  213 +
  214 +1. **三个并行 fork 分工做 P1-P5 这个模式本身是对的**——文件边界分得干净,`git diff` 核实过没有意外重叠,最后能精确按 hunk(不是整文件)拆成三个独立 commit。值得作为以后类似规模改动的默认工作方式。
  215 +2. **"全部测试通过"不能替代真实跑一遍**——见上,这是本次复盘最核心的教训,直接决定了 41 号文档 Phase 1-2 优先做"让已经在跑的东西更可靠"而不是急着加新功能。
  216 +3. **"这一步做完了"的判据容易只验证了子集**——`graph-run.js` 的鉴权缺口就是典型:地基 0 当时的验证只覆盖了 `company.js`,`ceo` 实际需要触达的控制面横跨两个路由文件。以后给 agent 开权限类的工作,落地前要先列一份"这个角色实际会调哪些端点"的清单,逐条核对,不能默认"改了一个入口就等于都改了"。
  217 +4. **重启共享的开发服务器要谨慎**——为了让代码改动生效重启了一次服务器,没意识到这会杀掉正在跑的真实 Claude CLI 子进程,浪费了一整轮真实 LLM 调用(真金白银)。以后类似场景应该先确认有没有 in-flight 的真实执行,能等就等它到一个安全点再重启。
  218 +5. **"为什么不能自动恢复"这类追问是复盘里最值钱的部分**——第一反应往往是"手动修一下继续跑",是持续追问"这本该由谁负责、为什么没发生"才把表面的一次性意外,看清楚成系统性盲区(僵尸节点检测缺失)。这次几个最高价值的发现,都来自这种不满足于表面解释、继续往下问一层的模式,值得作为以后排查问题的默认姿势,不只是这一次的运气。
... ...
docs/todo/system-design/41-org-work-graph-reliability-followups.md 0 → 100644
  1 +# 41. Org/Work Graph 可靠性收尾 —— 39 号文档真跑复盘的待办
  2 +
  3 +> **状态**:📝 待开工,未实现。
  4 +> **日期**:2026-08-30
  5 +> **背景**:[39-boss-agent-task-supervision.md](./39-boss-agent-task-supervision.md) 的 P1-P5 落地后,用它做了一次真实端到端测试(建公司 → 下 goal → `ceo` 监督 → 真实跑出一个可玩的贪吃蛇游戏,Playwright 5/5 验证通过)。**1469 个单测全绿,这次真跑却测出至少 3 个单测完全没覆盖到的真问题**——这份文档是那次复盘沉淀下来的待办,按开工顺序分四个 phase。
  6 +> **读者须知**:每个 phase 的现状描述都来自那次真实测试里实际撞到的场景,不是推测;具体文件/行号落地前按惯例重新核实。
  7 +
  8 +---
  9 +
  10 +## 一、为什么排这个顺序
  11 +
  12 +四个 phase 分两类:**Phase 1-2 是"让已经在跑的东西更可靠"**(不改变行为,只是补上恢复路径和验证),**Phase 3-4 是"补功能缺口"**(新增能力)。前者优先——真跑复盘里代价最大的坑(僵尸节点、鉴权漏挂)都是"东西本来能跑,只是卡住了没人能救"这一类,越早补上,后面几个 phase 开工时越不容易被同样的坑绊到。Phase 5(timeline 承接规划轨迹)单独放最后,因为它是结构性改动,需要先摸清 `org-coder` 之外还有哪些角色也在打那条断线的 `/task/transition`,工作量比其余四个 phase 加起来还大。
  13 +
  14 +```
  15 +Phase 1:卡住恢复闭环(僵尸节点检测 + 强制判失败端点)——两个互补的口子一起补
  16 +Phase 2:§四.4 闸门穿透性回归测试 —— 纯测试,零设计风险
  17 +Phase 3:target_dir 的 API/UI 入口 —— 纯加法
  18 +Phase 4:timeline.ndjson 承接 planning/reflection 轨迹 —— 结构性改动,排最后
  19 +```
  20 +
  21 +---
  22 +
  23 +## 二、Phase 1:卡住恢复闭环
  24 +
  25 +### 1a. 服务器重启的僵尸节点检测
  26 +
  27 +**现状(真跑撞到的)**:`graph_run_nodes.status='running'` 这一行,没有任何机制确认"背后真的还有个活的进程"。真实测试里为了让 `graph-run.js` 鉴权修复生效重启了一次服务器,正在跑 `org-planner-sdd` 的 Claude CLI 子进程被连带杀掉,但那一行的状态永远停在 `running`——不会自己变成 `blocked`,`ceo` 的两条唤醒路径(事件驱动 + 30 分钟兜底扫描)判据都是 `status='blocked'`,对这个状态完全不可见。
  28 +
  29 +查过现成的"重启后清理残留"机制:`server/index.js` 的 `resumeIncompleteTasks()` 只处理 ArenaSK 的 Skill 训练任务(`skill-task-manager`),跟 org/work graph 毫无关系;`shutdown()`(`SIGTERM`/`SIGINT` 处理)只关插件管理器,不碰 `graph_run_nodes`。两处都不覆盖这个场景。
  30 +
  31 +**设计**:启动时扫一遍 `graph_run_nodes`,`status='running'` 且 `updated_at`(或 `startedAt`)早于**本次进程启动时间**的行,判定为孤儿(整个进程树都被换过一轮,不可能有真实进程还在跑它)。判定后按 **`infra_flake` 语义**处理,不发明新状态——原地重跑逻辑本来就是为"基础设施打断,跟设计/实现对不对无关"这类场景设计的,服务器重启只是这个桶里更严重的一个成员。复用 `graph-engine.js` 已有的 `routeFailure` 里 `INFRA_FLAKE_REASON` 那条分支,不用另起一套判定/熔断逻辑。
  32 +
  33 +**落点**:`server/index.js` 启动流程(挂在 `resumeIncompleteTasks()`/`sweepStuckActiveGoals()` 同一批"开机补一次全量扫描"的地方,跟这些既有钩子形状一致),加一个 `sweepOrphanedRunningNodes()`;判定逻辑放 `graph-engine.js` 或 `work-graph-db.js`(参照 `infra_flake` 现有实现挂在哪个文件)。
  34 +
  35 +**验收**:新增测试——起一个 `status='running'`、`startedAt` 早于"当前进程启动时间"的节点,跑一次 `sweepOrphanedRunningNodes()`,断言它被转成 `infra_flake` 路径能处理的状态,且不影响 `startedAt` 晚于启动时间的正常运行节点。
  36 +
  37 +### 1b. "强制判失败以便恢复"的 REST 端点
  38 +
  39 +**现状**:`POST /:id/nodes/:nodeId/retry` 只接受 `status ∈ {fail, blocked}` 的节点。真实测试里遇到一个卡住的 `running` 节点,唯一的恢复手段是**直接写一段脚本调用 `graph-run-sse.transitionNode` 内部函数**,绕过了任何 REST 接口——这对人类操作者或 `ceo` 都不现实,两者都只能走 HTTP。
  40 +
  41 +**设计**:新增 `POST /api/graph-runs/:id/nodes/:nodeId/force-fail`,语义是"我确认这个节点已经不可能再自己完成了,把它判成 `fail`(`failReason` 可选,默认某个通用值如 `operator_intervention`),交给既有的 fail→retry/backward-edge 路径处理"。**只接受 `status='running'` 的节点**(`fail`/`blocked` 已经有 `retry` 端点了,不重复造轮子);调用方是人类还是 `ceo` 不做区分,鉴权走已有的 `requireAuthOrInternal`(跟着 Phase 1a 一起补,`graph-run.js` 这次已经全部换过了)。1a 落地后,这个端点的日常触发频率应该会显著下降(大部分"卡住"场景 1a 已经自动兜底),但保留它作为人工/`ceo` 判断"这个不是重启造成的,就是卡了"时的手动出口。
  42 +
  43 +**验收**:新增测试——`running` 节点调 `force-fail` 转 `fail` 且可被 `retry` 端点接上;`fail`/`blocked`/`pending` 状态调用返回 400(沿用 `ensureStatus` 现成的错误形状)。
  44 +
  45 +---
  46 +
  47 +## 三、Phase 2:§四.4 闸门穿透性回归测试
  48 +
  49 +**现状**:39 号文档 §四.4 已拍板"`ceo` 全权自主 `enqueue`,但 `max_parallel_tasks`/`DEFAULT_TASK_BUDGET`/goal 级 `org-validator` 这几道闸门必须照样拦住它",落地时没有补这条测试(§六"已知遗留"记过一笔)。理论上这几道闸门挂在数据层/调度器层,不认"谁创建的",天然覆盖,但没有测试钉死就不能说"已确认"。
  50 +
  51 +**设计**:不涉及任何设计决策,纯粹是补测试。新增用例(大致形状,落地时按现有测试文件的辅助函数抄写法):
  52 +1. 把公司 `max_parallel_tasks` 设成 1,已有一条 task 占着坑,`ceo` 走 `draft`+`enqueue` 建第二条——断言它排队而不是插队执行。
  53 +2. 把公司 `DEFAULT_TASK_BUDGET` 设小,`ceo` 建的任务跑到预算耗尽——断言按现有 `budget-exhausted` 路径 `blocked`,不会因为"是 agent 建的"就获得额外预算。
  54 +3. `ceo` 建的多 task goal 全部 `completed` 后,断言 goal 级 `org-validator` 照样被调一次(不因创建者是 agent 而跳过收敛验收)。
  55 +
  56 +**落点**:新建 `server/tests/unit/ceo-created-task-gates.test.js`,或并入现有 `task-blocked-ceo-wake.test.js`(按当时哪个更顺手)。
  57 +
  58 +---
  59 +
  60 +## 四、Phase 3:`target_dir` 的 API/UI 入口
  61 +
  62 +**现状**:P5 落地的是"配了 `company.json.target_dir` 就生效"这一半,"人怎么把这两个字段填进去"这一半没做——真实测试里是手工编辑公司态目录下的 JSON 文件配的。
  63 +
  64 +**设计**:
  65 +- **后端**:`PATCH /api/company/:companyId`(如果这个端点已存在就扩展,不存在就新建)接受 `target_dir`/`target_mode`,写回 `company.json`。校验复用 `path-resolver.js` 的 `validateLinkedTarget`(39 号文档 P5 已经抽出来的共享校验函数),配置阶段就能提前报错(不用等到第一次 `ensureProjectRoot` 才发现路径不对)。
  66 +- **前端**:公司设置页面(或 `TaskBoard.vue`/`GraphConsole.vue` 附近,具体挂哪个页面待定)加一个表单,填目录路径 + 选 `linked-new`/`linked-existing`。
  67 +
  68 +**验收**:新增测试覆盖 `PATCH` 端点的校验分支(合法路径写入成功、`isSensitivePath` 命中黑名单拒绝、`linked-new` 目录非空拒绝);前端至少过一次真实点击验证表单能提交成功。
  69 +
  70 +---
  71 +
  72 +## 五、Phase 4:`timeline.ndjson` 承接 planning/reflection 轨迹
  73 +
  74 +**现状(真跑撞到的,这是四个 phase 里最大的一个)**:`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 里早就记录过的"两个互不相干的会话池"那个坑的又一个受害者。结果:`org-coder` 在 work graph 里调这条流转指令时,`sessions.entries()` 根本找不到它的会话,阶段流转的业务逻辑(P0 审批拦截、OKR 子状态更新)**静默跳过**;它的中间产物(`_research.md`/`_plan.md`/`_lessons.md`)不进 `graph_files` 契约(这个是刻意的,见下"不改的部分"),但也**不进任何跨节点/跨角色共享的轨迹**,人类或 `ceo` 事后想搞清楚"这一步当时是怎么想的",只能去读原始的 agent 会话 transcript(`GET /:id/nodes/:nodeRunId/transcript`),没有结构化的入口。
  75 +
  76 +**不改的部分(上一轮已经讨论定案,写在这里防止实现时跑偏)**:
  77 +- `_plan.md` 这类中间产物**不进 `needs`/`produces` 契约**,`org-validator` 判定依据仍然只认 `acceptance.md` + `artifact`/`log`。理由:validator 判定"代码对不对",不该依赖"coder 计划写得像不像样",这条边界一旦模糊,两个坏结果都会发生——coder 学会把计划写漂亮而不是把代码写对;`graph_files` 的契约面失控(这个仓库反复栽在"两套体系不统一"上)。
  78 +- 本 phase 只解决**可见性**(人类/`ceo` 事后能不能追溯),不解决**契约**(下游节点该不该依赖)——这是两件事,上一轮讨论已经拆开过。
  79 +
  80 +**设计**(需要先做的调研,落地前必做):
  81 +1. **调研范围**:`org-coder` 是这次撞到的一个,不代表只有它——扫一遍 `system/agents/*/agent.md` 里还有哪些角色的提示词也在调 `bin/lemon.js task transition`,把范围摸清楚再动手,不要只按 `org-coder` 这一个样本改。
  82 +2. **迁移方向**:把"阶段流转"这个动作,从打 `agent-chat.js` 的 `/task/transition`(legacy,不认 graph run),改成写一条 `timelineNote()`(`graph-engine.js` 已有机制,per-run、append-only、永不改已写行),带上 `phase`(research/plan/execute/verify 或角色自定义的阶段名)+ `artifact` 路径。**具体是改 `bin/lemon.js task transition` 这条命令本身去打新端点,还是给它接一条新命令**,两种做法都要过一遍现有 agent 提示词的改动成本再定——这些提示词文件散在几十个 `agent.md` 里,改一条命令的调用方式等于牵动所有引用它的本体。
  83 +3. **P0 审批拦截 / OKR 子状态更新这两块旧逻辑怎么处理**:`/task/transition` 里现在还挂着 P0 任务拦截、OKR 子状态更新这两段业务逻辑——这两者本身是不是还需要(尤其 OKR 那段,跟 P3C 的新 goal/task 模型是不是还有效对应),需要单独判断,不能假设"迁移流转机制"就等于"这两段逻辑原样照抄"。
  84 +
  85 +**验收**(待设计细化后补全,这里先占位):至少要有一条测试证明——`org-coder`(或调研出的任意一个角色)在 work graph 里完成一次阶段流转后,`timeline.ndjson` 里能查到对应的 `phase`+`artifact` 记录,且这条记录不影响 `graph_files`/`needs`/`produces` 的既有行为(回归测试守住"不改的部分"那条边界)。
  86 +
  87 +---
  88 +
  89 +## 六、开放问题
  90 +
  91 +1. Phase 1a 的孤儿判定条件(`startedAt` 早于进程启动时间)在多进程/多副本部署下是否还成立?现在是单进程部署,这条先按当前架构写,多副本场景不在这次范围内。
  92 +2. Phase 3 的 `PATCH /api/company/:companyId` 端点如果本来就不存在,是新建这一个端点,还是这次顺手看一下公司配置类的改动有没有更合适的既有入口——落地前查一遍 `company.js` 现状再定,不在这里预判。
  93 +3. Phase 4 的调研如果发现有 10+ 个角色都在用旧的 `/task/transition`,工作量会明显超出"一个 phase"的量级,届时可能需要再拆细——先按"调研完再评估"处理,不在这里预先估工作量。
... ...