---
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` 前展示产出、测试结果与评审发现，等用户说「继续」（受轮次约束的评审步上轮次内的回退除外：直接 `transition`，用一句话告诉用户为什么回退）。评审门：`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`
   → 要用户给出**一对**确切的候选（名字 + 工作流），然后结束回合；dispatch 带 `change_candidate:` 行
   （候选任务各建在自己的 worktree 里，`change=<名字>;worktree_path=<路径>`）时，把这些任务连同路径列给用户，
   请用户点名其中一个，然后结束回合；`intent: resume` → 只接手点名的那个任务，dispatch 带
   `worktree_path` 说明它建在那个 worktree 里（见第 3 步）。没有 dispatch 的手动 `/tenon`：
   `tenon list --json`，多于一个就问（每个任务的 `worktree` 是它所在 worktree 的路径，`null` 表示就在当前目录）。
   选通道以 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 init` 会为新任务建自己的 worktree，输出里给出它的绝对路径（文本最后两行的 `worktree: <路径>`，
   `--json` 的 `worktree`）。先切进该路径，再 `tenon session activate <c> --host-session <host_session_id> [--continuous]`：
   Claude Code 用 `EnterWorktree` 的 `path` 切进去，之后的命令、评审确认与 hook 都以该 worktree 为项目根；
   Codex 没有对应能力，告诉用户在该路径开新会话继续，然后结束回合。输出里没有 worktree 路径（原地建立，
   输出会说明原因）就直接 activate。恢复：dispatch 带 `worktree_path` 时同样先切进去（Claude Code 用
   `EnterWorktree` 的 `path`；Codex 请用户在该路径开新会话并结束回合），再跑同一条 activate；没有就直接
   activate。`<host_session_id>` 取 dispatch 里的 `host_session_id`；dispatch 带了它就必须传，
   否则下一轮的「确认继续」认不出本会话的任务，会被当成新任务。dispatch 没有它时才省略该参数。
   `tenon init` 可能执行测试目录声明的 `setup`（如 `npm ci`），耗时可达数分钟：执行 init 的 Bash 调用要给足
   超时（例如 600000 ms）。超时中断的只是 `setup`，任务已经建好，用 `tenon status <c>` 确认（任务建在
   worktree 里时，在主检出执行它的报错会给出 worktree 路径）。
   activate 会生成 `.pipeline/.gitignore`、`.tenon/.gitignore`（带 `--host-session` 时还有
   `openspec/.gitignore`）：它们只把本机状态挡在 git 外，本身是项目文件，随本任务的第一次提交一起入库。
4. 新建或恢复之后立即重新加载 tenon（这是新的步骤访问）：第一次读 `next` 时就不会只剩一条 `load-tenon`。
5. 宿主提供 Todo 工具（TaskCreate / TodoWrite）时用 `tenon workflow plan <c> --json` 的步骤标签建 Todo，当前项取 `current_step`；没有就跳过——终端步骤清单由 hook 打印。
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`，
文档类步骤上一次调用顶过去逐份 read 的好几次（升级前创建的任务还有 scaffold / record）。它不加载技能、不派 agent、不写文档内容、不跑测试：这些仍照动作表做。

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

| `action` | 做什么 |
| --- | --- |
| `stop` | 报告 `message` 后结束。`code: rounds-exhausted`（验证轮次用完，必需测试仍失败）时，把失败的测试、剩余阻断与 `message` 里的出路摆给用户，结束回合等用户决定，不要自行选一条出路。`code: finished` 且带非空 `cleanup` 时，同 `finish-change` 的 `cleanup`：原样转告用户，不要自己执行。 |
| `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_type` 是通用类型时返回里带 `reason` 与 `hint`，照用即可并把 `hint` 转告用户；派发仍报找不到时改用通用子代理跑同一份提示词，登记时加 `--subagent <实际用的类型>`；完全没有派发、在主线顺序跑的，登记时加 `--subagent none`。返回的 `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`）。报「还没有得到你的信任」按 `run-tests` 处理。 |
| `test-discover` | 项目还没有测试目录（`tenon init` 通常已经自动识别过；没识别到就是没找到测试工具）：跑 `command`（`tenon test discover --write`），核对 `.tenon/tests/catalog.yaml`（每个套件的命令、报告路径、种类、测试文件 glob；种类写 `unit` / `integration` / `e2e` / `static` / `benchmark` / `custom` 这几类，旧种类名是别名）；glob 要覆盖测试放的位置，改动过的测试文件按它自动认领，不用逐个登记；缺的套件补进目录（`tenon test catalog add`）。项目确实没有某类测试就在目录里声明不适用：`tenon test catalog not-applicable <kind> --reason <原因>`——它要经评审确认一次才生效。 |
| `test-plan-seed` | 跑 `command`（`tenon test plan <c> --seed`）生成本任务的测试计划初稿。按档位（`test:`）要求测试的步骤，初稿不写套件和测试文件——运行集由档位、目录和本任务改动在求值时算出，测试文件按套件 glob 自动认领，只报告缺的必需测试类。计划、基线、已知失败清单只经 `tenon test …` 写入，不要手改 `test-plan.yaml`（会被判 `test-plan-tampered`），hook 也会拒绝。 |
| `test-plan-map` | 只在缺必需测试类（目录里没有该类套件），或档位要求场景映射（`scenarios: required` / `passing`）时下发；默认不要求映射场景。逐条处理 `items`，每条自带 `fix` 命令：缺的类照 `fix` 补套件（`tenon test catalog add`）或声明不适用，旧 `test_policy` 缺的种类 `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` 是本任务新增或修改、像测试却没有任何套件的 glob 认领的文件（`test-file-orphan`）。执行一条 `fix`（`tenon test register <c> --auto`）认领全部：只把各文件的精确路径追加进合适套件的 `files`，不放宽 glob，也不动本任务没改过的文件；没有合适的套件才先补目录。被套件 glob 认领的改动文件不用登记，也不会下发这个动作。 |
| `run-tests` | 动作带 `trust` 时先请用户在自己的终端运行 `trust.command`（把 `trust.commands` 列给用户看；它只列本次要执行、尚未信任的命令，信任在同一仓库的所有 worktree 共享），得到答复后再执行，不要先撞一次拒绝。执行 `command`（`tenon test run <c> --stage`）：一条命令跑完本阶段按档位要求的全部套件；改动没碰到任何套件时它说明没有要运行的套件并以 0 退出，不是失败。跑完用例失败、覆盖率不足之类不是「没跑」，`next` 会改发 `fix`（评审门上有回退边就走回退边；受轮次约束的步骤上，轮次内直接 `transition`，不需要评审回执）。命令以 exit 1 报「还没有得到你的信任」时，是本次要执行的命令里有尚未得到用户本人确认的：把它列出的命令摆给用户，请用户在自己的终端运行 `tenon test trust`，然后再重跑。不要自己运行 `tenon test trust`，也不要设置 `TENON_TEST_TRUST`（hook 会拒绝）。 |
| `test-report` | 执行 `command`：把追溯矩阵写进验证报告。档位不要求场景映射时，矩阵只列已映射的行，另加一行「另有 N 条场景 / 任务未映射（本步不要求映射）」，那不是缺陷。报告是已登记文档，写完会变过期，照 `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`（另外的出路）摆给用户：只有用户回复放行语（「确认继续」等）的人工确认才接受这些剩余阻断；持续授权的委托确认会被拒、评审保持待确认，如实告诉用户，不要自己批准也不要绕开。受轮次约束的评审步上，轮次内的回退边不走这个动作：`next` 直接给 `transition`，不需要评审回执。 |
| `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 仓）就不提交。`cleanup` 不是 `null`（任务建在 worktree 里）时，把它原样转告用户：这是清理 worktree 与分支的命令，要在主检出里执行（不能在要删的 worktree 里执行），由用户自己决定何时执行，不要自己执行。 |

## 决定、字段、出口

- 带 `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 由 `tenon init` 建，不要自己另建分支或 worktree；用户明确要求原地时才给 `tenon init` 传
  `--in-place`。worktree 任务的 `isolation` 已由 init 设为 `worktree`；原地建立的任务（`tenon init`
  输出说明了原地原因，或用户要求 `--in-place`）`isolation` 没有预设，build 问到 `isolation` 时设
  `in-place`，不要设 `worktree` 或 `branch`。提交只照 `next` 的
  `commit` / `finish-change` 做（交付步在交付值之前点名提交交付物，完结后点名提交归档），`next` 没点名
  就不提交；技能自带的提交步骤同样不做。
- 出口：`ready` 的前进边直接走；回退边只在它的含义成立时走（必需测试或评审者不通过 → 回到实现
  的那条边；已确认的需求变了 → 回到规格的那条边）。受轮次上限约束的评审步（评审门且有回退边）上，
  轮次还没用完时走回退边不需要评审回执：`next` 会直接给 `transition`，任何模式都直接走，用一句话
  告诉用户为什么回退；评审待确认时用户要求回去修改，同样直接
  `tenon transition <c> <回退事件>`，不要等确认。前进边、剩余阻断与测试豁免仍只认人工确认。没有轮次约束的步骤上的
  回退边，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。
- 技能按它自己的 SKILL.md 写文件，路径由技能决定。Tenon 在离开步骤时自动记录本步新增、修改、删除的文件与
  来源技能，不需要登记。
- 步骤要求的「必须产出」（`required-output`）没写出时，出口阻断（`fix`）会点名路径：让技能把文件写出来。
- 技能自带的「提交」「归档」「同步规格」「推送」「建 PR」等收尾动作，只有 `step.next` 点名时才做。
- tasks 只勾当前步骤标题下的复选框。
- 升级前创建的任务（`step.documents.mode` 为 `contract`）照旧：文档写在 `step.documents` 给的 `path` 上，
  缺结构先 `scaffold-document`；`path` 为 `null` = 这一条的路径还要你拍板，按 `path_template` 里剩下的占位符定值再 scaffold
  （delta-spec 缺 `{capability}`，即 `tenon document scaffold <c> delta-spec --capability <x>`）；只经 `record-document`
  登记；tasks 的重新登记会出现在 `step.documents.updates` 里。

## 出错时

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