---
name: handoff
description: 用 handoff CLI 以协调者身份把实现计划派发给独立 executor（opencode / claude / grok / codex / agy）执行并盯完全程。只要涉及「把这个 plan 交给远程开发机跑」「派发任务给 executor 执行」「盯 handoff 任务进度」「想写个轮询/sleep 循环等 handoff 任务」「任务卡在 running / waiting_review」「reply 返回 502 / continue 报 409 / done 报 404」「wait 返回了旧事件」「新会话接管一个已经在跑的 handoff 任务」「坐下」「叫机器人」「换绑」「card bind」「card coordinate」「card rebind」，哪怕用户一个字没提「handoff」，也必须先读这份 skill——handoff 的状态机对操作顺序有硬约束，凭印象敲命令会撞 404/409，并把任务卡成没人收的孤儿。
---

<!--
职责：
  - 教会「协调者」角色如何用 handoff CLI 驱动一个派发任务的完整生命周期。
    不叫审核者：用户把审核者理解成 code review，协调者才是派发与盯任务的那一端。
  - 固化那些一旦搞错就会卡住任务的硬约束：ID 形态、状态机前置条件、事件分诊、失败出口。

边界：
  - 不讲 agentd 的部署与配置（config.yaml 各段、approver 审批链、env 注入）——见仓库 README。
  - 不讲各 executor 的内部差异与协议实现——见 README「各 executor 须知」与 docs/superpowers/specs/。
  - 不替协调者做审批判断：批不批、改不改由协调者（必要时升级给人）决定。
-->

# handoff：以协调者身份驱动派发任务

## 心智模型

handoff 把「写计划的人」和「干活的人」拆成两个进程：

- **agentd** 跑在 executor 所在机器上，持有**全部**状态——任务、事件、工单、executor 生命周期，落 SQLite。
- **你（协调者）**只是一个客户端。你不持有任何状态，随时可以崩溃、断网、换一台机器接管。
- 你和 agentd 之间只有一条通道：`handoff` CLI。

这条架构直接决定了三件事，后面所有纪律都是它的推论：

1. **你的会话不是权威**。「我记得这个任务已经批过了」不作数，`handoff show` 说了算。
2. **断网不丢事件**。事件在 agentd 侧持久化并带 cursor，`wait` 重连后从断点续拉。所以你没必要一直挂着。
3. **绕过 CLI 就会失配**。ssh 到执行机去杀 executor 进程、手删任务目录、直接进工作区改代码——这些 agentd 全都不知道，它记的运行态和真实存活性会当场对不上，任务卡成孤儿。

### 铁律：一切经 CLI

需要看 executor 在干什么，用 `handoff attach`（经 agentd 的 render 流，远程也不需要 ssh）。需要看代码，用 `handoff diff` / `fetch` / `run`。需要回收，用 `handoff done` / `stop`；归档后残留的 managed worktree 用 `handoff reclaim` 清；终态任务的 tmp/gocache 叶子和残留 managed 树用 `handoff gc`（默认预览，`--yes` 才删）。

**唯一例外**：任务已经彻底死了、CLI 三条路（`resume` / `continue` / `done`）全被拒，此时按任务目录 `proc.json` 里的 `handle.pid` 手工 kill shim 进程是兜底。但那是排障，不是日常。

## 任务 ID 必须是完整 UUID

所有接受 `<task>` 的子命令都是**精确匹配**，没有前缀补全。传 8 位短 id 一律 404「任务不存在」。

短 id（`id8`）只出现在 `--notify` 的通知文案里，不能当命令参数用。

拿完整 id 的办法：`dispatch` 的输出 JSON 里的 `.id`，或者 `handoff tasks | jq -r 'select(.name=="...") | .id'`。

## 状态机：先看状态，再敲命令

六个状态。**`continue` 和 `done` 都硬要求 `waiting_review`**，状态不符返回 409 `ErrBadTransit`——这是最常撞的一堵墙。

| 状态 | 含义 | 此时能做 | 此时会被拒 |
|------|------|----------|-----------|
| `pending` | 已建任务，executor 还没起来 | `show` / `stop` | `continue` / `done` |
| `running` | executor 正在干活 | `wait` / `attach` / `show` / `diff` / `stop` | `continue` / `done` |
| `waiting_answer` | 有工单挂起，等你裁决 | `reply --ticket ...` / `attach` / `stop` | `continue` / `done` |
| `waiting_review` | 一轮干完了，等你审 | `diff` / `fetch` / `run` / **`continue`** / **`done`** / `stop` | — |
| `completed` | 已归档 | `show` / `diff`（只读） | 一切写操作，含 `stop` |
| `failed` | 已失败 | `show` / `diff` / `pull`（只读取证） | `continue` / `done` / `stop` |

> **回合失败发的是 `turn_failed` 事件，不是 `failed`**（B100 拆开的两个类型）：`turn_failed` 时任务进 `waiting_review`——executor 会话与上下文都在，`continue` 就能续接重试。`failed` **事件**如今只在真终态出现：`stop`、`resume --force`、dispatch 期启动失败、看门狗补正——此时任务落 `failed` **状态**，想继续只能重新 `dispatch`。
>
> `diff` / `fetch` / `run` 无状态门禁：`running` 中也能看实时进度。但 `completed`（已归档）的 `--new-worktree` 任务其 worktree 已被回收：`run` 会给出真因并返回 400（「managed worktree 可能已被 done/stop 回收」），`diff` 则是普通 git 失败（500），没有专门归因。

**动手前先确认状态**：`handoff show <task>` 输出一行 JSON，含任务体 + `pending_tickets` + 最近事件。不确定就先 show，比吃一个 409 便宜。

## 主循环

> 本项目启用了账本（`ledger.enabled: true`）时，**外层**循环见下面的
> 「账本模式」一节；本节仍然完整适用于内层——醒来之后怎么处置一个具体
> task 的事件，两种模式一模一样。

```bash
# 1. 派发（仓库工作区必须干净，否则被拒——脏改动会被污染进任务分支）
handoff dispatch --new-worktree --executor opencode plan.md
# stdout 第一行是任务 JSON，取 .id 作为后续所有命令的 <task>

# 2. 挂等待（阻塞，一次只返回一个事件）
handoff wait <task> --notify --timeout 1h

# 3. 按事件类型分诊、处置（见下表）

# 4. 回到第 2 步继续 wait，直到进入审核
```

**`wait` 的三条契约**，记牢了省很多事：

- **默认一次只吐一个事件**。stdout 单行 JSON，然后退出；`--follow` 持续订阅，事件逐条流入，退出即任务终结或超时。处理完**不用重挂**——follow 订阅活到会话结束。
- **退出码有语义**。`0` = 事件到达；`124` = `--timeout` 到点（可以接着挂）；`1` = 真失败。`--timeout` 在三种模式下三种含义：一次性 wait 是**等不到事件的总时长**，`--follow` 是**空闲上限**（见下节），`--until-done` 是**总时限**（中间帧不续命）。
- **失败会立刻退出，不会闷等**。token 没同步（401）、task-id 不存在（1008）都是立即报错。`wait` 长时间不返回**只**意味着「还没有事件」，那是正常态——stderr 里的「WS 连接断开，等待后重连」也是正常态。

无人值守时务必带 `--timeout`：它是配置错误的最后一道防线，退出码 124 可以和真失败区分开。

`progress` / `approver_decision` / `approver_disabled` / `tickets_voided` 四类事件**不会**唤醒 `wait`（只入库）。你只会在 `show` 的事件历史里见到它们，日常不用管。

## 执行器选型与模型

- **codex**：缺省执行者，稳重不跑偏——整份 plan 的执行、大规模机械改动
  （codemod、跨文件重命名、依赖升级修编译）。
- **grok**：快、挖 bug 能挖到根——根因排查（复现、加日志、二分定位）、真机烟测、
  tool 调用多的纯执行。根因后分流：一两行小修顺手修掉，架构级修复回本地走流程。
- **claude**：重构、细粒度设计、复杂推理或多文件协同排查。
- **agy**（Antigravity CLI）：适用于基于 Google Antigravity / Gemini 的工程任务与复杂子代理编排，权限门通过 PreToolUse 钩子挂载。
- **opencode**：备选，codex 不可用时顶上。
- **一律不传 `--model`**，除非要点名某个具体模型：缺省执行者用机器默认模型；
  改派非缺省执行器时留空、由其自身默认接管。模型名**按机器不同**，跨机/跨执行器
  复用模型名，第一个事件就是 400。

## 在 agent 会话里挂 wait

上面的主循环假设操作者能前台阻塞一小时。agent 的 Bash 工具做不到（前台超时上限通常只有几分钟到十分钟），于是最常见的走样就是自己发明 `show` + `sleep` 轮询循环，或把几百轮 wait 包进一条 shell 大循环。**两种都不要。** 正确形态按你所在 harness 的能力二选一：

- **有后台任务/监控机制的 harness（Claude Code 的 Monitor、grok 的 background task）**：挂一条后台 `wait --follow` 长订阅，见下节。
- **没有后台唤醒机制的 harness（opencode、codex）**：挂不了 `--follow` 订阅，退回**前台一次性 wait 逐轮挂**：`handoff wait <完整 task-id> --timeout <小于前台超时上限，如 5m>`，阻塞到返回一个事件就处置，处置完再挂下一条；退出码 124 表示这轮没等到，直接再挂即可。每轮一条独立命令，事件 JSON 完整落在命令输出里——这不是被禁止的轮询循环，禁的是拿不到事件的 `show`+`sleep` 和吞掉输出的 shell 大循环。

### 订阅：开一次，活到会话结束（Claude Code / grok）

    Monitor({
      command: "handoff wait --follow <完整 task-id> --timeout 3h",
      description: "handoff <任务名> 事件流",
      persistent: true
    })

事件作为通知逐条流入本会话，**没有「重挂」这个动作**。

**命令后面不要接任何过滤器**（`grep` / `sed` / `awk`）。INFO 噪声在 **stderr**，
stdout 上本来就只有 JSON 事件——要静音用 `2>/dev/null`，一个过滤器都不需要。
接了会**静默掐断唤醒**：2026-08-25 实测，裸跑 `card wait` 从账本落账到事件出流
是 274ms；同一条流接上 `2>&1 | grep -vE "INFO"` 后一条不出，直到进程超时退出才
一次性吐出来（3/3 复现，加 `--line-buffered` 也救不回来）。C1.5 那轮工单因此在
无人应答里躺了 23 分钟，而会话把它误判成「镜像没过来」，多挂了一层 task 级订阅。
判据：卡上/任务上明明有新事件，Monitor 却一声不吭——先看自己的命令里有没有管道。

- `--timeout` 是**空闲**上限（距上一次收到任何帧，含不唤醒的 progress），
  必须**大于**对端 agentd 的 stalltimeout（默认 2h），故取 3h。设小了，
  客户端的超时会抢在 agentd 的 stalled 诊断前面退出——把一条带 last_seq 的
  诊断换成一句「我没收到东西」。
- **follow 进程退出本身就是信号**，必须看退出码：
  - `0`：收到真终结事件（`failed` 或 `archived`）→ 先 `show` 确认。两个都是终态：
    `failed` 来自 stop / `resume --force` / 启动失败，`archived` 来自 `done`。
    **`turn_failed`（回合失败）不会让 follow 退出**——任务进 `waiting_review`，
    订阅还活着，照常审核即可
  - `124`：空闲 3 小时一帧都没收到 → **可疑**。正常情况下 agentd 的 stalled
    会先到；先 `handoff show`，再怀疑 agentd 失联
  - 其他非 0：鉴权失败 / 任务不存在 / 连接永久失败 → 看 stderr 按排障表办，
    **不要盲目重开**（401、404 重开一百次还是同样的结果）

醒来 → `handoff show <完整 task-id>` → 处置。**没有重挂这一步。**

`show` 是权威，事件只是唤醒信号——`--follow` 下这条比以前更要紧：事件可能在
你正忙时流入，cursor 已经推进而你还没看。**任何处置前先 show，以 `state` +
`pending_tickets` 为准。**

### 另一个会话只等本任务归档

后续会话的实现依赖当前任务**真正审核归档**时，那个会话挂一条门闩就够了：

    handoff wait <完整 task-id> --until-done --timeout 3h

它不消费协调者的游标，也不把 question / permission_request / completed 送进后续
会话；只在 `handoff done` 产生 `archived` 后输出一行原始事件。退出 `0` 才能开工，
`124` 表示本轮等待到期（任务还没归档），其他非 0 是依赖失败或配置错误。

**它只负责唤醒，不自动 dispatch，也不触发分支自动同步**——下游会话拿到 `archived`
后要自己 `handoff pull`。它也不能替代本任务自己的 `wait --follow` 审核订阅——
工单、completed、`done` 仍由本任务的协调者照常处理。`--timeout` 在这个模式
下是**总时限**，中间帧不续命：否则一个永远没人 `done` 的任务能把门闩拖到天荒地老。

### cursor 语义：为什么 wait 可能吐出旧事件

`wait` 的「不重不丢」靠协调者**本机**的游标文件（`~/.handoff/cursors/` 下按 agentd 地址分命名空间，每任务一个），且**只有 wait 成功交付事件时才推进**。两个直接后果：

- `show` / `reply` 不推进 cursor。走「show → reply」恢复流程之后再挂 wait，第一批返回的可能是**你早已处理过的历史事件**（答过的 question、continue 过的 completed）。
- 换一台机器接管时本机没有 cursor 文件。**`wait --follow` 会在建连前先对账**，
  把水位之前的一切折成一行 `backlog_summary`（带 `missed` / `stale` / `actionable`），
  而不是逐条重放；**对账同时把磁盘游标直接推到水位**——被折叠的积压从此不会再逐条
  交付，要看历史只能 `handoff show`。一次性 `wait`（不带 `--follow`）没有对账机制，
  会从本机游标（换机接管时即 seq 0）起逐条重放。

这不是 bug，是「事件即信号、show 即权威」分工的推论。所以纪律固定为：醒来先 show。发现 reply 返回 404 后，必须用任务实际所在机器执行 `handoff show <task> --target <机器>`，读取 `pending_tickets`：ticket 仍在列表就原样重发 reply；不在列表才按已消耗处理。历史 completed 是否代表当前状态仍由 state 决定。

## 远程派发：代码怎么过去，改动怎么回来

用 `--target <name>` 派发到远程执行机时，两台机器上是**两个独立的 git 仓库**。handoff 不传代码，只做校验和同步——所以「本地写的改动」要靠 git 自己走过去。

### 去程：先 push，再 dispatch

`dispatch --target` 会在**当前工作目录**跑 `git rev-parse HEAD`，把这个 sha 作为「基线」上送。agentd 收到后：查这个 commit 在不在任务仓库的对象库里 → 不在就 `git fetch --all --prune` 一次再查 → 还不在就 **400 拒发**，报文是「基线提交在任务仓库中不存在 …… 请先在本地 git push，或用 --no-sync-check 跳过校验」。

这条机制有三个必须记住的边界：

- **只 commit 不够，必须 push。** 校验的是「远端能不能 fetch 到这个 commit」。没推上去的提交，远程永远拿不到；未提交的改动更是完全不可见——校验会拿你的 HEAD 去比，而 HEAD 不含工作区的脏改动，所以它会**静默通过**，然后 executor 基于一份没有你最新改动的代码开工。
- **项目本身就取自 cwd，所以必须在项目目录里发 `dispatch`。** 项目由当前工作目录的 origin 识别，基线同样取自 cwd。未给 `--project` 时，cwd 不是 git 仓库**直接被拒**。**注意：`--project` 不会跳过基线校验**——跨项目派发时它照样拿 cwd 的 HEAD 去校验目标仓库，会假拒绝；cwd 与目标项目不是同一个仓库时，必须自己加 `--no-sync-check`。
- **新分支的起点是你派发时的本地 HEAD，不是执行机仓库的 HEAD。** agentd 收到基线后，既拿它做存在性校验，也拿它做新分支的起点——两件事出自同一次决议，不会再分叉（B35 之前会：校验的是你的基线，开分支用的是执行机 HEAD，中间可以差出几十个提交而毫无痕迹）。派发成功后 stderr 会打一行 `分支 <名>，起点 <短号>`（B76 起的三件套文案）；执行机仓库比这个起点新时还会补上「领先 N 个提交，新分支不含它们」。
- **`--no-sync-check` 关掉的不止是校验。** 它同时关掉起点决议——没有基线可用时，新分支的起点退回执行机仓库当前的 HEAD（很可能是旧的）。只在 cwd 与 `--project` 指定的项目不是同一个仓库时用它。

稳妥的远程派发姿势：

```bash
git push                                                  # 缺这步必被拒
handoff dispatch --target devbox \
  --new-worktree --new-branch feat/x plan.md              # 起点自动取你当前的 HEAD
```

`--base` 仍然可用，用于**刻意**从别处开分支（比如从某个 tag 或更早的提交起）；给了它就以它为准，也不会再提示分叉。

### 纪律块：agentd 自动注入，别手工拼

派发时 agentd 会按 executor **自动**把执行纪律块注入首回合 prompt（B129）：内置
两版——subagent 版（opencode / claude）与 single-context 版（codex / grok，
未登记的 executor 也走这版，保守方向）。**不要再手工把纪律块拼到 plan 文件头部**：
模板会再注入一份，纪律在 prompt 里出现两遍（codex 因常驻 developer instructions
会有三遍）。

- 按机器覆盖：映射与正文在 Web 控制台改（设置页编正文、开发机详情配
  executor→纪律映射），落盘即生效，不必重启 agentd。CLI 没有开关。
- 派发成功后 stderr 回显 `纪律块: <来源>`（如 `内置:single-context` /
  `配置:my-rules.md`）——这是你确认「注入了哪版」的唯一入口，派发后瞄一眼。
- 纪律块文件不可用时 agentd 直接拒发（500 带真因），不会静默不注入。

### 回程：wait 自动 fetch，合并是你的决定

回合结束（`completed` / `turn_failed`）时 `wait` 会自动把远程任务分支同步回来（配置 `sync.auto` 默认开，`--no-sync` 可关；`--follow` 下**每个回合结束都同步一次**，`archived` 与 `--until-done` 不触发）；也可以随时手动：

```bash
handoff pull <task> --target devbox
```

`pull` 主路径走 agentd 的 **HTTP bundle**（复用已有连接与鉴权，不需要 ssh）；只有对端 agentd 太旧不支持（404）才回落老的 ssh fetch，其他任何错误如实报错、不回落。落到**当前工作目录**的仓库，**只 fetch，不 checkout、不合并**——合并进你的主线是审核决定，handoff 不替你做。

去程回程都以 cwd 为准，所以 `dispatch` / `wait` / `pull` 最好都在同一个本地仓库目录里发。`--target` 机器配置里的 `user` 字段（ssh 用户名）**只在回落 ssh 老路时**才用得上——对端 agentd 够新时它完全不参与；ssh 老路在 Windows 执行机上不可用。`attach` 走 agentd 的 render 流，从来不需要 ssh。

本机派发（不带 `--target`）完全不走这一套：代码本来就在同一台机器上，基线校验直接跳过，`pull` 也会告诉你「本机任务，无需同步」。

**项目由 cwd 识别，第一次派到某台开发机会自动登记。** 你不需要（也无法）告诉
handoff「代码在那台机器的哪个目录」——那是它自己的事。首次派发会多一次往返，
远程可能含一次全量 clone（落点已存在时会直接认领、不重复 clone），stderr 会打出
「正在让 <机器> 落地项目 …」。

**在 worktree 里派发会归并到主仓。** 项目位置永远是主工作树，不是你当前所在的
那个 worktree。想接着某个分支干，用 `--base <分支>` 显式表达。

### 重连/补挂后的第一行：`backlog_summary`

`wait --follow` 每次建立连接前都会对账一次。本机 cursor 之后有积压时（断网重连、
忘挂之后补挂、换机接管），它先吐**一行**摘要再转入实时流：

    {"type":"backlog_summary","task_id":"…","from_seq":2489,"to_seq":2537,
     "state":"waiting_answer","missed":14,"missed_truncated":false,"stale":11,
     "actionable":[{"id":"…","kind":"gate","request":{…}}]}

怎么读：

- **`actionable` 是权威的「你还欠什么」**，每张带完整请求原文，可直接
  `reply --ticket <id>`。它**不限于间隙内**——断网前你就看见过、一直没答的也在里面。
- `stale` 只是间隙里已经被审批链答掉的工单数，不能单独决定某个 404 是否可跳过。遇到 404，先在任务实际所在机器执行 `handoff show <task> --target <机器>`，检查 `pending_tickets`；仍在列表就原样重发，列表没有才跳过。不要把 backlog_summary 的计数当成当前欠办清单。
- `missed_truncated` 为 `true` 时，`missed` / `stale` 的语义是「**至少**这么多」
  ——快照的事件窗口没覆盖到 cursor。此时 `actionable` 仍然精确。
- 摘要行**不是**事件，`agentd` 不存这个类型；它只在客户端合成。

积压事件不会再逐条推给你——那会让一次重连变成 N 次会话唤醒。要看被折叠掉的历史，
用 `handoff show`。

## 事件分诊表

`wait` 返回的 JSON 形如 `{"seq":N,"task_id":"...","type":"...","payload":{...}}`。按 `type` 分诊：

| type | 含义 | 你要做的 |
|------|------|---------|
| `permission_request` | executor 要执行一个需授权的操作 | 判断后 `reply <task> --ticket <id> --approve` 或 `--deny --reason "..."` |
| `question` | executor 卡在一个需求取舍上 | `reply <task> --ticket <id> --answer "..."` |
| `completed` | 一轮干完了，任务进 `waiting_review` | 进入审核：`diff` → 决定 `continue` 还是 `done` |
| `turn_failed` | 一轮以失败收尾，任务进 `waiting_review`（executor 会话还在） | 与 `completed` 同路：`diff` 取证后 `continue` 续接重试或 `done` 归档。follow 订阅**不会退出**，不用重挂。**别急着重新 dispatch** |
| `failed` | 任务真终结：`stop`、`resume --force`、启动失败、看门狗补正 | follow 随之退出（码 0）。先 `show` 取证；想继续只能重新 `dispatch` |
| `approval_dropped` | 你**批准**的裁决没送到 executor（回合已结束），agentd 已代回一个 reject | 后果比 deny 丢失重：那一步被打断了。`continue` 让 executor 重跑该步 |
| `resource_pressure` / `task_proc_pressure` | 执行机进程余量告警 / 单任务进程数越预算 | 看 payload 的 `used/limit`（或 `used/budget`）；连续告警时 `attach` 查 executor 是否在泄漏进程 |
| `archived` | 任务被 `done` 归档，`payload.note` 是协调者留的完成说明 | 这是任务真正结束的信号。等这个任务的下游会话据此开工；自己是协调者时无需动作。只有 `wait --until-done` 把它当成功信号；`wait --follow` 收到它后随连接正常结束 |
| `delivery_failed` | 裁决落库了但没送到 executor | **`handoff resume <task>`**（详见排障） |
| `stalled` | 看门狗：长时间无产出 | `attach` 或 `show` 判断 executor 是真死还是在长跑：真死就 `stop`；若模型其实已干完（如 `attach` 能看到结果、`git log` 有新提交）而事件流停在 `question`/无终态，那是 agentd 断连窗口丢了终态事件——**先 `handoff resume <task>` 对账补回**（自动补发后任务会自然迁移），判不出再 `handoff resume <task> --force` 收口，`stop` 是最后手段 |

`ticket_id` 在 payload 里，**一次性消耗**。同一个 ticket 回答两次，第二次 404。

## 审批：批什么，不批什么

`--approve` 批的是**这一条**操作，不是一类操作的长期授权。两个自动化例外要心里有数：同一任务内**等价**的权限请求会自动复用你先前的 allow——判等不是逐字比对，而是三域指纹（命令域 / 路径域 / 全文域，B91），同一条命令换个包装也会命中（`permission_reuse` 事件留痕，跨任务不复用）；还有一档**静态规则自动放行**，根本不会来问你，B249 起覆盖三类：**落在任务范围内的写入**（任务工作区、任务私有目录、任务临时目录三个根；共享的 `/tmp/<executor>` **不在**范围内，写它照旧升级）、**已知安全命令**（`go build|test|vet`、`gofmt`、`npm test|run`、`make`、`ls|cat|grep`、`git status|diff|log`，以及 charter 台账纪律的法定动作 `git add <范围内路径> && git commit --amend --no-edit`）、**handoff 自身的只读子命令**。白名单匹配命令主体形态而非子串，`echo "go test"` 不会被放行；未登记的 `handoff` 子命令一律 fail-closed。命令白名单的每次放行都**补一条事件**，所以「这一段静默放行了什么」能从事件流查到，不必开 Debug 日志。

**`--deny` 一定要带 `--reason`**。理由会随应答回到模型手里；不给理由，模型只知道「被拒了」，下一步大概率原地再试一次同样的操作，白烧一轮。理由是否送达的留痕分执行器：claude **与 agy** 的理由与裁决**同帧送达**，事件历史里**不会**有留痕事件——没有留痕不等于没送达，反而是送得更早；其余 executor（opencode / grok / codex）走带外注入，事件历史里有 `deny_guidance_relayed` / `deny_guidance_dropped`。

```bash
handoff reply <task> --ticket <id> --deny --reason "别装全局包，加到 go.mod 里"
```

**这些别自己批，升级给用户**：删除数据、`git push` / 改写历史、往外部服务写入或发布、装全局依赖、动 CI/密钥/生产配置。你是替用户看着这个 executor 的，不是替它签字的。判断不了就把 `permission_request` 的原文贴给用户问。

## 审阅取证

任务进 `waiting_review` 后，三条只读命令帮你判断改得对不对：

```bash
handoff diff <task>                      # git diff + 提交列表（主要素材）
handoff diff <task> --base main          # 指定比较基线
handoff fetch <task> internal/foo/bar.go # 读任务仓库里的单个文件
handoff run <task> go test ./...         # 在任务仓库执行命令（sh -c，10min 超时）
```

`handoff diff` 默认用任务自己的基线提交，没有才按仓库默认分支推导。所以默认 diff
就是这个任务的改动，不再含 base 分支与任务分支之间的历史。

**`handoff run` 的参数顺序有坑**：handoff 自己的 flag 必须写在 `<task>` **之前**，任务名之后的一切（含 `-v`、`--race`）都原样透传给被执行的命令。

**`handoff run` 的参数按个数分两档**：

- **只给一个参数** = 一条 shell 命令原文，原样交给远端 `sh -c` 解析：
  `handoff run T1 "cd web && npm test"`
- **给多个参数** = argv，逐个做 shell 转义后再拼接。你敲的引号、空格、元字符
  原样到达远端：`handoff run T1 grep -rn 'foo bar' .`

B66 之前多参数形态是直接空格重拼的，`'foo bar'` 到远端会变成两个参数——静默失真，
不报错。

```bash
handoff run --target devbox T1 go test -race ./...   # ✅ --target 在任务名之前
handoff run T1 --target devbox go test ./...         # ❌ --target 会被当成 go test 的参数
```

## 改与收

```bash
handoff continue <task> "把重试次数改成 3，并给这个分支补一条失败用例"
handoff done <task> --note "已验收：重试与失败用例都符合预期"
```

- `continue` 是**同一会话续接**，executor 的上下文完整保留——不需要在指令里重述前情。
- `continue` 之后任务回到 `running`；follow 订阅还活着时会继续收到新一轮事件，**不需要重挂**。回合失败（`turn_failed`）同样不断订阅——只有真终结的 `failed` / `archived` 才让 follow 退出，而那之后也没有 `continue` 可言。
- `done` 归档任务并回收 executor（停进程、删 managed worktree；**任务目录不删**，留作排查素材）；`--note` 的说明会写进任务记录与 `archived` 事件，等这个任务的下游会话靠它知道结果。对已是 `completed` 的任务重发 `done` 幂等返回 200，不算错。

**`done` 返回成功之前，什么都不要删。** `done` 会因状态不符被拒（409）；如果你已经先手删了任务目录或杀了 executor 进程，就会留下一个 agentd 记着、但资源已经被你拆掉的孤儿，只能手工补清。顺序永远是：先 `done`，看到 `{"ok":true}` 再谈清理。

`handoff stop <task>` 是另一条出口：主动中止，停 executor、作废挂起工单、任务落 `failed`。任务跑偏了不想再等，用它。

## 账本模式：把任务回路包在卡回路里

> **前置条件：本项目的 agentd 配了 `ledger.enabled: true`。**账本是可选功能，
> 默认关闭——没开就跳过整节，按上面的任务回路做即可（`card` 命令会直接报
> 「账本未启用」）。

账本模式**不是第二条主循环**，是把上面那条包了一层：

- **外层（本节）**管「卡」的调度——哪张卡该开工、派给谁、做完推到哪个状态。
- **内层**就是上面的任务回路，一字不变——醒来处置 `permission_request` /
  `question` / `turn_failed` 用的还是 `reply` / `approve` / `continue`，
  事件分诊表、审批硬纪律、审阅取证、排障各节**原样适用**。

一句话记法：**`card` 族管卡，执行域动词管 task，两者分层不混用。**

### 卡命令族速查

| 动作 | 命令 |
|---|---|
| 建卡 | `handoff card add "<标题>" --project <项目> [--workflow <流>] [--priority 高\|中\|低] [--parent <父卡>]`（不指定流时按账本内流集合解析：零条先建流、唯一条自动使用、多条要求显式指定） |
| 按原号导入 | `handoff card import <B号> "<标题>" --project <项目> --source <来源>`（撞号即拒） |
| 看板 / 单卡 | `handoff card list [--status <列>] [--needs] [--all] [--json]` / `handoff card show <id>` |
| 挂附件 / 改卡 | `handoff card update <id> --attach <kind>:<仓内相对路径> / --title / --priority / --accept` |
| 移列 | `handoff card move <id> <状态>`（CAS；`--expect` 钉前值；gate 拒绝会说清缺什么） |
| 跨流迁移 | `handoff workflow migrate <id> --workflow <流> --column <落点列> --yes` |
| 记一笔 | `handoff card note <id> "<正文>" [--correction] [--reset-node <节点>]` |
| 记验收 | `handoff card accept <id> --evidence "<命令+结果>"` / `--unverified` |
| 拆子卡 / 阻塞边 | `handoff card split <id> "<子卡标题>"` / `card link <阻塞者> <被阻塞>` |
| 等人标记 | `handoff card needs <id> "<原因>"` / `--clear` |
| 搁置 / 复活 / 终止 | `handoff card close <id> --reason 搁置\|取消\|废弃` / `handoff card revive <id>` |
| 工作流形状 | `handoff workflow show <流>`——**列序与门以账本为准，任何文档都不复制** |
| 坐下 | `handoff card bind <id>`（空座；见「占座」） |
| 叫机器人 | `handoff card coordinate <id>`（空座；在 leader 载体机器的项目主工作树打开控制台 TUI tab；TUI 用宿主 session 自己 bind） |
| 换绑 | `handoff card rebind <id> --self` 或 `--launch`（有人；见「占座」） |

### 占座：三颗按钮，建卡不占座

一张卡一个协调者席位，身份是这场对话的 CLI + session id。建卡、领卡、`note`、
`move spec` **都不占座**。`--coordinate` 已废止，传了会失败并指向
`card coordinate`。

| 要做什么 | 命令 | 空座 | 已有人（含旧人尺度席位） |
|---|---|---|---|
| 我来坐 | `handoff card bind <id>` | 当前对话入座 | 拒绝，走换绑 |
| 叫机器人 | `handoff card coordinate <id>` | 从协调者小队拉起 | 拒绝，走换绑 |
| 换人 | `handoff card rebind <id> --self` 或 `--launch` | 拒绝，用上面两颗 | `--self` 这场对话接班；`--launch` 新叫机器人接班 |

`--self` 与 `--launch` 必须二选一。没有 `--to` / `--carrier` / `--expect`。
`--cli` / `--session` 是命令本地 flag，仅属于 `card bind`、`card rebind --self`、
已有席位的 `card dispatch --step` 和 `kind != user` 的 `room send`；两项必须成对。
`card rebind --launch` 与 `card coordinate` 不接受这两个 flag。
坐下 / `--self` 不查小队；叫机器人 / `--launch` 才查。普通终端出示不出会话身份时，
`bind` / `rebind --self` 失败，空座上仍可 `coordinate`。浏览器页不能坐下。

坐下立刻返回，不代替 wait：这场对话接着挂 `handoff card wait <id>`（已经挂过就
不要再挂）。叫机器人由那边的无头会话收消息。

`--step` **不占座、不换座**。空座可以派；有人则出示必须等于席位，否则拒绝并提示
`rebind`。`takeover` 一律失败并指向 bind / coordinate / rebind。`release` 空座幂等
成功，有席位失败并指向 `rebind`。

`kind != user` 的 `room send`（`escalation` / `closing` 等）也要出示这场席位；出示
失败改 `--kind user`，或先 `bind` / `rebind --self`。

### 状态不会自己流转

账本里每一次状态转移都必须有 actor 落进事件流（取证要能回答「这步是谁推的」），
所以没有「自动变态」这回事。区别只在**谁**去推：

| 谁 | 推什么 |
|---|---|
| **代码自动** | `--step` 取得运行锁（**不写席位**）；有席位则出示必须匹配；裁决 pass 自动进下一列、fail 退回上一节点再来一轮；声明了 `produces` 的节点在 pass 时自动挂附件再路由；一切失败出口打「等人」标记 |
| **你（主会话）** | 逐节点点火 `card dispatch --step`（**连点就是跳过协调者检查点**）；人工列（spec / acceptance / finish）做完后自己 `card move`；定级跳边 |
| **人** | 答裁决、批工单、合 main、推「已完成」 |

- 节点声明的法定产出路径必须逐字使用；不要在 basename 前加 `YYYY-MM-DD-` 日期前缀。带日期前缀的是历史文件，不是本节点的法定产出；写错时按 prompt 给出的法定路径改名。

一期你就是那台发动机。三期规则引擎接手「按按钮」的活，调的是同一个环节执行体。

### 1. 唤醒先查账，不信会话记忆

被唤醒（新会话接管、压缩后续跑、隔了一夜）后**先重建现场再动作**：

```bash
handoff card list --needs          # 需要你的：等人标记 + 挂卡裁决
handoff decision list              # 未答复的裁决（含项目级、无卡可挂的那些）
handoff card show <id>             # 在飞的卡：字段 + 关系 + 挂账 task + 事件流
```

修复回合计数、环节走到第几轮这类推进状态**一律从事件流推导**，不存会话记忆——
和「`show` 是权威、事件只是信号」是同一条纪律。

### 2. 派发前查账，防重复开工

```bash
handoff card list --project <项目>     # 落在执行列（既非「待办」也非「已完成」）的就是在飞的
```

这条是「重复开会话把同一件事又做一遍」的正解。**别按 `--status 进行中` 筛**——
现役 charter 流没有这一列，那条查询稳定返回**空表**，看起来像「没人在做」
（2026-08-24 实测；这是文档腐烂里最阴的一种，它不报错）。

真撞上了通常不会出事：`--step` 派发前先取得运行锁，第二个会话干净失败并报出持有者。席位仍按「占座」节，不在这里写入。
入口失败（运行锁占用、派发失败）落卡事件：comment + `needs_human`，`card wait` 收得到。

### 3. 外层派发与等待

```bash
handoff card dispatch <id> --step <节点名>   # 走工作流节点（节点名 = 看板列名）
handoff card wait <id> [--subtree] [--timeout 3h]
```

- **裸 `card dispatch`（不带 `--step`）不要用在卡驱动上。** 卡驱动一律走 `--step`
  ——它才带节点语义（自动挂卡、模板与纪律块快照、裁决路由）。占座只走「占座」三颗按钮。
- `--step` 会自动做三件事：**取得运行锁**（**不写席位**；有席位则出示必须等于席位，
  否则拒绝并指向 `rebind`；纯人工节点跳过运行锁）、把 task 回链到卡、把模板版本与
  纪律块 hash 快照进派发事件。**「挂卡」不是一个你要单独做的动作。** 占座只走上面
  「占座」三颗按钮。
- **`--step` 提交后先短等首态**：CLI 只把请求交给本机 agentd，HTTP 仍是 202，编排仍在
  agentd 里异步运行。CLI 在 POST 前记下本机账本 seq 水位，最多短等约 20 秒，只看这次水位
  之后的卡事件：看到 `dispatched` 就在 stdout 打出目标机、新分支 `branch`、起点分支
  `base`、起点短号和纪律块名；看到 reason=`派发失败` 的 `needs_human` 就把卡上
  `haltForHuman` 的 comment 正文写到 stderr 并以非零退出。HTTP 202 之前的 404/400/409、
  纪律探活/拒发闸 400 仍当场失败。短等窗口内没有这两类首态时，stdout 打「已受理，首态未到；
  进展见 handoff card wait」，退出 0；命令返回值不带回合结论。不要用 task WS 或卡上历史
  `dispatched` 推断这一次派发。CLI 与 agentd 必须同批升级。
- 本机卡派发省略 `--target`；不要在 `targets` 登记指向本机 loopback 的自机。`--target 本机`
  不是合法键；版本不一致时的「目标机未定」仍是版本 skew 文案，不表示本机 target 缺失。
- **节点绑了小队（charter 派发列的 runner）时，禁止 `--target` / `--executor`。**
  机器和 CLI 由 Admit 选。点名会被代码拒绝，不是纪律劝告。pass 后执行机把工作分支
  `git push origin`；下一台 fetch 续接。没推上去就跨机，仍走原来的跨机锁并
  `needs_human`。日常不要用 `--base` 当跨机逃生口。
- 裁决落在卡的事件流（`review_verdict`）：pass 自动进下一列，fail 退回上一节点
  再来一轮；裁决解析失败或超轮打 `needs_human`，人工裁决后把结论 `note` 落卡，
  重派前 `card needs <id> --clear`。
- 一次性覆盖：绑小队的节点不能 `--executor`；`--model`（B203）、`--extra "<本轮补充>"`（进 prompt
  的「本次补充」小节，不落卡、不影响后续轮次）、`--discipline-override <角色>`（应急）。
- `card wait` 跟的是**账本单流**（卡或整棵子树的事件，含镜像进来的 task 事件），
  不是 task 集合——所以挂起期间新拆的子卡、新派的任务天然进流，没有动态成员问题。
- **建连第一行是 `card_snapshot`**（B356）：`actionable` 是当时成员集上未决工单
  （`ticket_id` / `source_task` / `source_target` / 子卡 `card_id`），`needs` 是未清
  的等人标记。建连前已经镜像到子卡的工单靠这一行，不靠回放。之后的新事件仍是
  `task_mirrored`。处置工单与任务回路相同：`show <task> --target <source_target>`
  再 `reply`。
- **一次工作流只挂一次 `card wait`，不必再叠 task 级 `wait --follow`**。唤醒语义
  与 `wait --follow` 同款：逐条事件即时流出、命令不退出、不用重挂；工单
  （`question` / `permission_request`）由镜像子系统转成 `task_mirrored` 进卡流，
  只跳过 `progress` / `approver_decision` / `approver_disabled`
  （`internal/ledgermirror/mirror.go` 的 `mirrorSkip`）。**卡流该有的事件却没动静时，
  先查自己的命令有没有接管道**（见上文「订阅」一节的过滤器禁令），别先怀疑镜像。
  父卡 coordinate、子卡空座时，工单会冒泡叫醒父卡；bind 席位仍只靠本命令 stdout。
- 醒来之后**处置方式与任务回路完全相同**：先 `handoff show <task>` 以 state
  为准，再按事件分诊表办。别在这里另发明一套。

### 4. task 完成之后的推进

```bash
handoff card dispatch <id> --step <节点名>   # 触发下一个派发列（节点名 = 看板列名）
handoff card accept <id> --evidence "go test ./... 全绿"
handoff card move <id> <下一列>              # 人工列的跳转（如 acceptance → finish）
```

四条要点：

- **各流的列序与逐节点卡操作对照表在 `product-backlog` skill 的「推进 charter 流」**
  ——那边是驾驶手册。工作流定义由用户或上层方法论安装，handoff 出厂不预设任何流；
  本节不复制某条流的列序，只说明流无关的通用机制。建卡未指定流时，账本按流数量作
  唯一解析：空账本指向先建流，恰好一条自动使用，多条必须显式指定。
- **审阅类节点的 fail 会自动 `continue`**（带发现项原文），**3 轮封顶**，超限自动
  打「等人」。要人工重置计数用 `handoff card note <id> --reset-node <节点名>`
  （注意这个 flag 仍叫 `--reset-node`：「节点→环节」改名只动了
  `card dispatch` 的 `--step`，没顺带改它）。
- **`card accept` 的「已验」必须带 `--evidence`**——已验是一个断言，无证据的
  断言不许落账。还没验就用 `--unverified`。
- **合并主线永远人工**：现役各流都没有自动合并节点——charter 流的 finish 是
  人工列，合并归人（`charter:finish`）。账本里不存在「跑完自动合 main」这回事。

`card move` 的每一步都拿卡钉的那版工作流当法律——状态名合不合法、gate（如
「进 `implement` 需 plan 或 breakdown 附件」）过不过，全按配置判。被拒了先 `handoff workflow show
<name>` 看形状，别硬推。

### 5. 回合末四分法落账

聊天里的长报告照旧写，但那四类信息**必须同时落进账本**——不然并行几个会话时，
真正需要用户的两三行会被战报淹没（这正是账本要解决的痛点）：

| 报告里的 | 落账动作 |
|---|---|
| 完成项（带证据） | `card move` 推状态 ＋ `card accept --evidence` 记验收 |
| 更正 | `card note <id> --correction "<更正内容>"` |
| 请示裁决 | `decision open "<正文>" [--card <id>] [--option A --option B]` |
| 阻断需人工 | `card needs <id> "<原因>"`（原因必填，解除用 `--clear`） |

`decision open` 不挂 `--card` 就是项目级裁决（如「推不推汇流线」），照样进
「需要你」。**open 的裁决不答复不消失**——用户从看板清账，不再从聊天记录里捞。

### 6. 验收后发现 bug：开新卡，不 reopen

```bash
handoff card add "<标题>" --project <项目>          # 缺省按账本流集合解析；多流时显式加 --workflow
handoff card note <新卡> "发现自 <原卡 id> 的验收"
# 定性后按级别走 charter：L1 挂 spec+plan 合体页跳 implement（见 product-backlog）
```

账本历史不改写，与 task 机的「归档了就是归档了」对齐。

**别用 `card link` 挂血缘**：它加的是**阻塞边**（`link <blocker> <blocked>`，
前者阻塞后者），语义完全不同——把新 bug 卡和原卡 link 起来，等于声明其中一张
挡着另一张开工。血缘关系（`discovered_from` / `relates`）在数据模型里有，但
**今天 CLI 与 API 都造不出来**，所以先用 `card note` 把出处记进 timeline。
真正需要挂边时，用 `card link` 表达的必须确实是「A 没完成 B 不能开工」。

### 账本模式的红旗

| 念头 | 事实 |
|------|------|
| 「状态应该会自动流转吧」 | 每次转移都要 actor。一期的发动机是你，不是数据库。 |
| 「我记得这张卡已经推到 review 了」 | 和 task 一样：`card show` 是权威，会话记忆不是。 |
| 「审阅没过，我再 continue 一轮」 | 环节自己会 continue，3 轮封顶。手工绕开封顶就是绕开防死循环的安全阀。 |
| 「验过了，accept 一下，证据就不写了」 | 已验必须带证据，命令会直接拒。这是取证文化不是输入校验。 |
| 「节点跑完就合进 main 了吧」 | 主线永远人工。合并发生在 finish 人工列，由你本地做。 |
| 「先 `handoff dispatch` 派了，回头再挂卡」 | 那样出来的是「未挂账」task，重复开工检测看不见它。要挂卡就用 `card dispatch`。 |
| 「卡的事件流里没有，那就是没发生」 | 镜像可能滞后。看板会显式标「事件流滞后」，`card show` 的挂账 task 也能对账。 |
| 「开卡即绑 / `card add --coordinate`」 | 建卡不占座。该 flag 已废止。要坐走 `card bind`，要机器人走 `card coordinate`。 |
| 「`--step` 会把我写成协调者」 | 派发不占座。席位只由 bind / coordinate / rebind 写。 |

## 协作房间纪律：升级简报、收口摘要与重建

> 前置条件：本节三款走 B156.2 协作房间层（房间、简报、收口摘要都是账本事件流，
> 契约见 `docs/superpowers/specs/b156.2-contract.md`）。该层未部署的机器上按原样
> 回退：请示用 `decision open`、过程记录用 `card note`——「回合末四分法落账」不变。

### 升级简报：六段契约（缺段拒收）

推翻级偏差停卡后发升级简报等答复。
简报是「对 spec 的 diff」的载体契约，**缺段拒收，格式是契约不是模板建议**。六段：

1. **一句话**：什么被推翻/要动什么，挂卡号；
2. **商定 vs 实测**：spec 第几条写的什么 ↔ 实际发现什么，证据用链接锚进 timeline（测试原文、文件清单、轮次），正文不贴代码；
3. **根因**：为什么当时没预见（世界变了/当时信息不足/理解偏差）；
4. **影响半径**：动哪条判据/范围/契约，波及哪些卡；
5. **选项 + 后果 + 推荐**：2~3 个，只要可行必含「维持原商定」（防简报变成既成事实追认书）；
6. **停在哪**：不答复卡停在什么状态、多久后再提醒。

拒收判据（发出前自检，收到时执法）：六段任缺一段即退回补写，不得代答、不得跳过；
第 5 段没有「维持原商定」选项视同缺段；证据没有锚进 timeline（正文贴代码或口说
无凭）视同第 2 段缺失。

简报的语言是「对 spec 的 diff」，不是对代码的 diff——spec 是用户注意力驻留过的
地方，这是把用户语境接回来的最短路径。

### 收口摘要：四段与偏差上报义务

卡到终态时，协调者发的最后一条消息是收口摘要，四段：

1. 做了什么（一段话）；
2. **全部偏差清单**：推翻级裁决结果 + 填补级逐条——自主消化的不许消失，这是安全网执法点；
3. 验收证据指针（timeline 锚）；
4. 残余去向（落 roadmap/新卡的逐条链接）。

父卡收口另带子卡汇总（几张顺利、几张有值得读的偏差、链接）。

执法点两条：

- **协调者侧**：第 2 段不是选填，缺「全部偏差清单」不得发收口；「没有偏差」也是
  一行明写的事实，不许留白。
- **执行者侧**：「执行中偏差如实记入 ledger 收尾段」——填补级偏差账的完备性来源
  （该义务经 charter-implement 纪律块下发，种子见
  `docs/superpowers/disciplines/b156.2-charter-implement-seed.md`）。执行者回合
  收尾必须如实列出本回合自主消化的偏差，协调者汇总进上面的清单；任何一侧漏列，
  安全网即破。

### 换绑与重建四步

限额、载体更换、接管：按「占座」节换绑，接班者只能是当前对话或新叫的机器人：

```bash
handoff card rebind <id> --self      # 这场对话接班
handoff card rebind <id> --launch    # 新叫机器人接班
```

没有 `--to` / `--carrier` / `--expect`。换绑写入即撤销旧会话的房间写权与推进权
（防旧会话醒来继续发消息推卡）。新任协调者开局先做**重建四步**，一步不跳：

1. **读卡**（字段/附件/验收判据）：`handoff card show <id>`；
2. **读卡会话史**：房间页或 `handoff room read`——会话史天生是交接简报，第一读者
   是用户，第二读者是你；
3. **读 timeline**：`handoff card show <id>` 的事件流；
4. **读仓内文档**：spec / 契约 / plan 等附件与分支上的相关文档。

配套义务：**凡承重的必须落账**——跨 agent 换载体时旧 transcript 不可携带，账外无
不可再生之物。限额检测/自动拉起/自动换绑归三期。

### 叙事文体：同事协作叙事（三问齐全，正文不贴代码）

技术细节的家是卡 timeline，语义现状不变（timeline 是账，房间是播）。
文体：同事协作叙事——讲发生了什么/为什么/意味着什么；凡引证据给链接，正文不贴代码。

书写者规则：叙事类消息的书写者是房间成员中的协调者实体，executor 与非成员不可写；
无协调者在场时房间面退化为机械信号（工单上浮、needs-human、指针），不伪造叙事，
接管者唤醒后补写。

文体三条，发出前自检、收到时执法：

1. **三问齐全**：每条叙事消息要答得了三个问题——发生了什么／为什么／意味着什么。
   只报状态不说原因＝缺「为什么」；说了变化不说影响＝缺「意味着什么」；缺任何一问
   即不合规，退回补写。
2. **证据给链接，正文不贴代码**：证据一律给引用锚（git 路径 / timeline 锚 / 卡号 /
   附件），正文出现成块代码或日志原文即不合规——技术细节回卡 timeline 去，那里才是
   它的家。
3. **叙事不是心跳**：「开始了/进行中/跑测试了」这类进度播报不进房间——没消息 = 按 spec 在走；
   白名单的敌人是心跳与日志，不是对话。

拒收话术（审阅者拿它对任一条具体消息说出 X 才算执法）：「这条缺『为什么』」/
「这条正文贴了代码，证据没给链接」/「这是心跳消息，不在白名单」。

## 会话恢复：从零接管

一个完全没有前文的新会话，先重建现场：

1. `handoff tasks` —— 每行任务 JSON 现在带 `watchers`（有几个连接在听）
2. 给每个 `watchers == 0` 的**活跃**任务（`pending` / `running` /
   `waiting_answer`）补开一条 follow 订阅（无后台机制的 harness 改为前台
   逐轮 wait，见「在 agent 会话里挂 wait」）。`waiting_review` 不用补：
   它在等你裁决，挂几天都正常。补订阅与播报分开：补订阅照旧对上述所有
   `watchers == 0` 的任务做；向用户播报需处置事项时，只报无挂账卡或卡上
   没有 `DriverSession` 的真孤儿。有驱动归属的任务不打扰用户，恢复报告里
   最多列一行事实，例如「卡 B177 由 <session> 驱动，已补挂订阅」
3. `handoff show` 逐个清 `pending_tickets`

`handoff status` 会把归属结论直接标在活跃任务行上：真孤儿是 `⚠ 无人值守`；
有卡驱动但暂时无人订阅的是 `⚠ 无人订阅（卡 <id> 驱动 <session>，心跳 <时长>）`。

`pending_tickets` 是关键——它是「我还欠哪些没答」的权威清单。把里面每张工单
`reply` 掉，然后按当前 state 决定：`running` → 订阅已在（follow 不需要重挂）；
`waiting_review` → 进审核。接管后第一次 wait 可能重放历史事件（见「cursor
语义」）——照旧以 `show` 为准即可。

会话崩溃、主动关掉重开、换一台机器接管，三种场景都是这一套。**不要**因为「我不记得这个任务了」就重新 dispatch 一个——先 `tasks` 看有没有。

## 确认 agentd 在不在

    handoff status --target <名字>

**不要 ssh 上去查进程、查端口、查二进制。** 那是在验证零件，而问题是「这个服务
现在能不能用」；零件检查有无数种失败方式（PATH、平台差异、引号嵌套），每一种都
长得像「没有」。

| 输出 | 结论 | 处置 |
|---|---|---|
| 正常一屏（版本/数据/任务） | 能用 | 直接派发 |
| `可用（版本过旧）` | 能用，但远端 agentd 不支持 status | 想看详情就升级远端；不看也不影响派发 |
| `target "x" 未在配置 … 中定义` | 你的本机配置问题，不是远端问题 | 补 target 配置 |
| `dial tcp …: connect: connection refused` | 真的没有 agentd 在跑 | 见下面的红线 |
| `状态码 401`（通用报错，无专门提示） | agentd 在，但 token 对不上 | 同步两边的 token |

退出码：**0 = 能用**（含版本过旧）；**1 = 够不着**。

**红线：查到有 agentd 在跑就复用它，不要起第二个。**
两个 agentd 抢同一份数据目录、同一批 worktree 与 executor 进程，正是状态机最怕的
失配。这条现在由代码兜底——同一个 DataDir 起第二个 agentd 会直接被文件锁挡下
并报错，什么都不会被改动。别把它当逃生口：它挡的是事故，不是流程。

**升级 agentd 要先停旧的再起新的。** 以前是新进程起来撞端口失败（但那时破坏
已经造成了），现在是新进程被锁挡在门外。好处是安全，代价是不能再指望「起个新
的把旧的顶掉」。

活跃任务行末尾的存活结论有三态：`executor 存活` / `executor 已不在（理由）` /
`存活性未知（理由）`。**`未知` 不等于 `已不在`**——探不出结论时不要按「死了」
处置，先看理由。

## 排障

| 症状 | 根因 | 处置 |
|------|------|------|
| 任何命令 404「任务不存在」 | 传了 8 位短 id | 用 `handoff tasks` 取完整 UUID |
| `continue` / `done` 报 409 | 任务不在 `waiting_review` | `handoff show` 看真实状态，按状态机表办 |
| `reply` 返回 502，或收到 `delivery_failed` | 裁决已落库但没送到 executor（executor 半死） | `handoff resume <task>`：幂等重投；executor 还在就继续跑，确已不在则转交审核 |
| `resume` 之后 `reply` 404、`attach` 看不到挂起项 | 工单已被消耗 | 正常。按 `resume` 报告里的结论走 `continue` 或 `done` |
| `wait` 立刻报错退出 | 401（token 与 agentd 不一致）或 1008（task-id 错） | 看报错原文，修 `~/.handoff/config.yaml` 或核对 id。**别重开**，它不会自己好 |
| 远程任务的 `wait` / `show` / `reply` 报 `task not found`（1008 / StatusPolicyViolation），id 明明是刚 dispatch 出来的 | **漏了 `--target`**，命令打到了本机 agentd——任务在执行机上，本机当然没有 | 看 stderr 里的 `addr=`：是 `127.0.0.1` 就是漏了 `--target`。补上重发即可，任务本身没事 |
| `wait` 一直不返回 | 通常只是还没有事件 | 正常。stderr 的重连日志也正常。加 `--timeout` 兜底 |
| 重开 follow 后吐出旧事件 | cursor 只在 wait 交付时推进；show/reply 不推进，换机接管从 0 起 | 以 show 为准；若 reply 404，先在任务实际所在机器执行 `handoff show <task> --target <机器>` 并检查 `pending_tickets`；仍在列表就原样重发，不在列表才跳过 |
| `dispatch` 报「工作区不干净」 | **执行机上**的任务仓库有未提交/未跟踪改动 | 在执行机上提交或 stash 后重试（`--new-worktree` 可绕开主工作区的脏检查，但主仓库仍需可用） |
| `dispatch` 报「本地工作区有 N 处未提交的已跟踪改动」 | **你本地**（不是执行机）有改动没提交，远程派发的基线不含它们，executor 会基于旧代码开工 | `git commit` 或 `git stash` 后重试；确认这些改动与本次任务无关时加 `--allow-dirty`（放行仍会打印被忽略的文件） |
| `dispatch` 报 400「基线提交在任务仓库中不存在」 | 本地 HEAD 没 push，或执行机 fetch 不到（无凭证/网络不通） | `git push` 后重试；报文里的 fetch stderr 是根因原文。确实是不同仓库才用 `--no-sync-check` |
| 远程派发成功，但 executor 基于旧代码开工 | 改动只 commit 没 push——校验拿 HEAD 比，HEAD 不含未提交改动，会静默通过 | 派发前先 `git push`。起点本身不用管：新分支自动落在你派发时的 HEAD 上，stderr 的「分支 …，起点 …」行就是实际起点 |
| `continue` 报 500 / 恢复失败 | executor 进程死了但 agentd 记的运行态是陈的 | 先 `handoff show` 确认状态；`agentd.log` 里搜「恢复阶梯」看走到哪一级 |
| 任务归档后有残留（worktree / executor 进程） | 回收失败（事件里会带残留提示） | worktree 用 `handoff reclaim` 回收；进程按事件提示处置，彻底死透按 `proc.json` 的 `handle.pid` 手工 kill shim |
| `card dispatch --step` 已受理后短等超时、卡上仍无 `dispatched`/`派发失败` 首态 | 202 只代表请求已受理；编排仍在 agentd 异步运行，正常首态可能在约 20 秒窗口外；运行锁占用会在卡上 comment + `needs_human` 留痕，ViaTemplate 派发失败也会落卡 | stdout 的「已受理，首态未到；进展见 card wait」是正常短等超时，跟 `card wait`；若短等捕获 reason=`派发失败`，stderr 会有卡上 comment 正文且命令非 0。运行锁问题先读卡上 comment 的 holder/reason。席位用 `bind` / `coordinate` / `rebind`，`takeover` 不再占座。 |
| `card add --coordinate` 失败 | 建卡不占座，该 flag 已废止 | 先建卡，再 `card bind` 或 `card coordinate` |
| `card rebind --to` 报 unknown flag | 任意 session id 已废止 | `--self` 或 `--launch` 二选一 |
| `card bind` / `rebind --self` 报未出示席位身份 | 当前来源依次是完整 HANDOFF_SESSION_CLI/ID、单独的 GROK_SESSION_ID 或 CLAUDE_CODE_SESSION_ID；普通终端/已关会话没有来源，或环境残缺、双宿主、手填与当前来源不一致 | 在 grok/claude 对话里直接重试；没有当前来源时在同一命令带 `--cli <物种> --session <id>`，两项必须成对且与当前来源一致。完整 HANDOFF 对优先；不要用 `USER`/hostname/PID，不要给 `rebind --launch` 或 coordinate 带这两个 flag。`--step`/非 user room send 也沿用同一对。 |
| `card bind` 报已有席位 | 桌子上有人（含旧人尺度席位） | `rebind --self` 或 `--launch` |
| `card coordinate` 报席位状态不适合此操作 / 409 | 空座才叫机器人；有人不能再 launch | `rebind --launch` 或 `--self` |
| `card takeover` 失败 | 不再通过 takeover 占座 | 空座 `bind` 或 `coordinate`；有人 `rebind` |
| `room send --kind escalation` 报书写者与房间身份不符 | `kind != user` 要比对账本席位，不是 `cli:user@host` | 未入座用 `--kind user`；本对话发简报/收口先 `bind` 或 `rebind --self` |

**日志在哪**（在 executor 所在机器上）：

- `~/.handoff/agentd.log`：agentd 主日志。`HANDOFF_LOG_LEVEL=debug` 可调低级别。
- `~/.handoff/tasks/<完整 task-id>/render.log`：模型回合正文实况，`handoff attach` 流式读取的就是它。
- 同目录下按 executor 分：opencode / grok / codex 是 `serve.log` + `proc.json`（连接凭据与探活依据）；claude 是 `claude.log`（stderr）+ `out.jsonl`（stdout 事件流）+ `perm.sock` + `proc.json`；agy 是 `agy.log`（stderr）+ `out.jsonl`（stdout 事件流）+ `perm.sock` + `proc.json`。`shim.log` 是进程承载层日志，`proc.lock` 是存活锁。

## 红旗——想到这些说明你在偷懒

| 念头 | 事实 |
|------|------|
| 「我记得这个任务的状态是……」 | 你的会话不是权威。`handoff show` 是。 |
| 「短 id 应该也能认吧」 | 精确匹配，没有前缀补全。一定 404。 |
| 「先删掉任务目录再 done」 | 顺序反了。`done` 可能被拒，先删就留孤儿。 |
| 「ssh 上执行机手动杀进程/改工作区更快」 | agentd 不知道你改了什么，运行态当场失配。走 CLI。 |
| 「收到 turn_failed，只能重新 dispatch 了」 | 回合失败进 `waiting_review`，`continue` 能续接，重派才是浪费。真要重派的只有 `failed`（stop / 启动失败 / force-reclaim）。 |
| 「派 plan 前先把纪律块拼到文件头」 | B129 后 agentd 自动注入，手工拼会让纪律出现两遍。看 stderr 的「纪律块: <来源>」确认即可。 |
| 「拒了就拒了，不用写理由」 | 理由是给模型看的。不给，它就原地重试同样的操作。 |
| 「reply 报 502，我再 reply 一次」 | 工单已被消耗，第二次必 404。要的是 `resume`。 |
| 「wait 没动静，是不是挂了？」 | 没有事件就是没有事件。看退出码和 stderr，别瞎重启。 |
| 「Bash 工具会超时，我写个 show+sleep 轮询循环」 | wait 本身就是轮询的替代品。一条后台 wait，退出即唤醒你。见「在 agent 会话里挂 wait」。 |
| 「把几百轮 wait 包进一条 shell 大循环省事」 | 大循环吞掉事件 JSON，且循环期间你无法处置任何工单。每轮一条后台 wait。 |
| 「wait 返回了 completed，直接 continue」 | 可能是 cursor 重放的历史事件。醒来先 `show`，state 说了算。 |
| 「这个权限请求看起来问题不大」 | 破坏性、不可逆、外部可见的操作一律升级给用户。 |
| 「任务好像不见了，重新 dispatch 一个」 | 先 `handoff tasks`。重复派发会开出第二个 executor 抢同一个仓库。 |
| 「代码 commit 完了，可以远程派发了」 | 校验的是远端能否 fetch 到这个 commit。没 `git push` 等于没有。 |
| 「工作区里改了几行还没提交，先派了再说」 | 校验拿 HEAD 比对，看不见脏改动，会**静默放行**——executor 拿到的是没有你改动的代码。 |
| 「stderr 说领先 3 个提交，应该问题不大」 | 那 3 个提交不在任务分支里。执行者会找不到刚加的文件、目录、backlog 行——先想清楚它们是不是这次任务要用的东西。 |
| 「`pull` 完了改动就在我本地分支上了」 | `pull` 只 fetch，不 checkout 不合并。合并是你自己要做的事。 |
| 「Monitor 退出了，再开一个就行」 | 先看退出码。401 / 404 重开一百次也是同样的结果 |
| 「事件流进来了，直接按它处置」 | 事件是唤醒信号，`show` 是权威。`--follow` 下 cursor 会跑在「已读」前面，这条比以前更要紧 |
| 「重连后没收到那 14 条 permission_request，是不是丢了？」 | 没丢。它们被折进了一行 `backlog_summary`，其中仍需处置的在 `actionable` 里，其余是已被审批链答掉的。 |
| 「开卡即绑，加个 `--coordinate`」 | 建卡不占座。坐下 `card bind`，叫机器人 `card coordinate`。 |
| 「`rebind --to` 指定任意会话」 | 没有这条 flag。接班者只有 `--self` 或 `--launch`。 |

## 延伸阅读

这份 skill 只覆盖协调者回路。以下不在范围内，需要时读仓库文档：

- **agentd 部署、`config.yaml` 各段、分级审批链、env 注入**：仓库 `README.md`。
- **各 executor 的差异与就绪判据（opencode / claude / grok / codex / agy）**：`README.md` 的「各 executor 须知」。
- **架构与协议设计**：`docs/superpowers/specs/2026-08-07-handoff-design.md`。
- **协调者回路之外的子命令**（`frames` 结构化回合帧、`footprint` 进程足迹体检、
  `machines` / `project` 机器与项目登记、`console` / `sessions` Web 控制台、
  `upgrade` / `service` 换版与托管、`skill` 同步本 skill）：各命令 `--help`。
