dsh-tower 2.3.0 → 2.4.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 (83) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/COMPATIBILITY.md +2 -0
  3. package/CONTRIBUTING.md +20 -3
  4. package/PRINCIPLES.md +18 -0
  5. package/README.en.md +29 -16
  6. package/README.md +22 -15
  7. package/cordis.patch.yml +56 -28
  8. package/docs/README.md +2 -1
  9. package/docs/adr/0004-surface-budget.md +20 -0
  10. package/docs/adr/0005-component-surface.md +130 -0
  11. package/docs/adr/README.md +1 -0
  12. package/docs/agents/workflow.md +1 -1
  13. package/docs/architecture/overview.md +69 -7
  14. package/docs/guide/hot-reload.md +4 -4
  15. package/docs/guide/install-upgrade-migration.md +13 -0
  16. package/docs/operations/security.md +1 -1
  17. package/docs/reference/component-surface.md +135 -0
  18. package/docs/reference/config.generated.md +2 -10
  19. package/docs/reference/config.md +11 -9
  20. package/docs/reference/doctor.md +14 -9
  21. package/docs/reference/surface-budget.md +52 -23
  22. package/docs/reference/tools.generated.md +4 -2
  23. package/docs/reference/tools.md +6 -3
  24. package/docs/zh/README.md +1 -1
  25. package/docs/zh/guide/hot-reload.md +3 -3
  26. package/docs/zh/guide/install-upgrade-migration.md +10 -0
  27. package/docs/zh/operations/security.md +1 -1
  28. package/docs/zh/reference/config.md +9 -9
  29. package/docs/zh/reference/tools.md +6 -4
  30. package/domain/components.js +641 -0
  31. package/domain/config/schema.js +37 -27
  32. package/domain/contracts/outputs.js +10 -0
  33. package/domain/errors.js +11 -1
  34. package/domain/mission.js +12 -1
  35. package/domain/surface.js +154 -55
  36. package/host/capability-batch.js +15 -0
  37. package/host/capability-hmr.js +30 -0
  38. package/host/capability-import.js +11 -0
  39. package/host/capability-leases.js +11 -0
  40. package/host/capability-registry.js +149 -0
  41. package/host/capability-tests.js +16 -0
  42. package/host/doctor.js +41 -9
  43. package/host/hmr.js +2 -4
  44. package/host/presets.js +51 -9
  45. package/host/tool-registry.js +150 -34
  46. package/index.js +32 -20
  47. package/locale/batch/en.json +6 -0
  48. package/locale/batch/zh.json +6 -0
  49. package/locale/en.json +31 -11
  50. package/locale/hmr/en.json +6 -0
  51. package/locale/hmr/zh.json +6 -0
  52. package/locale/import/en.json +6 -0
  53. package/locale/import/zh.json +6 -0
  54. package/locale/leases/en.json +6 -0
  55. package/locale/leases/zh.json +6 -0
  56. package/locale/skills/en.json +6 -0
  57. package/locale/skills/zh.json +6 -0
  58. package/locale/tests/en.json +6 -0
  59. package/locale/tests/zh.json +6 -0
  60. package/locale/zh.json +30 -10
  61. package/package.json +25 -4
  62. package/scripts/check-components.mjs +235 -0
  63. package/scripts/check-pack.mjs +4 -0
  64. package/scripts/component-surface.json +395 -0
  65. package/scripts/generate-capability-locale.mjs +148 -0
  66. package/scripts/generate-component-surface.mjs +59 -0
  67. package/scripts/generate-tools-reference.mjs +7 -25
  68. package/scripts/generate-types.mjs +127 -0
  69. package/scripts/lib/tool-surface.mjs +85 -0
  70. package/scripts/mount-smoke/fixtures.mjs +3 -3
  71. package/scripts/mount-smoke/surface.mjs +96 -27
  72. package/scripts/mount-smoke.mjs +3 -1
  73. package/scripts/pack-manifest.txt +30 -0
  74. package/scripts/surface-baseline.json +35 -4
  75. package/scripts/surface-baseline.mjs +32 -34
  76. package/scripts/types-coverage.mjs +184 -0
  77. package/scripts/types-strict-manifest.txt +10 -0
  78. package/scripts/types-strict.mjs +141 -0
  79. package/skills.js +8 -1
  80. package/tools/index.js +2 -1
  81. package/tools/reload.js +34 -35
  82. package/tools/status.js +8 -1
  83. package/types/index.d.ts +28 -12
package/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@
4
4
 
5
5
  ## 未发布(Unreleased)
6
6
 
7
+ ## 2.4.1(2026-10-24)
8
+
9
+ ### 修复
10
+
11
+ - **可选组件行在 Plugins 页面只显示模块名**:每个可选能力导出现在提供
12
+ `<specifier>/locale/{en,zh}.json`(`meta.title` / `meta.description`),由
13
+ `locale/<lang>.json` 的 `components` 块经 `npm run docs:generate:locale` 生成,并由
14
+ `docs:check:generated:locale` + CI 步骤防漂移;描述同时说明能力用途与关闭后移除的内容,
15
+ 开关无需离开页面即可判断。
16
+
17
+ ## 2.4.0(2026-10-24)
18
+
19
+ ### 新增
20
+
21
+ - **#121 可选能力回到组件**:一个 `tower-bundle` 组拆成强制 `tower` 宿主 + 可独立开关的顶层行
22
+ (`tower-skills`/`tower-batch`/`tower-import`/`tower-leases`/`tower-tests`/`tower-hmr` 默认关/
23
+ `preset-tower-reviewer` 依 `review.requireReview` 条件存在);新增 `./batch`/`./import`/`./leases`/
24
+ `./tests`/`./hmr` 导出,每个入口只注册自己的能力;8 个可见组件的 title/summary/disables 双语覆盖;
25
+ 任一可选行关闭后 Tower 仍可用(模式、规划、状态、其余工具),缺失能力在 status/doctor 中以
26
+ “disabled by component <id>” 呈现。
27
+ - **#120 组件面政策载体**:`PRINCIPLES.md` 第 11 条(“optional capabilities are switches; the core is not”)、
28
+ ADR 0005、64 键的组件轴分类(core / optional-component / optional-config / forbidden-to-disable,由
29
+ `scripts/component-surface.json` 单一来源生成)、`check-components`(locale 覆盖、forbidden-to-disable
30
+ denylist、组件计数 baseline)与 `components` 基线;组件面 4→1→8 的变化以带日期的 decision 记录。
31
+ - **#115 类型面完整性**:`types:coverage` 从 `package.json files` 与 `tsc --listFiles` 派生检查程序
32
+ (146/146 运行时 .js 全覆盖,旧配置下可复现 19 个漏检);tsconfig 改为 glob;`types:generate` 从运行时常量
33
+ 生成发布词汇(ErrorCode/状态/角色等)并纳入 `--check`,修掉 8 个幽灵码与 4 个缺失码;
34
+ `tsconfig.strict.json`(noImplicitAny)以显式文件清单起步,manifest 禁止清单收缩。
35
+ - **#118 surface 计数覆盖完整工具面**:`tower_doctor`/生成参考/`docs:check`/`surface:check` 统一为
36
+ 29 个工具面(默认装配 28,tower-hmr 关闭时少 `tower_reload`),直接注册的 `tower_mode`/`tower_control`
37
+ 也进入刹车;baseline 以 correction 记录 27→29。
38
+
39
+ ### 变更
40
+
41
+ - **#122 安全网永不可关**:`tower_doctor`/`tower_recover` 在任何配置下都注册;删除
42
+ `tools.exposeDoctor`/`exposeRecovery`/`exposeOperational` 与整个 `tools` 组、`dev.hotReload`;
43
+ 旧配置作为未知键被拒绝并停止挂载(升级指南 en/zh 已记录迁移)。
44
+ - 配置面从 64 键/15 节收缩为 56 键/14 节;生成的 config/tools/component-surface 参考同步更新。
45
+
46
+ ### 修复
47
+
48
+ - **F11/F9**:`operationalRegistry` 解析容忍 trailing comma 与 `OPERATIONAL_TOOLS.set` 路由;
49
+ 根 README 的工具计数标题修正为 29(20 core + 9 optional)并注明默认装配 28。
50
+ - 真实 `smoke:mount` 在 pinned host `0.2.1-rc.2` 上通过(默认 28/28),required `mount` CI 上下文保持绿。
7
51
  ## 2.3.0(2026-10-23)
8
52
 
9
53
  ### 新增
package/COMPATIBILITY.md CHANGED
@@ -5,6 +5,8 @@ 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.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 |
9
+ | 0.2.1-rc.2 | v2.4.0 | 2026-10-24 | mount smoke (28/28 default tools, 3 skills, 2 presets, 1 bundle row), `npm test` (612), `types:coverage` 146/146, strict ratchet 10, docs + six generated checks, components + philosophy + surface gates |
8
10
  | 0.2.1-rc.2 | v2.3.0 | 2026-10-23 | mount smoke (29 tower tools, `/tower`, 3 skills, 2 presets, single bundle row), `npm test` (573), `npm run docs:check`, four generated docs checks, philosophy + surface gates, live `--check` |
9
11
  | 0.2.1-rc.2 | v2.2.0 | 2026-10-23 | mount smoke (29 tower tools, `/tower`, 3 skills, 2 presets, single bundle row), `npm test` (536), `npm run docs:check`, four generated docs checks |
10
12
  | 0.2.1-rc.2 | v2.1.2 | 2026-10-23 | mount smoke (29 tower tools, `/tower`, 3 skills, 2 presets, single bundle row), `npm test` (495), `npm run docs:check`, generated docs checks |
package/CONTRIBUTING.md CHANGED
@@ -11,7 +11,7 @@ Two short documents say what this project refuses to do:
11
11
  pointer does not resolve fails `npm run docs:check:philosophy`.
12
12
  - [docs/adr/](docs/adr/README.md) — dated decision records for choices already
13
13
  made (ports as data, one `state.json`, `allowAgentControl`, the surface
14
- budget).
14
+ budget, the component surface).
15
15
 
16
16
  Every pull request answers **"which principle does this change touch, or which new
17
17
  one does it require?"** in the PR template. "None" is not an answer: if no
@@ -21,6 +21,16 @@ concept budget and its classification table are in
21
21
  or tool may grow only with an ADR or a provably matching deletion, checked by
22
22
  `npm run surface:check`.
23
23
 
24
+ The component axis — who may be switched off — is principle 11 and
25
+ [ADR 0005](docs/adr/0005-component-surface.md). It is generated from
26
+ [scripts/component-surface.json](scripts/component-surface.json) into
27
+ [docs/reference/component-surface.md](docs/reference/component-surface.md) and
28
+ enforced by `npm run docs:check:components`: every visible component needs
29
+ `title`/`summary`/`disables` in both locales, the host/health/recovery/review/
30
+ merge-gate capabilities may not be gated by any row or boolean key (the
31
+ `gateableTools` allowlist is empty since #122), and the component count — 8
32
+ visible rows in 2.4.0 — is the fourth baselined surface.
33
+
24
34
  ## Landing changes
25
35
 
26
36
  `main` is protected. Every change lands through a pull request; direct pushes to `main` are rejected,
@@ -108,10 +118,11 @@ the push gates; CI schedules it nightly.
108
118
  - `worker.js` — the `tower-worker` preset policy (tool surface + protocol text).
109
119
  - `tests/` — add a regression test for every behavior change. The offline suite is `tests/*.test.mjs`; live-runtime scripts are documented in `tests/README.md`.
110
120
  - `scripts/architecture.mjs` — the fitness rules; `scripts/architecture-waivers.json` — the only exemptions, each with a `reason` and an `owner`/`ref`. An entry without a reason fails the blocking `waiver-contract` rule, The three layering rules that keep the core pure — `domain-pure`, `host-scope`, and `no-lib-from-core` — cannot be waived at all. The `let` scan behind `host-scope` is statement-aware: every binding of every top-level declaration is recorded, and only `host/**` may hold ambient state.
121
+ - `types/` — the published declaration surface (`package.json` `types` is `types/index.d.ts`). The vocabulary block in that file is generated from the runtime constants by `npm run types:generate` and gated by `npm run docs:check:generated:types`; never edit it by hand. `tsconfig.strict.json` turns `noImplicitAny` on for an explicit file list that may grow but never silently shrink: add a file, then record it with `npm run types:strict:write` (a removal also needs `--allow-shrink`). `npm run types:coverage` fails when a shipped runtime `.js` file is outside the `tsc` program.
111
122
 
112
123
  ## Pull request checklist
113
124
 
114
- 1. `npm test` passes locally (573 tests today, including protocol end-to-end against a real temporary Git repository and mock spawn coverage) and `npm run docs:check` passes.
125
+ 1. `npm test` passes locally (620 tests today, including protocol end-to-end against a real temporary Git repository and mock spawn coverage) and `npm run docs:check` passes.
115
126
  2. New behavior has a test. Bug fixes should add a test that fails before the fix.
116
127
  3. No secrets, tokens, personal paths, session logs, or generated artifacts are added. `.gitignore` covers local acceptance output; do not force-add it.
117
128
  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.
@@ -123,7 +134,13 @@ the push gates; CI schedules it nightly.
123
134
  reports no failure; a `Regression test:` path must exist.
124
135
  9. If you added a config key or tool, `scripts/surface-baseline.json` and
125
136
  [docs/reference/surface-budget.md](docs/reference/surface-budget.md) are updated in the
126
- same PR, and `npm run surface:check` passes.
137
+ same PR, and `npm run surface:check` passes. A visible component change also updates
138
+ `scripts/surface-baseline.json` (`components`), `scripts/component-surface.json` and
139
+ `docs/reference/component-surface.md` (`npm run docs:generate:components`), and must pass
140
+ `npm run docs:check:components`. A new visible capability row also ships its own
141
+ `<specifier>/locale/*.json` row label via `npm run docs:generate:locale` and passes
142
+ `npm run docs:check:generated:locale` (see
143
+ [architecture/overview.md](docs/architecture/overview.md#row-display-metadata-121)).
127
144
 
128
145
  ## Mount smoke
129
146
 
package/PRINCIPLES.md CHANGED
@@ -135,6 +135,24 @@ without a test path, and a postmortem that names the cause but not the proof.
135
135
  Enforced by: `docs/operations/incident.md`, `scripts/regression-audit.mjs`, and
136
136
  `tests/regression-audit.test.mjs`; gate: `npm run audit:regressions`.
137
137
 
138
+ ## 11. Optional capabilities are switches; the core is not
139
+
140
+ A component may exist only for a capability whose off state leaves the rest of the
141
+ product intact. Every capability that qualifies gets a component, so the user can
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.
145
+
146
+ Refused because of this: a single all-or-nothing switch offered as "simpler"; a
147
+ disableable `tower_doctor`/`tower_recover`; a core feature exposed as a toggle; a
148
+ component whose off state is worse than uninstalling the plugin.
149
+
150
+ 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`.
155
+
138
156
  ## Non-goals
139
157
 
140
158
  This file is not a governance process: there is no RFC queue, no architecture
package/README.en.md CHANGED
@@ -128,20 +128,33 @@ Image and file attachments are forwarded together with the instruction as one co
128
128
 
129
129
  ## Component model
130
130
 
131
- This bundle is **all-or-nothing**: `cordis.patch.yml` inserts one `tower-bundle`
132
- `cordis:group` row, and the Host plugin, skills, and worker/reviewer presets are internal sections
133
- of it. The plugin page should treat that group as one component; disabling it disables all sections
134
- together, so no orphan preset or skill points at missing `tower_*` tools. Do not enable/disable the
135
- child rows independently.
136
-
137
- Mode entry and `tower_plan` validate both presets before work starts. A missing preset is refused
138
- with `preset-missing` and names `preset-tower-worker` / `preset-tower-reviewer`, instead of planning
139
- or spawning first and dead-ending at review/merge.
131
+ The bundle ships a **mandatory host anchor plus individually switchable optional
132
+ rows** (#121): `cordis.patch.yml` inserts a top-level `tower` group that mounts
133
+ the Host plugin and the mandatory `preset-tower-worker`, then the optional
134
+ components `tower-skills`, `tower-batch`, `tower-import`, `tower-leases`,
135
+ `tower-tests`, `tower-hmr` (shipped off) and `preset-tower-reviewer`. Turn a row
136
+ off from the Plugins page and exactly that capability goes away; the mode, the
137
+ planning tools, `tower_status`, and the remaining tools keep working.
138
+
139
+ Mode entry and `tower_plan` validate the required presets before work starts. The
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).
146
+
147
+ [Principle 11](PRINCIPLES.md) and
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`.
140
153
 
141
154
  ## Configuration
142
155
 
143
156
  ```yaml
144
- - id: tower
157
+ - id: tower-host
145
158
  name: 'dsh-tower'
146
159
  config:
147
160
  section: 'Optional legacy alias replacing the Tower-mode prompt text'
@@ -166,13 +179,13 @@ or spawning first and dead-ending at review/merge.
166
179
  - `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.
167
180
  - `/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.
168
181
  - 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.
169
- - Every key and default is in [docs/reference/config.md](docs/reference/config.md); unknown top-level keys are rejected so typos cannot silently disable security-relevant settings, while unknown keys inside a group are ignored. Tool error codes are in [docs/reference/errors.md](docs/reference/errors.md).
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).
170
183
 
171
- ## Tools (27 core)
184
+ ## Tools (29: 22 core + 7 optional)
172
185
 
173
186
  `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`
174
187
 
175
- `tower_control` is registered by default (`allowAgentControl:true`); set it to `false` to disable it. `tower_reload` is registered while `dev.hotReload` is on (default true) and returns `reload-unavailable` without an HMR service (so up to 29 tools). Mission-scoped tools also accept `id` as an alias for `missionId`; snake/camel spellings such as `task_done`/`taskDone` are both accepted.
188
+ The list above is the 22 core tools plus 5 of the component-owned optional tools. `tower_control` is the sixth optional tool, registered by default (`allowAgentControl:true`); set it to `false` to disable it. `tower_reload` is the seventh, registered only while the `tower-hmr` component row is on (shipped off) and returning `reload-unavailable` without an HMR service (so up to 29 tools: 22 core + 7 optional). `tower_doctor` and `tower_recover` are core: they are registered for every configuration (#122). Mission-scoped tools also accept `id` as an alias for `missionId`; snake/camel spellings such as `task_done`/`taskDone` are both accepted.
176
189
 
177
190
  ## Safety and guardrails
178
191
 
@@ -201,7 +214,7 @@ Workers use the bundled `tower-worker` preset (writable worktree surface); revie
201
214
 
202
215
  | Path | Contents |
203
216
  |---|---|
204
- | `package.json`, `cordis.patch.yml` | bundle manifest and loader patch (one `tower-bundle` group containing the Host, skills, and both presets) |
217
+ | `package.json`, `cordis.patch.yml` | bundle manifest and loader patch (a mandatory `tower` host group plus the optional capability rows) |
205
218
  | `index.js`, `host/`, `adapters/`, `app/`, `domain/`, `ports/`, `tools/`, `worker.js`, `reviewer.js` | Host entry; `domain/` pure core; `ports/` contracts; `adapters/` the only I/O; `app/` use cases; `host/` composition root and session glue; `tools/` one handler per tool; worker and reviewer preset policies |
206
219
  | `skills.js`, `skills/` | bundled agent skills (worker protocol, review checklist, recovery playbook) |
207
220
  | `docs/` | guides, reference, architecture, operations, FAQ and examples |
@@ -228,7 +241,7 @@ Every push and PR runs the same set of jobs; each job backs one checkable claim
228
241
 
229
242
  | Job | Claim | Evidence |
230
243
  |---|---|---|
231
- | `test (22)`, `test (24)` (Ubuntu) | the offline suite, documentation consistency, generated references, the published file list, architecture, types, and domain purity all hold | `npm test`, `npm run docs:check`, the four `docs:check:generated*` checks, `pack:check`, `architecture:check`, `types:check`, `domain:check` |
244
+ | `test (22)`, `test (24)` (Ubuntu) | the offline suite, documentation consistency, generated references, the published file list, architecture, types, and domain purity all hold | `npm test`, `npm run docs:check`, the five `docs:check:generated*` checks (including the generated type vocabulary), `pack:check`, `architecture:check`, `types:coverage`, `types:strict:check`, `types:check`, `domain:check` |
232
245
  | `test (22, windows)` (`windows-latest`, not required) | the same offline suite and offline gates hold on a real Windows filesystem, so path casing, the `pwsh` shell, and the rename/unlink retries are measured instead of assumed | `npm test` plus the same offline gates as Ubuntu (no release/coverage) |
233
246
  | `test`'s `Coverage (Node 22)` step | coverage is actually measured in CI (reported only: no threshold, no badge) | `npm run test:coverage` |
234
247
  | `test`'s `Release check` step (Node 22) | a tag is a claim: the version moved past the previous version tag, a pushed tag is `v<version>`, and the changelog carries release content | `npm run release:check` |
@@ -242,7 +255,7 @@ The live suite downloads a browser and boots a real host, so it is out of push/P
242
255
 
243
256
  `tests/platform.test.mjs` injects `platform`/`rename`/`unlink` so both the win32 and POSIX branches are asserted on any OS; the only case that needs a real Windows filesystem (renaming over an open destination) is counted as `# SKIP` on Linux (`tests 11`, `skipped 1`), never silently absent. There is no macOS runner: macOS follows the same POSIX branch as Linux, which is a #86 non-goal.
244
257
 
245
- `mount` asserts the throwaway profile's **loaded surface**, never the text of `cordis.patch.yml`: 29 `tower_*` tools (27 `tools/*.js` modules plus `tower_mode` / `tower_control`), the 3 bundled skills, both the `tower-worker` and `tower-reviewer` presets, the `/tower` command, and exactly one top-level `tower-bundle` group row (the #66 invariant). It calls no model and opens no browser. Measured on the maintainer's Windows machine: about 100 seconds on a cold host (install included) and about 85 seconds with a cached host; the CI step allows 3 minutes (`timeout-minutes: 3`) inside the 5-minute `mount` job. A red run prints the resolved profile path, the host build, the loaded counts, and every unmet claim.
258
+ `mount` asserts the throwaway profile's **loaded surface**, never the text of `cordis.patch.yml`: 28 `tower_*` tools in the shipped profile (27 `tools/*.js` modules plus `tower_mode` / `tower_control`, minus `tower_reload` because `tower-hmr` ships off; 29 with every optional capability row on), the 3 bundled skills, both the `tower-worker` and `tower-reviewer` presets, the `/tower` command, one top-level `tower` host group, and the optional top-level rows (`tower-hmr` is shipped off). It calls no model and opens no browser. Measured on the maintainer's Windows machine: about 100 seconds on a cold host (install included) and about 85 seconds with a cached host; the CI step allows 3 minutes (`timeout-minutes: 3`) inside the 5-minute `mount` job. A red run prints the resolved profile path, the host build, the loaded counts, and every unmet claim.
246
259
 
247
260
  The host build is pinned in exactly one place, `scripts/mount-smoke/host.json`; a unit test keeps the newest `COMPATIBILITY.md` row equal to it, so a release never retypes it. `npm run smoke:fixtures` proves the smoke still fails for the right seeded reasons (a renamed tool id, a removed skills section). CI runs it as the non-required `mount-fixtures` job, where the control and both seeds share one cached host install through `--host-dir` (about 2 minutes warm here).
248
261
 
package/README.md CHANGED
@@ -127,19 +127,26 @@ tower_teardown repoRoot=D:\work\repo
127
127
 
128
128
  ## 组件模型
129
129
 
130
- 本 bundle 采用 **all-or-nothing**:`cordis.patch.yml` 只插入一个 `tower-bundle`
131
- `cordis:group` 行,Host 插件、skills、worker/reviewer preset 都是它的内部 section。插件页只应把
132
- 这个 group 当作一个组件;关闭它时四个 section 一起关闭,不会留下指向已消失 `tower_*` 工具的
133
- orphan preset/skill。不要再把子行单独启停。
134
-
135
- 进入 Tower 模式与 `tower_plan` 前会校验两个 preset 是否已注册;缺失时直接以 `preset-missing`
136
- 拒绝(并在消息中点名 `preset-tower-worker` / `preset-tower-reviewer`),不会先完成规划/派发再在
137
- review/merge 阶段死锁。
130
+ 本 bundle 采用 **强制 host 锚点 + 可单独启停的可选行**(#121):`cordis.patch.yml` 插入顶层
131
+ `tower` group(挂载 Host 插件与强制的 `preset-tower-worker`),随后是可选组件 `tower-skills`、
132
+ `tower-batch`、`tower-import`、`tower-leases`、`tower-tests`、`tower-hmr`(随包关闭)与
133
+ `preset-tower-reviewer`。在插件页关闭某一行,只会移除该能力;模式、规划工具、`tower_status`
134
+ 与其余工具继续工作。
135
+
136
+ 进入 Tower 模式与 `tower_plan` 前会校验必需的 preset。worker preset 是强制依赖;reviewer
137
+ preset 只在 `review.requireReview` 为 true 时必需,因此审查可选时允许关闭 reviewer 行,强制审查
138
+ 开启时会以带解释的 `preset-missing` 锁定。`tower-tests` 关闭且 `gates.requireTests:true` 时同样
139
+ 拒绝。`tower_doctor` 与 `tower_recover` 不是组件、也没有配置键:任何配置都会注册它们(#122)。
140
+
141
+ 原则 11([PRINCIPLES.md](PRINCIPLES.md))与
142
+ [ADR 0005](docs/adr/0005-component-surface.md) 要求:只有关闭后仍不破坏其余能力的 capability
143
+ 才能成为组件;host、健康检查、恢复通道、审查契约与合并门禁永远不是开关。逐键答案见生成的
144
+ [组件表面](docs/reference/component-surface.md),由 `npm run docs:check:components` 强制。
138
145
 
139
146
  ## 配置
140
147
 
141
148
  ```yaml
142
- - id: tower
149
+ - id: tower-host
143
150
  name: 'dsh-tower'
144
151
  config:
145
152
  section: '(可选,兼容别名)覆盖 Tower 模式系统提示文案'
@@ -164,13 +171,13 @@ review/merge 阶段死锁。
164
171
  - 代码中 `mode.allowAgentControl` 默认 `true`;随包 `cordis.patch.yml` 也显式设为 `true`:模型可在用户意图/任务状态明确时自行进出,但必须给出理由;设为 `false` 时只有人类能切换。随包 patch 还默认开启 `mode.workspaceLock`(单活塔)、`send.interruptDefault`、`init.autoGitInit`,并把 `spawn.staleMs` 设为 15s。
165
172
  - `/tower base <branch>` 可设置会话级 init base 偏好;`/tower on` 会清除活跃 plan mode(`mode.planModePolicy:"refuse"` 则拒绝进入),保证 plan/tower 互斥。
166
173
  - 仓库级配置:在 `<git-common-dir>/dsh-tower/config.json`(通常是 `.git/dsh-tower/config.json`)放入同样的 JSON 结构,可覆盖 `review`/`merge`/`worktrees`/`spawn`;优先级为代码默认 < profile < 仓库配置 < 工具参数。`mode.*` 属于会话级,只在 profile 生效。
167
- - 所有键与默认值见 [docs/reference/config.md](docs/reference/config.md);未知顶层键会被拒绝(防止拼写错误静默失效),嵌套分组内的未知键会被忽略。工具错误码见 [docs/reference/errors.md](docs/reference/errors.md)。
174
+ - 所有键与默认值见 [docs/reference/config.md](docs/reference/config.md);未知键(顶层与分组内)都会被拒绝,防止拼写错误静默失效。工具错误码见 [docs/reference/errors.md](docs/reference/errors.md)。
168
175
 
169
- ## 工具(27 个核心)
176
+ ## 工具(29 个:22 个核心 + 7 个可选)
170
177
 
171
178
  `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`
172
179
 
173
- `tower_control` 默认注册(`allowAgentControl:true`);设为 `false` 可禁用;`tower_reload` 在 `dev.hotReload`(默认 true)时注册,无 HMR 服务调用会返回 `reload-unavailable`(因此最多 29 个工具)。mission 级工具同时接受 `id` 作为 `missionId` 的别名;`task_done`/`taskDone` 等 snake/camel 写法均可。
180
+ 上面列出的是 22 个核心工具加 5 个由组件行拥有的可选工具。`tower_control` 是第 6 个可选工具,默认注册(`allowAgentControl:true`);设为 `false` 可禁用。`tower_reload` 是第 7 个,仅在 `tower-hmr` 组件行开启时注册(随包关闭),无 HMR 服务调用会返回 `reload-unavailable`(因此最多 29 个工具:22 个核心 + 7 个可选)。`tower_doctor` 与 `tower_recover` 属于核心:任何配置都会注册它们(#122)。mission 级工具同时接受 `id` 作为 `missionId` 的别名;`task_done`/`taskDone` 等 snake/camel 写法均可。
174
181
 
175
182
  ## 安全与约束
176
183
 
@@ -199,7 +206,7 @@ worker 与 reviewer 是使用内置 `tower-worker` preset 的普通 DSH 子代
199
206
 
200
207
  | 路径 | 内容 |
201
208
  |---|---|
202
- | `package.json`、`cordis.patch.yml` | bundle 清单与 loader patch(单个 `tower-bundle` group,内含 Host、skills 与两个 preset) |
209
+ | `package.json`、`cordis.patch.yml` | bundle 清单与 loader patch(强制 `tower` host group + 可选能力行) |
203
210
  | `index.js`、`host/`、`adapters/`、`app/`、`domain/`、`ports/`、`tools/`、`worker.js` | Host 入口;`domain/` 纯核心;`ports/` 契约;`adapters/` 唯一 I/O;`app/` 用例;`host/` 组合根与会话胶水;`tools/` 每工具一个 handler;worker preset 策略 |
204
211
  | `locale/`、`icon.svg` | 本地化与图标 |
205
212
  | `tests/` | 纯函数、协议 e2e、插件级测试;可选的真实 Web 运行时脚本 |
@@ -222,7 +229,7 @@ npm test # node --test tests/*.test.mjs
222
229
 
223
230
  | Job | 声明 | 证据 |
224
231
  |---|---|---|
225
- | `test (22)`、`test (24)`(Ubuntu) | 离线套件、文档一致性、生成文档、发布文件清单、架构、类型与 domain 纯度全部成立 | `npm test`、`npm run docs:check`、四个 `docs:check:generated*`、`pack:check`、`architecture:check`、`types:check`、`domain:check` |
232
+ | `test (22)`、`test (24)`(Ubuntu) | 离线套件、文档一致性、生成文档、发布文件清单、架构、类型与 domain 纯度全部成立 | `npm test`、`npm run docs:check`、五个 `docs:check:generated*`(含生成的类型词汇表)、`pack:check`、`architecture:check`、`types:coverage`、`types:strict:check`、`types:check`、`domain:check` |
226
233
  | `test (22, windows)`(`windows-latest`,非必需检查) | 同一套离线套件与离线 gates 在真实 Windows 文件系统上成立;路径大小写、`pwsh` shell、rename/unlink 重试都不是只在 Linux 上被假设 | `npm test` + 与 Ubuntu 相同的离线 gates(不跑 release/coverage) |
227
234
  | `test` 的 `Coverage (Node 22)` 步骤 | 覆盖率在 CI 中真实测量(只报告,不设阈值、不加徽章) | `npm run test:coverage` |
228
235
  | `test` 的 `Release check`(Node 22)步骤 | tag 是一种声明:版本已越过上一个版本 tag、推送的 tag 是 `v<version>`、CHANGELOG 带有发布内容 | `npm run release:check` |
@@ -236,7 +243,7 @@ live suite 需要下载浏览器并真实启动 host,因此移出 push/PR,
236
243
 
237
244
  `tests/platform.test.mjs` 用注入的 `platform`/`rename`/`unlink` 在任意 OS 上同时断言 win32 与 POSIX 两条分支;唯一需要真实 Windows 的“rename 覆盖被打开的目标文件”用例在 Linux 上以 `# SKIP` 计入 TAP(`tests 11`、`skipped 1`),而不是静默消失。macOS 不单设 runner:它与 Linux 走同一条 POSIX 分支,这是 #86 的非目标。
238
245
 
239
- `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、已加载计数与每一条未满足的声明。
246
+ `mount` 断言一次性 profile 的**已加载表面**,而不是 `cordis.patch.yml` 的文本:随包 profile 加载 28 个 `tower_*` 工具(27 个 `tools/*.js` 模块 + `tower_mode` / `tower_control`,减去因 `tower-hmr` 随包关闭而不注册的 `tower_reload`;所有可选能力行全开时为 29 个)、3 个内置 skill、`tower-worker` / `tower-reviewer` 两个 preset、`/tower` 命令、一个顶层 `tower` host group 以及可选的顶层能力行(`tower-hmr` 随包关闭)。它不调用模型、不打开浏览器。在维护者的 Windows 机器上实测:host 冷装(含安装)约 100 秒,host 已缓存约 85 秒;CI 中该步骤上限为 3 分钟(`timeout-minutes: 3`),`mount` job 上限 5 分钟。红叉会打印解析后的 profile 路径、host build、已加载计数与每一条未满足的声明。
240
247
 
241
248
  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 分钟)。
242
249
 
package/cordis.patch.yml CHANGED
@@ -1,13 +1,25 @@
1
- # dsh-tower is an all-or-nothing bundle. The single group row is the only
2
- # user-visible component: disabling it disables every internal section (host,
3
- # skills, worker/reviewer presets) together, so no orphan preset or skill can
4
- # point at missing tower_* tools. See docs/architecture/overview.md.
1
+ # dsh-tower component rows (#121).
2
+ #
3
+ # The first top-level row is the mandatory host group: the Host plugin and its
4
+ # worker-preset dependency. Turning it off equals uninstalling Tower (no tools,
5
+ # no presets, no skills), which is the accepted all-off boundary; every other
6
+ # top-level row is an individually switchable optional capability whose off
7
+ # state leaves the rest of Tower working.
8
+ #
9
+ # Policy: principle 11 in PRINCIPLES.md + docs/adr/0005-component-surface.md.
10
+ # The per-key answer is docs/reference/component-surface.md, generated from
11
+ # scripts/component-surface.json. `npm run docs:check:components` refuses a new
12
+ # switch, a missing locale label, or a moved component count without a written
13
+ # decision. `tower_doctor` and `tower_recover` are part of the host and are never
14
+ # switches (#122); a capability row owns exactly one optional capability, and
15
+ # `tower_status` / `tower_doctor` name any row that is off.
5
16
  - insert:
6
- - id: tower-bundle
17
+ # Mandatory host anchor. Not an optional capability: off == uninstall.
18
+ - id: tower
7
19
  name: cordis:group
8
20
  group: true
9
21
  config:
10
- - id: tower
22
+ - id: tower-host
11
23
  name: 'dsh-tower'
12
24
  config:
13
25
  mode:
@@ -21,8 +33,6 @@
21
33
  interruptDefault: true
22
34
  spawn:
23
35
  staleMs: 15000
24
- - id: tower-skills
25
- name: 'dsh-tower/skills'
26
36
  - id: preset-tower-worker
27
37
  name: '@deepseek-ai/dsh-agent-preset'
28
38
  config:
@@ -41,23 +51,41 @@
41
51
  sampleOverCapGlobResults: false
42
52
  - id: tower-worker-pwsh
43
53
  name: '@deepseek-ai/dsh-tool-pwsh'
44
- - id: preset-tower-reviewer
45
- name: '@deepseek-ai/dsh-agent-preset'
46
- config:
47
- id: tower-reviewer
48
- name: Tower Reviewer
49
- description: Read-only independent reviewer for a completed Tower mission.
50
- order: 61
51
- plugins:
52
- - id: tower-reviewer-policy
53
- name: 'dsh-tower/reviewer'
54
- - id: tower-reviewer-fs
55
- name: '@deepseek-ai/dsh-tool-fs'
56
- config:
57
- write: false
58
- edit: false
59
- readImage: false
60
- - id: tower-reviewer-fs-search
61
- name: '@deepseek-ai/dsh-tool-fs-search'
62
- config:
63
- sampleOverCapGlobResults: false
54
+ # Optional capabilities: each row is the switch for exactly one capability.
55
+ - id: tower-skills
56
+ name: 'dsh-tower/skills'
57
+ - id: tower-batch
58
+ name: 'dsh-tower/batch'
59
+ - id: tower-import
60
+ name: 'dsh-tower/import'
61
+ - id: tower-leases
62
+ name: 'dsh-tower/leases'
63
+ - id: tower-tests
64
+ name: 'dsh-tower/tests'
65
+ # dev hot reload: shipped off. Enable the row for a linked dev profile.
66
+ - id: tower-hmr
67
+ name: 'dsh-tower/hmr'
68
+ disabled: true
69
+ # Conditional optional component: off is supported only while
70
+ # review.requireReview is false; with the gate on planning/mode entry refuse
71
+ # the combination with an explanatory error.
72
+ - id: preset-tower-reviewer
73
+ name: '@deepseek-ai/dsh-agent-preset'
74
+ config:
75
+ id: tower-reviewer
76
+ name: Tower Reviewer
77
+ description: Read-only independent reviewer for a completed Tower mission.
78
+ order: 61
79
+ plugins:
80
+ - id: tower-reviewer-policy
81
+ name: 'dsh-tower/reviewer'
82
+ - id: tower-reviewer-fs
83
+ name: '@deepseek-ai/dsh-tool-fs'
84
+ config:
85
+ write: false
86
+ edit: false
87
+ readImage: false
88
+ - id: tower-reviewer-fs-search
89
+ name: '@deepseek-ai/dsh-tool-fs-search'
90
+ config:
91
+ sampleOverCapGlobResults: false
package/docs/README.md CHANGED
@@ -9,7 +9,7 @@ Choose a path:
9
9
  | New user | [When to use Tower](guide/scenarios-and-philosophy.md) → [Quick start](guide/quickstart.md) → [Tools](reference/tools.md) → [Example](examples/first-mission.md) |
10
10
  | Choosing a workflow | [When to use Tower, and how it differs from Agent Teams, Ralph, Workflow and parallel subagents](guide/scenarios-and-philosophy.md) |
11
11
  | Operator | [Runbook](operations/runbook.md) → [Blockers](reference/blockers.md) → [Error codes](reference/errors.md) → [Events](reference/events.md) → [Doctor](reference/doctor.md) → [Testing](reference/testing.md) → [Batches/leases/legacy](reference/operations.md) → [Security](operations/security.md) → [Incidents and regressions](operations/incident.md) |
12
- | Maintainer | [Engineering principles](../PRINCIPLES.md) → [ADRs](adr/README.md) → [Architecture](architecture/overview.md) → [Surface budget](reference/surface-budget.md) → [Config](reference/config.md) → [Hot reload](guide/hot-reload.md) → [Release checklist](guide/release.md) → [Tracker board](board.md) → [Contributing](../CONTRIBUTING.md) |
12
+ | Maintainer | [Engineering principles](../PRINCIPLES.md) → [ADRs](adr/README.md) → [Architecture](architecture/overview.md) → [Surface budget](reference/surface-budget.md) → [Component surface](reference/component-surface.md) → [Config](reference/config.md) → [Hot reload](guide/hot-reload.md) → [Release checklist](guide/release.md) → [Tracker board](board.md) → [Contributing](../CONTRIBUTING.md) |
13
13
 
14
14
  Reference pages are the source of truth for the current release; the [tools reference](reference/tools.md),
15
15
  [blocker list](reference/blockers.md) and [error codes](reference/errors.md) are checked against the
@@ -37,6 +37,7 @@ Other useful pages: [When to use Tower](guide/scenarios-and-philosophy.md), [Ins
37
37
  - [Batches, leases and legacy import](reference/operations.md)
38
38
  - [Doctor](reference/doctor.md)
39
39
  - [Surface budget and classification](reference/surface-budget.md)
40
+ - [Component surface: who may be switched off](reference/component-surface.md)
40
41
 
41
42
  **Operations and background**
42
43
 
@@ -36,6 +36,26 @@ note for each niche key.
36
36
  - The numbers are three lines in `tower_doctor` output, not a new dashboard.
37
37
  - This does not remove any key in this release; it is a brake, not a clean-up.
38
38
 
39
+ ## Measurement correction (2026-10-23)
40
+
41
+ The first baseline counted 27 tools because it recorded only the `define()`
42
+ registration path. `tower_mode` and `tower_control` are registered directly by
43
+ `index.js`, so the shipped surface was always 29 (#118). The baseline now carries
44
+ an explicit correction decision (27 → 29) with a reason, a date and a reference
45
+ to this ADR, and the recorder in `host/tool-registry.js` covers both registration
46
+ paths. This is a measurement correction, not surface growth: a later count change
47
+ still needs its own ADR or a provably matching deletion.
48
+
49
+ ## Amended by ADR 0005 (2026-10-23)
50
+
51
+ The decision above counts three surfaces. [ADR 0005](0005-component-surface.md)
52
+ extends the budget with a fourth — the number of user-visible component rows in
53
+ `cordis.patch.yml` — and records it as its own `components` baseline entry with a
54
+ two-way decision rule (a contraction needs a decision just like growth). The
55
+ three original counts are still measured by `npm run surface:check`; the
56
+ component count is validated by `npm run docs:check:components`. The historical
57
+ decision above is unchanged: "three surfaces" describes the 2.3.0 rule.
58
+
39
59
  ## Rejected alternatives
40
60
 
41
61
  - **A line-count or file-size rule** — the project already has those and the
@@ -0,0 +1,130 @@
1
+ # 0005 — The component surface: mandatory host, switchable optional rows
2
+
3
+ - Date: 2026-10-23
4
+ - Status: Accepted
5
+ - Deciders: maintainer
6
+ - Supersedes: none
7
+
8
+ ## Context
9
+
10
+ The plugin had no durable statement of what may and may not be a switch. The
11
+ policy that #65 was resolved against lived only in the issue body, so when the
12
+ issue closed the policy evaporated: the 2.0.4 surface (four top-level rows:
13
+ `tower`, `tower-skills`, `preset-tower-worker`, `preset-tower-reviewer`) became
14
+ one all-or-nothing `tower-bundle` group row plus seven `tools.expose*` config
15
+ keys. The user-controllable component count went 4 → 1 while nothing recorded
16
+ that decision or refused the next one.
17
+
18
+ Two failure modes are already visible in the 2.3.0 tree:
19
+
20
+ 1. Optional capabilities have no component. Skills, the reviewer preset, the
21
+ batch/import/leases/test tools and hot reload can only be changed by editing
22
+ YAML under `tools:`, which is the discoverability problem components exist to
23
+ solve. A config key is still a switch, just a worse-labelled one.
24
+ 2. The safety nets are switchable. `tools.exposeOperational:false` removes all
25
+ seven operational tools including `tower_recover` and `tower_doctor`, so the
26
+ equipment that diagnoses and repairs a stale `state.lock` can itself be
27
+ turned off, while the plugin stays mounted. That off state is worse than
28
+ uninstalling the plugin, because uninstalling would remove the lock problem
29
+ too.
30
+
31
+ `PRINCIPLES.md` already says an enforcement pointer that does not resolve is a
32
+ slogan; by that standard the component policy was a slogan.
33
+
34
+ ## Decision
35
+
36
+ Adopt this shape and record it as principle 11:
37
+
38
+ - **The host is mandatory.** `tower` (and its worker preset dependency) is never
39
+ a switch, not as a row and not as a boolean that removes it.
40
+ - **Each optional capability is its own component row**, individually toggleable
41
+ from the plugin page, with `title`, `summary` and `disables` in both locales and
42
+ a documented fallback stating what still works while it is off. A capability
43
+ qualifies only when its off state leaves the rest of Tower intact and is not
44
+ worse than uninstalling.
45
+ - **The safety nets are never switches.** The health contract, the recovery
46
+ escape, the review contract and the merge gates are configuration or code; no
47
+ group, row or boolean key may gate them. Enforcement covers both routes: a
48
+ `cordis.patch.yml` row that defaults any key classified `forbidden-to-disable`
49
+ to `false` is refused, and every `tools.expose*` route in
50
+ `host/tool-registry.js` must match the `gateableTools` allowlist in the source
51
+ of truth, so a new route to a review, merge or host tool fails even when that
52
+ capability lists no tools of its own.
53
+ - **Config keys stay on the component axis.** Every config key is classified
54
+ `core`, `optional-component`, `optional-config` or `forbidden-to-disable` in
55
+ `docs/reference/component-surface.md`, generated from the single source of
56
+ truth `scripts/component-surface.json`, so "should this be a component?" is a
57
+ table lookup rather than a judgement call.
58
+ - **The component count is a baselined surface.** `scripts/surface-baseline.json`
59
+ gains `components` (seed 4, the 2.0.4 row count; value 1, today's group row) and
60
+ the 4 → 1 contraction is itself a dated decision. Unlike the other surfaces, the
61
+ component count is two-way: a contraction needs a decision record just like
62
+ growth does. `npm run surface:check` and `npm run docs:check:components` refuse a
63
+ count that moved without one. That is what makes the next contraction — or the
64
+ coming growth — visible in a diff.
65
+
66
+ This ADR records the policy. The individual instance fixes follow it: #121 turns
67
+ the qualifying capabilities into optional rows, and #122 deletes
68
+ `tools.exposeDoctor` / `tools.exposeRecovery`, removes the `exposeOperational`
69
+ master switch and the whole `tools` config group. While that migration was in
70
+ flight the two `tools.expose*` keys that gated the health and recovery contracts
71
+ were recorded as dated debt in `scripts/component-surface.json`, so a *new*
72
+ switch failed the gate immediately even before #122 landed.
73
+
74
+ ## Migration (2.4.0): the #65 policy is restored from the carrier
75
+
76
+ - Date: 2026-10-23
77
+ - Status: implemented by #121 and #122
78
+
79
+ The one `tower-bundle` group is gone. `cordis.patch.yml` now ships the mandatory
80
+ `tower` host anchor (the Host plugin and the worker preset are its internal
81
+ sections) plus the individually switchable top-level rows `tower-skills`,
82
+ `tower-batch`, `tower-import`, `tower-leases`, `tower-tests`, `tower-hmr`
83
+ (shipped off) and `preset-tower-reviewer` (conditional on
84
+ `review.requireReview`). The visible component count moved 1 → 8 and is recorded
85
+ as a dated decision in `scripts/surface-baseline.json`.
86
+
87
+ The capability switches became rows: the seven `tools.expose*` keys and
88
+ `dev.hotReload` were deleted from the schema and are now rejected as unknown
89
+ keys. `tower_doctor` and `tower_recover` are registered for every configuration
90
+ and have no switch; the `gateableTools` allowlist is empty. This is the
91
+ carrier's promised instance fix, not a new policy: #120 wrote the rule down, and
92
+ this wave makes the shipped surface obey it instead of carrying debt.
93
+
94
+ ## Consequences
95
+
96
+ - The user can control every capability that legitimately has an off state, from
97
+ the place components are controlled, instead of editing YAML.
98
+ - The host, the diagnostics and the lock escape are always registered; a failure
99
+ message may safely point at `tower_recover`.
100
+ - A component count change now needs a written decision, so the 2.3.0 4 → 1
101
+ contraction can never recur silently; the 1 → 8 restoration is recorded the
102
+ same way.
103
+ - The 2.3.0 debt is paid: `tools.exposeDoctor`, `tools.exposeRecovery` and the
104
+ `exposeOperational` master switch are gone, so no configuration can remove the
105
+ health contract or the lock escape. The `gateableTools` allowlist stays in the
106
+ source of truth, empty, so the next `tools.expose*` route is refused rather
107
+ than legalised by default.
108
+ - Adding a component costs a locale row in two languages and a fallback sentence
109
+ in the source of truth; that is the intended marginal cost.
110
+
111
+ ## Rejected alternatives
112
+
113
+ - **One all-or-nothing group row (the shipped 2.3.0 shape).** Simple, and it
114
+ removes the orphan-mount hazard, but it removes every user control as well:
115
+ the qualifying capabilities become unreachable from the plugin page. It also
116
+ makes the "off state" question vacuous, because the only off state is
117
+ "uninstalled".
118
+ - **Config-key switches instead of components.** `tools.expose*` and
119
+ `dev.hotReload` carry the same capabilities with none of the discoverability,
120
+ and they make safety nets disableable in a place where no label warns about it.
121
+ A config key is a switch with worse labelling, not an absence of a switch.
122
+ - **Per-tool components.** 29 rows is a worse surface than 29 tools, and it turns
123
+ every implementation detail of the tool registry into a user decision.
124
+ - **A denylist-only rule with no component count.** It would catch a forbidden
125
+ switch but not the 4 → 1 contraction, which is the failure that actually
126
+ happened; the count baseline is the part that keeps the policy alive across the
127
+ next refactor.
128
+ - **A GitHub issue label or a docs-only policy.** The brake must run offline in
129
+ `npm test` and in CI without a token, and the policy must survive the closure
130
+ of the issue that introduced it.
@@ -15,6 +15,7 @@ alternatives, Date and Status.
15
15
  | [0002](0002-single-state-json.md) | One `state.json` is the mission persistence contract | 2026-10-23 |
16
16
  | [0003](0003-allow-agent-control-default-true.md) | `allowAgentControl` defaults to `true` | 2026-10-23 |
17
17
  | [0004](0004-surface-budget.md) | The concept surface is frozen by a committed baseline | 2026-10-23 |
18
+ | [0005](0005-component-surface.md) | Mandatory host plus individually toggleable optional component rows | 2026-10-23 |
18
19
 
19
20
  ## Template
20
21