28. Agent 注册表统一 —— 本体归位 system/agents/、运行态按公司/任务下沉
状态: 已实施 + 已真 CLI E2E 验证(v0.6,已归档到
docs/done/) 未完成与未确认的部分不在本文档 —— 见 28-followups(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 虚拟引用寻址 的收窄方案(P1 只虚拟化了agent.md,其余照旧拷贝) 前置: P0 图地基(system_agents/org_graph_nodes)、P1 引用模式(systemAgentRef+company-context.md)已上线
一、目标
一句话:Agent 本体只有一份,住在 system/agents/<agent-id>/;公司不持有 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/<id>不存在时走 create-agent —— 从元 Agent 模板复制骨架到system/agents/<id>,再由 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')。只有一条窄豁免:
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/<agent-id>/ ← 本体,只读,全系统唯一一份,git 跟踪
agent.md soul.md identity.md tools.md config.json
.claude/skills/main_skill/SKILL.md
system/agents/<agent-id>/<company-id>/ ← 公司态:长期,git ignore
company-context.md ← P1 产物,公司归属语境
user.md ← 汇报关系(本来就是整份重写的私有文件)
memory/ ← memory.md / short-term / cerebellum
conversations/
system/agents/<agent-id>/<company-id>/<task-id>/ ← 任务态: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-id>/
company.json okr/ pipelines/
workspace/ ← 公司共享工作区,agent 交付物落这里
runs/<rootRunId>/artifacts/ ← run 级共享产物区(跨 org node 只读)
tasks/<taskId>/ ← 任务级产物(acceptance.json 等)
projects/<projectId>/ ← 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 数据模型
-- 本体注册表: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):<taskDir> —— 只放草稿和日志,会被清理
- 你的本体文件:<bodyDir>(相对量由 path.relative 实算) —— 只读,不要改
# 差异只在第三行
[graph 节点链路] - 你的交付物:按任务描述里的「[写入边界 …]」写(项目根 / 节点私有目录),本节不另给归宿
[agent-chat 链路] - 你的交付物必须写到:<companyRoot>/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,因此不产生新的顶层污染。需要处理的是两件小事:
- 清理已有的顶层污染
system/agents/templates/(里面装着 2 个 agent,提到顶层)。 - 保留 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 上枚举了运行态目录:
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,枚举法失效。改成白名单式:
# 本体目录下,除 .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 守卫侧:豁免从「五个文件名」放宽到整棵注册表树(已实施)
// 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。
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 的硬阻塞)
- 按 §5.5 改写
.gitignore为白名单式;删除system/templates/*相关规则的时机放到批次 1 之后。 0b. 加一个最小验证:起一次 agent 调用 →git status --porcelain为空。
批次 1:本体合并(G1)—— 不动运行时
- 裁决 §5.7 的 7 个冲突;
system/templates/*其余 43 个目录git mv到system/agents/。 system/templates/base→system/agents/_base;清理system/agents/templates/(2 个 agent 提到顶层)。- 补齐 §2.3 的残缺本体:
ceo(当前只有 plan/todo)、devloop-*(只有 prompt.md,需决定它是否等价于 agent.md)、screenwriter。 graph-seed-service.js:seedGlobalAgents()+seedTemplates()合并为seedAgents();source_kind→origin;verifyCounts()同步改;扫描加_前缀过滤。graph-db.jsmigrate:system_agents加origin列并回填(template/global_agent→builtin,company_hired→created),legacy_path保留一版兼容后删。 > 实际:origin已加并回填完(实测builtin54 /created2);source_kind与legacy_path两列仍在、写侧仍在填,读侧已基本改走 id 推导 —— 唯一还真读legacy_path的是graph-seed-service的两处「这行指着一个早已不存在的目录吗」判据(存量行里留着批次 4 之前写下的公司侧路径,正是要认出来的东西)。删列因此后置,不在本轮。graph-service.lookupSharedAgentMd()去掉source_kind='template'过滤,路径改为system/agents/{id}/agent.md。
这批做完,§2.2 的 7 个冲突消失,hire() 的引用模式命中率从 8/17 升到接近全量。
批次 2:寻址收编
- 新增
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 的校验)
- §2.5 的 6 处
getAgentDir/resolveAgentName全部改调agent-locator。graph-engine.js:624的「靠目录存在性判断」删除 —— 地址恒为{cid}:{aid},aid恒指向本体。 company-service.create()接入isReservedCompanyId()校验。
批次 3:运行态下沉(G2)
agent-manager.js:180-190:prompt指本体agent.md;workDir改为taskDir(...);companyContextPath改为companyDir(...)/company-context.md;新增companyDir字段供 memory 注入用。prompt-builder.js:159:按 §4.3 扩到五段,加运行时坐标声明。agent-manager.js:321effectiveWorkDir+sessionPool的 key:key 必须含(cid, taskId),否则两家公司调同一 agent 会复用同一个 CLI session。本批最容易漏的一处。memory-service.js/agent-chat.js的 conversations 落点改到companyDir。- 按 §5.2 机械改写 5 份
agent.md的相对引用。 graph-run-sse.js/goal-contract.js/company-paths.js一行不改 —— 按 §5.4,root run 产物目录、公司 workspace、task 产物目录全部原地不动。本批只改「agent 在哪里跑」,不改「产物写到哪里」。- 按 §4.4 把三行坐标声明接进提示词装配;同时校对
agent-chat.js:297-308那段已有的公司 workspace 声明,让两处走同一份company-paths推导(现在是各写各的字符串)。
批次 4:公司实体清空(G3)
graph-db.js:建company_agents表,从company.json.agents[]+org_graph_nodes回填。company-service.hire():删掉_copyTemplate/_provisionReferenceAgent两条分支,改为「查本体 → 写company_agents一行 → 初始化system/agents/{aid}/{cid}/(写 company-context.md + user.md + 空 memory)」。company.json.agents[]的读取方改查company_agents:company.js的 9 处DATA_DIR、task-service.js:29、agent-chat.js:1008、heartbeat-service.start()。agent-transcript.js:33的路径推导重写(其注释里的前提已失效)。
批次 5:create-agent 归位(G4)
- 四个
new AgentScaffold(...)的 baseDir 统一为system/agents(agent-chat.js:139、agent-chat.js:1415、company.js:226、company-service.js:500)。 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死角从此不再产生。- 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-<ts>/;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。
文档与代码的一致性检查(去掉任一处改名同步就红):
# 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/todo/system-design/27-p3b-real-run-runway.md \
docs/todo/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 | 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 的原料,不是缓存。
dd319bb">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 才有料可炼。任何「为了干净而少建本体 / 删本体」的做法都与它相悖。