# dsh-taskboard-kit

[![DSH Insights health](https://dsh-insights.com/badge/ice5kysl/dsh-taskboard-kit.svg)](https://dsh-insights.com/p/ice5kysl/dsh-taskboard-kit/)

给每个 dsh workspace 一块本地任务看板：Agent 用 model tools 创建、认领、推进任务，人类在 dsh web 的「看板」页签里看同一块板、也能直接操作。看板的唯一真实来源是 workspace 里的一个 JSON 文件——**没有服务端、没有账号体系、不需要部署任何东西**。

[English README](./README.md)

## 兼容性

- **dsh ≥ 0.1.7** — 自 **v0.5.3** 起完整支持：会话 cwd 改吃 `sessionId` 插槽 prop（0.1.7 从会话列表 state 里移除了 `current`）；看板入口适配 0.1.7 的紧凑居中 dock 布局；注入的看板通知改用 0.1.7 持久化层要求的 v4 生产者 source kind（`plugin:taskboard-kit`）。旧版 dsh 通过兜底逻辑继续可用。

## 包含什么

- **8 个 model tools**，Agent 在该 workspace 的任何会话里都能调用：
  - `taskboard_inbox` — **会话开始第一步**：现在压在你身上的事，按急迫度排序，每条都带该敲的命令
  - `taskboard_list` — 列出任务（按状态 / 列 / 负责人 / **在等谁**过滤）
  - `taskboard_create` — 新建任务，可选直接指派给谁
  - `taskboard_claim` — 从待认领池原子认领（并发下恰好一人成功；**在等谁的卡不可认领**）
  - `taskboard_update` — 开始 / 暂停 / 提交审核（指定审核人）/ 通过 / 打回 / 完成 / 关闭 / 重开 / **block / unblock**、改派、改字段（含价值度）、附注
  - `taskboard_comment` — 给任务留言（实现发现 / 交接说明 / 测试反馈），不改任务状态
  - `taskboard_get` — 任务全文 + 事件时间线 + 留言串 + 列龄 / SLA / 审核人 / 在等谁
  - `taskboard_roster` — 名册：谁真的在场（最后一次动手是什么时候、别名）
- **「看板」会话页签**，三个可切换的视角（共享同一份数据）：
  - **按进度**：六条泳道（待认领 · 已指派 · 进行中 · 待审核 · 待收口 · 已结清），卡片带优先级 / 价值度 / 负责人 / **当前列停留时长** / **陈旧点** / **审核人** / **在等谁**；顶部一条 **「◷ N 张卡在等你决定」** strip（完整问题原文 + 一键「回复并解除等待」）；详情抽屉里有事件流水、留言与协作事实，一键认领 / 开始 / 提交 / 通过 / 打回 / 收口 / 改派。
  - **按负责人**：一个负责人一条泳道（别名自动归并），一眼看清每个人头上挂了什么。
  - **统计**：宏观读数——KPI（未结清 / 待收口 / 进行中 / 卡住 / 中位周期 / 收口延迟 / 打回率）、
    每日新建 vs 结清（叠未结清累计折线）、状态与优先级分布、各负责人负载与周期、各列停留时长。
- **「开启看板」向导**：尚未开启看板的工作区给一键开启——建 `.dsh/taskboard.json`
  并写入本工作区的 `.dsh/BOARD-PROTOCOL.md`（规范）。两者都已存在时都不会被覆盖。
- **抽屉里的 markdown（v0.7.0）**：详情与评论按 markdown 渲染——标题、列表（**按缩进嵌套，上限两级**）、GFM 表格、代码块、引用、链接、**图片**（`![alt](url)`，只放行 http/https）与**只读任务复选框**（`- [ ]` / `- [x]`）。渲染管线先转义、只注入自造标签，任何 Agent 写的卡都只是数据；渲染实拍见 `docs/images/markdown-fidelity.png`。
- **多 Agent 协作规范**：完整规范见 [`docs/COLLABORATION.md`](./docs/COLLABORATION.md)；它的精简版在会话开始写入系统提示词，CLI `--help` 与面板「? 指南」里的片段同源。
- **会话开始感知**：Agent 收到的是**自己那一份行动清单**（不是我欠审核、谁在等我、哪张卡被打回、哪张派出去没人接），不是一句干巴巴的计数。
- **看板变化推送**：对每个 live 会话的板文件做 fs.watch——任务被指派给你、你的任务有了审核结论、**有人把审核 hand off 给你**、**有人开始等你**、**有卡开始等人类**、你的任务有新留言时自动通知（只注入上下文，绝不唤醒）；5 秒风暴窗口内的连续变化合并成一条通知。
- **时钟自检（v0.5.4）**：文件 watcher 永远看不到「三天没人碰这张卡」——所以另有一条按时的自检：只看压在你身上的事（欠审核、有人等你、被打回没动、自己的卡陈旧、派出去的活成了孤儿、等人类超时），每 5 分钟一轮、每 workspace 30 分钟最多催一次，并附上该敲的命令。
- **等人类 = 真的叫得动人类（v0.5.4）**：卡片 `block --on human` 时进面板「等你」清单；配了 `TASKBOARD_NOTIFY_CMD` 就外发通知（msg9 / 桌面通知 / webhook 随你接）；等超 24h 自动升级催办。

## 板文件

每个 workspace 一块：`<workspace>/.dsh/taskboard.json`（形状见英文版 README，字段自解释）。

文件跟着 workspace 走，所以在同一目录干活的每个 harness、每个人都能读写同一块板——提交进 git 或 gitignore 都可以。所有变更经过锁文件（过期锁自动回收）+ tmp+rename 原子写，并发 Agent 和面板同时操作不会写花文件；`claim` 在锁内裁决：N 个并发认领恰好一个成功，其余收到冲突。

## 升级到 0.6.0（用过旧版看板的话，这一段请读）

**`done` 不再是终点，`closed` 才是。** 0.6.0 之前流程停在 `done`、`closed` 表示「废弃」，
于是「干完了但没人收口」这件事在规范里根本不存在：卡永远躺在 done，没有任何规则说
"有人欠这一步"。

现在 `done` 的含义是**干完且审核通过、但仍欠一次收口**：卡还在看板上、还计入未结清，
直到有人 `close` 它。由此带来的变化：

- 面板顶部新增「待收口」条，列出所有 done 未结清的卡，一键收口；
- done 超过 72h 会被判为陈旧（以前永不算陈旧）；
- `taskboard_inbox` 会点名卡主「你的卡已 done 但没收口」，并给出该敲的命令；
- 统计里的**中位周期**改成量 `created → done`（活），**收口延迟**单独量 `done → closed`（流程）。
  拆成两个数是因为合成一个时，只要收口一拖，快板子也会显示成慢板子。

「这事不做了」仍然走 `close`——在 note 里写清原因；没有单独的 abandoned 状态。
旧板文件无感加载（`cancelled` → `closed`，缺的 `value` / `comments` / `actors` 在下次写入时补齐）。
如果你现有的板子里积了一批 `done` 卡，它们现在会显示为「待收口」的工作——这是预期读数，不是 bug。

## 状态模型

| 列 | 规则 |
|---|---|
| 待认领 | `open` 且无负责人且未在等谁——任何人可 `claim` |
| 已指派 | `open` 且有负责人——已委派、未开始 |
| 进行中 | 已认领或已开始 |
| 待审核 | 已提交（submit），等 **`reviewer`** 通过 / 打回 |
| 待收口 | 审核通过（或直接 `done`）——**不是终态**，还欠一次收口 |
| 已结清 | 已收口——`close`（面板默认折叠，toggle 可显示） |

两条**与状态正交**的协作轴（v0.5.4，都不新增状态）：

| 字段 | 含义 | 谁写 |
|---|---|---|
| `reviewer` | 谁欠这次审核。`reviewer` 本人 / 卡主 / 人类才能 approve|reject；**不能审自己的活** | `submit`（默认解析：卡主 → 最近活跃的其他 Agent → 人类） |
| `waiting_on` | 这张卡在等谁（`human`/`agent`/`external` + `who` + `question` + `since`）。**等谁的卡不能被认领** | `block` / `unblock` |

板级还有 `actors` **名册**（谁出现过、最后一次动手、别名 `dsh ≡ dsh-agent`）——用来回答「这活派给一个已经不在场的 Agent 了吗」。

**只有 `closed` 是终态**：`done` 的含义是「干完且审核通过、但还没结清」，卡仍留在看板上、仍算未结清，
需要卡主 / PO / 人类收口 `close`。分两步是因为审核通过 ≠ 事情了结（可能还要部署、等上游、补文档）。
「这事不做了」也走 `close`，但**必须在 note 里写清原因**——没有单独的 abandoned 状态，
「做完了」与「放弃了」的差别只存在于留言里。面板顶部有「待收口」条，一键收口，防止干完了没人收口。

流转图：`open → in_progress → review → done → closed`；`done | closed → open`（reopen）。按动作说：`open → in_progress`（claim / start）、`in_progress → open`（stop）、`in_progress → review`（submit）、`review → done`（approve）、`review → in_progress`（reject）、`open|in_progress|review → done`、`open|in_progress|review|done → closed`（close，旧名 `cancel` 是它的别名）、`done|closed → open`（reopen）。状态名刻意对齐 msg9 任务模型，未来接服务端看板时语义不变。旧版本写出的板文件无感加载：`cancelled` 状态/日志事件归一为 `closed`，缺的 `value` / `comments` 字段自动补齐。

每个任务还有**价值度**（value points，斐波那契刻度 ½ / 1 / 2 / 3 / 5 / 8，`null` = 未评估），用来回答"这张卡值多少"——创建或更新时设置，CLI 里 `--value 1/2` 表示 ½、`--value none` 清除。

## 安装

```bash
dsh plugin --profile web add dsh-taskboard-kit@0.7.4
# 重启 dsh web，打开任意会话，「看板」页签就在
```

**务必写死版本号。** `dsh plugin` 转发给 pnpm 11，而 pnpm 11 默认带一道供应链
`minimumReleaseAge` 闸门：刚发布几分钟的版本会被拦下，此时不带版本的
`add dsh-taskboard-kit` **会静默装上一个旧版本** —— 你拿到的看板里 `done` 仍被当成终点，
而且没有任何提示。写上 `@0.7.4` 就是让这次安装绕过闸门、装到你指定的版本。
等某个版本发布满一天后，不带版本也能装到它。

<a id="release-notes"></a>
## v0.9.2 —— 修正：看板列宽刚性 1310px（窄屏裁列 / 宽屏留白）

- **修正**（T-85）：`styles.column` 是 `width: 240 + flexShrink: 0` ⇒ 一整排**刚性 1310px** ✗ —— 面板比它窄就**裁掉右侧的列**（1280 裁 **317px**、1440 裁 **157px**），比它宽就**右侧留白**（1680 留白 83px、2560 留白 **963px**）✗。
  现在 `column` 改为 `flex: 1 1 240px; min-width: 168px` ✓（宽屏**等分铺满** ✓、窄屏收缩到 168 ✓）；宽度规格收进 `TB_LANE` 常量作为唯一来源 ✓；
  `overflowX:auto` 保留但**降级为兜底** ✓；**没有用 `overflow:hidden` 糊** ✗。
  **这是既有缺陷，不是 v0.9.x 的回归** ✓（v0.9.0 与 v0.9.1 在五个宽度上**逐值相同** ✓，`lanes/column` 源码一字未改 ✓）。
- **工具**：新增**第 18 个测试套件** `tests/layout-invariants.test.mjs` ✓ —— 真无头 Chrome 挂**构建产物**，四个宽度断言：
  **整页不横溢 · 泳道行不被裁 · 末列右边界 ≤ 视口 · 行必须铺满 · 列宽 ≥ 下限 · 面板内无越界元素** ✓。
  **关键**：文档级 `scrollWidth === clientWidth` **改前也是 true** ✗（溢出被关在泳道里）⇒ 只断言"整页不溢出"**拦不住这个 bug** ✗ ⇒ 必须断言"**行内不被裁 + 必须铺满**" ✓。
  **红绿对照**：指向 v0.9.0/v0.9.1 的 `client.js` ⇒ **真红** ✓（裁 310/150px、留白 90/970px）；仓内塞回 `flex:0 0 240px` 或 `width:100vw` ⇒ **红** ✓，且**先断言 saboteur 真的生效** ✓。
- **已知后果（有意接受）**：「按负责人」视角在**只有一个负责人**时那一列会铺满面板（1280 下 976px ✓）—— 这是"必须铺满"的直接结果 ✓；**不要改回 `flexShrink: 0`** ✗（若要限宽应显式加 `max-width`，代价是重新引入留白 ✗）。见 `docs/DESIGN.md` ✓。

**版本口径（沿用 v0.9.1 起的规则）**：0.x 下**修正与内部改动走 patch** ✓ —— 本版全是修正与工具，故为 **0.9.2** ✓。

## v0.9.1 —— 320px 抽屉头部修复 · 移除「待收口」条 · 提交前守卫

**版本口径（本版起执行）**：0.x 下**修正与内部改动走 patch** ✓；**只有新能力或需要用户知晓的行为变更才走 minor** ✓ —— 一次 minor 应当是一个「能力台阶」，不是「攒了一堆修复」。

- **修正**（T-75）：320px 视口下抽屉头部标题被压成**竖排**、头部高达 **953.95px** ✗ ⇒ **117.98px**（标题 175 → 15px ✓）；
  **560/900 与改前完全一致**（60.89 / 40.59px）⇒ **宽档零回归** ✓。
  改法为实测选定：`flex-wrap: wrap` + 标题 `flex-basis: 160px`（**两条必须一起上** —— 单独 `flex-basis: 0` 时标题 hypothetical size 也是 0，换行算法永远认为放得下 ✗；`flex-basis: 100%` 会把 560/900 改到 102.39px ✗，已用数据否决 ✓）。
- **变更**（T-77）：**移除顶部「待收口」条** ✗（实测 1 张 done 时 **147.59 / 100.80 / 75.80px**、4 张时 **332 / 235 / 210px** ⇒ 现为 **0**，看板内容上移 **−154 / −107 / −82px** ✓）。
  那句规则说明移入 `done` 列头的 `title` ✓；**卡面权威口径与两条收口路径都保留** ✓（抽屉里那枚「收口结清」与列里那枚按钮**是同一个 `action: close`**）⇒ 代价只是收口从 **1 次点击**变成 **2 次** ✓。
- **工具**（T-79 / T-82）：新增**提交前守卫** —— `npm run check:commit` / `npm run commit:guarded`：
  ① **源码 ↔ 产物一致**（构建退出码必须为 0；构建不得产生新改动；工作区 `lib/` = **索引** `lib/` ✓）；
  ② **有并发写时拒绝提交**（两次采样 = `porcelain` **+ 被跟踪文件内容哈希** ✓ —— 只用 `porcelain` 拦不住"已修改态的文件被继续写" ✗）；
  ③ **索引为空时说人话**（"是不是忘了 `git add`" ✓，且在构建之前 ⇒ 注定失败的提交不动产物 ✓）。
  可选**仓内 git hook**：`npm run install-hooks` / `uninstall-hooks`（**只写 repo-local `core.hooksPath`，绝不改全局** ✓；**逃生门 `git commit --no-verify` 保持可用** ✓）。
- **文档**：`docs/DESIGN.md` 两条设计系统事实 —— `ACCENT` 是宿主的**反相单色**（要蓝色强调请用 `LINK` ✓）；**模板串里的 CSS 不受 `tsc` 检查**（注释写在 `*/` 之后会吃掉整条规则 ⇒ 需骨架合法性判据 ✓）。
  `docs/COLLABORATION.md §13.5` 记 hook 的三条硬纪律 ✓。

**验证**：`npm test` **351 项 / 17 套件全绿** ✓ + typecheck 干净 + `action-parity` 全绿 ✓；
负面对照均**真红**：拆掉 T-75 修复 ⇒ 头部回到 953.95px 并再次竖排 ✓；把 settle strip 塞回 ⇒ 对应断言 FAIL ✓；守卫的"红时不提交"用**真 git 仓库量 `git log` 不增长** ✓；
加载面用**页面级 sha256**（不是 `rev` 请求 200 —— 那条口径已被证明无效 ✗）。

## v0.9.0 —— 权限闸门补漏 + 卡详情与看板列表的密度重塑

**本版起每条都标类别（能力 / 修正 / 文档 / 工具）** —— 否则「版本数」会被当成「交付量」读。

- **修正 · 闸门侧门**（T-69）：`block` 把卡挂到一张**已经在等**的卡上 = **替换**那段等待 ⇒ 现在走与 `unblock` **同一道归属门**，并**先显式记一条 `unblocked`** 再记 `blocked`；**首次挂起不受约束**（卡上本来没有等待 ⇒ 谁都能挂）。起因：一天内 4 次同类越权，其中一次正是"替换等待"这条侧门。
- **能力 · 卡详情抽屉密度重塑**（T-74）：属性 / 动作 / 指派三块重排 —— 属性 11 行 → **4 行语义行**（人 / 值 / 时 / 标签；字段形如 `标签 值`，行内自动折行）；动作 9 行 → **一行胶囊 + 一行折叠的「暂不可用」**（**原因一条不删**）；指派 → **一行 `当前：X（更换）` + 可展开 combobox**；详情页签顺序改为 **属性 → 描述 → 动作 → 指派**。
  **真机实测（抽屉宽 560）**：三块合计 **1010.67 → 233.92px（−776.75，−76.9%）**；**描述区 top 1182 → 237**。顺带修掉一个真 bug：**可复制字段的 `title` 是死的**（新增 `propTitle()` 让复制与悬浮共存）。
- **能力 · 看板顶部横幅 → 卡片凸显**（T-76）：删掉 **156.52px** 的大横幅，改为 **卡片左侧竖条 + ⏳「等你决定」徽章**（徽章 `title` = **问题原文**）+ **该列内排最前**，顶部只留一行 **28px** 汇总条（**无等待卡时不渲染**，点「跳过去」滚到该卡并闪现 1.2s）。
  **真机实测**：顶部 **156.52 → 28px**、看板内容上移 **135px**、等待卡位次 **6/7 → 0/7**。
  **颜色纪律**：「等你决定」= **主色蓝 + 实心矩形**；「已超时」= **琥珀 + 胶囊描边** ⇒ 颜色与形状双重区分（浅/深色实测）。回复人类等待的入口从横幅**搬进抽屉的等待框**（能力不丢，代价是多一次点击）。
- **修正 · CHANGELOG 更正（不改写历史）**：v0.8.0 的发行说明里有一句宣称 —— 「every path now runs the same ownership check」 —— **被 T-69 否证**（当时 `block` 替换等待没有过门）。按"已发布的文本不改写"的口径，该句**保持原样**，在此更正。
- **文档 · 两条设计系统事实**：`ACCENT` 是宿主的**反相单色**（不是强调蓝 ⇒ 要蓝色强调请用 `LINK`）；**模板串里的 CSS 不受 `tsc` 检查**（把注释写在 `*/` 之后会让解析器吃掉整条规则 ⇒ 必须有骨架合法性判据，本版已加）。

**验证**：`npm test` **333 项 / 16 套件全绿** + typecheck 干净 + `action-parity` 全绿（7 动作 × 5 状态 ⇒ **未移除任何动作入口**）；
**负面对照**：320px 关掉 `flex-wrap` ⇒ 横向溢出真红；注入隐藏徽章/竖条 ⇒ **可见性** True→False（不是"DOM 里有没有节点"）；源码变异 ⇒ 对应断言真红；
**加载面**用**页面级 sha256**（不是 `rev`+200 —— 那个方法已被证明无效）。

## v0.8.0 —— 权限从「约定」变成「机制」+ reviewer 的主动通道

这一版全是**行为变更**（不是文案），装完刷新页面即可看到客户端部分；**host 面（权限判定、通知）需重启 `dsh web` 才生效**。

1. **`unblock` / `close` 的 actor 校验**（T-61）：起因为**一天内 4 次同类越权** —— 有 agent 自行解挂"挂人类"的卡并替人类裁决、还自行 `npm publish` 两个版本。
   - **`unblock`**：挂**人类**的卡 ⇒ **agent 一律不得解挂**（只有人类）；挂**具名 agent** ⇒ 只有本人或其**别名等价名**、或人类；`who` 为空 / `kind:'external'` ⇒ 有意放宽为任何在场 agent 或人类（板外对方永远不会来敲这块板子，按字面锁死只会把卡烂在板上）。
   - **`close` / `cancel` / `reopen`**：只有**卡主 / 持卡人 / 裁决人 / 人类**；被拒走 `conflict`，拒信写清"谁被拒 / 在等谁 / 由谁挂起 / 该找谁"。
   - **别名折叠**：名册优先 + 配置兜底（内置 `dsh ≡ dsh-agent`、`TASKBOARD_ACTOR_ALIASES` / `WATCH_NAMES` / `SIBLING_NAMES`，组间传递合并）—— 否则自己会被自己拒 ✗。
   - **向后兼容**：缺 `created_by` 的老卡**退回改前行为**（不锁死老卡）；**顺序是状态先于归属**（没人在等 / 非法转移仍是 `invalid-transition`）。
   - **定位写明**：这是**防误操作与越权，不是安全边界** —— `--by` 不是凭据、可被伪造。
2. **`submit` 归属门 + 等待释放归属 + 新机制锁死修复**（T-62）：`submit` 此前**任何人都能提交别人 in_progress 的卡** ⇒ 现在只允许**持卡人 / 卡主 / 人类**；
   `submit` / `approve` / `reject` / `done` / `close` / `reopen` 此前会**静默**清 `waiting_on`（可以让一张"等人类"的卡被 `done` 直接变"已完"）⇒ 现在**每条路径都过同一道归属判断，并显式补写 `unblocked` 事件**；
   同时修掉新机制引入的锁死：**"谁被等"由排他许可改成追加许可**（等 agent/external/未指名 ⇒ 卡主/持卡人/裁决人始终可解；**等人类仍然只有人类能解**）。
3. **reviewer 的主动通道**（T-56）：`submit` 成功后**主动通知指定 reviewer** —— 打印一条可原样复制的 `msg9 send` 命令；环境里有 `msg9` 且**板里名册有他的地址**时才真发；地址未知 / 无 `msg9` / 发送失败 / 内部异常 ⇒ **一律降级为只打印，`submit` 照常成功**（提交是核心动作，通知是附属）；
   幂等锚点是卡上最后一条 `submitted` 事件的下标（不是时间戳）；`stale` 新增「🕵 欠审核」按 reviewer 分组（一行一个人 + 他该敲的命令 + `--json.review_owed`）。
   > 起因：9 张卡挂在 `review` 列、而那位 reviewer 反馈「没有他待办的」—— 因为他 cwd 不在本工作区、看的是"指派给我"而不是"欠我审核"；**"被人指定为 reviewer"这件事此前没有任何东西会主动告诉他**。
4. **等待区间读侧兜底**（T-60）：区间配对此前只认 `unblocked` ⇒ 被隐式结束的等待段闭不上、一路延伸到此刻（**走势图末点说"还在等"、而当期大数字说"没在等"，同屏自相矛盾**）；现在把 `submit`/`approve`/`reject`/`done`/`closed`/`reopened` 也作为区间终点。**口径变更留痕**：统计页「被卡住」会因历史等待被计入而变化，属于主人已批准的方向。

**验证**：`npm test` **312 项全绿**（14 套件）+ typecheck 干净；新增 `tests/authority.test.mjs`（18 项）与 `tests/authority-wait.test.mjs`；
**变异**：摘掉"挂人类的卡 agent 不可解挂" ⇒ 红 · 摘掉别名折叠 ⇒ 红 · 摘掉 `submit` 归属门 ⇒ 红；
**向后兼容用真实板当夹具**：有 `created_by` 的卡**挡住陌生人**，抹掉后**逐字回到改前**。

## v0.7.5 —— 把「复核里说过、但没人跟」的待办变成清单

**全是客户端与 CLI 改动，刷新页面即可**（CLI 侧无需重启）。

1. **新增 `taskboard tails`（复核尾巴扫描器）**：扫**已 closed / done** 卡的裁决 note 与复核留言，抽出"看起来是待办"的句子；
   每条带 **卡号 + 位置（`log:6` / `comment:3`）+ 句偏移 + 原文片段 + 命中判据**。处置：`--file <尾巴> --card <卡号>` 落卡、
   `--waive <尾巴> --reason "…"` 显式作废（**必须给理由**）、`--reset` 撤回；落进板文件的**可选**新字段 `Board.tails?`（旧板可读）。
   同时接进既有路径：`stale` 加「🔻 复核尾巴」一节、`inbox` 给责任人提示（带上限与"还有 N 条"）。
   **判据是被真实数据三轮收窄出来的**（第一版 240 条＝没人看的墙纸 → 94 条），六条收窄规则各自有单测；
   另有 **`not-done` 类只在小节标题层面放行**（"明确没做"标题下的条目也会被列出，而噪声控制由断言咬住 —— 变异二路证明）。
   **为什么做这个**：38 张已闭卡里至少 10 条复核遗留**掉了地**（含"注释声称有测试、其实没有"这种假绿）——
   根因是**复核结论写在 note 里、而 note 不改变列** ⇒ 没有载体、没有提醒、没有清单 ⇒ 天生静默。
2. **清理 kimi 复核遗留 9 条**：含**补上那个"注释声称有、其实不存在"的对照测试**（`tests/action-parity.test.mjs` **驱动 host**：
   7 动作 × 5 状态逐格跑 `updateTask`，与抽屉真正渲染的结果比 —— 不比字面量比行为）；统计页**整页去掉 `brand-primary` 数据色**
   （`done` 改用宿主 success 绿，**刻意不用 link 系以免与 in-progress 撞色**，两主题实测对比度 2.28/6.89 与 4.23/6.75）；
   `MILESTONE_TAG` 大小写口径与 `semverParts` 对齐；`?` 指南新增**卡面记号图例**（由真常量派生，不手抄）；
   卡面 ref 补「任务编号」；`T-93-hotfix` / `T-93.5` **补齐编号边界**；两处半角逗号 + 一条"源码里不存在『汉字,汉字』"的可咬规则。
3. **卡面渲染层补护栏**：无名等待（`waiting_on.who` 为空）**不得渲染成"可认领"** —— 此前把渲染分支顺序改回旧顺序，
   **76 项测试仍全绿**（派生层有护栏、**最该护的卡面层没有**）；现在**变异只让它一条红**。
   另补「裁决人 {reviewer} 仍可裁决」提示（一处 `holderTitle()` 三处生效），`reviewer` 为空时不加也不回落到创建者（与属性表那行一致）。
4. **两条语义小项**：`start` 后若等待仍在，结果列显式提示「**等待仍在（{who}）**」（host 确实不清 `waiting_on`）；
   抽屉**不加** `stop`/`block`（都不改列，加了会出现"按下去状态没变"的按钮）⇒ 文档给出可直敲的 CLI 路径。
5. **统计页「被卡住」口径变更（主人拍板）**：`blockedAt` 改为按 `log` 里 `blocked` → `unblocked` **配对重建等待区间**
   （旧板没有该事件时回退 `waiting_on.since`；连续两次 `blocked` 会产生重叠段、`some` 判定不受影响）
   ⇒ **这个数字会上升**，而这正是要的：从此能查"**这张卡被卡过多久**"，历史等待不再隐身。

**验证**：`npm test` **256 项全绿** + typecheck 干净；新判据与清理项各带**变异红/绿**（T-44 13/13 · T-42 10/10 · T-50 二路 · T-40 单点）；
`tails` 在**本工作区真实板**上把 94 条未收口处置到 **0**（落卡 47 / 作废 47，逐条带理由）。

## v0.7.4 —— 键盘能用了 · 走位不迷路 · 导航区不再"不稳定"

**全是客户端改动，刷新页面即可。** 三件：

1. **键盘导航 `j` / `k`**：在当前视图里按**视觉顺序**（从上到下、从左到右）移动选中，抽屉开着内容也跟着换；`Enter` 打开第一张。顺序**直接由渲染用的同一份泳道派生**（不是另算一套），所以"键盘顺序 === 屏幕顺序"是结构性的；收起的「已结清」竖条不进顺序（没渲染就不该落到看不见的卡上）。**焦点在输入框 / 评论框 / contenteditable 里时一律走原生行为**（⌘/Ctrl/Alt、输入法组字、指南/关于/成员选择器/新建表单开着时也不抢）；**`Esc` 仍只有一套分层**，没有第二套语义。
2. **走位自动滚入视口**：长板 / 窄窗口下 `j`/`k` 走到视口外时把选中的卡带回可见区；用 `block:'nearest'` 且**已经可见时根本不滚**（否则每按一次都跳，比不滚更烦），也不会带着抽屉一起滚。
3. **导航区重构（三段式 + 永远在场）**：这一版是回应"排列和显示很不稳定"——
   - **同一行现在有三段**：视图（`按进度|按负责人|统计`）· 筛选（`含已关闭`）· 操作（信息 `?` `ⓘ` · 动作 `↻` · 主按钮）。**组内 4px、组间 16px**；
   - **`含已关闭` 永远在场**（原先只在「按负责人」视角出现 ⇒ 切视角时这一行凭空多/少一个控件，这才是"不稳定"的真正来源）；
   - **三个工具图标换成内联 SVG**（原先 `?` 是 ASCII、`↻`/`ⓘ` 是符号字形，CSS 完全相同也对不齐）；
   - **"刷新"不再夹在"使用指南"和"关于"中间**；
   - **所有可点控件统一 28px 高 / 同圆角 / 同 hover / 键盘 focus 描边**；
   - **按宽度跳档而不是挤压**：`≥720` 全标签 → `520–720` 按 `路径 → 计数 → 标题` 顺序舍、筛选只留方框 → `<520` 把筛选与三个工具收进 **`⋯`**（视图组与主按钮**永远在位**；320px 时视图组也进菜单）。`⋯` 接进既有的 Esc 分层，一次按键只关一层。
   - 顺带修：删样式表时误删了一个键，英文界面 @720 下视图组会掉出 flex 被挤到第二行。

**验证**：`npm test` **230 项全绿** + typecheck 干净；每处改动都有**变异验证**（键盘守卫 / 顺序 / 走位闸门 / 图标形态 / 组间间距 / 菜单分层 …），并用**真浏览器**逐条量过：三个图标 16×16、按钮 28×28、**所有可点控件垂直中心差 0.00px**、各档单行且不溢出、`⋯` 开合与 Esc、focus 可见、超长路径截断、空板与英文最长文案。

## v0.7.3 —— 「关于」浮层 · 持球行铺到每个面 · 两处清理

**全是客户端改动，刷新页面即可，不用重启 `dsh web`。** 这一版里唯一会离开你机器的，是你自己点的那几个 GitHub 链接。

1. **「关于」浮层（工具栏的 `ⓘ`；0.7.4 起它在信息组里与 `?` 相邻，`↻` 已归到动作侧）**：名称 + 版本（版本**由构建注入**——`scripts/build.mjs` 用 esbuild `define` 从 `package.json` 取；构建若漏了注入会回落成 `dev`，不会印出空版本）、一句话简介，然后是真正值得读的一块：**本地透明度**——看板文件的绝对路径 · 卡片数 · 花名册人数 · 数据格式版本（`board.version`）· 插件 id，以及一句话说清「**数据全在这一个 JSON 里，没有服务器、没有账号、没有云**」。下面四个 GitHub 入口：仓库 · 问题反馈 · 协作规范（`docs/COLLABORATION.md`）· 变更记录（就是这段），每个都是 `target="_blank" rel="noreferrer"`，末尾是 MIT 与作者。Esc 只关它（它是最上层，按键不会穿透到底下的抽屉或指南）。
2. **持球行成为「球在谁手上」的唯一答案，每个面都一样。** 卡面、抽屉、状态栏 mini 抽屉全部用同一个派生（`currentHolder`）渲染 `记号 + 名字`，严格一行：`➤` 待提交 · `○` 待认领（池子）· `◷` 待回复 / 待回执（等人类 / 等 Agent）· `⚑` 待裁决（待审核）· `⌂` 待收口（已完成）。`closed` 没有持球人。它由状态机派生而不是读 `assignee`：`review` 阶段负责人被冻结（球在裁决人手上）、`done` 阶段裁决人已清空（球在卡主手上），所以「当前处理人」的字面读法在这两处都会**指向错的人**。mini 抽屉原先堆了三枚逐字段徽章、同一件事说两遍，现在只剩这一行。
3. **标题前缀只在显示层剥离。** 与卡面重复的 `【owner】 T-93 ·` 前缀不上卡面，**原文留在 tooltip 里**；板文件里的字符串一个字符都不改。
4. **长名字改为截断，不再溢出。** `.tb-badge-outline`（以及所有同类 10px 药丸）补上 `max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap`：长 actor 名以前会**溢出虚线边框**、再被泳道从中间切掉，看起来像排版坏了而不是「名字太长」。
5. **两处清理**：0.7.2 之前就死掉的三个样式键（`cardSep` / `reviewerBadge` / `cardWaitAge`，各定义 1 处、引用 0 处）删掉。

**验证**：`npm test` **214 项全绿** + typecheck 干净；版本回落、持球记号、徽章 ellipsis 规则、Esc 分层各自都有**变异对照**（改坏 ⇒ 对应测试必须红）。

## v0.7.2 —— 卡面与统计的「一眼看懂」改造（十项）

主人 2026-10-01 连着给了四轮反馈，这一版把它们一起收了（**全是客户端改动，刷新页面即可，不用重启 `dsh web`**）：

1. **卡面三行版式**：第 1 行=固定属性（优先级点 · 编号 · 价值点 · 状态 · 负责人/创建者 …… 列龄靠右，10px）；第 2 行=标题（≤2 行，冗余前缀在**显示层**剥离）；第 3 行=会变的（处理人 · 等待 · 审核 · 标签，严格一行）。卡高由 90–123px **收敛到统一 88px**。
2. **标题去噪**：`【owner】 T-93 ·` 这类**与本卡重复**的前缀不再上卡面（`【owner】` 仅当它就是本卡的负责人/创建者/裁决人；编号仅当就是本卡自己的 id；**原文留在 tooltip**）。编号匹配带完整 token 边界 —— `T-930` / `T-93X` / `T-93-2` 不会被误吃（这是复核打回的真 bug）。
3. **持球人派生视图**：卡面只回答一句「**球在谁手上（待什么动作）**」——`➤ 待提交` / `◷ 待回复·待回执` / `⚑ 待裁决` / `⌂ 待收口` / `○ 待认领`（`closed` 无持球人）。它由状态机派生（`review` 阶段负责人被冻结、`done` 阶段裁决人已清空，所以"当前处理人"的字面读法在这三处都会**指向错的人**）。
4. **卡面图标化**：标签词换成记号（`@` 负责人 · `✎` 创建者 · `◆` 价值点 · `◷` 等待 · `⚑` 裁决 · `⌂` 收口 · `➤` 持球），**只用文本字形不用 emoji**，含义在 tooltip 里；卡面文字 **−10.6%**。
5. **统计页 v2**：时间窗 **7/14/30** 天可切；KPI 分「要你动手的」与「背景数字」两组并带**与上一等长窗口的环比**（样本不足 <3 时显式标注"样本不足"，不装趋势）；新增「**现在该动什么**」（持球人排行 + 异常清单，**每条可点开对应卡**）与「**价值度视角**」（积压 vs 交付、吞吐、每卡价值/周期）；流量图补 y 轴刻度/网格/数值标签、状态改环形图；「里程碑进度」由 tag 派生（如 `v1.42.0`）。
6. **详情抽屉 v2**：粘性头部（标题 + 持球行 + 主操作常驻）；属性表（值可复制）；动作**永远全部列出**并带**结果列**（`关闭 → 进入「已结清」· 可 reopen 恢复`），不可用的**置灰并说明原因**；指派改可搜索、**默认只列在场者**（久未活动者要显式展开）；描述可折叠；标签可点。
7. 两条**潜在死路**随之修掉：池子里的卡点「开始」会造出无主的「进行中」；被等待的卡在抽屉里只有「认领」这个必然失败的出口（现在有「解除等待」）。
8. 无名等待（`waiting_on.who` 为空）**不再渲染成"可认领"**（卡面与统计页两处）——store 会拒绝认领等待中的卡，之前那是个点了必然失败的入口。

**验证**：`npm test` **207 项全绿** + typecheck 干净；每一处都带**变异验证**（改错即红）。

**0.7.1 = 把「看不清的小字」修好 + 卡片更紧凑。** 卡面上那一档「淡字」原来用的是 `--dsw-alias-label-dimmed` —— shell 里**故意做到近乎不可见**的一档（浅色 `#e1e5ee`、深色 `#43454a`，对比度 ≈1.1:1 / 1.3:1），于是引用号 `#18`、价值度 `◆2`、列龄这些字**两套主题都看不清**；现在改用 `--dsw-alias-label-tertiary`（`#81858c` / `#adb2b8`）。同时按「结构重塑」把卡片从 **5 行压到 3 行**（`#N` 与 `◆value` 移到 meta 行右侧，负责人与等待/审核胶囊合一行），泳道 `260 → 240px`，间距/行高同步收紧：**同一块板上卡高平均 125px → 97px（−22%）**，一屏单泳道约 7 张 → 9 张。等待胶囊允许内部换行，保住「等了多久」这半条信息。

**0.7.0 = markdown 保真度 + 两处安全加固。** 详情与评论的渲染补齐了四件事：**图片**（`![alt](url)`，只放行 http/https）、**嵌套列表**（按缩进两级，子列表开在父 `<li>` 内）、**只读任务复选框**（`- [ ]` / `- [x]`）、评论容器改块级 `div`（表格/列表不再塞进 span）。安全面同步收紧两处：**cwd 白名单按方向分档** —— 写（`POST`）在无板目录里必须由宿主明确回答「这个工作区我服务」，读保持既有契约（面板要能读「会话不在线 / 板还没建」的工作区）；**信任门开始比 scheme**（`Origin: https://…` 不再被当成本机 http 源放行）。另外收了审计剩下的三条 minor（CLI 打错 `--cwd` 不再与空板难分、锁等待预算覆盖 stale 窗口、上文那条 scheme）。

**0.6.2 修的是「样式表归属」。** 状态栏那枚「看板 · N ◷M」胶囊**偶尔**会退化成浏览器
默认按钮（灰底灰边、图标被挤到上一行）。根因不在布局：dsh 的客户端模块加载器会把文档里
**所有还没有主的 `<style>`** 收编给下一个加载的插件模块，再在它卸载 / 热重载时删掉；
我们的样式表原本是 React 渲染的（天然无主），于是被别人的一次重载顺手删掉，而 React 还以为
它还在（fiber 既不会补回，界面上也没有任何报错）。现在 TB_CSS 只由
`ensureTaskboardStyles()` 注入 `<head>`，出生就带 `data-plugin="dsh-taskboard-kit"`，
加载器再也抢不走，丢了下次挂载还会自愈。**建议所有 0.6.x 用户升级。**

（0.6.1 是纯文案版本：「叫人类」改说成**你自己的通知通道**（msg9 / 桌面通知 / webhook /
邮件），不再把 msg9 写成唯一方式；`msg9HookExample` 改名为 `notifyHookExample`，
旧名保留为 deprecated 别名。零行为变化。）

升级同理：`dsh plugin --profile web add dsh-taskboard-kit@<版本号>`（0.x 锁小版本，
`dsh plugin update` 不会带你跨版本）。

## 给没有插件的 Agent 用（Kimi Code、Claude Code、任何 shell）

板就是一个文件，但**绝不要手改它**——锁和原子认领都在 store 里。kit 自带一个零依赖 CLI，封装的正是同一个 store，让每个 Agent 用同一个安全入口操作：

```bash
taskboard inbox --by kimi                   # ★ 会话开始第一步：现在该你处理的事（带该敲的命令）
taskboard list --waiting human               # 谁在等人类（--waiting agent|external|any 同理）
taskboard stale                              # 协作健康：在等人类 / 审核没人认领 / 交接断了 / 列陈旧
taskboard roster                             # 名册：谁还在场（派活前查）
taskboard claim T-3 --by kimi                # 原子认领，log 记 "kimi"（等谁的卡会被拒）
taskboard update T-3 --action submit --reviewer claude --by kimi   # 提交并指定审核人（**持卡人的动作**：持卡人 / 卡主 / 人类，T-62）
taskboard update T-3 --action approve --by claude                  # 审核通过（非 reviewer/卡主/人类会被拒）
taskboard update T-3 --action block --on human --who iceskysl \
  --question "现在就发，还是等 T-8 修完？" --by kimi                # 挂到人类身上（触发外发通知）
taskboard update T-3 --action unblock --by iceskysl                # 答复到了 —— 等人类的卡**只有人类**能解挂（agent 会被拒，T-61）
taskboard update T-3 --action close --note "已部署" --by kimi       # 收口 —— 只有卡主 / 持卡人 / 裁决人 / 人类能敲（T-61）
taskboard comment T-3 --text "交接：…" --by kimi        # 不改任务状态
taskboard create --title "…" --priority high --value 3 --by claude
```

`--by` 指定 log 里的操作者（默认 `$TASKBOARD_ACTOR` 或 `cli-agent`）；`--cwd` 指向别的 workspace；`--json` 输出机器可读结果；认领失败退出码 `3` 并给出可读的冲突原因。包还没上 npm 之前，直接从仓库调用：`node /path/to/dsh-taskboard-kit/bin/taskboard.mjs list`。dsh web 跑不跑都能用；web 在跑时，本机进程也可以直接调回环 bridge `/dsh-taskboard/*`。

## 环境开关

| 变量 | 作用 |
|---|---|
| `TASKBOARD_ACTOR` | Agent 工具写进 log 的默认操作者名（默认 `dsh-agent`） |
| `TASKBOARD_HUMANS` | 逗号分隔：哪些名字算人类（默认 `human`） |
| `TASKBOARD_ACTOR_ALIASES` | `规范名:别名1\|别名2,…`——把同一个 Agent 的多个名字并成一个主人（默认 `dsh:dsh-agent`） |
| `TASKBOARD_WATCH_NAMES` | 逗号分隔的「自己人」名字列表（第一位为规范名，其余自动成为别名）；watcher 按它判断哪些变化与我有关、哪些是我的自回声（默认 `dsh,dsh-agent`） |
| `TASKBOARD_SIBLING_NAMES` | 同一实例的**其他会话**名：它们的卡算我的，但它们的动作会通知我（默认空）。默认配置下两个 dsh 会话会互相看不见，这一项就是解药 |
| `TASKBOARD_ALLOW_SELF_REVIEW` | `1` = 允许自审（只给「一个 Agent 独占一个 workspace」的场景） |
| `TASKBOARD_NOTIFY_CMD` | 卡片开始等人类 / 等超 SLA 时执行的外发通知命令；卡片 JSON 从 stdin 进，`TASKBOARD_TASK_ID` / `TASKBOARD_QUESTION` / `TASKBOARD_NOTIFY_REASON` 等从环境变量进。失败只记日志，绝不让写板失败 |
| `TASKBOARDKIT_LOCALE` | `en` 强制英文工具输出（默认中文） |
| `TASKBOARD_WATCH` | `0` 整体关闭变化推送与时钟自检 |

## 浏览器桥

面板通过 `/dsh-taskboard/*` 与宿主通信。只接受回环 / 同源调用；所有 POST 必须带 `x-taskboard: mutate` 头。浏览器侧的操作在任务 log 里记为 `human`。

## 开发

```bash
npm install
npm run build       # esbuild → lib/index.js + lib/client.js
npm test            # node 原生 runner：store、host smoke、client
npm run typecheck   # tsc --noEmit
```

零运行时依赖。浏览器 bundle 只 require shell 内置的 `react` / `react/jsx-runtime`。

## License

MIT
