Commit ab43ffce5d96d26bf636f99b80c6df69be78348a

Authored by yibo
1 parent 14d83bc2

feat(goals): dispatch 拒绝未经人工审阅的 CEO 自主立项草稿

51 号文档阶段 A 上线后发现:CEO 心跳自主立项建出的草稿(split:false)其实自动带 1 条
draft task,dispatch 技术上立刻能跑,原以为的"零 task 闸门"并不存在——安全性曾经完全靠
ceo/tools.md 里"不许自己调 dispatch"这一句提示词约束,没有代码层面的东西挡着。

POST /goals/:goalId/dispatch 新增判断:created_by==='ceo' 且 updated_at===created_at
(自建成后从没被 PATCH 过)直接 409。复用既有时间戳字段代理"有没有人碰过",不加新列——
已知简化(ponytail 注释标了天花板:分不清"真审过"和"随手碰一下",精确区分需要专门的
reviewed_at 列,留给阶段 B)。

task-queue-routes.test.js 新增两条用例:未审阅的 ceo 草稿 dispatch 409、PATCH 一次后放行;
human/未填 created_by 的草稿不受影响。相关四个测试文件 84 条用例 + 全量 unit 套件跑过,
无回归(4 个不相关的预存在失败:工作区不干净的环境闸门 + 两个与本次改动无关的仓库卫生检查)。

51 号文档 §四/七/八、docs/README.md 索引同步更新。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWsH2YBwBWTsQggPaPbsKJ
docs/README.md
... ... @@ -70,7 +70,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d
70 70 | [36-prd-agent-methodology-training-feasibility.md](./todo/system-design/36-prd-agent-methodology-training-feasibility.md) | 📝 **调研 + 阶段一/二均已完成**。回应"落地页太单薄是不是该用 Innovator 先训练专业 agent、再用 Dream 持续迭代"——认同方向。**阶段一**:`graph-engine.js` 结算节点后把 `success`/`fail:<reason>` 写进本体 `memory.md`(此前只有人类对话路径会写,Dream 天天空转)。**阶段二**:新建独立试验节点 `prd-methodology-trial` 走真实 Hunt→Digest→Evolve,2 轮收敛(96/97 分,真实基线 50 分),三个真实历史项目反推验证方法论有效;过程中连带修复同一个 2026-08-16 改名批次遗留的 5 处 `innovator`→`org-innovator` 死链(后端两处硬编码、前端"重新训练"按钮、`/api/agent/list` 展示 id 来源的四个 `config.json`),以及 `AgentScaffold` 默认模板一处裸相对路径缺陷。结论:FDE 对方法论类角色确实管用,但收敛速度/验收严谨度依赖任务书显式写明的自纠正要求,非免人工黑盒。环节 3(训练产物 → 雇佣本体转换)尚未开工 |
71 71 | [49-antigravity-inspired-parallel-verification-proposal.md](./todo/system-design/49-antigravity-inspired-parallel-verification-proposal.md) | 📝 **设计讨论,不代表已拍板**(2026-09-04)。对照 Google Antigravity「Teamwork」多智能体框架博客文章,给 27 号 P4「真并行」与 34/35 号「验证严谨度不稳定」两个议题出的方案草稿。**先纠正一个前提**:重读 27-P4 全文后发现阶段 A/B/C 早已全部实现(又一次「文档落后于代码」),当时判断真并行只剩 T2/role 列/`advanceTree` 兄弟子 run 并发三件小事——2026-09-08 收口时又发现这个判断本身有两处误判(role 列其实早就落库、T1 的✅遗漏了 scope 参数的生产接线),三件小事(连同两处误判一起)已全部核实/补齐,见 [27-P4](./done/system-design/27-p4-parallel-and-artifacts.md)。**新提案 A**:借鉴长证明模式的「竞争候选+对抗批评+综合」,区别于现有 `executor-fanout.json` 的分解型 fan-out——需要先有真实场景验证价值,不建议凭空立项,详细设计见 [50 号文档](./todo/system-design/50-competing-candidates-critic-synthesis-design.md)。**新提案 B**:把 35 号提案 D「浏览器关键词命中才要证据」泛化成 benchmark 段全维度默认要求执行级证据(**唯一建议现在推进的部分**);实地核实过把验收路径引导到 `test_script` 段的工作量后**结论是不做**——撞上 `test_script.path` 解析根不含 `org-coder` 实际写代码的项目根这个结构性冲突,改造面远超预期;新增「陷阱注册表」,把 validator 判卷失败的结构化原因喂回 Dream 机制,落点已定案为独立文件 `known_pitfalls.md`(不复用 `memory.md`,避免语义混淆)。建议推进顺序:陷阱注册表最低成本先做,证据泛化次之,竞争候选需要真实场景才立项 |
72 72 | [50-competing-candidates-critic-synthesis-design.md](./todo/system-design/50-competing-candidates-critic-synthesis-design.md) | 📝 **设计草稿,不代表已拍板**(2026-09-04)。49 号文档提案 A 的展开:竞争候选骨架 JSON 形状草稿(`candidate-council.json`,未定案)、命名(按 27-NAMING 节点带前缀/本体不带前缀两轴)、`workgraph-planning` 选型判据方向。**关键设计约束**:critic(评判)与 synthesis(择优/融合)必须拆成两个节点,依据是仓库真实事故——`qa-judge` 早期"评判+终态判决合一"设计导致过一次无限循环(20 次调用/31 分钟才熔断,记录在 `deliverable.json` 的反模式说明里),新骨架延续 `validator-triager`/`benchmark-validator` 已验证的"归因/打分/判定三者分离"范式。**预算风险量化**:N 路候选并行消耗 N 倍预算,且 `checkBudget` 只在批量拾取前查一次,实际可能超支到 `maxConcurrency-1`(默认最多多花 4 次调用)才被拦下,候选数 N 目前代码层没有硬顶 |
73   -| [51-ceo-autonomous-goal-refill-design.md](./todo/system-design/51-ceo-autonomous-goal-refill-design.md) | ✅ **阶段 A 已实现**(2026-09-09 设计 / 2026-09-17 实现+实测)。回应"公司能否 7×24 自转"的提问——核实执行层(存活扫描/僵尸 goal 收口/CEO 阻塞任务兜底心跳)已经扎实,真正缺口在决策层:`ceo/heartbeat.md` 与 `tools.md` 原本"该提新任务"判断全部要求先有一个 goal/task 存在,没有一条覆盖"公司没有任何 active/draft/planning goal"这种完全空转场景;CEO 也原本**没有创建 goal 的工具**(`POST /:companyId/goals` 存在于路由层但从未暴露给它)。已接线:心跳先查 `GET /goals` 判空转 → 查公司使命("待定义"时拒绝立项转人工)→ 去重 → `POST /goals` 建 `split:false` + `created_by:'ceo'` 的草稿。**起服务实测(不只是读代码)纠正了两处判断**:① 原设计的多状态/`created_by` 查询参数服务端不支持,改客户端过滤;② 最初以为零 task 的草稿会让 `/dispatch` 天然 409、这就是审批闸门——实测发现 `split:false` 其实自动带 1 条 draft task,`dispatch` 技术上立刻能成功,**没有代码层闸门**,阶段 A 的安全性改为完全依赖 `ceo/tools.md` 里"不许自己调 dispatch"这条提示词约束,是当前最脆弱的一环(见四)。对所有公司默认生效,未改任何路由/服务层代码,纯 agent 人设文档改动(见七)。阶段 B(CEO 自动 dispatch + guardrail + 公司级开关)仍是设计草稿,未实现 |
  73 +| [51-ceo-autonomous-goal-refill-design.md](./todo/system-design/51-ceo-autonomous-goal-refill-design.md) | ✅ **阶段 A 已实现 + 代码闸门已补上**(2026-09-09 设计 / 2026-09-17 实现+实测 / 2026-09-18 代码闸门)。回应"公司能否 7×24 自转"的提问——核实执行层已经扎实,真正缺口在决策层:`ceo/heartbeat.md`/`tools.md` 原本没有一条覆盖"公司没有任何 active/draft/planning goal"这种完全空转场景,CEO 也原本没有创建 goal 的工具。已接线:心跳查空转 → 查公司使命("待定义"拒绝转人工)→ 去重 → `POST /goals` 建 `split:false` + `created_by:'ceo'` 的草稿。**实测(不只是读代码)纠正了两处判断**:多状态/`created_by` 查询参数服务端不支持,改客户端过滤;`split:false` 其实自动带 1 条 draft task、`dispatch` 技术上立刻能成功——最初设想的"零 task 闸门"不存在。**2026-09-18 补上真正的代码闸门**:`POST /goals/:goalId/dispatch` 新增判断,`created_by==='ceo'` 且 `updated_at===created_at`(从没被 `PATCH` 过)直接 409,用既有字段代理"有没有人碰过",不加新列(已知简化,见文内 ponytail 说明,阶段 B 会换成更精确的 `reviewed_at`);`task-queue-routes.test.js` 新增 2 条用例,相关 4 个测试文件 84 条用例全绿无回归。阶段 B(CEO 自动 dispatch + guardrail + 公司级开关)仍是设计草稿,未实现 |
74 74  
75 75 ## todo/future/(暂缓,非当前重心)
76 76  
... ...
docs/todo/system-design/51-ceo-autonomous-goal-refill-design.md
1 1 # 51 · CEO 心跳自主立项设计——补齐"目标耗尽后无人接续"的缺口
2 2  
3   -> **状态**: ✅ **阶段 A 已实现**(2026-09-17,见七)。阶段 B(CEO 自动 enqueue + guardrail)
4   -> 仍是设计草稿,不代表已拍板,公司级配置字段形状、guardrail 数值需要单独立项再定案。
5   -> **日期**: 2026-09-09(设计)/ 2026-09-17(阶段 A 实现)
  3 +> **状态**: ✅ **阶段 A 已实现 + 代码闸门已补上**(2026-09-17 实现,2026-09-18 加代码闸门,见七/八)。
  4 +> 阶段 B(CEO 自动 dispatch + guardrail)仍是设计草稿,不代表已拍板,公司级配置字段形状、
  5 +> guardrail 数值需要单独立项再定案。
  6 +> **日期**: 2026-09-09(设计)/ 2026-09-17(阶段 A 实现)/ 2026-09-18(代码闸门)
6 7 > **背景**: 回应"设计一个 goal,让 company 能 7×24 小时自我运行,差距还有多远"的提问——
7 8 > 结论是执行层(graph 引擎的存活扫描 / 僵尸 goal 收口 / CEO 阻塞任务兜底心跳)已经相当扎实,
8 9 > 真正的缺口在**决策层**:没有任何机制在"公司没有任何活着的目标"时提出下一个目标。
... ... @@ -189,9 +190,13 @@ task&quot;这道技术闸门**不存在**——`split:false` 唯一确定省下的是
189 190 `POST /goals {split:false}` 才发现同步路径其实自动带了 1 条 `draft` task
190 191 (`company.js:1859-1862`),`dispatch` 判的是"有没有 `draft` 状态的 task",1 条就满足,
191 192 **技术上立刻能跑,没有任何接口层面的东西挡着**。原先"这就是审批闸门本身"的结论是错的,
192   - 已重写进 3.3/3.5 节:阶段 A 的唯一把关变成了"`ceo/tools.md` 明确写死 CEO 不许自己调
193   - `dispatch`",一条纯提示词约束,不是代码层面的硬闸门。**这是本文档目前最大的剩余风险**——
194   - 这条约束是否真的会被 CEO 稳定遵守,只能靠实际跑几轮观察,读代码/读提示词都验证不了。
  193 + 已重写进 3.3/3.5 节。
  194 +- ~~阶段 A 的唯一把关是提示词约束,不是代码闸门,是当前最大的剩余风险~~ **已加代码闸门
  195 + (2026-09-18,见七)**:`dispatch` 路由现在会判 `goal.created_by === 'ceo' &&
  196 + goal.updated_at === goal.created_at`(自建成后从没被 PATCH 过),命中直接 409。
  197 + `ceo/tools.md` 里"不许自己调 dispatch"那条提示词约束继续保留(纵深防御,不指望单一层),
  198 + 但已经不是唯一的把关手段了。这道闸门本身是一个已知的简化(见七的 ponytail 说明),
  199 + 不是万能的"确实被人认真审过"判据。
195 200 - **心跳频率与立项频率耦合**:3.4 节的去重能防止无限堆草稿,但没有解决"CEO 多久该重新评估一次
196 201 公司使命有没有新方向"这个更长期的问题——目前设计只覆盖"完全空转"这个最紧迫场景,节奏性的
197 202 战略复盘不在本文档范围(见二、非目标)。
... ... @@ -251,3 +256,43 @@ task&quot;这道技术闸门**不存在**——`split:false` 唯一确定省下的是
251 256 - 未改动任何路由 / 服务层代码——`POST /:companyId/goals`、`.../dispatch`、`listCompanyGoals`
252 257 均按现状直接复用,阶段 A 完全是 agent 人设文档层面的改动。
253 258 - 阶段 B(公司级自转开关、guardrail 数值、CEO 自动 enqueue)未实现,见五、明确不做。
  259 +
  260 +---
  261 +
  262 +## 八、代码闸门实现记录(2026-09-18)
  263 +
  264 +补上七、第 2 点标出的缺口——把"CEO 不许自己调 dispatch"从纯提示词约束升级成真正的代码闸门。
  265 +
  266 +**改动**:`server/routes/company.js` 的 `POST /:companyId/goals/:goalId/dispatch`
  267 +(`requireGoal` 之后、原有"没有待下达的 task"检查之前)新增一段判断:
  268 +
  269 +```js
  270 +if (goal.created_by === 'ceo' && goal.updated_at === goal.created_at) {
  271 + return res.status(409).json({ error: `goal ${goal.id} 是 CEO 自主立项的草稿,还没有人工审阅过,不能直接下达`, ... });
  272 +}
  273 +```
  274 +
  275 +**判据选择**:`updated_at === created_at`(自建成后从没被 `PATCH` 过)代理"有没有人碰过",
  276 +不新增数据库列。依据:
  277 +- `createGoal`(`work-graph-db.js:1800-1825`)在 INSERT 时把 `created_at`/`updated_at`
  278 + 设成同一个 `now` 值;`updateGoal`(走既有的 `PATCH /:companyId/goals/:goalId` 端点)每次
  279 + 调用都会把 `updated_at` 刷新——两者结合,"从没被 PATCH 过"和"两个时间戳相等"严格等价。
  280 +- 已核实 `split:false` 同步创建 goal+task 的路径(`insertTasksForGoal`)只写 `tasks` 表,
  281 + 不会碰 `goals` 行——不会出现"建完就已经被判定为碰过"的假阳性。
  282 +- `ponytail`:这是"有没有被 PATCH 过"代理"有没有被认真审过"的简化,天花板是"PATCH 一次
  283 + 同值字段"也会被判定为"审过",分不清"真看懂了"和"随手碰一下解锁"。要分清这两者需要专门的
  284 + `reviewed_by`/`reviewed_at` 列(谁在什么时候明确按了"我看过了"),留给阶段 B 一起做——阶段 B
  285 + 本来就要给"CEO 自己 dispatch"设计更细的 guardrail,那时候一并设计更精确的审阅判据更划算,
  286 + 不为这一步单独开一次 schema 改动。
  287 +
  288 +**测试**:`server/tests/unit/task-queue-routes.test.js` 新增两条用例(复用文件既有的 org
  289 +graph + `newGoal` 测试夹具,无需新建 harness):`created_by:'ceo'` 且未 `PATCH` 过的草稿
  290 +直接 dispatch → 409,`PATCH` 一次后 dispatch → 201;`created_by` 是 `'human'`/不填的草稿不受
  291 +这道闸门影响,照常直接 dispatch(防止误伤既有行为)。相关四个测试文件(`task-queue-routes`/
  292 +`company-goal-task-routes`/`goal-task-acceptance`/`task-dispatch-store`,共 84 条用例)全部
  293 +通过,无回归。
  294 +
  295 +**对三、3.5 节的影响**:阶段 A 现在是纵深防御——`ceo/tools.md` 的提示词约束是第一层(CEO
  296 +按设计根本不该去调 `dispatch`),本节的代码闸门是第二层(万一提示词约束失效,`created_by`
  297 +匹配的草稿在没被 `PATCH` 过之前技术上也跑不起来)。`ceo/tools.md` 的措辞保留不变,不因为
  298 +有了代码闸门就放松对 CEO 的行为要求。
... ...
server/routes/company.js
... ... @@ -1982,6 +1982,22 @@ router.post(&#39;/:companyId/goals/:goalId/dispatch&#39;, (req, res) =&gt; {
1982 1982 const goal = requireGoal(req, res);
1983 1983 if (!goal) return;
1984 1984  
  1985 + // 51 号文档「CEO 自主立项」:CEO 心跳在公司空转时会自己起草一条 `created_by:'ceo'` 的
  1986 + // goal 草稿,设计上这条草稿必须等人审过才能跑——但草稿本身(`split:false`)建出来就已经
  1987 + // 带着 1 条 draft task,技术上立刻能 dispatch,原设计以为的「零 task 挡住」并不存在(见
  1988 + // 51 号 §四)。这里补上真正的代码闸门:`updated_at === created_at` 说明这一行自建成后
  1989 + // 从没被 PATCH 过,没人碰过。
  1990 + // ponytail: 用「有没有被 PATCH 过」代理「有没有被人审过」,是省一次 schema 改动的简化——
  1991 + // 天花板是「PATCH 一次同值字段」也会被判定为「审过」,判不出内容有没有真被看懂。真要分清
  1992 + // 「审过」和「随手碰过」,需要专门的 `reviewed_by`/`reviewed_at` 列,留给阶段 B 一起做。
  1993 + if (goal.created_by === 'ceo' && goal.updated_at === goal.created_at) {
  1994 + return res.status(409).json({
  1995 + error: `goal ${goal.id} 是 CEO 自主立项的草稿,还没有人工审阅过,不能直接下达`,
  1996 + hint: `先 PATCH /api/company/${companyId}/goals/${goal.id}(哪怕只是确认一下 title/priority)`
  1997 + + `做一次人工确认,再重试 dispatch`,
  1998 + });
  1999 + }
  2000 +
1985 2001 const drafts = wdb.listGoalTasks(goal.id).filter((t) => t.status === 'draft');
1986 2002 if (!drafts.length) {
1987 2003 return res.status(409).json({
... ...
server/tests/unit/task-queue-routes.test.js
... ... @@ -176,6 +176,33 @@ describe(&#39;批 3 排队 · HTTP 端点(27-P3C §六.3)&#39;, () =&gt; {
176 176 });
177 177  
178 178 // ────────────────────────────────────────────────────────────────
  179 + // 51 号文档:CEO 自主立项的草稿,没人碰过不许 dispatch
  180 + // ────────────────────────────────────────────────────────────────
  181 +
  182 + it('created_by=ceo 且没被 PATCH 过的草稿,dispatch 直接 409;PATCH 一次之后放行', async () => {
  183 + const cid = newCompany();
  184 + const goal = await newGoal(cid, [{ description: '干活' }], { created_by: 'ceo' });
  185 +
  186 + const [blockedStatus, blockedBody] = await call('POST', `/api/company/${cid}/goals/${goal.goalId}/dispatch`);
  187 + expect(blockedStatus).toBe(409);
  188 + expect(blockedBody.error).toMatch(/CEO 自主立项/);
  189 +
  190 + const [patchStatus] = await call('PATCH', `/api/company/${cid}/goals/${goal.goalId}`, { priority: 'P0' });
  191 + expect(patchStatus).toBe(200);
  192 +
  193 + const [dispatchStatus, dispatchBody] = await call('POST', `/api/company/${cid}/goals/${goal.goalId}/dispatch`);
  194 + expect(dispatchStatus).toBe(201);
  195 + expect(dispatchBody.started.taskId).toBe(goal.tasks[0].taskId);
  196 + });
  197 +
  198 + it('created_by=human(或不填)的草稿不受这道闸门影响,照常直接 dispatch', async () => {
  199 + const cid = newCompany();
  200 + const goal = await newGoal(cid, [{ description: '干活' }]); // 不传 created_by
  201 + const [status] = await call('POST', `/api/company/${cid}/goals/${goal.goalId}/dispatch`);
  202 + expect(status).toBe(201);
  203 + });
  204 +
  205 + // ────────────────────────────────────────────────────────────────
179 206 // 验收 3:409 消失
180 207 // ────────────────────────────────────────────────────────────────
181 208  
... ...
system/agents/ceo/tools.md
... ... @@ -124,7 +124,8 @@ draft task 謖ょ惠霑吩クェ goal 荳銀披比ク肴弍髮カ task縲りソ呎э蜻ウ逹霑吩クェ goal
124 124 `POST /goals/<goalId>/dispatch`**窶披皮ォ矩。ケ讓。蠑丈ク倶ス逧′雍」蛻ー"蟒コ螳瑚拷遞ソ"荳コ豁「形dispatch` 霑吩ク豁・
125 125 豌ク霑懃蕗扈吩ココ邀サ蜴サ謖峨ょ悪謚"謠蝉ク譚。譁ー莉サ蜉。"驍」闃る"菴陲ォ謗域揀蜿ッ莉・閾ェ荳サ enqueue"逧揀髯千アサ豈泌芦霑咎
126 126 驍」譏ッ蟇ケ逹莠コ邀サ蟾イ扈冗恚霑噪 goal 荳句刈逧菴 task瑚ソ咎譏ッ菴閾ェ蟾ア蜃ュ菴ソ蜻ス迪懷譚・逧眠譁ケ蜷托シ御ク、莉カ莠
127   -鬟朱勦郤ァ蛻ォ荳堺ク譬キ御ク崎螂礼畑蜷御ク譚。譚剞縲
  127 +鬟朱勦郤ァ蛻ォ荳堺ク譬キ御ク崎螂礼畑蜷御ク譚。譚剞縲ゑシ2026-09-18 襍キ `dispatch` 遶ッ轤ケ閾ェ蟾ア荵滉シ壽拠扈晁ソ咏ァ
  128 +豐。陲ォ `PATCH` 霑噪 `created_by:'ceo'` 闕臥ィソ窶披比スソ呎弍蜈懷コ包シ御ク肴弍蜈∬ョク菴蜴サ隸慕噪逅罰縲ゑシ
128 129  
129 130 **`decomposed_work` 蟄蝉ササ蜉。襍ー蜷御ク荳ェ遶ッ轤ケ**窶披覗subtask-planning` 蟾イ扈乗滑蜀ョケ諡・ス莠シ御ス荳咲畑蜀榊愛譁ュ
130 131 "貂ク肴ク・"悟宵蛻、譁ュ闃ょ・擾シ育鴫蝨ィ謾セ霑俶弍遲会シ峨ょ酔荳譚。 task 荳狗噪蟄蝉ササ蜉。蜿ッ莉・荳谺。諤ァ蜈ィ驛ィ enqueue
... ...