28-agent-registry-unification.md 51.6 KB

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,因此不产生新的顶层污染。需要处理的是两件小事:

  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 上枚举了运行态目录:

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 的硬阻塞)

  1. 按 §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:寻址收编

  1. 新增 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. §2.5 的 6 处 getAgentDir / resolveAgentName 全部改调 agent-locator。graph-engine.js:624 的「靠目录存在性判断」删除 —— 地址恒为 {cid}:{aid},aid 恒指向本体。
  3. company-service.create() 接入 isReservedCompanyId() 校验。

批次 3:运行态下沉(G2)

  1. agent-manager.js:180-190:prompt 指本体 agent.md;workDir 改为 taskDir(...);companyContextPath 改为 companyDir(...)/company-context.md;新增 companyDir 字段供 memory 注入用。
  2. prompt-builder.js:159:按 §4.3 扩到五段,加运行时坐标声明。
  3. agent-manager.js:321 effectiveWorkDir + sessionPool 的 key:key 必须含 (cid, taskId),否则两家公司调同一 agent 会复用同一个 CLI session。本批最容易漏的一处。
  4. memory-service.js / agent-chat.js 的 conversations 落点改到 companyDir。
  5. 按 §5.2 机械改写 5 份 agent.md 的相对引用。
  6. graph-run-sse.js / goal-contract.js / company-paths.js 一行不改 —— 按 §5.4,root run 产物目录、公司 workspace、task 产物目录全部原地不动。本批只改「agent 在哪里跑」,不改「产物写到哪里」。
  7. 按 §4.4 把三行坐标声明接进提示词装配;同时校对 agent-chat.js:297-308 那段已有的公司 workspace 声明,让两处走同一份 company-paths 推导(现在是各写各的字符串)。

批次 4:公司实体清空(G3)

  1. graph-db.js:建 company_agents 表,从 company.json.agents[] + org_graph_nodes 回填。
  2. company-service.hire():删掉 _copyTemplate / _provisionReferenceAgent 两条分支,改为「查本体 → 写 company_agents 一行 → 初始化 system/agents/{aid}/{cid}/(写 company-context.md + user.md + 空 memory)」。
  3. company.json.agents[] 的读取方改查 company_agents:company.js 的 9 处 DATA_DIR、task-service.js:29、agent-chat.js:1008、heartbeat-service.start()。
  4. agent-transcript.js:33 的路径推导重写(其注释里的前提已失效)。

批次 5:create-agent 归位(G4)

  1. 四个 new AgentScaffold(...) 的 baseDir 统一为 system/agents(agent-chat.js:139、agent-chat.js:1415、company.js:226、company-service.js:500)。
  2. 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 死角从此不再产生。
  3. 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 才有料可炼。任何「为了干净而少建本体 / 删本体」的做法都与它相悖。