这份图要回答的是一件事:我在网页上敲一句话,到最后看到答案,中间经过了谁、谁做了什么决定、什么东西被存在哪里。看懂了,你提的下一个需求就知道该落在哪一层、会撞上哪条物理约束。
顺序是从「一句话的旅程」往外扩:先看单条消息怎么走通(第一层),再看为什么多件事能同时干、代价是什么(第二层),再看对话记忆到底住在哪(第三层),再看有哪些闸门在拦着不出事(第四层),最后如实交代哪些东西还只是设计稿、一行代码都没有(第五层)。
读法说明:每层一张流程图(第九层三张),一屏内看完;图下最多三行小注点出这张图的题眼;再往下是名词解释,每条讲三句话——是什么(物理上是个什么东西)、起什么作用、不要它会怎样。图里只放短标签,说明句和代码出处一律不进方框,全部收在每层那个「展开…」折叠区里(出处形如 services/orchestrator/server.mjs:2714,冒号后面是行号,可自行核对)。凡拿不准的地方写成「待核」,没有编造。
图形约定(十一张图统一):矩形=步骤或组件;菱形=判断;带箭头的线=流向(绿=循环、蓝=回流、红=否决或拒绝);虚线框=分区或泳道;小圆=起止。全部是内嵌 SVG,零外链、断网也能看。
第二轮追加的三层(第六 / 七 / 八层)回答的是另一个问题:一件事怎么从「我想要个东西」变成一份可开工的合同,这份合同又怎么被派出去、跑完、验完、上线,以及每次派活之前要不要复用上一次的对话。前五层讲机器怎么跑,后三层讲人和 AI 之间的交接规则——两者一样是硬约束,撞上了同样会出事。
第九层回答的是最落地的一个问题:这些层反复提到的文件路径,在磁盘上到底是哪些、少了哪个就跑不起来;那几处都被叫做「记忆」的东西,又分别跟着什么走。它是前八层的实物对照——前面讲机制,这一层给出机制落在哪个文件、哪张表、哪几个字段上。
本稿不做无限画布、不做点击下钻。右上角可整页缩放(70%–160%);较宽的图可以在图里按住左右拖动。
后三层里有一个反复出现的区分,先在这里说明白,因为它决定了「这条规矩靠不靠得住」:硬闸=有代码在拦,你绕不过去;软闸=界面上拦一下,改一行请求就能绕过;只是文档=写在文档或角色卡里、进 AI 的提示词,但没有任何程序会检查,全靠自觉。本图凡涉及一条规矩,都标了它是哪一种——这一点比规矩本身更重要。
Status 三格式的第三种是哪种 / 非空 linked_run_id 为 0 未复测),不另立清单。
四个角色接力:网页(你看到的界面)→ 调度台(在云服务器上排队记账)→ 常驻程序(在你自己 Mac 上,主动来领活)→ 命令行 AI(真正干活的那个)。关键点是:调度台从来不主动去找你的 Mac,永远是你的 Mac 每两秒来问一次「有活吗」。
绿色那条循环箭头是这一层的题眼:调度台从不主动联系你的 Mac,永远是常驻程序每 2 秒自己回头问一次「有活吗」。
蓝色是回流,四跳原路返回:AI 每走一步 → 常驻程序 → 调度台(存一份 + 进程内实时推送)→ 网页 → 你看见。
两个虚线框是分区:左边整块在云服务器上,右边整块在你自己的 Mac 上——你的代码从不离开本机。
/api/…,由托管平台的转写规则替换成真正的后端地址——这样浏览器就不认为这是跨站请求,省掉一轮跨站预检。
queued(排队中)。workbench-data/store.json,线上是 /opt/personal-workbench/workbench-data/store.json。这个文件里一共 15 类东西(任务、事件、工人、产物、会话、消息、连接器状态、待排期需求、附件、工作区、记忆、编排状态、界面偏好、自主循环、任务卡)。
POST /workers/claim。另有一条独立的心跳,每 15000 毫秒 报一次「我还活着 + 我这边两个 AI 的健康状况」。
claimableRunsForWorker,「能领哪些活」和「该起几个工人」都从它派生,所以两者不可能对不上:
queued(排队中)或 waiting_for_worker(等工人);waiting_for_rate_limit(等额度恢复)的必须已经到点了才算。没到点的、时间无效的,对「能领的活」和「该起几个工人」同时隐身——所以系统不需要为它专门跑一个定时器。claude-code.execute、project:personal-workbench)。agents/<角色>/;③ 普通会话任务 → 为这条会话单独建一个旁路工作副本(第二层详述);④ 都不满足 → 主仓根目录。所有路径都必须落在白名单目录里,否则直接报错不干。
claude --print [--resume <原生会话号>] --output-format stream-json --verbose --permission-mode acceptEdits --add-dir <工作目录> -- <你的原话>。明文禁止三个参数:--dangerously-skip-permissions、--continue、--fork-session。codex exec --json --sandbox <沙箱模式> -c approval_policy="…" --skip-git-repo-check --output-last-message <文件> -C <工作目录> <你的原话>。--ephemeral 绝对禁止出现,因为它会跳过会话记录文件,事后就再也接不回去了。POST /runs/<任务号>/events。单条最多试 4 次,间隔 250 → 500 → 1000 毫秒翻倍等待。四次都失败就不再硬撑,把错误暴露出来让整个任务显性失败——不允许「悄悄少了几条事件却报成功」。
GET /runs/<任务号>/stream)。浏览器连上时先拿一份快照(任务本体 + 已有事件 + 消息 + 产物),之后增量收;任务已经结束的话直接给快照然后关连接。
POST /workers/<工人号>/results,带最终答案、产物、以及本次认领代号。调度台在做任何一件事(存产物、绑原生会话、写消息、触发后续钩子)之前先验代号:状态必须还是 running、代号必须等于当前代号、且这个代号还没被别的上报用过。验不过就当审计记录收下(回 200),但不产生任何业务副作用。这样一个掉线又复活的旧工人,不可能把一个已经被别人接手的任务给「改回去」。
常驻程序上报:我的编号 / 名字 / 能力标签 / 当前在跑哪个任务 / 两个 AI 的健康状况 / 本机有哪些命令行会话正被人手动占用(每次全量替换,所以不会留下过期的占用记录)。
调度台在回复里搭车捎两样东西回去:① cancel_run_id——你在网页上点了「停止」,就靠这个字段让常驻程序去杀掉 AI 进程;② 待读取的对话需求清单(第三层)。
常驻程序每 5000 毫秒 只读地扫一遍本机进程表,看有没有人正在终端里手动跑 claude/codex 的续聊。一旦和上次结果不同,立刻补发一次心跳而不是等满 15 秒——这样网页那边最多 11 秒就能知道「这条会话此刻被人在终端占着,别去动它」。
它会按进程号排除掉自己刚起的那个 AI 子进程,避免「自己干活把自己锁了」。
workbench.heyyys1.com;顺便做「同源转写」,把 /api/… 转到真正的后端。mockup.heyyys1.com 是另一个独立的 Vercel 项目,和生产站互不影响。/api/xxx,平台悄悄转发到 https://api.workbench.heyyys1.com/xxx。浏览器全程以为自己在跟同一个网站说话。claude-code.execute、codex.execute、connector.auth、local_fs.write、project:personal-workbench。claude 和 codex。它们各自会读工作目录里的规则文件、调用模型、改文件、跑命令。kimi-cli 的登记,代码里只有登记,没有适配。隔离单位是会话,不是任务、不是项目。每条会话独占一个代码分支 + 一份旁路工作副本。因此:同一条会话内部必然串行,不同会话之间才是真并行。代价有一条最坑人:工作副本是建的时候从主干切一刀,之后再也不会自动跟上主干。
三条并列虚线泳道=三条会话真并行:各自一个分支、一份旁路副本,改文件互相看不见,不需要任何锁。
泳道内部那串纵向箭头=同一条会话必然串行:领活层一看到这条会话已有一个在跑,就把它名下其余的全藏起来。
底部汇聚处的红框是并行的真实代价——合并不是 git merge,是按改动清单逐文件拷贝,撞同一个文件不会报冲突。
session/<会话号>,起点默认是 main。调度台本机没有代码仓库,所以它只能传分支名字,不能传具体版本号;具体解析交给工人。<父目录>/personal-workbench,副本就放在 <父目录>/personal-workbench-wt/<会话号>。刻意放在仓库外面,这样主仓的 git status 永远是干净的,回收副本时也绝不会碰到主仓。
git worktree prune,清掉上次崩溃留下的登记残骸。/var 解析成 /private/var,直接比字符串会误判成不存在而白重建一次)。git worktree add <目录> <分支>;分支不存在 → git worktree add -b <分支> <目录> <起点>,起点用 main,main 都没有才退回 HEAD。node_modules 和两个环境变量文件做成软链接指过去。软链是幂等的:已经对了就不动,坏了就换掉,源文件不存在就跳过(不报错)。
--slots N 的话,这个 N 就是上限,手动设定优先)。每个工人是一个独立的 worker.mjs 进程,编号是「基础编号 + -s + 序号」,例如 mac-worker-s1、mac-worker-s2。
GET /workers/scale-signal,绝不碰任务状态。它算「能领的会话数」时用的是和领活完全同一个函数,所以「说该有几个工人」和「实际能领到几个活」不可能对不上。两个工人进程同一毫秒来领活,为什么不会领到同一个任务?因为调度台是单进程单线程的,而「找到一个排队任务」和「把它改成进行中」这两步之间没有任何等待动作。这在 JavaScript 里意味着这段代码会被一口气执行完,中途插不进第二个请求。整套并行方案就建立在这条不变量上,代码里一个锁都没加。
反过来说这是个约束:哪天调度台被改成多进程(比如为了扛并发起两个副本),这条保证立刻失效,抢同一个任务就会真的发生。所以「调度台单进程」在现在这套设计里不是偶然,是前提。workers/mac-worker/supervisor.mjs:54-60 该不变量的书面记录services/orchestrator/server.mjs:2719-2732 无 await 的临界区
--fixed 或环境变量可以关掉,退回「固定起 N 个」。三个地方,各管一段,刻意不合并:① 对话正文的真相在你自己 Mac 上的原生会话文件;② 台账里只有网页自己那份气泡记录和元数据;③ 网页看到的原生对话,是一份调度台内存里的短命缓存,不落盘。三条硬约束跟着来:为什么不抄进台账、引擎一旦绑定不能换、终态任务的记录会被定时清掉。
左右两个虚线框是两个不同的存储区:对话正文的真相在你 Mac 上的原生会话文件里,云端台账只存一个指针(原生会话号 + 哪个 AI)。
中间那对蓝箭头是按需读取——调度台不主动联系工人,需求搭在领活/心跳的回复里下发;读回来的内容只进内存缓存,永不落盘(防第二个真相源)。
下方菱形是引擎锁:不一致就走红色出口,开一条空记忆的新会话;台账下面那两个虚线方块是定时闸(2 小时清事件 / 7 天删整行,默认关、线上开)。
claude:~/.claude/projects/<按工作目录编码的目录>/<会话 uuid>.jsonl——按工作目录分开存,所以目录一挪,续聊就找不到了。
codex:~/.codex/sessions/<年>/<月>/<日>/rollout-<时间戳>-<会话 id>.jsonl——全局按日期存,和工作目录无关。
这是两个命令行 AI 自己写的文件,我们只读、从不改。它就是「这条对话到底说了什么」的唯一答案。
存的是:网页自己那份气泡(你发的话、系统提示行)、会话的标题/角色/项目绑定、任务行、事件流、产物指针。
刻意不存的是:原生对话正文。它只有指针(原生会话号 + 是哪个 AI)。
这层里的事件流和任务行会被清掉(见图 3c),产物集合不被清理。
这个模块刻意不引用任何磁盘或台账:需求登记和解析好的对话轮次只活在进程内存的两张表里。进程一重启就是彻底冷启动——这是故意的,为的是保证磁盘上永远不会出现第二份对话真相。
上限写死四条:单条会话缓存 ≤ 1.5 MB(超了从最老的轮次开始丢)、最多缓存 8 条会话(按最近使用淘汰)、缓存活 15 分钟、需求登记活 30 秒。
浏览器来读只能刷新「最近用过」的排序,不能延长过期时间——只有工人推来新数据才会重设 15 分钟。
{run: null, 需求清单} 的信封——只有在真有需求时才这么做。
GET /sessions/<会话号>/transcript选通道时列了五个方案,「把对话搬进台账」被判为禁止,理由两条写在文档里:① 用户明令否决——那会造出第二个真相源;② 它是第四层那次膨胀事故的复发路径。
「第二真相源」不是个抽象说法,它的具体后果是:同一段对话在你 Mac 上和服务器上各有一份,两份一旦不一致,没有任何办法判断谁对。而不一致是必然的——你在终端里直接续聊,服务器那份根本不知道。所以宁可让服务器上那份是「会过期的缓存」,也不让它变成「另一份档案」。docs/88-native-transcript-as-truth-contract.md:175 选项 E 判定
什么时候锁上:这条会话同时有了「原生会话号」和「绑定的是哪个 AI」两个值之后。
行为不是拒绝,是强制拨回:你在界面上选了另一个 AI 并发消息,调度台照样受理,但把本轮用的 AI 拨回原来那个,并往会话里插一条可见的说明:「该会话已绑定 xxx 原生会话;换引擎会开一条新的空记忆会话,故本轮仍按 xxx 执行;要换引擎请新建会话。」
为什么要这样:续聊靠的是「原生会话号」,而这个号是某个 AI 自己的。换成另一个 AI,续聊解析结果为空,就会开一条全新的、什么都不记得的原生会话。宁可强行拨回并明说,也不让实际行为悄悄偏离你在界面上看到的。
两个时限,只对已结束(完成/失败/取消/错误/超时)的任务生效,正在跑的永远不碰:
但它默认是关着的:只有环境变量 WORKBENCH_STORE_PRUNE=1(或代码里显式传参)才启用。线上是部署脚本写进服务配置里的,所以生产开、本地和测试关——测试要靠「不清理」来验证审计留痕。启用后:启动时扫一次,之后每 10 分钟 扫一次。
对你的直接影响:任何结论如果只活在某次任务的事件流里,两小时后就没了;只活在任务行里,七天后就没了。所以结论必须另存成产物或文档——产物集合不在清理范围内。
五道防线:发版前的门禁(跑几百个检查)、写入白名单(不认识的字段直接丢)、容量红线(一次真实的生产卡死换来的)、身份分层(三种凭证,各自只能干各自的事)、部署链路(三条固定路径,不许手工操作)。
边框样式就是这道闸的强度:粗实线=服务端代码在拦;虚线=有条件或只有浏览器在拦;点线=只写在文档和角色卡里,没有任何程序检查。
右侧那些短箭头是每道闸的拒绝出口。注意右列下面两道——它们的出口是灰色虚箭头、通向「没有程序会拦」,这是全图最该记住的一处。
左列四道都是实拦:凭证不对回 401、不在白名单的字段写的时候就被丢掉、门禁失败即停、部署三步用 && 串联,任一步失败就地停住。
npm run cc:verify —— 完整门禁.mjs / .js 文件(214 个,已排除依赖包、私密目录、构建产物、台账目录)逐个跑一遍 node --check。这只检查「能不能被解析」,不执行代码。cc:test:*(纯逻辑单测)和 cc:probe:*(起一个真服务器打接口)。npm run cc:qa —— 回归门禁每一类数据都有一份「允许写哪些字段」的集合,写入时逐个字段判断,不在集合里就跳过。目前有九类:待排期需求、任务卡、附件、连接器状态、工作区、角色记忆、编排状态、界面偏好、自主循环。台账代码里有十几处这样的过滤点。
现场数字(代码注释里逐条记着,标注为「已用数据确认」):台账文件 92 MB,326 个任务里塞了 71286 条事件。
怎么死的:每次改动都要把整个 92 MB 同步序列化并写盘,一次约 2 秒;而工人每约 2 秒 就会触发一次写。于是那条单线程的事件循环几乎 100% 被占满——所有请求(连健康检查 /healthz 都算)被拖到大约 60 秒,连 ssh 登录进程都抢不到 CPU。
根因判定:不是「格式化输出太占地方」,而是事件流没有上限。所以修法是加保留策略(第三层图 3c),不是压缩格式。
现在的三道缓解:① 紧凑 JSON(不缩进);② 先写临时文件再改名覆盖,保证读到的永远是完整文件;③ 同一时刻的多次写合并成一次(用微任务队列),并且单次写超过 50 毫秒 就打一条慢日志,带上字节数、任务数、事件数。services/orchestrator/storage.mjs:403-414 事故记录services/orchestrator/storage.mjs:467-486 合并写 + 原子写 + 慢日志
怎么带:Authorization: Bearer <操作者令牌>
能干什么:几乎所有业务接口——建任务、改会话、管待排期需求、看系统页、读产物。你和总控用的都是这个。
范围:最大。它是这套系统的「万能钥匙」,所以它只存在 .private 里,不进版本库、不进日志。
怎么带:X-Worker-Token 请求头(也接受同值的 Bearer)。
能干什么:只有工人那几个接口——领活、心跳、报事件、报结果、上传对话、查该起几个工人。
为什么单列一种:工人跑在你自己 Mac 上,风险画像和「人在浏览器里操作」完全不同。它拿不到操作者专属的那些接口。默认值上它可以回退到操作者令牌,但接口分组是硬分开的。
长什么样:wsat_ 加 48 位十六进制随机串(不可猜)。
能干什么:只能读写它绑定的那一条会话。A 会话的凭证碰不到 B 会话,操作者专属接口一个也碰不到。
时效:默认 1 小时过期;只存在调度台内存里,服务一重启全部失效(要用就重新签一个)。过期的会在被使用时顺手删掉,所以那张表不会无限长大。
用途:让另一端(比如终端里重新接上来的命令行)能继续给某条会话下指令,而不必拿到万能钥匙。
npm run cc:deploy:api → 阿里云那台服务器/opt/personal-workbench,环境变量文件单独同步;然后写系统服务定义、重载、重启服务 personal-workbench-api;最后在服务器本机 curl 一次 /healthz 自检——不通就当部署失败。服务监听 3001 端口,定时清理开关在这里被写成 1。
npm run cc:deploy:web → Vercel 生产环境apps/web 目录(Vercel 项目里配的根目录就是它),绑定 workbench.heyyys1.com。发之前会先对着本地文件校一遍页面标记——因为曾经出现过「标记漂移只能在发上去之后才发现」的情况(2026-07-26 红过一次)。
npm run cc:deploy:mockup → 独立 Vercel 项目personal-workbench-mockup,绑 mockup.heyyys1.com,源是 design/mockups,刻意与生产站隔离。项目硬规矩:所有设计稿一律发到这一个域名下,不许为单个稿子新开子域名。目前 inventory 里这个项目的状态标的是 pending。
npm run cc:deploy = 门禁 && 后端 && 前端&& 串联,任意一步失败就地停住,不会出现「测试没过但已经发上去了」。
api.workbench 是 A 记录直指服务器 IP、不走代理;workbench 和 mockup 都是 CNAME 指向 Vercel。访问控制(Cloudflare Access)已启用,白名单是一个邮箱。非密事实全部记在 infra/inventory.json 里,密钥另存在不进版本库的目录。
cc:test:* 是纯逻辑检查,不联网不动磁盘;cc:probe:* 会真起一个本地调度台进程,用临时台账打真接口,验完关掉。--delete 会把远端多出来的文件删掉,让两边严格一致。--delete 是关键:没有它,删掉的旧文件会一直留在服务器上。三种状态严格区分:已上线(生产在跑、有测试守着)、在建 / 只通了一半(代码在但闸没开,或明确划在范围外)、只有设计(文档写了很多,代码一行没有)。
填充与边框=状态:绿实线=已上线(生产在跑、有门禁守着);黄实线=在建(代码在但路没走通);灰点线=只有设计(零行代码)。
每格第二行是判定依据,不是印象:已上线的都指得出对应门禁项,只有设计的都给得出「搜某字段命中 0 处」这类证据。
底部那条红虚线连着两个最容易搞混的格子——这是本层唯一一条跨区连线,画出来就是为了防止下一个需求建错地方。
能在生产上用,且有对应门禁
cc:test:worktree 守着。cc:test:supervisor、cc:test:scale-signal。cc:test:transcript-parse、cc:test:transcript-channel。tasks 集合、有 /tasks 系列接口、有可编辑字段白名单,门禁里有 cc:test:task-center。这是 docs/69 的「任务卡看板」,不是下面那个「事项卡」。cc:test:backlog / connectors / attachments / gm-loop 等)。代码在,但路没走通或明确划在范围外
rollout-*-<id>.jsonl),但调度台那道闸没开:非 claude 的上传直接回 409「不支持这个 AI」。所以功能实际不可用。workers/mac-worker/transcript-sync.mjs:58-85 定位已就位services/orchestrator/server.mjs:3216-3221 服务端未放行pending(生产站是 deployed)。infra/inventory.json:69-76registry/runners/kimi-cli.yaml 这份登记,但工人的分发表里只有 claude 和 codex 两个适配。项目文档也是这么定的:先只做 codex,另两个只留登记。workers/mac-worker/worker.mjs:108-116 分发表只有两个文档写了很多,代码一行都没有
services / workers / packages / apps / scripts / registry 里搜 contract_card,命中 0 处。文档里那些字段名(contract_cards / items[] / decisions[] / outcome / 分级门 / tracks[])全都只存在于文档。Status: planned,并且明确「澄清未清零(四个问题阻塞),未打 reviewed,不得派发」。调度台里也确实没有「枚举本机会话清单」的接口。sess_* 换成原生会话号。文档实测:现存 189 条会话里有 175 条没有原生身份,会被划进「历史 · 未纳管」分区,如实标明而不假装。feishu.cn / larksuite.com / feishu.net);另有一条连接器授权路径,且凭证只留在你的 Mac 上;归档是个独立脚本。在任何 AI 动手之前,这件事得先被写成一份八项内容齐全的任务书,并且被明确标记为「已评审」。这一层最反直觉的一句话是:「八项缺一项就不许派发」这条规矩,代码里一行都没实现——真正会拦人的只有一个布尔状态。图上把这个区分逐条标了出来。
左边三格是写合同:需求落行(默认「未评审」)→ PM 出提案 → TechLead 转成八要素契约。中间两格是点线——那些规矩只写在角色卡里,没有程序检查。
菱形是整层唯一的硬闸:评审状态不等于「已通过」就直接回 400,而且拒在读请求体、找会话、建任务之前,一点痕迹都不留。
下方那条绕回菱形的蓝箭头是循环:待定项没清零 → 开一场澄清(不起任务)→ 拍板后标记已评审并清空待定项 → 回到评审门重判。
pending_review(未评审)——不是「已通过」。也就是说,一个刚落进来的需求天生就是派不动的,必须有人显式把它拨到「已评审」。
pending_review(还没人看)、needs_clarification(看了,但有会改变范围的待定项没定)、reviewed(可以派了)。
docs/ 下起草候选方案文档给你挑。一份方案必须交代四件事:问题是什么、收集到的证据、最小可用范围、还没定的待定项。
forbidden_actions 逐条列出);能改的文件被限定为「只有文档」(local_change: docs_only);能跑的命令只有一条 npm run cc:status。permissions、forbidden_actions、allowed_scripts 全都只是被拼进 AI 的提示词,没有任何程序在执行时校验它们。真正会拦住命令的是命令行工具自己的白名单(第四层讲过)。所以这一格标「只是文档」——它靠的是 AI 听话。
docs/agent-playbooks/round-dispatch.md:9-12,原文是「round 文档必须包含:…缺一项则先补文档,不派发」):
git add -A 这类事故。这是本层最该记住的一句。我在仓库里逐项搜过:
services/、packages/、workers/、apps/、registry/ 全部 0 处命中。scripts/ 只有 1 处,是一句种子文本描述(不是校验);剩下 4 处都在设计稿的示意文案里。文档里出现 23 次、分布在 20 份文档——全是人写给人看的。那到底什么在拦?只有下一格那个「已评审」标记——它检查的是一个布尔状态,不检查这八项在不在。也就是说:一份只写了两项的烂契约,只要有人把它标成「已评审」,就能顺利开工。这不是设计缺陷的猜测,是代码事实。
POST /backlog/<需求号>/dispatch。它的第一件事不是干活,是先查评审状态:只要不等于 reviewed,直接回 400「未通过评审」。
needs_clarification(需澄清)天然落进被拒分支——因为它不等于 reviewed。代码注释明确说这是故意的,不给它单开一条判断。POST /backlog/<需求号>/clarify。它新建一条挂在 PM 名下的会话,塞进一条开场消息——内容是需求标题 + 需求描述 + 待澄清问题清单(逐条编号)+ 一句「请就以上待定项与我澄清,先不要开始实现」。然后把这条会话号记回需求那一行。POST /backlog/<需求号>/resolve-clarification。它把评审状态拨到 reviewed,并且把待澄清问题清单清空——因为「澄清完成 = 待定项已被拍板解决」,不清空的话下一步会撞上界面上那道「还有遗留问题」的确认门。PM 的结论文字(review_note)保留。clarify_session_id),否则回 409「从未澄清过」。代码注释解释了为什么用这个字段而不是猜文档标题:「item 与澄清产物的可靠绑定是 item.clarify_session_id——不靠文档标题猜」。reviewed,只要待澄清清单还没清零,网页上的「推进」按钮就要你显式勾选一下「不阻塞」才启用。这是防止「评审通过了但遗留问题被忘掉」。它只活在浏览器里——改一行请求就能绕过,所以是软闸。
Status: …,写这一轮走到哪了(草案 / 契约冻结 / 待派 dev / 已完成待你验收 / 卡住)。有一条命令负责批量改它:node scripts/workbench-set-round-status.mjs --status "…" --docs a.md,b.md --table 排期表.md --rows 行关键字,同时改文档头部那行、和排期表里对应行的状态格。
Status:(判断语句就是「这一行是不是以 Status: 开头」),找不到就打印 NOT FOUND 并以退出码 2 结束。docs/82 / 83 / 84 / 85 / 88 / 89 这批新契约改用了引用块写法:> 作者:tech-lead-agent(…)。状态:契约冻结,待派 dev。——既不在行首(前面有 >),用的也不是英文 Status: 而是中文「状态:」。脚本一条都改不到。Status: 的文档 50 份,用引用块「状态:」的 10 份。docs/90:143 原文「Status 行三格式分叉不修,让新机制绕开它」。(那份文档说的是「三种格式」,但没有逐条列出它数的是哪三种;我实测到的是文档头两种写法,第三种按脚本设计推测应是排期表里的状态格——这一条我标「待核」,不替它编。)stripStatusLine,把文档前导区的 Status: 行丢掉(空行和 # 标题行保留、仍算前导区;一碰到第一段实质正文就停止剔除)。
> 开头不匹配那条规则,而且它会直接终止「前导区」。所以那批新契约的卡片标题上会挂着「作者:tech-lead-agent(本轮 run)。状态:…」。同一个改动同时打坏了改状态的脚本和清理标题的前端。
澄清项(代码字段 open_questions,文档里写成 OQ-1、OQ-2…)=会改变做法或范围、但还没拍板的问题。
output_contract.required 把 open_questions 和 summary、evidence、output_artifacts 并列,四项都必须交。OQ-1 不是全局编号同一个编号 OQ-1 在不同文档里指的是完全不同的问题。实测:18 份文档里都有 OQ-1。
| 文档 | OQ-1 在那里指的是 |
|---|---|
| docs/68 | agent 卡片能不能拖动、拖了要不要记住 |
| docs/74 | 右栏「当前任务」面板留不留 |
| docs/82 | 续聊时还要不要重新注入身份与规则 |
| docs/84 | 「收编本地会话」这个接口该做成什么形态 |
| docs/85 | 旁路工作副本的会话,续聊目录从哪来 |
| docs/86 | 新组件库的视觉基准照搬原站还是映射本站 |
| docs/89 | 原生会话清单存在哪个文件里 |
后果不是理论上的:用户 2026-08-13 拍板的那条 OQ-1=B,只活在 docs/86 一个文件里——文件内出现 7 次、文件外 0 次。后来靠 docs/91 的长期约束清单第 C5 条把它抢救成全局约束,才没丢。
修法已提出,零行代码:给决策发全局唯一编号(形如 dec_xxxx)来根治重号。这只是提案,没有落地。
--objective-file 参数读的就是这个文件本身,一字不改地送进去。reviewed,派发接口在读请求体之前就回 400。OQ-1 在 18 份文档里含义各不相同,跨文档引用会指错。Status:(50 份文档符合),最新一批 10 份契约改用了引用块 +「状态:」,脚本改不到;网页剥噪音的规则也同样漏掉这批。docs/90:143 已明确决定不修。docs/86 头部写着「撰写:pm-agent」,那份八要素契约是 PM 写的,不是技术负责人。角色卡是提示词,不是程序。第一层讲的是「一句话」怎么走完一圈(九步,机器视角)。这一层讲的是一轮正式开发怎么走完一圈(十二步,人机协作视角):多出来的是写任务书、独立复核、合并回主干、部署、你来验收这五件只有人参与的事。每一步都标了真实命令、接口和文件。
一条主流水从左上走到右下。中间有三处结构:菱形「会话正忙?」扇出三个选项(等 / 排队 / 停)、虚线泳道里三个并行工人槽位、以及从槽位绕回去的每 2 秒循环。
蓝色那条长箭头是事件回流:AI 每走一步,事件按序回到调度台,再实时推给你的浏览器——所以你能看着它一行行长出来。
三个红框是陷阱,挂在它们真正发生的那一步旁边:队列旁边那个最要命——没有工人在线就没有超时把它踢出来,会一直等下去。
--objective-file 参数直接读那份契约文档,整篇读进来、去掉首尾空白、一字不改地当作这次任务的描述。文档命名有约定:docs/NN-<主题>-roundN-<内容>.md。
node scripts/workbench-dispatch-round.mjs --title "…"|--session <会话号> --agent <角色> --objective-file docs/NN-….md
--runner 显式指定 > 该角色在 registry/agents/<角色>.yaml 里写的 default_runner > 最后兜底 claude-code。注释写明:registry 读不到(文件缺失、字段缺失)时静默回退,绝不因为默认值解析失败而挡住派发。GET /runs?limit=100 拉最近 100 个任务,算出「还没跑完的有哪些」,再决定拦不拦。拦了就退出码 2。--title → POST /sessions 新建一条(带标题、项目、默认引擎、绑定角色);给了 --session → 先 GET 确认这条会话在,然后复用。二者必须给一个,都不给直接报错退出。POST /sessions/<会话号>/messages,正文就是整篇契约。返回里如果没有任务,退出码 3。evaluateDispatchGuard 到底拦什么(这是理解第八层的钥匙):
--title)→ 永远放行。一条新会话不可能有正在跑的任务,而且它会拿到自己的工作副本,所以它跟别的会话天然并行。--session)→ 只在「这条会话自己」还有没跑完的任务时拒绝。同一条会话的多个任务必须串行,因为它们共用同一个工作副本和同一个分支。--force 是逃生口,两种情况都能强行放过。--dry-run:只打印计划(复用还是新建、用哪个引擎、哪个项目、任务书多少字、前 200 字预览)然后退出,一个写请求都不发。派高风险轮次之前先跑这个。
--on-busy queue:透传成请求里的 on_busy 字段。含义是——这条会话已经有任务在跑时不报错,改为排队。注释里特别澄清了一句容易误解的话:领活层保证同会话严格串行(共用一个工作副本、改动按序累积),所以「排队」不等于「并行」。另一个取值 stop 会先取消正在跑的那个,属破坏性操作,必须显式指定。
on_busy → 回 409「该 session 正在被使用」,附上活跃任务号和三个选项 ["wait","queue","stop"],什么都不写。带了 queue → 直接往下走;带了 stop → 先取消活跃那个再走。注释解释了为什么要在写入边界上再拦一次:另一端(命令行、第二个浏览器标签)可能不知道已经有任务在跑,不能悄悄多派一个。pickRunnerId(请求里的值, 这条会话记住的默认值 ?? 全局默认)。这个函数只认白名单里的两个(codex-cli / claude-code),两个都不是就回全局默认 claude-code——注释明写「不再硬编码回退到 codex-cli」。queued。有一条不变量在这里守着:任务的提示词 === 你这条消息的原文,不许注入身份、历史或规则。claimableRunsForWorker 里:状态可领(排队中 / 等工人 / 等额度且已到点)+最老的先走(按创建时间正序)+同一条会话已有一个在跑,就把这条会话名下其它排队任务全部隐藏+任务要的能力标签,工人必须每一条都声明过。
--on-busy queue 塞进同一条会话的第二个任务,会一直被这条规则藏着,直到第一个跑完为止。这是好事:它们共用同一个工作副本,同时跑一定互相踩。
native_cwd)→ 就在那条会话自带的目录里跑,优先级最高。原因:命令行 AI 是按目录存对话记录的,把目录挪走,续聊直接找不到那条会话、绑定就断了。目录不是可读目录、或不在白名单里 → 直接失败并报错,不静默换目录。agent_home,例如常驻聊天座席)→ 在 <项目根>/agents/<角色号>/ 这个固定不动的目录里跑。原因同上:让这个角色的对话记录始终堆在同一个地方,命令行上 claude --resume 也能看到同一批会话。目录不存在 → 报错并告诉你该跑哪条命令补,不降级到一个没有身份的目录。worktree_ref)→ 在旁路工作副本里跑:路径 <项目父目录>/<项目名>-wt/<会话号>,挂着分支 session/<会话号>。这个旁路刻意放在仓库外面,好让主仓的 git status 保持干净、回收时也永远碰不到主仓。isAllowedCwd),越界一律抛错。服务端那份白名单是第一道闸,工人这道是第二道、物理闸——同一件事拦两次是故意的。
main 切一份(连 main 都找不到就用当前 HEAD)。崩溃残留(登记了但目录没了 / 目录在但分支不对)会被强制回收后重建。顺便把主仓的依赖目录和密钥文件软链进去,否则新副本里连依赖都没装、跑不起来。
POST /runs/<任务号>/events。调度台收下之后做两件事:存进台账,以及在进程内广播给所有正连着 GET /runs/<任务号>/stream 的浏览器——你在网页上看到过程一行行长出来,就是这条推送。这条推送只活在内存里,服务重启会断,重连时靠快照补齐。
POST /workers/<工人号>/results,带一批只有工人才知道的事实changed_files:这次到底改了哪些文件,格式是 git 的机器可读状态行(形如 M docs/x.md、?? a/b.css)。第 10 步的合并完全靠它,所以它是整条链上最关键的一个字段。diff_stat(改了多少行的汇总)、tests_run(跑了哪些测试)——让任务中心能显示这些,不必再去问工人一次。workspace_dir:真实的执行目录(工作副本型任务就是那个旁路路径)。这是「下次续聊该在哪个目录里 resume」唯一缺的原料。usage:这次烧了多少 token,只落原始数字;折算成美元是读的时候再算的(价格表在服务端另一处),所以改价格不用回填历史数据。native_session_id:这次真正产生/续上的那条原生会话号。第一次上报会把它绑到会话上,连引擎一起记——第八层判据 3 的引擎锁就是从这里开始生效的。npm run cc:qa:agent —— 它不是「另一个 AI 来审」,而是把项目内固定的脚本和探针聚合成一个入口,回答一个是非题:这一轮的自动验收链过没过?共 15 道门:
docs/86 头部那行状态就是一次真实记录:「GM独立复核14/14通过(verify脚本重跑+截图目检)」。
npm run cc:merge:run -- <任务号> [--dry-run] [--allow <路径前缀>]…
changed_files 清单。--allow <前缀> 可以把可合并路径锁死(可重复给)。给了就只允许这些前缀,越界文件一律拒绝并以非 0 退出——防止某一轮越界改到别人的目录。--dry-run 只打印计划。cp -R <工作副本>/<目录>/. <主仓>/<目录>/ 整目录覆盖,导致并行任务互相盖掉成果,一天内发作四次,其中一次把批注层的两行引用整段删除。开发阶段那几组轮次都是并行的,整份覆盖会直接吃掉别人的工作。所以现在改成「只搬这个任务自己声明改过的文件」。
npm run cc:deploy:api —— 把后端文件同步到那台云服务器的 /opt/personal-workbench,然后重启系统服务 personal-workbench-api。登录走固定密钥 + 严格的主机指纹校验(StrictHostKeyChecking=yes)。它顺便还负责一件事:把「清理开关」写进线上环境变量——所以第四层那套保留策略只在线上是开着的(见下面陷阱②)。npm run cc:deploy:web —— 把前端那个 HTML 目录拷到一个没有 git 的临时目录再发布。为什么要绕这一下:从仓库里直接跑发布命令会无限挂住(注释里写明了)。发完之后逐条断言生产标记:会话对话界面、任务中心、过程时间线、结果面板、任务操作按钮…缺任何一条就抛错。它还刻意拒绝一种情况:拿不到可直连的发布地址时大声失败,而不是拿一个登录页去对标记(否则会报出「标记缺失」这种误导人的错)。cc:deploy:mockup 和 cc:deploy:intro;用户点名的展示站通道是仓外的 publish-kit:npx --yes github:layyyback/publish-kit publish <目录> --to <子域>。要如实说一句:这个仓库里 0 处引用 publish-kit——发布是仓外动作,由总控或你执行,开发任务不部署。POST /runs/<任务号>/review,动作取三个值之一:accept(验收通过)、block(打回,可带说明)、clear(清掉标记)。落一条复核记录并写一条事件。accepted_by、user_acceptance 这类「记下是谁点的」字段,服务端 0 处写入。也就是说服务端分不清这一下是你点的还是总控点的。forbidden_actions 里那条 mark_user_acceptance 是拼进 AI 提示词的一行文字,没有 gate。现象:派发成功、界面上能看到这个任务,但它永远停在「排队中 / 等工人」,没有任何超时会把它踢出来。因为调度台从不主动去找工人(第一层第 3 步),没人来领活就没人来领活。
怎么提前发现:健康检查接口会返回当前在线工人数 workers_online;npm run cc:status 会直接打出来。派发手册里也写了这条:worker 离线时派发会停在「等工人」,可以照常派发但必须在汇报里说明。长期约束清单第 C8 条把它列为硬规矩:派发前必核 worker 在线。
两个数字:终态任务超过 2 小时 → 它那一整串过程事件被清空(任务行本身和最终答案保留);超过 7 天 → 整行连同事件一起删除。正在跑的任务永远不碰。启动时扫一遍,之后周期性再扫。
一个必须知道的限定:这套清理默认是关着的,只有环境变量 WORKBENCH_STORE_PRUNE=1 才启用——线上由部署脚本写入,本地和测试环境是关的(测试要靠「不清理」来验审计留痕)。
为什么必须有它:真实事故——线上台账文件长到 92MB、71,286 条事件、326 个任务;每次落盘都同步把整个 92MB 序列化重写一遍(约 2 秒),而工人那边每约 2 秒就会触发一次写 → 事件循环几乎 100% 被堵住 → 连健康检查都要 60 秒才回,ssh 服务被 CPU 饿死。根因是那条无上限的事件流。
对你的实际影响:7 天前的任务详情在线上是真的没了。要留证据(比如某次事故的完整过程),得在 7 天内自己抄出来。
能找到出处的事实(三处,都实读过):
open 命令被禁——所以「渲染页面 + 截图」这件事根本不能在普通开发任务里做,必须落到你 Mac 上那个常驻程序(它声明了本地渲染能力)。awk、重定向写临时文件、甚至 node --check 语法检查都被拦了,临时目录和工作目录内都不行。那一轮的交付说明里直接写了「任何点不出来的交互请报给我修」。ls 被沙箱拦截(用读文件的工具能读,只是命令不行)。我没能核实的部分:「沙箱专门挡住 AI 写 git 元数据」这个说法,我在整个仓库里找不到任何出处——待核,不替它编。
真正成立的对应事实是「契约级」的,不是沙箱级的:无人值守轮次的契约里明写「dev 禁擅自部署 / 重启,收尾由 GM 执行」,禁止动作清单里也列着「禁止亲自部署 / 重启」。所以合并、部署、git 收口这些事由总控在 AI 之外做——原因是规矩这么定的,不是因为沙箱拦不住。这两个原因导致同样的结果,但含义完全不同:前者可以改,后者改不了。
wait(客户端自己等)、stop(先取消正在跑的那个,破坏性)。session/<会话号>,放在主仓外面的兄弟目录里。一条会话一个。git status 始终干净、回收副本时永远碰不到主仓。--allow <前缀> 还能把可搬路径锁死,越界拒绝并非 0 退出。派活时只有一个选择:--session <旧会话号>(复用)还是 --title "新标题"(新建)。看着像个小参数,实际上它同时决定了AI 记不记得前面聊的事、改动落在哪个分支、会不会跟别人撞车、它看到的代码是哪一天的。四条判据里第一条是唯一的正向理由,后三条都是否决权——只要任何一条否决,就必须新建。
四个菱形串成一条判定链:第 1 个是唯一的正向理由(要不要旧记忆),后 3 个都是否决权——任何一条成立,「我想让它记得」这个愿望立刻作废,不再往下问。
右边四条出口汇聚到同一个结论。红色那三条是否决,各自的理由写在箭头旁:撞文件会被静默盖掉、换引擎记忆归零、副本陈旧就看不到新增的文件。
只有一路走到底才是复用。拿不准就新建——新建的代价只是重讲一遍背景,误判复用的代价是别人的成果被盖掉,或者 AI 按看不见的旧文件白做一轮。
apps/web/index.html),不许并行,必须排先后。而排先后的正确做法是塞进同一条会话排队,不是开两条会话同时跑。
session/<会话号>,并且独占一份旁路工作副本 <项目父目录>/<项目名>-wt/<会话号>。两条会话就是两份各自完整的代码目录,各自在自己那份里把同一个文件改了一遍。它们互相看不见对方改了什么。--on-busy queue 排队。它们会顺序执行、改动累积在同一份工作副本里,根本不存在两份要合。
这不是推理,是实测出来的:会话 sess_299b637d8fa4 的 31 个任务,跨了 4 个不同的原生会话 id:
019ffb70;b6cedb9d;01a000e7。规律:每一次换引擎,都必然开一条新的原生会话,那侧记忆从零开始。这条线反复出现的「重切阶段 / 写交接文档 / 返工」,根因就是这个。
而且已经排除了另一种解释:核实过任务的提示词与派发出去的任务书逐字节一致——所以不是 AI 装傻或偷懒,是记忆真断了。这一点很重要:如果误判成「AI 不好好干」,你会去改任务书措辞,而那完全没用。
merge、pull、rebase——0 处命中。也就是说没有任何代码会去把主干的新变化拉进老副本。今天这台机器上一共有 159 条会话工作副本。主干当前停在 f2aef57(2026-08-18)。其中:
9e90abe —— 那是 2026-07-19 的提交,落后主干 152 个提交。9e90abe 那个版本里:design/DESIGN.md 的第一行写的是「Workbench 设计系统 DESIGN.md(v1)」,而且里面声明的真源文件是 design/tokens.css。design/tokens.micro.css 在那个版本里根本不存在(git 的原话:「路径 design/tokens.micro.css 在磁盘上,但是不在 9e90abe 中」)。这个文件是 2026-08-15 才随 v2 规范一起加进来的。所以那个事故是这样发生的:让设计角色「按刚定稿的 DESIGN v2 出稿」,但它那条老会话的副本里躺着的是 v1、v2 的 token 文件根本不存在。它读不到新规范,只能拿手边的旧稿凑——它没有说谎,它是真的看不到。
怎么避免:派活给一条老会话之前,先确认它的副本里包含这一轮要读的那些文件(尤其是最近才新增的文件)。不包含就新建一条会话——新会话会从当前主干切,一定是最新的。
--session <旧会话号>):要旧记忆 + 不撞别人的文件 + 不换引擎 + 副本里有这一轮要读的文件。四条全中才行。--title "…"):其余任何情况。新建的代价只有一个——得把背景重讲一遍;而误判复用的代价是别人的成果被静默盖掉、或者 AI 按看不见的旧文件瞎做一轮。拿不准就新建。--on-busy queue),不要开两条并行。docs/91 长期约束清单docs/91:26),并且下面附了那段 31 个任务跨 4 个原生 id 的实测证据(:36-42)。docs/91:33),对应第七层陷阱①。docs/91 是「总控长期约束的唯一落点」,进 git、不会被任何进程覆盖——这份文件本身就是为了解决一次事故才建的:当时总控为了记一条长期指令,手写了一个伪装成平台产物的文件,而那个文件会在下一次运行时被无条件覆盖,那条指令即将静默消失。
CLAUDE.md(「物理约束核对」那一段)和总控自己的记忆里。docs/91 的 C1–C9 里没有这一条。docs/91,也不在 CLAUDE.md。它目前只存在于总控的记忆和这次实测里。为什么这是个问题:这两条都是否决权级别的判据,而且都会造成静默损失(成果被盖掉、AI 按旧文件瞎做)——恰恰是最需要写进受管清单的两条。按 docs/91 自己定的规矩,长期约束一律该写进那份文件。
(这是本图对现状的如实标注,不是本图在提议改动——本稿只讲系统怎么运转,不提改动方案。)
session/ 加上会话号。一条会话一条分支,全局唯一(会话号本身唯一),所以多个项目之间也不会撞名。design/tokens.micro.css 都不存在。派活前不核这一点,AI 就会「按它看得见的东西」交一份你没要的稿子。前八层反复提到文件路径,但只解释了名词。这一层回答两件具体的事:把这套系统跑起来,最少需要哪些文件、每个文件干什么;以及那些被叫做「记忆」和「需求」的东西,在磁盘上到底长什么样。目录树只画跑得起来所必需的路径,无关文件不画。
怎么看「少了哪个就跑不起来」:紫色是调度台的、青色是常驻程序的、蓝色是前端的、黄色三者共用。黄色那几行是最不能少的——尤其 packages/contracts/,它是三个入口唯一的跨目录依赖。
依赖面实测很干净:三个进程入口的相对引用只指向自己同目录 + ../../packages/contracts,没有别的跨目录依赖。所以「最小可跑集」不是估的,是扫出来的。
灰色虚线那三行不在仓库里。这一点最容易踩:clone 一份仓库并不会带上记忆和对话记录,它们跟着这台机器的家目录。
一句话分清:① 跟着人 + 启动目录、② 跟着角色、③ 跟着项目、④ 跟着这条对话。问「为什么它忘了」之前,先确定问的是哪一处。
④ 严格说不是记忆,是对话的原始记录——系统只读、从不改写(第三层)。把它当记忆去「清理」或「搬运」,就会撞上引擎锁那类问题。
② 与它下面那个 memory.md 的关系是真相与副本:台账里那 9 个字段是真相,文件只是派活时物化出来给 AI 自己读的一份,会被下次运行无条件覆盖。
槽名怎么来的:把启动时的工作目录绝对路径里的 / 全换成 -。从 /Users/linoyeung/Documents/codexworkspace 启动,就落在 -Users-linoyeung-Documents-codexworkspace 这个槽——也就是本会话读到的那 41 条。
那 25 条最后修改于 2026-08-11(已经沉了),其中四条至今有效,已并入 docs/91 的 C11–C14。这是「跨槽共享只能靠入 git 的文件」的一个现成例子。
| 字段 | 它是干什么的 |
|---|---|
name | 这条记忆的短名,同时是文件名(去掉 .md)。[[wikilink]] 靠它互链。 |
description | 一句话摘要。召回时先读它判断相关性,所以它决定这条记忆会不会被想起来。 |
metadata.node_type | 固定 memory。标明这是记忆节点,不是别的图谱节点。 |
metadata.type | 分类。GM 实测该目录只出现三种:project(在做的事)/ feedback(用户给的工作方式纠正)/ reference(外部资源指针)。 |
metadata.originSessionId | 这条记忆是在哪次对话里写下的。用来回溯「当时为什么这么记」。 |
metadata.modified | 最后修改时间(UTC,带 Z)。时区写法本身是个识别线索:平台产物只产 UTC,写成 +08:00 的就是人手写伪造的。 |
| 正文 | 事实本体。feedback/project 类要跟 Why(为什么)与 How to apply(怎么用)两行。 |
MEMORY.md 索引(每个会话开场被加载)为什么要分成「索引 + 一条一文件」两层:索引每次开场全量加载,所以必须短;正文按需才读。索引里那句钩子写得好不好,直接决定这条记忆会不会被想起来。
agent_memory:9 个字段就是全集(封闭白名单)| 字段 | 它是干什么的 |
|---|---|
id | 这行记忆的编号。 |
agent_id | 属于哪个角色。这就是「记忆跟着角色走」的落点。 |
scope | 作用范围(个人 / 项目 / 工作区 / 某角色自己)。角色卡里的 memory_scopes 声明它能读写哪些范围。 |
project_id | 跨项目隔离:A 项目的角色记忆不会漏到 B 项目。 |
workspace_id | 工作区级隔离(可空)。 |
key | 这条记忆的键名,用来定位与覆盖。 |
content | 记忆正文。 |
created_at / updated_at | 建立与最后更新时间,由台账写入,不由调用方控制。 |
「封闭白名单」的意思:写入时逐个字段判断,不在这 9 个里的键直接丢掉。所以这张表就是字段全集,不是「目前已知」。services/orchestrator/storage.mjs:185-195 定义storage.mjs:812-813 写入边界
它不在保留策略的清理范围内(那套只清终态 run 的事件与行)。但它物化出来的 agents/<角色>/memory.md 会被下一次运行覆盖,且不入版本库。
backlog_items:15 个字段(封闭白名单,即全集)| 字段 | 它是干什么的 |
|---|---|
id · title · description | 编号、标题、需求描述(描述上限 4000 字)。 |
assignee_agent_id | 负责这条需求的角色。 |
priority | 优先级,只能是 p0/p1/p2,默认 p2。 |
review_status | 整个契约层唯一真会拦人的字段:三档 pending_review/needs_clarification/reviewed,新建默认未评审;不等于 reviewed 就拒派。 |
review_note | PM 的评审结论文字。 |
open_questions | 待澄清项清单(最多 20 条、每条 500 字)。没清零时界面上要你显式勾选才让推进。 |
clarify_session_id | 为这条需求专门开的澄清会话。只由澄清端点内部写,不进 PATCH 白名单——所以它是「澄清过没有」的可靠依据,不靠猜文档标题。 |
status | 流转状态:backlog(待排期)/dispatched(已派)/closed。只有待排期的能派。 |
source_doc · doc_url | 来源文档路径与外链(飞书链接走这里,飞书只做信息仓)。 |
linked_run_id | 首次派发时由服务端回写的单向指针,指向那次任务。 |
created_at · updated_at | 建立与最后更新时间。 |
出处 services/orchestrator/storage.mjs:75-94。注意 review_note/open_questions 可经 PATCH 写,clarify_session_id 不行——这个区别写在代码注释里,是刻意的。
orchestration_state:13 个字段(每个项目一行)| 字段 | 它是干什么的 |
|---|---|
id · project_id · created_at | 身份三件套,建后不可改(连 PATCH 也改不了)。 |
owner_agent_id | 这份编排状态归哪个角色(总控)。 |
workspace_id | 预留、可空。当前一个项目一行,将来按工作区拆分时才用。 |
active_goal | 当前在推的目标。 |
backlog_refs · session_refs | 关联的需求编号与会话编号数组。只存字符串编号,写入时不做存在性校验——指向一个已删的行也不会报错。 |
pending_acceptance | 等你验收的轮次清单,每项只留 round_id/summary/status 三个键。 |
open_questions | 编排层的待澄清项。 |
updated_by · updated_at | 谁改的、什么时候改的;时间由台账写,不由调用方控制。 |
public_view_token | 只读公开视图的不可猜凭证。建行时铸一次,之后永不可改,也不能从外部 PATCH。 |
出处 packages/contracts/orchestration-state.mjs:30-48(字段集):54-59(不可改字段)。它不是长期约束清单——那份在 docs/91,入 git、人手写。
| 东西 | 形态与出处 | 已知问题 |
|---|---|---|
| 契约文档 | docs/NN-<主题>-roundN-<内容>.md,头部一行 Status: …;派活时整篇当任务描述灌给 AI。 | 格式三分叉:行首英文 Status: 50 份、引用块中文「状态:」10 份;写回脚本只认行首英文,对新契约已失效。第三种是哪种待核 |
| run 的事件流 | 终态 run 结束超 2 小时 → 清空事件流,保留任务行与最终答案。storage.mjs:425 | 默认关闭,只有 WORKBENCH_STORE_PRUNE=1 才启用(线上开、本地关)storage.mjs:415-416 |
| run 的整行 | 终态 run 结束超 7 天 → 整行删除,事件一起删。storage.mjs:426 | 结论只活在事件流里的,两小时后就没了;只活在任务行里的,七天后就没了。结论必须另存成产物或文档。 |
| 需求 ↔ 任务 | 只有 backlog 侧的单向 linked_run_id。 | 结构性断链:在 services/+schemas/+packages/ 里搜 backlog_id,命中 0 处——run 上没有回指需求的字段。至于「非空 linked_run_id 有 0 条」,那是 docs/90:7-10 的记录,本轮没有独立复测(隔离工作仓没有生产台账)待核 |
为什么断链要单独标出来:它意味着「这个需求派出去的那次任务跑成什么样了」只能从需求那一侧单向查,反过来拿着一个任务问「它是为哪条需求跑的」查不到。
.claude/(技能包与权限白名单)和 agents/(角色家目录)。.private/**、.env*)。node <外部脚本> 而跑不了校验命令。AGENTS.md 与 CLAUDE.md(身份卡,入 git),以及运行时物化出来的 memory.md(不入 git)。cd 进去就等于以该角色对话。server.mjs:120 算出(可用环境变量覆盖),线上被部署脚本指到远程目录下。.gitignore 是 *,所以数据从不入库——这也意味着 clone 一份仓库拿不到任何业务数据。.md 文件(GM 实测本槽 41 条),外加一个 MEMORY.md 扁平索引。每条有前置元数据,正文用 [[wikilink]] 互链。<槽> 不是固定的一个:它是启动时的工作目录绝对路径把 / 换成 -(例:从 /Users/linoyeung/Documents/codexworkspace 启动 → 槽 -Users-linoyeung-Documents-codexworkspace)。实测本机有六个槽,见图 9c。docs/91-gm-standing-instructions.md。memory.md。画这份图的规矩是「凡与代码不符,以代码为准并在图上标注」。以下 19 条都已按代码更正,正文里也都标了出处。①–⑨ 来自第一轮(前五层),⑩–⑲ 来自第二轮追加的三层(第六 / 七 / 八层)。
WORKBENCH_STORE_PRUNE=1(或代码显式传参)才启用;线上由部署脚本写入,本地和测试环境是关的(测试要靠"不清理"来验审计留痕)。services/orchestrator/storage.mjs:415-416scripts/cc-deploy-api.mjs:77wsat_ 前缀,只对绑定的那一条会话有效,默认 1 小时过期,存内存重启即失效)。搜 AGENT_TOKEN 命中 0 处。services/orchestrator/server.mjs:410-437services/orchestrator/server.mjs:191GET /runs/:id/stream 的浏览器;这条推送只活在内存里,重启会断,重连时靠快照恢复。services/orchestrator/server.mjs:3134-3167services/orchestrator/server.mjs:218contract_card 命中 0 处),但这个说法会误导:仓库里已经上线了一个 tasks 集合 + /tasks 接口 + 可编辑字段白名单的「任务卡看板」(docs/69)。两者名字像、完全不同物。本图在第五层把这一点单独标了出来,避免下一个需求建在错的那个上面。registry/agents/product-research-agent.yaml)」docs/86 头部逐字写着「撰写:pm-agent」——那份八要素契约确实是 PM 写的。角色卡是拼进提示词的文字,不是程序。registry/agents/product-research-agent.yaml role 段registry/agents/tech-lead-agent.yaml role 段docs/86:5services/orchestrator/server.mjs 约 4749-4756」OQ-1 这个编号在 10 份文档里含义各不相同」docs/90:11 的原话,那是写那份方案当天的计数;今天 grep -rl 'OQ-1' docs/ 命中 18 份。也就是说这个缺陷在被记录之后还在继续扩大。正文图 6b 里列了 7 份的具体含义供核对。grep -rl OQ-1 docs/ = 18(实测)docs/90:11 原文「10 份文档」Status: 行已对最新契约失效(只认行首 Status:,而 docs/84/85 改用了引用块写法)」Status: 的 50 份、用引用块的 10 份。②失效不只因为「在引用块里」——那批文档用的是中文「状态:」,连英文 Status: 这个词都没有,所以哪怕脚本放宽到允许前导 > 也照样改不到。③docs/90:143 把它叫「Status 行三格式分叉」并明确决定不修,但没有逐条列出它数的是哪三种;我只实测到文档头的两种,第三种按脚本设计推测应是排期表里的状态格——这一条标「待核」,没有替它编。scripts/workbench-set-round-status.mjs:41 只认行首实测 50 份行首 / 10 份引用块docs/90:143stripStatusLine)」Status: 开头的行」——引用块写法以 > 开头,既不匹配这条规则,还会直接终止「前导区」。所以 docs/82-89 那批的任务卡标题上会挂着「作者:tech-lead-agent(本轮 run)。状态:…」。同一个写法改动,同时打坏了改状态的脚本和清标题的前端。apps/web/index.html:12343-12360native_cwd,优先级最高)→ ② 家目录型角色(agents/<角色号>/)→ ③ 旁路工作副本(worktree_ref)→ ④ 都不满足则退回项目根目录(第四态骨架没提)。前两态优先于工作副本是有原因的:命令行 AI 按目录存对话记录,挪目录等于让续聊失效。workers/mac-worker/worker.mjs:1146-1211 resolveWorkspaceDirnpm run cc:merge:run -- <任务号>,它按这个任务自己上报的 changed_files 清单逐文件从工作副本拷回主仓(删除项则删掉),清单外一律不碰。为什么这么做:脚本头部逐字记着事故——此前用 cp -R 整目录覆盖,并行任务互相盖掉成果,一天内发作四次,其中一次删除了批注层两行引用。代价必须一起说:因为不是 git 合并,没有冲突提示——两条会话都改过同一个文件时,后搬的会静静整份盖掉先搬的。这正是第八层判据 2 拥有否决权的原因。scripts/cc-merge-run.mjs:1-17POST /runs/:id/review {action:"accept"},它唯一做的校验是「请求带的令牌等不等于那个固定的操作员令牌」,而总控手里拿的就是同一个令牌。搜 accepted_by / user_acceptance 这类记录主体的字段,服务端0 处写入——服务端分不清这一下是用户点的还是总控点的。真正的约束只在提示词层:docs/91:28 C3、总控手册 :55、以及各角色卡 forbidden_actions 里那条 mark_user_acceptance。顺带指出:docs/91 C3 那句「服务端校验主体」本身就与代码不符,这份长期约束清单里记着一条做不到的实现声明。services/orchestrator/server.mjs:2952-2977 / :398-402docs/91:28open 被禁(docs/65:87);某一轮连 awk、重定向写临时文件、甚至 node --check 都被拦(docs/77:385、:621);ls 被拦(docs/30:93)。而「总控在 AI 之外代做收尾」的真实原因是契约级明令,不是沙箱:无人值守轮次的契约写着「dev 禁擅自部署 / 重启,收尾由 GM 执行」。这个区分很要紧:契约是人定的、可以改;沙箱是拦死的、改不了。docs/65:87 / docs/77:385,621 / docs/30:93docs/83:6,287 / docs/84:6docs/91 长期约束清单)」docs/91 只收了其中一条。逐条核过 C1–C9:判据 3 引擎锁=C1(:26,还附了 31 个任务跨 4 个原生 id 的实测证据 :36-42);判据 2「会不会撞同一文件」不在 C1–C9 里,只写在工作区 CLAUDE.md 的「物理约束核对」段;判据 4「工作副本新鲜度」哪里都没有正式记录——不在 docs/91,也不在 CLAUDE.md。偏偏这两条都是否决权级、且失败是静默的。docs/91:26-34 C1–C9 全九条services/、packages/、workers/、apps/、registry/ 全部 0 处;scripts/ 仅 1 处是种子文本描述;其余 4 处在设计稿文案里。搜八个英文字段名,整个代码区只命中 1 行注释。契约文档在派发时是被当成一整段纯文本灌进任务的,服务端不解析、不检查结构——少写一项,系统照样把活派出去。唯一会拦人的是评审标记,而它检查的是一个布尔状态,不检查这八项在不在。grep 八要素 → services/ packages/ workers/ apps/ registry/ = 0scripts/cc-test-task-center.mjs:29(唯一命中的英文字段名,是注释)