• 把「人类提的一个需求」与「机器执行的一件活」拆成两级。这不是新概念:
    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 »
  • 设计见 docs/done/system-design/28-agent-registry-unification.md。
    
    改造前 Agent 本体散在三处(system/agents/ 11 个、system/templates/ 50 个、
    data/companies/{id}/agents/ 17 个),同一个 AgentScaffold 被三个 baseDir 实例化。
    后果是同一角色在 N 家公司下有 N 份互不相干的副本,模板更新不会同步;
    org-coder / org-planner 这类走 hireFromBaseMetaAgent 造出来的本体
    legacy_path 指回自己公司目录,永远无法被第二家公司复用。
    
    本次:
    - G1 单一注册表:system/templates/ 整体并入 system/agents/,base → _base。
      7 个同名 id 逐个裁决,被丢弃的一份归档到 docs/_archive/ 而非删除。
      source_kind(template/global_agent/company_hired) → origin(builtin/created)。
    - G2 运行态下沉:CLI 的 cwd 从本体根改为 system/agents/{aid}/{cid}/{taskId}/。
      memory / conversations 落公司态 {aid}/{cid}/,按公司隔离不串。
      sessionPool 的 key 含 (companyId, taskId),两家公司调同一 agent 不复用会话。
    - G2b 产物不下沉:交付物仍落 data/companies/{cid}/,system/agents/ 只留运行痕迹。
      写入边界按链路分叉——graph 节点走节点私有目录(被 indexNodeOutputs 索引),
      chat 走公司 workspace,同一提示词里不再出现两条互斥指令。
    - G3 公司只存引用:data/companies/{id}/agents/ 删除,新增 company_agents 关联表。
      不复用 org_graph_nodes——后者是执行流图,work 层本体被雇时不写它,天生覆盖不全。
    - G4 缺则创建:本体缺失时从 _base 复制骨架进 system/agents/{id}/,
      agent-creator 填 agent.md/soul.md 后才从 draft 转 active(哨兵判据,
      骨架未填时哨兵仍在,不许被 hire)。产物即刻是全系统可复用的本体。
    
    新增 server/services/agent-locator.js 收编原先散在 6 处的寻址逻辑。
    
    边界守卫(graph-engine)的豁免从「本体根上五个文件名」放宽为整棵 system/agents/:
    新本体的骨架本来就要进 git,gitignore 挡不住 ?? system/agents/{newId}/,
    旧判据会把一个真正成功的招聘节点判成 boundary_escape fail。
    
    存量迁移见 scripts/migrate-agent-registry.js(幂等,备份到 data/_migration-backup-*)。
    
    真 Claude CLI E2E 已验证:新建公司 → orggraph-planning 自动规划 →
    自动招聘 org-planner/org-coder/org-validator 三节点流水线;
    data/companies/{cid}/agents/ 不存在,company_agents 4 行,
    system_agents 56→56(零新建私有本体,纯复用),git status 零增量。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 用户授权清理 system/templates/ 的过期引用。全仓库核了 34 处,
    结论与预期相反:没有该删的死代码,但有两处正在静默失效。
    
    真正坏掉的(demo-harness-company.js):
      path.join(ROOT, 'system/templates/base/agent.md')   → system/agents/_base/
      path.join(ROOT, 'system/templates/base/config.json') → system/agents/_base/
    两处都包在 fs.existsSync() 里,所以目录消失后**不报错**,只是
    静默跳过 base 本体的挂载 —— demo 跑出来的 agent 没有四段式骨架,
    但脚本照常打印成功。又一个静默降级。
    
    顺带:real-pm-smoke.js 的提示文案、migrate-agent-naming.js 里
    已失效的遍历项、docs/README.md 的 qa-judge 落点。
    
    CLAUDE.md 目录速查表原来只有 `system/templates/devloop-*/` 一行,
    连 system/agents/ 都没列。换成两行并写清三个容易踩的点:目录才是
    数据源头(seedAgents 直接扫目录)、_registry.json 只补
    benchmark_score、_ 前缀是保留 id 会被跳过。
    
    **刻意不动的**:server/tests/ 下 30 处引用。它们分两类,都得留 ——
    ① 记录迁移过程的注释;② 断言目录已不存在的守卫
    (`expect(git(['ls-files','--','system/templates'])).toBe('')`、
    `expect(fs.existsSync(...)).toBe(false)`)。删掉后者等于删掉
    「迁移已完成」的证明。legacy_path 那几个测试夹具同理,是历史数据形态。
    
    验证:三个脚本 node --check 通过;改后的四个路径实测存在。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 并行的 agent 重构那条线同步来的,已核实三个函数都在:
    - agent-locator.js  missingBodyDefinition()   判什么算没填完
    - graph-service.js  promoteDraftIfComplete()  转正动作
    - graph-service.js  isUngraduatedStatus()     hire() 挡人的判据
    
    关键是最后一个只挡 draft / training 两个状态,不是「≠ active 就挡」。
    库里有 36 个本体的 status 是 enabled/idle/ready/trained,按「不等于
    active」判会被全部误判成没填完 —— 这个坑已经被踩过一次。
    
    取值域的唯一持有者是 graph-service,不要在别处复述 —— 与本文档
    §六.3 记的 canDispatch 教训(前端复述了一份取值域还多写一个
    blocked,导致公司失败一次后再也下不了任务)是同一个病。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 另一条工作线正在做 agent 目录结构重构(system/templates 并入
    system/agents),约 900 个变更尚未提交。逐条核实交集:
    
    好消息 —— 本文档引用的全部函数/常量(settleRun / markRunReady /
    stopRun / defaultPlanWorkGraph / PLAN_MAX_ROUNDS /
    writeTaskGoalArtifact / reportRunOutcome / ACTIVE_ROOT_RUN_STATUS /
    saveTaskMeta / getAggregatedTasks)在 650 行改动后全部存活,五条
    行为性结论逐条复验仍成立,work-graph-db.js 完全没被碰(第二轮
    复核最关键的那批发现不受影响)。27-P3/P4 那条「只写函数名不写
    行号」的体例在这里直接兑现了价值。
    
    坏消息 —— §四.9 有一条写反了,已纠正并留档:
    - system/templates/ 彻底取消,本体落 system/agents/task-planning/
    - _registry.json 不再是数据源头(seedAgents 直接 readdirSync 扫
      目录当真相,registry 只按 id 补 benchmark_score),所以「不登记
      会把 CI 跑红」不成立 —— 建目录才是关键动作,登记是可选的
    - 真正的闸门是新增的「毕业」判据:agent.md + soul.md 非空且不带
      _base 骨架哨兵,否则 status 停在 draft、hire() 返回
      body_not_ready。「先建骨架后填」的中间态雇不进公司
    - 本体正文路径三条硬规矩(cwd 深度 3):自身文件 ../../、仓库根
      ../../../../../、交付物必须用 $W 绝对路径。写错全都不报错、
      只静默降级 —— 静默故障家族的最新成员
    
    排期影响:批 1 照常(work-graph-db.js 无人触碰);批 2 的
    task-planning 本体等重构落地后再建,否则要按旧形态做一遍再改;
    批 5 开工前重新核 agent-chat.js(+455 行)。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • companyService.get 已经在读这个文件,零新增读路径;建表要多一张表、
    一套读写、一次迁移,而目前公司级配置项只有这一个。涨到 3 个以上时
    再考虑建表。
    
    这是 P3C 最后一个开放问题,§十一 至此全部关闭。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • R-20.6:
    1. 退役 /api/goals 与 /api/company/:cid/goals 两个 OKR 别名,确认执行
    2. goal 级验收仍用 org-validator,在 goal 层再调一次同一个本体。
       不复用 task 级结论 —— 两次验的是基数不同的东西:task 级问
       「这一个 task 的 task-goal 达成没有」,goal 级问「N 个 task
       合起来人要的东西做出来没有」。单 task 的 goal 跳过这次调用。
       goal 状态机加 failed,判词落 goals.verdict
    3. 预算先不考虑,不做 goal 级闸门;但 planning_tokens /
       validation_tokens 两个记账字段保留 —— 它们不是闸门,是让
       goal 层那两次 agent 调用的花费可见
    4. queued 开放改 depends_on。当初担心的「改完位次怎么算」不成立:
       queuePosition 只按 priority→seq→created_at 排,depends_on 决定
       的是够不够格出队,两者正交
    5. 两套任务体系统一到 tasks 表,新增 §十二(批 5)
    
    定案 5 核实出一个反直觉的前提:两套不是并列的,新体系的回调链路
    长在旧体系身上 —— reportRunOutcome 收敛后调的正是
    /api/agent/task/report。所以「删掉另一套」= 删存储与会话反推
    (system/tasklog + TaskService.getAggregatedTasks),不能删入口
    与回调:/task/dispatch 的 URL 与请求体写在 tools-base.md 里,
    改它等于同时改所有 agent 的提示词。
    
    tasks 表为此吸收三个字段(executor / assignee_agent_id /
    parent_task_id):agent 派单与 graph 执行的载体确实不同,
    硬说成一回事是骗自己;调度器只管 executor='graph'。
    顺带修好:派单去重从扫目录变一条 SQL,两套 T- id 合一。
    
    开放问题 2/4/6/7 全部关闭,只剩 5(max_parallel 放哪)。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 上游失败时下游自动转 blocked 已定案,开放问题里还留着同一个问题
    的疑问句 —— 这正是「代码改了、文档停在上一轮」那类漂移。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • R-20.5 四条:
    1. task-planning agent 提进 v1(原计划 v2 人手拆)。产出一律 draft
       清单,人确认编辑后才下达 —— 那一步是唯一一道成本闸门,
       拆出 5 条 task 就是 5 份 DEFAULT_TASK_BUDGET
    2. 并行仍留批 4,v1 只排队(max_parallel_tasks=1,唯一索引不动)
    3. manual 任务不参与自动出队:startRun(manual) 只点亮入口就停下等
       /next,而 run 已经是 running 占着坑,自动拉起会堵死队头
    4. 上游失败 → 下游自动推导为 blocked 并标红,给「重跑上游」和
       「强制放行」两个出路。blocked 每次 tick 重算而不落死,
       上游重跑成功后下游自己回 queued
    
    定案 1 牵出一处新缺陷:task-planning 跑在 goal 层、任何 run 存在
    之前,它烧的 token 没有预算池可扣 —— 正是 defaultPlanWorkGraph
    注释里点名的「隐形的一半花费」在 goal 层的新实例。上一轮刚以
    「零消费方」为由砍掉的 goal 级预算字段,以记账字段 planning_tokens
    的形式回来(闸门仍不做)。
    
    链路照抄 defaultPlanWorkGraph 不另造:调 agent → 校验 → errors
    原样回喂,最多 3 轮;3 轮不过降级成 1 条同描述的 task,绝不能把
    goal 卡在 planning。新本体必须与 _registry.json 登记同 commit,
    否则 template-refs.test.js 当场把 CI 跑红(§八.21 有前科)。
    
    验收 19-24 新增:拆分正常 / 失败降级 / 记账 / 不该花钱的路径不花钱
    / 上游失败下游标红且可恢复。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 逐条对着代码核初稿,最要命的一条是基础事实搞错了:
    
    - createRun 在建行的事务里就按 readDefGraph 把 graph_run_nodes 全插好了。
      初稿三处基于「queued run 没有节点行、进去就是空指针」的论证全部作废。
      真实情况是 queued run 与 ready run 在数据上只差 run_status 一个字段,
      所以「靠没有节点行自然报错」那道想象中的防线根本不存在,
      唯一的闸门是 advanceOne 里 run_status !== 'running' 那一句,必须加断言钉死。
    
    由此派生出一个既存、但被排队从毫秒窗口放大成必然事件的缺陷:
    节点行在 createRun 时快照,而 getRunState 每次读都取当前定义。
    排队期间点「重新规划组织」→ 新节点没有 run 行 → 出队即卡死。
    补 syncRunNodesToDef,出队前对齐。
    
    其余六处:
    - 出队原语写错:markRunReady 把 from 写死成 planning,复用不了;
      正解是 updateRunStatus(runId,'queued','ready') 这一条条件更新
    - 唯一索引冲突是 throw 不是 changes=0,初稿的错误处理只覆盖一半
    - 触发点枚举本身是漏的:stopRun 与预算耗尽→blocked 两条最常见的
      释放路径都不走 settleRun。改为挂在唯一漏斗 transitionRun 上,
      一个钩子覆盖全部路径(枚举法在这里靠不住,而漏的症状是静默的)
    - report_to 只活在请求闭包里,排队后回调链路静默断掉 → 必须落库
    - okr_node_id 逐 task 下发会反复覆写同一个 OKR 节点,
      done 会被后来的 failed 翻掉 → task 的 run 一律不带 okr_node_id
    - manual 任务占着 running 等人点「下一步」,会永久堵住队头
      → 不参与自动出队
    
    补漏:存量回填脚本(不做的话批 1 反而让历史任务更看不见)、
    任务级 rerun 与节点级 retry 的命名撞车、queuePosition 排序口径
    必须与调度器同源、砍掉没有消费方的 goals.budget_json。
    
    错误结论留档不删(§三 ⚠️ 块 / §五.1 / §九.1 开头)——
    它们是最容易再犯一次的那类。验收从 12 条涨到 19 条。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 控制台走查暴露的「一个 company 只能有一个任务」,根因不是并发约束,
    是「人类提的需求」和「机器执行的单元」被压在同一条 root graph_run 上。
    
    三个实地核实的前提,让这次不是发明新概念:
    - 27-NAMING §四 早就定义了 goal 1 : task N 的基数
    - graph_runs.goal_id / task_id 两列早就在,company.js 注释也写着
      「三者基数不同不可混用」—— 但全仓库没有任何写入方,goal_id 是悬空外键
    - G- 前缀已被 OKR 正名释放;/api/goals 与 /api/company/:cid/goals 这两个
      OKR 向后兼容别名全仓库零调用方,可以退役后把路径要回来
    
    设计要点:
    - goals / tasks 两张新表 + graph_runs 新增 run_status='queued'
    - queued 的唯一理由是把出队的并发临界区交给既有唯一索引兜底:
      idx_graph_runs_one_active_root 覆盖 ready/running/paused 而不覆盖 queued,
      超发会被索引直接拒绝,不需要应用层的「先查后插」
    - 排队推翻了 P3「拒绝而不排队」的定案,但那条定案的理由(没有出队 worker =
      静默丢任务)今天仍成立,所以调度器的四个触发点是功能主体而非收尾
    - 已下达的描述冻结:入队即写 task-goal.json 并被 describeTask 消费,
      改库不改 artifact 会让两份对不上,两份都改等于篡改审计输入
    - 现有 T-${Date.now().toString(36)} 在「一个 goal 批量建 N 个 task」下
      同毫秒必撞,且 tasks.id 是主键 —— 改用仓库既有约定的 4hex 后缀
    
    R-20 四条定案回记至 27-requirement.md。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »

  • 控制台原先只能看引擎的事件轨(stream.log / timeline.ndjson),里面是结构化事件、
    自由文本还被截断到 600 字;引擎更是根本不消费流式 chunk(onAcpEvent 只取
    session/result 的 usage)。所以 LLM 的逐字输出从来不在它的轨道上。
    
    新端点 GET /api/graph-runs/:id/nodes/:nodeRunId/transcript **惰性读** Claude CLI
    自己的会话文件:不进 SSE、不进节点行、不落第二份库 —— 那正是引擎当初决定不存全文的
    三条理由要避免的(输出无上限而 getRunState 每次轮询 SELECT * 全量回传 /
    产物已有 output_ref 这个唯一真相源 / fail_reason 是选边匹配键不能掺自由文本)。
    这里没有推翻那个设计,只是多开一个读取入口。
    
    目录推导与条目规范化抽到 server/services/agent-transcript.js,
    scripts/tail-agent.js 改为复用它 —— 一份实现两个入口。今天已经修过三次
    「两份必须保持一致的实现漂了」,不想制造第四次。
    
    ## 界面上明说的三个前提(不藏)
    
    1. transcript 按 (公司, 角色) 分、**不按节点分**,只能用节点 started_at/completed_at
       截时间窗口 —— 近似而非精确对齐,窗口边界原样显示。
    2. <env_status>(harness 每轮注入的环境块,几 KB 机器噪音)服务端剥掉,
       但**剥了几处如实显示**。静默删内容比留噪音更坏。
    3. 默认只看 agent 说的话;喂进去的内容(提示词全文、工具返回)要勾选才出来 ——
       提示词里有写入边界与公司上下文,不默认摊开,但可以看。
    
    subgraph 节点不给这个 tab(它自己不调 agent,活在子图里)—— 给了就是个空 tab,
    而空 tab 会被读成「没记录」。正在跑的节点靠「刷新」取最新,**不轮询**
    (整份会话几百 KB,跟着 run 状态轮询太贵)。
    
    ## 三处「看着对、实际错」都有用例钉住
    
    ① listRoles 的目录前缀差一个 '-':path.join 丢掉空的最后一段,切出来的角色名
       全带前导 '-'(实测 '-sdd-planning')。我是靠肉眼看输出发现的,所以补回归测试。
    ② 路由取本体取错层:本体绑在定义层节点上,graph_run_nodes 只有 def_node_id,
       第一版直接读运行时行 → SqliteError: no such column。
    ③ 找不到文件时回 ok:false + 期望目录,不回空列表 ——
       空列表会被读成「这个 agent 没说过话」,而真相是「我们没找到文件」。
    
    另外测试当场抓到我自己那个诚实机制的漏洞:整条正文都是 env_status 的条目会被剥空,
    normalize 早返回 null 导致它剥掉的那一处**没计入总数** —— 号称如实回报却少报。
    改成 parts 空但剥过东西时仍返回,由 readEntries 先记账再决定入不入列表。
    
    ## 验证
    
    880 全绿(88 文件)。3 处变异精确变红后逐字还原。
    运行中的实例上走查过:org-planner-sdd 的原文逐条带时间戳显示,
    第一条就是它去读上游 prd.md / contract.json —— R-19 那条链路被 agent 真的用上了。
    subgraph 节点(org-planner)确认没有这个 tab。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 控制台「运行日志」看不到 LLM 逐字输出,不是坏了,是引擎按设计不存:
    
    1. **引擎根本不消费流式 chunk**:defaultAgentExecutor 的 onAcpEvent 只处理
       session/result(累 usage)与 mock/files,文本 chunk 一个字都不接。
    2. **事件里的自由文本截断到 600 字**(EVENT_TEXT_MAX),graph-engine.js 那段注释
       写了三条理由:完整输出无上限而 getRunState 每次轮询 SELECT * 全量回传;
       完整产物已有地址(output_ref)不该有第二个真相源;fail_reason 是选边的匹配键
       不能掺自由文本。
    所以 stream.log / timeline.ndjson 里只有结构化事件 + 短摘要。
    
    但 Claude CLI 自己把完整会话写在
      ~/.claude/projects/<cwd 路径的 / 换成 ->/<sessionId>.jsonl
    而 agent 的 cwd 就是 data/companies/<公司>/agents/<角色>,目录名可推。
    这个脚本把那份 jsonl 渲染成可读的实时流:
    
      node scripts/tail-agent.js <公司>                  列出哪些角色有 transcript
      node scripts/tail-agent.js <公司> <角色>            跟看(增量轮询,Ctrl+C 退出)
      node scripts/tail-agent.js <公司> <角色> --last 40  只看最后 40 条
    
    只渲染 assistant 的 text 当「LLM 输出」:user 侧的 text 是喂进去的东西(提示词全文、
    每轮注入的 <env_status>),跟看时纯噪音 —— 第一版没滤,实测满屏 env_status。
    user 侧只留 tool_result(agent 到底读到了什么,这个有用)。
    
    在运行中的实例上验过:mycompany/sdd-planning 正在跑时列表、--last、跟看三种都对,
    角色名写错时明确报「目录不存在 + 先跑列表看有哪些」,不静默空跑。
    
    ponytail: projects 目录名按 CLI 现行的「/ → -」规则推导,规则变了会找不到目录
    (会明说,不会假装没输出)。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 用错了变量:写的是 `background: var(--bg-surface, #fff)`,而 `--bg-surface`
    不是「不透明表面」,它是 2% 的叠加色调(App.vue 里深色 rgba(255,255,255,0.02)、
    浅色 rgba(0,0,0,0.02))。于是弹框几乎全透,把身后 42% 的遮罩透了出来;
    文字色 var(--text-primary) 又跟着主题走成近白色,叠起来就是一片看不清。
    以为兜底的 #fff 永远轮不到 —— 变量确实存在,只是语义不是我以为的那个。
    
    该看的是周围的约定:**整个控制台是硬编码浅色的**
    (GraphConsole.vue:363 `.console { background:#fff; color:#1a1a1a }`,
    NodeDetailPanel.vue 里 CSS 变量用了 0 次)。我的弹框是唯一读主题变量的组件,
    所以只有它坏。改成与邻居一致的硬编码浅色,遮罩阴影 .18 → .28 让边界更清楚。
    
    在运行中的实例上用真实产物核过(mycompany 的 brd.md):白底、标题/表格/列表
    渲染正常。871 全绿(纯样式改动,无逻辑变更)。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • ## 为什么加
    
    「输入输出」tab 把产物的绝对路径摆出来了,但那是服务器本地路径 —— 浏览器里点不开,
    走查时只能靠肉眼确认「路径长得对」,而真正要确认的是「这份 brd.md 里写了什么、
    下游读到的是不是它」。
    
    ## 前端
    
    新增 web/src/components/graph-console/FilePreviewModal.vue,输入/输出两处路径
    改成可点按钮。渲染三档:.md 走 marked(已是既有依赖,没新增包)、.json 重新缩进、
    其它原文。两处刻意的选择:
    - .json 解析失败时**退回原文**并说明,不替它藏起来 —— 产物 json 写坏了本身就是要看的信息
    - 超过 512KB 时后端回 truncated,界面如实说「不是完整内容」,不装作读全了
    output_ref 那一行不做成可点:它是目录不是文件。
    
    后端没有新造端点:复用现成的 GET /api/company/:cid/files/content,它的根目录正是
    data/companies/<cid>,而 graph_files.rel_path 本来就是相对公司根的,正好对上。
    
    ## 修掉六处路径越界(真漏洞)
    
    company.js 里 3 个 GET + 3 个 PUT(templates / company / workspace)用的是同一个写法:
        if (!absPath.startsWith(safeBoundary)) → 403
    「一个目录名是另一个的前缀」时它会放行:
        cid = co-a,path = ../co-a-evil/company.json
        → abs = .../data/companies/co-a-evil/company.json
        → startsWith('.../data/companies/co-a') === true → 放行
    于是公司 co-a 能读 co-a-evil 的文件;PUT 那三处是同一缝隙上的跨公司**写**。
    抽 resolveInside() 用 path.relative 判断,六处共用一份(复制六份判据迟早会漂)。
    新增 7 条用例走真 HTTP 验,PUT 那条断言的是**文件没被改**而不只是响应码。
    
    ## 顺带修掉一个被你的实际使用触发的 bug
    
    跑全套时 graph-seed-service 的对账用例红了,报
    `org_graph_nodes WHERE company_id='mycompany' expected 10 / actual 9`。
    不是并发偶发 —— 单跑也红。原因:seedCompanyAgents 对 type='meta-bootstrap' 的角色
    是 continue(既不落 org_graph_nodes 也不落 system_agents),而 verifyCounts
    复算期望值时把 agents[] 全算进去。mycompany 的 10 个角色里 orggraph-planning
    正是 meta-bootstrap → 差 1。
    
    也就是说:**界面上点一遍新建公司就会踩到**。这条缺陷上一轮就被预告过
    (当时那个 agent 说「不在我的独占文件里」没修),今天被真实使用兑现了。
    修法是让 verifyCounts 复用同一个 isMetaBootstrap 判据,不复制那个 type 判断。
    新增独立测试文件(不并进 graph-seed-service.test.js —— 往它的 hermetic 夹具里
    加角色会连带改既有断言),用 GRAPH_SEED_ROOT 自建最小夹具。
    
    那个文件里第二条用例是刻意的:只断言「对账干净」时,seed 一个都没种也会“通过”,
    两种原因症状相同。第一版就是这么错的 —— 我误以为 runStartupSeed 会种公司角色
    (它只做 reconcile + 全局 agent + 模板),是第二条断言把「夹具没种」和
    「过滤没生效」分开的。
    
    ## 验证
    
    871 全绿(87 文件)。3 处变异精确变红后逐字还原
    (startsWith 回退 → 精确打红跨公司读/写两条;verifyCounts 去掉过滤 → 打红对账条)。
    拿运行中的实例 + 用户真实公司 mycompany 走查过:读到真实 brd.md 内容,
    `../idearun/company.json` 与 `/etc/hosts` 均 403。web:build 通过。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • ## 起因
    
    integration 测试里 `process.env.API_KEYS = 'test-api-key-123'` 是死代码:
    生产读的是 OPEN_API_KEYS(config.js:31),而这个测试文件里 API_KEYS 只出现
    这一次赋值,没有任何 /api/open/* 请求、没有任何 key 相关断言。已删。
    
    (更正上一轮的判断:它不是「为了错误的原因而绿」——它什么都没测,
    所以也谈不上错。两个缺陷互相抵消成了无害。)
    
    ## 为什么错名字能活这么久
    
    api-key-auth.test.js 原有 7 条用例**全在测 extractApiKey**(怎么从 header/query
    取出字符串),而做鉴权决定的 requireApiKey 一条都没有。OPEN_API_KEYS 那条分支
    从来没被执行过 —— 没有测试执行过的路径,喂给它什么名字都不会有人知道。
    
    补 4 条(拦/放两侧各两条):无 key → 401「需要 key」/ key 无效 → 401「key 不对」/
    命中系统 key 列表 → 放行且挂 req.apiKey / 命中用户 api_key → 放行且挂 req.user。
    
    ## 补完当场抓到一个真缺陷(这一条才是重点)
    
    requireApiKey 把 findByApiKey 返回的**原始数据库行**挂上 req.user,带着
    password_hash。JWT 那条路(jwt-auth.extractUser)一直是过 toPublicUser 才挂的 ——
    两条鉴权路径行为不一致,而 req.user 是 handler 唯一的身份来源。
    
    当前没有实际泄漏(open-api 的 handler 只读 req.user?.id / ?.email,
    没有哪个把整个对象回给调用方),但将来任何一个 res.json({ user: req.user })
    都会把 bcrypt 哈希发给外部 API 调用方。改成过 toPublicUser。
    
    顺带确认 requireApiKey 本身是 fail-closed 的:无 key 401;key 既不匹配用户
    api_key 也不在配置列表 401。OPEN_API_KEYS 为空只是让系统级 key 全部失效,
    不放开任何东西 —— 所以那个错名字不构成安全漏洞。
    
    ## 验证
    
    862 全绿(85 文件)。4 处变异逐一精确变红后逐字还原。
    
    其中 T3 值得记:第一版「无 key → 401」只断言状态码,而把 if (!key) 改成
    if (false) 之后测试仍全绿 —— 因为纵深防御,key=null 会落到第二道门照样 401。
    两者的区别在**文案**(「需要 key」vs「key 不对」),而我在另一条用例的注释里
    自己写了「两种失败要分得开」却没断言它。补上文案断言后 T3 才真的变红。
    只断言状态码的鉴权测试基本都有这个盲区。
    
    数据核查:跑完全套后共享 users.sqlite 的 sha256 不变、用户数仍为 1
    (新用例用 USERS_DB_PATH 打到 /tmp —— 这个覆盖口是上一轮刚补的,
    在它存在之前这组用例会往共享生产库里插测试用户)。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • ## 问题
    
    server/middleware/jwt-auth.js 的兜底密钥 'lemon-dev-secret-change-in-production'
    提交在仓库里,而 .env.example **没有 JWT_SECRET 这一项** —— npm run setup 就是
    从 .env.example 生成 .env(CLAUDE.md「快速启动」),所以照文档装出来的部署
    必然用这个公开密钥签会话,且启动时零告警。
    
    危害不止「密钥弱」。extractUser 拿 decoded.userId 回库重查用户
    (所以伪造 role:'admin' 这条路本身是堵住的,这个设计是对的),但
    user-db 的 id 是自增小整数、不是 uuid:拿已知密钥签 {userId: N}、从 1 试起
    就能命中真实账号,拿到的是该账号在库里的真实 role。命中 admin 就是
    requireAdmin 守着的整个 /api/users/* 全部敞开。不需要密码、不需要 OAuth,
    只要读过这个仓库。
    
    本机 .env 有 64 字符密钥,未受影响;受影响的是默认部署路径。
    
    ## 三处改动
    
    1. .env.example 补 JWT_SECRET(含 openssl rand -hex 32 生成命令与危害说明)
    2. jwt-auth.js:NODE_ENV=production 且未设 JWT_SECRET 时**拒绝启动**
       (签名密钥缺失不是可以边跑边补的事);开发环境保留兜底但每次起进程吼一次。
       三种情形都实测过:生产无密钥→抛错拒启;生产有密钥→正常且不告警;开发无密钥→放行+告警。
    3. 新增 guard server/tests/unit/env-example-secrets.test.js:
       「安全类 env + **非空**硬编码兜底」必须在 .env.example 里有一行。
       只管非空兜底是刻意的 —— 空串兜底(FEISHU_ENCRYPT_KEY、OPEN_API_KEYS)是
       「未配置」的正当写法,缺了 fail closed;把它们也算进来只会制造噪音,
       然后被加白名单,然后白名单腐烂。危险的是「缺了也能跑、还跑得像配好了」那一类。
    
    ## 顺带修掉一个能把全新克隆直接搞坏的
    
    .env.example 写 PORT=3000,而 config.js 默认 3010、web/vite.config.js 的代理
    目标硬编码 3010 → npm run setup 装出来的环境里前端每个 API 调用都 502,
    而症状离原因很远。改成 3010,并加一条 guard 钉住「example / config 默认 /
    vite 代理」三者一致。
    
    ## 验证
    
    857 全绿(85 文件),既有断言未改。3 处变异精确变红后逐字还原。
    其中 S3 那条变异(把兜底常量改成内联字面量)是必要的:guard 第一版只认内联
    字面量,而我重构时把兜底提成了命名常量 DEV_FALLBACK_SECRET,于是 guard 对它
    诞生的那个 case 恰好失明 —— 被它自己的自检当场抓到,遂加上同文件常量绑定解析。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 起因是一个看起来无害的问题:<repo>/data/users.sqlite 为什么是 0 字节。
    答案是没人用它——user-db 走三级上跳落在仓库外的 ~lemonAI/data(跨兄弟仓库
    共享的平台数据,与 skill-db / benchmark-db 一致;仓库内 data/ 放 graph/companies/
    devloop 这类租户数据)。那个空文件是遗留,已删。
    
    顺着查下去发现三处同型缺陷,形态都是「产生 → 传递 → 落库 → 读回」漏掉读回,
    而且是最有欺骗性的一种:测试设了隔离用的 env,没有人读,于是数据全写进
    **共享的生产库**,测试照样全绿,afterAll 删的 /tmp 文件从来没被创建过、清理还“成功”。
    
    1) USERS_DB_PATH 压根不存在
       user-db.test.js 只能直写共享用户库(注释老实写着 "uses hardcoded
       data/users.sqlite"),afterAll 是空的、拿「邮箱按 UUID 唯一所以不会撞」
       当成了不用清理的理由。不撞是真的,不堆积是假的:357 个用户里 356 个是夹具。
       加 USERS_DB_PATH 覆盖口(口径同 graph-db 的 GRAPH_DB_PATH),测试改为
       先设 env 再清 require.cache 再 require(DB_PATH 是加载时求值的常量,
       顺序错了照样静默失效),afterAll 真删库文件。
    
    2) BENCHMARK_DB_PATH 有人设、没人读
       benchmark-db.test.js:13 与 integration/api-routes.test.js:43 都在设它,
       而 benchmark-db.js 那一行不读 → 共享 benchmark 库攒了 342 行
       skill_name=benchmark_test。补上读取后,两个测试文件一行不改就真隔离了
       (日志里 Initialized SQLite at /tmp/... 第一次是真的)。
    
    3) SKILLS_DIR 是写错的变量名
       config.js:13 读的是 SKILLS_CODE_DIR,所以 skillsCodeDir 一直指向那个
       2.7 万条的真实共享 skills 目录。两个测试文件的变量名改正。
    
    新增 guard server/tests/unit/test-isolation-env.test.js:
    测试赋值过的**路径类** env(_DIR/_PATH/_ROOT 结尾)必须有生产读者。
    只管路径类是刻意的——那是会造成静默数据污染的地方,而 HOME(os.homedir 间接读)、
    JWT_SECRET、PORT 这类天然不在范围内,不需要维护会腐烂的白名单。
    顺带钉住「共享 data 在仓库外」这个有意的分层,免得被“修”回仓库内。
    guard 第一版把自己文档注释里的举例当成真赋值、自己判红了,所以两侧都先剥注释——
    凡是静态扫源码的检查都要先想清楚该量的是代码还是文本。
    
    数据清理:共享 ~lemonAI/data/users.sqlite 删掉 358 行夹具
    (不是 356——修好之前又跑了两轮测试,每轮 +2,正好印证泄漏一直活跃),
    删前备份 + 计数闭合校验(保留行 + 匹配行 == 总数才动手),
    删后真人 bboonnee@gmail.com 完好,总数 1。
    
    验证:854 全绿(84 文件)。跑完全套后 users / benchmark_logs / skills
    三个共享库的 sha256 逐位不变——修好之前这不可能。
    3 处变异逐一精确变红(去掉任一 env 读取、把变量名改回错的)后逐字还原。
    
    未动:JWT_SECRET / API_KEYS 生产侧无读者或名字不符(config.js 读 OPEN_API_KEYS),
    不造成数据污染,另报;共享 benchmark 库那 342 行夹具未清,等确认。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 第六次真 CLI 冒烟 gapClosed=true:org 层契约落库 4 行(此前恒 0),
    且那段文字真的进了 workgraph-planning 收到的提示词(52677 字节,第 746 行起)。
    
    ## workflow 四路并行的产出
    
    - **org 层两半一起接**(关键顺序):先接读回方 buildPlanningPrompt 的 ③bis 段
      (org 边入边 → 上游绝对路径;出边 produces → 「你规划的子图必须有节点产出这些」),
      再接声明方 orggraph-planning 模板。只补模板的话就是又一次「声明了没人读」——
      org 图节点全是 subgraph,而 describeInputs 对 subgraph 直接返回空。
      resolveInboundLines 从 describeInputs 抽出共用,两侧口径物理上不可能漂移。
    - **清掉 404 死路径**:模板三处教的 POST /api/company/:id/work-graphs 从未注册。
      真跑复验:agent 侧零调用,改为按新 Step 7 写项目根绝对路径,还自己 require
      validateWorkGraph 预检了一遍才交回,round=1 一次过。
    - **四张骨架补 produces/needs**:从「靠模板文字施压」变成结构保证,每张过规则 6。
      真跑里 agent 照抄了 planner.json 并跟着任务删掉 sdd 节点——是改编不是复制。
    - **graph-seed 顺序依赖**:靠生产代码里已有的 GRAPH_SEED_ROOT 缝隙指向冻结快照。
      不是「跑几次都绿」结案:构造了原先必红的 churn,修前 12 次 3 红、修后 15 次 0 红。
    
    ## 我自己收的 4 条 major + 1 条 minor(都在 graph-engine.js)
    
    复验挖出「引擎自己拼的提示词在教 agent 做模板明令禁止的事」,同一条提示词里对立:
    1. 「已自行落库成功时回一行 workGraphId: wg_xxx」vs 模板「你永远拿不到真的 wg_ id,
       永远别写」。agent 没有任何受支持的落库途径,写出来必是编的,而 extractPlannedGraph
       优先匹配那个形状、查库查不到就整轮作废。删掉邀请,保留引擎解析真实 wg_ 的能力。
    2. 内联图示例的边一个 produces 都没有——agent 唯一见过的「边长什么样」是零声明的。
    3. 「端点尚未上线」暗示将来会上线,于是它还想去试一次。改成「不存在、也不会上线」。
    4. indexNodeOutputs 收集 declared 不按 role 过滤,把出边的 needs 当成本节点的产出。
       Chrome 走查实测:产品需求文档.md 落成 artifact,下游 prd 输入变 ⚠️。
       更坏的一面是错标(brd_notes.md 命中中继 needs[brd] → 归因给错的产出方)。
       1ea9efa 加的 role 列,这里正是它存在的理由。
    5. (minor) contract.prev.json(retryNode 的契约快照)被索引成 artifact 噪声。
    
    新增 guard:引擎提示词不许与模板对立(静态读源码,剔掉注释行——
    第一版把「解释为什么不许说某句话」的注释也扫进去了,量错了对象)。
    
    测试 850 全绿(83 文件),既有断言未改。5 处变异精确变红后逐字还原。
    Chrome 走查确认控制台正常分支(此前只验过降级分支):契约药丸、绝对路径、
    needs 中继说明、tab 上的 ⚠️ 计数全部渲染,零 console 报错。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 第五次真 CLI 冒烟(gapClosed=true)暴露的两条,都是「这轮成立是侥幸」型。
    
    1) indexNodeOutputs 靠文件名定 kind
       真跑成立纯属 brd 节点恰好把文件命名成 `brd.md`。换写 `业务需求文档.md`
       就落成 kind=artifact → 下游入边声明的 brd 查不到实例 → 提示词退化成
       「⚠️ 索引里查不到实例」,即刚关掉的断点换个形式重现。agent 给产物起中文名
       是完全正常的行为,不该由它决定索引对不对。
       改成两遍:先按文件名认领,再在**一对一无歧义**时(还差一个 kind 没人产、
       正好还剩一个认不出的文件)按声明认领。有歧义不猜 —— 猜错比留 artifact 更坏,
       错的 kind 让下游读到一份不是它要的文件,artifact 只是查不到、会如实报 ⚠️。
    
    2) describeInputs 任务级共享不去重
       真跑里 brd 与 prd 两个节点各写了一份 contract.json,都是 kind=contract,
       于是往后每个节点的提示词里「contract(任务级共享)」逐个累加。节点一多是噪声,
       且给 agent 两个同类路径等于把一个我们自己都没定的选择推给它。
       按 kind 保留最新那份,与沿边那一半的 byKind.set 同口径。
       ponytail 标了根因(节点私有目录的文件也会按文件名落成 task-shared 的 kind)
       与真正的修法(任务级产物只认 tasks/{taskId}/),后者要动 R-9.3 现有语义,
       归到那条线一起定。
    
    测试 829 全绿(+3),既有断言未改。4 处变异精确变红后逐字还原。
    含 verify agent 回填的 §4.6「第五次真 CLI 冒烟」小节。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • R-19.A 把 declareEdgeContract 的调用方补上之后,inboundContracts 终于非空,
    「沿边传递」这类数据流在真跑里生效了(真实规划 agent 自己在边上写出了
    produces/needs 声明)。本次收掉它留下的三个缺口。
    
    1) role 落库(第 14 次「四环漏一环」)
       normalizeContracts 产出 role,declareEdgeContract 入参没有它、表也没这列 ——
       产生 → 传递(丢) → 落库 → 读回。后果不止是控制台不精确:规则 6(data-contract)
       靠「有人 needs 但没人 produces」判错,只存 kind 就没法从库里重算,而
       patchGraph 第 5 步的重新校验读的正是库 —— 增量 PATCH 又恰恰是
       「此前满足的 needs 失去产出方」的唯一发生地,校验偏偏在最该生效的通道上失效。
       - graph_edge_contracts 加 role 列(CHECK + 老库 ALTER 后回填 produces:
         NULL 在规则 6 里既不算产出也不算需求,会误拒一张合法的图;闸门宁可漏判不可错拦)
       - 两层调用点透传 role;readGraph 把 contracts 读回来并递进 validateGraph
    
    2) 控制台三字段(前端在、后端没喂)
       GET /api/graph-runs/:id 补 edges[].contracts / companyRoot / taskFiles,
       否则 R19-C 那三块界面整块降级:声明和索引都在库里,界面上看不见。
       contracts 只在非空时出现,与 failReason 同一个约定。
    
    3) 收拢 companyRootDir/taskDirRel 镜像
       goal-contract.js 里那份镜像靠注释约定「与 graph-engine 逐字同构」维持,
       控制台路由成为第三个调用方时这个约定就不成立了。抽到 company-paths.js,
       GRAPH_RUNS_DIR 改为调用时读(原先两个模块各自在加载时快照,
       加载顺序不同会算出不同的根)。goal-contract 那条「镜像漂了就报警」的用例
       按其自身预告改成比函数返回值。
    
    测试 826 全绿(+11:role 8 / 路由 3),既有断言未改。
    11 处变异逐一验证精确变红后逐字还原;其中一条断言初版在 fixture 上永远自洽
    (图里没有无声明的边),补了一条无声明的边才真正有判别力。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 第四次真跑的断点:org-planner-prd 失败,它自己的归因原话是
    「不是上游没产出,是它的产出**结构上到不了我手上**」。核实属实 ——
    上游 brd.md / contract.json 落在**节点私有目录**,prd 的提示词只有它自己的私有目录
    + 只读 artifacts/,而 brd--success-->prd 这条边**不携带任何文件引用**。
    边一直只承载控制流(condition/edge_kind/fail_reason),R-4 要的「边的输入输出」
    被落成了从 output_ref **推断**的 UI 展示、没有实体,而推断的那一端下游读不到。
    
    ## 定案(27-REQ R-19)
    
    - 建 sqlite 表而不是 files.json:JSON 会重演 _registry.json 的并发重写与 git 冲突
      (本轮刚被它咬过,W13 只能把 usedCount 挪去另一个被 ignore 的文件)
    - **两类数据流都要**:纯边绑定表达不了 R-9.3 的「planner 生成、validator 执行」——
      org graph 是 planner→coder→validator,acceptance 跳过 coder,这两点之间没有边
    - workspace 根**代码推导不入库**:存绝对路径会烂,W13 清的那 10 条 legacy_path
      悬空行就是这个病。表里只存相对公司根的路径
    - 路径走 tasks/{taskId}/,不走 workspace/:后者语义是「Agent 自治领地、框架不干预」
      (06-沙盒自治),与「框架管着、可检索的交付物区」是两种所有权
    - 契约(kind)与实例(path)分规划时/运行时两阶段 → 引擎可在节点跑之前校验
      「你声明要吃 brd,但上游没人产 brd」,这类图结构错误此前只能等跑到那步才发现
    
    ## 实现
    
    work-graph-db:graph_files(实例)+ graph_edge_contracts(契约)两张表。
    graph_files 的当前版本走**部分唯一索引** WHERE superseded_by IS NULL ——
    与 idx_org_graph_nodes_company_role_active 同一模式:要「同键只有一个活的」
    又要留历史,条件得写进索引而不是靠删行。API:recordFile / findFiles /
    declareEdgeContract / edgeContracts / inboundContracts / rebuildFilesFromNodes。
    
    graph-engine 两侧都接:
    - 输入侧 describeInputs:按**入边声明的 kind** + **本 task 共享产物** 查实例,
      把绝对路径拼进提示词。声明了却查不到的**明说缺哪个 kind**,不静默省略。
      刻意不是「注入一句『索引在某文件,自己去检索』」—— 那仍靠 agent 自觉拼路径,
      真跑已证明它拼不出来;引擎直接递到手上,不给它猜的机会
    - 产出侧 indexNodeOutputs:节点 success 时登记落盘文件,kind 先看出边声明、
      再按文件名兜底;attempt-*.json 是引擎自读的归因文件,不当产物登记
    
    文档:27-REQ 新增 R-19(四条,含两段原话);27-P4 §4.6 按定案重写
    (原「公司根一个 files.json」作废)。
    
    测试 788/788 全绿。后续(下一轮):规划侧声明契约、goal-contract 路径归并、
    前端「输入输出」tab 展示解析结果、真 CLI 复验 prd 能拿到输入。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • ## W11 最后一块:agent 侧的假承诺
    
    方案 (b) 此前只把 agent-chat.js 的空转喊出来了 —— 那是喊给**运维**听的。
    而 agent 读的是 system/templates/base/agent.md,那一侧还写着:
    
    - 开场「你当前默认只被挂载了 meta-research 这个元技能」—— 那个包不存在,没挂载过
    - 阶段三「*系统给你换上 meta-execute 技能时,你才会拥有修改核心文件的破坏性权限*」
      —— **纯假承诺**:权限自始至终由 config.json 的 cli.toolPolicy 决定,与 phase 无关,
      而 base/config.json 从 RESEARCH 第一秒就给了 bash 与 str_replace
    
    **一个以为自己被拦着的 agent,比知道没人拦它的 agent 更危险。**
    
    改成如实表述:开场加一段说明「技能包不存在、换装不会发生、权限全程不变、
    四段式真正给的是上下文隔离而不是权限隔离」;三处「换上 X 技能」改成
    「记忆已清空,你手上只有 <artifact>」;阶段三那句改成「写入权限从第一阶段起
    就没变过 —— 前三步不落盘是纪律要求,不是系统给你的限制」。
    
    至此 (b) 才算闭合:运维侧有告警、agent 侧不再被误导、文档侧记清了根因。
    
    ## 自动模式真跑:跑出一个结构性缺口
    
    关看门狗后自动模式接管(pause → start{mode:auto},运行中的 run 要先暂停才能切模式),
    它自己把 brd 之后的节点接上跑。结果:
    
    - brd → success(产出 brd.md + contract.json)
    - prd → **fail(impl_bug)**,run 收敛 blocked,预算 calls 3/100 tokens 52,096
    - worktree 零污染(与跑前逐行一致)
    
    prd 自己的归因说清了原因,而且是对的:**它拿不到上游的产出**。
    brd 的 brd.md / contract.json 躺在**它自己的私有目录**里,
    而共享区 artifacts/ 只有 project-root.json 与 task-goal.json ——
    同一张 work graph 内部**上游→下游没有任何数据通路**。
    
    这不是引擎 bug,是 §四.A.5 设计里只做了「跨 org node 走 artifacts/ 共享区」,
    没做「同图内相邻节点怎么把 contract 交出去」:CONVENTIONS §12 的隔离原则是
    「下游只能看到上游的契约、看不到实现」,但契约被写进了私有目录,
    下游同样读不到;而写 artifacts/ 是只给验收契约产出者的特权动作。
    
    W12 补完在这一轮同时得到验证:fail_reason=impl_bug 落库,概览 tab 显示
    「[归因产出] impl_bug」+ 完整 remediation_note,不再是「失败、无原因」。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • LEMONBOSS_DISABLE_WATCHDOG=1 时不启动。
    
    第三次真跑记录:Master 智能巡检带 full 工具权限、会在工作树里改源码,还会自主
    下任务单;而图执行的边界守卫用整仓 git status 差集判归属,于是并行 Master 的写
    被算到「当时正在跑的 work 节点」头上,把一个只是超时的节点额外署成 boundary_escape。
    守卫做不到区分是谁写的(W3「拦不住」第 5 条),所以真跑期间工作树里不能有第二个
    agent 在活动 —— 这是规程问题,不该指望守卫变聪明。此前没有任何关闭开关。
    yibo authored
     
    Browse Code »
  • 测试 788/788 全绿(76 文件,起点 786),未改任何既有断言。
    
    ## 1. idleTimeoutMs 硬编码 120s(「work 节点 → success」跑不通的唯一技术原因)
    
    agent-manager.js 里 `this.config?...?.idleTimeout || 120_000`,且 `...options`
    摊在前面被下一行覆盖,两个后果叠加:
    - 默认 120 秒,比 stream-executor 自己 JSDoc 写的 30 分钟(1_800_000)小一个数量级,
      也和同进程 ClaudeCliProtocol 早已放到 24h 的 idle 上限完全对不上
    - 调用方**无法覆写** —— 传了 options.idleTimeoutMs 也会被盖掉
    
    真 CLI 下的后果:任何「先读几千行源码再动笔」的节点都在动笔前被杀
    (readme-writer: idle timeout: no stream data for 120000ms)。
    
    改成 调用方 > agent 配置 > DEFAULT_IDLE_TIMEOUT_MS(30min),并把常量真的定义出来
    —— 上一轮 Master agent 自己诊断出这个问题也动手改了,但引用了未定义的
    DEFAULT_IDLE_TIMEOUT_MS,跑起来是 ReferenceError。
    
    ## 2. W7 判据在零边最小图上失效
    
    判「org graph 成型了没有」原先看**边数**,理由是「节点 hire() 自己就会造,
    拿节点数当信号会废掉『连雇两个人第二个不入图』」——理由成立,但边只是
    「writeGraph 跑过」的代理,而真跑里 orggraph-planning 裁出过一张**零条边的单节点图**,
    代理当场失效:三条判据全落空 → work 层本体被写进 org_graph_nodes,
    下一次下达任务时被 advanceOne 当成 org 节点执行。
    
    改成记录**来源**而不是猜:org_graph_nodes 加 authored_by(idempotent ALTER),
    writeGraph 写 'planner'、hire 写 'hire',判据看有没有 planner 写下的节点。
    hire 先记的行随后被 planner 纳入时升格,反向不降级。
    存量行 authored_by 为 NULL,回落到原来的边数代理,行为不变。
    
    新增两条用例(既有 9 例造的全是有边的成型图,正好绕开这个形状),
    并做过变异验证:把判据改回纯边数,两条精确变红,还原后逐字一致。
    
    ## 3. 前后端「能不能下达任务」的规则各写一份且不一致
    
    前端 ACTIVE_RUN 含 blocked、后端 ACTIVE_ROOT_RUN_STATUS 不含(唯一索引
    idx_graph_runs_one_active_root 里也没有)。后果:一家公司只要失败过一次,
    控制台上就再也下不了第二个任务,而后端其实放行。
    
    规则只留服务端一处:GET /graph-runs 返回 canDispatch / activeRootRunId,
    前端照抄结果,不再自己复述取值域。字段缺失时保守放行,交给端点 409 兜底。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • 测试 786/786 全绿(76 文件,起点 760/72),未改任何既有断言。
    
    ## 本轮三件新东西,全部经真 CLI 验证
    
    W12 补完 · 失败原因四环打通
    - 引擎自判的失败(executor 抛异常、边界越界、子 run 无归因)现在也落
      graph_run_nodes.fail_reason,不再只有 agent 归因产出的才有值
    - 概览 tab 展示「[引擎判定] boundary_escape」+ 终端样式 excerpt,
      取不到 fail_reason 时回落 output_excerpt,并标明来源
    - 真跑实查:两行 fail_reason='boundary_escape' 都在库里,
      产生→传递→落库→读回四环在真 CLI 下走通
    
    W13 · 运行态文件根治(守卫误判的根因)
    - 17 个 todo.md / plan.md / scheduled_tasks.json git rm --cached + gitignore;
      源码(agent.md/config.json/soul.md/tools.md)一个未碰,模板本体仍跟踪
    - _registry.json 的 usedCount 挪进已忽略的 data/template-usage.json
    - 真跑验收:整轮(agent-creator、Master 各一次真 CLI、3 次 hire)结束时
      worktree git status 与基线逐字相同,_registry.json 零 diff
    
    单步进度感 · 真 CLI 一个节点 3-7 分钟
    - 画布卡片秒表 + 详情面板常驻条「运行中 2m40s · 最近事件 …(2m40s 前)」
    - 不做假进度条:引擎不知道 agent 还要多久
    - 顺手修 NodeDetailPanel 不跟 SSE 走(否则秒表也不会动)
    
    W9 失败上下文进提示词、W10 跨层地址两半、W11 Hot Swap 显式告警同批完成。
    
    ## 真跑结论:链路更长,但「work 节点 → success」仍未跑到
    
    走到:orggraph-planning 落图自雇 → agent-creator 补定义 → workgraph-planning
    真跑规划 + **先雇后提交**(W5/W6 再次成立)→ 子 run 起来 → work 节点 RUNNING
    → 7m19s 后 fail。两层原因叠加,都与 W1-W14 无关:
    
    1. **新缺陷**:agent-manager.js:324 把 idleTimeoutMs 硬编码 120s,
       readme-writer 通读源码静默超 2 分钟被掐死(动笔前就死了)。
       同进程 ClaudeCliProtocol 早把自己的 idle 上限放到 24h,
       stream-executor 的 JSDoc 默认值写的是 30 分钟 —— 这里把两者都压掉一个数量级;
       且 ...options 透传的 idleTimeoutMs 会被下一行覆盖,调用方无法覆写。
    2. **并行 agent 污染**:retrain-watchdog 每 5 分钟唤起的 Master 在同一工作树里
       改了两个源文件,被守卫 git status 差集算到该节点头上 → 额外署 boundary_escape。
       这是 W3「拦不住」第 5 条第一次真实发生。
    
    顺带记一笔:这一轮任务是 **LemonBoss 自己的 Master 自主下的单**,
    走的正是 POST /api/company/:id/tasks,它还自行诊断出了上面那个 idle timeout
    并动手改(引用了未定义的常量,是坏的)。
    
    ## 已知未修(下一轮)
    
    - idleTimeoutMs 硬编码 120s —— 「work 节点 → success」跑不通的唯一技术原因
    - W7 在**零边最小图**上失效:判据③要求存在 active org_graph_edges,
      零边图上三条判据全落空,work 层本体被写进 org_graph_nodes。
      服务层那 9 例测试造的都是有边的成型图,正好绕开这个形状
    - 前端 ACTIVE_RUN 含 blocked、后端 ACTIVE_ROOT_RUN_STATUS 不含:
      一家公司只要失败过一次,控制台上就再也下不了第二个任务(后端其实放行)
    - 真跑期间工作树里不能有第二个 agent 活动,否则守卫判罚不可信 —— 应写进真跑规程
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • ## W14 · POST /:cid/tasks 补上前端调用方
    
    该端点此前**全仓库没有任何前端调用方** —— 界面走查确认过:用户在控制台上
    没有任何办法给公司下达任务,只能 curl。这是「实现在、零调用方」在 P3 里的
    第 N 次出现(W8 刚给 replan 补上,tasks 还欠着)。
    
    新增 DispatchTaskButton.vue:
    - 手动/自动**在下达时就选**,不是跑起来再改 —— execution_mode 是 run 的属性,
      起 run 时定;后补一个下拉会让人以为能中途换
    - 自动模式在这一步就给成本预警(真 CLI 一次中等任务 $10-20 / 1-2 小时),
      GraphRunPane 的「启动」那道确认是针对已有 run 的,下达是更早的闸门
    - 409(v1 并发约束)连同服务端 hint 原样透出,比翻译成「出错了」有用
    - run 在跑时按钮直接不出现,而不是让人点了吃 409
    
    挂载:有图未跑 run 时给主操作位(这时候它是页面唯一该做的事),
    有 run 时收进标题栏。顺手修 .left-actions 缺 gap 导致两个按钮重叠。
    
    ## 第二次真 CLI 冒烟(worktree 隔离,主仓库 data/ 零写入)
    
    链路比上一轮走得远得多,两个硬缺陷的修复都在真跑中直接验证:
    
    1. **R-18.1 端到端成立**:POST /create 自动触发 orggraph-planning 真跑,
       它自己把图落库(3 节点 4 边,含两条 backward fail 边)**并自己雇了
       org-planner / org-coder / org-validator** —— Step 7 的招聘真执行了。
    2. **W5/W6 自雇再次验证,且规划质量明显更高**:workgraph-planning 规划出
       brd → prd → benchmark → validator 四节点主干,两条回退边一条带
       fail_reason=input_underspecified、一条无 reason 作兜底 —— 它自己用上了
       D.2.2 的细分路由并满足 fail-fallback-edge 规则。四个本体全部自雇。
    3. **守卫归属修复直接命中**:timeline 里
       「本次调用窗口内有 1 处 agent 运行态文件改动,无法归属到本节点,不计入越界
         unattributable = ['M system/agents/agent-creator/.claude/scheduled_tasks.json']」
       ——正是上一轮把成功节点判成 fail 的那个文件,这次被正确摘出,
       org-planner-brd → **success**(上一轮它跑起来了但被误判)。
    4. tokens 真的在计量(24,218),mock 下恒为 0。
    
    产出位置正确:brd.md 落在 runs/<rootRunId>/nodes/<nodeRunId>/,
    项目根 projects/p-4hjo36 按 root run 分配。worktree 源码跑完已复原到跑前状态。
    
    测试 760/760 全绿(72 文件)。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse Code »