Agents
An agent is a task-level executor or reviewer that a workflow step declares. It is not an instruction file (AGENTS.md, CLAUDE.md): it only runs inside a task, at the step that names it. Agents are registered in the terminal; the Dashboard only displays them and edits their bodies.
The agent file
One Markdown file per agent: a closed frontmatter and a body.
---
name: sql-review
description: SQL review — injection, indexes, migrations
role: reviewer
version: 0.1.0
skills: [security-review]
tools: [Read, Grep, Glob, Bash, Skill]
model: sonnet
---
# sql-review (reviewer)
## Responsibility
## Does and does not
## Method
## Self-check
## Reportroleisexecutor(does the step's work) orreviewer(checks the step before it is left). Files written beforeroleexisted still load; the role is inferred from the tools (any ofWrite/Edit→ executor) andtenon agent validateasks you to add the line.versionis optional semver.hosts(optional) limits the agent to some hosts;modelis passed only to a host that knows it.attach_on(optional, reviewers) scopes a reviewer by risk:attach_on: [auth, dependency, contract]attaches it to a step only when the task's changes touch a path in one of those classes (auth,dependency,contract,migration; the same classification as theliteTrack's risk probe). A reviewer that is not attached is not listed, not waited for and not required;agent promptrefuses it (exit2). If the changes cannot be read the reviewer is attached (fail closed). The officialsecurityreviewer declares[auth, dependency, contract]. Without the field a reviewer is always attached, and a task that froze an older copy keeps its old behaviour.host(optional,codex|claude|any) is where a reviewer should run. It is a suggestion that routes the reviewer to the other vendor; the workflow step is what makes it binding (see Cross-vendor review). Do not confuse it withhosts, which lists where the agent can run.- The report the agent writes ends with a
tenon-resultblock. A reviewer lists findings and never states its own verdict — Tenon derives pass or fail from the severities and the step'sblock_at. An executor reportsdoneorfailed.
Sources
| Source | Where | Editable |
|---|---|---|
| Official | shipped with the plugin (templates/agents/, recorded in templates/agents/manifest.json with name, version, role and digest), synced to the user config | read-only; copy it to change it |
| Custom | user config (<config>/agents/custom/) | yes |
| Project | the project's .tenon/agents/, committed and shared with the team | yes |
A project agent wins over a custom agent with the same name (the custom one is listed as overridden). No layer can reuse an official name; such a file is listed as a conflict and never used.
The official agents are builder and researcher (executors) and architecture, backend-quality, code-review, code-size, e2e, frontend-quality, security and spec-consistency (reviewers). code-review is the lite Track's reviewer: it reads the diff against the goal and acceptance check the host appends to its prompt, and needs no specification documents.
Author in the terminal
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] [--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|codex- The official layer is called "official" in text and in
--source official; the machine-readable value in--jsonand inGET /api/agentsissource: "builtin"(--source builtinis accepted as an alias). Scripts should compare againstbuiltin. newasks for whatever is missing in an interactive terminal; with every required flag it runs non-interactively. Without--fromit writes a body skeleton with the five sections;--from builderstarts from an official agent's fields and body.validate(andadd) check the frontmatter and body, that every skill exists in the plugin, that the body no longer contains the skeleton placeholdersnewwrites (<第一步>,<第二步>,<写报告前必须满足的条件>and the "what this agent does" line), and the tool names per host: for Claude Code an unknown tool name fails; for an agent only for Codex it is a warning, because Codex does not restrict tools by name.newitself registers the skeleton and asks you to complete it;validatefails until you do.rmrefuses an official agent and any agent a workflow step still uses, and lists those steps (exit 2).new,add,copyandrmchange the library only. They write no host subagent file and print a reminder to runtenon agent installand open a new session; see Host subagents and the install command.- In Claude Code or Codex, the
tenon:agent-authorskill drafts the body from your description (responsibility, does and does not, method, self-check, report with thetenon-resultblock), validates the draft and registers it withtenon agent add. It writes no Tenon state.
Host subagents and the install command
A task runs its agents as host subagents. The dedicated tenon-<name> subagent files are written once, in the terminal, at user level; no Tenon command that runs inside a session writes them. A subagent file added in the middle of a session is picked up at a time the host does not specify, so only files that exist before the session starts are dependable.
tenon agent install [--host claude|codex]
tenon agent uninstall [--host claude|codex]| Host | File | Fields |
|---|---|---|
| Claude Code | <Claude config dir>/agents/tenon-<name>.md | name, description, tools, model |
| Codex | <Codex config dir>/agents/tenon-<name>.toml | name, description, developer_instructions, model; sandbox_mode = "read-only" when the tools cannot write or run commands |
The config dir is $CLAUDE_CONFIG_DIR for Claude Code and $CODEX_HOME for Codex when that variable is set to an absolute path (surrounding spaces and a trailing slash are ignored); a relative or empty value is not used. Otherwise it is ~/.claude and ~/.codex. A host reads subagents only from its own config dir, so the install, the uninstall and the tenon agent prompt check all resolve it the same way, from the environment of the process that runs the command.
- Install in the environment the host starts in. If your Claude Code or Codex starts with
CLAUDE_CONFIG_DIR/CODEX_HOMEset, run the install with the same variable, or the files land in~/.claude/agentsand the host never sees them. The first line oftenon agent installanduninstalloutput,目标目录:<dir>("target directory"), shows where it went. From inside a session you can run it with the!prefix, for example! tenon agent install --host claude, which uses the session's environment. - Ownership is recorded per target directory, so installing or uninstalling in one config dir never deletes, rewrites or claims the files Tenon wrote in another (for example
~/.claude/agentsand$CLAUDE_CONFIG_DIR/agentson the same machine). tenon agent installrenders every official and custom agent that applies to the host (an agent whosehostslist leaves the host out is skipped) into those files. Project agents (.tenon/agents/) are never installed, so the result is the same in whichever directory you run it.--hostdefaults to the host detected from the process environment, and toclaudein a plain terminal.tenon setup --claude|--codexandtenon update --claude|--codexrun the same install after they succeed (updatealso when it finds the release already current; installing is idempotent) and print what was written. A failed install only warns: the exit code ofsetup/updatedoes not change, and you can runtenon agent install --host <host>yourself.--dry-runinstalls nothing, and the once-a-day automatic update (--auto) speaks only when the install fails.- Open a new session afterwards. The host loads subagent files when a session starts; the install command and
tenon agent new|add|copy|rmall say so. The four library commands write no host file: they only print the reminder to runtenon agent install(for a project agent they add that it is not installed). - A file is written only when that is safe: it does not exist yet, it already has exactly the rendered content (Tenon then records that it owns it), or Tenon wrote it earlier and nobody has edited it since. A same-named file that is yours, or one you edited, is left alone and listed as a
WARN; a target file that is itself a symlink is refused (a symlinked parent directory, such as a dotfiles-managed~/.claude, is fine). When an agent has left the library, its file is removed if Tenon wrote it and it is unedited. An agent that is still in the library but cannot be loaded (a file that fails to parse or to read) does not count as removed: its installed file is neither rewritten nor deleted. If the library has no usable agent at all (it could not be read), no file is removed and aWARNsays so. Ownership is kept as a content hash per file in<config>/agents/host-owned.json, grouped by the subagent directory it was written to; if that file cannot be parsed, no existing file is changed or deleted, only missing files are written, and the command says so. tenon agent uninstall [--host claude|codex]deletes the user-level files Tenon wrote and that are unedited; edited ones are kept and listed asmodified.tenon uninstallworks on the project's.pipeline-owned.jsonand does not touch these user-level files, so runtenon agent uninstall --host <host>first if you want them gone.
Dispatch: what tenon agent prompt returns
tenon agent prompt <change> <agent> --host claude|codex --json writes no host file. It renders the task's frozen definition for the host, compares it with the installed file, and returns subagent_type: tenon-<name> with native: true only when the user-level file exists and is byte-for-byte the same, and the project has no different same-named file (.claude/agents/tenon-<name>.md or .codex/agents/tenon-<name>.toml): a project-level file outranks the user-level one and would shadow it. Inside a task worktree both the current root and the main checkout are checked, because a session started from the main checkout loads the files that stay there.
Claude Code enforces the tools allowlist of a dedicated subagent. Codex custom agents have no per-agent tool allowlist: Tenon only sets sandbox_mode = "read-only" for an agent whose tools cannot write or run commands, and every other restriction is an instruction in the agent body. A tool the host does not provide (for example Grep in some headless runs) answers No such tool available; the agent uses the shell equivalent. A model alias the host does not know is omitted.
Otherwise it returns the host's generic subagent (general-purpose for Claude Code, default for Codex) with native: false, a reason and a hint. Both are null for a dedicated subagent and when no host is detected.
reason | Meaning | What to do |
|---|---|---|
not-installed | no user-level file for this agent | tenon agent install --host <host>, then a new session |
definition-mismatch | the file exists but differs from the task's frozen definition: the library or the agent changed after the file was installed, or you edited the file | install again and start a new session (an edited file is kept and listed; delete it to let install rewrite it). A task keeps the definition it froze, so a task that froze an older version stays on the generic subagent instead of silently using a newer one |
shadowed | a project-level tenon-<name> file with different content outranks the user-level file | remove or fix the project file (usually left by an older version, see Files left in the project by older versions) |
project-agent | the agent comes from the project layer (.tenon/agents/); Tenon installs no host file for those | nothing: a project agent always runs as the generic subagent |
hint is one line, printed in Chinese whatever TENON_LANG says: 执行 tenon agent install --host <host> 后开新会话生效(目标目录 <dir>;设了 CLAUDE_CONFIG_DIR / CODEX_HOME 时要在带同样变量的环境里执行,例如会话里用 ! 前缀) (run tenon agent install --host <host>, then open a new session; <dir> is the user-level subagent directory prompt checked, resolved from the session's CLAUDE_CONFIG_DIR / CODEX_HOME; run the install in an environment with the same variable, for example with the ! prefix inside a session). If <dir> is not where you installed, that is why it says not-installed. The line is the same for all four reasons; for shadowed and project-agent installing does not change the result.
The tenon skill dispatches subagent_type as returned and passes the hint on to you. If the host still reports that the subagent cannot be found (for example it was installed in this very session), the skill runs the same prompt in a generic subagent and records what actually ran with tenon agent record <change> <run-id> --subagent <type>. When no subagent was dispatched at all and the work ran in the main thread, record --subagent none: the run record then reads subagent: { host, type: "none", native: false, reason: "not-dispatched" }. not-dispatched is a registration-only reason, written by whoever records the run; agent prompt never returns it.
- Every run record carries
subagent: { host, type, native }, plusreasonwhen the run did not use the dedicated subagent.--subagent <type>keeps thereasonthatpromptgave for a generic type and drops it when the type actually used is the dedicatedtenon-<name>. - The
reasonis kept beside the ledger, in the run side note.pipeline-agent-run-meta.jsonl(assubagent_reason), and merged back when the record is read. The ledger row itself keeps its threesubagentkeys, so a runtime released before this change still reads the ledger.
Files left in the project by older versions
Earlier versions wrote .claude/agents/tenon-<name>.md and .codex/agents/tenon-<name>.toml into the project when a task froze its agents. Because a project-level file outranks the user-level one, a leftover file would shadow the one you install. Tenon therefore cleans them up, and writes no new ones:
- when: at
tenon init, and when a task is finished (archived), terminated or deleted (in the CLI and in the Dashboard); - which files: a leftover is deleted when the project's
.pipeline-owned.jsonlists it and its content still matches the recorded hash, or when its content equals what the current library renders for that agent (the CLI and the Dashboard use the same two rules). If the library cannot be read, aWARNsays so and only the first rule applies. The.pipeline-owned.jsonentry is removed with the file, and the file is deleted only if no path component is a symlink; - everything else is kept and reported as a
WARNnaming the file: edited files, files that match neither rule, unreadable ones. Delete those by hand once you are sure you do not need them; - from inside a task worktree the cleanup targets the main checkout, where those files and
.pipeline-owned.jsonlive; - the cleanup is best effort: a failure only warns and never changes the exit code of
init, finish, terminate or delete.
Use agents in a workflow
A step declares its executors and reviewers:
agents:
executors:
- agent: builder
reviewers:
- agent: code-size
required: true
block_at: medium
reads_tests: [code-size]
- agent: security
required: false
block_at: mediumExecutors run first and must finish done before the step can be left; required reviewers must pass on the current candidate; advisory reviewers only report. The heavy Tracks of the default workflow (pm, frontend, backend) attach researcher to every Explore, builder to every Build (one subagent per independent task, merged into one report), and the code-size reviewer with its code-size test to every Verify; the lite Track runs the reviewers code-review and security at its Verify. The code-size reviewer reads the measured files_changed, lines_added, lines_deleted and largest_added_lines and judges, from the numbers and the nature of the change, whether the size is reasonable (generated output and bulk migrations are large by nature); the default test sets no line ceiling, and when the project's workflow gives the test a pass.metrics ceiling, that ceiling applies. See Default workflow and Custom workflows.
The skills an agent declares (skills: in its file) can only be loaded while that agent runs. A return edge, the way back to fix something (for example verify-fail from Verify or requirements-changed from Build), therefore does not require the skills of the agents the step declares, so you never have to start a builder just to go back. Skills the main thread loads itself are still required on a return edge, and a forward edge requires everything, as before. The same rule applies to tenon status, tenon check and the Dashboard transition.
Cross-vendor review
Claude can write and Codex can review (or the other way round). A reviewer in a step names the host it must run on:
agents:
reviewers:
- agent: security
required: true
block_at: medium
host: codex # codex | claude | any| Where | Meaning |
|---|---|
step host: codex or claude | binding: the verdict only counts when the host recorded with it is that host |
step host: any | no requirement; overrides the agent's suggestion |
no host on the step, host: in the agent file | a suggestion: tenon agent prompt still routes to that host, a verdict from another host still counts |
| neither | no routing, no requirement |
Routing. tenon agent prompt <change> <agent> knows the host that runs it (from the process environment, or --host). If that is not the required host it still opens the run, but writes the prompt to openspec/changes/<change>/.pipeline-agent-reports/<run-id>.prompt.md and prints the exact command for the other host instead of the prompt:
codex exec --sandbox workspace-write - < openspec/changes/demo/.pipeline-agent-reports/<run-id>.prompt.md
# a Claude reviewer from Codex:
claude -p --allowedTools "Read,Grep,Glob,Write,Bash(tenon agent record:*)" < openspec/changes/demo/.pipeline-agent-reports/<run-id>.prompt.mdTenon does not start that CLI for you. Run the command (or have the agent in the current host run it); the prompt ends with tenon agent record <change> <run-id> --host <host>, so the review registers itself when it can. When the other CLI cannot write to Tenon state, record from the original host with tenon agent record <change> <run-id> --host codex: the host is then declared instead of detected.
Binding. A cross-vendor verdict is bound twice. To the code: the run row carries the candidate (the content hash of the workspace the reviewer was started on); record refuses when the code changed during the review, and any later change makes the verdict stale and requires a new run. To the host: record stores host and host_source; when the step requires a host, a record from another (or an unknown) host is refused with exit 2, and a ledger row that disagrees anyway is not a verdict — the reviewer shows stale, the step exit reports reviewer-wrong-host, and rerunning on the right host needs no --rerun-reason. The host is the recorder's claim, in the same trust model as the rest of the ledger; host_source shows whether it was detected or declared so a human can tell.
With attach_on. Scoping by risk comes first. A reviewer that attach_on does not attach to the task is not part of the step at all, so its host requirement is neither routed nor checked: it is not listed, not waited for and never reports reviewer-wrong-host. The host rules above apply to the reviewers that are attached.
The status JSON (step.reviewers[]: required_host, route_host, host, host_source, wrong_host) and the run-agent action (host) carry the same facts for a runner. The workflow page edits the reviewer's host next to block_at.
The Dashboard's agent run drawer shows the host row as the recorded host and the required host side by side, the required one with a small "required" marker (the two merge into one when they match), and a red dot with "Host mismatch" when the recorded host breaks the requirement. It also shows the bound candidate. While the reviewer has no valid run on the required host, and the step is the task's current or a later one in an open task, a Command row gives a copyable tenon agent prompt <change> <agent>. The Dashboard only displays it and never runs it.
Dashboard
The Library page lists agents grouped by role, with source (official / project / custom) and version on each row. The detail shows the fields, the rendered body, the workflow steps that use the agent and its recent runs in the selected project (with the subagent that ran each). Custom and project agents can edit their body; an official agent can be copied as a custom one. An empty library shows the tenon agent new command to copy.