Commit 14d83bc2c801b18665afd9898abee4bb6afd7198

Authored by yibo
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
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 &quot;${LEMON_API_BASE:-http://localhost:3010/api}/company/&lt;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&gt;` 下面查到的、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"的权限 |
... ...