跳到正文

故障排查 ​

先确定事实面,再采取修复。Tenon 的状态、文档、session、runtime 和 UI 是不同层,单看一个“等待”标签不足以判断根因。

目标 ​

用只读命令定位问题属于路由、Change 状态、文档证据、review、AFK、runtime、Dashboard 还是 Pages,并选择不会破坏历史或绕过 guard 的恢复动作。

前置条件 ​

  • 记录项目根、Change 名、当前宿主和完整错误;
  • 保留命令退出码,不只截取最后一行;
  • 不删除 .pipeline-run、ledger、marker 或 archive 尝试“解锁”;
  • 对 token、prompt、绝对路径和真实用户数据做脱敏。

步骤 ​

1. 采集基础事实 ​

先在本机生成脱敏诊断包(不联网、不上传,会逐项打印包含了什么与抹掉了什么):

bash
tenon support bundle [--out <路径>]

包里有版本、doctor、runtime status、配置摘要(只有名称与计数,没有值)、最近的 Dashboard server 日志和 hook 耗时(有记录才有),总大小不超过 5 MiB,文件权限 0600。写入前会抹掉 token、cookie、一次性登录码、 私钥、邮箱、home 路径与用户名;分享前仍要自己解开看一遍。

再按需逐项采集:

bash
tenon status <change> --json
tenon document status <change> --json
tenon inbox --json
tenon doctor --json
tenon runtime status

2. 对照症状 ​

正常对话恢复了旧 Change ​

新目标不应自动绑定用户的 active-change。确认 router 是否把请求识别为 resume;多个候选时必须显式点名。独立目标创建新 Change 或走 discussion。 宿主提供 session id 时,全新未绑定会话里的通用“继续执行”必须 fail-closed,不得回落到仓库级 用户的 active-change;应显式点名 Change,或先建立该会话的精确绑定。

Todo 与 Workflow 不一致 ​

tasks.md 的一级标题应来自实际 DAG。检查 Change 选择的 Workflow、step label 和 Todo projection。不要硬编码 default 七阶段到 custom。

文档语言不正确 ​

新 Change 默认 zh-CN,固定信息在:

text
openspec/changes/<change>/.pipeline-document-locale.json

旧 Change 保留原语言;首次补文档时可以显式 --locale en 或 --locale zh-CN 固定。切换全局设置不会翻译现有文件,修改已登记文档后必须重新 record/read。

Review 无法推进 ​

正确顺序是 check、request、用户确认、transition;确认只来自用户(回复放行语、Dashboard,或用户本人在自己的终端运行 tenon review acknowledge),agent 不得代为执行。receipt 必须匹配 exact phase/event。删除 marker 不能替代 canonical acknowledgement。

受轮次上限约束的步骤(评审门且有回退边)上,轮次还没用完时走回退边不需要评审回执,直接 tenon transition <change> verify-fail,即使此时正有 verify-pass 的评审在等确认;转换会一并清掉过期的评审标记。前进边、剩余阻断与测试豁免仍只认人工确认。

评审待确认期间,写门拦写入但不拦只读工作:git status --short | grep x、cd sub && ls 这类每段都只读的组合命令放行,本任务(标记所属任务)恰好 tenon transition <change> <event> 也放行(缺已批准回执的前进边仍由 CLI 拒绝)。拦截提示按标记种类区分:评审提示写明任务与事件,列出放行语与 Dashboard 确认。

Dashboard 一直等待 ​

比较 canonical state、host session 和 AFK executor。queued、review waiting、session ended 和 running 是不同状态。生产 Dashboard 默认地址是 127.0.0.1:18765;先确认该端口服务的页面标题和项目根。

Dashboard 页面或 API 返回 401 ​

没有会话时 Dashboard 什么都不提供。运行 tenon dashboard --open,浏览器会自动打开并登录;server 重启或升级后需要再运行一次。提示无法打开浏览器时见登录。curl /api/snapshot 返回 401 是有意的,读状态请用 tenon status --json 等 CLI 命令;只有 /api/health 不需要会话。写操作还需要打包后的同源 Dashboard:Vite 开发端口拿不到写 token。

在 Dashboard 里批准评审提示需要本人确认 ​

批准需要页面上的第二次明确点击(它会为这一次评审申请一次性 nonce)。脚本做不到这一步;如果点击之后仍出现这条提示,刷新页面重新批准。

Codex 插件已安装但认证为黄灯 ​

先运行 codex login status。若未登录,ChatGPT 方案包含 Codex 时运行 codex login;远程或 无浏览器终端运行 codex login --device-auth。使用 Platform API Key 时,先到 https://platform.openai.com/api-keys 创建,再运行 printenv OPENAI_API_KEY | codex login --with-api-key,最后重新执行 codex login status。 Platform API Key 按用量计费。Tenon 不会替你登录,也不会读取凭证内容。

auth:codex 是本机 Codex 宿主登录态;afk:credential-codex 是 AFK 容器能否收到 API Key 或可读 Codex home。两个灯相互独立,不能用其中一个绿色推断另一个也已就绪。

测试或宿主 Bash 里 tenon: command not found(exit 127) ​

tenon 在 PATH 上解析不到时,tenon doctor 的 env:path-tenon 是黄灯。tenon test run 会给测试进程把正在运行的 tenon 放到 PATH 最前,所以必需测试 tenon test code-size --json 在没有启动器的环境里也能跑;宿主自己的 Bash 和你的 CI 仍然需要它。把启动器目录(通常是 ~/.local/bin)加进 PATH,或运行 tenon setup --claude / tenon setup --codex 装上启动器。

code-size 测试挡住出口,或大改动没被挡住 ​

默认的 code-size 测试没有上限:它记录 files_changed、lines_added、lines_deleted、largest_added_lines,退出码 0, 由 code-size 评审者按实测值判断规模是否合理。所以大改动不会被行数挡住,这条测试也不需要豁免。如果项目自己的工作流给这条测试写了 pass.metrics 上限,超限的运行以 metric-threshold 失败:在那份工作流里调高或去掉上限;只此一次放行就 tenon test waive <change> --test code-size --reason …(评审确认时批准它,并把实测值显示在你的理由旁边)。要启用上限,在工作流 YAML 的该测试下加:

yaml
pass:
  metrics:
    - name: lines_added
      max: 3000

新测试文件被报为孤儿,或被认领的文件没有执行 ​

改动过的测试文件由目录里套件的 files glob 认领,不用逐个登记。没有任何 glob 匹配的文件是孤儿(test-file-orphan):运行 tenon test register <change> --auto,它把该文件的精确路径追加进合适的套件,从不放宽 glob。被认领的文件没有执行过的用例 (registered-test-not-executed),说明套件的 files glob 与测试命令实际跑的文件不一致:到 .tenon/tests/catalog.yaml 改 glob 或命令。没有源码扩展名的路径(例如 fixtures/data.spec.json)和 openspec/、.tenon/、.pipeline/ 下的任何路径都不会被当成测试文件。

tenon doctor 的 env:platform 是红灯 ​

你在原生 Windows 上,Tenon 不支持(hooks 与安装脚本是 bash,测试服务的启停走 /bin/sh)。装 WSL 2(管理员 PowerShell 运行 wsl --install),再在 WSL 终端里安装并运行 Tenon,项目放在 WSL 的文件系统里。完整的平台与 Node 版本矩阵见 安装与宿主配置。

Pages 或本地预览 404 ​

本仓是 project site,base 必须为 /tenon/:

bash
npm run docs:sync
npm run docs:build
npm run docs:smoke

如果 HTML 200 但哈希 JS 404,通常是 preview 进程仍缓存旧 dist 文件表。停止旧 preview,基于当前 dist 重启,再验证搜索、主题和移动导航。

看 Dashboard server 日志 ​

server 把自己的输出镜像进 <state>/logs/dashboard.log,按大小轮转(当前文件 + .1 + .2,每个 1 MiB); 凭证、cookie 与一次性登录码在落盘前已抹掉。

bash
tenon logs                 # 轮转文件合起来的最近 100 行
tenon logs --lines 500
tenon logs --follow        # 持续输出新增日志,Ctrl+C 结束

受管后台 server 没有终端,这个文件是它的告警和意外 500(只记方法、路径与消息,不记查询串)唯一的去处。

切换 CLI 语言 ​

CLI 的帮助、用法错误和最常见的错误按 TENON_LANG=en|zh,其次 LC_ALL、LC_MESSAGES、LANG 选语言; 没有语言信号、或 LC_ALL=C/POSIX(hook 为输出稳定会这样钉)时保持历史的中文输出。消息码和退出码与语言无关。 tenon transition 遇到非法或未知 event 时,会用当前语言列出当前 step 的合法 event。

回滚 更新 setup 都因未完成的回滚被拒绝 ​

从 v0.1.0 到 v0.3.1 的每个版本里,tenon update 之后运行 tenon runtime repair --rollback,在正常安装上都会失败,报 rollback refuses a third-party launcher checkpoint: tenon。这时选择已经换到了上一份 release(tenon runtime status 里它是 active), 但回滚日志 runtime-rollback.json 被留了下来;之后 tenon runtime repair --rollback、tenon update、tenon setup 都以 存在未完成的 runtime rollback;请先重跑 tenon runtime repair --rollback 拒绝,重跑那条命令也一样失败。

原因是同一份文件有两个生成器、内容漂移了:安装器会在稳定 launcher 里写 export TENON_NODE_PATH=…,bootstrap 自己生成的 launcher 文本没有这一行, 于是 bootstrap 在已经翻转选择之后,把安装器写的 launcher 当成第三方文件拒绝。除了遗留的日志,没有任何东西损坏。

你手头的 Tenon 修不了它:tenon update 与 tenon setup 跑的是你回滚到的那份 release,tenon runtime repair --rollback 跑的是那份 release 装下的 bootstrap,两者都是旧代码。对该宿主运行一次 v0.3.2 的版本化 install.sh。它不经过 launcher,它运行的 setup 会先收尾遗留的回滚,再装 v0.3.2:

bash
/usr/bin/curl -fsSL https://raw.githubusercontent.com/jefferysha/tenon/v0.3.2/install.sh | /bin/bash -s -- --codex
# 或
/usr/bin/curl -fsSL https://raw.githubusercontent.com/jefferysha/tenon/v0.3.2/install.sh | /bin/bash -s -- --claude

setup 按选择的状态处理这份日志。选择就是日志的目标(上面的情形):把 launcher 一对收敛好、删掉日志,runtime 停在你回滚到的那份 release,直到安装激活 v0.3.2。 选择还是日志的起点:回滚根本没有生效,直接删掉日志。两者都不是:说明有别的东西改过选择,setup 拒绝,tenon runtime status 能看到现状。 之后 tenon runtime status 与 tenon doctor 显示 active release 有效,上一份 release 就是你回滚到的那一份。

从 v0.3.2 起,bootstrap 写出的 launcher 字节与安装器完全一致(有测试固定了两种模式下的整份文本),并且在移动选择之前先证明 launcher 一对, 所以被拒绝的回滚什么都不改,也不留日志。回滚在选择移动之后被打断时,tenon update 与 tenon setup 会把它收尾而不是拒绝,同一条 tenon runtime repair --rollback 也一样。

回滚之后 tenon doctor 的 identity:release 是 WARN ​

回滚只换受管 runtime。宿主插件和仍在运行的 Dashboard 还是较新的那份 release,所以发布身份对不上。这是状态而不是损坏,v0.3.2 起 doctor 把它报成写明原因的警告。 只有回滚是最近一次 runtime 事件、且宿主插件恰好就是被回滚掉的那份时才这样报,其他任何不一致仍是红灯。二选一:

  • 回到较新的 release:tenon update --codex(或 --claude),这会撤销这次回滚;
  • 留在旧 release,让宿主插件跟上:tenon setup --codex(或 --claude)把宿主插件重新绑定到你正在运行的 runtime 的发布版本。

两者都不做,继续用回滚后的 runtime 也可以。

批准不适用声明之后任务卡在 protected-file-unapproved ​

v0.3.0 与 v0.3.1 里,.tenon/tests/catalog.yaml 的 not_applicable 条目如果在任务开始之前就已提交、且还没批准,你在评审里批准它之后任务可能被卡死。 批准会把你的名字写进条目的 approved_by,这次对 catalog.yaml 的改写没有被记成已批准,于是 tenon test status 与 tenon transition 以 protected-file-unapproved(目录文件)停下,tenon review request 又因为评审已确认拒绝第二次请求 (phase 'build' 的 event 'build-done' 已获确认;请直接执行该 transition,不能重复 request)。

v0.3.2 会记下这次批准,新任务不会再卡住。已经卡住的任务不会因为更新被修好,也没有命令能清掉这个状态。把 approved_by 改回 null 也没用: 条目回到未批准,请求照样被拒。可行的做法是终止卡住的任务,在同一个工作区里新建一个任务:

bash
tenon task terminate <name> --yes
tenon init <new-name> --workflow <workflow> --track <track> --preset full

新任务照常运行(tenon test register、tenon test run <new-name> --stage)。条目里已经带着批准人,所以 tenon review request <new-name> --event <event> 只列出待确认的测试配置改动(就是那次目录改写),而不是待批准的豁免。用 tenon review acknowledge <new-name> 确认一次,之后 tenon test status 没有阻断项,tenon transition 可以通过。

被终止的任务移进 openspec/changes/archive/,在工作台的「已结束」里标为已终止;它不能恢复,想保留关联就在新任务上 tenon set <new-name> depends_on <name>。终止不动也不提交你的代码,但新任务从它工作流的第一步开始,卡住的任务写下的文档留在它自己那里。

命令或 hook 报 tenon runtime Node identity changed ​

稳定 launcher 会钉住 setup 时选定的 Node:路径上没有符号链接,Node 的 inode、权限位、属主和大小,每级父目录的 inode、权限位和属主,以及 Node 字节的 SHA-256。v0.2.0 还钉了设备号,而 macOS 每次重启都会换设备号,于是重启后所有命令和 hook 都失败。v0.2.1 不再保存设备号。

  • 提示里写的是 tenon setup --claude(或 --codex):Node 的字节没变,只是身份信息变了。tenon setup、tenon update、tenon doctor 和 tenon runtime 仍能经 launcher 运行,对你的宿主运行 setup 命令即可重新钉住。
  • 提示里打印的是以 env TENON_RUNTIME_ROOTS= 开头的命令:钉住的 Node 被替换或删除了,例如原地升级了 Node。原样运行这条命令,它用你 PATH 上的 Node 启动 bootstrap,setup 会钉住这个 Node。Codex 把 --claude 换成 --codex。
  • v0.2.0 的 launcher 已经拒绝一切命令时,对该宿主运行一次版本化的 install.sh,它不经过 launcher。
  • launcher 仍钉着设备号但还能用(例如刚用 v0.2.0 执行过 tenon update)时,v0.2.1 及之后版本的第一条 Tenon 命令或会话启动会把它重写掉。在那之前 tenon doctor 的 runtime:launcher 显示 WARN,并写明手动修复方法 tenon setup --claude / --codex。

hook 最多每 30 分钟打印一次这条消息,其余时候无输出地以 0 退出,从不阻断宿主。标记文件是 Tenon 状态目录里的 launcher-node-identity.notice。

3. 选择恢复路径 ​

  • 实现缺陷:Verify 写失败报告,走 verify-fail 返回 Build;
  • 需求或设计变化:Build 走 requirements-changed 返回 Spec;
  • runtime 损坏:使用 tenon runtime repair,不改项目证据;
  • review 待确认:完成产物后处理准确 receipt;
  • AFK queued:检查 scheduler、预算、依赖与 gate;
  • projection drift:使用受控 inspect/repair,不手改 canonical current。

预期结果 ​

你应能把问题归到一个明确层级,保留失败证据,并选择状态机允许的回边或修复命令。修复后,相同只读命令应显示一致状态,而不是 UI 绿、CLI 红或步骤产出记录缺失。

验证 ​

重新运行基础采集命令,并针对问题层补充:

  • UI:真实浏览器桌面、窄屏、键盘、主题和控制台;
  • 文档:docs:check、docs:build、docs:smoke 和真实搜索;
  • runtime:doctor 与 runtime status;
  • Change:status、document status 与 exact transition guard。

常见失败 ​

  • 只看 Dashboard 标签:回到 canonical state 和 executor 证据;
  • 端口 18765 可访问就判定成功:必须核对服务身份;
  • 删除 marker 解锁:不会生成 canonical review receipt;
  • 轮次内的回退边还在等评审确认:直接 tenon transition <change> verify-fail,只有前进边、剩余阻断与测试豁免才需要人工确认;
  • 拿某个前进 event 的批准去放行另一个 event:receipt 绑定 exact event,不能互相复用;
  • 手工改 .pipeline.yaml:它只是投影,可能制造 drift;
  • 将构建通过当浏览器通过:需要打开当前应用并验证交互;
  • 用旧验证报告验新基线:Build 修订后必须重新 Verify。

下一步 ​

仍无法解决时创建最小复现,移除 token、prompt、绝对路径和用户数据后提交 Issue。安全问题使用私密漏洞报告。

本地优先 · 证据驱动 · 开源可迁移