dsh-tower 2.5.0 → 2.6.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 (63) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/COMPATIBILITY.md +1 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/GLOSSARY.md +6 -0
  5. package/PRINCIPLES.md +15 -7
  6. package/README.en.md +16 -15
  7. package/README.md +12 -10
  8. package/app/review.js +7 -0
  9. package/app/spawn-prechecks.js +12 -2
  10. package/app/tower.js +5 -5
  11. package/client.js +321 -0
  12. package/cordis.patch.yml +5 -3
  13. package/docs/adr/0005-component-surface.md +58 -13
  14. package/docs/architecture/overview.md +12 -6
  15. package/docs/examples/first-mission.md +3 -2
  16. package/docs/faq.md +9 -7
  17. package/docs/guide/install-upgrade-migration.md +32 -7
  18. package/docs/guide/quickstart.md +20 -10
  19. package/docs/guide/scenarios-and-philosophy.md +35 -28
  20. package/docs/operations/security.md +11 -10
  21. package/docs/reference/blockers.md +12 -7
  22. package/docs/reference/component-surface.md +14 -7
  23. package/docs/reference/config.generated.md +3 -2
  24. package/docs/reference/config.md +29 -3
  25. package/docs/reference/errors.generated.md +1 -0
  26. package/docs/reference/errors.md +2 -1
  27. package/docs/reference/surface-budget.md +20 -3
  28. package/docs/reference/testing.md +1 -1
  29. package/docs/reference/tools.generated.md +5 -5
  30. package/docs/reference/tools.md +3 -3
  31. package/docs/zh/faq.md +7 -5
  32. package/docs/zh/guide/install-upgrade-migration.md +25 -6
  33. package/docs/zh/guide/quickstart.md +17 -10
  34. package/docs/zh/guide/scenarios-and-philosophy.md +23 -15
  35. package/docs/zh/operations/security.md +9 -8
  36. package/docs/zh/reference/blockers.md +11 -7
  37. package/docs/zh/reference/config.md +21 -3
  38. package/docs/zh/reference/errors.md +2 -1
  39. package/docs/zh/reference/testing.md +2 -2
  40. package/docs/zh/reference/tools.md +3 -3
  41. package/domain/components.js +12 -4
  42. package/domain/config/schema.js +40 -5
  43. package/domain/contracts/outputs.js +2 -0
  44. package/domain/contracts/tool-inputs/missions.js +1 -1
  45. package/domain/errors.js +6 -0
  46. package/domain/gate.js +6 -3
  47. package/domain/integration/review.js +203 -9
  48. package/host/config.js +40 -3
  49. package/host/doctor.js +18 -7
  50. package/host/presets.js +9 -8
  51. package/host/tool-registry.js +9 -1
  52. package/package.json +7 -1
  53. package/scripts/check-components.mjs +120 -6
  54. package/scripts/check-pack.mjs +3 -0
  55. package/scripts/component-surface.json +16 -10
  56. package/scripts/pack-manifest.txt +1 -0
  57. package/scripts/surface-baseline.json +8 -1
  58. package/scripts/types-coverage.mjs +7 -0
  59. package/tools/preflight.js +1 -1
  60. package/tools/review.js +1 -1
  61. package/tools/spawn.js +2 -2
  62. package/tools/status.js +8 -1
  63. package/types/index.d.ts +10 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@
4
4
 
5
5
  ## 未发布(Unreleased)
6
6
 
7
+ ## 2.6.0(2026-11-01)
8
+
9
+ ### 新增
10
+
11
+ - **#M39/#M40 审查策略成为单一三态控件**:`review.mode` 提供 `off` / `optional`(默认)/
12
+ `required`;插件页 `dsh-tower` 配置区只有一个三态单选组,不是组件行,也不是三个互斥行。
13
+ `off` 是受支持的模式:不注册 `tower_review`、拒绝 reviewer spawn、合并门禁忽略审查;
14
+ `optional` 记录审查但从不阻塞合并;`required` 恢复"干净且绑定分支 tip 的审查"合并门禁。
15
+ - **#M42 双语迁移说明**:安装、升级与迁移文档新增 2.5.0 → 2.6.0 一节,说明三种模式、控件位置、
16
+ 旧布尔别名的映射与单版本兼容窗口、从 `off` 启动需要重启的限制,以及 `required` 对
17
+ `preset-tower-reviewer` 行的依赖。
18
+
19
+ ### 变更
20
+
21
+ - **#M42 审查策略的组件表面重新分类(2026-11-01 决策)**:`review.mode` 是一个三态配置控件;
22
+ `review.requireReview` 从 `forbidden-to-disable` 改为 `core`(已弃用的单版本别名)。`required`
23
+ 模式下的合并门禁、审查证据与豁免路径仍不可被行/布尔关闭;`preset-tower-reviewer` 仍是可关闭的
24
+ 能力行。PRINCIPLES 原则 11 与 ADR 0005 增加 wave-5 说明;组件门禁新增"审查策略不是行、没有互斥行"
25
+ 的检查。数量不变:54 个配置键、11 个可见组件。
26
+ - **#M42 文档全面改用 `review.mode`**:README、quickstart、FAQ、场景、架构、安全、测试参考与
27
+ reviewer 行注释不再把旧布尔写成当前用法;历史迁移记录保留。
28
+ - `GLOSSARY.md` 增补 review policy / 审查策略 术语与"一个三态控件,不是三个行"的文案规则。
29
+
30
+ ### 修复
31
+
32
+ - **F25 reviewer 行注释过时**:`cordis.patch.yml` 不再声称"仅当旧布尔为 false 时才能关闭",
33
+ 改为按 `review.mode` 的三态语义描述(`off`/`optional` 可关闭,`required` 时拒绝该组合)。
34
+ - **F26/F28 文档不再把审查策略称为"永不可关闭"**:生成的组件参考与架构总览现在只把宿主、
35
+ 健康契约、恢复出口与合并门禁列为永不可关闭;`required` 模式下的审查门禁(证据、tip 绑定、
36
+ 豁免路径)仍不可关闭,而策略本身是一个三态控件。
37
+ - **F27 测试计数同步**:CONTRIBUTING 与 COMPATIBILITY 的 2.6.0 行更新为 729 个离线测试。
38
+
7
39
  ## 2.5.0(2026-11-01)
8
40
 
9
41
  ### 新增
package/COMPATIBILITY.md CHANGED
@@ -5,6 +5,7 @@ is shipped in the npm package; doctor reads it locally and never contacts a regi
5
5
 
6
6
  | dsh version | tag / commit | verified on | what was run |
7
7
  |---|---|---|---|
8
+ | 0.2.1-rc.2 | v2.6.0 | 2026-11-01 | mount smoke (28/28 default tools, 3 skills, 2 presets with distinct labels, 1 bundle row), `npm test` (729), `npm run test:property` (111), `npm run check` (docs + generated + component + surface + pack 294 + types + architecture + domain), `npm run smoke:mount` |
8
9
  | 0.2.1-rc.2 | v2.5.0 | 2026-11-01 | mount smoke (28/28 default tools, 3 skills, 2 presets with distinct labels, 1 bundle row), `npm test` (696), `npm run test:property` (111), `npm run check` (docs + generated + component + surface + pack 293 + types + architecture + domain), `npm run release:check -- --mode tag --tag v2.5.0` |
9
10
  | 0.2.1-rc.2 | v2.4.2 | 2026-10-24 | mount smoke (28/28 default tools, 3 skills, 2 presets with distinct labels, 1 bundle row), `npm test` (631), docs + seven generated checks, types coverage 148/148, components + philosophy + surface gates |
10
11
  | 0.2.1-rc.2 | v2.4.1 | 2026-10-24 | mount smoke (28/28 default tools, 3 skills, 2 presets, 1 bundle row), `npm test` (620), `types:coverage` 146/146, docs + seven generated checks incl capability-row locales, components + philosophy + surface gates |
package/CONTRIBUTING.md CHANGED
@@ -126,7 +126,7 @@ the push gates; CI schedules it nightly.
126
126
 
127
127
  ## Pull request checklist
128
128
 
129
- 1. `npm test` passes locally (696 tests today, including protocol end-to-end against a real temporary Git repository and mock spawn coverage), `npm run test:property` passes (111 generated property cases; also part of `npm run check`), and `npm run docs:check` passes.
129
+ 1. `npm test` passes locally (729 tests today, including protocol end-to-end against a real temporary Git repository and mock spawn coverage), `npm run test:property` passes (111 generated property cases; also part of `npm run check`), and `npm run docs:check` passes.
130
130
  2. New behavior has a test. Bug fixes should add a test that fails before the fix.
131
131
  3. No secrets, tokens, personal paths, session logs, or generated artifacts are added. `.gitignore` covers local acceptance output; do not force-add it.
132
132
  4. Keep the public API and state-file formats compatible; if a change is breaking, call it out in `CHANGELOG.md` and bump the version.
package/GLOSSARY.md CHANGED
@@ -28,6 +28,7 @@ off-state sentence.
28
28
  | preset | preset | 预设 | A shipped role definition. Never leave the English word in Chinese copy. |
29
29
  | worktree | worktree | 工作树 | The isolated checkout a worker runs in. |
30
30
  | review | review | 审查 | The review contract and the act of reviewing. Do not use 评审 in plugin-page copy. |
31
+ | review policy | review policy | 审查策略 | The one tri-state control on the plugin-page configuration form: off, optional (default) or required. It is not a component row and never three mutually exclusive rows. |
31
32
  | lease | lease | 租约 | Advisory resource coordination. Never 咨询式租约: the lease coordinates, it does not consult. |
32
33
  | worker | worker | 执行代理 | The role that executes a mission. "Tower Worker" is a product name and stays as-is. |
33
34
  | reviewer | reviewer | 审查代理 | The role that independently reviews a mission. "Tower Reviewer" is a product name and stays as-is. |
@@ -65,3 +66,8 @@ is migrated separately under the same glossary.
65
66
  working: a reviewer preset needed for merging is a precondition, not an
66
67
  optional extra, and test evidence that cannot be recorded keeps merges blocked
67
68
  until the requirement is cleared.
69
+ 7. Describe the review policy as one tri-state control (off / optional /
70
+ required), never as three rows and never as a capability that is permanently
71
+ on. In required mode it is a merge precondition; in off mode review is
72
+ intentionally unavailable. The deprecated boolean alias maps true → required,
73
+ false → optional, and unset → optional.
package/PRINCIPLES.md CHANGED
@@ -140,18 +140,26 @@ Enforced by: `docs/operations/incident.md`, `scripts/regression-audit.mjs`, and
140
140
  A component may exist only for a capability whose off state leaves the rest of the
141
141
  product intact. Every capability that qualifies gets a component, so the user can
142
142
  control it. Every capability that does not — the host, the health contract, the
143
- recovery escape, the review contract, the merge gates — is configuration or code,
144
- never a switch: not as a row, and not as a boolean that removes it.
143
+ recovery escape, the merge gates — is configuration or code, never a switch: not
144
+ as a row, and not as a boolean that removes it.
145
+
146
+ The review policy is the deliberate middle case: it is one tri-state configuration
147
+ control (`off`, `optional`, `required`) on the plugin page, not a component row and
148
+ never one row per mode. Its off mode is documented and supported, while the review
149
+ gate in required mode — the clean, tip-bound review evidence and its exemption
150
+ path — remains non-disableable. A row may not own the policy, and the reviewer row
151
+ is not mutually exclusive with it.
145
152
 
146
153
  Refused because of this: a single all-or-nothing switch offered as "simpler"; a
147
154
  disableable `tower_doctor`/`tower_recover`; a core feature exposed as a toggle; a
148
- component whose off state is worse than uninstalling the plugin.
155
+ component whose off state is worse than uninstalling the plugin; a review policy
156
+ exposed as a row or as three mutually exclusive rows.
149
157
 
150
158
  Enforced by: `scripts/check-components.mjs` (locale coverage, the
151
- forbidden-to-disable denylist and the generated component reference),
152
- `scripts/surface-baseline.mjs` (the component count is a baselined surface),
153
- `docs/adr/0005-component-surface.md`, and `tests/components.test.mjs`; gate:
154
- `npm run docs:check:components`.
159
+ forbidden-to-disable denylist, the review-policy control and the generated
160
+ component reference), `scripts/surface-baseline.mjs` (the component count is a
161
+ baselined surface), `docs/adr/0005-component-surface.md`, and
162
+ `tests/components.test.mjs`; gate: `npm run docs:check:components`.
155
163
 
156
164
  ## Non-goals
157
165
 
package/README.en.md CHANGED
@@ -16,7 +16,7 @@ The repository root **is** the DSH bundle: `package.json` declares `dsh.bundle.p
16
16
 
17
17
  - **Configurable Tower mode.** `/tower` toggles a session-level stance that narrows the tool surface and injects the Tower protocol. `/tower on|off <instruction>` also forwards your instruction to the model in the same step. By default the model may also enter/leave with `tower_control` and a concrete reason; set `mode.allowAgentControl:false` for human-only control.
18
18
  - **Missions with real isolation.** Every mission gets its own branch and Git worktree; workers never touch your checkout.
19
- - **Review-gated merges.** When `review.requireReview:true`, a reviewer bound to the current branch tip must submit a `clean` / `merge` verdict before merge; the merge gate re-checks scope, staleness, task completion, review history, and worktree ownership before a `--no-ff` merge. Independent review is optional by default (the gate is off); use `reviewPolicy:"none"` + reason plus `gates.allowReviewExemption:true` for a single mission when the global gate is on. Multi-route review is recorded per round: while a non-clean round keeps the mission `active`, a later reviewer may still submit its verdict for the same reviewed tip and it appends to that round. A completed `survey` dependency satisfies the dependency gate (a build dependency still must be merged).
19
+ - **Review-gated merges.** The review policy is one tri-state control on the plugin page: `off`, `optional` (the default) or `required` (`review.mode` in config). With `required`, a reviewer bound to the current branch tip must submit a `clean` / `merge` verdict before merge; the merge gate re-checks scope, staleness, task completion, review history, and worktree ownership before a `--no-ff` merge. Independent review is optional by default; use `reviewPolicy:"none"` + reason plus `gates.allowReviewExemption:true` for a single mission when the policy is required. Multi-route review is recorded per round: while a non-clean round keeps the mission `active`, a later reviewer may still submit its verdict for the same reviewed tip and it appends to that round. A completed `survey` dependency satisfies the dependency gate (a build dependency still must be merged). Upgrade note: the old boolean review toggle is a one-release alias; the mapping is in [install, upgrade and migration](docs/guide/install-upgrade-migration.md).
20
20
  - **Durable coordination.** Roster, inbox, findings, reviews, and mission state are persisted under `.git/dsh-tower/`, so a restart does not lose the plan.
21
21
  - **Bounded state growth.** Terminal missions whose worktree is gone move to `.git/dsh-tower/archive/` once `archive.afterDays`/`archive.maxMissions` says so, keeping their tasks, reviews, findings and completion report; mutations then rewrite only active work. `tower_status includeArchived` lists compact archived summaries, and `tower_doctor` reports archive size, count and policy.
22
22
  - **Safe by default.** While Tower mode is active, `create_goal`, `update_goal(resume)`, `todo_write`, the Team task board, and `spawn_teammate` are refused; a paused goal is never resumed automatically.
@@ -100,10 +100,10 @@ tower_init repoRoot=D:\work\repo # records the base (current bra
100
100
  tower_plan repoRoot=D:\work\repo missions=[{"title":"Feature A","scope":["src/a/**"],"tasks":["implement","commit"],"kind":"build"}]
101
101
  tower_spawn repoRoot=D:\work\repo missionId=M1 role=worker
102
102
  # the worker implements and commits in its worktree, calls tower_complete; its final report returns here
103
- # Optional: enable review.requireReview:true first if you need mandatory independent review
103
+ # Optional: set review.mode:required first if you need mandatory independent review
104
104
  tower_spawn repoRoot=D:\work\repo missionId=M1 role=reviewer
105
105
  # the reviewer inspects read-only and calls tower_review (clean+merge, or p1-Nitems/p2-Nitems)
106
- tower_preflight repoRoot=D:\work\repo missionId=M1 # review optional by default; with review.requireReview expect no-review before review and ok:true after
106
+ tower_preflight repoRoot=D:\work\repo missionId=M1 # review optional by default (review.mode:optional); with review.mode:required expect no-review before review and ok:true after
107
107
  tower_merge repoRoot=D:\work\repo missionId=M1
108
108
  tower_teardown repoRoot=D:\work\repo
109
109
 
@@ -138,18 +138,19 @@ planning tools, `tower_status`, and the remaining tools keep working.
138
138
 
139
139
  Mode entry and `tower_plan` validate the required presets before work starts. The
140
140
  worker preset is mandatory; the reviewer preset is required only when
141
- `review.requireReview` is true, so the reviewer row may be off while review is
142
- optional and is locked with an explanatory `preset-missing` error when the gate
143
- is on. `tower-tests` off with `gates.requireTests:true` is refused the same way.
144
- `tower_doctor` and `tower_recover` are not components and have no config key: they
145
- are registered for every configuration (#122).
141
+ `review.mode` is `required`, so the reviewer row may be off while the policy is
142
+ `off` or `optional` and is locked with an explanatory `preset-missing` error when
143
+ the policy is `required`. `tower-tests` off with `gates.requireTests:true` is
144
+ refused the same way. `tower_doctor` and `tower_recover` are not components and
145
+ have no config key: they are registered for every configuration (#122).
146
146
 
147
147
  [Principle 11](PRINCIPLES.md) and
148
148
  [ADR 0005](docs/adr/0005-component-surface.md) say a component may exist only for
149
- a capability whose off state leaves the rest intact; the host, health, recovery,
150
- review and merge-gate capabilities are never switches. The per-key answer is the
151
- generated [component surface](docs/reference/component-surface.md), enforced by
152
- `npm run docs:check:components`.
149
+ a capability whose off state leaves the rest intact; the host, health, recovery
150
+ and merge-gate capabilities are never switches. The review policy is the one
151
+ tri-state configuration control (off/optional/required), not a row. The per-key
152
+ answer is the generated [component surface](docs/reference/component-surface.md),
153
+ enforced by `npm run docs:check:components`.
153
154
 
154
155
  ## Configuration
155
156
 
@@ -164,7 +165,7 @@ generated [component surface](docs/reference/component-surface.md), enforced by
164
165
  autoPauseGoal: true
165
166
  denyTools: [create_goal, todo_write, team_task_create, team_task_update, team_task_reopen, spawn_teammate]
166
167
  review:
167
- requireReview: false # review optional by default; set true to enforce independent review
168
+ mode: optional # one tri-state control: off, optional (default) or required
168
169
  maxRoundsPerReviewer: 5
169
170
  merge:
170
171
  timeoutMs: 900000
@@ -179,7 +180,7 @@ generated [component surface](docs/reference/component-surface.md), enforced by
179
180
  - `mode.allowAgentControl` defaults to `true` in code; the bundled `cordis.patch.yml` also sets it explicitly to `true` so the model may enter/leave when the user intent or task state warrants it, always stating a reason. Set it to `false` for human-only control. The bundled patch also enables `mode.workspaceLock` (single-active tower), `send.interruptDefault`, `init.autoGitInit`, and sets `spawn.staleMs` to 15s.
180
181
  - `/tower base <branch>` records a session-level init base preference; `/tower on` clears an active plan mode (`mode.planModePolicy:"refuse"` instead refuses entry), keeping plan and Tower mutually exclusive.
181
182
  - Repository-level config: put the same JSON shape in `<git-common-dir>/dsh-tower/config.json` (usually `.git/dsh-tower/config.json`) to override `review`/`merge`/`worktrees`/`spawn`. Precedence is code defaults < profile config < repository config < tool arguments. `mode.*` is session-scoped and only the profile layer controls it.
182
- - Every key and default is in [docs/reference/config.md](docs/reference/config.md); unknown keys are rejected (top-level and inside a group) so typos cannot silently disable security-relevant settings. Tool error codes are in [docs/reference/errors.md](docs/reference/errors.md).
183
+ - Every key and default is in [docs/reference/config.md](docs/reference/config.md); unknown keys are rejected (top-level and inside a group) so typos cannot silently disable security-relevant settings. The three review modes, where the plugin-page control lives, and the one-release alias migration are in [install, upgrade and migration](docs/guide/install-upgrade-migration.md). Tool error codes are in [docs/reference/errors.md](docs/reference/errors.md).
183
184
 
184
185
  ## Tools (29: 22 core + 7 optional)
185
186
 
@@ -190,7 +191,7 @@ The list above is the 22 core tools plus 5 of the component-owned optional tools
190
191
  ## Safety and guardrails
191
192
 
192
193
  - **No merge without a green gate.** `tower_preflight` is read-only and reports stable blocker codes; `tower_merge` refuses when any blocker remains.
193
- - **Review is bound to a commit when enabled.** With `review.requireReview:true`, a moved tip reports `tip-moved` and requires a fresh review; reviewers use a read-only preset, and a branch change during review is rejected.
194
+ - **Review is bound to a commit when required.** With `review.mode:required`, a moved tip reports `tip-moved` and requires a fresh review; reviewers use a read-only preset, and a branch change during review is rejected. `off` is a documented mode that removes `tower_review`; `optional` records reviews but never blocks a merge.
194
195
  - **Scope enforcement.** A mission may only change files matched by its declared scope globs; `out-of-scope` blocks the merge.
195
196
  - **Worktree ownership.** Dirty worktrees, a running worker, or a branch owned by a closed mission block teardown/merge.
196
197
  - **Goal isolation.** Entering Tower mode pauses the active goal and records it; leaving does not resume it. Use `/tower off` and resume the goal yourself.
package/README.md CHANGED
@@ -16,7 +16,7 @@ Tower 模式与 mission/审查工作流的灵感来自 Moonshot AI 的 **Kimi Co
16
16
 
17
17
  - **可配置的 Tower 模式。** `/tower` 切换会话级模式:收窄工具面并注入 Tower 协议;`/tower on|off <指令>` 还能把指令作为下一步用户消息转发给模型。默认也允许 Agent 通过 `tower_control` 带理由进出;设 `mode.allowAgentControl:false` 可改为仅人类控制。
18
18
  - **任务级隔离。** 每个 mission 使用独立分支和 Git worktree,worker 不会碰到你的主检出。
19
- - **审查门禁合并。** 开启 `review.requireReview:true` 时,审查者针对当前分支 tip 给出 `clean` / `merge` 结论;合并门禁重新校验 scope、分支新鲜度、任务完成度、审查记录与 worktree 归属,通过后才执行 `--no-ff` 合并。独立审查默认可选(门禁关闭);全局开启时,单个 mission 用 `reviewPolicy:"none"` + 理由并开启 `gates.allowReviewExemption:true` 做更窄豁免。
19
+ - **审查门禁合并。** 审查策略是插件页上的一个三态控件:`off`、`optional`(默认)或 `required`(配置键 `review.mode`)。设为 `required` 时,审查者针对当前分支 tip 给出 `clean` / `merge` 结论;合并门禁重新校验 scope、分支新鲜度、任务完成度、审查记录与 worktree 归属,通过后才执行 `--no-ff` 合并。独立审查默认可选;策略为 `required` 时,单个 mission 可用 `reviewPolicy:"none"` + 理由并开启 `gates.allowReviewExemption:true` 做更窄豁免。升级提示:旧的布尔审查开关是保留一个版本的别名;映射关系见[安装、升级与迁移](docs/zh/guide/install-upgrade-migration.md)。
20
20
  - **可恢复的协作状态。** roster、inbox、findings、reviews 与 mission 状态持久化在 `.git/dsh-tower/` 下,重启不会丢计划。
21
21
  - **受控的状态增长。** worktree 已移除的终态任务在达到 `archive.afterDays`/`archive.maxMissions` 后移入 `.git/dsh-tower/archive/`,保留 tasks、reviews、findings 与完成报告;此后 mutation 只重写活跃工作。`tower_status includeArchived` 列出压缩后的归档摘要,`tower_doctor` 报告归档大小、数量与策略。
22
22
  - **默认安全。** Tower 模式开启期间拒绝 `create_goal`、`update_goal(resume)`、`todo_write`、Team 任务板与 `spawn_teammate`;被暂停的 goal 不会被自动恢复。
@@ -99,10 +99,10 @@ tower_init repoRoot=D:\work\repo # 首次初始化,记录 base
99
99
  tower_plan repoRoot=D:\work\repo missions=[{"title":"Feature A","scope":["src/a/**"],"tasks":["implement","commit"],"kind":"build"}]
100
100
  tower_spawn repoRoot=D:\work\repo missionId=M1 role=worker
101
101
  # worker 在 worktree 内实现并提交,调用 tower_complete;最终报告会返回本会话
102
- # 可选:需要强制独立审查时,先开启 review.requireReview:true
102
+ # 可选:需要强制独立审查时,先设 review.mode:required
103
103
  tower_spawn repoRoot=D:\work\repo missionId=M1 role=reviewer
104
104
  # reviewer 只读审查并调用 tower_review(clean+merge 或 p1-Nitems/p2-Nitems)
105
- tower_preflight repoRoot=D:\work\repo missionId=M1 # 默认审查可选;开启 review.requireReview 时审查前预期 no-review,审查后 ok:true
105
+ tower_preflight repoRoot=D:\work\repo missionId=M1 # 默认审查可选(review.mode:optional);设为 review.mode:required 时审查前预期 no-review,审查后 ok:true
106
106
  tower_merge repoRoot=D:\work\repo missionId=M1
107
107
  tower_teardown repoRoot=D:\work\repo
108
108
 
@@ -140,13 +140,15 @@ tower_teardown repoRoot=D:\work\repo
140
140
  与其余工具继续工作。
141
141
 
142
142
  进入 Tower 模式与 `tower_plan` 前会校验必需的 preset。worker preset 是强制依赖;reviewer
143
- preset 只在 `review.requireReview` 为 true 时必需,因此审查可选时允许关闭 reviewer 行,强制审查
144
- 开启时会以带解释的 `preset-missing` 锁定。`tower-tests` 关闭且 `gates.requireTests:true` 时同样
145
- 拒绝。`tower_doctor` 与 `tower_recover` 不是组件、也没有配置键:任何配置都会注册它们(#122)。
143
+ preset 只在 `review.mode` 为 `required` 时必需,因此策略为 `off` 或 `optional` 时允许关闭
144
+ reviewer 行,策略为 `required` 时会以带解释的 `preset-missing` 锁定。`tower-tests` 关闭且
145
+ `gates.requireTests:true` 时同样拒绝。`tower_doctor` 与 `tower_recover` 不是组件、也没有配置键:
146
+ 任何配置都会注册它们(#122)。
146
147
 
147
148
  原则 11([PRINCIPLES.md](PRINCIPLES.md))与
148
149
  [ADR 0005](docs/adr/0005-component-surface.md) 要求:只有关闭后仍不破坏其余能力的 capability
149
- 才能成为组件;host、健康检查、恢复通道、审查契约与合并门禁永远不是开关。逐键答案见生成的
150
+ 才能成为组件;host、健康检查、恢复通道与合并门禁永远不是开关。审查策略是唯一的
151
+ 三态配置控件(off/optional/required),不是组件行。逐键答案见生成的
150
152
  [组件表面](docs/reference/component-surface.md),由 `npm run docs:check:components` 强制。
151
153
 
152
154
  ## 配置
@@ -162,7 +164,7 @@ preset 只在 `review.requireReview` 为 true 时必需,因此审查可选时
162
164
  autoPauseGoal: true
163
165
  denyTools: [create_goal, todo_write, team_task_create, team_task_update, team_task_reopen, spawn_teammate]
164
166
  review:
165
- requireReview: false # 默认审查可选;需要强制独立审查时改为 true
167
+ mode: optional # 一个三态控件:off、optional(默认)或 required
166
168
  maxRoundsPerReviewer: 5
167
169
  merge:
168
170
  timeoutMs: 900000
@@ -177,7 +179,7 @@ preset 只在 `review.requireReview` 为 true 时必需,因此审查可选时
177
179
  - 代码中 `mode.allowAgentControl` 默认 `true`;随包 `cordis.patch.yml` 也显式设为 `true`:模型可在用户意图/任务状态明确时自行进出,但必须给出理由;设为 `false` 时只有人类能切换。随包 patch 还默认开启 `mode.workspaceLock`(单活塔)、`send.interruptDefault`、`init.autoGitInit`,并把 `spawn.staleMs` 设为 15s。
178
180
  - `/tower base <branch>` 可设置会话级 init base 偏好;`/tower on` 会清除活跃 plan mode(`mode.planModePolicy:"refuse"` 则拒绝进入),保证 plan/tower 互斥。
179
181
  - 仓库级配置:在 `<git-common-dir>/dsh-tower/config.json`(通常是 `.git/dsh-tower/config.json`)放入同样的 JSON 结构,可覆盖 `review`/`merge`/`worktrees`/`spawn`;优先级为代码默认 < profile < 仓库配置 < 工具参数。`mode.*` 属于会话级,只在 profile 生效。
180
- - 所有键与默认值见 [docs/reference/config.md](docs/reference/config.md);未知键(顶层与分组内)都会被拒绝,防止拼写错误静默失效。工具错误码见 [docs/reference/errors.md](docs/reference/errors.md)。
182
+ - 所有键与默认值见 [docs/reference/config.md](docs/reference/config.md);未知键(顶层与分组内)都会被拒绝,防止拼写错误静默失效。三种审查模式、插件页控件位置与单版本别名迁移见[安装、升级与迁移](docs/zh/guide/install-upgrade-migration.md)。工具错误码见 [docs/reference/errors.md](docs/reference/errors.md)。
181
183
 
182
184
  ## 工具(29 个:22 个核心 + 7 个可选)
183
185
 
@@ -188,7 +190,7 @@ preset 只在 `review.requireReview` 为 true 时必需,因此审查可选时
188
190
  ## 安全与约束
189
191
 
190
192
  - **门禁不全绿不合并。** `tower_preflight` 是只读检查,返回稳定的 blocker code;存在 blocker 时 `tower_merge` 拒绝合并。
191
- - **审查绑定提交。** 开启 `review.requireReview` 时,审查后分支 tip 变化会报 `tip-moved`,必须重新审查;reviewer 使用只读 preset,若在审查期间改动分支,提交审查会被拒绝。非 clean 的一轮审查会让 mission 回到 active,此后其他 route 仍可为同一 reviewed tip 提交结论并追加到同一轮;依赖为 `completed` 的 survey 视为已满足(build 依赖仍需 merged)。
193
+ - **审查绑定提交。** 策略为 `review.mode:required` 时,审查后分支 tip 变化会报 `tip-moved`,必须重新审查;reviewer 使用只读 preset,若在审查期间改动分支,提交审查会被拒绝。`off` 是受支持的模式,会移除 `tower_review`;`optional` 记录审查但从不阻塞合并。非 clean 的一轮审查会让 mission 回到 active,此后其他 route 仍可为同一 reviewed tip 提交结论并追加到同一轮;依赖为 `completed` 的 survey 视为已满足(build 依赖仍需 merged)。
192
194
  - **scope 强制。** mission 只能修改其 scope glob 匹配的文件,越界会以 `out-of-scope` 阻止合并。
193
195
  - **worktree 归属。** 脏工作树、运行中的 worker、已关闭 mission 的分支都会阻止合并或 teardown。
194
196
  - **goal 隔离。** 进入 Tower 模式会暂停活跃 goal 并记录;退出不会自动恢复,需要你自行恢复。
package/app/review.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { sweepArchive } from './archive.js'
3
3
  import { validateState } from '../domain/state.js'
4
4
  import { TowerToolError } from '../domain/errors.js'
5
+ import { reviewUnavailable } from '../domain/integration/review.js'
5
6
  import {
6
7
  assertReviewableMission,
7
8
  resolveReviewActor,
@@ -78,6 +79,12 @@ async function prepareReview(tower, input) {
78
79
  }
79
80
 
80
81
  export async function submitReview(tower, input) {
82
+ // #M39: `review.mode:off` means review is intentionally unavailable. The tool
83
+ // is not registered in that profile, but the repository layer can also turn it
84
+ // off, so the app layer refuses before any state is read.
85
+ if (reviewUnavailable(tower.config)) {
86
+ throw new TowerToolError('review-disabled', 'review.mode is off, so tower_review cannot record a verdict')
87
+ }
81
88
  // Two-phase (#51): the lock is held only for the state reload, revision
82
89
  // revalidation, and write; every git call happens in prepareReview.
83
90
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -2,6 +2,7 @@
2
2
  // service/provider/lease validation. Split out of tools/spawn.js; no I/O.
3
3
  import { TowerToolError } from '../domain/errors.js'
4
4
  import { ROSTER_INACTIVE_STATES } from '../domain/roster.js'
5
+ import { reviewUnavailable } from '../domain/integration/review.js'
5
6
 
6
7
  /**
7
8
  * Pick a unique roster name before any activation.
@@ -27,10 +28,19 @@ export function resolveRosterName({ role, missionId, requested, roster }) {
27
28
 
28
29
  /**
29
30
  * Role/mission preconditions checked before any provisioning.
30
- * @param {{ role: string, context: any }} input
31
+ * `config` is the repository-resolved Tower config (#M39): with
32
+ * `review.mode:off` there is no review to dispatch, so the reviewer role is
33
+ * refused before a worktree, preset or activation is touched.
34
+ * @param {{ role: string, context: any, config?: any }} input
31
35
  */
32
- export function assertSpawnableMission({ role, context }) {
36
+ export function assertSpawnableMission({ role, context, config }) {
33
37
  if (role === 'reviewer') {
38
+ if (reviewUnavailable(config)) {
39
+ throw new TowerToolError(
40
+ 'review-disabled',
41
+ 'review.mode is off, so review is unavailable and a reviewer cannot be spawned',
42
+ )
43
+ }
34
44
  if (!context.mission.worktree) {
35
45
  throw new TowerToolError('reviewer-prerequisite', 'reviewer needs an existing mission worktree; spawn a worker first')
36
46
  }
package/app/tower.js CHANGED
@@ -8,6 +8,7 @@ import { assertPorts } from '../ports/index.js'
8
8
  import { validateCreate as validateBatchCreate, validateClose as validateBatchClose, summarize as summarizeBatch } from '../domain/batch.js'
9
9
 
10
10
  import { rebaseWaived } from '../domain/gate.js'
11
+ import { isReviewExempt } from '../domain/integration/review.js'
11
12
  import { DEFAULT_MERGE_TIMEOUT_MS, gitLockTimeoutMs } from '../domain/lockBudget.js'
12
13
  export { ROSTER_INACTIVE_STATES } from '../domain/roster.js'
13
14
 
@@ -508,11 +509,10 @@ export class Tower {
508
509
  if (!mission.worktree) return null
509
510
  const branch = mission.worktree.branch
510
511
  const reviews = (state.reviews ?? []).filter((review) => review.branch === branch)
511
- const exempt =
512
- this.config.review?.requireReview === false ||
513
- (mission.reviewPolicy === 'none' &&
514
- this.config.gates?.allowReviewExemption === true &&
515
- String(mission.reviewExemptionReason ?? '').trim().length > 0)
512
+ // #M39: reconciliation follows the same policy as the gate — only
513
+ // `review.mode:required` demands review evidence; `off`/`optional` reconcile
514
+ // a landed branch without one.
515
+ const exempt = isReviewExempt(mission, this.config)
516
516
  if (reviews.length === 0 && !exempt) return null
517
517
  const latest = reviews.length > 0 ? reviews.reduce((a, b) => (a.round >= b.round ? a : b)) : null
518
518
  if (latest && !exempt && (latest.status !== 'clean' || latest.mergeVerdict !== 'merge')) return null