智能体
智能体(agent)是工作流步骤声明的任务级执行者或评审者,和指令文件(AGENTS.md、CLAUDE.md) 不是一回事:它只在任务里、在点名它的那一步运行。智能体在终端登记;Dashboard 只展示并编辑正文。
智能体文件
每个智能体一个 Markdown 文件:闭集 frontmatter + 正文。
---
name: sql-review
description: SQL 评审:注入、索引与迁移
role: reviewer
version: 0.1.0
skills: [security-review]
tools: [Read, Grep, Glob, Bash, Skill]
model: sonnet
---
# sql-review(评审者)
## 职责
## 只做与不做
## 方法
## 自检
## 报告role是executor(完成步骤的工作)或reviewer(离开步骤前检查)。没有role的旧文件照常 读取,按工具推断(带Write/Edit的算执行者),tenon agent validate会提示补上这一行。version是可选的 semver;hosts(可选)限定适用的宿主;model只交给认得它的宿主。attach_on(可选,评审者用)按风险挂载:attach_on: [auth, dependency, contract]表示只有任务的改动碰到这些路径类 (auth、dependency、contract、migration,与lite轨道风险探针同一套分类)之一,它才加入该步骤。没挂载的评审者不列出、 不等待、不要求;agent prompt对它退出2。读不出改动时一律挂载(失败关闭)。官方security声明[auth, dependency, contract]。 不写这个字段就总是挂载;已冻结了旧副本的任务行为不变。host(可选,codex|claude|any)是评审者该在哪个宿主上跑。它只是建议,用来把评审者路由到另一家厂商; 真正有约束力的是工作流步骤里的声明(见跨厂商评审)。别和hosts(智能体能在哪些宿主上跑)混淆。- 智能体写的报告以
tenon-result代码块结尾。评审者只列问题、不自报结论——结论由 Tenon 按问题级别 与步骤的block_at算出;执行者报done或failed。
来源
| 来源 | 位置 | 能否修改 |
|---|---|---|
| 官方 | 随插件发布(templates/agents/,templates/agents/manifest.json 记录名称、版本、身份与摘要),同步到用户配置 | 只读,复制后再改 |
| 自定义 | 用户配置(<config>/agents/custom/) | 可以 |
| 项目 | 项目的 .tenon/agents/,随仓库提交、团队共享 | 可以 |
同名时项目级优先于自定义(自定义那条列为被覆盖)。任何一层都不能用官方的名字:这样的文件列为冲突, 不会生效。
官方智能体:执行者 builder、researcher;评审者 architecture、backend-quality、code-review、code-size、 e2e、frontend-quality、security、spec-consistency。code-review 是 lite 轨道的评审者:对照宿主写在提示末尾的目标与 验收检查看 diff,不需要规格文档。
在终端生成与登记
tenon agent list [--role executor|reviewer] [--source official|custom|project] [--json]
tenon agent show <name> [--json]
tenon agent new [<name>] --role <role> --description <text> [--skills a,b] [--tools A,B] [--model <m>] [--hosts a,b] [--host codex|claude|any] [--scope user|project] [--from <agent>]
tenon agent add <file> [--scope user|project] [--replace]
tenon agent validate <file|name>
tenon agent copy <from> <to> [--scope user|project]
tenon agent rm <name> [--scope user|project]
tenon agent export <name> --host claude|codexnew在交互终端里逐个问缺的项;必填参数给全时非交互也能用。没有--from时写一份五节的正文骨架;--from builder以官方智能体的字段与正文为底。validate(add同样)检查 frontmatter 与正文、每个技能在插件里存在、正文里不再有new写出的骨架占位符 (<第一步>、<第二步>、<写报告前必须满足的条件>和「做:」那一行),以及按宿主检查工具名:Claude Code 上未知工具名是失败; 只给 Codex 用的 agent 只是警告,因为 Codex 不按名限制工具。new自己照常登记骨架并提示补全,补全之前validate失败。- 官方这一层在文字输出和
--source official里叫「官方 / official」;--json与GET /api/agents里机器可读的值是source: "builtin"(--source builtin作为别名同样接受)。脚本请按builtin比较。 rm拒绝删除官方智能体和仍被工作流步骤引用的智能体,并列出这些步骤(exit 2)。new、add、copy、rm只改库,不写任何宿主子代理文件,只打印提醒:执行tenon agent install并开新会话;见 宿主子代理与安装命令。- 在 Claude Code 或 Codex 里,
tenon:agent-author技能按你的描述起草正文(职责、只做与不做、方法、 自检、带tenon-result块的报告格式),校验草稿后用tenon agent add登记;它不写任何 Tenon 状态。
宿主子代理与安装命令
任务把自己的智能体当作宿主子代理来跑。专属的 tenon-<name> 子代理文件只在终端里写一次、写到用户级目录;会话里运行的 Tenon 命令一概不写它们。会话中途新增的子代理文件什么时候被宿主加载没有保证,只有会话启动前就存在的文件才可靠。
tenon agent install [--host claude|codex]
tenon agent uninstall [--host claude|codex]| 宿主 | 文件 | 字段 |
|---|---|---|
| Claude Code | <Claude 配置目录>/agents/tenon-<name>.md | name、description、tools、model |
| Codex | <Codex 配置目录>/agents/tenon-<name>.toml | name、description、developer_instructions、model;工具既不能写也不能跑命令时加 sandbox_mode = "read-only" |
配置目录:Claude Code 取 $CLAUDE_CONFIG_DIR、Codex 取 $CODEX_HOME,值是绝对路径时才采用(首尾空白与末尾斜杠会去掉;相对路径或空值不采用); 没设时分别是 ~/.claude、~/.codex。宿主只读自己配置目录下的子代理,所以安装、卸载与 tenon agent prompt 的判定都按执行该命令的进程环境 用同一个方式解析出这个目录。
- 在宿主启动时的环境里安装。 如果你的 Claude Code / Codex 是带着
CLAUDE_CONFIG_DIR/CODEX_HOME启动的,安装也要在带同样变量的环境里执行, 否则文件落到~/.claude/agents,宿主看不到。tenon agent install与uninstall输出的第一行目标目录:<dir>告诉你装到了哪里; 在会话里可以用!前缀执行,例如! tenon agent install --host claude,它用的就是会话的环境。 - 所有权按目标子代理目录分别记录:在一个配置目录上安装或卸载,不会删除、改写或认领另一个目录里 Tenon 写的文件 (例如同一台机器上的
~/.claude/agents与$CLAUDE_CONFIG_DIR/agents)。 tenon agent install把适用于该宿主的全部官方与自定义智能体渲染成上面的文件(hosts列表不含该宿主的智能体跳过)。 项目层智能体(.tenon/agents/)从不安装,所以在哪个目录执行结果都一样。--host缺省取进程环境判出的宿主, 普通终端里是claude。tenon setup --claude|--codex与tenon update --claude|--codex成功后会执行同一套安装(update发现已是最新版时也执行, 安装是幂等的),并把写了哪些文件打印出来。安装失败只给 WARN:setup/update的退出码不变,你可以自己再执行tenon agent install --host <host>。--dry-run不安装;每天一次的自动更新(--auto)只在安装失败时出声。- 装完要开新会话。 宿主在会话启动时加载子代理文件;安装命令和
tenon agent new|add|copy|rm都会提醒这一点。这四条库命令 只改库、不写任何宿主文件,只打印「执行tenon agent install并开新会话」的提示(项目层智能体另说明它不安装宿主文件)。 - 文件只在安全时才写:它还不存在;内容已经和渲染结果完全一样(Tenon 补记它归自己);或 Tenon 之前写过、此后没人改过。同名但归你的文件、 或你改过的文件原样不动,列成
WARN;目标文件本身是符号链接则拒绝写(父目录是符号链接,例如 dotfiles 管理的~/.claude,没有问题)。 智能体已不在库里时,它的文件如果是 Tenon 写的且没被改过就删除;条目还在库里、只是加载不了(文件解析失败或读不出来)的不算「已删除」, 它已装的文件既不改写也不删除;库里一个可用的智能体都没有(库读不出来)时,一个文件也不删,并给WARN说明。所有权按文件记内容摘要,存在<config>/agents/host-owned.json,按写入的子代理目录分组; 这个文件解析不了时,已有文件一律不改不删,只写还不存在的文件,并在输出里说明。 tenon agent uninstall [--host claude|codex]删除 Tenon 写入且没被改过的用户级文件;改过的保留,在结果里列为modified。tenon uninstall作用于项目的.pipeline-owned.json,不碰这些用户级文件,想清掉它们请先执行tenon agent uninstall --host <host>。
派发:tenon agent prompt 返回什么
tenon agent prompt <change> <agent> --host claude|codex --json 不写任何宿主文件。它按任务冻结的定义为该宿主渲染期望内容, 与已安装的文件比较;只有用户级文件存在且逐字节一致、并且项目里没有内容不同的同名文件(.claude/agents/tenon-<name>.md 或 .codex/agents/tenon-<name>.toml)时,才返回 subagent_type: tenon-<name> 与 native: true——项目级文件的优先级高于用户级, 会把它遮住。在任务 worktree 里判定时,当前根与主检出两处都会检查,因为从主检出启动的会话会加载留在那里的文件。
专属子代理里,Claude Code 强制执行 tools 白名单;Codex 的自定义智能体没有按 agent 的工具白名单,Tenon 只在 agent 的工具里没有写文件或 执行命令的能力时设 sandbox_mode = "read-only",其余限制只是 agent 正文里的指令。宿主不提供的工具(例如部分无头运行里的 Grep)会回答 No such tool available,agent 改用 shell 里的等价命令。宿主不认识的型号别名直接省略。
其余情况返回宿主的通用子代理(Claude Code 是 general-purpose,Codex 是 default),native: false,并带 reason 与 hint; 专属子代理、或检测不到宿主时这两项都是 null。
reason | 含义 | 怎么办 |
|---|---|---|
not-installed | 这个智能体没有用户级文件 | tenon agent install --host <host>,再开新会话 |
definition-mismatch | 文件存在但与任务冻结的定义不一致:文件安装之后库或智能体变了,或你改过文件 | 重新安装并开新会话(你改过的文件会被保留并列出,删掉它安装才会重写)。任务沿用它冻结的定义,所以冻结了旧版本的任务会留在通用子代理上,而不是悄悄换成新版本 |
shadowed | 项目里有内容不同的同名 tenon-<name> 文件,优先级高于用户级文件 | 移除或修正项目里的那个文件(多半是旧版本留下的,见旧版本留在项目里的文件) |
project-agent | 智能体来自项目层(.tenon/agents/),Tenon 不为它安装宿主文件 | 无需处理:项目层智能体一律走通用子代理 |
hint 是一行中文,不随 TENON_LANG 变化:执行 tenon agent install --host <host> 后开新会话生效(目标目录 <dir>;设了 CLAUDE_CONFIG_DIR / CODEX_HOME 时要在带同样变量的环境里执行,例如会话里用 ! 前缀)。 其中 <dir> 是 prompt 检查的用户级子代理目录,按会话环境里的 CLAUDE_CONFIG_DIR / CODEX_HOME 解析;它与你安装的位置不同,就是 not-installed 的原因。 四个原因给出的是同一句;shadowed 与 project-agent 时安装并不会改变结果。
tenon 技能照返回的 subagent_type 派发,并把 hint 转告你。宿主仍报找不到该子代理(例如就在这个会话里刚装的),技能改用通用子代理跑同一份 提示词,并用 tenon agent record <change> <run-id> --subagent <type> 记下实际用的类型。完全没有派发子代理、工作在主线里跑完时,登记用 --subagent none,运行记录随之读作 subagent: { host, type: "none", native: false, reason: "not-dispatched" }。not-dispatched 是只在登记时出现的 原因,由登记者如实写下,agent prompt 永远不会返回它。
- 每条运行记录都带
subagent: { host, type, native },没有用专属子代理时另有reason。--subagent <type>对通用类型保留prompt给出的reason,实际用的是专属tenon-<name>时则去掉。 reason存放在台账旁边的运行旁注.pipeline-agent-run-meta.jsonl里(字段subagent_reason),读取记录时再合并回来。台账行本身的subagent仍是 三个键,所以本次变更之前发布的运行时照常读得了台账。
旧版本留在项目里的文件
旧版本在任务冻结智能体时,会把 .claude/agents/tenon-<name>.md 和 .codex/agents/tenon-<name>.toml 写进项目。项目级文件的优先级高于 用户级,留下的旧文件会遮住你新装的那份,所以 Tenon 会清理它们,并且不再写新的:
- 什么时候:
tenon init时,以及任务完结(archived)、终止或删除时(CLI 与 Dashboard 都一样); - 删哪些:项目的
.pipeline-owned.json记着该文件且内容仍与记录的摘要一致的,或者内容与当前库为该智能体渲染的结果完全一致的 (CLI 与 Dashboard 用同样两条规则)。读不到库时给一条WARN,只按第一条规则删。同时移除.pipeline-owned.json里对应的条目; 路径上有符号链接的不删; - 其余一律保留,并用
WARN点名文件:被改过的、两条规则都不符合的、读不了的。确认不需要之后请自己手动删除; - 在任务 worktree 里执行时,清理的对象是主检出,旧文件与
.pipeline-owned.json都在那里; - 清理尽力而为:失败只给 WARN,不改变
init、完结、终止、删除的退出码。
在工作流里使用
步骤声明自己的执行者与评审者:
agents:
executors:
- agent: builder
reviewers:
- agent: code-size
required: true
block_at: medium
reads_tests: [code-size]
- agent: security
required: false
block_at: medium执行者先跑,必须 done 才能离开步骤;必需评审者必须在当前候选上通过;参考评审者只报告。默认工作流的 重型轨道(pm、frontend、backend)在调研挂 researcher、实现挂 builder(按独立任务各起一个子代理,合成一份报告)、 验证挂 code-size 评审者及其 code-size 测试;lite 轨道的验证挂评审者 code-review 与 security。code-size 评审者读 files_changed、lines_added、lines_deleted、largest_added_lines 的实测值,按数值和改动性质判断规模是否合理(生成产物、批量迁移本来就大); 默认测试不设行数上限,项目在工作流里给该测试写了 pass.metrics 上限时以它为准。见 默认工作流 与 自定义工作流。
智能体声明的技能(它文件里的 skills:)只能在该智能体运行期间加载。所以退回边——用来回头修问题的那条路,例如验证的 verify-fail、 实现的 requirements-changed——不要求本步声明的智能体的技能,你不必为了退回去先开始一次 builder。主线自己加载的技能在退回边上仍然要求, 前进边则照旧要求全部。tenon status、tenon check 与 Dashboard 的流转用的是同一条规则。
跨厂商评审
Claude 写、Codex 审(反过来也行)。步骤里的评审者写明它必须在哪个宿主上跑:
agents:
reviewers:
- agent: security
required: true
block_at: medium
host: codex # codex | claude | any| 写在哪 | 含义 |
|---|---|
步骤 host: codex 或 claude | 硬要求:只有随裁决登记的宿主就是它,裁决才算数 |
步骤 host: any | 不要求;盖过智能体的建议 |
步骤没写 host,智能体文件里有 host: | 建议:tenon agent prompt 仍然路由到那个宿主,别的宿主给出的裁决照样算数 |
| 都没有 | 不路由、不要求 |
路由。 tenon agent prompt <change> <agent> 知道自己在哪个宿主里跑(进程环境,或 --host)。不是要求的宿主时,它照常开出这次运行, 但把提示词写进 openspec/changes/<change>/.pipeline-agent-reports/<run-id>.prompt.md,并打印在另一个宿主上运行的确切命令,不再打印提示词:
codex exec --sandbox workspace-write - < openspec/changes/demo/.pipeline-agent-reports/<run-id>.prompt.md
# 要求 Claude 审时(在 Codex 里):
claude -p --allowedTools "Read,Grep,Glob,Write,Bash(tenon agent record:*)" < openspec/changes/demo/.pipeline-agent-reports/<run-id>.prompt.mdTenon 不会替你起那个 CLI:命令由你、或当前宿主里的 agent 去执行。提示词末尾是 tenon agent record <change> <run-id> --host <host>, 所以评审能自己登记时就自己登记;另一个 CLI 写不了 Tenon 的状态时,在原宿主执行 tenon agent record <change> <run-id> --host codex, 宿主来源就记为 declared(声明),而不是 detected(检测)。
绑定。 跨厂商的裁决绑两次。绑代码:运行行带着候选(评审开始时工作区的内容哈希),评审期间代码变了 record 拒绝,之后任何改动都让裁决过期、 必须重跑。绑宿主:record 存下 host 与 host_source;步骤要求了宿主时,来自别的(或未知的)宿主的登记被拒(exit 2),台账里仍然对不上的行 也不算裁决——评审者显示 stale,离开步骤报 reviewer-wrong-host,在对的宿主上重跑不需要 --rerun-reason。宿主是登记者的声明,与台账其余部分同一信任 模型;host_source 让人看得出它是检测来的还是声明的。
与 attach_on 的关系。 先按风险挂载。attach_on 没有挂载到本任务的评审者根本不在该步骤里,所以它的 host 要求既不路由也不检查: 不列出、不等待,也不会报 reviewer-wrong-host。上面的宿主规则只作用于已挂载的评审者。
状态 JSON(step.reviewers[] 的 required_host、route_host、host、host_source、wrong_host)和 run-agent 动作(host)给运行器带同样的事实; 工作流页在 block_at 旁边编辑评审者的 host。
Dashboard 的 agent 运行抽屉里,宿主一行并排显示登记的宿主和要求的宿主,要求的那个带一个小的「要求」标记(两者一致时合成一个);登记的宿主违反要求时另有红点加「宿主不符」。抽屉还显示绑定的候选。 评审者在要求的宿主上还没有有效运行、且该步骤是未归档任务当前或之后的步骤时,多出一行「命令」,给出可复制的 tenon agent prompt <change> <agent>。Dashboard 只展示它,从不运行。
Dashboard
库页按身份分组列出智能体,每行显示来源(官方 / 项目 / 自定义)与版本。详情显示字段表、正文渲染、 使用它的工作流步骤,以及所选项目里的最近运行(含每次用的子代理)。自定义与项目级可编辑正文;官方可 「复制为自定义」。库为空时给出可复制的 tenon agent new 命令。