# 28. Agent 注册表统一 —— 本体归位 system/agents/、运行态按公司/任务下沉 > **状态**: **已实施 + 已真 CLI E2E 验证**(v0.6,已归档到 `docs/done/`) > **未完成与未确认的部分不在本文档** —— 见 [28-followups](../../todo/system-design/28-followups.md)(dream 机制、未验证项、未实施的工程项)。 > —— 六个批次全部落地,2026-08-18 用真 Claude CLI 跑通「建公司 → org graph 规划 → 自动招聘三节点流水线」, > 实测数据与遗留项见 §十一「实施结果」。改动仍在工作区,未 commit / 未 push。 > v0.4 修正了 v0.3 把「本体」深度写成 `../../../` 的差一级错误,见 §5.2 的深度对照表。 > **日期**: 2026-08-17(v0.4)/ 2026-08-18(v0.5 实施结果) > **取代**: [27-P1 虚拟引用寻址](./27-p1-virtual-reference.md) 的收窄方案(P1 只虚拟化了 `agent.md`,其余照旧拷贝) > **前置**: P0 图地基(`system_agents` / `org_graph_nodes`)、P1 引用模式(`systemAgentRef` + `company-context.md`)已上线 --- ## 一、目标 一句话:**Agent 本体只有一份,住在 `system/agents//`;公司不持有 Agent 实体,只持有一条引用关系;同一个 Agent 为不同公司、不同任务干活时,运行态下沉到自己本体目录内的 `{companyId}/{taskId}/` 子树。** 拆成五条可验收的目标: - **G1 单一注册表**:`system/templates/` 整体并入 `system/agents/`,全系统只有一个 Agent 本体目录。`source_kind` 里 `template` / `global_agent` 的二分消失。 - **G2 运行态下沉**:Agent 执行的 cwd 从本体根目录改为 `system/agents/{aid}/{cid}/{taskId}/`。同一个 `agent-id` 被 N 家公司同时调用互不干扰,memory / conversations 按公司留存、不丢不串。 - **G2b 产物不下沉**:Agent 的**交付物**仍然落在公司侧(`data/companies/{cid}/workspace/` 与 run 产物区),`system/agents/` 下只留 agent 自己的运行痕迹。产物有且只有一个家。 - **G3 公司只存引用**:`data/companies/{id}/agents/` 整个目录消失。公司与 Agent 的关系退化成一张 `company_agents` 关联表。 - **G4 缺则创建**:`system/agents/` 不存在时走 create-agent —— 从元 Agent 模板复制骨架到 `system/agents/`,再由 agent-creator 按需求写 `agent.md` / `soul.md` / `SKILL.md`,创建完成即成为全系统可复用的本体。 ### 非目标 - 不做跨公司的 memory 共享。记忆归属 `(agentId, companyId)`,落在 `system/agents/{aid}/{cid}/memory/`。 - 不做 `work_graph_*` / `graph_run_*` 表结构变更 —— 它们已经是 `system_agent_id` 引用制,本次改造对它们零影响(这也是现状唯一一处已经符合目标形态的地方)。 - 不做 Skill 市场(`data/skills/`)与 Agent 注册表的合并。 --- ## 二、现状盘点(2026-08-17 实测) ### 2.1 Agent 本体散在三个地方 | 位置 | 目录数 | 入库数 | `source_kind` | 谁往这写 | |------|-------:|------:|---------------|----------| | `system/agents/` | 11 | 8 | `global_agent` | `agent-chat.js:139`、`agent-chat.js:1415`(AgentScaffold) | | `system/templates/` | 50 | 45 | `template` | `company.js:226`(AgentScaffold) | | `data/companies/{id}/agents/` | 17(3 家公司) | 2 | `company_hired` | `company-service.js:500`(AgentScaffold)、`_copyTemplate`、`_provisionReferenceAgent` | 同一个 `AgentScaffold` 被三个不同的 `baseDir` 实例化 —— 这就是碎片化的直接来源。 ### 2.2 7 个同名 id 同时存在于两个本体目录 ``` agent-creator evolver-youtube-crawler innovator-digest innovator-evolver innovator-hunt org-innovator youtube-crawler ``` `seedTemplates()` 遇到已被 `global_agent` 占用的 id 会跳过,所以 `system/templates/` 下这 7 份**从未入库**,`lookupSharedAgentMd()` 对它们恒返回 null → 雇佣时一律退回全量拷贝。P1 §4.6 明确写了「只绕开、不解决」,本次必须逐个裁决。 其中 `system/agents/youtube-crawler` 只有 `.claude/`、没有 `agent.md`,`system/templates/youtube-crawler` 才是完整本体 —— 合并方向不是靠目录优先级能猜出来的。 ### 2.3 本体目录本身就不完整(顺带发现的坏账) ``` system/templates/ceo 只有 plan.md + todo.md —— 没有 agent.md / soul.md / config.json system/templates/devloop-* (6) 只有 config.json + prompt.md —— 没有 agent.md system/agents/screenwriter 没有 agent.md / config.json(未入库) system/agents/templates 一个叫 "templates" 的目录,里面装着 2 个 agent ``` `ceo` 是在库的 `template` 行,`hire(companyId, 'ceo')` 会走引用模式、然后 `lookupSharedAgentMd('ceo')` 指向一个不存在的 `agent.md`。这解释了 `mycompany/company.json` 里 `"ceo": null`。 ### 2.4 存量公司实例:8 引用 / 9 拷贝 ``` REF mycompany/{brd,prd,sdd,orggraph,workgraph}-planning, goal-benchmark, org-validator, planner-validator COPY mycompany/org-coder, org-planner ← 走 hireFromBaseMetaAgent,无共享本体 COPY idearun/* (6) ← 命中 §2.2 的同名冲突 or P1 上线前雇的 COPY p2-verify/code-planner ``` `org-coder` / `org-planner` 在 `system_agents` 里是 `source_kind='company_hired'`、`legacy_path` 指回公司自己的目录 —— 它们是**永远无法被第二家公司复用**的死角,正是 G4 要根治的场景。 ### 2.5 寻址逻辑重复了 6 处 | 文件 | 行 | 用途 | |------|----|------| | `server/src/core/agent-manager.js` | 142 | 执行寻址(唯一实现了 `systemAgentRef` 的一处) | | `server/routes/agent-chat.js` | 31 | Chat / 会话 / 文件读写 | | `server/services/memory-service.js` | 20 | 记忆读写 | | `server/services/heartbeat-service.js` | 8 | 心跳 | | `server/services/agent-transcript.js` | 33 | 日志回溯(注释里写死了「company agent 的 cwd 恒为 `data/companies/<公司>/agents/<角色>`,所以目录名可推」—— 这个推导前提本次被推翻) | | `server/services/graph-engine.js` | 624 | `resolveAgentName()`:**靠目录是否存在**决定用公司地址还是全局地址 | ### 2.6 提示词实际只装配 agent.md(决定 §5.2 的关键事实) `prompt-builder.js:130-177` 的 `build()` 只往 prompt 里放三样:protocol(按 `agent.type`)、`agent.prompt` 指向的那一份文件(`agent.md` 或 `SKILL.md`)、P1 加的 `companyContextPath`。 **`soul.md` / `identity.md` / `tools.md` / `user.md` 从来不进提示词。** 它们唯一的到达路径是 agent 运行时自己用文件工具按 cwd 相对路径去读 —— 而全仓只有 **5 份** `agent.md` 真的写了这种引用(`qa-judge` 是典型:`soul.md`、`identity.md`、`../../workspace/innovator/qa_judge_ideal_model.md`)。 ### 2.7 边界守卫会扫整仓 git status(决定 §5.5 的关键事实) `graph-engine.js` 的 `beginBoundaryGuard()` / `applyBoundaryGuard()`:每次 agent 调用前后各拍一次宿主仓库 `git status`,**diff 非空就把节点判 fail**(`violation: 'boundary_escape'`)。只有一条窄豁免: ```js const AGENT_RUNTIME_STATE_RE = // 旧名:28 之后已改名并放宽为 AGENT_REGISTRY_TREE_RE,见 §5.5 /^system\/agents\/[^/]+\/(todo\.md|plan\.md|memory\.md|_lessons\.md|\.claude\/scheduled_tasks\.json)$/; ``` 现有注释已经记录过这个坑:agent-creator 的 cwd 就是 `system/agents/agent-creator/`,它重写自己的 `todo.md` 就会让**别的**正在跑的节点被判 fail。本次改造把所有运行态搬进 `system/agents/` 内,**如果不先把 gitignore 做对,每一个节点都会稳定越界 fail。** 详见 §5.5。 而且 gitignore 只能挡住「运行态」,挡不住**新建本体**:28 之后 `hireFromBaseMetaAgent` 造出来的 `system/agents/{newId}/` 骨架本来就要进 git,于是它必然以 `?? system/agents/{newId}/` 出现在 `git status` 里。这一类必须在守卫侧解决 —— 也见 §5.5。 --- ## 三、为什么 P1 的方案走不到这里 P1 §1 的非目标写死了「不做 `soul.md` / `identity.md` / `tools.md` / `.claude/skills/` 的去重」,理由在 P1 §3.3: > Claude CLI 子进程以 `workDir`(= 私有 agent 目录)为 cwd 启动,模板里写的是相对路径。如果这些文件不物理存在于私有目录,agent 运行时直接读不到。**结论:`workDir` 必须继续指向私有目录。** 这个结论是**在「私有目录 = 运行目录 = 本体目录」三者合一的前提下**成立的。本次把运行目录下沉成本体目录的子目录之后: - 本体文件仍然在 cwd 的**祖先路径上**,嵌套深度恒定为 3(`{aid}/{cid}/{taskId}/`),所以**本体在 `../../`**,可达性没丢; - 私有可写状态(memory / conversations / workspace)各自有独立落点,不再需要为了「能写」而复制一份本体。 所以 P1 §3.3 的约束在新布局下自动解除,而且解除的代价比「run 目录另起一棵树」小得多 —— 这正是本次布局决策的价值。 --- ## 四、目标形态 ### 4.1 目录布局 **Agent 侧(`system/`)—— 只放 agent 自己的东西:** ``` system/agents// ← 本体,只读,全系统唯一一份,git 跟踪 agent.md soul.md identity.md tools.md config.json .claude/skills/main_skill/SKILL.md system/agents/// ← 公司态:长期,git ignore company-context.md ← P1 产物,公司归属语境 user.md ← 汇报关系(本来就是整份重写的私有文件) memory/ ← memory.md / short-term / cerebellum conversations/ system/agents//// ← 任务态:CLI 的 cwd,git ignore logs/ 本次任务的运行日志(按 {nodeRunId}.log 分文件) scratch/ 中间草稿,不是交付物,可随时清 result.json 本次调用的结构化结果 ⚠️ 这里**没有** workspace/ 和 artifacts/ —— 交付物一律落公司侧,见下 system/agents/_base/ ← 元 Agent 模板(原 system/templates/base) 下划线前缀 = 保留 id,不是可调用 agent ``` **公司侧(`data/companies/`)—— 产物的唯一归宿,本次不改,只是明确边界:** ``` data/companies// company.json okr/ pipelines/ workspace/ ← 公司共享工作区,agent 交付物落这里 runs//artifacts/ ← run 级共享产物区(跨 org node 只读) tasks// ← 任务级产物(acceptance.json 等) projects// ← DevLoop 项目根 ``` `data/companies/{id}/agents/` 删除,`data/companies/{id}/runs/` **保留**(它是 root run 的产物根,跨多个 agent,不属于任何单个 agent)。 四层归属: | 层 | 归属 | 落点 | 读写 | git | |----|------|------|------|-----| | **本体** | `agentId` | `system/agents/{aid}/` | 只读(仅 evolver / agent-creator 可写) | 跟踪 | | **公司态** | `(agentId, companyId)` | `system/agents/{aid}/{cid}/` | 长期读写,不随任务清理 | ignore | | **任务态** | `(agentId, companyId, taskId)` | `system/agents/{aid}/{cid}/{taskId}/` | 一次性,可按保留策略清理 | ignore | | **产物** | `(companyId[, taskId])` | `data/companies/{cid}/…` | 跨 agent 共享,长期 | ignore | ### 4.2 数据模型 ```sql -- 本体注册表:template / global_agent 二分取消 system_agents ( id, name, scope TEXT CHECK (scope IN ('public','company')), -- 保留:私有 agent 仍可只对一家公司可见 owner_company_id TEXT, origin TEXT CHECK (origin IN ('builtin','created')), -- 取代 source_kind;'created' = 走 create-agent 生成的 status TEXT, -- 'active' | 'draft'(见 §6 批次 5-20) benchmark_score, created_at, updated_at -- legacy_path 删除:路径恒为 system/agents/{id},不再需要存 ) -- 新表:公司 ⇄ Agent 引用关系 company_agents ( company_id TEXT NOT NULL, system_agent_id TEXT NOT NULL REFERENCES system_agents(id), role_name TEXT NOT NULL, -- 同一个本体可在一家公司里担两个角色 status TEXT NOT NULL DEFAULT 'active', hired_at TEXT NOT NULL, PRIMARY KEY (company_id, role_name) ) ``` **`company_agents` 不能用 `org_graph_nodes` 顶替**:后者是**执行流图**的节点(带 `reports_to_node_id` / 出入边 / `authored_by`),而 P3B 明确规定 work 层本体被雇时**不写** `org_graph_nodes`(`company-service.decideOrgNodeRecording`)—— org graph 天生覆盖不全「这家公司雇了谁」。雇佣关系需要独立成员表,`org_graph_nodes` 继续只管执行流。 `company.json.agents[]` 废弃,读取方改查 `company_agents`。 ### 4.3 执行时的装配 `PromptBuilder.build()` 从三段扩到五段: ``` protocol(按 agent.type,现有逻辑不变) system/agents/{aid}/agent.md ← 本体(共享,evolve 一次全公司生效) system/agents/{aid}/{cid}/company-context.md ← 现有 companyContextPath 字段,只是换了路径 system/agents/{aid}/{cid}/user.md memory 注入(memory-service 现有逻辑,路径改为 {aid}/{cid}/memory/) + 运行时坐标声明(三行,第三行「交付物写哪」按链路分叉,见 §4.4) ``` CLI 启动参数:`cwd = system/agents/{aid}/{cid}/{taskId}/`。 ### 4.4 输出契约:cwd 只留痕迹,交付物归宿**按链路分叉** > **修订(二轮)**:本节 v0.1 写的是「所有链路统一把交付物写到 `{companyRoot}/workspace/`」。那是**过度一般化**,与 graph 层两条既有契约直接冲突:`graph-engine.describeWriteBoundary` 告诉节点「你只被允许写 ① 项目根 ② 节点私有目录,禁止写这两处之外的任何路径,尤其是 REPO_ROOT」,而 `{companyRoot}/workspace/` 正在 REPO_ROOT 下、既非项目根也非节点私有目录;`indexNodeOutputs` 又只扫 `nodeRun.output_ref`(= `runs/{rootRunId}/nodes/{nodeRunId}/`)。两段文本进的是同一个提示词(`graph-engine → agent-manager.callAgent → promptBuilder.build`),于是 agent 听坐标声明就产物进不了索引、还可能被边界守卫判越界,听写入边界则坐标声明那行是废话 —— **两种都不报错**。现按链路分叉修正。 共同的部分不变:agent 的 cwd 在 `system/agents/` 下,交付物**不**放 cwd —— 那里只有草稿和日志、会被清理。所以坐标声明必须给绝对路径,否则 agent 会顺手写在 cwd 里,产物散落到 agent 私有目录、别的 agent 找不到。 两条链路各自的坐标声明: ``` # 两条链路都有(前两行恒定) - 你的工作目录(cwd): —— 只放草稿和日志,会被清理 - 你的本体文件:(相对量由 path.relative 实算) —— 只读,不要改 # 差异只在第三行 [graph 节点链路] - 你的交付物:按任务描述里的「[写入边界 …]」写(项目根 / 节点私有目录),本节不另给归宿 [agent-chat 链路] - 你的交付物必须写到:/workspace/ —— 同事共享,会被索引 ``` - **graph 节点链路**(`graph-engine` 发起,任务描述里带 `describeWriteBoundary` 那段):归宿只有一份,就是写入边界段里的项目根 / 节点私有目录 —— 那也是 `indexNodeOutputs` 唯一会扫的地方。坐标声明**只指路、不给第二个归宿**。 - **agent-chat 链路**(人直接对话,没有节点也没有项目根):交付物落 `{companyRoot}/workspace/`。这是 `agent-chat.js:297-308` 早于本次改造就有的既有约定(「你写入的所有文件……都必须放在 `data/companies/{cid}/workspace/` 目录下……你的同事共享同一个目录」),`graph_files.rel_path` 也一直是**相对公司根**存的(`work-graph-db.js:178`)。 判据显式命名为 `prompt-builder.taskCarriesGraphWriteBoundary(userTask)`:任务描述里在不在场那个写入边界段头。之所以不用「有没有 nodeRunId」——nodeRunId 只到 `callAgent` 的 metadata 为止,进不了 `PromptBuilder.build(name, task)` 的入参;而这里要防的本来就是「同一段提示词里出现第二份写入归宿」,按文本在不在场判断是直接命中。段头字符串 `GRAPH_WRITE_BOUNDARY_HEADER` 由 `prompt-builder` 单点定义、`graph-engine` 引用,两侧共用同一个常量(一旦漂移,症状就是分叉静默失效)。 所有路径由 `agent-locator` + `company-paths` 统一算出,写侧(登记 `graph_files`)和读侧(graph-engine 拼提示词)用同一份实现 —— `goal-contract.js` 的注释已经踩过一次「两份逐字同构的约定失效」的坑,本次不再新增第三份。锁在 `server/tests/unit/prompt-write-boundary.test.js`(两条链路各自的归宿指令必须**恰好一条**)。 **边界守卫的连带影响**:`data/companies/` 整体在 `.gitignore` 里,`system/agents/{aid}/{cid}/` 按 §5.5 加入 ignore —— 两个可写根都不进 `git status`,守卫看到的仍是干净仓库。这也是「产物不搬进 system/」的另一个理由:搬过去等于把一批本来就 ignore 掉的文件挪进源码树再重新 ignore 一遍,白折腾且多一次踩雷机会。 --- ## 五、已定决策及其派生约束 ### 5.1 决策:run 目录下沉到本体目录内(已定) `system/agents/{aid}/{cid}/{taskId}/`,而不是另起 `system/runs/` 树。第三层用 **`taskId`(不是 `nodeRunId`)**:同一个 task 内同一个 agent 被多次调用(retry、或图里出现两次)共享同一个工作目录,上下文连续、retry 能读到上一轮的中间产物;调用维度的区分放在 `logs/{nodeRunId}.log` 和 `result.json` 的历史里。 **扫描污染的处理**:`system/agents/*` 的每个子目录都被当成一个 agent id 扫描 —— `agent-chat.js:1008` `scanAgentDir()`、`heartbeat-service.start()`、`agent-manager.js:194` 的 scoped skill 遍历、`graph-seed-service.seedGlobalAgents()`。本布局把公司目录放在**深度 2**,这四处只看深度 1,因此**不产生新的顶层污染**。需要处理的是两件小事: 1. 清理已有的顶层污染 `system/agents/templates/`(里面装着 2 个 agent,提到顶层)。 2. 保留 id 前缀约定:`_base` 等下划线开头的目录不视为可调用 agent,四处扫描统一加 `!name.startsWith('_')` 过滤。 **新增的命名冲突面(深度 2)**:公司 id 会和本体自带的子目录同层。冲突集合是固定的,**实施时按「扫每个本体目录下真实存在的子目录」重算过一遍**,比设计稿多两个(`code` / `system` —— `scrapling_crawler`、`weibo-scraper` 这类带代码的本体带来的),`.claude` 则由下划线 / 点前缀规则覆盖、不必单列: ``` agent_data code conversations data docs knowledge logs memory system tasks train uploads workspace ``` → `company-service.create()` 增加 companyId 校验:不得命中上述保留字、不得以 `_` 或 `.` 开头。现有 7 家公司(`co-*` / `mycompany` / `idearun` / `c-test` / `p2-verify` / `diag-co-*`)全部安全。名单以 `agent-locator.RESERVED_SUBDIRS` 为准。 **全局调用(无公司)怎么办**:`master`、`agent-creator`、`innovator-*` 这些不属于任何公司的 agent,用保留公司 id `_global` —— `system/agents/master/_global/{taskId}/`。这样「有没有公司」不再有两条代码路径。 ### 5.2 决策:相对路径引用不再是阻塞项(已定,但不是零成本) 依据是 §2.6 的两个实测事实:提示词根本不加载 `soul.md`/`identity.md`/`tools.md`,且全仓只有 **5 份** `agent.md` 写了 cwd 相对引用。 新布局下深度恒为 3,所以修法是**一次机械改写**,不需要模板重构: ``` soul.md → ../../soul.md ../../workspace/innovator/x.md → ../../../../../workspace/innovator/x.md ``` 外加 §4.3 那行运行时坐标声明兜底(agent 拿到绝对路径,改写漏了也能自己找到)。 > **深度对照表**(v0.3 曾把「本体」写成 `../../../`,多数一级 —— 那个值落在 `system/agents/` 本身: > 目录存在、可列、但没有 soul.md,**读不到不报错**,正是本改造最怕的静默降级。 > 实现与测试用的是下表的值,以本表为准): > > | 从 cwd `system/agents/{aid}/{cid}/{taskId}/` 出发 | 相对路径 | 层数 | > |---|---|---:| > | 本体目录 `system/agents/{aid}/` | `../../` | 2 | > | 公司态目录 `system/agents/{aid}/{cid}/` | `../` | 1 | > | `system/agents/` | `../../../` | 3 | > | `system/` | `../../../../` | 4 | > | 仓库根 | `../../../../../` | 5 | > > 但**首选做法始终是装配期填绝对路径**,而不是让 agent 自己数 `../`(见 §4.4 的坐标声明)。 > 这条相比 v0.1 的评估是实质性降级:原方案(run 树独立)下深度不定,只能靠「每次开跑复制一份本体副本」兜底;新布局下深度恒定,5 个文件 sed 即可,不产生任何副本。 ### 5.3 决策:`company_agents` 是新表(已定) 见 §4.2 的理由。 ### 5.4 决策:产物留在公司侧(已定) `data/companies/{cid}/runs/`、`workspace/`、`tasks/`、`projects/` 全部原地不动。`system/agents/{aid}/{cid}/{taskId}/` 里**不设** `workspace/` 和 `artifacts/`,避免出现「两个都像是产物目录」的歧义。具体的坐标声明与理由见 §4.4。 > 这条同时把 `data/companies/{id}/` 的瘦身范围收窄成一句话:**只删 `agents/` 这一个子目录**,其余全留。 ### 5.5 派生约束:gitignore 必须先行,否则边界守卫会判死每一个节点 ⚠️ 这是本次改造的**头号硬阻塞**,来自 §2.7。(gitignore 只是两道防线中的第一道,第二道见 §5.5b —— 当时把它写成「唯一」是漏算了「新建本体」那一类。) 现状 `.gitignore` 已经在**深度 2** 上枚举了运行态目录: ```gitignore system/agents/*/conversations/ system/agents/*/logs/ system/agents/*/workspace/ system/agents/*/uploads/ system/agents/*/memory/ system/agents/*/todo.md system/agents/*/plan.md system/agents/*/tasks/ ``` 新布局下深度 2 变成**任意公司 id**,枚举法失效。改成白名单式: ```gitignore # 本体目录下,除 .claude 外的所有子目录都是运行态(公司态 / 任务态) system/agents/*/*/ !system/agents/*/.claude/ system/agents/*/.claude/* !system/agents/*/.claude/skills/ !system/agents/*/.claude/skills/** # 本体根上的 agent 自写文件(保留现有规则) system/agents/*/todo.md system/agents/*/plan.md ``` 这套写法本仓已经在 `.claude` 上用过一次(现有 gitignore 末段),语义可组合。副作用是顺手删掉 8 行枚举 + 整段 `system/templates/*` 规则(§6 批次 1 后该目录不存在)。 > **实际落地时多了一组例外**:`train/`(原 `system/templates/*/train/*` 那组规则,随批次 1 的目录一起搬过来),外加几个本体自带的 `docs/` `code/` `agent_data/` 白名单反选。上面这段是骨架,**以 `.gitignore` 现文为准**。 **验证方式**:改完 gitignore 后跑一次真 agent 调用,`git status --porcelain` **零增量**(跑前跑后行数一致)。原文写的是「必须为空」—— 那在这个仓库里从来不成立:工作区本身就带着 28 改造的未提交改动。守卫比的也是**前后差集**,不是「快照为空」,所以零增量才是与实现同构的判据。这就是 §8 验收 3 的前置。 **但 gitignore 只是两道防线里的一道,另一道在守卫侧,两道不重叠**(实施时按这条改了,见下)。 #### 5.5b 守卫侧:豁免从「五个文件名」放宽到整棵注册表树(已实施) ```js // server/services/graph-engine.js const AGENT_REGISTRY_TREE_RE = /^system\/agents\//; // 旧名 AGENT_RUNTIME_STATE_RE,语义已换 ``` 改名不是美化,是**语义换了**: - **旧语义**「豁免本体根上的五个运行态文件」(`todo.md` / `plan.md` / `memory.md` / `_lessons.md` / `.claude/scheduled_tasks.json`)。那份名单是按**旧落点**裁剪的 —— 改造前 agent 只会在自己**既有**的本体目录里改那么几个文件。 - **新语义**「`system/agents/` 是 **agent 注册表**,不是纯源码树」。运行期**合法地**写这棵树的有三方:① `hireFromBaseMetaAgent` 从 `_base` 复制骨架、新建整个 `system/agents/{newId}/`;② agent-creator 往里写 `agent.md` / `soul.md` / `SKILL.md`;③ evolver 迭代已有本体。判罚留给真正的源码目录:`server/` `web/` `packages/`。 **为什么非放宽不可(gitignore 接不住的那一类)**:28 之后新本体的落点从 `data/companies/{cid}/agents/`(整个目录在 `.gitignore` 里,天然不进 `git status`)搬到了**被 git 跟踪的** `system/agents/{id}/`。于是运行期新建的目录会以 ``` ?? system/agents/{newId}/ ``` **一整条目录项**的形态进 `git status --porcelain` —— 五个文件名的名单接不住一条目录项,它落进 escapes,触发招聘的那个节点就被降级成 fail + `violation:'boundary_escape'`,正是 §九 风险表第一行点名的症状:**agent 明明成功却 fail,且归因指向错误的节点**。 而 gitignore **永远**挡不住这一类:新本体的骨架本身就是要进版本库的(这正是 G4 的目的 —— 产物立刻成为全系统可复用的本体),把它 ignore 掉等于取消改造。所以两道防线的分工是固定的:**gitignore 管「运行态不该进 git」,守卫管「注册表树里的合法写入不判罚」**,谁也替代不了谁。 归进 `unattributable` 桶 = **保留证据、放弃判罚**(timeline 照记 `node.boundary`,不参与降级),与该桶已有的语义一致 —— 守卫本来就无法把这类改动归属到当前节点:同期可能有别的招聘在跑,整仓 `git status` 的差集里根本没有「谁写的」这一维。 > 上限没变:同一次调用里 agent 若在**注册表树之外**写了源码,仍算当前节点头上。 ### 5.6 派生约束:任务态的保留策略 `system/agents/{aid}/{cid}/{taskId}/` 在源码树里无限增长。需要一条清理策略(可以放到后续阶段,但设计上要留位): - 建议 `graph_runs` 终态后 N 天清理任务目录,`artifacts/` 先归档到 `graph_files` 记录的位置; - 公司态目录(`{aid}/{cid}/`)永不自动清理,只在 `company_agents` 行被 fire 时软删。 #### 5.6b 公司态是 **dream** 的原料 —— 不许被当垃圾扫掉 ⚠️ 上面两条的不对称(任务态可清 / 公司态不可清)不是保守,是**有下游依赖**的: > ⚠️ **以下关于 dream 的描述属于「方向已定、设计未定」,不是已拍板的设计。** > 用户口述了它要解决什么问题和大致形态,但触发时机、提炼规则、去敏策略、产物落点**都还没有设计**。 > 本节写它,只是为了锁住一条**已经生效的约束**(公司态不许被清),不构成 dream 的实现依据。 > 真要实现时请另开设计文档,别照本节的措辞去写代码。未确认项清单见 [28-followups](../../todo/system-design/28-followups.md)。 **dream**(暂定名)是设想中定时跑的另一套逻辑,它会去整理**所有 agent 在所有公司**的 history 与 memory,**显式提炼**后沉淀到 skill memory 上。这是「经验汇流」的正解 —— 与之相对的错误版本是「每轮对话自动把内容写进共享 `SKILL.md`」:方向对,但直接灌原始对话等于把 A 公司的机密喂给 B 公司。**沉淀必须是提炼后的抽象,不是自动搬运原文。** 于是: - **任务态**(`{aid}/{cid}/{taskId}/`)是草稿和日志,清掉不影响 dream; - **公司态**(`{aid}/{cid}/` 下的 `memory/` / `conversations/`)是 dream 的**唯一原料**。任何一次「磁盘清理」「按时间扫 system/ 下的大目录」把它当垃圾扫了,损失的不是缓存而是**这个 agent 全部的经验历史**,且不可再生。写清理脚本的人请先读这一节。 **当前布局对 dream 是友好的,这也是 §5.1「下沉到本体目录内、不另起 `system/runs/` 树」的一个额外收益**:公司态挂在 agent 底下,所以 ``` 某个 agent 在所有公司的全部记忆 == glob: system/agents/{aid}/*/memory/ ``` 一个 glob 就够,不用跨树 join。如果当初把运行态放到另一棵 `system/runs/{cid}/{aid}/` 树上,同样的问题就变成「先枚举所有公司、再逐公司拼路径」——dream 每跑一次都要重建一次 agent ⇄ 公司的映射。**目录布局在这里恰好等于 dream 的主查询索引,别改。** ### 5.7 已裁决:7 个同名 id(本文档最后一个未定项,已收) 不能自动化,逐个决定了权威版本,**落败的一份不删除、只归档**到 `docs/_archive/agent-merge-2026-08-17/`(连同 §5.1 提到的 `system/agents/templates/` 里那 2 个,共 9 份),保留可回溯的原文: | id | 权威版本 | 结果 | |----|----------|------| | `agent-creator` | agents 版(在跑的本体) | templates 版归档 | | `org-innovator` | agents 版 | templates 版归档 | | `innovator-{digest,evolver,hunt}` | agents 版 | templates 版归档 | | `evolver-youtube-crawler` | agents 版(diff 后确认更全) | templates 版归档 | | `youtube-crawler` | **templates 版**(agents 版只有 `.claude/`、无 agent.md) | templates 版就位,agents 版的 `.claude/` 并入 | 现状:`system/templates/` 目录已不存在,`system/agents/` 下每个 id 只剩一份权威本体。 --- ## 六、改造清单 按可独立提交的批次排。每批结束系统都应该能跑。 > **状态(2026-08-18):批次 0-5 全部已实施**,并已真 CLI E2E 跑通(§十一)。下列条目保留原文当改造轨迹;与最终实现有出入的地方已就地标注(§5.1 的保留字名单、§5.7 的裁决结果、批次 1-5 的 `legacy_path` 留列)。 ### 批次 0:gitignore 先行(§5.5 的硬阻塞) 0. 按 §5.5 改写 `.gitignore` 为白名单式;删除 `system/templates/*` 相关规则的时机放到批次 1 之后。 0b. 加一个最小验证:起一次 agent 调用 → `git status --porcelain` 为空。 ### 批次 1:本体合并(G1)—— 不动运行时 1. 裁决 §5.7 的 7 个冲突;`system/templates/*` 其余 43 个目录 `git mv` 到 `system/agents/`。 2. `system/templates/base` → `system/agents/_base`;清理 `system/agents/templates/`(2 个 agent 提到顶层)。 3. 补齐 §2.3 的残缺本体:`ceo`(当前只有 plan/todo)、`devloop-*`(只有 prompt.md,需决定它是否等价于 agent.md)、`screenwriter`。 4. `graph-seed-service.js`:`seedGlobalAgents()` + `seedTemplates()` 合并为 `seedAgents()`;`source_kind` → `origin`;`verifyCounts()` 同步改;扫描加 `_` 前缀过滤。 5. `graph-db.js` migrate:`system_agents` 加 `origin` 列并回填(`template`/`global_agent` → `builtin`,`company_hired` → `created`),`legacy_path` 保留一版兼容后删。 > **实际**:`origin` 已加并回填完(实测 `builtin` 54 / `created` 2);`source_kind` 与 `legacy_path` **两列仍在**、写侧仍在填,读侧已基本改走 id 推导 —— 唯一还真读 `legacy_path` 的是 `graph-seed-service` 的两处「这行指着一个早已不存在的目录吗」判据(存量行里留着批次 4 之前写下的公司侧路径,正是要认出来的东西)。删列因此**后置**,不在本轮。 6. `graph-service.lookupSharedAgentMd()` 去掉 `source_kind='template'` 过滤,路径改为 `system/agents/{id}/agent.md`。 **这批做完,§2.2 的 7 个冲突消失,`hire()` 的引用模式命中率从 8/17 升到接近全量。** ### 批次 2:寻址收编 7. 新增 `server/services/agent-locator.js`,统一导出: - `bodyDir(agentId)` → `system/agents/{aid}` - `companyDir(agentId, companyId)` → `system/agents/{aid}/{cid}`(`companyId` 缺省 `_global`) - `taskDir(agentId, companyId, taskId)` → `system/agents/{aid}/{cid}/{taskId}` - `parseAddress(addr)` / `formatAddress(cid, aid)` - `RESERVED_SUBDIRS` / `isReservedCompanyId()`(§5.1 的校验) 8. §2.5 的 6 处 `getAgentDir` / `resolveAgentName` 全部改调 `agent-locator`。`graph-engine.js:624` 的「靠目录存在性判断」删除 —— 地址恒为 `{cid}:{aid}`,`aid` 恒指向本体。 9. `company-service.create()` 接入 `isReservedCompanyId()` 校验。 ### 批次 3:运行态下沉(G2) 10. `agent-manager.js:180-190`:`prompt` 指本体 `agent.md`;`workDir` 改为 `taskDir(...)`;`companyContextPath` 改为 `companyDir(...)/company-context.md`;新增 `companyDir` 字段供 memory 注入用。 11. `prompt-builder.js:159`:按 §4.3 扩到五段,加运行时坐标声明。 12. `agent-manager.js:321` `effectiveWorkDir` + `sessionPool` 的 key:key 必须含 `(cid, taskId)`,否则两家公司调同一 agent 会复用同一个 CLI session。**本批最容易漏的一处。** 13. `memory-service.js` / `agent-chat.js` 的 conversations 落点改到 `companyDir`。 14. 按 §5.2 机械改写 5 份 `agent.md` 的相对引用。 15. **`graph-run-sse.js` / `goal-contract.js` / `company-paths.js` 一行不改** —— 按 §5.4,root run 产物目录、公司 workspace、task 产物目录全部原地不动。本批只改「agent 在哪里跑」,不改「产物写到哪里」。 16. 按 §4.4 把三行坐标声明接进提示词装配;同时校对 `agent-chat.js:297-308` 那段已有的公司 workspace 声明,让两处走同一份 `company-paths` 推导(现在是各写各的字符串)。 ### 批次 4:公司实体清空(G3) 17. `graph-db.js`:建 `company_agents` 表,从 `company.json.agents[]` + `org_graph_nodes` 回填。 18. `company-service.hire()`:删掉 `_copyTemplate` / `_provisionReferenceAgent` 两条分支,改为「查本体 → 写 `company_agents` 一行 → 初始化 `system/agents/{aid}/{cid}/`(写 company-context.md + user.md + 空 memory)」。 19. `company.json.agents[]` 的读取方改查 `company_agents`:`company.js` 的 9 处 `DATA_DIR`、`task-service.js:29`、`agent-chat.js:1008`、`heartbeat-service.start()`。 20. `agent-transcript.js:33` 的路径推导重写(其注释里的前提已失效)。 ### 批次 5:create-agent 归位(G4) 21. 四个 `new AgentScaffold(...)` 的 baseDir 统一为 `system/agents`(`agent-chat.js:139`、`agent-chat.js:1415`、`company.js:226`、`company-service.js:500`)。 22. `hireFromBaseMetaAgent()` 重写: ``` system/agents/_base → system/agents/{newId} (复制骨架) → system_agents 插一行 origin='created', status='draft' → 触发 agent-creator 填 agent.md / soul.md / SKILL.md → 校验齐全 → status='active' → 写 company_agents 一行 ``` 产物立刻是全系统可复用的本体 —— §2.4 的 `org-coder` / `org-planner` 死角从此不再产生。 23. agent-creator 的产出契约明确化:必须写满 `agent.md` + `soul.md` + `.claude/skills/main_skill/SKILL.md`,缺一则本体停在 `status='draft'`,不允许被 hire。(现状是 fire-and-forget,填不填没人验,`org-coder` 就是这么变成空定义的。) --- ## 七、存量数据迁移 一次性脚本 `scripts/migrate-agent-registry.js`,幂等,分三步: **M1 本体归位**(批次 1):`git mv` + §5.7 冲突裁决。纯代码库操作,可 review。 **M2 公司实例拆解**(17 个目录): | 类型 | 数量 | 处理 | |------|-----:|------| | REF(无 agent.md,有 `systemAgentRef`) | 8 | `company-context.md` + `user.md` + `memory/` → `system/agents/{aid}/{cid}/`;`company_agents` 插一行;删原目录 | | COPY 且本体已存在(idearun 6 + p2-verify 1) | 7 | 同上,但**先 diff** 私有 `agent.md` 与本体:无差异直接丢弃;有差异(运行中被 agent 自己改过)落到 `system/agents/{aid}/{cid}/agent.md.local` 并在迁移报告列出,人工裁决 | | COPY 且无本体(`org-coder`, `org-planner`) | 2 | **毕业**:本体文件搬到 `system/agents/{id}/`,私有状态搬到 `system/agents/{id}/mycompany/`;`system_agents` 行改 `origin='created'` | **M3 历史 run 搬迁**:`data/companies/{cid}/runs/` 保持原地只读留档(历史 run 没有 agent 维度,硬塞进新布局没有意义)。 **回滚**:M1/M2 全程 `git mv` + 原目录快照到 `data/_migration-backup-/`;DB 只加列不删列,回滚 = 恢复目录 + 忽略新列。 > **已执行(2026-08-18)**:`scripts/migrate-agent-registry.js` 跑完,M1 / M2 全部落地 —— > `system/templates/` 已不存在,`data/companies/*/agents/` 全部消失,快照留在 > `data/_migration-backup-2026-08-17/`(配合 §5.7 的 `docs/_archive/agent-merge-2026-08-17/`, > 落败副本与原目录两条回溯线都在)。M3 按原计划没动历史 run。 --- ## 八、验收标准 | # | 标准 | 2026-08-18 实测 | |--:|------|-----------------| | 1 | `ls data/companies/*/agents/` 为空;`system/templates/` 不存在 | ✅ 两者都不存在(`e2e-demo` 建完只有 `company.json` / `docs` / `logs` / `okr` / `pipelines` / `workspace`) | | 2 | `system_agents.origin` 只有 `builtin` / `created` 两个值,`legacy_path` 已删列 | ⚠️ **一半**:`origin` 已到位(`builtin` 54 / `created` 2),但 `source_kind` / `legacy_path` **两列仍在**,删列后置(理由见批次 1-5 的实际注记) | | 3 | **跑一次真 agent 调用后 `git status --porcelain` 为空**(§5.5 的探针,也是批次 3 能不能上线的判据) | ✅ 按**零增量**验收:E2E 前后都是 864 行,招聘写出的新本体目录没有把节点判 fail(原文的「为空」在这个仓库里从来不成立 —— 工作区本来就带着 28 的未提交改动) | | 4 | 同一 `agent-id` 被两家公司**并发**调用:各自 cwd 不同、两边 memory 不串、CLI session 不复用 | ✅ `agent-manager-concurrent-scope.test.js` + `session-pool-scope-key.test.js` + `agent-memory-landing.test.js` | | 5 | 改一次 `system/agents/qa-judge/agent.md`,两家公司下一次调用都读到新版 —— 无需任何同步动作 | ✅ 结构上恒成立(本体只有一份,公司态里不再有 agent.md 副本);`memory-body-readonly.test.js` 锁住「公司态不得写回本体」 | | 6 | hire 一个 `system/agents/` 下不存在的角色 → `system/agents/{新id}/` 出现完整本体(agent.md + soul.md + SKILL.md 齐全,`status='active'`),且第二家公司可直接 hire 同一 id 复用 | 🟡 **后半在真链路上兑现、前半本轮未触发**:E2E 三个角色全部命中已有本体(`system_agents` 56→56,零新建),走的正是「第二家公司直接复用」。「新建本体」这条路径本轮只有单测覆盖 | | 7 | `grep -rn "data/companies/.*agents" server/` 零命中 | ✅ 代码零命中;剩下的 15 处全在**注释**里记「这是批次 4 之前的旧落点」,属于有意保留的考古线索 | | 8 | `npm run test:unit` 全绿;`graph-agent-p1-*.test.js` 按新形态重写(原断言全部基于「公司私有目录」,会整体失效) | ✅ 全绿(写本节时 `npx vitest run` = 108 文件 / 995 例,无失败;套件总数还在随其它并行改动往上走,以当次实跑为准)。P1 的集成文件已按新形态重写 —— 原文写「三个文件」是记错了,实际只有 `graph-agent-p1-acceptance` / `graph-agent-p1-hire` 两个 | | 9 | 跑完一个真实 work graph 后,`system/agents/*/*/*/` 下只有 `logs/` `scratch/` `result.json`;所有交付物都能在 `data/companies/{cid}/` 下找到,且 `graph_files` 里每条 `rel_path` 拼出的绝对路径都真实存在 | 🟡 **待完整 work graph 真跑**:本轮 E2E 停在 org 层(规划 + 招聘),还没跑到会大量产出交付物的 work 节点 | --- ## 九、风险 | 风险 | 影响 | 缓解 | |------|------|------| | gitignore 没做对(§5.5) | **每个节点被边界守卫判 fail**,症状是「agent 明明成功却 fail」,且归因指向错误节点 | 批次 0 先行;验收 3 是硬门。**已发生并已修**:新建本体(`?? system/agents/{newId}/`)是 gitignore 原理上挡不住的一类,靠 §5.5b 的守卫豁免放宽兜住 | | sessionPool key 漏改(批次 3-12) | 两家公司串 CLI 会话、串上下文 | 验收 4 覆盖 | | 5 份 agent.md 的相对引用漏改 | agent 读不到 soul.md,**静默降级**成通用 Base Agent 文本、不报错 | §4.3 的运行时坐标声明兜底;改写清单固定 5 份,可穷举 | | 公司 id 撞保留字(§5.1) | 公司态目录覆盖本体子目录 | 批次 2-9 的 `isReservedCompanyId()` 校验;现有 7 家公司已验证安全 | | Agent 把交付物写在 cwd 里 | 产物散落进 `system/agents/{aid}/{cid}/{taskId}/`,同事读不到、`graph_files` 索引指向空气且不报错 | §4.4 的坐标声明(第三行按链路分叉,两条链路各只给**一个**归宿);`{taskId}/` 下**不建** `workspace/` 目录,让「写在 cwd」看起来就不像交付;验收 9 覆盖 | | 7 个同名 id 裁错版本 | 某 agent 行为回退到旧版本 | 迁移脚本对每对冲突输出 diff 到报告,人工确认后执行 | | `origin='created'` 本体质量不可控 | 空定义 agent 被别家公司 hire,污染面从 1 家扩到全系统 | 批次 5-23 的 `status='draft'` 闸门 | | 任务目录在源码树内无限增长 | 磁盘 / `git status` 变慢 | §5.6 的保留策略(可后置,但设计留位) | | 清理策略把**公司态**一起扫了 | dream 的原料被删,agent 的跨公司经验**不可再生**地丢失 | §5.6b:只清任务态;公司态仅在 fire 时软删。写清理脚本前必须读 §5.6b | --- ## 十、与 27 号系列的关系 - **P0**(`system_agents` / `org_graph_nodes` 地基):保留,本次改的是 `source_kind` 语义和 `legacy_path` 的存在必要性。 - **P1**(虚拟引用):其「运行时装配公共定义 + 私有语境」的机制(`systemAgentRef` → `companyContextPath` → PromptBuilder 拼接)被**完整继承并扩展**,本次把适用范围从「命中模板的 agent.md」扩到「所有 agent 的全部本体文件」。P1 §3.3 的 workDir 约束因运行态下沉而解除(§三)。 - **P3B W13**(把 agent 运行态文件移出 git):本次是它的**根治版**。W13 当时只做到「守卫不冤枉节点」的缓解,注释里写的「根治要把这些运行态文件从 git 移出去」正是批次 0 干的事。 反过来 W13 那条豁免也被本次改写了:**它当年的名单接不住 28 之后的新落点** —— 旧名 `AGENT_RUNTIME_STATE_RE`(本体根上五个固定文件名)已放宽成 `AGENT_REGISTRY_TREE_RE`(整棵注册表树),见 §5.5b。两处文档(27-P3B、27-P4)已同步。 - **P3/P3B**(work graph):`work_graph_nodes.system_agent_id` 已是引用制,零影响。批次 4 需确认 `ensureSystemAgentRow()` 的 fallback 插入在新 `origin` 语义下仍幂等。 --- ## 十一、实施结果(2026-08-18,真 Claude CLI E2E) 批次 0-5 全部落地后,用**真 Claude CLI**(不是 mock)跑了一条端到端链路: **建公司 `e2e-demo` → `orggraph-planning` 自动规划 org graph → 按规划自动招聘 → 三节点流水线** (`org-planner` → `org-coder` → `org-validator`)。 ### 11.1 实测数据 | 观测点 | 值 | 说明 | |--------|----|------| | `data/companies/e2e-demo/agents/` | **不存在** | G3 兑现:公司侧一个 agent 字节都没落。目录里只有 `company.json` / `docs` / `logs` / `okr` / `pipelines` / `workspace` | | `company_agents`(e2e-demo) | **4 行** | `orggraph-planning` + `org-planner` / `org-coder` / `org-validator`,全部 `status='active'`。雇佣关系退化成关联表 | | `system/agents/{角色}/e2e-demo/` | **4 个** | 公司态目录按 agent 下沉,与上面 4 行一一对应 | | `system_agents` | **56 → 56** | **零新建私有本体**:四个角色全部命中共享注册表里已有的本体,纯复用 | | `git status --porcelain` 行数 | **864 → 864** | 零增量。边界守卫全程没有冤杀任何节点 | ### 11.2 最该记一笔的一条:§2.4 的死角被真实复用了 `org-coder` / `org-planner` 正是 §2.4 点名的那两个 **「永远无法被第二家公司复用」** 的死角 —— 改造前它们是 `source_kind='company_hired'`、`legacy_path` 指回 `mycompany` 自己的目录, 只对 `mycompany` 存在,别的公司要用只能重新走 `hireFromBaseMetaAgent` 再造一个空定义副本。 这次一家**全新公司**(`e2e-demo`)从共享注册表里把它们**直接雇走**, `system_agents` 一行没长、`data/companies/e2e-demo/agents/` 一个字节没写。 **改造的核心目标在真实链路上兑现**:本体只有一份,公司只持有引用,复用是默认路径而不是例外。 这也直接支撑 §5.6b 的 dream —— 越多公司复用同一个本体, `system/agents/{aid}/*/memory/` 这个 glob 底下积累的跨公司经验就越厚。 ### 11.3 边界守卫:这次没冤杀,是因为判据先改对了 说清楚因果,别把运气记成能力:**这一轮四个角色全是复用,`hireFromBaseMetaAgent` 根本没触发**, 所以 864 → 864 里没有「新建本体」这一项。守卫豁免是在跑之前就按 §5.5b 放宽好的 (旧名 `AGENT_RUNTIME_STATE_RE` 的五文件名名单 → `AGENT_REGISTRY_TREE_RE = /^system\/agents\//`), **它接住的是下一次**:只要有一个角色在注册表里查不到,落点就会以 `?? system/agents/{newId}/` 一整条目录项出现,旧判据会把那个**成功**的招聘节点判成 fail。 本轮的零增量因此只证明了「运行态没漏进 git」,没有替「新建本体不被冤杀」作证 —— 后者见 §11.4。 **文档与代码的一致性检查**(去掉任一处改名同步就红): ```bash # 1) 三份文档必须引用当前常量名;2) 旧名只能以「旧名」标注的形式出现 # 只认 const 声明行,否则会把注释里提到的旧名也捞进来 name=$(grep -oE '^const AGENT_[A-Z_]+_RE' server/services/graph-engine.js | sed 's/^const //' | sort -u) for f in docs/done/system-design/28-agent-registry-unification.md \ docs/done/system-design/27-p3b-real-run-runway.md \ docs/done/system-design/27-p4-parallel-and-artifacts.md; do grep -q "$name" "$f" || { echo "MISS $name in $f"; exit 1; } done grep -rn 'AGENT_RUNTIME_STATE_RE' docs/todo/system-design/*.md docs/done/system-design/28-*.md \ docs/done/system-design/27-p3b-real-run-runway.md docs/done/system-design/27-p4-parallel-and-artifacts.md \ | grep -v '旧名' && { echo 'STALE: 旧名未标注'; exit 1; } echo OK ``` ### 11.4 本轮**未**覆盖到的(别把「已实施」读成「全验完」) - **新建本体那条路径**(验收 6 前半):E2E 四个角色全部命中已有本体,`hireFromBaseMetaAgent` → agent-creator → `status='draft'→'active'` 这条链只有单测覆盖,没有真 CLI 证据。 - **完整 work graph 真跑**(验收 9):本轮停在 org 层,交付物归宿(§4.4 的按链路分叉)还没被大批量产物压过。 - **`source_kind` / `legacy_path` 删列**(验收 2 后半):两列仍在,见批次 1-5 的实际注记。 - **任务态清理策略**(§5.6):仍然只有设计、没有实现。实现时**必须**先读 §5.6b —— 公司态是 dream 的原料,不是缓存。 ### 11.4b 收尾:清掉活定义里所有已死的 `system/templates` 引用(`dd319bb`) 批次 1 把目录并走之后,**目录本身在磁盘和 git 里都已不存在**(`git ls-files system/templates` 返回空), 但引用还散在各处。这一轮把「agent 运行时真会照着读写的那些」全部清掉: | 位置 | 处理 | 不清会怎样 | |------|------|-----------| | 四份本体提示词(`workgraph-planning` / `orggraph-planning` 的 `agent.md`+`tools.md`、`org-innovator/agent.md`) | 9 处路径 + 4 条产出落点 | agent 读不到就**静默降级**,不报错 | | `evolver-youtube-crawler` 的三个 skill js | 4 处硬编码目标 | 同上 | | `innovator-hunt/collect_seed.js` | 裸相对路径改为按 `__dirname` 推导 | **真 bug,已发作过**:相对 cwd 解析,在 agent 自己目录下造出一棵野生嵌套树 `system/agents/innovator-hunt/system/templates/…` | | `work-graph-templates` / `org-graph-templates` 的 README | CI 防线三条规则 | 文档写的规则与实际执行的检查对不上(测试早改对了,只有 README 没跟上) | | DevLoop PRD(13 份 HTML → 重生成 52 份 `_spec` + manifest + search-index) | 按真实形状改 | **根和形状都错**:写的是 `system/templates/devloop-pm.md`(扁平 `.md`),而 `loop-controller.js:166` 读的是 `system/agents/{agentName}/prompt.md`(目录 + `prompt.md`) | 最后一行推出一条值得记的方法论: **批量替换字符串时,替换的正确性取决于替换后的整体是否成立,而不是被替换的那一段是否过期。** 无脑把 `templates` 换成 `agents` 会把一个错路径改成另一个错路径。 顺带订正两处**早于本次改造就已过期**的内容: - 「避开 6 个命名冲突 id」列的 `digester` / `evolver` / `hunter` / `innovator` **四个 id 根本不存在**(早已改名), 而它的理由(两个目录下各有一份、内容不同步)也已随合并消失。约束保留(元 agent 本来就不该进执行流), 理由与清单按实际本体重写。 - `orggraph-planning/agent.md` 里「真相源是 `company.json.agents[]`」—— 28 号起真相源是 `company_agents` 表。 **刻意不动的三类**(改了是净损失):① 解释迁移历史的注释、`docs/done/`、`tasks/` 下的执行记录; ② `server/tests/` 里断言该目录已不存在的守卫(删掉等于删掉「迁移已完成」的证明); ③ 未跟踪的 `workspace/` `conversations/` 与 `train/` 下的历次训练报告(那是记录,不是定义)。 新增 `server/tests/unit/no-stale-templates-path.test.js` 守住「活定义」这一档,**零例外**。 判据是「**git 跟踪的才是人工维护的定义,未跟踪的是运行态产物**」—— 这条判据本身就把上面三类自动排除掉了,不需要写脆弱的例外规则。 ### 11.5 交付状态 **改动已全部提交并推送**(`ca468ab..dd319bb`,四个提交)。当时的提交注意事项保留备查:测试过程中在真实 `system/agents/` 下建出来的 agent 本体(`zz-*` 这类,当前工作区里共 **17 个**,如 `zz-tenant-ceo` / `zz-draft-role` / `zz-heartbeat-scoped` …) **是正式注册表条目,一并进版本库** —— 它们不是测试垃圾,而是可被后续任何公司复用的本体,agent 复用得越多、积累的历史与经验越多。 清理只清 company(`data/companies/{cid}/` 与对应的公司态目录),**本体一律留下**。 两条**不要**(都被试过、都被否掉): - **不要**给 `system/agents/` 下测试建出来的本体加 gitignore —— 那等于把注册表的一部分藏起来, 而它们和「正经招聘出来的本体」在结构上没有任何区别,藏一类就等于给守卫和 seed 扫描留一个不可见的例外。 - **不要**给测试另开一个「临时 agents 根目录」绕道 —— 绕过去之后测的就不是真链路了, §5.5b 那条豁免(以及它背后的整个「注册表不是纯源码树」的判断)在测试里永远不会被走到。 底层的产品取向只有一句:**尽量复用 agent**。复用得越多,同一个本体在 `system/agents/{aid}/*/` 底下积累的历史与经验越厚,后面的 evolve / dream 才有料可炼。任何「为了干净而少建本体 / 删本体」的做法都与它相悖。