---
name: tenon
description: "按任务冻结的工作流逐步执行：路由、恢复、每步照 `tenon status --json` 的下一步做。"
---

# /tenon — 工作流执行入口

每一步做什么全部来自工作流数据。本文件只讲怎么读那份数据、怎么照做；default 与自定义工作流
一视同仁，这里不出现任何轨道名或阶段名。

## 何时用

- `<tenon-dispatch> action: invoke-skill skill: tenon` 或用户输入 `/tenon` 时进入。
- 纯聊天不创建任务，直接回答。
- 面向用户的回复（进度、暂停说明、最终总结）使用用户所用的语言：用户用中文下指令就用中文回复；
  命令、字段名、动作名与路径保持原样。

## Codex 的技能读取（硬规则）

- 插件技能一律按 `tenon:<id>` 加载，让宿主读插件 cache 里的 `skills/<id>/SKILL.md`；
  同名的 `~/.agents`、`~/.claude`、项目副本一概不读（已激活任务时 hook 会拒绝）。
  技能不可用就跑 `tenon setup --codex` 或 `tenon update --codex`，不用同名外部技能代替。
- 每个技能一次完整读取，并把内部结果整体转发给宿主，使 `exit_code` 可审计：

```javascript
const result = await tools.exec_command({ cmd: "<读取 SKILL.md 的命令>", workdir: "<绝对项目目录>" });
text(result);
```

  等价的 `text(await tools.exec_command({ ... }));` 也接受。不要用 `text(result.output)`：
  它丢内部退出码。`max_output_tokens` 要装得下全文；被截断的读取不算证据，之后
  `tenon document record` 会报 `lacks exact host confirmation`。

## 模式（取 `step.mode`）

| 模式 | 来源 | 规则 |
| --- | --- | --- |
| `interactive` | 缺省 | 有 `allowed` 的字段用 AskUserQuestion 问（Codex：一个纯文本问题后结束回合）。每次 `transition` / `complete` 前展示产出、测试结果与评审发现，等用户说「继续」。评审门：`request-review` 后展示并结束回合；能确认这道门的回复是「确认继续」「继续执行」「同意继续」，以及简短同意「继续」「可以」「同意」「好的」「按推荐」「按你的推荐」（拒绝或带条件的回复不算）。 |
| `continuous` | 本任务已有交互授权（`tenon session activate --continuous`） | 有 `recommended` 的选择不停；评审门在 `request-review` 后用 `tenon review acknowledge <c> --delegated`。必需测试或评审者不通过时走回退边修问题，绝不「接受偏差」。 |
| `afk` | `TENON_AFK=1` | 同 continuous；`await-review` 直接以当前步骤状态结束本轮。 |

三种模式都一样：技能、文档、测试、评审者、门禁、读取回执，一项都不跳过；推送、PR、部署等
外部动作需要本次任务里已有明确授权；交互式技能自己跑它的对话。

评审确认由 hook 在用户回复时写入回执：本轮上下文出现 `<tenon-review-confirmed>`，或
`step.review.status` 已是 `approved`，就是用户已经确认了这道门——直接照 `next` 做（`transition`），
不要再让用户说「确认继续」，也不要说「这道门只认某句话」。

「按推荐」在评审门上只表示确认这道门，不是采纳你自己的建议。之后要设置字段时仍以 `next` 给的
`recommended` 为准；你自己的偏好与 CLI 的推荐不同（例如 `next` 推荐了某个取值而你想用另一个）
时，先明确问用户，得到回答再设，不要把一句「按推荐」当成授权。

## 进入

1. `command -v tenon`，没有就停下并给出重装提示。
2. dispatch `intent: new` → 起一个 kebab-case 名；`intent: select` 或 `selection_required: true`
   → 要用户给出**一对**确切的候选（名字 + 工作流），然后结束回合；`intent: resume` → 只接手
   点名的那个任务。没有 dispatch 的手动 `/tenon`：`tenon list --json`，多于一个就问。
   选通道以 dispatch 为准，不自己按关键词定轻重：`track_basis: user-named` 表示 `track` 是用户在消息里
   点名的轨道，建任务时照用，不评估；`score` 是按内容评分的推测，用户另有说法以用户为准。
   dispatch 带 `assess: required` 时，`candidate: workflow=<w>;track=<t>` 各行是可选的轨道（轻量一条、
   领域重型一条），轻重由预计改动范围决定：建任务前先读相关代码，列出预计要改的文件，运行
   `tenon assess --files <文件…> [--json]`——退出码 0 = 未超轻量阈值（推荐轻量候选），3 = 超限（输出列出
   被突破的阈值，如 `auth_files 1 > 0`，推荐领域重型候选），1 = 出错（如路径越出仓库，改正后重跑）。
   把评估结果与推荐摆给用户确认：interactive 结束回合等回复；continuous / afk 直接采用推荐。确认之后才
   `tenon init`；选了轻量候选时加 `--domain <另一个 candidate 的轨道>`，升级时 `next` 就能直接给出领域轨道。
   以 `/` 开头的提示词是显式命令，路由钩子不处理，所以 `/tenon <请求>` 要新建任务时由你选通道，选法与钩子一致：
   用户点名了工作流或轨道就照用，不评估；请求带重型信号（架构、跨模块、迁移、schema、鉴权、依赖升级、
   发布、全栈）就直接选对应领域的重型轨道；其余实现类请求同样先评估——读代码、列出预计要改的文件、
   `tenon assess --files <文件…>`（不带 `--workflow` / `--track` 时评估默认工作流的轻量轨道），用
   `tenon tracks list` 看有哪些轨道，再按上一段把评估与推荐摆给用户确认。
   项目自定义的工作流只在用户点名时用，不要自己挑一个。
3. 新建：`tenon init <c> --workflow <w> --track <t> --preset full`（用户可以点别的 preset；dispatch 的
   `track` 与 `workflow` 同名时是平铺工作流，省略 `--track`）；
   随后 `tenon session activate <c> --host-session <host_session_id> [--continuous]`。恢复只跑同一条
   activate。`<host_session_id>` 取 dispatch 里的 `host_session_id`；dispatch 带了它就必须传，
   否则下一轮的「确认继续」认不出本会话的任务，会被当成新任务。dispatch 没有它时才省略该参数。
   activate 会生成 `.pipeline/.gitignore`、`.tenon/.gitignore`（带 `--host-session` 时还有
   `openspec/.gitignore`）：它们只把本机状态挡在 git 外，本身是项目文件，随本任务的第一次提交一起入库。
4. 新建或恢复之后立即重新加载 tenon（这是新的步骤访问）：第一次读 `next` 时就不会只剩一条 `load-tenon`。
5. 用 `tenon workflow plan <c> --json` 的步骤标签建 Todo，当前项取 `current_step`。
6. 进入循环。

## 循环

```
repeat:
  S = tenon status <c> --json | .step        # 或 tenon step run <c> --json | .step（见下）
  S.next[0].action == stop → 报告 message，结束（已完结的任务也有 step：next 是 stop，code finished）
  把 S.next 里与 next[0] 同 action 的项一起做完（一波），按下面的动作表
  做完 transition / complete → 立即重新加载 tenon（新的步骤访问，不必先 status 一次才知道），按模式继续或暂停
  做完 request-review → 把产出摆给用户，结束本轮等「确认继续」（不要再 status 一次去看 await-review）
  做完 finish-change → 任务结束，不必再 status
```

`tenon step run <c> --json` 读的是同一份 `step`，并先把 `next` 里确定性的部分一次做完——铺文档骨架、登记已经写好的文档、
读取回执、生成测试计划初稿——再给出最新的 `next`（`did` 是它做了什么，`stopped` 是它为什么停）。它能代替循环开头的 `status`，
文档类步骤上一次调用顶过去逐份 scaffold / record / read 的好几次。它不加载技能、不派 agent、不写文档内容、不跑测试：这些仍照动作表做。

## 动作表（闭集，与 `step.next` 一一对应）

| `action` | 做什么 |
| --- | --- |
| `stop` | 报告 `message` 后结束。`code: rounds-exhausted`（验证轮次用完，必需测试仍失败）时，把失败的测试、剩余阻断与 `message` 里的出路摆给用户，结束回合等用户决定，不要自行选一条出路。 |
| `load-tenon` | 重新加载本技能（Claude 用 Skill 工具；Codex 按上面的读取规则整读一次）。 |
| `read-documents` | 逐个读完 `documents` 列出的文件，把内容读进上下文（不得丢弃输出：`cat … >/dev/null` 这类读取不算读过），再 `tenon document read <c> all`。这些是已登记的输入：只有 `editable` 里的 kind 本步可以改（改完照 `next` 重新登记），其余只读——需求语义变了走 `requirements-changed` 回到规格步，不要直接改已登记的规格文档；tasks.md 只勾当前步骤标题下的复选框。 |
| `run-agent` | 逐项：`tenon agent prompt <c> <agent> --host <host> --json`（`<host>` 是本宿主 id：Claude Code 用 `claude`，Codex 用 `codex`；agent 声明了 `hosts` 且不含本宿主时 exit 2，照报错告诉用户，不换宿主硬跑）→ 用返回的 `subagent_type` 派发子代理跑返回的 `prompt`（Claude 用 Agent 工具；Codex 用子任务或 `codex exec`；没有子代理的宿主就在主线顺序跑）→ 把报告写到返回的 `report_path`（正文末尾一个 `tenon-result` 代码块）→ `tenon agent record <c> <run_id>`。`subagent_type` 是 `tenon-<agent>` 时，Claude Code 按该 agent 的 `tools` 白名单执行；Codex 没有按 agent 的工具白名单，只在工具里没有写或执行能力时设只读沙箱，其余限制就是 agent 正文里的指令；宿主说找不到这个子代理（会话中途才生成的），改用通用子代理跑同一份提示词，登记时加 `--subagent <实际用的类型>`。返回的 `host.run_on` 非空（`action` 里带 `host` 的评审者）时，这个评审者必须在另一个宿主上跑（跨厂商评审：Claude 写、Codex 审）：不要在本宿主里自己审；从仓库根目录执行 `run_on.command`（它读 `run_on.prompt_file`；这是另一家的 CLI，由你执行，Tenon 不会替你起），评审写完报告并登记后继续；对方登记不了 Tenon 状态时，在本宿主执行 `run_on.record`（带 `--host`）。在不符的宿主上登记会被拒（exit 2），台账里宿主不符的结论无效。`host.run_on` 为空照常派发。同一波并行。执行者的正文只做「一个 task」（如 `builder`）时，本步未勾的独立任务各起一个同类型子代理并行，提示词后面写明各自的任务；全部回来后主线合成一份报告写到 `report_path`，只 record 一次。带 `status: running` 与 `run_id` 的项是已经开始的那次运行：不要重新 prompt，等它跑完把报告写到给出的 `report_path`，再 `tenon agent record <c> <run_id>`。评审者在同一份代码（候选没变）上已经有结论时，不能靠重跑换结论：`prompt` 会 exit 2——改代码换候选后再跑；确有需要（例如上一轮提示缺上下文）加 `--rerun-reason <原因>`，原因与重跑次数会展示给用户，没写原因的同候选重跑按最严结论判定。子代理还在跑的时候，主线不要去加载会向用户提问的技能（`brainstorming`、`grilling`、`domain-modeling` 等）：它们落下的待处理交互会让主线停下等用户回复；等执行者返回、报告登记之后再加载。 |
| `load-skill` | 加载本波每个技能，按下面的「上游技能怎么用」执行。带 `review_bar` 时按下面的「下一步评审口径」审查。 |
| `scaffold-document` | 文件不存在时先 `tenon document scaffold <c> <kind> [--capability <cap>]`，再动笔写内容：骨架里的 `[待填写…]` / `[pending…]` 占位要全部替换成真内容，留着占位符登记会被拒。`tenon step run <c>` 会替你铺。 |
| `record-document` | `tenon document record <c> <kind> <path> --producer <producer>`；本步有好几份文档都写好了，用 `tenon document record <c> --all` 一次登记（没写完的它会列出并跳过）。 |
| `register-field` | `tenon artifact register <c> <field> <path> --producer <producer>`。 |
| `set-field` | 先按下面的「决定」定值，再 `tenon set <c> <field> <value>`。 |
| `validate-spec` | `tenon spec apply <c> --dry-run`；退出码 2 就按报错改 delta spec 再跑。 |
| `apply-spec` | `tenon spec apply <c>`。 |
| `run-test` | `tenon test run <c> <test>`；`fail` 先改代码再重跑，不改就重跑没有意义。报「未配置（test-unconfigured）」不是失败：按提示配置（见 `fix`）。 |
| `test-discover` | 项目还没有测试目录（`tenon init` 通常已经自动识别过；没识别到就是没找到测试工具）：跑 `command`（`tenon test discover --write`），核对 `.tenon/tests/catalog.yaml`（每个套件的命令、报告路径、种类、测试文件 glob）；缺的套件补进目录（`tenon test catalog add`）。项目确实没有某类测试就在目录里声明不适用：`tenon test catalog not-applicable <kind> --reason <原因>`——它要经评审确认一次才生效。 |
| `test-plan-seed` | 跑 `command`（`tenon test plan <c> --seed`）生成本任务的测试计划初稿。计划、基线、已知失败清单只经 `tenon test …` 写入，不要手改 `test-plan.yaml`（会被判 `test-plan-tampered`），hook 也会拒绝。 |
| `test-plan-map` | 逐条处理 `items`，每条自带 `fix` 命令：缺的种类 `tenon test register <c> --suite <id>`，没映射的场景 `--case "<covers>" --test "<文件> › <用例名>"`（用例要真的存在）。确实不适用的种类：整个项目都不适用用 `tenon test catalog not-applicable <k> --reason <原因>`（评审确认一次后对所有任务生效），只有本任务不适用才 `tenon test waive <c> --kind <k> --reason <原因>`——两者都要评审批准，不是绕过。`show` 是查看当前计划的命令。 |
| `test-register-files` | `files` 是本任务新增或修改、还没登记的测试文件：逐个执行各自的 `fix`（`tenon test register <c> --file <path> --suite <id>`）；没有套件认领的（`test-file-orphan`）执行它的 `fix`（`tenon test register <c> --auto`：把文件并进合适套件的 glob 并登记），没有合适的套件才先补目录。 |
| `run-tests` | 动作带 `trust` 时先请用户在自己的终端运行 `trust.command`（把 `trust.commands` 列给用户看），得到答复后再执行，不要先撞一次拒绝。执行 `command`（`tenon test run <c> --stage`）：一条命令跑完本阶段要求的全部套件。跑完用例失败、覆盖率不足之类不是「没跑」，`next` 会改发 `fix`（评审门上有回退边就走回退边）。命令以 exit 1 报「还没有得到你的信任」时，是目录或步骤测试里的命令尚未得到用户本人确认：把它列出的命令摆给用户，请用户在自己的终端运行 `tenon test trust`，然后再重跑。不要自己运行 `tenon test trust`，也不要设置 `TENON_TEST_TRUST`（hook 会拒绝）。 |
| `test-report` | 执行 `command`：把追溯矩阵写进验证报告。报告是已登记文档，写完会变过期，照 `next` 重新登记。 |
| `fix` | 逐条解决 `blockers[]`（改代码或文档），然后回到循环。`source: tasks` 的 blocker 带 `items`（截至本步仍未勾的任务原文）：把这些任务真的做完，再在 tasks.md 里勾上。`code: test-unconfigured`：项目没有这条必需测试要的 npm 脚本——在 package.json 加上运行本项目真正这类测试的脚本，不要复制别的测试命令凑数；在计划步提出时，把「写这类测试」列进本步的计划与 tasks，并把新增的测试脚本与测试同步写进 proposal（What Changes / Impact）与 design，删掉与之矛盾的表述（如「不改 package.json」）；在之后的步骤提出时只补 package.json 的脚本（及它要跑的测试代码），不要改已登记的规格文档（proposal / design / plan）。项目不用 npm 时停下告诉用户去改工作流的测试命令。 |
| `request-review` | `tenon check <c>` → `tenon review request <c> --event <event>` → 把产出与结论摆给用户。动作带 `waivers` 时，里面是待用户批准的项：计划里的测试豁免（`kind:<种类>`）、目录里项目级的「不适用」声明（`not-applicable:<种类>`），以及本任务改动了的测试配置文件（目录、基线、已知失败清单、项目工作流的路径）。把 request 输出逐条列出的这些项连同理由、文件摘要一并摆给用户：用户的确认同时批准它们，确认之后配置文件再变要重新确认。委托确认（持续授权）不批准它们，只能等用户回复「确认继续」。动作带 `residual`（验证轮次已用完，必需评审者在当前候选上仍不通过）时，还要把 `review request` 输出里逐条列出的剩余阻断（评审者、阻断级发现）连同 `alternatives`（另外的出路）摆给用户：只有用户回复放行语（「确认继续」等）的人工确认才接受这些剩余阻断；持续授权的委托确认会被拒、评审保持待确认，如实告诉用户，不要自己批准也不要绕开。 |
| `await-review` | interactive：结束回合等人。continuous：`tenon review acknowledge <c> --delegated`（计划里有待批准的测试豁免或待接受的剩余阻断时委托确认会被拒、评审仍待确认：如实告诉用户，等用户回复「确认继续」人工批准，不要自己批准也不要绕开）。afk：结束本轮。 |
| `commit` | 交付物提交（交付步有未勾任务时它排在勾选之前：先提交，再勾「提交代码」这类任务）：`git add -A -- <commit.paths…>`；`commit.untrack` 非空时接着 `git rm --cached -q --ignore-unmatch -- <commit.untrack…>`；最后 `git commit -m "<commit.message>"`。paths / untrack 原样用、不增不减（`:(exclude)…` 是挡住仓库根门禁标记的 pathspec，照抄）。宿主不让写 `.git` 时如实告诉用户这一步留给他，不要说已提交。 |
| `choose-exit` | 按下面的「出口」挑一条边。 |
| `transition` | `tenon transition <c> <event>`。带 `escalate` 的 `transition`（`event: scope-expanded`）是风险升级：改动风险探针没通过，`escalate.reasons` 是被突破的阈值——照做这条转换（它不要求其余证据），然后照 `escalate.then` 新建重型轨道任务并 `tenon set <新任务> depends_on <本任务>`（`then` 直接给出 `tenon init …` 命令；它没有领域、只列出可选轨道时，把可选项摆给用户选一个再建）；不要为了让探针通过去拆改动或藏文件。当 `next` 里只剩这一条 `transition` 时直接执行，不问用户，interactive 也一样（不等「继续」，不给「升级 / 调探针阈值 / 不走流程」这类选择题）：执行前用一句话告诉用户为什么升级（`escalate.reasons`）、接下来要做什么，然后动手。 |
| `complete` | `tenon transition <c> <event>`——走完终态自边，状态机到此结束。归档由下一条 `finish-change` 单独下发，不要在这里抢跑 `tenon task finish`。 |
| `finish-change` | `command` 不是 `null` 时照原样跑（`tenon task finish <c>`）：Tenon 把 change 目录搬进 `openspec/changes/archive/` 并记下任务结果，不要自己执行 `openspec` 命令。`commit` 不是 `null` 时再提交：`git add -A -- <commit.paths…>`；`commit.untrack` 非空时接着 `git rm --cached -q --ignore-unmatch -- <commit.untrack…>`；最后 `git commit -m "<commit.message>"`。paths / untrack 原样用、不增不减（宿主不让写 `.git` 时如实告诉用户这一步留给他，不要说已提交）；`commit` 为 `null`（不是 git 仓）就不提交。 |

## 决定、字段、出口

- 带 `allowed` 的字段是一次决定：interactive 把 `recommended` 排在第一位问；continuous / afk
  直接用 `recommended`。决定在动手之前下发，按你接下来真的要用的方式填，之后照它执行。
- `kind: outcome` 的字段只在本步必需测试与评审者都过了之后才出现在 `next` 里；它们没有 `recommended`，填 `required` 给的值。`pre_verify_review_result` / `verify_result` 是通过结论：CLI 写入前核对本步证据，被拒就按错误里点名的测试或 agent 去补，不要换个写法绕过。
- 下一步评审口径：动作带 `review_bar`（下一步声明的评审者，每项 `step` / `agent` / `required` /
  `block_at` / `focus`）时，本步的实现与自审——技能自带的代码评审、子代理审查、写
  `pre_verify_review_result` 之前的就绪审查——按同一口径判级：落在这些评审者关注点上、级别达到其
  `block_at`（`medium` 即 medium 及以上）的问题就是阻断，在本步修完，不要判成「建议」留给下一步；
  下一步的评审者会以同一级别打回，多走一轮回退。派发审查子代理时把 `review_bar` 原样写进提示。
- `pr_url`、`prd_path` 和各类文件路径只填真值，绝不编造。`pr_url` 是真实的 http(s) PR 地址；仓库没有 远端时 `next` 会推荐 `no-remote`（本地交付、没有 PR，CLI 会复核确实没有远端）。有远端却开不了 PR 就停下说明。
- 暂停等用户（评审门、interactive 的「继续」）之前如实报告工作区：以 `git status --short` 为准。
  交付步照 `next` 做完、走出口之前，工作区应当是干净的（`set pr_url` 写下的状态文件已由 `next` 点名的
  提交入库）；不干净就照实列出。`transition` 自己还会改 change 目录里的状态文件（`.pipeline.yaml`、
  `.pipeline-history.jsonl` 等），之后暂停时说「只剩流转写下的状态文件未提交，完结时由 `finish-change`
  提交」——不要说工作区是干净的。
- 不要自己建分支或 worktree。提交只照 `next` 的 `commit` / `finish-change` 做（交付步在交付值之前
  点名提交交付物，完结后点名提交归档），`next` 没点名就不提交；技能自带的提交步骤同样不做。
- 出口：`ready` 的前进边直接走；回退边只在它的含义成立时走（必需测试或评审者不通过 → 回到实现
  的那条边；已确认的需求变了 → 回到规格的那条边）；interactive 先问。走到终态的
  `scope-expanded` 表示目标超出了这条轨道：之后按 `escalate.then` 新建重型轨道任务，并
  `tenon set <new> depends_on <old>`（由改动风险探针触发时，`next` 会直接给出它；它是 `next` 里唯一的 `transition` 时不用问，见动作表的 `transition`）。
- 验证轮次上限 `max_rounds` 是用户的决定：不要自行执行 `tenon set <c> max_rounds <N>`，只在用户明确指示时执行；设置会改变任务状态，已发起或已批准的评审请求随之失效，执行后重读 `next` 并重新 `tenon review request`。上限用完时回退边会被拒，不要换事件、改状态或删 marker 绕过，照 `stop` / `request-review` 把出路摆给用户。

## 上游技能怎么用

- 任务已经存在且已绑定：不要 `openspec new change`，也不要另选或另建 change。
- 文档写在 `step.documents` 给的 `path` 上；缺结构先 `scaffold-document`。
- `path` 为 `null` = 这一条的路径还要你拍板：按 `path_template` 里剩下的占位符定值再 scaffold
  （delta-spec 缺 `{capability}`，即 `tenon document scaffold <c> delta-spec --capability <x>`）。
- 只经 `record-document` 登记。技能自带的「提交」「归档」「同步规格」「推送」「建 PR」等收尾动作，
  只有 `step.next` 点名时才做。
- tasks 只勾当前步骤标题下的复选框；它的重新登记会出现在 `step.documents.updates` 里。

## 出错时

| 情况 | 怎么办 |
| --- | --- |
| 没有 `tenon` | 停下，给重装提示 |
| 技能加载不了 | 停下，提示 `tenon setup --<host>` / `tenon update --<host>` |
| `lacks exact host confirmation` | 按上面的读取规则重新加载产出者技能，再登记一次 |
| `stop` 且 `code: retired-skills` | 告诉用户新建任务；旧任务可以终止（`tenon task terminate`）或删除 |
| 评审待确认、回复不是放行语 | 继续等；hook 会打印解锁提示 |
| 回退边被拒、报「验证轮次已用完」 | 不要换事件或调高上限绕过；把已用轮次、上限与出路摆给用户，等用户决定 |
