@joekytc/dsh-swarm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +504 -0
  3. package/README.zh-CN.md +452 -0
  4. package/client/BoardCard.tsx +41 -0
  5. package/client/ConnectionBanner.tsx +16 -0
  6. package/client/KanbanBoard.tsx +168 -0
  7. package/client/KanbanTab.tsx +26 -0
  8. package/client/RenameModal.tsx +38 -0
  9. package/client/TaskDrawer.tsx +212 -0
  10. package/client/WorkflowRail.tsx +175 -0
  11. package/client/board-store.ts +196 -0
  12. package/client/css.d.ts +4 -0
  13. package/client/index.ts +31 -0
  14. package/client/kanban.css +653 -0
  15. package/client/useKanbanBoard.ts +7 -0
  16. package/client/workflow-model.ts +229 -0
  17. package/cordis.patch.yml +88 -0
  18. package/lib/client.js +1205 -0
  19. package/lib/config.d.ts +46 -0
  20. package/lib/config.js +43 -0
  21. package/lib/dispatcher/agent-runner.d.ts +23 -0
  22. package/lib/dispatcher/agent-runner.js +429 -0
  23. package/lib/dispatcher/chain-auditor.d.ts +47 -0
  24. package/lib/dispatcher/chain-auditor.js +194 -0
  25. package/lib/dispatcher/dispatcher.d.ts +50 -0
  26. package/lib/dispatcher/dispatcher.js +280 -0
  27. package/lib/dispatcher/event-waker.d.ts +13 -0
  28. package/lib/dispatcher/event-waker.js +24 -0
  29. package/lib/dispatcher/git-credentials.d.ts +33 -0
  30. package/lib/dispatcher/git-credentials.js +78 -0
  31. package/lib/dispatcher/merge-gate.d.ts +28 -0
  32. package/lib/dispatcher/merge-gate.js +74 -0
  33. package/lib/dispatcher/model-candidates.d.ts +13 -0
  34. package/lib/dispatcher/model-candidates.js +31 -0
  35. package/lib/dispatcher/session-events.d.ts +28 -0
  36. package/lib/dispatcher/session-events.js +33 -0
  37. package/lib/dispatcher/target-repo.d.ts +15 -0
  38. package/lib/dispatcher/target-repo.js +42 -0
  39. package/lib/dispatcher/v-orchestrator.d.ts +74 -0
  40. package/lib/dispatcher/v-orchestrator.js +452 -0
  41. package/lib/dispatcher/watchdog.d.ts +15 -0
  42. package/lib/dispatcher/watchdog.js +30 -0
  43. package/lib/dispatcher/workspace-attach.d.ts +40 -0
  44. package/lib/dispatcher/workspace-attach.js +112 -0
  45. package/lib/domain/delivery-contract.d.ts +18 -0
  46. package/lib/domain/delivery-contract.js +80 -0
  47. package/lib/domain/delivery-evidence.d.ts +12 -0
  48. package/lib/domain/delivery-evidence.js +41 -0
  49. package/lib/domain/event-store.d.ts +20 -0
  50. package/lib/domain/event-store.js +53 -0
  51. package/lib/domain/kanban-service.d.ts +94 -0
  52. package/lib/domain/kanban-service.js +430 -0
  53. package/lib/domain/permissions.d.ts +7 -0
  54. package/lib/domain/permissions.js +54 -0
  55. package/lib/domain/planning-checklist.d.ts +22 -0
  56. package/lib/domain/planning-checklist.js +81 -0
  57. package/lib/domain/prefetch-manifest.d.ts +21 -0
  58. package/lib/domain/prefetch-manifest.js +73 -0
  59. package/lib/domain/projection.d.ts +3 -0
  60. package/lib/domain/projection.js +165 -0
  61. package/lib/domain/review-evidence.d.ts +13 -0
  62. package/lib/domain/review-evidence.js +76 -0
  63. package/lib/domain/state-machine.d.ts +4 -0
  64. package/lib/domain/state-machine.js +32 -0
  65. package/lib/domain/task-parents.d.ts +17 -0
  66. package/lib/domain/task-parents.js +39 -0
  67. package/lib/domain/tdd-classify.d.ts +5 -0
  68. package/lib/domain/tdd-classify.js +20 -0
  69. package/lib/domain/types.d.ts +142 -0
  70. package/lib/domain/types.js +2 -0
  71. package/lib/index.d.ts +5 -0
  72. package/lib/index.js +56 -0
  73. package/lib/roles/preset-installer.d.ts +8 -0
  74. package/lib/roles/preset-installer.js +53 -0
  75. package/lib/roles/toolsets.d.ts +62 -0
  76. package/lib/roles/toolsets.js +332 -0
  77. package/lib/roles/wiki-worker.d.ts +20 -0
  78. package/lib/roles/wiki-worker.js +44 -0
  79. package/lib/routes/kanban-http.d.ts +6 -0
  80. package/lib/routes/kanban-http.js +159 -0
  81. package/lib/routes/kanban-sse.d.ts +8 -0
  82. package/lib/routes/kanban-sse.js +58 -0
  83. package/lib/routes/planning-driver.d.ts +16 -0
  84. package/lib/routes/planning-driver.js +52 -0
  85. package/lib/routes/prefix-router.d.ts +29 -0
  86. package/lib/routes/prefix-router.js +28 -0
  87. package/lib/services/kanban-provider.d.ts +16 -0
  88. package/lib/services/kanban-provider.js +14 -0
  89. package/lib/tools/kanban-tools.d.ts +11 -0
  90. package/lib/tools/kanban-tools.js +169 -0
  91. package/lib/tools/main-session-tools.d.ts +20 -0
  92. package/lib/tools/main-session-tools.js +170 -0
  93. package/lib/tools/planning-tools.d.ts +32 -0
  94. package/lib/tools/planning-tools.js +102 -0
  95. package/lib/tools/prefetch-tools.d.ts +5 -0
  96. package/lib/tools/prefetch-tools.js +59 -0
  97. package/lib/tools/spec-card-tools.d.ts +4 -0
  98. package/lib/tools/spec-card-tools.js +75 -0
  99. package/lib/tools/wiki-tools.d.ts +4 -0
  100. package/lib/tools/wiki-tools.js +57 -0
  101. package/lib/wiki/kb-linkage.d.ts +9 -0
  102. package/lib/wiki/kb-linkage.js +87 -0
  103. package/lib/wiki/page-path.d.ts +6 -0
  104. package/lib/wiki/page-path.js +28 -0
  105. package/lib/wiki/wiki-vault-client.d.ts +28 -0
  106. package/lib/wiki/wiki-vault-client.js +48 -0
  107. package/package.json +83 -0
  108. package/personas/kanban-d/agent.cordis.yml +157 -0
  109. package/personas/kanban-d/preset.yml +2 -0
  110. package/personas/kanban-dt/agent.cordis.yml +71 -0
  111. package/personas/kanban-dt/preset.yml +2 -0
  112. package/personas/kanban-p/agent.cordis.yml +66 -0
  113. package/personas/kanban-p/preset.yml +2 -0
  114. package/personas/kanban-pt/agent.cordis.yml +47 -0
  115. package/personas/kanban-pt/preset.yml +2 -0
  116. package/personas/kanban-v/agent.cordis.yml +47 -0
  117. package/personas/kanban-v/preset.yml +2 -0
  118. package/personas/kanban-w/agent.cordis.yml +47 -0
  119. package/personas/kanban-w/preset.yml +2 -0
  120. package/personas/persona-d.md +26 -0
  121. package/personas/persona-dt.md +18 -0
  122. package/personas/persona-p.md +13 -0
  123. package/personas/persona-pt.md +13 -0
  124. package/personas/persona-v.md +18 -0
  125. package/personas/persona-w.md +12 -0
@@ -0,0 +1,452 @@
1
+ # dsh-swarm
2
+
3
+ [简体中文](README.zh-CN.md) · [English](README.md)
4
+
5
+ ---
6
+
7
+ **受管六角色 DSH agent 蜂群:把单个需求变成严格、证据可核验的流水线。**
8
+
9
+ 编排者(V)把已批准的规格拆成严格有序的相位链(`p → (pt?) → w2 → d → dt → w3 → summary`);六个单一职责的角色(V / P / W / D / PT / DT)以隔离、受权限约束的工具面执行每个相位;每份交接都经过针对证据契约的机器校验;故障通过幂等重试与人工把关的评审恢复;实时 Workflow 看板标签页通过 SSE 把全部状态流式同步到浏览器。设计灵感源自 Hermes Agent kanban。
10
+
11
+ ![TypeScript](https://img.shields.io/badge/TypeScript-5.8-blue)
12
+ ![License](https://img.shields.io/badge/license-MIT-green)
13
+ ![npm](https://img.shields.io/npm/v/@joekytc/dsh-swarm)
14
+
15
+ ---
16
+
17
+ ## 为什么
18
+
19
+ 在单一任务上协调多个 AI agent,通常以三种方式失败:
20
+
21
+ 1. **角色漂移** ——"规划者"开始写代码,"执行者"评审自己的工作,没有人对结果负责。
22
+ 2. **不可验证的交接** ——agent 声称"完成",却没有可复现的证据,下游在流沙上继续建设。
23
+ 3. **静默死锁** ——agent 中途停住不再推进,管线挂起;或坏代码在任何人评审之前就被合并。
24
+
25
+ dsh-swarm 针对以上三种问题编码了*契约*:每个角色只有一项机器强制的职责;每次交接必须携带结构化证据,否则相位无法关闭;每次停滞或评审失败都会落入可见、可恢复的状态,并以**人类作为信任锚**。它被构建为**正确性优先**——确定性状态机、只追加的事件溯源、幂等调度器,以及一套红队测试套件——重放事件日志并拒绝任何非法转换。
26
+
27
+ ---
28
+
29
+ ## 角色与执行管线
30
+
31
+ 六个角色由调度器作为一次性 agent 会话派发(确定性会话 id `kbn-<taskId>`,重试/返工时经 `resumeSessionId` 恢复)。每个角色 agent 会话绑定到恰好一个任务(`boundTaskId`),并获得裁剪后的工具面。V 是例外:链级编排会话(`kbn-v-<chainId>`),无 `boundTaskId`。
32
+
33
+ | 角色 | 别名 | 职责 | 工具面(要点) |
34
+ |---|---|---|---|
35
+ | **V** | 编排者 | 驱动相位机,逐相位建卡,停滞时发布 `[blocked-review]` 指引。绝不执行。 | `kanban_create` + 任务工具 + 规格查看 |
36
+ | **P** | 规划者 | 读取规格 + 仓库事实(含只读自查),编写 OpenSpec 实施计划,用 `pt_decision.needed` 决定是否需要 PT。绝不执行。 | 任务工具 + 规格查看,只读(仅写 `openspec/changes/`) |
37
+ | **PT** | 计划评审者 | 对 P 的计划做只读评审(需求对齐、完整性、逻辑)。输出裁决 + 问题清单。 | 任务工具 + 规格查看,**只读 ToolGuard** |
38
+ | **W** | 知识库桥 | W2/W3 知识库同步(`w:kb`)。绝不碰代码/git。 | 任务工具 + `wiki_search/read/write` + 只读规格查看 |
39
+ | **D** | 执行者 | *唯一*写代码的角色:worktree → 实现 → 验证 → `[AI-GEN]` 提交 → 推送特性分支(合入 TARGET_BRANCH 由 system 在 DT 通过后执行)。 | 任务工具 + wiki 只读 + bash/fs/run_code(完整开发面)+ subagent(spawn/fork/list-agents)+ goal |
40
+ | **DT** | 实现评审者 | 实证验证 D 的工作(test/build/typecheck/diff/git + open-code-review),把评审页写入知识库。对仓库只读。 | 任务工具 + wiki 读写(评审命名空间)+ bash/fs/run_code,**只读 ToolGuard** |
41
+
42
+ 管线(R20 相位顺序,链路内严格串行,链路间并行):
43
+
44
+ ```text
45
+ p ──> (pt?) ──> w2 ──> d ──> dt ──> w3 ──> summary
46
+ | | | | | | |
47
+ 计划 计划评审 计划同步 实现 实现评审 知识库同步 收尾
48
+ (P) (仅当 P 自选) (W2) (D) (固定) (W3) (system)
49
+ ```
50
+
51
+ - `pt` 仅在 P 的交接交付 `pt_decision = { needed: true, reason }` 时创建——V 只负责建卡,system 从不覆盖该判定。`needed: false` 直接跳入 `w2`。
52
+ - `d` 之后**总是**创建 `dt`。
53
+ - 仓库事实由阶段 0 规划会话采集(`planning_prefetch`,只读),不再由 W 相位承担。
54
+ - 链路由机械规则完成,而非 agent:最后完成的任务是 W3(`w/kb`),D(`execute`)任务已带交付证据完成,且无未完成任务。
55
+
56
+ ---
57
+
58
+ ## 安装
59
+
60
+ ### 前置条件
61
+
62
+ - 可用的 [DSH](https://github.com/deepseek-ai) 安装(`@deepseek-ai/*` 运行时包:cordis、dsh-agent、dsh-tools、dsh-persona、dsh-session)。
63
+ - Node.js ≥ 22.19 与 npm(对齐 DSH 运行时要求)。
64
+ - DSH 随附的 peer 依赖:`@deepseek-ai/dsh-tool-bash`、`@deepseek-ai/dsh-tool-fs`、`@deepseek-ai/dsh-tool-fs-search`、`@deepseek-ai/schemastery`。
65
+ - 可选:供 W/P/D 知识库读取及 W2/W3 同步的 wiki-vault HTTP 服务(见[配置](#配置))。
66
+
67
+ ### 构建
68
+
69
+ ```bash
70
+ npm install
71
+ npm run build # tsc -p tsconfig.build.json (lib/*.js) + client bundle (lib/client.js)
72
+ ```
73
+
74
+ ### 安装为 DSH 插件
75
+
76
+ ```bash
77
+ # 从 npm 安装——Web profile 同时附带 kanban 浏览器标签页
78
+ dsh plugin --profile web add @joekytc/dsh-swarm
79
+
80
+ # 从本地检出安装(开发)
81
+ dsh plugin --profile <name> add ./dsh-swarm
82
+ ```
83
+
84
+ > 也可从 GitHub 源码安装:`dsh plugin --profile web add github:joekytc/dsh-swarm`。
85
+ >
86
+ > `storageDir` 必须使用**不加引号**的 `!!js dshHomePath("storages/kanban")` 写法。加引号会把路径退化成字面量字符串(已知陷阱)。
87
+
88
+ ### 快速上手
89
+
90
+ 1. 启动 DSH 会话,输入:
91
+
92
+ ```
93
+ /plan: <需求> / <项目> / <API>
94
+ ```
95
+
96
+ 进入阶段 0 规划(零副作用——此时不建任何卡):`grill-me` 一次只问一个澄清问题,
97
+ `planning_prefetch` 只读采集仓库事实,对话收敛为规划清单的规格六段
98
+ (`problem / solution / user_stories / impl_decisions / testing / out_of_scope`)
99
+ 外加仓库 manifest。`planning_checklist_save` 对清单做 schema 校验——非法或不完整会阻塞批准。
100
+
101
+ 2. 确认并启动:
102
+
103
+ ```
104
+ /openspec: 确认执行
105
+ ```
106
+
107
+ 从已保存的清单创建链路与规格卡;挂上 `file-prefetch`(仓库路径)与 `kb`(清单页)
108
+ 附件,规格被批准,链路转入 `executing`,调度器唤醒 V 编排者,后者逐相位搭建管线。
109
+
110
+ 3. 在**看板标签页**观察进度(会话中心的第三个标签:对话 → 轨迹 → 看板)。点击卡片查看
111
+ 概览 / 轨迹 / 交接 / 规格 / 评论。
112
+
113
+ 4. 链路完成时,系统审计工作区中是否有链路之外的写入,并(对 D 链路)把 D 的特性分支
114
+ 合并到 `TARGET_BRANCH`。若触发审计警告,需先在 GUI 中确认归属,才会展示最终汇报。
115
+
116
+ ---
117
+
118
+ ## 配置
119
+
120
+ 所有键均可选;默认值如下。schema 位于 `src/config.ts`。
121
+
122
+ | 键 | 默认值 | 说明 |
123
+ |---|---|---|
124
+ | `storageDir` | `$DSH_HOME/storages/kanban` | 事件日志(`events.jsonl`)、编排状态、每任务工作区、`dispatcher.log` |
125
+ | `wikiVault.baseUrl` | `http://192.168.122.111:3000` | 知识库读写用的 wiki-vault HTTP 服务 |
126
+ | `wikiVault.pagePrefix` | `projects/` | W 页面写入的白名单前缀 |
127
+ | `roles.models.<role>` | `{}` | 每角色模型:`{ provider, model, reasoningEffort?, fallbacks?[] }` |
128
+ | `roles.models.<role>.reasoningEffort` | `high` | 所有角色默认推理强度 |
129
+ | `roles.models.<role>.fallbacks` | `[]` | 静默回退候选(经 `[model-fallback]` 评论审计) |
130
+ | `dispatcher.staleTimeoutSeconds` | `14400` | 心跳超时;无心跳的 running 任务被回收 |
131
+ | `dispatcher.maxRetries` | `3` | 失败重试上限,超出进入熔断 → `blocked(gave_up)` |
132
+ | `dispatcher.heartbeatIntervalSeconds` | `300` | 看门狗心跳周期 |
133
+ | `dispatcher.maxProtocolViolations` | `2` | 协议违规护栏:连续违规超过该次数后,下一次即终局(`gave_up`) |
134
+ | `dispatcher.maxReworksPerRole` | `{ pt: 2, dt: 3 }` | 评审返工轮数上限,超出进入 `review/gave-up` + `[review-final]` |
135
+ | `prefixRoutes.plan` | `/plan:` | 阶段 0 规划前缀 |
136
+ | `prefixRoutes.openspec` | `/openspec:` | 批准并执行前缀 |
137
+ | `ui.enabled` | `true` | 启用看板 Web 标签页 |
138
+ | `ui.contentMinWidth` | `715` | 看板内容最小宽度(px) |
139
+ | `ui.contentMaxWidth` | `780` | 看板内容最大宽度(px) |
140
+ | `ui.sseHeartbeatSeconds` | `20` | SSE 心跳间隔 |
141
+
142
+ ---
143
+
144
+ ## 护栏
145
+
146
+ ### 权限矩阵
147
+
148
+ `can(action, actor, task, { boundTaskId })` 定义于 `src/domain/permissions.ts`。
149
+ "Bound" 表示该 actor 是*针对那个精确任务*派生的角色 agent 会话(`boundTaskId === task.id`,
150
+ 且 `complete` 还要求 `actor === task.assignee`)。
151
+
152
+ | 动作 | V | P | W | D | PT | DT | 人类 | 系统 |
153
+ |---|---|---|---|---|---|---|---|---|
154
+ | create-chain / create-task | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
155
+ | claim | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
156
+ | complete | ❌ | bound | bound | bound | bound | bound | ✅(GUI) | ✅ |
157
+ | block | ❌ | bound | bound | bound | bound | bound | ✅ | ✅ |
158
+ | heartbeat | ❌ | bound | bound | bound | bound | bound | ❌ | ❌ |
159
+ | comment | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
160
+ | unblock | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
161
+ | archive | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
162
+ | spec-approve | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
163
+ | spec-edit | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
164
+ | spec-attach | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
165
+ | update-title | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
166
+ | delete-chain | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
167
+ | wiki-write | ❌ | ❌ | ✅ | ❌ | ❌ | ✅(评审命名空间) | ❌ | ❌ |
168
+ | wiki-read | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ |
169
+ | prefetch | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
170
+ | audit-confirm | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
171
+ | create-rework-task | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
172
+
173
+ 关键保证(两点):
174
+
175
+ - **主会话不能执行。** 它只拿到 `kanban_show`/`kanban_list`/`kanban_comment` +
176
+ `spec_card_view` + `kanban_route` —— 绝无 `kanban_create`/`kanban_complete`/
177
+ `kanban_block`。建链/建规格只经 `/plan:`+`/openspec:`;GUI 只观察与变更任务状态,
178
+ 从不建链/建任务——"谁决定运行什么"保持显式、可审计。
179
+ - **会话绑定阻止跨任务越权**(绑定到任务 A 的 W agent,即使任务 B 同为 W 任务,也
180
+ 不能 complete/block 任务 B);DT 的写入被矩阵之上的 ToolGuard 限定在
181
+ `projects/<chain>/review/` 命名空间;且任何角色 agent 都不能批准规格、解除阻塞或
182
+ 确认审计——这些是人类信任锚;`system` 只做机械性记账。
183
+
184
+ ### 交付契约(上游欠下游)
185
+
186
+ 每个相位的交接必须携带下游真正会读到的键(`src/domain/delivery-contract.ts`)。
187
+ 缺键会立即阻塞当前角色的卡(且编排者不会在阻塞的父任务上建下游卡):
188
+
189
+ | 卡 | 必需交接键 |
190
+ |---|---|
191
+ | W2 / W3(`w:kb`) | `kb_url` + `page_path` |
192
+ | P(`p:openspec`) | `artifacts_path` + `pt_decision`(`needed` 布尔必填;`needed: true` 时 `reason` 必填) |
193
+ | D(`d:execute`) | `changed_files` +(`commit_hash` 或 `push`)——`hasDeliveryEvidence`;`branch`(特性分支)是合并闸门的期望输入,非硬性完成阻塞项;`tdd`(`test_files` 或 `skipped.reason`,二选一) |
194
+ | PT / DT | `review_evidence`(schema 合法)——`validateReviewEvidence` |
195
+
196
+ ### TDD 硬闸(证据门槛)
197
+
198
+ D 只有带 `tdd` 才能完成——`test_files`(含 `test_first`)或 `skipped.reason`
199
+ (二选一,见 `delivery-evidence.ts`)。DT 的 `review_evidence` 必须携带 `tdd`;
200
+ `pass` 裁决下 runner 必须是 `vitest`(`test.runner`)且 `test_first === true`
201
+ (见 `review-evidence.ts`)。这让"测试确实跑过、且先写测试"成为机器校验的属性,
202
+ 而非一句声明。
203
+
204
+ ### 阶段 0 规划清单
205
+
206
+ `/plan:` 跑只读规划会话(`grill-me` → `planning_prefetch` → `planning_checklist_save`,
207
+ 见 `planning-driver.ts`)。清单携带结构化 manifest(仓库事实 + 文件基线,见
208
+ `prefetch-manifest.ts`);非法 manifest 阻塞保存,`/openspec:` 把清单以 `file-prefetch`
209
+ + `kb` 附件挂到规格卡(见 `prefix-router.ts`)。
210
+
211
+ ### 评审质量链
212
+
213
+ - **P** 完成后,仅当 P 的交接交付 `pt_decision.needed = true` 时才创建 **PT** 卡;
214
+ 编排者从不覆盖该判定(V 只负责建卡)。
215
+ - **D** 完成后**总是**创建 **DT** 卡。
216
+ - **PT/DT 只读**:ToolGuard 机械性拒绝写仓库源码、git 变更,以及(对 DT)评审命名空间
217
+ 之外的 wiki 写入。
218
+ - **DT 评审引擎**:`open-code-review`(ocr,委派模式,diff `--from TARGET_BRANCH --to <特性分支>`)
219
+ → 回退 `superpowers code-review` → 两者都不可用才 block `review-tool-unavailable`。
220
+ - `review_evidence` 必须通过 `validateReviewEvidence`,否则评审卡无法完成:PT 需要
221
+ verdict + issues + 计划引用;DT 额外需要 test(通过时退出码 0)、build/typecheck、
222
+ lint、非空 diff、git、ocr/回退结论,以及 `tdd`。
223
+
224
+ ### 返工(评审失败)
225
+
226
+ 评审失败**从不改写** `done` 卡。系统改为记录 `review/failed`,创建**返工任务**
227
+ (`[返工] ...`),继承源会话(`resumeSessionId`)、`reviewAttempt + 1`,初始为
228
+ `todo`(`reviewStatus: 'pending'`),然后为返工重新派发全新评审卡。当 `reviewAttempt`
229
+ 达到 `maxReworksPerRole`(PT 2 / DT 3)时,系统记录 `review/gave-up` 并发布
230
+ `[review-final]` 证据链评论;管线停在评审阶段等待人类介入。
231
+
232
+ ### 故障恢复
233
+
234
+ 两条正交的故障路径,都可人工恢复:
235
+
236
+ - **协议违规**(agent 空闲却未 `complete`/`block`):角色 agent →
237
+ `blocked(protocol_violation)` → V 发布幂等 `[blocked-review]` 指引 → 人类解除阻塞 →
238
+ 同会话恢复(NOT 重新开始)。超过 `maxProtocolViolations`(2)次可恢复循环后,
239
+ 下一次违规 → `blocked(gave_up)` + system 发布 `[blocked-final]` 证据链(阻塞时间线 +
240
+ 评审/评论时间线 + 最终原因)。
241
+ - **硬故障与熔断**:`task/failed` 累加 `attempts`;调度器在 `attempts < maxRetries` 时
242
+ 重新派发(同会话恢复),随后熔断到 `blocked(gave_up: max retries)`。看门狗回收在
243
+ `staleTimeoutSeconds` 内停止心跳的 `running` 任务(心跳本身只是*状态*信号,绝不是
244
+ 业务变更;SSE 心跳从不携带看板状态)。每角色模型候选(主模型 + 回退,默认
245
+ `reasoningEffort: high`)静默回退(经 `[model-fallback]` 评论审计);*所有*候选都失败
246
+ 则 block `model-unavailable` 等待人类。单个挂起的 V 唤醒不会卡死调度器——每次派发
247
+ 都被包在超时里。
248
+
249
+ ### 链路完成:审计闸门 + 合并闸门
250
+
251
+ 机械性链路完成规则触发时,两个闸门在 `chain/completed` 钩子中运行:
252
+
253
+ 1. **完成审计闸门(D23)**:`ChainAuditor` 交叉核对链路工作区中是否存在已知任务输出
254
+ 之外的产物。发现孤儿写入即发出 `chain/audit-warning`;UI 显示警告横幅并阻塞最终
255
+ 汇报,直到人类确认归属(`chain/audit-confirmed`,仅限人类)。
256
+ 2. **合并闸门(DT 通过后的系统合并)**:D 从不合并到 `TARGET_BRANCH`,也不推送它——
257
+ D 只提交到(可选推送)自己的特性分支,并在交接中携带 `branch`。DT 批准且链路完成后,
258
+ `merge-gate.ts` 以 `system` 身份执行:`git checkout TARGET_BRANCH → git merge --no-ff
259
+ <feature-branch> → git push`。结果以幂等评论记录:`[merge-done]`(带 hash)、
260
+ `[merge-skip]`(合并输入无法解析)、`[merge-failed]`(checkout/merge/push 失败,例如
261
+ 冲突)。失败绝不抛错——坏合并*不执行*,这是安全方向;人类事后可修复。
262
+
263
+ ---
264
+
265
+ ## 事件溯源与领域模型
266
+
267
+ 每次状态变更都追加到 `<storageDir>/events.jsonl`,每行一个 JSON 事件。`seq` 由存储
268
+ 分配(每次追加时从文件尾部重读,并发实例永不冲突)。**轨迹即事件日志本身**;重启回放
269
+ 日志即可重建看板。
270
+
271
+ ```jsonc
272
+ // events.jsonl 中的一行
273
+ { "seq": 12, "chainId": "ch_x_...", "taskId": "t_y_...",
274
+ "kind": "task/completed",
275
+ "payload": { "summary": "...", "metadata": { /* 交接证据 */ } },
276
+ "author": "w", "at": 1760000000000 }
277
+ ```
278
+
279
+ 事件族:`chain/*`(created, executing, completed, aborted, root-task-set,
280
+ audit-warning, audit-confirmed, title-updated)、`spec-card/*`(created, edited,
281
+ approved)、`task/*`(created, claimed, heartbeat, commented, completed, blocked,
282
+ unblocked, failed, archived, renamed)、`review/*`(passed, failed, gave-up)。
283
+
284
+ 回放是**严格**的:投影把每个事件经过状态机应用,任何非法转换都会抛错——损坏或被篡改的
285
+ 日志会响亮失败,而不是静默产出不一致的看板(由 `tests/redteam/anti-escalation.test.ts`
286
+ 与 `tests/domain/projection.test.ts` 覆盖)。
287
+
288
+ 服务通过串行队列发布事件(先落盘再发布),订阅方(SSE)按序收到每个事件且恰好一次。
289
+ UI 与调度器消费的是同一份持久化事件——不存在第二个真相源。
290
+
291
+ ---
292
+
293
+ ## Web 客户端(Workflow 看板标签页)
294
+
295
+ 注册为第三个 `conversation.view` 槽位的浏览器半 React 标签页(`id=kanban`、`order=20`,
296
+ 位于 对话 与 轨迹 之后)。它**不**注册 shell 级浮层、侧栏或详情抽屉。
297
+
298
+ - **数据路径**:初始快照(`GET /kanban/board`)→ SSE 流(`GET /kanban/events?after=<seq>`)
299
+ → board-store 增量应用事件、按 `seq` 去重,任何缺口都重拉完整快照。**无业务轮询。**
300
+ - **布局**:多链路垂直轨道;内容宽度固定 715–780 px,整高;当前链路展开,阻塞链路
301
+ 始终显示警告摘要。页内改名/删除用轻量弹窗(无 shell 浮层);无拖拽、无宽度记忆。
302
+ - **卡片**:紧凑双行卡片 + 按 profile 着色的节点;状态线为 绿实线(完成)/ 蓝实线(当前)/
303
+ 灰虚线(等待)/ 红断点(阻塞)。
304
+ - **详情抽屉**:五区——概览 / 轨迹 / 交接 / 规格 / 评论;`Esc` 或返回回到列表。
305
+ - **动作**(`POST /kanban/action`):block / unblock / retry / complete / archive /
306
+ comment,外加链路级 `confirm-audit`、`rename`(链或任务)与 `delete`(整链,仅 human,
307
+ GUI 二次确认)。人类动作应用带回滚的乐观更新;store 在任何分歧时对权威快照重新对账。
308
+ - **构建**:`npm run build:client` 生成 `lib/client.js`,采用 `window.__ModuleLoader__.load()`
309
+ 格式(与 `dsh-client-*` 相同的约定)。把 dsh-swarm 加入 web profile 会自动把它嵌入
310
+ `__DSH_BOOT__`。
311
+
312
+ ---
313
+
314
+ ## 架构
315
+
316
+ 五层结构,领域层**不依赖任何 DSH**,因此可以被完全单测并独立回放。
317
+
318
+ ```mermaid
319
+ flowchart TB
320
+ subgraph Client
321
+ Tab["conversation.view tab (id=kanban, order=20)"]
322
+ Store["board-store: snapshot + SSE + seq gap resync"]
323
+ Model["workflow-model: pure view projection"]
324
+ end
325
+
326
+ subgraph Domain ["domain/ (pure TS, zero DSH deps)"]
327
+ ES["event-store (JSONL append-only, monotonic seq)"]
328
+ SM["state-machine (task/chain/spec transitions)"]
329
+ PJ["projection (events → BoardState)"]
330
+ PM["permissions (actor × session-bound matrix)"]
331
+ KS["kanban-service (three-interface facade)"]
332
+ EC["delivery-contract / delivery-evidence / review-evidence / prefetch-manifest"]
333
+ end
334
+
335
+ subgraph Integration ["integration (cordis)"]
336
+ TOOLS["tools: kanban_* / spec_card_* / wiki_* / prefetch_* / kanban_route"]
337
+ ROUTES["prefix-router + planning-driver (/plan: /openspec:)"]
338
+ HTTP["kanban-http + kanban-sse (/kanban/board, /kanban/events, /kanban/action)"]
339
+ end
340
+
341
+ subgraph Dispatcher ["dispatcher/"]
342
+ WAKER["event-waker (events → wake V)"]
343
+ VORCH["v-orchestrator (R20 phase machine)"]
344
+ RUNNER["agent-runner (one-shot role sessions, presets, ToolGuards)"]
345
+ WD["watchdog (heartbeat / stale reclaim / circuit)"]
346
+ AUDIT["chain-auditor (D23 completion audit)"]
347
+ MG["merge-gate (post-DT system merge)"]
348
+ end
349
+
350
+ subgraph Roles ["roles/ + personas/"]
351
+ PRESETS["preset-installer (6 trimmed presets)"]
352
+ TOOLSETS["toolsets (per-role tool faces + write guards)"]
353
+ WK["wiki-worker (W prefetch worker)"]
354
+ end
355
+
356
+ subgraph Wiki ["wiki/"]
357
+ WVC["wiki-vault-client (search/read/write)"]
358
+ end
359
+
360
+ Store <-->|HTTP/SSE| HTTP
361
+ Tab --> Store --> Model
362
+ ROUTES --> KS
363
+ TOOLS --> KS
364
+ HTTP --> KS
365
+ WAKER --> VORCH
366
+ VORCH --> KS
367
+ VORCH --> RUNNER
368
+ RUNNER --> TOOLSETS --> PRESETS
369
+ RUNNER --> WVC
370
+ WK --> WVC
371
+ AUDIT --> KS
372
+ MG --> KS
373
+ KS --> ES --> PJ --> SM --> PM
374
+ EC --> KS
375
+ ```
376
+
377
+ ### 各层职责
378
+
379
+ - **领域层**(`src/domain/`)—— 整个业务模型,纯 TypeScript:事件存储、状态机、投影、
380
+ 权限矩阵、交付/评审/manifest 校验器,以及把来自工具、CLI、UI 的每次写入统一路由到
381
+ 单一权威的 `KanbanService` 门面。单测充分覆盖。
382
+ - **集成层**(`src/tools/`、`src/routes/`)—— cordis 工具与路由:角色工具面、主会话
383
+ 工具(`kanban_route` + 只读子集)、`/kanban/*` HTTP/SSE 桥。
384
+ - **调度层**(`src/dispatcher/`)—— 事件唤醒、R20 编排、一次性 agent 运行器(persona
385
+ preset 挂载、模型候选链、ToolGuard 安装)、看门狗、链路审计器、合并闸门。
386
+ - **角色层**(`src/roles/`、`personas/`)—— 安装到 `$DSH_HOME/.agent-presets/` 的裁剪
387
+ preset、每角色工具装配、写保护逻辑。
388
+ - **知识库层**(`src/wiki/`)—— 面向 wiki-vault 的轻量 HTTP 客户端。
389
+
390
+ ---
391
+
392
+ ## 开发
393
+
394
+ 质量闸门(见 `AGENTS.md`):
395
+
396
+ ```bash
397
+ npm run typecheck # tsc -p tsconfig.json --noEmit (0 errors)
398
+ npm test # npx vitest run (52 个文件 / 450 用例,全绿)
399
+ npm run build # tsc -p tsconfig.build.json + build:client (lib/client.js)
400
+ ```
401
+
402
+ GUI 验证(仅当端口 3080 上已有 dsh web 实例时;**不要**启动第二个实例):
403
+
404
+ ```bash
405
+ python tests/e2e/gui-check.py --url http://127.0.0.1:3080/
406
+ ```
407
+
408
+ > 部署到运行中的 DSH 实例需要插件重载/重启;仅构建不会热重载正在运行的插件。
409
+
410
+ ---
411
+
412
+ ## 路线图与已知限制
413
+
414
+ ### 已实现(v0.1.0)
415
+
416
+ - [x] 事件溯源领域 + 确定性状态机(红队回放)
417
+ - [x] 6 角色 R20 管线 + 裁剪 preset + 会话绑定权限
418
+ - [x] 交付契约 + 评审证据闸门 + 返工生命周期
419
+ - [x] TDD 硬闸(D `tdd` 交接 + DT `test_first` / `runner=vitest` 核验)
420
+ - [x] 协议违规恢复、心跳看门狗、失败熔断
421
+ - [x] 链路完成审计闸门(D23)+ 人工确认
422
+ - [x] DT 通过后合并闸门(D 只推送特性分支)
423
+ - [x] 阶段 0 规划清单 + `file-prefetch` 附件
424
+ - [x] GUI 链/任务改名 + 整链删除(T7,仅 human)
425
+ - [x] 模型候选链:静默回退 + High 推理强度
426
+ - [x] 实时 SSE 看板标签页(对话 → 轨迹 → 看板)
427
+
428
+ ### 规划中
429
+
430
+ - [ ] 每任务预算护栏(最大 token / 工具调用 / 墙钟时间)与按故障分类的退避
431
+ - [ ] 可复现的 DT 验证(回放命令 + stdout 证据)与硬标记上的双模型仲裁
432
+ - [ ] 结构化指标 + 每链路审计轨迹聚合
433
+ - [ ] V 上下文压缩 / 状态摘要注入 + 会话自愈
434
+ - [ ] 多 agent 流程的端到端契约测试框架
435
+ - [ ] 更多人工介入点(推送前 / 硬标记时)+ 系统辅助硬标记检测
436
+
437
+ ### 已知限制
438
+
439
+ - **写保护是字符串启发式,不是硬隔离。** PT/DT ToolGuard 依赖路径/命令正则,评审者
440
+ 没有 git 凭据;这是软约束加审计轨迹,而非挂载级沙箱。
441
+ - **验证环境中没有 `open-code-review` CLI**:回退路径(superpowers `code-review`)
442
+ 已实现并测试,但 ocr 委派模式输出解析有待在装有 ocr 的机器上验证。
443
+ - **评审证据是存在性检查,而非回放证明。** 字段必须存在且格式合法;证明测试确实运行
444
+ 在路线图上。
445
+ - **配置默认值里只有一个 wiki-vault 主机**——请把 `wikiVault.baseUrl` 指向你的部署。
446
+ - **PT 建卡依赖 P 自报的 `pt_decision.needed`**——从仓库信号做系统辅助检测在路线图上。
447
+
448
+ ---
449
+
450
+ ## 许可证
451
+
452
+ [MIT](LICENSE)
@@ -0,0 +1,41 @@
1
+ import type { TaskCardView } from './workflow-model.js';
2
+
3
+ /** T26:双行 Profile 任务卡。Profile 只用于头像/节点强调,不整卡染色。
4
+ * 角色卡标题不可改(仅需求链标题可改),根元素为 div role=button。 */
5
+ export function BoardCard(props: { view: TaskCardView; onOpen: (taskId: string) => void }) {
6
+ const { view } = props;
7
+ const { task } = view;
8
+ const blocked = view.lineState === 'blocked' && view.dependencyLabel.length > 0;
9
+ return (
10
+ <div
11
+ role="button"
12
+ tabIndex={0}
13
+ className={`dsh-kb-task dsh-kb-task--${view.lineState}${view.related ? ' dsh-kb-task--related' : ''}`}
14
+ data-selected={view.selected || undefined}
15
+ onClick={() => props.onOpen(task.id)}
16
+ onKeyDown={(e) => {
17
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); props.onOpen(task.id); }
18
+ }}
19
+ aria-label={`${view.phase} ${task.title} ${view.statusLabel}`}
20
+ >
21
+ <span className={`dsh-kb-profile dsh-kb-profile--${task.assignee}`}>{task.assignee.toUpperCase()}</span>
22
+ <span className="dsh-kb-task__title">{task.title}</span>
23
+ <span className="dsh-kb-task__status-row">
24
+ <span className="dsh-kb-task__status">{view.statusLabel}</span>
25
+ </span>
26
+ <span className="dsh-kb-task__meta">
27
+ {view.phase} · {view.activityLabel}{!blocked && view.dependencyLabel ? ` · ${view.dependencyLabel}` : ''}
28
+ </span>
29
+ {blocked && (
30
+ <span className="dsh-kb-task__warn">
31
+ <svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
32
+ <path d="M8 1.5 14.5 13.5h-13L8 1.5Z" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
33
+ <path d="M8 6.2v3.6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
34
+ <circle cx="8" cy="11.6" r="0.9" fill="currentColor" />
35
+ </svg>
36
+ <span>{view.dependencyLabel}</span>
37
+ </span>
38
+ )}
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,16 @@
1
+ import type { BoardConnectionState } from './board-store.js';
2
+
3
+ /** T28:连接状态提示;ready 不占空间。 */
4
+ export function ConnectionBanner(props: { connection: BoardConnectionState; lastSuccessAt: number | null; onRetry?: () => void }) {
5
+ if (props.connection === 'ready') return null;
6
+ const label = props.connection === 'loading' ? '正在加载' : props.connection === 'reconnecting' ? '正在重连' : '连接错误';
7
+ return (
8
+ <div className={`dsh-kb-banner dsh-kb-banner--${props.connection}`} role="status">
9
+ {label}
10
+ {props.connection === 'error' && props.lastSuccessAt ? ` · 最后成功 ${new Date(props.lastSuccessAt).toLocaleTimeString()}` : ''}
11
+ {props.connection === 'error' && (
12
+ <button type="button" className="dsh-kb-banner__retry" onClick={props.onRetry}>重试</button>
13
+ )}
14
+ </div>
15
+ );
16
+ }