dsh-tower 2.0.3 → 2.1.0-beta.1

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 (193) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/COMPATIBILITY.md +38 -0
  3. package/CONTRIBUTING.md +150 -2
  4. package/README.en.md +59 -16
  5. package/README.md +57 -16
  6. package/SECURITY.md +31 -1
  7. package/adapters/default.js +10 -0
  8. package/adapters/store-fs.js +6 -0
  9. package/adapters/vcs-git.js +4 -0
  10. package/app/batch.js +79 -0
  11. package/app/board.js +25 -0
  12. package/app/checkCompletion.js +76 -0
  13. package/app/enrichedMissions.js +26 -0
  14. package/app/findings.js +78 -0
  15. package/app/gate.js +108 -0
  16. package/app/init.js +249 -0
  17. package/app/merge.js +567 -0
  18. package/app/messages.js +66 -0
  19. package/app/mission.js +35 -0
  20. package/app/missionContext.js +14 -0
  21. package/app/plan.js +42 -0
  22. package/app/provision.js +193 -0
  23. package/app/read.js +57 -0
  24. package/app/rebase.js +302 -0
  25. package/app/recording.js +72 -0
  26. package/app/review.js +145 -0
  27. package/app/roster.js +123 -0
  28. package/app/spawn.js +125 -0
  29. package/app/teardown.js +184 -0
  30. package/app/tower.js +524 -0
  31. package/cordis.patch.yml +61 -53
  32. package/docs/README.md +33 -1
  33. package/docs/architecture/overview.md +114 -14
  34. package/docs/board.md +72 -0
  35. package/docs/examples/first-mission.md +2 -1
  36. package/docs/faq.md +11 -7
  37. package/docs/guide/hot-reload.md +1 -1
  38. package/docs/guide/install-upgrade-migration.md +22 -6
  39. package/docs/guide/quickstart.md +12 -7
  40. package/docs/guide/release.md +125 -0
  41. package/docs/guide/scenarios-and-philosophy.md +86 -69
  42. package/docs/operations/security.md +65 -14
  43. package/docs/reference/blockers.md +11 -10
  44. package/docs/reference/config.generated.md +66 -0
  45. package/docs/reference/config.md +11 -7
  46. package/docs/reference/errors.generated.md +39 -0
  47. package/docs/reference/errors.md +10 -2
  48. package/docs/reference/events.md +7 -4
  49. package/docs/reference/testing.md +2 -1
  50. package/docs/reference/tools.generated.md +33 -0
  51. package/docs/reference/tools.md +8 -7
  52. package/docs/zh/README.md +4 -3
  53. package/docs/zh/faq.md +9 -6
  54. package/docs/zh/guide/hot-reload.md +1 -1
  55. package/docs/zh/guide/install-upgrade-migration.md +17 -5
  56. package/docs/zh/guide/quickstart.md +13 -8
  57. package/docs/zh/guide/scenarios-and-philosophy.md +55 -50
  58. package/docs/zh/operations/security.md +43 -7
  59. package/docs/zh/reference/blockers.md +10 -10
  60. package/docs/zh/reference/config.md +11 -7
  61. package/docs/zh/reference/errors.md +4 -1
  62. package/docs/zh/reference/events.md +6 -3
  63. package/docs/zh/reference/testing.md +3 -2
  64. package/docs/zh/reference/tools.md +8 -7
  65. package/domain/collab/batch.js +112 -0
  66. package/domain/collab/messages.js +127 -0
  67. package/domain/collab/roster.js +158 -0
  68. package/domain/collab/spawn.js +188 -0
  69. package/domain/config/schema.js +404 -0
  70. package/domain/contracts/outputs.js +279 -0
  71. package/domain/contracts/tool-inputs/collaboration.js +58 -0
  72. package/domain/contracts/tool-inputs/findings.js +29 -0
  73. package/domain/contracts/tool-inputs/index.js +94 -0
  74. package/domain/contracts/tool-inputs/integration.js +37 -0
  75. package/domain/contracts/tool-inputs/missions.js +94 -0
  76. package/domain/contracts/tool-inputs/ops.js +75 -0
  77. package/domain/contracts/tool-inputs/shared.js +25 -0
  78. package/domain/errors.js +96 -0
  79. package/domain/gate.js +125 -0
  80. package/domain/integration/gate.js +176 -0
  81. package/domain/integration/init.js +225 -0
  82. package/domain/integration/liveness.js +31 -0
  83. package/domain/integration/merge.js +377 -0
  84. package/domain/integration/provision.js +124 -0
  85. package/domain/integration/rebase.js +236 -0
  86. package/domain/integration/review.js +52 -0
  87. package/domain/integration/teardown.js +118 -0
  88. package/domain/mission.js +68 -0
  89. package/domain/missions/completion.js +114 -0
  90. package/domain/missions/findings.js +201 -0
  91. package/domain/missions/lifecycle.js +185 -0
  92. package/domain/missions/plan.js +149 -0
  93. package/domain/missions/review.js +179 -0
  94. package/domain/planning.js +118 -0
  95. package/domain/read/enrichedMissions.js +58 -0
  96. package/domain/read/missionContext.js +53 -0
  97. package/domain/read/views.js +128 -0
  98. package/domain/report.js +59 -0
  99. package/domain/roster.js +40 -0
  100. package/domain/scope.js +105 -0
  101. package/domain/state/schema.js +233 -0
  102. package/domain/state/semantics.js +117 -0
  103. package/domain/state.js +33 -0
  104. package/domain/summary.js +114 -0
  105. package/domain/vocabulary.js +179 -0
  106. package/index.js +86 -34
  107. package/lib/briefing.js +176 -0
  108. package/lib/compatibility.js +108 -0
  109. package/lib/config.js +54 -302
  110. package/lib/doctor.js +108 -9
  111. package/lib/git.js +93 -15
  112. package/lib/hmr.js +4 -3
  113. package/lib/leases.js +3 -2
  114. package/lib/legacy.js +20 -9
  115. package/lib/modeState.js +39 -0
  116. package/lib/modeStore.js +35 -33
  117. package/lib/persistence.js +349 -0
  118. package/lib/presets.js +35 -0
  119. package/lib/spawn-prechecks.js +168 -0
  120. package/lib/spawn-support.js +79 -0
  121. package/lib/store.js +57 -85
  122. package/lib/tests.js +46 -11
  123. package/lib/tool-registry.js +544 -0
  124. package/lib/workspaceLock.js +46 -22
  125. package/lib/worktreeOwnership.js +5 -4
  126. package/locale/en.json +23 -1
  127. package/locale/zh.json +22 -0
  128. package/package.json +33 -4
  129. package/ports/clock.js +6 -0
  130. package/ports/ids.js +7 -0
  131. package/ports/index.js +2 -0
  132. package/ports/store.js +11 -0
  133. package/ports/vcs.js +11 -0
  134. package/scripts/architecture.mjs +94 -0
  135. package/scripts/check-docs.mjs +44 -3
  136. package/scripts/check-pack.mjs +49 -0
  137. package/scripts/domain-metrics.mjs +48 -0
  138. package/scripts/generate-config-reference.mjs +46 -0
  139. package/scripts/generate-error-reference.mjs +29 -0
  140. package/scripts/generate-tools-reference.mjs +56 -0
  141. package/scripts/issue-audit.mjs +259 -0
  142. package/scripts/lib/cli.mjs +76 -0
  143. package/scripts/lib/tmp.mjs +20 -0
  144. package/scripts/mount-smoke/fixtures.mjs +162 -0
  145. package/scripts/mount-smoke/host.json +4 -0
  146. package/scripts/mount-smoke/probe.mjs +125 -0
  147. package/scripts/mount-smoke/surface.mjs +141 -0
  148. package/scripts/mount-smoke.mjs +270 -0
  149. package/scripts/pack-manifest.txt +203 -0
  150. package/scripts/release-check.mjs +303 -0
  151. package/skills/tower-recovery.md +7 -5
  152. package/tools/adopt.js +72 -0
  153. package/tools/batch.js +78 -0
  154. package/tools/complete.js +53 -0
  155. package/tools/diff.js +39 -0
  156. package/tools/doctor.js +50 -0
  157. package/tools/finding.js +40 -0
  158. package/tools/findings.js +39 -0
  159. package/tools/import-legacy.js +42 -0
  160. package/tools/inbox.js +39 -0
  161. package/tools/index.js +39 -0
  162. package/tools/init.js +63 -0
  163. package/tools/interrupt.js +42 -0
  164. package/tools/legacy-map.js +44 -0
  165. package/tools/merge.js +39 -0
  166. package/tools/mission.js +41 -0
  167. package/tools/plan.js +39 -0
  168. package/tools/preflight.js +44 -0
  169. package/tools/rebase.js +39 -0
  170. package/tools/recover.js +45 -0
  171. package/tools/reload.js +52 -0
  172. package/tools/resource.js +54 -0
  173. package/tools/review.js +45 -0
  174. package/tools/roster.js +60 -0
  175. package/tools/send.js +147 -0
  176. package/tools/spawn.js +196 -0
  177. package/tools/status.js +94 -0
  178. package/tools/teardown.js +42 -0
  179. package/tools/test.js +61 -0
  180. package/types/cordis.d.ts +6 -0
  181. package/types/index.d.ts +339 -0
  182. package/ui-bundle/README.md +111 -0
  183. package/ui-bundle/client.js +1980 -0
  184. package/ui-bundle/cordis.patch.yml +3 -0
  185. package/ui-bundle/icon.svg +6 -0
  186. package/ui-bundle/index.js +2 -0
  187. package/ui-bundle/locale/en.json +6 -0
  188. package/ui-bundle/locale/zh.json +6 -0
  189. package/ui-bundle/package.json +35 -0
  190. package/lib/errors.js +0 -111
  191. package/lib/tools.js +0 -2250
  192. package/lib/tower.js +0 -2689
  193. /package/{lib → domain}/batch.js +0 -0
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/louifinn/dsh-tower/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/louifinn/dsh-tower/actions/workflows/test.yml)
4
4
  [![npm](https://img.shields.io/npm/v/dsh-tower.svg)](https://www.npmjs.com/package/dsh-tower)
5
+ [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/louifinn/dsh-tower)
5
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
7
  [English](README.en.md)
7
8
 
@@ -13,9 +14,9 @@ Tower 模式与 mission/审查工作流的灵感来自 Moonshot AI 的 **Kimi Co
13
14
 
14
15
  ## 特性
15
16
 
16
- - **由人类掌握的 Tower 模式。** `/tower` 切换会话级模式:收窄工具面并注入 Tower 协议;`/tower on|off <指令>` 还能把指令作为下一步用户消息转发给模型。
17
+ - **可配置的 Tower 模式。** `/tower` 切换会话级模式:收窄工具面并注入 Tower 协议;`/tower on|off <指令>` 还能把指令作为下一步用户消息转发给模型。默认也允许 Agent 通过 `tower_control` 带理由进出;设 `mode.allowAgentControl:false` 可改为仅人类控制。
17
18
  - **任务级隔离。** 每个 mission 使用独立分支和 Git worktree,worker 不会碰到你的主检出。
18
- - **审查门禁合并。** 审查者针对当前分支 tip 给出 `clean` / `merge` 结论;合并门禁重新校验 scope、分支新鲜度、任务完成度、审查记录与 worktree 归属,通过后才执行 `--no-ff` 合并。独立审查默认可选,设 `review.requireReview:true` 可全局强制(仍可随时提交 review)。
19
+ - **审查门禁合并。** 开启 `review.requireReview:true` 时,审查者针对当前分支 tip 给出 `clean` / `merge` 结论;合并门禁重新校验 scope、分支新鲜度、任务完成度、审查记录与 worktree 归属,通过后才执行 `--no-ff` 合并。独立审查默认可选(门禁关闭);全局开启时,单个 mission 用 `reviewPolicy:"none"` + 理由并开启 `gates.allowReviewExemption:true` 做更窄豁免。
19
20
  - **可恢复的协作状态。** roster、inbox、findings、reviews 与 mission 状态持久化在 `.git/dsh-tower/` 下,重启不会丢计划。
20
21
  - **默认安全。** Tower 模式开启期间拒绝 `create_goal`、`update_goal(resume)`、`todo_write`、Team 任务板与 `spawn_teammate`;被暂停的 goal 不会被自动恢复。
21
22
  - **可追溯的 finding 生命周期。** `tower_findings` 列表、读取并按 `open → triaged → fixed` / `wontfix` 流转,也可重新打开;只有 tower 或 worker 能改状态,reviewer 只能读。
@@ -33,7 +34,7 @@ Tower 是**面向 Git 仓库变更的交付协议**,不是所有多代理场
33
34
 
34
35
  - 一个特性/重构能拆成 2–8 个 scope 清晰、可独立审查的 mission;
35
36
  - 有依赖顺序、需要分支新鲜度与 scope 约束的迁移;
36
- - 敏感仓库、发布绑定变更,或需要"被审查 commit + 测试证据 + 可审计合并"的工作;
37
+ - 敏感仓库、发布绑定变更,或需要"可审计合并 + 测试证据"(需要时开启独立审查)的工作;
37
38
  - 跨上下文压缩、重启、多天协作,需要从本地状态恢复的工作;
38
39
  - 需要把"完成"定义成任务 + 测试(可选独立审查),而不是聊天里的一句"好了"。
39
40
 
@@ -51,7 +52,7 @@ Tower 模式会拒绝 Team/Workflow/Ralph/普通 subagent 工具并清除 plan m
51
52
 
52
53
  ## 环境要求
53
54
 
54
- - DeepSeek Harness `0.2.1-rc.2`(或兼容的新版本)
55
+ - DeepSeek Harness `0.2.1-rc.2`(已验证的 host 构建见 [COMPATIBILITY.md](COMPATIBILITY.md))
55
56
  - Node.js 22+
56
57
  - Git(worktree 在本地创建,不需要联网)
57
58
 
@@ -78,6 +79,16 @@ dsh plugin --profile <profile> add <本仓库的绝对路径>
78
79
 
79
80
  Host 插件在 DSH 进程启动时加载,**安装或更新后需重启 DSH**。卸载使用 `remove_bundle`,包名 `dsh-tower`。
80
81
 
82
+ 可选的 Web UI 随 npm 包一起发布在 `ui-bundle/`:npm 安装后目标为
83
+ `<profile>/node_modules/dsh-tower/ui-bundle`,源码安装时为目标仓库的 `ui-bundle/` 目录。用
84
+ `plugin_manager({action:"install_bundle", target:"<该目录>"})` 安装并重启 DSH,然后刷新页面。
85
+
86
+ ### 已知冲突
87
+
88
+ 不要同时安装 `dsh-plugins-tower`(第三方包)。它的 loader row 也使用 `name: dsh-tower`,会解析到
89
+ 本包,导致本包被加载两次或对方代码不生效。`tower_doctor` 的 `loader-conflicts` 检查会报告该情况;
90
+ 卸载冲突包并重启 DSH。
91
+
81
92
  ## 快速开始
82
93
 
83
94
  ```text
@@ -87,16 +98,17 @@ tower_init repoRoot=D:\work\repo # 首次初始化,记录 base
87
98
  tower_plan repoRoot=D:\work\repo missions=[{"title":"Feature A","scope":["src/a/**"],"tasks":["implement","commit"],"kind":"build"}]
88
99
  tower_spawn repoRoot=D:\work\repo missionId=M1 role=worker
89
100
  # worker 在 worktree 内实现并提交,调用 tower_complete;最终报告会返回本会话
101
+ # 可选:需要强制独立审查时,先开启 review.requireReview:true
90
102
  tower_spawn repoRoot=D:\work\repo missionId=M1 role=reviewer
91
103
  # reviewer 只读审查并调用 tower_review(clean+merge 或 p1-Nitems/p2-Nitems)
92
- tower_preflight repoRoot=D:\work\repo missionId=M1 # 审查前 预期 no-review;审查后应为 ok:true
104
+ tower_preflight repoRoot=D:\work\repo missionId=M1 # 默认审查可选;开启 review.requireReview 时审查前预期 no-review,审查后 ok:true
93
105
  tower_merge repoRoot=D:\work\repo missionId=M1
94
106
  tower_teardown repoRoot=D:\work\repo
95
107
 
96
108
  /tower off
97
109
  ```
98
110
 
99
- 小改动通常一轮 worker + 一轮 reviewer 即可完成。完整可复制示例见 [docs/examples/first-mission.md](docs/examples/first-mission.md),逐工具参数见 [docs/reference/tools.md](docs/reference/tools.md)。
111
+ 小改动通常一轮 worker 即可完成;需要独立审查时再加一轮 reviewer。完整可复制示例见 [docs/examples/first-mission.md](docs/examples/first-mission.md),逐工具参数见 [docs/reference/tools.md](docs/reference/tools.md)。
100
112
 
101
113
  ## 命令
102
114
 
@@ -107,10 +119,22 @@ tower_teardown repoRoot=D:\work\repo
107
119
  | `/tower on <指令>` | 开启/保持,并把 `<指令>` 作为下一条用户消息转发 |
108
120
  | `/tower off` | 退出 Tower 模式 |
109
121
  | `/tower off <指令>` | 退出并转发 `<指令>` |
122
+ | `/tower ack` | 记录人类确认,供 `mode.requireHumanAckOnFirstEntry` 使用 |
110
123
  | `/tower <指令>` | 确保开启,然后转发 `<指令>` |
111
124
 
112
125
  图片与文件附件会随指令一起作为一条完整的 `UserMessage` 转发。
113
126
 
127
+ ## 组件模型
128
+
129
+ 本 bundle 采用 **all-or-nothing**:`cordis.patch.yml` 只插入一个 `tower-bundle`
130
+ `cordis:group` 行,Host 插件、skills、worker/reviewer preset 都是它的内部 section。插件页只应把
131
+ 这个 group 当作一个组件;关闭它时四个 section 一起关闭,不会留下指向已消失 `tower_*` 工具的
132
+ orphan preset/skill。不要再把子行单独启停。
133
+
134
+ 进入 Tower 模式与 `tower_plan` 前会校验两个 preset 是否已注册;缺失时直接以 `preset-missing`
135
+ 拒绝(并在消息中点名 `preset-tower-worker` / `preset-tower-reviewer`),不会先完成规划/派发再在
136
+ review/merge 阶段死锁。
137
+
114
138
  ## 配置
115
139
 
116
140
  ```yaml
@@ -124,6 +148,7 @@ tower_teardown repoRoot=D:\work\repo
124
148
  autoPauseGoal: true
125
149
  denyTools: [create_goal, todo_write, team_task_create, team_task_update, team_task_reopen, spawn_teammate]
126
150
  review:
151
+ requireReview: false # 默认审查可选;需要强制独立审查时改为 true
127
152
  maxRoundsPerReviewer: 5
128
153
  merge:
129
154
  timeoutMs: 900000
@@ -135,21 +160,21 @@ tower_teardown repoRoot=D:\work\repo
135
160
  maxDepth: 1
136
161
  ```
137
162
 
138
- - 代码中 `mode.allowAgentControl` 默认 `false`;随包 `cordis.patch.yml` 设为 `true`:模型可在用户意图/任务状态明确时自行进出,但必须给出理由;设为 `false` 时只有人类能切换。随包 patch 还默认开启 `mode.workspaceLock`(单活塔)、`send.interruptDefault`、`init.autoGitInit`,并把 `spawn.staleMs` 设为 15s。
163
+ - 代码中 `mode.allowAgentControl` 默认 `true`;随包 `cordis.patch.yml` 也显式设为 `true`:模型可在用户意图/任务状态明确时自行进出,但必须给出理由;设为 `false` 时只有人类能切换。随包 patch 还默认开启 `mode.workspaceLock`(单活塔)、`send.interruptDefault`、`init.autoGitInit`,并把 `spawn.staleMs` 设为 15s。
139
164
  - `/tower base <branch>` 可设置会话级 init base 偏好;`/tower on` 会清除活跃 plan mode(`mode.planModePolicy:"refuse"` 则拒绝进入),保证 plan/tower 互斥。
140
165
  - 仓库级配置:在 `<git-common-dir>/dsh-tower/config.json`(通常是 `.git/dsh-tower/config.json`)放入同样的 JSON 结构,可覆盖 `review`/`merge`/`worktrees`/`spawn`;优先级为代码默认 < profile < 仓库配置 < 工具参数。`mode.*` 属于会话级,只在 profile 生效。
141
166
  - 所有键与默认值见 [docs/reference/config.md](docs/reference/config.md);未知顶层键会被拒绝(防止拼写错误静默失效),嵌套分组内的未知键会被忽略。工具错误码见 [docs/reference/errors.md](docs/reference/errors.md)。
142
167
 
143
- ## 工具(26 个核心)
168
+ ## 工具(27 个核心)
144
169
 
145
- `tower_init` · `tower_plan` · `tower_mission` · `tower_status` · `tower_spawn` · `tower_roster` · `tower_adopt` · `tower_diff` · `tower_send` · `tower_inbox` · `tower_complete` · `tower_interrupt` · `tower_finding` · `tower_findings` · `tower_review` · `tower_preflight` · `tower_merge` · `tower_rebase` · `tower_teardown` · `tower_mode` · `tower_doctor` · `tower_test` · `tower_batch` · `tower_resource` · `tower_import_legacy` · `tower_legacy_map`
170
+ `tower_init` · `tower_plan` · `tower_mission` · `tower_status` · `tower_spawn` · `tower_roster` · `tower_adopt` · `tower_diff` · `tower_send` · `tower_inbox` · `tower_complete` · `tower_interrupt` · `tower_finding` · `tower_findings` · `tower_review` · `tower_preflight` · `tower_merge` · `tower_rebase` · `tower_teardown` · `tower_mode` · `tower_doctor` · `tower_recover` · `tower_test` · `tower_batch` · `tower_resource` · `tower_import_legacy` · `tower_legacy_map`
146
171
 
147
- `tower_control` 仅在 `allowAgentControl: true` 时注册;`tower_reload` 在 `dev.hotReload`(默认 true)时注册,无 HMR 服务调用会返回 `reload-unavailable`(因此最多 28 个工具)。mission 级工具同时接受 `id` 作为 `missionId` 的别名;`task_done`/`taskDone` 等 snake/camel 写法均可。
172
+ `tower_control` 默认注册(`allowAgentControl:true`);设为 `false` 可禁用;`tower_reload` 在 `dev.hotReload`(默认 true)时注册,无 HMR 服务调用会返回 `reload-unavailable`(因此最多 29 个工具)。mission 级工具同时接受 `id` 作为 `missionId` 的别名;`task_done`/`taskDone` 等 snake/camel 写法均可。
148
173
 
149
174
  ## 安全与约束
150
175
 
151
176
  - **门禁不全绿不合并。** `tower_preflight` 是只读检查,返回稳定的 blocker code;存在 blocker 时 `tower_merge` 拒绝合并。
152
- - **审查绑定提交。** 审查后分支 tip 变化会报 `tip-moved`,必须重新审查;reviewer 使用只读 preset,若在审查期间改动分支,提交审查会被拒绝。
177
+ - **审查绑定提交。** 开启 `review.requireReview` 时,审查后分支 tip 变化会报 `tip-moved`,必须重新审查;reviewer 使用只读 preset,若在审查期间改动分支,提交审查会被拒绝。
153
178
  - **scope 强制。** mission 只能修改其 scope glob 匹配的文件,越界会以 `out-of-scope` 阻止合并。
154
179
  - **worktree 归属。** 脏工作树、运行中的 worker、已关闭 mission 的分支都会阻止合并或 teardown。
155
180
  - **goal 隔离。** 进入 Tower 模式会暂停活跃 goal 并记录;退出不会自动恢复,需要你自行恢复。
@@ -173,12 +198,12 @@ worker 与 reviewer 是使用内置 `tower-worker` preset 的普通 DSH 子代
173
198
 
174
199
  | 路径 | 内容 |
175
200
  |---|---|
176
- | `package.json`、`cordis.patch.yml` | bundle 清单与 loader patch(插入 Host 插件和 `tower-worker` preset) |
201
+ | `package.json`、`cordis.patch.yml` | bundle 清单与 loader patch(单个 `tower-bundle` group,内含 Host、skills 与两个 preset) |
177
202
  | `index.js`、`lib/`、`worker.js` | Host 入口、协议实现、worker preset 策略 |
178
203
  | `locale/`、`icon.svg` | 本地化与图标 |
179
204
  | `tests/` | 纯函数、协议 e2e、插件级测试;可选的真实 Web 运行时脚本 |
180
205
  | `ui-bundle/` | 可选 Client 小挂件(Tower 工具卡片、Tower·on 状态 chip、模式感知的 disabled 行、dock 摘要、临时 overlay 状态条),默认不安装 |
181
- | `.github/workflows/test.yml` | CI:Node 22/24 上跑离线套件 |
206
+ | `.github/workflows/test.yml` | CI:Node 22/24 离线套件、release 与覆盖率检查、Node 22 挂载 smoke 与种子失败 fixtures |
182
207
 
183
208
  ## 开发与测试
184
209
 
@@ -190,10 +215,26 @@ npm test # node --test tests/*.test.mjs
190
215
 
191
216
  离线套件不需要 DSH 进程,只在系统临时目录创建一次性 Git 仓库。隔离 Web 运行时检查、模型驱动验收和会话诊断脚本见 `tests/README.md`。
192
217
 
218
+ ## CI 证明了什么
219
+
220
+ 每次 push 与 PR 都运行同一组 job;每个 job 对应一条可检查的声明,而不是一句 README 承诺。
221
+
222
+ | Job | 声明 | 证据 |
223
+ |---|---|---|
224
+ | `test`(Node 22/24) | 离线套件、文档一致性、生成文档、发布文件清单、架构、类型与 domain 纯度全部成立 | `npm test`、`npm run docs:check`、三个 `docs:check:generated*`、`pack:check`、`architecture:check`、`types:check`、`domain:check` |
225
+ | `test` 的 `Coverage (Node 22)` 步骤 | 覆盖率在 CI 中真实测量(只报告,不设阈值、不加徽章) | `npm run test:coverage` |
226
+ | `test` 的 `Release check`(Node 22)步骤 | tag 是一种声明:版本已越过上一个版本 tag、推送的 tag 是 `v<version>`、CHANGELOG 带有发布内容 | `npm run release:check` |
227
+ | `mount`(Node 22) | 打出的 tarball 经 plugin CLI 装进一次性 profile 后,真实 loader 挂载并注册了 bundle 声称的表面 | `npm run smoke:mount` |
228
+ | `mount-fixtures`(Node 22,非必需检查) | mount smoke 仍会因正确的种子失败而变红(改名一个工具 id、删除 skills 段),三次运行共用一次缓存的 host 安装 | `npm run smoke:fixtures -- --host-dir <缓存的 host>` |
229
+
230
+ `mount` 断言一次性 profile 的**已加载表面**,而不是 `cordis.patch.yml` 的文本:29 个 `tower_*` 工具(27 个 `tools/*.js` 模块 + `tower_mode` / `tower_control`)、3 个内置 skill、`tower-worker` / `tower-reviewer` 两个 preset、`/tower` 命令,以及唯一一个顶层 `tower-bundle` group 行(#66)。它不调用模型、不打开浏览器。在维护者的 Windows 机器上实测:host 冷装(含安装)约 100 秒,host 已缓存约 85 秒;CI 中该步骤上限为 3 分钟(`timeout-minutes: 3`),`mount` job 上限 5 分钟。红叉会打印解析后的 profile 路径、host build、已加载计数与每一条未满足的声明。
231
+
232
+ host build 只固定在一个地方:`scripts/mount-smoke/host.json`,`COMPATIBILITY.md` 最新一行由单元测试强制与它一致,发布时不再各写一遍。`npm run smoke:fixtures` 用两种种子失败(改名一个工具 id、删除 skills 段)证明 smoke 会以正确原因变红;CI 中它作为非必需的 `mount-fixtures` job 运行,control 与两个种子通过 `--host-dir` 共用一次缓存的 host 安装(本机热跑约 2 分钟)。
233
+
193
234
  ## 文档
194
235
 
195
236
  - [docs/README.md](docs/README.md) — 文档索引(新用户 / 运维 / 维护者三条阅读路径)
196
- - [中文文档](docs/zh/README.md) · [快速开始](docs/guide/quickstart.md) · [安装、升级与迁移](docs/guide/install-upgrade-migration.md)
237
+ - [中文文档](docs/zh/README.md) · [快速开始](docs/guide/quickstart.md) · [何时使用 Tower](docs/guide/scenarios-and-philosophy.md) · [安装、升级与迁移](docs/guide/install-upgrade-migration.md)
197
238
  - [工具参考](docs/reference/tools.md) · [配置参考](docs/reference/config.md) · [blocker 与失败码](docs/reference/blockers.md) · [错误码](docs/reference/errors.md) · [事件](docs/reference/events.md) · [测试准入](docs/reference/testing.md) · [批次/租约/旧版](docs/reference/operations.md) · [Doctor](docs/reference/doctor.md) · [热重载](docs/guide/hot-reload.md)
198
239
  - [架构与状态机](docs/architecture/overview.md) · [运维 runbook](docs/operations/runbook.md) · [安全模型](docs/operations/security.md)
199
240
  - [FAQ](docs/faq.md) · [示例](docs/examples/first-mission.md)
@@ -201,7 +242,7 @@ npm test # node --test tests/*.test.mjs
201
242
  ## 常见问题
202
243
 
203
244
  - **安装/更新后插件不出现。** Host 插件在 DSH 启动时加载,请重启 DSH;并确认安装目标指向本仓库根目录(含 `package.json` 与 `cordis.patch.yml` 的那一层),而不是子目录。
204
- - **没有 `tower_control`。** 配置 `allowAgentControl` 为 `false`;改为 `true` 并重启。
245
+ - **没有 `tower_control`。** 配置中被设为 `allowAgentControl:false`;改回 `true` 并重启。
205
246
  - **合并被阻止。** 运行 `tower_preflight` 查看 blocker code;`stale-base` 表示先执行 `tower_rebase`。
206
247
  - **基线前进后分支过期。** `tower_rebase` 在 worktree 内完成 rebase;干净且无冲突的 rebase 可免去上一轮审查的重审。
207
248
  - **合并提交已经存在,但 `tower_merge` 报了失败。** 重新执行 `tower_merge` 即可:它会检测已落地的合并提交(或分支已包含在 base 中)并记录为 reconciled,不会重复合并。
@@ -213,7 +254,7 @@ npm test # node --test tests/*.test.mjs
213
254
 
214
255
  ## 致谢
215
256
 
216
- - **Kimi Code(Moonshot AI)**:Tower 模式的整体概念——由人类开启的模式、每个 mission 独立 Git worktree、审查门禁合并协议——灵感来自 Kimi Code 的实验性 Tower 模式([官网](https://www.kimi.com/code)、[源码](https://github.com/MoonshotAI/kimi-code))。本项目在 DeepSeek Harness 的服务之上重新实现这些想法,不包含 Kimi Code 源码,也与 Moonshot AI 无隶属关系。
257
+ - **Kimi Code(Moonshot AI)**:Tower 模式的整体概念——明确的会话级模式开关、每个 mission 独立 Git worktree、审查门禁合并协议——灵感来自 Kimi Code 的实验性 Tower 模式([官网](https://www.kimi.com/code)、[源码](https://github.com/MoonshotAI/kimi-code))。本项目在 DeepSeek Harness 的服务之上重新实现这些想法,不包含 Kimi Code 源码,也与 Moonshot AI 无隶属关系。
217
258
  - **DeepSeek Harness**:本插件构建所依赖的插件、工具、会话与子代理 API。
218
259
 
219
260
  ## License
package/SECURITY.md CHANGED
@@ -11,10 +11,40 @@ Areas that matter for security reports:
11
11
  - Command forwarding: injecting content into the model history that the user did not send.
12
12
  - Writes outside the declared scope, the mission worktree, or the DSH home directory.
13
13
 
14
+ ## Repository protection on `main`
15
+
16
+ The repository's own landing path is part of the security surface, not separate from it: the merge
17
+ gate, state lock, persistence and scope code decide whether a user's repository is modified, so a bad
18
+ change there is exploitable for every repository that installs the plugin. `main` is therefore
19
+ protected with:
20
+
21
+ - a pull request required for every change, with **admins enforced** — a direct push is rejected even
22
+ for the maintainer;
23
+ - required checks `test (22)`, `test (24)` and `mount` — the packed bundle must mount and register its
24
+ surface before a change can merge, and a red or missing check blocks the merge;
25
+ - force-pushes and branch deletion blocked, so a released tag's history cannot be rewritten;
26
+ - no mandatory approvals and no CODEOWNERS gate. With one maintainer, self-merge is the workflow;
27
+ what replaces the second approval is the four-line PR body for the two-key paths in
28
+ [CONTRIBUTING.md](CONTRIBUTING.md#landing-changes).
29
+
30
+ Rationale, the enforcement boundary (server-side protection is not a plugin feature) and the list of
31
+ what the setting does **not** buy are in
32
+ [docs/operations/security.md](docs/operations/security.md#repository-protection). Verify the setting:
33
+
34
+ ```bash
35
+ gh api repos/louifinn/dsh-tower/branches/main/protection \
36
+ --jq '{checks: .required_status_checks.contexts, strict: .required_status_checks.strict,
37
+ admins: .enforce_admins.enabled, force_push: .allow_force_pushes.enabled,
38
+ deletions: .allow_deletions.enabled, reviews: .required_pull_request_reviews}'
39
+ ```
40
+
14
41
  ## Reporting
15
42
 
16
43
  Please do **not** publish exploit details in a public issue. Open a minimal issue titled `security: request private contact` describing only the impact category (for example “merge gate bypass”), and a maintainer will arrange a private channel. Include the affected version and, if possible, a minimal reproduction in the private follow-up.
17
44
 
18
45
  ## Supported versions
19
46
 
20
- Security fixes target the latest released version (`1.x`). Older pre-release snapshots are not maintained.
47
+ Security fixes target the latest released `2.x` line (currently `2.0.4`). Older pre-release snapshots
48
+ are not maintained. Private reports are acknowledged in the minimal public issue and handled through
49
+ the maintainer's private channel; do not attach exploit details, tokens, or production logs to a
50
+ public issue.
@@ -0,0 +1,10 @@
1
+ // Default composition root for Tower dependencies (#50).
2
+ import * as store from '../lib/store.js'
3
+ import * as vcs from '../lib/git.js'
4
+
5
+ export const defaultDeps = {
6
+ store,
7
+ vcs,
8
+ clock: { now: () => Date.now() },
9
+ ids: { uuid: () => crypto.randomUUID() },
10
+ }
@@ -0,0 +1,6 @@
1
+ // The only lib/ module that imports node:fs (#50).
2
+ import fs from 'node:fs'
3
+ import fsp from 'node:fs/promises'
4
+
5
+ const { createWriteStream } = fs
6
+ export { fs, fsp, createWriteStream }
@@ -0,0 +1,4 @@
1
+ // The only lib/ module that imports node:child_process (#50).
2
+ import { execFile, spawn } from 'node:child_process'
3
+
4
+ export { execFile, spawn }
package/app/batch.js ADDED
@@ -0,0 +1,79 @@
1
+ // Batch use cases.
2
+ import { TowerToolError } from '../domain/errors.js'
3
+ import { withLock, writeJsonAtomic, appendJsonl } from '../lib/store.js'
4
+ import { validateState } from '../domain/state.js'
5
+ import { validateCreate as validateBatchCreate, validateClose as validateBatchClose } from '../domain/batch.js'
6
+ import {
7
+ nextBatchId,
8
+ createBatchRecord,
9
+ closeBatchRecord,
10
+ abandonBatchCheck,
11
+ applyBatchAbandon,
12
+ batchStatusPayload,
13
+ } from '../domain/collab/batch.js'
14
+
15
+ function fail(message, code = 'batch-invalid') {
16
+ throw new TowerToolError(code, message)
17
+ }
18
+
19
+ export async function createBatch(tower, input = {}, actor = 'tower') {
20
+ return withLock(tower.paths, async () => {
21
+ const state = await tower.load()
22
+ const normalized = validateBatchCreate(state, input)
23
+ if (!Array.isArray(state.batches)) state.batches = []
24
+ const batch = createBatchRecord(nextBatchId(state.batches), normalized, actor, Date.now())
25
+ state.batches.push(batch)
26
+ state.revision += 1
27
+ state.updatedAt = batch.createdAt
28
+ validateState(state, tower.paths.state)
29
+ await writeJsonAtomic(tower.paths.state, state)
30
+ await appendJsonl(tower.paths.activity, {
31
+ at: batch.createdAt,
32
+ type: 'batch',
33
+ text: `created ${batch.id}: ${batch.missionIds.join(', ')}`,
34
+ })
35
+ return batch
36
+ })
37
+ }
38
+
39
+ export async function closeBatch(tower, batchId, options = {}, actor = 'tower') {
40
+ return withLock(tower.paths, async () => {
41
+ const state = await tower.load()
42
+ const batch = (state.batches ?? []).find((entry) => entry.id === batchId)
43
+ if (!batch) fail(`unknown batch "${batchId}"`)
44
+ const result = validateBatchClose(state, batch, { ...options, state })
45
+ const blockers = Array.isArray(result.blockers) ? result.blockers : []
46
+ if (blockers.length > 0) return { ok: false, batch, blockers, checks: result.checks ?? [] }
47
+ closeBatchRecord(batch, options, actor, Date.now())
48
+ state.revision += 1
49
+ state.updatedAt = batch.closedAt
50
+ validateState(state, tower.paths.state)
51
+ await writeJsonAtomic(tower.paths.state, state)
52
+ await appendJsonl(tower.paths.activity, { at: batch.closedAt, type: 'batch', text: `closed ${batch.id}` })
53
+ return { ok: true, batch, blockers: [], checks: result.checks ?? [] }
54
+ })
55
+ }
56
+
57
+ /** @param {string} batchId @param {string} [actor] @param {string | null} [reason] */
58
+ export async function abandonBatch(tower, batchId, actor = 'tower', reason = null) {
59
+ return withLock(tower.paths, async () => {
60
+ const state = await tower.load()
61
+ const batch = (state.batches ?? []).find((entry) => entry.id === batchId)
62
+ if (!batch) fail(`unknown batch "${batchId}"`)
63
+ const { alreadyAbandoned } = abandonBatchCheck(batch, batchId)
64
+ if (alreadyAbandoned) return { ok: true, batch, alreadyAbandoned: true }
65
+ const now = Date.now()
66
+ applyBatchAbandon({ batch, actor, reason, now })
67
+ state.revision += 1
68
+ state.updatedAt = batch.abandonedAt
69
+ validateState(state, tower.paths.state)
70
+ await writeJsonAtomic(tower.paths.state, state)
71
+ await appendJsonl(tower.paths.activity, { at: batch.abandonedAt, type: 'batch', text: `abandoned ${batch.id}` })
72
+ return { ok: true, batch, alreadyAbandoned: false }
73
+ })
74
+ }
75
+
76
+ export async function batchStatus(tower, batchId) {
77
+ const state = await tower.load()
78
+ return batchStatusPayload(state, batchId)
79
+ }
package/app/board.js ADDED
@@ -0,0 +1,25 @@
1
+ // Mission board (MISSIONS.md) writing use cases.
2
+ import path from 'node:path'
3
+ import { fsp } from '../adapters/store-fs.js'
4
+ import { renameWithRetry } from '../lib/store.js'
5
+
6
+ export function boardFile(tower) {
7
+ return tower.config.docs?.boardLocation === 'repo'
8
+ ? path.join(tower.root, 'MISSIONS.md')
9
+ : path.join(tower.paths.dir, 'MISSIONS.md')
10
+ }
11
+
12
+ export async function writeBoard(tower) {
13
+ if (tower.config.docs?.board === false) return null
14
+ try {
15
+ const state = await tower.load()
16
+ const file = boardFile(tower)
17
+ await tower.ensureExcluded(file)
18
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`
19
+ await fsp.writeFile(tmp, tower.boardMarkdown(state), 'utf8')
20
+ await renameWithRetry(tmp, file)
21
+ return file
22
+ } catch {
23
+ return null
24
+ }
25
+ }
@@ -0,0 +1,76 @@
1
+ // Worker completion preparation and commit use cases.
2
+ import { TowerToolError } from '../domain/errors.js'
3
+ import {
4
+ normalizeChecks,
5
+ assertCompletionRequest,
6
+ assertCompletionInbox,
7
+ completionReport,
8
+ } from '../domain/missions/completion.js'
9
+ import { fullMission } from '../domain/summary.js'
10
+ import { validateState } from '../domain/state.js'
11
+ import { countCommits, isClean } from '../lib/git.js'
12
+ import { withLock, writeJsonAtomic, appendJsonl } from '../lib/store.js'
13
+
14
+ export { normalizeChecks }
15
+
16
+ export async function checkCompletion(tower, input) {
17
+ const state = await tower.load()
18
+ const mission = state.missions.find((entry) => entry.id === input.missionId)
19
+ if (!mission) {
20
+ throw new TowerToolError('invalid-argument', `unknown mission "${input.missionId}"`)
21
+ }
22
+ const worker = assertCompletionRequest({ mission, roster: state.roster, input })
23
+ await tower.assertWorktree(mission.worktree)
24
+ const commits = await countCommits(mission.worktree.path, mission.worktree.baseCommit)
25
+ if (commits < 1) {
26
+ throw new TowerToolError(
27
+ 'invalid-argument',
28
+ `mission branch has no commits since ${String(mission.worktree.baseCommit).slice(0, 12)}`,
29
+ )
30
+ }
31
+ if (!(await isClean(mission.worktree.path))) {
32
+ const dirty = (await tower.git(mission.worktree.path, ['status', '--porcelain']))
33
+ .split('\n')
34
+ .filter(Boolean)
35
+ .map((line) => line.slice(3).trim())
36
+ throw new TowerToolError(
37
+ 'worktree-dirty',
38
+ `mission ${mission.id} worktree has uncommitted changes (${dirty.slice(0, 8).join(', ')}${dirty.length > 8 ? ', …' : ''}); commit, remove, or record them before tower_complete`,
39
+ )
40
+ }
41
+ const unread = tower.unreadFor(state, worker.name)
42
+ assertCompletionInbox({ mission, worker, unread })
43
+ return { state, mission, worker, commits }
44
+ }
45
+
46
+ export async function complete(tower, input) {
47
+ // Two-phase pilot (#51): expensive git/state checks run before the lock;
48
+ // the lock only revalidates the state revision and commits the transition.
49
+ const prepared = await tower.checkCompletion(input)
50
+ return withLock(tower.paths, async () => {
51
+ const state = await tower.load()
52
+ if (state.revision !== prepared.state.revision) {
53
+ throw new TowerToolError('stale-base', 'tower state changed while completion was being prepared; retry')
54
+ }
55
+ const mission = state.missions.find((entry) => entry.id === input.missionId)
56
+ if (!mission) throw new TowerToolError('mission-not-found', `unknown mission "${input.missionId}"`)
57
+ const { worker, commits } = prepared
58
+ const checks = normalizeChecks(input.checks)
59
+ const now = Date.now()
60
+ mission.status = 'completed'
61
+ mission.completionReport = completionReport({ report: input.report, worker, checks, now })
62
+ mission.updatedAt = now
63
+ state.revision += 1
64
+ state.updatedAt = now
65
+
66
+ validateState(state, tower.paths.state)
67
+ await writeJsonAtomic(tower.paths.state, state)
68
+ await appendJsonl(tower.paths.activity, {
69
+ at: now,
70
+ type: 'complete',
71
+ missionId: mission.id,
72
+ text: `${worker.name} reported completion (${commits} commit(s))`,
73
+ })
74
+ return { mission: fullMission(mission), commits, unread: 0 }
75
+ })
76
+ }
@@ -0,0 +1,26 @@
1
+ // Dashboard mission enrichment use case.
2
+ import { exists } from '../lib/store.js'
3
+ import { revParse } from '../lib/git.js'
4
+ import {
5
+ enrichedMissionView,
6
+ latestReviewForMission,
7
+ missionsToEnrich,
8
+ } from '../domain/read/enrichedMissions.js'
9
+
10
+ export async function enrichedMissions(tower, state, onlyIds) {
11
+ const out = []
12
+ for (const mission of missionsToEnrich(state.missions, onlyIds)) {
13
+ const latest = latestReviewForMission(state, mission)
14
+ let tipMoved = false
15
+ if (latest && mission.worktree && (await exists(mission.worktree.path))) {
16
+ try {
17
+ const tip = await revParse(mission.worktree.path, 'HEAD')
18
+ tipMoved = tip !== latest.reviewedCommit
19
+ } catch {
20
+ tipMoved = false
21
+ }
22
+ }
23
+ out.push(enrichedMissionView({ mission, latest, tipMoved }))
24
+ }
25
+ return out
26
+ }
@@ -0,0 +1,78 @@
1
+ // Findings use cases.
2
+ import { withLock, writeJsonAtomic, appendJsonl } from '../lib/store.js'
3
+ import { validateState } from '../domain/state.js'
4
+ import { TowerToolError } from '../domain/errors.js'
5
+ import {
6
+ normalizeFindingInput,
7
+ newFinding,
8
+ normalizeFindingActor,
9
+ wantsFindingUpdate,
10
+ readFindingsView,
11
+ applyFindingUpdate,
12
+ findingUpdateView,
13
+ } from '../domain/missions/findings.js'
14
+
15
+ function fail(message, code = 'invalid-argument') {
16
+ throw new TowerToolError(code, message)
17
+ }
18
+
19
+ export async function addFinding(tower, input) {
20
+ return withLock(tower.paths, async () => {
21
+ const state = await tower.load()
22
+ if (!Array.isArray(state.findings)) state.findings = []
23
+ const fields = normalizeFindingInput(input, state.missions)
24
+ const now = Date.now()
25
+ const seq = (state.findingSeq ?? 0) + 1
26
+ state.findingSeq = seq
27
+ const finding = newFinding(fields, { seq, now, reportedBy: input.reportedBy })
28
+ state.findings.push(finding)
29
+ state.revision += 1
30
+ state.updatedAt = now
31
+
32
+ validateState(state, tower.paths.state)
33
+ await writeJsonAtomic(tower.paths.state, state)
34
+ await appendJsonl(tower.paths.activity, {
35
+ at: now,
36
+ type: 'finding',
37
+ missionId: finding.missionId,
38
+ text: `${finding.id} ${finding.type}: ${finding.title}`,
39
+ })
40
+ return finding
41
+ })
42
+ }
43
+
44
+ /** List or read findings. Read-only paths never take the writer lock. */
45
+ export async function readFindings(tower, input = {}) {
46
+ const state = await tower.load()
47
+ return readFindingsView(state.findings ?? [], input)
48
+ }
49
+
50
+ /**
51
+ * List, read, or transition findings. Listing and reading are lock-free;
52
+ * only a transition takes the writer lock. Reviewers may only read.
53
+ */
54
+ export async function findings(tower, input = {}) {
55
+ const actor = normalizeFindingActor(input.actor)
56
+ if (!wantsFindingUpdate(input)) return tower.readFindings(input)
57
+
58
+ return withLock(tower.paths, async () => {
59
+ const state = await tower.load()
60
+ const all = state.findings ?? []
61
+ const finding = all.find((entry) => entry.id === input.id)
62
+ if (!finding) fail(`unknown finding "${input.id}"`, 'finding-not-found')
63
+ const now = Date.now()
64
+ const { noteAdded } = applyFindingUpdate({ finding, input, actor, now })
65
+ state.revision += 1
66
+ state.updatedAt = now
67
+ validateState(state, tower.paths.state)
68
+ await writeJsonAtomic(tower.paths.state, state)
69
+ await appendJsonl(tower.paths.activity, {
70
+ at: now,
71
+ type: 'finding',
72
+ missionId: finding.missionId,
73
+ text: `${finding.id} ${finding.status}${noteAdded ? ' +note' : ''} (${actor.name})`,
74
+ })
75
+ // Counts must reflect the transition just recorded.
76
+ return findingUpdateView(finding, all)
77
+ })
78
+ }