Commit 14d83bc2c801b18665afd9898abee4bb6afd7198
1 parent
20030e77
feat(ceo): CEO 心跳自主立项(阶段 A)——公司空转时补一条草稿 goal
公司没有任何 active/draft/planning goal 时,CEO 心跳原本会在第一步查询就拿到空列表、 直接回复"心跳正常",永久停摆、没有信号升级给人类。心跳清单加一步空转检测,命中后走 使命前置闸门(company-context.md 的"公司使命"是"待定义"就拒绝立项、转人工)+ 去重 + POST /goals(split:false, created_by:'ceo') 建草稿,CEO 到此为止、不自己调 dispatch。 起服务实测(不只是读代码)推翻了设计草稿阶段的两处判断:GET /goals 不支持逗号分隔多状态 查询、也不支持按 created_by 过滤(改客户端过滤);split:false 建出的草稿其实自动带 1 条 draft task、dispatch 技术上立刻能成功,不存在"零 task 逼人工审批"这道代码闸门——阶段 A 的安全性因此完全押在 tools.md 里"不许自己调 dispatch"这条提示词约束上,是当前最脆弱的 一环,见 51 号文档 §四。 未改任何路由/服务层代码,纯 agent 人设文档改动。阶段 B(CEO 自动 dispatch + guardrail + 公司级开关)留白未实现。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AWsH2YBwBWTsQggPaPbsKJ
Showing
3 changed files
with
311 additions
and
1 deletions
docs/todo/system-design/51-ceo-autonomous-goal-refill-design.md
0 → 100644
| 1 | +# 51 · CEO 心跳自主立项设计——补齐"目标耗尽后无人接续"的缺口 | |
| 2 | + | |
| 3 | +> **状态**: ✅ **阶段 A 已实现**(2026-09-17,见七)。阶段 B(CEO 自动 enqueue + guardrail) | |
| 4 | +> 仍是设计草稿,不代表已拍板,公司级配置字段形状、guardrail 数值需要单独立项再定案。 | |
| 5 | +> **日期**: 2026-09-09(设计)/ 2026-09-17(阶段 A 实现) | |
| 6 | +> **背景**: 回应"设计一个 goal,让 company 能 7×24 小时自我运行,差距还有多远"的提问—— | |
| 7 | +> 结论是执行层(graph 引擎的存活扫描 / 僵尸 goal 收口 / CEO 阻塞任务兜底心跳)已经相当扎实, | |
| 8 | +> 真正的缺口在**决策层**:没有任何机制在"公司没有任何活着的目标"时提出下一个目标。 | |
| 9 | +> **前置阅读**: [27-goal-contract-format.md](./27-goal-contract-format.md)(goal/task 数据模型) | |
| 10 | + | |
| 11 | +--- | |
| 12 | + | |
| 13 | +## 一、问题现状(已核实的证据链) | |
| 14 | + | |
| 15 | +### 1.1 心跳机制只监督"已存在的目标",不处理"目标列表是空的" | |
| 16 | + | |
| 17 | +`server/services/heartbeat-service.js:221-229` 发给每个 agent 的通用心跳文案是: | |
| 18 | + | |
| 19 | +> "请根据你的身份和 heartbeat.md 规则,扫描当前最新的全局 OKR 目标状态和节点。如果发现某个 | |
| 20 | +> 负责人超时、卡点或产出需要仲裁,请主动使用 dispatch 工具给他们发送通知或驳回。如果一切正常, | |
| 21 | +> 无事发生,请直接回复'[心跳正常]'" | |
| 22 | + | |
| 23 | +这条文案的前提是"存在目标/节点可扫描"。CEO 把它覆写成了具体清单(`system/agents/ceo/heartbeat.md`): | |
| 24 | + | |
| 25 | +- 查 `GET /tasks?status=blocked` 和 `?status=failed` | |
| 26 | +- 查 `GET /goals?status=active`,看有没有该调优先级的 | |
| 27 | +- 查 `GET /tasks?subtaskKind=decomposed_work&status=draft`,看有没有等派发的子任务 | |
| 28 | +- 都正常就回复"心跳正常" | |
| 29 | + | |
| 30 | +四条全部是**对已存在目标/任务的处理**。没有一条覆盖"`active`/`planning`/`draft` 状态的 goal | |
| 31 | +一条都没有"这种情况——按现有规则,这种情况下 CEO 会在第一步查询就拿到空列表,然后直接走到 | |
| 32 | +"都正常,回复心跳正常",公司就此长期停摆,没有任何信号会升级给人类。 | |
| 33 | + | |
| 34 | +### 1.2 "该提新任务"的判断范围明确限定在"已有 goal 出问题" | |
| 35 | + | |
| 36 | +`system/agents/ceo/tools.md:143-155`「什么时候该提新任务、什么时候该改优先级」一节列出的 | |
| 37 | +三种触发条件——task 长期 blocked/failed、同 goal 下多个 task 优先级冲突、task-planning 降级成 | |
| 38 | +单任务——全部要求**先有一个 goal/task 存在**才能触发判断。没有"公司层面没有任何 active goal | |
| 39 | +时该怎么办"这一条。 | |
| 40 | + | |
| 41 | +### 1.3 CEO 目前没有创建 goal 的工具,只有创建 task 的工具 | |
| 42 | + | |
| 43 | +`system/agents/ceo/tools.md` 里对 `/goals` 的全部使用示例都是 `GET`(第 14/30/44 行),写操作 | |
| 44 | +只有 `POST /:companyId/tasks`(给已存在的 goal 加草稿任务)和 `POST /goals/:goalId/dispatch` | |
| 45 | +(入队)。`POST /:companyId/goals`(`server/routes/company.js:1794`,建全新 goal)这个端点**存在 | |
| 46 | +于路由层,但从未出现在 CEO 的工具文档里**——即便 CEO 的心跳会话拿着有效的 | |
| 47 | +`x-internal-token`(`ceo/tools.md:11-16`,认证机制已就绪),它也不知道这个端点存在,更不知道 | |
| 48 | +该怎么用。这是比"决策逻辑缺失"更基础的一层缺口:**能力本身没有暴露给它**。 | |
| 49 | + | |
| 50 | +### 1.4 没有"公司使命"之外的任何立项锚点,且默认值是"待定义" | |
| 51 | + | |
| 52 | +`system/agents/company-context.md`(每个 agent 上下文自动注入,见 | |
| 53 | +`server/services/company-service.js:1149`)里"公司使命"字段直接就是 `company.description`, | |
| 54 | +没有默认值时会渲染成"待定义"。公司创建时没有任何 founding-goal 引导——`company-service.js` | |
| 55 | +全文搜不到任何"建司即建 goal"的 bootstrap 逻辑。也就是说,即便补上了 1.1/1.3 的缺口,如果 | |
| 56 | +`company.description` 从未被认真填写过,CEO 自主立项时除了一句"待定义"没有任何可以依据的锚点, | |
| 57 | +强行立项等于让它凭空编目标。 | |
| 58 | + | |
| 59 | +### 1.5 好消息:数据层的地基已经齐了,不需要新状态机 | |
| 60 | + | |
| 61 | +- `goals` 表本就有 `status='draft'` 默认值(`work-graph-db.js:499`)和 | |
| 62 | + `created_by`(`work-graph-db.js:505`,注释直接写着 "`'human' | agent id(Master/CEO 代下单)`")。 | |
| 63 | +- "建一个 goal 产出一律是 draft,入队是另一个动作"是既有明文约定 | |
| 64 | + (`work-graph-db.js:1796`),`POST /:companyId/goals` 的实现完全符合这条约定 | |
| 65 | + (`createGoal` 的 status 恒为 draft,见 `company.js:1836` 注释)。 | |
| 66 | +- CEO 心跳里已经在用同一套"系统自动开草稿,人/CEO 再决定要不要 enqueue"的模式处理别的场景 | |
| 67 | + (`ceo/heartbeat.md:49`提到的 `created_by: 'system'` 草稿任务)——本设计只是把这个已经验证过的 | |
| 68 | + 模式,从"task 级"复用到"goal 级"。 | |
| 69 | + | |
| 70 | +**一个需要在设计里明确处理的成本细节**:`POST /:companyId/goals` 默认路径 | |
| 71 | +(`body.split !== false`)会立刻用 `setImmediate` 在后台跑 `runGoalSplit` | |
| 72 | +(`company.js:1832-1852`),也就是说**即便只是"建一条草稿 goal",默认调用方式也会立刻烧 | |
| 73 | +task-planning 的 token**,而不是零成本的占位。CEO 自主立项如果用默认参数,"提案还没被人看到" | |
| 74 | +之前就已经产生了真实开销。 | |
| 75 | + | |
| 76 | +--- | |
| 77 | + | |
| 78 | +## 二、设计目标与非目标 | |
| 79 | + | |
| 80 | +**目标**:公司在没有任何 `draft`/`planning`/`active` goal 时,CEO 心跳能够依据公司使命提出一条 | |
| 81 | +新的候选 goal(草稿态),而不是无限期地回复"心跳正常"、让公司永久停摆。 | |
| 82 | + | |
| 83 | +**非目标**(本文档不覆盖): | |
| 84 | +- 不解决"OKR 周期性续期"(比如季度目标到期后自动开下一季度)——那是不同的触发条件, | |
| 85 | + 本文档只覆盖"完全空转"这一种更紧迫的场景。 | |
| 86 | +- 不改变"目标耗尽 → 无人接续"之外的任何执行层机制(存活扫描、僵尸收口等已经在正常工作, | |
| 87 | + 不在本次范围)。 | |
| 88 | +- 不实现具体的 guardrail 数值(预算上限、连续自动立项次数上限)——只给出这些参数应该存在, | |
| 89 | + 具体数字留给立项时结合真实场景定。 | |
| 90 | + | |
| 91 | +--- | |
| 92 | + | |
| 93 | +## 三、设计方案 | |
| 94 | + | |
| 95 | +### 3.1 触发条件 | |
| 96 | + | |
| 97 | +**实现时核实(2026-09-17)**:`listCompanyGoals` 的 `status` 过滤是精确匹配单个值 | |
| 98 | +(`work-graph-db.js:1836-1848`,`WHERE status = @status`),不支持逗号分隔的多状态查询—— | |
| 99 | +`GET /goals?status=active,planning,draft` 会被当成字面量 `"active,planning,draft"`, | |
| 100 | +永远匹配不到任何行,等于查询恒为空、每次心跳都会被误判成"公司空转"。实际接线改成:查 | |
| 101 | +`GET /goals`(不带 `status`,默认已排除 `archived`),CEO 自己看返回列表里每条的 `status` | |
| 102 | +是否都落在终态集合 `done`/`failed`/`cancelled`(与 `GOAL_TERMINAL_STATUS` 语义一致,见 | |
| 103 | +`work-graph-db.js:2218`,只是不含 `archived`——那一档已经被 `includeArchived` 默认过滤掉了)。 | |
| 104 | + | |
| 105 | +在 `ceo/heartbeat.md` 的检查清单前面插入一条: | |
| 106 | + | |
| 107 | +> 0. 查 `GET /goals`(不加 `status`)。**全部都是终态、或列表为空**时,本次心跳进入 | |
| 108 | +> "立项模式"(见下),跳过后面几条常规巡检(反正没有目标可巡);只要有一条落在 | |
| 109 | +> `draft`/`planning`/`active`,就按原清单走。 | |
| 110 | + | |
| 111 | +### 3.2 锚点:公司使命前置闸门 | |
| 112 | + | |
| 113 | +立项模式的第一步不是编目标,是检查锚点是否可用: | |
| 114 | + | |
| 115 | +- 读 `company-context.md` 的"公司使命"字段。 | |
| 116 | +- 如果是空字符串或字面量"待定义":**不生成任何草稿 goal**,直接在心跳回复里写明"公司使命未 | |
| 117 | + 设置,无法自主立项,需要人类补充"。这条回复走现有的通知路径(心跳失败/异常场景本就会被人 | |
| 118 | + 看到),本质上是把"要不要开自转"这个决定权,通过"必须先填一句真实的使命"这个动作,留在人类 | |
| 119 | + 手里——不填使命,系统就不会替你决定公司该做什么。 | |
| 120 | +- 使命存在时,CEO 依据使命 + 历史完成情况(`GET /goals?status=done`,看过去做过什么、还差什么, | |
| 121 | + 避免重复立项或跟已完成的工作方向冲突)起草候选 goal 的 title/description。 | |
| 122 | + | |
| 123 | +### 3.3 提案机制 | |
| 124 | + | |
| 125 | +调用 `POST /:companyId/goals`,需要显式传 `split:false`(对应 1.5 节的成本细节,见 | |
| 126 | +`company.js:1830`——不传这个参数会立刻在草稿还没被人看到前烧掉拆分 token): | |
| 127 | + | |
| 128 | +```bash | |
| 129 | +curl -s -X POST "${LEMON_API_BASE}/company/<companyId>/goals" \ | |
| 130 | + -H "x-internal-token: $LEMON_INTERNAL_TOKEN" -H "Content-Type: application/json" \ | |
| 131 | + -d '{"description":"...","title":"...","created_by":"ceo","split":false}' | |
| 132 | +``` | |
| 133 | + | |
| 134 | +`created_by` 填 `"ceo"`(或更细粒度的 `"ceo:auto-refill"`,命名留给立项时按 | |
| 135 | +`27-naming-conventions.md` 定),保证审计轨迹上能一眼区分"人类下的单"和"CEO 自主立项的单"—— | |
| 136 | +这是复用 `goals.created_by` 已有的语义(`work-graph-db.js:505` 注释本就写了 "agent id"), | |
| 137 | +不需要加新列。 | |
| 138 | + | |
| 139 | +对应地,需要在 `ceo/tools.md` 补上这个端点的使用说明——1.3 节确认了这是当前真正缺失的能力, | |
| 140 | +不只是加一条规则那么简单。 | |
| 141 | + | |
| 142 | +**`split:false` 的直接后果——实测跑过一遍,推翻了本文档最初的判断**:`company.js:1859-1862` | |
| 143 | +的同步路径写着"没给 `tasks` 就退化成 1 条同描述的 task",之前读这行注释时误判成"那就是 0 条", | |
| 144 | +实际起服务真调了一次 `POST /goals {split:false}` 才发现**建出来的是 1 条 `draft` task,不是 | |
| 145 | +零条**——`goal` 和这条 task 都停在 `draft`,但 `POST /goals/:goalId/dispatch`(`company.js: | |
| 146 | +1978-1985`)判的是"这个 goal 下有没有 `draft` 状态的 task",1 条也满足,**直接就能 dispatch | |
| 147 | +成功、真的跑起来,没有任何接口会挡住**。也就是说 3.5 节最初设想的"零 task 逼审批人先显式加 | |
| 148 | +task"这道技术闸门**不存在**——`split:false` 唯一确定省下的是 task-planning 的 token(同步 | |
| 149 | +路径不调 agent,也已实测核实),不是"强制多一道人工步骤"。真正的、也是唯一的把关手段回落到 | |
| 150 | +3.5 节重写后的版本:CEO 自己不许调用 `dispatch`。 | |
| 151 | + | |
| 152 | +### 3.4 去重/节流 | |
| 153 | + | |
| 154 | +立项前先查一遍有没有已经存在、还没被处理的系统草稿。同 3.1 节的核实结果,`created_by` 同样 | |
| 155 | +不是可查询过滤字段(`listCompanyGoals` 只认 `status`),实际查的是 `GET /goals?status=draft` | |
| 156 | +(这个单值过滤支持),CEO 自己看返回列表里每条的 `createdBy` 字段是不是 `"ceo"`——命中就不 | |
| 157 | +重复开,否则每次心跳间隔(默认 15 分钟一次)都会叠一条新草稿,草稿越堆越多、人类审查成本 | |
| 158 | +线性上升,而不是"公司在自转"。 | |
| 159 | + | |
| 160 | +### 3.5 两阶段放权 | |
| 161 | + | |
| 162 | +不建议一步做到"CEO 自己 enqueue"——那等于把"公司该做什么"这个最大的方向性决策完全交给一次 | |
| 163 | +心跳里的一次性判断,没有任何回滚窗口。分两阶段: | |
| 164 | + | |
| 165 | +- **阶段 A(默认,安全)**:草稿建完后只通知(复用现有的 dispatch/通知机制),停在 `draft`。 | |
| 166 | + 3.3 节实测确认了 `split:false` 建出的草稿其实已经带着 1 条 `draft` task、技术上立刻就能 | |
| 167 | + `dispatch` 成功——**没有任何接口层面的东西会替这一步把关**。所以阶段 A 的安全性完全押在 | |
| 168 | + 一条行为约束上:`ceo/tools.md` 明确写死"这条草稿建完,你不许自己调 `dispatch`",等人类 | |
| 169 | + 审过、人类自己按 `dispatch`。这比"技术闸门"弱——是纯提示词层面的约束,CEO 是否真的每次 | |
| 170 | + 都守规矩,需要观察真实运行几轮才能有把握。先跑几轮,观察提案质量、方向是否贴合真实使命、 | |
| 171 | + 以及这条"不许自己 dispatch"的约束有没有被绕过,再决定要不要开阶段 B。这是当前应该实现 | |
| 172 | + 的部分。 | |
| 173 | +- **阶段 B(每公司显式开关,非默认,本文档不实现)**:CEO 自己完成 `dispatch` 这一步, | |
| 174 | + 但需要 guardrail——单周期预算上限、连续 N 次自动立项后强制退回阶段 A 等人工看一眼、以及一个 | |
| 175 | + 能立刻关掉的 kill switch。这个开关本质上就是"这家公司要不要真正无人值守"的开关,需要在 | |
| 176 | + company 级配置里加一个字段(具体形状留给立项时定,可能是 `companies` 表新列,也可能是 | |
| 177 | + company-context 里的一个 flag——两种都不需要新状态机,只是存储位置的选择)。 | |
| 178 | + | |
| 179 | +--- | |
| 180 | + | |
| 181 | +## 四、开放问题 / 风险 | |
| 182 | + | |
| 183 | +- **立项质量没有验证机制**:CEO 起草的候选 goal 目前只能靠人工审草稿判断"是不是瞎编", | |
| 184 | + 没有类似 `org-validator` 那样的自动质检。阶段 A 阶段人工审查本身就是这道质检,但如果要推进 | |
| 185 | + 阶段 B,"提案质量怎么自动把关"是必须先解决的前置问题,不能跳过。 | |
| 186 | +- ~~`split:false` 之后草稿的"入队即拆分"路径需要确认~~ **已实测(2026-09-17),且推翻了 | |
| 187 | + 2026-09-09 当天的初步判断**:最初以为 `/dispatch`(`company.js:1978-1985`)对 | |
| 188 | + `split:false` 建出的 draft goal 会因为"零 task"直接 409,读代码判断如此;起服务真跑了一遍 | |
| 189 | + `POST /goals {split:false}` 才发现同步路径其实自动带了 1 条 `draft` task | |
| 190 | + (`company.js:1859-1862`),`dispatch` 判的是"有没有 `draft` 状态的 task",1 条就满足, | |
| 191 | + **技术上立刻能跑,没有任何接口层面的东西挡着**。原先"这就是审批闸门本身"的结论是错的, | |
| 192 | + 已重写进 3.3/3.5 节:阶段 A 的唯一把关变成了"`ceo/tools.md` 明确写死 CEO 不许自己调 | |
| 193 | + `dispatch`",一条纯提示词约束,不是代码层面的硬闸门。**这是本文档目前最大的剩余风险**—— | |
| 194 | + 这条约束是否真的会被 CEO 稳定遵守,只能靠实际跑几轮观察,读代码/读提示词都验证不了。 | |
| 195 | +- **心跳频率与立项频率耦合**:3.4 节的去重能防止无限堆草稿,但没有解决"CEO 多久该重新评估一次 | |
| 196 | + 公司使命有没有新方向"这个更长期的问题——目前设计只覆盖"完全空转"这个最紧迫场景,节奏性的 | |
| 197 | + 战略复盘不在本文档范围(见二、非目标)。 | |
| 198 | + | |
| 199 | +--- | |
| 200 | + | |
| 201 | +## 五、明确不做 | |
| 202 | + | |
| 203 | +- 不实现阶段 B(自动 enqueue + guardrail 数值)——只给出应该存在,具体数字留给有阶段 A 真实 | |
| 204 | + 运行数据之后再定。 | |
| 205 | +- 不实现"公司级自转开关"字段的具体存储形状——留给立项时决定放在 `companies` 表还是 | |
| 206 | + company-context。 | |
| 207 | +- 不解决 OKR 周期性续期(非目标已声明)。 | |
| 208 | +- 不实现提案质量的自动质检机制(四、开放问题已指出这是阶段 B 的前置阻塞项,不是本文档范围)。 | |
| 209 | + | |
| 210 | +--- | |
| 211 | + | |
| 212 | +## 六、与既有文档的关系 | |
| 213 | + | |
| 214 | +- [27-goal-contract-format.md](./27-goal-contract-format.md):本设计完全复用它定义的 goal | |
| 215 | + draft/dispatch 生命周期,不新增字段、不新增状态。 | |
| 216 | +- `system/agents/ceo/heartbeat.md` / `tools.md`:本设计的落地点就是这两份文档——三、3.1/3.3 | |
| 217 | + 节描述的改动直接对应它们需要新增的具体规则和端点说明。 | |
| 218 | +- [30-task-delegation-reliability-gaps.md](./30-task-delegation-reliability-gaps.md):该文档 | |
| 219 | + 记录的是"任务卡住后没人主动通知"的可靠性缺口,与本文档"没有任务时没人开新任务"是同一类 | |
| 220 | + "系统该主动但目前被动"问题的两个不同表现,修复思路(草稿 + 通知,而非静默自动执行)是一致的。 | |
| 221 | + | |
| 222 | +--- | |
| 223 | + | |
| 224 | +## 七、阶段 A 实现记录(2026-09-17) | |
| 225 | + | |
| 226 | +对所有公司默认生效(用户拍板:不加公司级开关,`ceo/heartbeat.md` 直接对全量公司生效)。 | |
| 227 | + | |
| 228 | +- `system/agents/ceo/heartbeat.md`:心跳清单最前面插入空转检测(`GET /goals` 不带 `status`, | |
| 229 | + 自己判每条 `status` 是否全落终态),命中就转入 `tools.md` 的立项步骤、跳过常规巡检; | |
| 230 | + 未命中按原清单走,行为不变。 | |
| 231 | +- `system/agents/ceo/tools.md`:新增「立项:公司没有任何活跃目标时才用」一节——使命前置闸门 | |
| 232 | + (读 `company-context.md` 拼进提示词的"公司使命",空/"待定义"直接拒绝并转人工)、去重检查 | |
| 233 | + (`GET /goals?status=draft`,client 侧过滤 `createdBy==='ceo'`)、`POST /goals` | |
| 234 | + 建草稿(`split:false` + `created_by:'ceo'`),并在「你不该用的工具」表里明确加了一行: | |
| 235 | + CEO 不许对自己刚建的 goal 草稿调 `dispatch`。 | |
| 236 | +- **实现后又用真实起服务的方式核实了一遍(2026-09-17,不只是读代码)**,推翻/修正了三处 | |
| 237 | + 设计草稿阶段的判断,均已同步回本文档正文,设计文档与实现保持一致: | |
| 238 | + 1. `listCompanyGoals` 不支持逗号分隔多状态查询、也不支持按 `created_by` 过滤——原设计草稿里 | |
| 239 | + `status=active,planning,draft` 和 `status=draft&created_by=ceo` 两个查询实际都会静默 | |
| 240 | + 失效(前者恒空、会把"公司正常运转"误判成"公司空转";后者的 `created_by` 参数被服务端 | |
| 241 | + 忽略)。改为查询后 CEO 在自己的推理里做客户端过滤。 | |
| 242 | + 2. **最重要的一处**:`split:false` 建出的草稿其实自动带 1 条 `draft` task,不是零 | |
| 243 | + task——`dispatch` 技术上立刻能成功,"零 task 逼人工先加 task"这道闸门根本不存在(见四)。 | |
| 244 | + 阶段 A 的安全性因此改为完全依赖 `ceo/tools.md` 里"不许自己调 dispatch"这条提示词约束, | |
| 245 | + 不是代码层闸门——这是当前实现里最脆弱的一环,需要靠观察真实运行验证,读代码/读提示词 | |
| 246 | + 都验证不了。 | |
| 247 | + 3. 上述第 2 点发现前,`ceo/tools.md` 的编辑一度把"把你(或别人)提的草稿正式下达" | |
| 248 | + (task 级 `enqueue` 的既有说明)误删了——一次 `Edit` 的 `old_string` 边界没圈对, | |
| 249 | + 已在同一轮修回,顺带在它和新「立项」一节之间加了一句显式区分:"task 级草稿的自主 | |
| 250 | + enqueue 权限不能类比到 goal 级草稿",避免 CEO 把两条权限混为一谈。 | |
| 251 | +- 未改动任何路由 / 服务层代码——`POST /:companyId/goals`、`.../dispatch`、`listCompanyGoals` | |
| 252 | + 均按现状直接复用,阶段 A 完全是 agent 人设文档层面的改动。 | |
| 253 | +- 阶段 B(公司级自转开关、guardrail 数值、CEO 自动 enqueue)未实现,见五、明确不做。 | ... | ... |
system/agents/ceo/heartbeat.md
| ... | ... | @@ -3,7 +3,21 @@ |
| 3 | 3 | 你收到的心跳消息会提"扫描全局 OKR 目标状态"——那是通用触发文案,对你不适用。 |
| 4 | 4 | 你要看的是本公司的 **goal/task 板**(`tools.md` 里的读接口),不是 OKR。 |
| 5 | 5 | |
| 6 | -## 每次心跳 | |
| 6 | +## 每次心跳,先做这一步:公司是不是完全没有活着的目标 | |
| 7 | + | |
| 8 | +```bash | |
| 9 | +curl -s "${LEMON_API_BASE:-http://localhost:3010/api}/company/<companyId>/goals" \ | |
| 10 | + -H "x-internal-token: $LEMON_INTERNAL_TOKEN" | |
| 11 | +``` | |
| 12 | +(不加 `status`,默认已经排除 archived。接口不支持一次查多个状态,自己看返回的每条 | |
| 13 | +`status`:只要有一条落在 `draft`/`planning`/`active`,就说明公司不是空转的,跳过本节, | |
| 14 | +走下面「每次心跳」原有清单。) | |
| 15 | + | |
| 16 | +**全部都是 `done`/`failed`/`cancelled`(或列表本身是空的)** → 公司没有任何在推进的目标, | |
| 17 | +按 `tools.md`「立项:公司没有任何活跃目标时才用」一节的步骤走(先查使命有没有填,再查有没有 | |
| 18 | +你自己之前已经提过、还没人审的草稿,避免重复),不要走下面的常规巡检——反正没有目标可巡。 | |
| 19 | + | |
| 20 | +## 每次心跳(公司不是空转状态时,按这个清单走) | |
| 7 | 21 | |
| 8 | 22 | - [ ] 查 `GET /tasks?status=blocked` 和 `?status=failed`,看有没有需要处理的 |
| 9 | 23 | - [ ] 查 `GET /goals?status=active`,扫一眼有没有明显该调整优先级的 | ... | ... |
system/agents/ceo/tools.md
| ... | ... | @@ -84,6 +84,48 @@ curl -s -X POST "${LEMON_API_BASE:-http://localhost:3010/api}/company/<companyId |
| 84 | 84 | 必要的——`enqueue` 之后就会真的排队执行、真的烧预算。判断依据不够就先别 enqueue, |
| 85 | 85 | 留着草稿等人类看到也行。 |
| 86 | 86 | |
| 87 | +**这条授权只覆盖 task 级草稿,不覆盖下面「立项」一节你自己新开的 goal 草稿**——两者风险级别 | |
| 88 | +不一样,见下一节末尾的说明。 | |
| 89 | + | |
| 90 | +### 立项:公司没有任何活跃目标时才用(心跳触发,见 `heartbeat.md`) | |
| 91 | + | |
| 92 | +只在 `heartbeat.md` 判定"公司没有任何 `draft`/`planning`/`active` 的 goal"时才走这一节—— | |
| 93 | +公司还有目标在跑,不该也不需要你凭空再开一个。 | |
| 94 | + | |
| 95 | +**第一步:使命有没有填**。看你提示词开头由 `company-context.md` 拼进来的"公司使命"那一行—— | |
| 96 | +不是空的、也不是字面量"待定义"才能往下走。是"待定义"就**不要编一个目标出来**,正文回复里 | |
| 97 | +写清楚"公司使命未设置,无法自主立项,需要人类先补充",到此为止,不要调用任何写接口。 | |
| 98 | +使命是你唯一的立项依据,没有它就是瞎猜。 | |
| 99 | + | |
| 100 | +**第二步:有没有已经提过、还没人审的草稿**(防止每次心跳都叠一条): | |
| 101 | +```bash | |
| 102 | +curl -s "${LEMON_API_BASE:-http://localhost:3010/api}/company/<companyId>/goals?status=draft" \ | |
| 103 | + -H "x-internal-token: $LEMON_INTERNAL_TOKEN" | |
| 104 | +``` | |
| 105 | +(同上,接口不支持按 `created_by` 过滤,自己看返回列表里每条的 `createdBy` 字段。) | |
| 106 | +已经有一条 `createdBy: "ceo"` 的草稿在等审 → 不要再开新的,正文回复里提一句"已有待审草稿 | |
| 107 | +`<goalId>`,等人工处理"就够了。 | |
| 108 | + | |
| 109 | +**第三步:都过了才建**: | |
| 110 | +```bash | |
| 111 | +curl -s -X POST "${LEMON_API_BASE:-http://localhost:3010/api}/company/<companyId>/goals" \ | |
| 112 | + -H "x-internal-token: $LEMON_INTERNAL_TOKEN" \ | |
| 113 | + -H "Content-Type: application/json" \ | |
| 114 | + -d '{"title":"<短标题>","description":"<依据公司使命,这条目标具体要做成什么,写清楚>","created_by":"ceo","split":false}' | |
| 115 | +``` | |
| 116 | +**必须带 `"split":false`**——不带的话这个端点会立刻在后台跑一次 task-planning 拆分,草稿 | |
| 117 | +还没被任何人看到就已经真花了 token。`split:false` 的响应是同步 `201`,不会调 agent, | |
| 118 | +不花 token(已实测核实,不是读代码猜的)。 | |
| 119 | + | |
| 120 | +**⚠️ 这条草稿建完不是"技术上跑不起来",是"你不许碰下一步"**(已实测核实,纠正一个直觉 | |
| 121 | +误判):`split:false` 不传 `tasks` 时,服务端会顺手把你的 `description` 退化成**一条** | |
| 122 | +draft task 挂在这个 goal 下——不是零 task。这意味着这个 goal 此刻**技术上已经可以被 | |
| 123 | +`dispatch` 直接跑起来**,没有任何接口会替你挡住。**唯一的把关是你不要去碰 | |
| 124 | +`POST /goals/<goalId>/dispatch`**——立项模式下你的职责到"建完草稿"为止,`dispatch` 这一步 | |
| 125 | +永远留给人类去按。别把"提一条新任务"那节里"你被授权可以自主 enqueue"的权限类比到这里: | |
| 126 | +那是对着人类已经看过的 goal 下加的具体 task,这里是你自己凭使命猜出来的新方向,两件事 | |
| 127 | +风险级别不一样,不能套用同一条权限。 | |
| 128 | + | |
| 87 | 129 | **`decomposed_work` 子任务走同一个端点**——`subtask-planning` 已经把内容拆好了,你不用再判断 |
| 88 | 130 | "清不清楚",只判断节奏(现在放还是等)。同一条 task 下的子任务可以一次性全部 enqueue, |
| 89 | 131 | `depends_on` 没完成的会被调度器自动拦住,不用你自己算顺序。 |
| ... | ... | @@ -165,3 +207,4 @@ runId>` 下面查到的、status 恰好是 `running` 的节点时,这个端点 |
| 165 | 207 | | 直接改 `graph_runs` / `graph_run_nodes`(裸 SQL 或内部函数) | 执行状态由引擎维护;**唯一的例外**是上面"卡住的 run 节点"一节写明的 `force-fail` 端点,别的一律不许碰 | |
| 166 | 208 | | 对着卡住的 run/节点自己拉起一个新的 agent 会话"亲自跑一遍" | 绕过引擎会跟卡住的状态打架,上一次这么干陷入了死循环(45 号文档)——只能用 `force-fail` 端点,没有第二条路 | |
| 167 | 209 | | `POST /:companyId/tasks`(不带 `draft:true`) | 会立刻烧预算,见上面的强调 | |
| 210 | +| `POST /goals/<goalId>/dispatch`(对「立项」一节你自己刚建的 goal 草稿) | 这条草稿只有你自己的判断背书,没有人类看过——建完就停手,`dispatch` 永远留给人类按,不要套用 task 草稿那条"你被授权自主 enqueue"的权限 | | ... | ... |