• ## 病症
    
    agent-chat-tenant-isolation.test.js 每跑一次,往共享生产库 data/graph.sqlite
    落 2 条 goal + 2 条 task。实测 29→31。库里 31 条 goal 里 **22 条是它的残留**,
    测试期间一直全绿。
    
    根因是批 5 的存储迁移留下的半截清理:/task/dispatch 的元数据从
    system/tasklog/*.json 换进了 tasks 表,而这个用例的 afterAll 只会清那个目录 ——
    清理代码还在、清的东西已经不在那儿了。
    
    ## 这是第四次,但换了个方向进来
    
    前三次(BENCHMARK_DB_PATH / USERS_DB_PATH / SKILLS_DIR,记在
    test-isolation-env.test.js 文件头)都是「设了 env,生产代码不读」。
    那条守卫查的正是这个形态。
    
    这次是**一个字都没设** —— 没有可查的东西,守卫自然查不出来。
    静态扫源码也判不出「这个测试会不会开库」(要跨 require 链推导)。
    
    ## 修法
    
    1. 用例自己开一次性库(fs.mkdtempSync + GRAPH_DB_PATH),afterAll 连目录删掉。
       赋值必须在 require 路由**之前** —— graph-db.js 的 DB_PATH 是模块级 const。
    2. graph-db.getDb() 加运行期闸门:VITEST 为真且没设 GRAPH_DB_PATH → 抛,
       报错里点名 env、点明「要在 require 之前设」。
       放在 getDb() 不放模块顶层:只为取常量而 require 是合法用法
       (goal-task-model.test.js 就这么用),顶层抛会误伤。真正开库才是越界那一刻。
    3. 守卫 graph-db-prod-guard.test.js,4 例:撞库要抛且报错可照做、设了 env 要能开、
       没有 VITEST 时不生效(防止把生产一起锁死)、样本非空。
       反向验过:注释掉闸门那一行,第 1 例立刻红。
    
    ## 顺带修一条 flaky(我自己上一版留下的)
    
    agent-docs-port.test.js 先 walk 出文件清单再逐个读,中间别的用例把 zz-* 夹具的
    运行态目录清掉 → ENOENT。**报错长得像端口违例,其实跟端口无关**,实测 8 次跑中 2 次红。
    只放过 ENOENT 一种(文件没了),其它读错误照抛 —— 一律 try/catch 吞掉的话,
    权限错、编码错会被当成「无违例」。
    
    ## 存量清理
    
    删掉生产库里 created_by LIKE 'zz-tenant-%' 的 22 goal + 22 task
    (备份 /tmp/graph.sqlite.bak-c3a5d46)。零 run 引用它们。
    其余 9 条 goal 是真实数据,一条没动。
    
    全量:136 文件 / 1276 测试,连跑 8 次全绿(改之前 8 次里红 2 次)。
    跑完后生产库行数零增量。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 主体由另一个会话完成(其会话已关闭,由我接手核对并提交),我补了两处修复
    与一条守卫。
    
    ## 它修的是什么
    六处「服务端自己给某个 agent 发一条 chat」各自写死
    `hostname:'localhost', port: systemConfig.port`(3010)。单测把路由挂到
    `app.listen(0)` 的临时端口上,回调却照样打进**线上 3010** —— 每跑一次汇报
    用例就给真 CEO 发一条 chat、起一个常驻 claude CLI 子进程、留一个
    callback-T-* 会话目录。症状完全静默:测试全绿,只有进程数和内存在涨。
    
    (这条是被我在批 5 新加的 task-dispatch-store 用例触发出来的:那之前没有
    任何测试会走到汇报回调。)
    
    抽成 selfChat:端口从 req.socket 取(收到这条请求的那个监听端口,天然就是
    「我自己」),测试环境整体短路。
    
    ## 我接手时发现并修掉的两处
    `task/transition` 的两个调用点删掉了 `const chatReq` / `const postData` 的
    声明,**却留下了 `chatReq.write(postData); chatReq.end();` 两行**。
    `node --check` 照样通过 —— 那是运行时 ReferenceError 不是语法错;而它在
    `setImmediate` 里抛就是未捕获异常,**直接杀进程**。
    
    真起服务打了一遍 POST /api/agent/ceo/task/transition:200,服务仍在。
    修之前这条路必炸。
    
    ## 守卫:server/tests/unit/self-chat-callbacks.test.js
    1. chatReq / postData 只许出现在 selfChat 函数体内 —— 「删了声明留下调用」
       这类改造残留,语法检查和单测都抓不到,只有真跑到那条路径才炸;
    2. 不许再有第二处自己拼 http.request 打 systemConfig.port(分叉的开始);
    3. 测试环境必须短路。
    
    第一版守卫把 selfChat 上方那段**注释**里引用的写法当成漏改报了红 ——
    那是文档不是代码,已跳过注释行。
    
    全量:135 文件 / 1272 测试全绿。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • ## ① system/tasklog/ 改名保留(§十二.5)
    派单存储统一进 tasks 表后,旧目录改名为 system/tasklog.bak-unify/ 而不是
    直接删 —— 跑顺一个版本再清。回填脚本改成**两处都认**(先原位置再备份位置,
    顺序不能反:万一有人从备份恢复出一份新的 tasklog,那份才是要回填的)。
    
    ## ② agent 提示词里的端口 3000 → 3010
    packages/agent-core/base/ 下那几份是**所有 agent 的工具说明**,印着
    curl http://localhost:3000/... 而实际端口是 3010。agent 照着打拿到的是
    连接被拒、不是「端口错了」,它多半会去猜别的原因。
    
    这不是假想:批 5 回填出来的历史派单正文里,有人手写了「(端口 3010,
    不是 3000)」在纠正它 —— 靠人肉在每条任务书里打补丁。
    
    守卫 agent-docs-port.test.js:扫 base 模板与 agent 本体,禁 localhost:3000。
    判据刻意宽松(不校验「必须等于 config.port」—— 端口可被 PORT 覆盖,写死会在
    部署环境误报)。带「样本非空」前置断言。第一版扫出 5 条误报,全是某 agent
    目录下 Python venv 里 playwright 的 ThirdPartyNotices —— 那是第三方文件不是
    提示词,已把 venv/site-packages/workspace/conversations/train 一并跳过。
    
    ## ③ 边界守卫不再把并行会话的改动算到 agent 头上(§15.5 第 1 条)
    守卫的前提是「窗口内新增的仓库改动 = 这个节点写的」。多会话共用 worktree 时
    这个前提是假的 —— git status 的差集里没有「谁写的」这一维。实测已发生:
    批 3 的开发改动把一条 run 判成 boundary_escape 卡住。
    
    可检测的信号:节点**开跑那一刻**仓库就已经有源码目录的未提交改动 → 有人正在
    这个 worktree 上开发 → 窗口内新增的源码改动无法归属 → **降级为只记证据、
    不判罚**(与既有的 unattributable 桶同一语义),而不是关掉守卫:证据照留,
    人一眼看得出「这次没判罚,因为当时仓库是脏的」。
    
    BOUNDARY_GUARD=strict 强制判罚(CI / 干净环境),=record 一律只记。
    守卫 8 例,含关键对照:仓库脏时不判、干净时同样一处越界仍判 fail ——
    守卫没被削弱,只是不再乱扣帽子。
    
    ## ④ TaskTimeline 的标签不再撒谎
    conversationCount 在批 5 之后只会是 0/1(「有没有起过执行会话」),而界面
    写的是 "Convs: N" —— 读的人会以为这条任务只有一次对话。改成有/无的表达。
    agents 的语义变化在注释里说明。
    
    注:innovator-hunt/todo.md 与 reddit-scraper/todo.md 里的端口也改了,
    但它们是 gitignored 的运行态文件,不入库。
    
    全量:134 文件 / 1268 测试全绿,web:build 通过。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 记三件这一批换来的东西:
    
    §16.5 第 3 步怎么做到不碰契约:dispatch 的九步副作用里只有第 ⑧ 步是存储,
    把 saveTaskMeta/loadTaskMeta 变成表的适配器、其余八步一行不动,URL 与请求体
    形态天然不可能变。字段落点的三个判断也记了(objectiveId 可推导所以不存、
    sourceConvId 零读取方所以删、没选 JSON blob 因为 R-19.2 否过那个形态)。
    
    §16.6 删掉的是「猜」不是「记录」:122 行的三策略启发式换成 parent_task_id
    显式记录;getTaskTimeline 保留,树从表来、正文仍读 agent 的一手会话记录。
    
    §16.7 两次「测试全绿 ≠ 没坏」:routes/tasks.js 漏改(没有测试走那条路)、
    控制台栏宽(构建/单测/探针都不会报)。与 §16.2 那个「存量库一启动就炸」
    (测试能覆盖但没人写)合起来是一条经验 —— 新建库的路径、纯逻辑的路径由单测
    天然覆盖;「存量数据」「真实接口」「肉眼可见」这三类必须另外安排人走一遍。
    
    §16.8 留了三条:tasklog 目录改名待做、tools-base.md 的端口仍是 3000(实际
    3010,已真咬过人)、TaskTimeline.vue 的两处字段语义变了。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • ## 第 6 步:调度器只管图任务(一行判据,但漏了是静默的)
    listQueue / deriveQueueBlocks / sweep 三处都加 executor='graph'。
    派单任务虽然也进 tasks 表,但它的执行载体是「直接起一次 agent 对话」,
    由 /task/dispatch 自己触发。漏这一条的后果:调度器拿一条根本没有 run 行的
    task 去出队 → syncRunNodesToDef 判空 → abandonRun → **把一条正在被 agent
    执行的派单收成 cancelled**。守卫用例把它摆成「最像可出队」的样子
    (queued + auto + 无依赖),只有 executor 这一维拦得住它。
    
    ## 第 5 步:GET /api/tasks 改读表,删掉 122 行的会话反推
    原来的 getAggregatedTasks 是启发式:扫全部 agent 的 conversations,按会话 id
    里的 T-xxx 归组,再用三条策略(meta 的 parentTaskId / from agent 的活跃会话
    时间包含 / master conv 的标题相似)**猜**父子关系。猜出来的树没法验证对错,
    而且每次请求都要遍历所有 agent 的会话目录 —— route 那 10 秒缓存就是为它加的。
    
    现在父子关系是 tasks.parent_task_id,派单时就显式写下来了,不用猜。
    task-service.js 391 行 → 236 行;_inferParentChild / _loadTaskMetas /
    TASK_META_DIR 一并删除(改完就是零调用方)。
    
    **getTaskTimeline 保留**,但来源拆清楚:任务树从表来(parent_task_id,
    可验证),时间线正文仍从 agent 的 conversations/ 扫 —— 那是一手记录,
    没有替代品。删掉的只是「从会话反推任务树」那一半,不是会话本身。
    
    两处口径变化(写在代码注释里):
    - agents:原来是「会话里出现过的 agent 集合」,现在是「谁派的 + 派给谁」。
      对派单任务这两者本来就是同一件事
    - conversationCount:原来是扫出来的文件数,现在是「有没有起过执行会话」(0/1)。
      为一个列表字段遍历所有 agent 的会话目录,正是这次要去掉的那笔开销;
      真要看内容走 /api/tasks/:taskId/timeline,那条仍读一手记录
    
    ## 漏改一个消费方,真打接口才发现
    routes/tasks.js 自己还在算 t.conversations.length,而 conversations 是会话
    反推那套的产物、已随之删除 → 500「Cannot read properties of undefined」。
    测试没覆盖这条路(它只在真跑接口时才走到)。已改成用 t.conversationCount。
    
    真打验证:GET /api/tasks 返回 17 条、12 个字段齐全 —— 条数比改前多,
    因为测试跑出来的派单现在也进表了,这本身就是「存储真的换了」的证据。
    
    全量:132 文件 / 1257 测试全绿。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • **本文件除了 saveTaskMeta / loadTaskMeta 的内脏和两处扫目录,其余一行没动。**
    /task/dispatch 与 /task/report 的 URL、请求体、响应体形态全部原样 —— 它们
    印在 packages/agent-core/base/common/tools-base.md 里,是所有 agent 的工具
    说明,改一个字等于同时改所有 agent 的提示词。
    
    ## 换掉文件存储的三个理由
    - 去重要「readdirSync 整个 tasklog + 逐个 JSON.parse」,任务一多就是 O(n) 次
      磁盘读,且没有索引可用
    - 派单任务在库里看不见 —— 控制台任务列表读 tasks 表,派单那套是平行世界
    - 同一个 T- 前缀两套 id 体系,读日志时分不清
    
    ## 落点
    - saveTaskMeta / loadTaskMeta 变成 tasks 表的适配器(upsert + 反向映射),
      meta 的形状一个字段不少:调用方按字段名取值,形状变了就是契约变了
    - 去重与父任务推断改成带索引的 SQL(idx_tasks_assignee),**判据一个字没改**,
      换的只是从哪儿把候选捞出来。查不动时留声不静默 —— 静默跳过等于去重形同虚设
    - 加 summary / execution_conv_id 两列(用户选的方案 A)
    - sourceConvId **删掉**:全仓零读取方,正是本仓库反复立案的「实现在、零调用方」,
      不往新表里再搬一份
    - objectiveId 不单独存:它恒等于 okr node id 的首段(OkrService.parseNodeId 与
      reportRunOutcome 同一口径),且两处读取方都是 `if (objId && meta.nodeId)`
    
    ## 真打过一遍,不只是测试绿
    照 tools-base.md 印的请求体原样 POST:200 + 老形态响应;再打一次 → 409 且
    existingTaskId 正确;落库 executor='agent' / assignee=qa-judge / status=running,
    自动建的 goal 标题「来自 ceo 的派单」、okr_node_id 落对;/task/report 之后
    status→completed、summary 与 finished_at 都在;loadTaskMeta 往返出来的形状与
    老 JSON 逐字一致,objectiveId 从 nodeId 首段推出 O-001;system/tasklog/ 不再
    产生新文件(仍是回填前那 4 个)。
    
    守卫:server/tests/unit/task-dispatch-store.test.js(4 例,盯的是「形状没变」)
    
    全量:132 文件 / 1255 测试全绿。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 记两条这一批换来的东西:
    
    1. 加列时把 CREATE INDEX 写进建表块,会让**所有存量库一启动就炸** ——
       建表块先于迁移块执行,存量库上 CREATE TABLE IF NOT EXISTS 是 no-op,
       索引引用的那一列还不存在。而新建库(单测默认路径)完全正常。
       由此得到一条可复用的规矩:凡是加列,必须同时测「存量库重启」那条路 ——
       新建库的路径由每个单测天然覆盖,存量库那条没人走,而生产上只有存量库。
    
    2. 回填沿用原 taskId 而不是新发:conversations/task-{taskId} 是目录名、
       汇报链路按它查找、summary 里互相引用。换 id 等于把引用全断掉,
       而回填的目的恰恰是别让历史消失。
    
    并记下第 3 步动手前的前提:/task/dispatch 的 URL 与请求体印在
    tools-base.md 里(所有 agent 的工具说明),一个字不能改;而 agent-chat.js
    在 8-19 被另一个会话大改过,预研写在那之前,所以要重新读现状再动手。
    
    顺带记一处过期且已咬过人的:tools-base.md 印的是 localhost:3000,
    实际端口是 3010 —— 回填出来的派单正文里有人手写「端口 3010,不是 3000」
    在纠正它。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 两类回填合在同一个脚本里(§十二.5 的要求):同一次数据形态变更分两个
    脚本,只会让「跑了哪个没跑哪个」变成新问题。
    
    **沿用原 taskId,不新发一个** —— 这是这一步最要紧的判断。conversations
    目录名是 task-{taskId}、汇报链路按 taskId 找回这条派单、summary 里互相
    引用。换 id 等于把这些引用全断掉,而回填的目的恰恰是「别让历史消失」。
    
    落点:
    - executor='agent'、assignee_agent_id ← dispatch 的 to、parent_task_id ← 派单链
    - company_id 落 _global:tasklog 的行没有 company 这一维(from/to 都是本体 id),
      全局 agent 的会话本来就落在这个保留 id 下(28 §5.1)。不去猜某条派单
      「其实属于哪家公司」—— 猜错会让它出现在错误的公司列表里,比都归 _global 更难发现
    - 每条派单自带一个 goal:tasks.goal_id 是 NOT NULL 真外键,而派单没有「人提的需求」
      那一层。与 §六.3 给 POST /:cid/tasks 设计的兼容路径同一条,不给 goal_id 开可空口子
    - 标题把 <br> 还原成换行再取首行,否则整条正文糊成一长条
    - dispatched → running(派出去了还没回话,不是终态)
    
    实跑:4 条派单全部回填成功、零失败;再跑一次全部跳过、零新增。
    回填后 executor='agent' 的行指向 hunter / digester / master / agent-creator,
    原 taskId 一个没变。
    
    守卫:server/tests/unit/tasklog-backfill.test.js(4 例)
    
    顺带修掉自己踩的一个:新测试用 Date.now() 拼临时库路径,撞了
    test-id-stability 那条守卫 —— 和另一个会话昨天踩的是同一个。改用
    fs.mkdtempSync(仓库既有写法,graph-engine.test.js 的 runsDir 就是它)。
    
    全量:131 文件 / 1251 测试全绿。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »

  • agent 之间派单(POST /api/agent/task/dispatch)与「跑一张 org graph」是
    两种执行载体,硬说成一回事是骗自己:前者直接起一次 agent 对话,后者走
    调度器出队 + 图执行。所以显式建模而不是让调度器去猜 ——
    executor(graph|agent)、assignee_agent_id、parent_task_id(派单链,
    与 depends_on 的「同级先后」不是一回事)。
    
    迁移的落点对:旧行 executor 缺省 'graph'(批 1-4 建的全是图任务),
    另两列 NULL。
    
    ## 加这一步时踩到并修掉的一个会让存量库开不了的 bug
    
    第一版把 CREATE INDEX ... ON tasks(assignee_agent_id) 写进了建表块。
    建表块整体先于迁移块执行,而存量库上 CREATE TABLE IF NOT EXISTS tasks
    是 no-op —— 表还是批 4 的形态、没有那一列,于是这句索引直接抛
    「no such column」,getDb() 起不来 = **所有存量库一启动就炸**。
    而新建库(单测默认走的那条路)完全正常,只测新建库根本看不见这个洞。
    已把索引挪进迁移块、排在 ALTER 之后。
    
    守卫:server/tests/unit/tasks-executor-migration.test.js —— 真造一个
    批 4 形态的库(删索引 + 删三列)再重开,断言三列与索引都补回、
    executor 默认值是 'graph'。
    
    另记一处自己踩的:SQL 模板字符串里的注释用了反引号,把 db.exec 的模板
    提前闭合了。DDL 注释里一律不用反引号,已写进那段注释。
    
    已知不对称:ALTER TABLE ADD COLUMN 加不了 CHECK 也加不了外键,所以存量库
    上 executor 的取值域只由应用层守,与新建库的 CHECK 不同源。写在注释里,
    免得下一个人以为库层拦得住。
    
    全量:129 文件 / 1245 测试全绿(+2 新守卫)。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 真在浏览器上看了才发现的:320px 减去 padding 只剩 292px 可用,而两个节点
    加一条边就要 380px+ —— 实际效果是节点名被从中间切断(`org-cod…`)、必须
    横向滚才看得全。而它是**全局导航**,是这个页面上最不该被压的一栏。
    
    同一时刻中间那栏(work graph)拿着 600px 却经常是空的:选中的 task 只要
    还没跑过 work graph,那栏就是一句空态提示。
    
    改成 `280px minmax(380px, 0.9fr) minmax(0, 1.1fr) 360px`:org graph 保底
    380px,两张画布按 0.9 : 1.1 分剩余空间,屏幕越宽一起长。work graph 仍拿
    多数 —— 它才是下钻之后的主画布。rail 态(≤1400px 最左栏收成图标条)同步。
    
    改后实测:org-planner → org-coder 两个节点完整显示,边上的 prd 标签与回退
    边的 0/3 熔断徽标都看得见,控制条从两行挤成一行。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • sessions.size 在此之前没有任何人过问。一次心跳泄漏就攒到过 82 个进程 /
    17.5 GB(约 215MB/个),而批 4 刚把公司级并行放开(单公司上限 20)——
    机器上同时能起多少完全没有闸门,正是最容易失控的时刻。
    
    AGENT_MAX_SESSIONS,缺省 12,**调用时读不是模块加载时快照**(快照一次就
    意味着改配置要重启才生效,而且不报错 —— company-paths.js 顶上那条教训)。
    非法值(0/负数/非整数)回落缺省,不当成无限。
    
    满了怎么办:**回收最久没用的空闲会话,而不是直接拒绝**。session 是可复用的
    池化资源,杀掉一个空闲的只是让它下次冷启动几秒;拒绝会让一个本来能跑的任务
    因为资源原因失败 —— 那是把可恢复的代价换成不可恢复的。
    
    只有一个空闲的都没有(全部在途)时才抛 SessionPoolFullError。刻意抛而不是
    静默放行 —— 静默超出正是这个仓库栽过的那一类「超出去但不报错」。
    
    两个实现细节:
    - 腾位置发生在 createConnection **之前**:起完再判等于该拦的那个已经起来了
    - 空闲态用显式的 idleSince 时间戳记,不去戳 IdleTimer 的私有 _timer;
      getOrCreate 复用时清空它(在途 = 不可被挑走)
    - _evictForRoom 故意不 await reset():腾位置只需要把它从 Map 摘掉,
      子进程收尾可以后台走完;await 的话一次腾位要串行等一次进程退出
    
    守卫:server/tests/unit/session-pool-max.test.js(5 例,不起真进程,
    只验判据本身:挑最久空闲的、在途的不许动、全在途就抛、没满不动、非法配置回落)
    
    全量:129 文件 / 1245 测试全绿。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • ## 批 4:解除「一家公司同时只有一个 active root run」
    - DROP idx_graph_runs_one_active_root(建表块删掉 + 迁移块显式 DROP,
      CREATE ... IF NOT EXISTS 对存量库不生效)
    - max_parallel_tasks 从 company.json 调用时读(不是模块加载时快照,
      否则改配置要重启才生效且不报错);缺省 1、非法回落 1 并留声、硬上限 20
    - 准入判据「从活跃集外进入活跃集」按额度计数
    
    **生死线用 8 个真进程并发抢坑实测**(不是纸面论证):max=1/2/3/5 四组,
    每组恰好抢到 max 个、库里活跃数 = max,落选进程各吃 40 次 RunSlotBusyError,
    零 other 错误。唯一索引没了之后,准入事务是真守卫。
    
    ## 批 3 对抗复核的四条遗留
    - N1 🔴 cancelQueuedRun 的前提被批 3 自己的 requeueBlockedRootRun 改没了:
      queued 的 run 现在可以是跑过一半的。探针实测取消后子 run 残留
      paused/stop_requested=0 —— 那条 running 的子 run 继续调 agent 烧 token。
      改为无条件走 engine.stopRun
    - L1 ⚠️ 恢复路径对 manual 只兑现一半:注释写着「T1 永远回不来这个后果因此
      消失」,但调度器故意不碰 manual → 退回队列后没人接。定案:「manual 不自动
      出队」约束的是首次起跑的授权,已被人亲手起过的不算新授权。
      判据用 attempt>0 而不是 started_at —— 后者会被 requeue 清成 NULL,
      恰好在需要它的那一刻是空的(实测踩过)
    - N2 🟠「退回队列」对用户完全不可见:200 + ok:true + 按钮全灰 + 图消失 +
      一句「节点状态一律 pending」的假提示。加 notice 明说
    - N3 🟠 isSlotRaceError 把 InvalidTransitionError 当竞态吞掉,而探针证伪了
      那条注释:被推走时条件更新返回 0 不抛,真正抛它的只有「调用方声明了非法
      转换」。删掉该分支,让它吵起来
    
    四条里三条是同一个模式:注释承诺的前提被本轮自己的改动改没了,注释还留着。
    文档 §十三.3 / §十四.4 已就这个模式立过两次案,这是第三、四、五次。
    
    ## 文档
    §十五 实施记录,含 §15.4 一次流程事故:87b776c 由第三方会话在对抗复核完成
    前误提,提交说明的「1201 用例全绿」压在一份尚未通过复核的代码上。
    
    全量:128 文件 / 1240 测试全绿,web:build 通过。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 起因是「全量测试偶发挂一条」。查下来既不是回归、也不是那 45 个孤儿进程(它们 CPU 早归零、
    不再写任何东西),而是**还活着的心跳在测试扫描 system/agents/ 的同时往里建目录**:
    
        locator.taskDir() 的落点是 system/agents/<agent>/<companyId>/<taskId>,
        运行态目录落在本体目录里属既定设计;但心跳每 5 分钟建一个新的
        (cto/_global/ 已 263 个子项且在涨),于是扫描 system/agents/ 的守卫测试
        (no-dead-path-references 那一类)会撞上瞬时竞态:列到了目录名,进去时那一层还没写完。
    
    症状是偶发假失败、重跑就好 —— 这类最费人,因为第一反应总是去怀疑自己刚改的代码。
    
    改法:server/index.js 启动的那个 setTimeout 块里,恰好装着全部「会自己起 agent 的东西」
    (两处中断恢复 + 任务队列扫描 + 两路心跳),所以一条早退就够,不必在五个地方各加一个判断。
    
        DISABLE_BACKGROUND_AGENTS=1 bash start.sh
    
    实测:127 文件 / 1226 用例全绿,跑前跑后 cto/_global 子项数都是 263。
    
    **验证它生效不能只看日志里的 ⏸** —— 那只证明代码走到了早退。硬判据是目录数前后相等,
    也就是真的没有东西在写。文档 §七 把这条写成了可粘的三行。
    
    排掉的两个坑,都记在文档和 .env.example 里:
    
    1. `.env` 里不能给这个键赋空值。start.sh 是 `set -a; source .env` 加载的,.env 里只要
       存在同名键(哪怕空的)就会盖掉命令行传进来的 =1,而且不报错。所以 .env.example 里
       它只以注释形式出现 —— 一个「示例文件顺手写全」的习惯会直接让开关失效。
    
    2. 开关生效后 `[TaskQueue] 🚀 已启动` 仍会打印,那是另一个模块:services/task-queue.js
       (ArenaSK 时代 /api/claude-chat 的队列,routes/claude-chat.js 模块加载时起),
       和批 3 的 services/task-scheduler.js 同名不同物。它的心跳只扫内存里的 this.tasks,
       不碰 system/agents/,不是干扰源。名字撞车,容易误判成开关没生效。
    
    文档同步:
      - docs/done/claude-cli-session-leak.md §七 重写(原文写的是「得让测试隔离,或跑前停心跳」,
        现在后者已实现,改成开关的用法 + 硬判据 + 上面两个坑);原第 2 条降为 §七之二
      - CLAUDE.md 测试命令块加一行(这是找「怎么跑测试」的人真正会看的地方)
      - docs/README.md 索引行补上开关
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 泄漏被修过一次(310a2b2 / 3e718aa),修完照样漏。第二次排查发现原因不是修错了,而是:
    
        系统里有两个互相独立的会话池,上一次只修了其中一个;而被修的那个,还漏了一条分支。
    
        池 A  routes/agent-chat.js 的 sessions       ← /api/agent/:id/chat,含两路心跳
        池 B  src/core/session-pool.js 的 SessionPool ← AgentManager.callAgent,图执行 / DevLoop
    
    两者没有任何共享代码,回收逻辑各写各的。这个结构性事实此前没有任何一处记着,
    所以第二个人查到同一个症状时会重走一遍全程 —— 这份文档就是为了让第三个人不必再走。
    
    正文八节,除了两个洞各自的成因与修法(b0b2b62 / 2092a2e),有三处是排查方法而非结论:
    
    §三 按 cwd 第三层判断孤儿归属的对照表。两个池子都把 cwd 下沉到运行态目录,
         `_global` / `_no-task` 是 agent-locator 的保留缺省值、两边都会出现,不能靠它区分。
    
    §四 为什么上一版守卫是瞎的。dropSession 和 closeSession 都会让会话从池子里消失,
         黑盒探针两种情况都返回 false —— 差别只在子进程死没死,而那是探针看不见的维度。
         选探针要问的不是「能不能测出正确行为」,而是「能不能区分正确行为和这个具体的错误行为」。
    
    §八 三步定位脚本,外加一条:先确认跑着的服务器进程是什么时候启动的,再去看代码。
         本次排查中,正在跑的实例比修复 commit 早 18 秒启动,代码里有的修复它并没有。
    
    线上验证(PID 63099,13 分钟窗口两轮心跳):触发 5 / 完成 5 / 回收 5,峰值子进程 3、稳态 0。
    17:13:32 那个 3 正是泄漏时代的签名(cto+coo+qa-judge 同秒触发),清理前那批孤儿一个都不回收,
    现在 30 秒内全部释放。窗口内没有失败心跳,所以 2092a2e 那条 catch 路径是靠单测的变异检验
    保证的、不是靠线上观察 —— 文档 §六 里写明了,没算成已验证。
    
    docs/README.md 同步加索引条目(no-dead-path-references 守卫 4 条全绿)。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • lemonboss-78 已补齐 §二 的 4 条(均带探针实跑证据),§七 第一条待办随之过期。
    把它勾掉,并把 N1/L1/N2/N3 各自拆成一条待办,附优先级判断:
    N1(取消排队留下还在烧 token 的孤儿子 run)是唯一有直接金钱成本的一条。
    
    顺带提一条给 lemonboss-78 判断的提议:「注释承诺的前提被本轮自己的改动改没了、
    注释还留着」这个模式已经第三次立案(§十三.3 / §十四.4 / 本次),
    第三次或许不该再只当个案记。提议不代表已立案。
    
    只提交本文件;docs/README.md 与 docs/done/claude-cli-session-leak.md
    是 lemonboss-89 的在途工作,未动。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • lemonboss-68 写交接文档时只确知 1 条,其余 3 条刻意留「待补」而不是
    凭二手转述硬写 —— 那个判断是对的,记错了反倒会变成新的事实来源。
    这里由批 3 作者按 17:00 那次对抗复核的原文补齐,每条都有探针实跑证据。
    
    四条:
    - N1 🔴 取消排队留下还在烧 token 的孤儿子 run(唯一有直接金钱成本的)
    - L1 ⚠️ 恢复路径对 manual 不成立(调度器故意不碰 manual,于是坑空着没人接)
    - N2 🟠「退回队列」对用户完全不可见(200 + 按钮全灰 + 图消失 + 一句假提示)
    - N3 🟠 isSlotRaceError 的判据注释被探针证伪,且吞真错误不打日志
    
    其中 3 条是同一个模式:注释承诺的前提被本轮自己的改动改没了,注释还留着。
    27-P3C §十三.3 / §十四.4 已就这个模式立过两次案,这是第三次。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 87b776c 把 lemonboss-78 还在等对抗复核的批 3 提进了 main(提交者是我,未经作者同意),
    复核随后给出 4 条问题。这份文档记下:时间线、main 上各笔提交的归属与状态、
    那笔提交说明里「1201 用例全绿」的更正、待决策项,以及这次踩实的多会话协作规则。
    
    顺带把最初那条卡住的 run(T-msyu6u9b-939c,boundary_escape 误伤)的诊断与
    界面重跑步骤一并留档 —— 它至今没动,等工作区收敛干净才能跑。
    
    只提交本文件,其余 13 个在途文件原样留在工作区未动。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 310a2b2 / 3e718aa 把「正常跑完的对话」补进了回收路径,但 catch 分支没跟上:
    
        } catch (err) {
            dropSession(key);      // 只摘出池子,不杀子进程
        } finally {
            if (kind?.[2]) closeSession(key, ...);   // 池子里已经没了,返回 false,空转
            else armSessionIdle(key);
        }
    
    dropSession 的语义是「子进程已经自己死了」,可这里子进程活得好好的 —— 错的是它背后的
    API 调用(529 / DNS / 证书),CLI 一个都不会自己退。于是会话先被摘出池子,再也没人
    杀得掉它:一个既没有引用、也没有回收路径的孤儿进程。
    
    实测:一个跑了 23 小时的实例攒下 45 个孤儿 / 9.6 GB RSS,其中 38 个是心跳。抽查泄漏的
    convId,status **无一例外**是 failed;对照 cto 最近 60 次心跳是 55 completed / 5 failed
    —— 8% 的失败率,一次漏一个,数字对得上。成功的心跳一个都没漏,说明 310a2b2 本身是对的。
    
    改法:catch 里 dropSession → closeSession。可观测行为完全不变(两者都把会话摘出池子、
    都让下一轮冷启动),只是多杀一个本该死的进程。
    
    守卫:agent-chat-session-reclaim.test.js 新增一条。这里必须说清楚为什么破例去 spy
    disconnect —— **旧探针对这个 bug 是瞎的**:dropSession 和 closeSession 都会让会话从
    池子里消失,`/chat/:convId/stop` 两种情况都返回 success:false,差别只在子进程死没死。
    上一版修复正是因此漏过了这条路径。用 vi.spyOn 让 sendPrompt reject 来复现「CLI 活着、
    API 挂了」,再断言 disconnect 被调过。做过变异检验:退回 dropSession 后该用例转红。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • SessionPool 此前只有三条回收路径:换新会话 / reset() / killActive()。「正常跑完」和
    「不可重试的报错」都不在其中,而 getKey 里带了 taskId —— 每个图节点一个新 key,于是
    一节点一进程、永不释放,且**不报错**。实测 23 小时漏出 7 个图执行进程。
    
    修法与 agent-chat 侧同一套:调用开始时停表(getOrCreate 里 cancel),调用结束时上弦
    (agent-manager 两个出口调 armIdle),计时器只在没有在途请求的间隙走,所以再长的单次
    生成也不会被 TTL 打断。旋钮共用 AGENT_SESSION_IDLE_MS(默认 30 分钟)。
    
    守卫:server/tests/unit/session-pool-idle-reclaim.test.js
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 队列与调度(§五.1):
    - 下达任务不再因「公司已有 active root run」吃 409,改为建 queued run + queued task
      排队;出队走 updateRunStatus 的 CAS + BEGIN IMMEDIATE 准入事务,触发挂在
      transitionRun 这个唯一漏斗上,60s sweep 兜底。
    - 依赖已不可能满足的排队任务就地推导成 blocked,且每次 tick 重算不落死,上游重跑
      成功后能原路解开。
    
    恢复路径(批 3 复核 F1/F2):三条恢复转换的 from 全在活跃集之外,撞坑满时退回队列而不是
    永远躺在 blocked;退不回去的裸 run 干净拒绝,不留半截状态。
    
    控制台三处可见性修复 —— 症状全是纯视觉、不报错、git status 干净:
    1. 顶栏墙钟恒 `now - started_at`,没有终点。一条昨晚就 blocked 的 run 第二天还在涨到
       「16h 58min」,而那是页面上唯一的时间信号 —— 「已经停了等人处置」被显示成了
       「还在跑再等等」,这两件事需要的动作相反。终点改取 run.updated_at,并标「已停」。
    2. 越界日志只报「留下 N 处改动」,文件清单躺在 timeline.ndjson 里翻不到。判罚就是按
       这份清单下的,而人要判断「这真是 agent 写的吗」唯一的依据就是路径(并行的开发会话
       改宿主仓库会被算到当时在跑的节点头上,那是误判,看一眼就知道)。改为列出 entries。
    3. run 收敛成 blocked 时只把 task 写成 blocked、不写原因,任务列表那一行只剩一个橙色
       的「挂起」,必须逐层点进 org 图 → work 图 → 节点详情才知道发生了什么。改为把
       「卡在哪个节点 + 什么 fail_reason + 判词摘要」写进 tasks.blocked_reason。
       原因绝不带 UPSTREAM_BLOCK_PREFIX —— 带了会被调度器当成依赖推导出的 blocked 自动
       推回 queued,而这类 blocked 本来就等着人处置(守卫在 task-blocked-reason.test.js)。
    
    测试:126 文件 / 1201 用例全绿。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »

  • goal 从「人写一句话」变成「agent 拆成一张 task DAG、人确认后下达」,
    收敛时由 org-validator 在 goal 层再验一次。
    
    ## 交付
    - task-planning 本体(毕业判据实测通过:agent.md + soul.md 一次写完、
      不带 _base 哨兵、路径按 cwd 深度 3 实算核对)
    - 拆分链路:3 轮 errors 原样回喂 → 不过就降级成 1 条同描述 task,
      goal 绝不卡在 planning;token 记 goals.planning_tokens
    - goal 级 org-validator 验收:≥2 条 task 全 completed 才调,单 task 跳过,
      有 failed 直接 failed 不调(省一次真花钱的调用)
    - 控制台四栏 + GoalTaskPane 两级树 + 两步表单(拆分中给逃生口)
    - 中断恢复:启动时补跑验收与汇报
    
    ## 三条跨进程竞态,全部用真探针实跑复现后才判定修好
    1. runGoalSplit 无条件把 goal 写回 draft,抹掉逃生口刚下达的 active ——
       而触发它的正是表单自己推荐的那个逃生口。后果是一个正在烧钱的 goal
       显示成「草稿」,且从此永不收敛(OKR 不写、上级不唤醒、无日志)
    2. CAS 把最终态提前写进库,验收窗口内重启即丢,且 verdict=NULL 与
       「单 task 合法跳过」形态相同,查不出来 → 占位判词 + 启动重推
    3. 认领语句 SET verdict=RESUMING WHERE verdict IN (PENDING, RESUMING)
       —— 写进去的值就在 WHERE 里,changes 恒为 1。而函数注释白纸黑字
       写着「认领写的是另一个占位值」。注释是对的,代码没兑现。
       实测:org-validator 调 2 次 / OKR 写 2 条 / token 双记
       → claim 带唯一 token + 时间戳租约,回写条件更新决胜,主路径同闸门
    
    ## 与设计正文的偏离(已记进文档 §十四)
    拆分后 goal 落 draft 而非 active(active 的语义已被 markGoalDispatched
    钉死为「已下达」);mode 三级继承对显式 tasks[] 同样生效;占位判词不出
    API;新增 planning_error / reported_at 两列,后者迁移带回填(否则升级后
    第一次启动会把历史终态 goal 全部重报一遍)。
    
    ## 过程
    四轮 26 个 agent,复核报出 22 条缺陷,其中 6 条是修复自己引入的。
    第四轮修完挂了 3 条测试,全是测试在钉住缺陷本身 —— 其中一条的注释
    写着「模拟进程死后重启」,构造的却是新鲜占位值:注释描述 A 场景、
    代码构造 B 场景、断言按 A 场景写。
    
    全量:118 文件 / 1112 测试全绿,web:build 通过。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 手工扫了两轮,扫出的是三类会持续复发的失真,不是零星笔误:
      ① package.json 里 12 处引用指向从未在 git 历史存在过的文件(server/src/cli/auto-dev.js 一族)
      ② CLAUDE.md 的 docs/ 那行列了 5 个文件名,文档移进 done/ 之后全成死链
      ③ 数据存储整节把两个不同的 data 根当成一个写,server/conversations/ 这个路径根本不存在
    
    共性是**不报错、不影响测试、git status 干净**,只有照着做的人(或 agent)撞墙才发现。
    而 CLAUDE.md 是每个会话都会加载进上下文的文件 —— 写错等于把错误信息直接灌给每一个后来者。
    
    四条扫描:
      - CLAUDE.md 里的仓库路径都存在
      - CLAUDE.md 里出现的每条 npm run 都在 package.json 里(守反方向:删脚本忘改文档)
      - package.json 的 scripts / bin / main 指向的文件都存在
      - docs/README.md 的相对链接都指向真实文件
    
    判据刻意窄(本仓库有过「防线被整条注释掉」的教训):路径只认「以仓库顶层目录开头 + 含 /」,
    且跳过 gitignored 的 —— 那是运行态产物(web/dist/、data/companies/、skills/),在干净机器上
    本来就不存在,缺席不是文档错误;这与 no-stale-templates-path.test.js 用的是同一条判据。
    上下文里的相对片段(_base/、todo/future/)、glob、<repo>/… 示意写法一律不判。
    
    四条各带「样本非空」下限断言(当前 26 路径 / 16 script 名 / 6 入口引用 / 22 链接),
    下限取当前值一半左右:正则一写错就会以「零违例」全绿,那和没有这条测试是一回事。
    
    验证:把同一套判据跑在旧 revision 的文件内容上(只读 git,不碰工作区)——
      3e718aa:路径 ❌3 条、入口 ❌10 条
      d6214f8:路径 ❌4 条、入口 ❌10 条
      HEAD   :全绿
    顺带修掉自己写的一处假路径占位符(`data/xxx` → 「以 `data/` 开头的路径」),
    文档里本来就不该出现假路径。
    
    28-followups 守卫清单补到第六条,并写明后两条与前四条的形状差异。
    npm run test:unit 1061/1061 绿。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 顺着上一笔的判据(引用了不存在的东西)继续扫,扫出的不是零星笔误,是一处结构性失真。
    
    事实(按绝对路径解析源码里的 path.resolve(__dirname, ...),不是数字面量 ——
    数字面量会错,path-resolver.js 比 db 文件深一层,同样的 '../../../data' 落点不同):
    
      仓库内 <repo>/data/    — 11 个模块(company / okr / graph / devloop / schedule / pipeline …)
      仓库外 <repo>/../data/ — 3 个模块(skill-db、user-db、benchmark-db,ArenaSK 时代遗留)
    
    原文把六条全挂在扁平的 `data/` 前缀下,读的人只会理解成仓库内一个根。这不是抠字眼:
    备份 / 迁移 / 换机器时漏掉仓库外那半边,skill、用户、benchmark 数据会**静默丢失**。
    
    一并修掉的具体错误:
      - `data/benchmark.sqlite` → 实际文件名是 `benchmark_logs.sqlite`
      - `server/conversations/` → 该路径不存在。会话持久化实际有**两套**:
        ArenaSK/Innovator 侧在 `<repo>/conversations/`(config.conversationsDir),
        LemonBoss agent-chat 侧在公司态目录下(getConvDir → locator.companyStateDir)
      - 目录速查里的 `data/skills/` 改成 `<repo>/../data/skills/`,点明 `<repo>/data/skills/` 并不存在
      - 补上仓库内侧原本一条没列的 graph.sqlite / companies/ / okr/ / logs/ / agents/
      - 每个库补上真实的 env 覆盖名(SKILLS_DB_PATH / USERS_DB_PATH / BENCHMARK_DB_PATH / GRAPH_DB_PATH)
    
    新写进去的每一条路径与每一个 env 名都用脚本核过存在性(10 路径 + 5 env 全 ✅)。
    docs/README.md 的 markdown 链接同批扫过,零死链。
    npm run test:unit 1057/1057 绿。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 判据统一:目标文件既不在盘上、**也从未在 git 历史里出现过**(逐个 git log --all 核过),
    不是「搬了地方」而是从来没有过。跑起来一律 MODULE_NOT_FOUND。
    
    删除(12 处,指向 4 个从未存在的目录):
      server/src/cli/       main、bin.auto-dev、example、example-bmi
      server/src/utils/     logs、analyze、parse
      server/src/modules/   test:browser-logger、test:design-validator
      server/src/tests/scripts/  test:multi-cli
      skills/whatsapp-bot/  whatsapp:auth、whatsapp:start
                            (skills/ 是 gitignored 的运行时目录,确实存在,
                              但里面没有也从没有过 whatsapp-bot;
                              /api/whatsapp 路由走的是 routes/whatsapp-auth.js,不受影响)
    
    只授权了 auto-dev 那 3 处,其余 9 处是同一类缺陷、同一条判据,一并删了 —— 要退回来
    `git revert` 这一笔即可。删完 package.json 零死引用(脚本目标存在性已脚本化核过)。
    
    CLAUDE.md 同步:
      - 常用命令去掉已删除的 6 条,补上真实存在但之前没列的 graph:seed / okr:migrate
      - 上一笔加的 auto-dev 警告块随引用一起去掉
      - /api/goals 从「向后兼容别名」改为「已退役」—— 7f24ef0(27-P3C §六.1)已落地,
        OKR 只剩 /api/okr 一个挂载点,且有守卫测试盯着
    
    验证:npm run test:unit 1057/1057 绿;npm run spec:check 52 份 _spec 全同步;
    CLAUDE.md 里出现的每一条 npm run 都在 package.json 里存在(脚本核对)。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 新增 §十三,体例照 27-P3/P4 §十:设计正文不改写,只在这里与实现对账,
    冲突处以实施记录为准(正文是设计时的判断,本节是跑出来的事实)。
    
    记了与设计的四处偏离,其中两处是设计正文写错了:
    
    ① blocked 在 task/goal 两层都不是终态。正文 §四.3 把它与
       completed/failed 并列画成终态,实现时实跑复现出后果:预算耗尽后
       加预算恢复、跑完,两次条件更新静默命中 0 行,最终 run=completed
       而 task=blocked/goal=failed/OKR=failed 永久对不上。RUN_TRANSITIONS
       里 blocked: ['running','stopped'] —— 对 run 它是可恢复态,
       reviveParentFromChild 还会自动拉回。
    
    ② goal 级是唯一汇报出口,reportRunOutcome 已删除(零其它调用方)。
       1 goal:1 task 形态下 run 级与 goal 级会把上级就同一件事唤醒两次。
       from 的口径同时修正为实际交付者(terminalAgentId 搬进
       org-graph-service 当唯一持有者),不再用 created_by ——
       那是「谁下的单」,默认字面量 'human',下游会静默落空。
    
    ③ dispatch 挑第一条可跑的 draft,连带补上 PATCH 改 seq —— 原先 409
       的 hint 写着「改依赖或 seq」,而那个端点从不读 body.seq。
    
    正文里三处提到 reportRunOutcome 的地方一并标注:§四.7 加了「本节写于
    实施之前」的前置说明,§十二(批 5)的前提改成「函数换了、依赖关系
    没变」—— A 的回调链路仍长在 B 身上,只是出口从 company.js 的 run 级
    换成了 task-settle 的 goal 级。
    
    过程也记了:三轮编排 21 个 agent,对抗复核报 19 条,其中 4 条是
    第二轮修复自己引入的。结论:对抗复核不能只做一轮 —— 修复和实现
    一样会出错,而且更隐蔽,它长得像在解决问题。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 逐条都对着 HEAD 核过(不含工作区里其它会话未提交的改动):
    
    ① docs/ 那一行列的 5 个文件全是死链 —— 它们都搬进了 docs/done/。
       根因不是路径写错,是同一份文件清单被抄在两处:CLAUDE.md 一份、docs/README.md 一份,
       文档一搬家,抄的那份就烂。改成只描述「按状态分目录」的结构并指向 docs/README.md,
       并写明「别在本表里加文件名」,免得再抄一次。
    
    ② 路由清单漏项 + 错项:
       - index.js 实挂 /api/okr 与 /api/graph-runs,表里都没有
       - 表里写的 /api/goals 是 okr 的向后兼容别名,不是独立路由;标注清楚
       - agent-server.js 子集同样漏了 /api/okr、/api/graph-runs
       (注:工作区里有另一个会话正在退役 /api/goals 别名,尚未提交。本次按 HEAD 的事实写,
         等那笔落地后这两行还要再改一次。)
    
    ③ 三个硬编码计数全部过期:routes 15→实际 17,services 26→38,views 10→15,
       agents「75 个」连测试建本体都会让它变。计数在这种表里只会烂,直接去掉;
       views 补上 GraphConsole 与 DevLoop 四页。
    
    ④ auto-dev 是幽灵入口:server/src/cli/ 整个目录不存在,且 git 历史里从未有过
       auto-dev.js。npm run example / example-bmi / bin.auto-dev 三处引用全是坏的
       (实测 MODULE_NOT_FOUND)。CLAUDE.md 里改成显式警告 —— 没删 package.json 的
       scripts/bin,那是代码改动,留给人定「补文件还是删引用」。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 把「人类提的一个需求」与「机器执行的一件活」拆成两级。这不是新概念:
    27-NAMING §四 早就定义了 goal 1 : task N,graph_runs.goal_id / task_id
    两列从 P3 起就一直在写,却没有任何表接得住 —— 本次补上那两张缺席的表。
    
    ## 交付
    - goals / tasks 两张表 + CRUD + depends_on 环检测 + ID 生成器
      T-{base36}-{4hex}:原格式同毫秒必撞,而 tasks.id 是主键,
      「一个 goal 拆 N 条 task」是循环建行,撞了就是 500
    - goal / task 全套端点;POST /:cid/tasks 向后兼容(不带 goal_id 自动
      建 goal);退役 /api/goals 与 /:cid/goals 两个 OKR 别名(零调用方)
    - 终态回写 + goal 收敛 + OKR 写一次:钩子挂在唯一漏斗
      graph-run-sse.transitionRun 上,而不是枚举路径 —— stopRun 与
      「预算耗尽 → blocked」两条最常见的释放路径都不走 settleRun,
      枚举一次漏一次。goal 收敛用 CAS 闸门,并行完成的 N 个 task 只有
      一个赢家做不可重放的副作用(写 OKR / 唤醒上级)
    - blocked 在 task/goal 两层都不是终态:它是「挂起等人介入」,
      与 completed/failed/cancelled 不同类。否则预算耗尽后加预算恢复、
      跑完,两次条件更新静默命中 0 行,最终 run=completed 而
      task=blocked/goal=failed/OKR=failed 永久对不上(实跑复现过)
    - goal 级是唯一汇报出口:删掉 run 级 reportRunOutcome(零其它调用方),
      1 goal:1 task 形态下它会把上级 agent 就同一件事唤醒两次
    - dispatch 挑第一条可跑的 draft,而不是恒取 seq 最小那条 ——
      否则「第 1 条依赖第 2 条」这种合法输入会让 goal 永远下达不了
    - 路径缺陷:writeTaskGoalArtifact 不认 GRAPH_RUNS_DIR 而读侧认,
      设了 env 就写进 A 处、从 B 处读,describeTask 拿到 null 后静默退回
      兜底文案「执行图节点任务」;三处模块加载时快照改为调用时读
    - 存量回填脚本(幂等 + --dry-run):不做的话批 1 反而让历史任务
      更看不见 —— 正是本批要修的问题的反向放大
    
    ## 过程
    三轮多 agent 编排 + 每轮对抗复核,共报出 19 条缺陷(10 条首轮实现的、
    4 条第二轮修复引入的、5 条判定为非问题)。全部已修并有用例覆盖。
    新增测试 4 个文件,含不变式测试 runs-root-single-holder(凡是声明了
    GRAPH_RUNS_DIR 覆盖的模块,该覆盖必须真的被用到)。
    
    全量:116 文件 / 1057 测试全绿。
    
    ## 未做(属批 2-5)
    task-planning 与 goal 级 org-validator 验收(批 2)、调度器与
    run_status='queued'(批 3)、并行(批 4)、两套任务体系统一(批 5)。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 承接 310a2b2 的两个遗留:
    
    ① 四类被错标成心跳
       company-heartbeat-service._sendAgentChat 给五类调用(心跳巡检 / Pipeline 步骤 /
       日程 / 告警 / 日报)无差别打 isHeartbeat:true,于是对话列表里全都显示成
       「⏱️ 自主心跳巡检」、meta.type 一律 'heartbeat'。改成显式传 kind,五类各自标注。
       isHeartbeat 仍然发,但只对真心跳为真,老消费方行为不变。
    
    ② 310a2b2 让 Pipeline 重试变成了冷启动
       Pipeline 步骤的 convId 是 `pipeline-{runId}-step-{i}`,被驳回重做时**复用同一个**。
       310a2b2 按 isHeartbeat 一刀切回收,等于每次重试都要走 buildPrompt 场景 C 重注入历史。
       现在回收策略挂在 CALL_KINDS 表上,判据只有一条:**convId 会不会被下一轮复用** ——
       会复用的(pipeline / 人类对话)留着走 TTL,不复用的(心跳 / 日程 / 告警 / 日报)跑完即回收。
       不按「是不是后台调用」分:五类后台调用里 pipeline 就是要留的那一类。
    
    守卫补到 12 条(原 3 + kind 标注 5 + 回收策略 4)。对 310a2b2 跑过:8 红 4 绿。
    convId 一律用固定字面量而非拼时间戳 —— 撞上 test-id-stability 那道防线后按它的判据改的,
    固定之后第二次跑还顺带走了「复用已有对话」那条分支。
    
    文档:
      - CLAUDE.md 注意事项加一条(新增会话创建路径时必须同时定回收路径,附症状与守卫位置)
      - 28-followups 守卫清单补第五条,并把「前四条守单一持有者」与第五条「守资源生命周期」
        的形状差异写明;顺手修掉表格已有 4 行、结尾却仍写「这三条」的失配
      - .env.example 补 AGENT_SESSION_IDLE_MS
    
    全量单测 667/667 绿。重启后 /api/health 正常,心跳照常挂载。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • sessions 里存的是**常驻**的 claude CLI 子进程(stream-json 模式,不是一次性的
    `claude -p`),此前只有四条回收路径:协议 error / 子进程 disconnected / 手动删对话 /
    手动 stop —— 「正常跑完的对话」不在其中。
    
    而两路心跳都用一次性 convId 打进 /api/agent/:id/chat:
      - heartbeat-service:`heartbeat-${Date.now()}`,cto / coo 各 5 分钟一次
      - company-heartbeat-service:_sendAgentChat 的五个调用点(CEO 巡检 / Pipeline 步骤 /
        日程 / 告警 / 日报)convId 也全是一次性的,且无差别带 isHeartbeat:true
    
    于是每次心跳新起一个进程且永不释放。实测单次运行 2h43m 攒到 82 个进程 / 17.5 GB RSS。
    
    改法:
      - 心跳会话跑完立刻 closeSession(一次性 convId,会话复用对它零价值)
      - 普通对话上空闲 TTL(默认 30 分钟,AGENT_SESSION_IDLE_MS 可覆盖),复用 engine/idle-timer
      - 计时器只在「没有在途请求」的间隙走:getOrCreateProtocol 命中复用时 cancel,请求结束时
        重新上弦。所以再长的单次生成也不会被 TTL 打断,比在流式回调里 touch() 少一个调用点
      - 删对话 / stop 两处手抄的 disconnect+delete 收敛到 closeSession,否则它们不停表
    
    守卫:agent-chat-session-reclaim.test.js 用现成的 /chat/:convId/stop 当黑盒探针
    (查得到会话才返回 success:true),不为测试加导出口。对 HEAD 版本跑过:2 红 1 绿 ——
    两条回收断言修前失败,「普通对话保留会话」修前修后都绿,证明复用能力没被顺手砍掉。
    
    线上验证:重启后第一轮心跳 2 触发 / 2 回收,server 名下 claude 子进程数 0。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 27-P3C 批 1 沉淀的不变式测试,与前三条守的是同一件事:同一个事实
    只能有一处持有者。断言的是「凡是把 process.env.GRAPH_RUNS_DIR 绑到
    标识符上的模块,那个标识符必须在代码里真正被用到」。
    
    来历一并记下:goal-contract.js 里曾有一行 RUNS_ROOT,注释写着
    「与 graph-engine.RUNS_ROOT 是同一个变量(测试隔离在用)」,全文件
    却零引用。声明了意图、代码不兑现的常量比没有更容易骗人 —— 读的人
    会据此认为该模块认这个覆盖,然后写侧写进临时目录、读侧去仓库里找,
    两个方向都静默错位。
    
    四条共有的「样本非空」前置断言仍是重点:扫不到文件时这类测试会以
    「零违例」的形式全绿,那正是它自己变成零调用方的方式。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 主文档新增 §11.4b,记 dd319bb 那轮清掉的内容与「不清会怎样」:
    四份本体提示词、三个 skill js、两份图模板 README 的 CI 防线规则、
    DevLoop PRD 13 份 HTML(连带重生成的 52 份 _spec + manifest + search-index)。
    
    其中两条值得单独记:
    - innovator-hunt/collect_seed.js 是真 bug 且已发作过 —— 裸相对路径相对 cwd 解析,
      在 agent 自己目录下造出一棵野生嵌套树。
    - DevLoop PRD 写的 system/templates/devloop-pm.md 根和形状都错
      (实际是 system/agents/{agentName}/prompt.md),由此推出一条方法论:
      批量替换字符串时,替换的正确性取决于替换后的整体是否成立,
      而不是被替换的那一段是否过期。
    
    同时订正两处早于本次改造就已过期的内容(「6 个命名冲突 id」清单里四个 id
    根本不存在;orggraph-planning 被告知的真相源仍是 company.json.agents[]),
    并写明刻意不动的三类:历史说明 / 迁移完成的证明 / 运行态记录。
    
    followups 新增 §五,登记本轮立起来的三条不变式测试
    (agents-root-single-holder / no-stale-templates-path / repo-hygiene),
    写明它们共有的「样本非空」前置断言为什么不能省,以及改动它们前要想清楚的事。
    §四 那条「脚本路径需要跟着更新」已在 cd2066a 完成,改成实跑结论。
    
    交付状态从「未 commit、未 push」更新为已推送(ca468ab..dd319bb)。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • system/templates/ 已随 28 号改造整体并入 system/agents/,目录在磁盘和 git 里都已不存在。
    但引用还散在各处,其中几处是 agent 运行时真会照着读写的:
    
    本体提示词(读不到就静默降级,不报错)
      workgraph-planning/agent.md   system/templates/base/ ×3、模板库路径 ×1
      orggraph-planning/agent.md    同上 ×1
      orggraph-planning/tools.md    ×1
      org-innovator/agent.md        四条产出落点 system/templates/weibo-scraper/train/*
    
    skill 代码里的硬编码目标
      evolver-youtube-crawler 的 refactor.js / mock_setup.js / evolve.js 共 4 处
      innovator-hunt/collect_seed.js —— 这处是真 bug 且已发作过:
        写的是裸相对路径 'system/templates/…',相对 cwd 解析,于是在 agent 自己目录下
        造出一棵野生嵌套树 system/agents/innovator-hunt/system/templates/…
        现在改成由 __dirname 推导,不再依赖 cwd(cwd 已经是深度 3 的任务态目录)
    
    图模板契约(work-graph-templates / org-graph-templates 的 README)
      CI 防线的三条规则写的还是「在 system/templates/ 下必须有目录」「在
      system/templates/_registry.json 里必须有条目」,与实际执行的检查已经对不上
    
    DevLoop PRD(13 份 HTML + 重新生成的 52 份 _spec + build-manifest + search-index)
      写的是 system/templates/devloop-pm.md(扁平 .md)。根和形状都错:
      loop-controller.js:166 实际读的是 system/agents/{agentName}/prompt.md。
      按真实形状改,然后跑 spec:extract / extract-manifest / build-search-index 重新生成。
    
    顺带订正两处早于本次改造就已过期的内容:
    - 「避开 6 个命名冲突 id」这条约束列的 digester / evolver / hunter / innovator
      四个 id 根本不存在(早已改名),而它的理由(两个目录下各有一份、内容不同步)
      也已随合并消失。约束保留(元 agent 本来就不该进执行流),理由与清单按实际本体重写。
    - orggraph-planning/agent.md 里「真相源是 company.json.agents[]」——
      28 号起真相源是 company_agents 表,company.json.agents[] 仅作兼容保留。
    
    刻意不动的三类(改了是净损失):
      历史说明   server/ 下解释「原来指向 X、批次 1 并进了 Y」的注释、docs/done/、tasks/
      完成证明   server/tests/ 里断言该目录已不存在的守卫
      运行态记录 未跟踪的 workspace/ conversations/ 与 train/ 下的历次训练报告
    
    新增 server/tests/unit/no-stale-templates-path.test.js 守住活定义这一档:
    扫 system/ 与 fixtures/ 下被 git 跟踪的文件(跟踪=人工维护的定义,
    未跟踪=运行态产物),零例外。带「样本非空」前置断言防 git 失灵时退化成永远通过。
    改坏一处实测能精确点名到文件行号。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 并行会话指出 graph-engine.js 的 indexNodeOutputs 注释里早就写着
    「goal-contract 零调用、setPlanWorkGraph 零调用、三值透传只接了读
    那一半」。顺着查下去,这个模式在仓库里被记录过至少 7 处,而且证据
    本身就是结论:
    
    - graph-engine.js 跨层判据处:「(『实现在、零调用方』的第五次)」
    - graph-run-route-io.test.js 文件头:「这一路踩了十几次的
      『实现在、零调用方』换个方向再来一次」
    - graph-run.js:243:「等于又一次『实现在、零调用方』」
    - 27-goal-contract-format.md:101:「T0 交付后有四轮时间里,全仓库
      唯一提到 goal-contract.js 的是一句注释 —— 实现完整、零调用方、
      零单测,验收 1 从未被执行过」
    
    有人数到第五次,有人写「十几次」,然后它继续复发。这些记录证明的
    不是「有这个问题」,而是「发现了也没人回来收」。
    
    本文档的三处要求是同一个惯犯的三个实例:砍掉 goals.budget_json
    (§四.1)、report_to 必须落库(§四.7 禁令二)、处理 goal-contract
    那个声明了却零使用的 RUNS_ROOT(验收 12c)。写明它们是硬要求,
    不是可以留到下一轮的整洁度问题。
    
    批 1 一并加不变式测试「凡是声明了 GRAPH_RUNS_DIR 覆盖的模块,该
    覆盖必须真的被用到」,形状照抄 agents-root-single-holder.test.js。
    「样本非空」前置断言别省 —— 扫不到文件时会静默全绿,那正是这类
    测试自己变成零调用方的方式。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 并行会话指出我上一版归因错了,逐条复验后确认它是对的:
    
    【纠正】runRootDir 与 companyRootDir 不是同一事实的两种拼法,是两个
    不同的事实:
      runRootDir     → run 目录,覆盖态 {RUNS_ROOT}/{rootRunId},
                       扁平即可(rootRunId 全局唯一)
      companyRootDir → 公司根,覆盖态 {RUNS_ROOT}/_companies/{cid},
                       那层 _companies 是必须的(公司 id 不是全局唯一坐标,
                       扁平放会和 run 目录撞名)
    company-paths.js 的注释本身写着「理由与 runRootDir 那处 override
    一致」,作者是有意区分的。我上一版写成「三种拼法争权威、实施前先定
    哪个权威」,会让批 1 实施者去做一个根本不存在的设计决策。真正错的
    只有一处:writeTaskGoalArtifact 自己拼路径。修法一行级。
    
    【新增缺陷 B】goal-contract.js:75 声明了 RUNS_ROOT、注释还写着「与
    graph-engine.RUNS_ROOT 是同一个变量、测试隔离在用」——然后全文件
    一次都没用过,同文件的 resolveRunDir 照旧自己拼。而且 resolveRunDir
    全仓库零调用方(只有定义、exports 和一行文档)。
    
    它今天不会出错,危害是注释在撒谎:声明了意图、代码不兑现的常量比
    没有这个常量更容易骗人,第一个接上去的人直接踩。批 1 一并处理 ——
    要么真的用上,要么连函数带常量删掉(零调用方,删更省)。
    
    【验收】12b 补上必须「先设 env 再 require」的姿势要求,12c 新增。
    第六个实例登记:graph-engine:84 / goal-contract:75 / graph-run-sse:34
    三处都在模块加载时快照 GRAPH_RUNS_DIR,而 company-paths:14 的注释
    恰恰记着相反的教训,既有测试已在为此做变通。教训在一个模块里学会
    了没传出去 —— 不是事实被复述,是「事实该怎么读取」的约定只传一半。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 并行会话把 system/agents 那一族收敛到 locator.AGENTS_ROOT 之后
    (e120645,13 处跨 5 文件,我复验 3/3 通过),顺着同一个模式查
    company.js 里的 data/companies 那一族,撞见一个真的分叉:
    
      写 company.js:writeTaskGoalArtifact
         path.resolve(__dirname,'../..','data','companies',...)   不认 GRAPH_RUNS_DIR
      读 graph-engine.js:runRootDir(describeTask / goal-contract 都走它)
         RUNS_ROOT ? path.join(RUNS_ROOT, root_run_id) : ...      认
      三 company-paths.js:companyRootDir
         runsRoot ? path.join(runsRoot,'_companies',cid) : ...    认,但形状又不同
    
    同一个事实三种拼法。GRAPH_RUNS_DIR 一旦设上(目前只有测试在用),
    写进 A 处、从 B 处读,describeTask 拿到 null 后静默退回兜底文案
    「执行图节点任务」—— agent 从此拿不到真实任务描述,且没有任何报错。
    goal-contract.test.js 的注释佐证读取方那套形状才是约定。
    
    生产环境两边恰好都退到同一个默认分支,所以今天不发作。但批 1 会把
    artifact 落盘点从「下达时」前移到「入队时」,正好要动这个函数 ——
    修的时机就是那时。新增验收 12b 专门验它。
    
    方法论也记一条:我第一次只 grep 了 TEMPLATES_DIR 这个名字,因此漏
    掉同一文件里语义相同但叫 DATA_DIR 的 3 处。同义异名的副本按名字
    grep 永远找不到,要按值 grep。
    
    这是本文档攒到的第五个同类实例。同一个事实只能有一处持有者 ——
    路径也是事实。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 28 号把本体根从 system/templates/ 搬到 system/agents/,agent-locator 就是
    为了当这个路径的唯一持有者而建的。但搬完之后仓库里仍有 13 处自己拼这个路径,
    等于 locator 只是多了一份副本、没有真正收敛:
    
      server/routes/company.js            9 处(6 个 TEMPLATES_DIR + 3 个 DATA_DIR,
                                              同一文件里只有 1 处用了 locator)
      server/services/task-service.js     1 处
      server/src/core/skill-config-loader.js  1 处
      server/src/core/devloop/loop-controller.js  1 处
      scripts/extract-agent-prompts.js    1 处
    
    危害不是「不好看」,而是下一次再搬时改 12 处、漏 1 处:漏掉的那处指向一个
    不存在的目录,而本仓库读这类路径几乎都包在 fs.existsSync() 守卫里 ——
    目录没了不报错,只是静默跳过。这个形状刚在 scripts/demo-harness-company.js
    上实测过(base 本体没挂上,demo 跑出来的 agent 没有四段式骨架,脚本照常打印成功)。
    
    局部变量名保留不动(TEMPLATES_DIR / DATA_DIR / SYSTEM_AGENTS_DIR / agentsRoot /
    OUT_DIR),只把取值改成 locator.AGENTS_ROOT —— 名字是局部别名,无害;
    「路径这个事实」则收敛成一处,下次再搬只改 agent-locator.js。
    
    新增 server/tests/unit/agents-root-single-holder.test.js 锁住这条不变式:
    扫 server/ packages/ scripts/ 下所有源码,禁止 path.resolve/join 自己拼
    「以 ../ 开头、以 system/agents 结尾」的字面量。带「样本非空」前置断言,
    防止扫描路径写错时退化成永远通过。改坏一处实测能精确点名到文件行号。
    
    线索来自并行会话(它在准备改 company.js 时发现同一文件里 7 份定义)。
    实际比它看到的更多:它只查了 TEMPLATES_DIR,同文件另有 3 个 DATA_DIR、
    另外 3 个文件各 1 处。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 并行会话提醒 company-service.js 的 TEMPLATES_DIR 名字过期(实指
    system/agents)。查调用点时发现更值得记的是 company.js:同一个
    路径常量在这个文件里被重复定义了 7 次,6 处硬编码
    path.resolve(__dirname,'../../system/agents'),只有 1 处用了权威
    访问器 locator.AGENTS_ROOT。
    
    这个路径 2026-08-18 刚被 agent 目录重构搬过一次 —— 下次再搬,
    改 6 处漏 1 处就是静默 bug。批 1 本来就要大改 company.js(全部新
    端点挂这里),顺手收敛成模块级一份,不是为改而改。
    
    这与本文档反复点名的是同一个病:canDispatch 的取值域被前端复述过
    一份、ACTIVE_ROOT_RUN_STATUS 在两个文件里逐字重复、
    isUngraduatedStatus() 的取值域不许在别处复述。同一个事实只能有
    一处持有者,路径也是事实。
    
    同时登记一条刻意不做的:company-service.js 那个 TEMPLATES_DIR
    改名要动 50 处、跨 6 个文件、零功能收益,不值得。留记录是为了
    说明它是已知的,不是没人看见。
    
    顺带独立复验:template-refs.test.js 4/4 通过,27-P3/P4 的
    blocker 21(CI 因骨架悬空引用而红)确实已关闭。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 刚提醒过并行会话「27-graph-agent-architecture 的状态行落后两个阶段,
    会连累每一次迁移判断」,回头看自己这份也有同类问题:文档名叫 p3c、
    自述「P3 收尾」,但内容已经溢出 P3 的边界。
    
    补清楚三件事:
    - 批 1-3 才是真正的 P3 收尾,是本文档主体
    - 批 4(并行)归 27-P4 §一.3,那边是设计权威(§五.2 已有警示块)
    - 批 5(两套任务体系统一)不属于任何既有 phase,是跨阶段技术债清理
    
    以及最要紧的一行:五批全部未开工,0 行代码。判断能否搬 done 时
    按批看,别按文档名看。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 两处都是实测校准,不是凭印象改:
    
    1. 27-graph-agent-architecture.md 的状态行落后两个阶段。
       原写「P2 起 📝 设计中/未实现」,实际 P2 的设计已归档在
       docs/done/system-design/27-p2-org-graph.md,P3 主体也已实现
       (27-p3b 自述「P3 主体已实现并提交 c3abf4a 起七轮,719/719 全绿」,
       27-p3-p4 有完整的 §十 实现记录 + 第六轮终验回填)。
       校准为 P0/P1/P2/P3主体 ✅、P4 📝 未实现。
    
       这一行是别人判断「哪些文档能从 todo 搬去 done」的依据,
       过期会连累后面每一次迁移判断,所以补了一句提醒。
    
    2. 27-p3-p4 的 blocker 21(骨架悬空引用,CI 红)已由 28 号改造顺带关闭。
       实测:六个 id 全部登记在 system/agents/_registry.json;
       template-refs.test.js 4/4 通过(全量 999/999)。
       原文登记为「未修」的第二个隐患(_registry.json 被 usedCount 原地改写)
       也已关闭 —— usedCount 已挪到 data/template-usage.json(27-P3B W13),
       跑完真 CLI E2E 后该文件 git status 干净。
       原始记录保留备查。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 27-P4 §一.3 的目标逐字写着「解除『一家公司同时只有一个 active
    root run』约束 —— P3 v1 用一个唯一索引强制它,并行能力具备后应
    放开」。本文档的批 4 在做同一件事。
    
    划清边界:放开约束的设计归 27-P4,本文档只负责在它之上定义
    「谁来排队、按什么序出队」。两边各写一版会产生两套并发守卫 ——
    而这次要删的正是最后一道 DB 兜底,守卫分裂的代价特别高。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »