dsh-tower 2.4.1 → 2.5.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 (82) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/COMPATIBILITY.md +2 -0
  3. package/CONTRIBUTING.md +11 -5
  4. package/GLOSSARY.md +67 -0
  5. package/README.md +6 -0
  6. package/app/archive.js +16 -1
  7. package/app/board.js +16 -1
  8. package/app/gate.js +14 -0
  9. package/app/spawn-prechecks.js +18 -3
  10. package/cordis.patch.yml +27 -8
  11. package/docs/adr/0004-surface-budget.md +10 -4
  12. package/docs/adr/0005-component-surface.md +166 -0
  13. package/docs/architecture/overview.md +47 -20
  14. package/docs/guide/install-upgrade-migration.md +21 -0
  15. package/docs/guide/quickstart.md +71 -0
  16. package/docs/guide/release.md +60 -19
  17. package/docs/reference/component-surface.md +29 -17
  18. package/docs/reference/config.generated.md +4 -7
  19. package/docs/reference/config.md +7 -5
  20. package/docs/reference/doctor.md +11 -5
  21. package/docs/reference/surface-budget.md +29 -11
  22. package/docs/reference/tools.generated.md +1 -1
  23. package/docs/reference/tools.md +2 -2
  24. package/docs/zh/guide/install-upgrade-migration.md +1 -1
  25. package/docs/zh/reference/config.md +7 -5
  26. package/docs/zh/reference/doctor.md +7 -2
  27. package/docs/zh/reference/tools.md +3 -2
  28. package/domain/component-rules.js +216 -0
  29. package/domain/components.js +169 -49
  30. package/domain/config/schema.js +46 -23
  31. package/domain/gate.js +17 -2
  32. package/domain/scope.js +176 -5
  33. package/domain/surface.js +21 -7
  34. package/host/capability-archive.js +18 -0
  35. package/host/capability-board.js +18 -0
  36. package/host/capability-registry.js +4 -0
  37. package/host/doctor.js +50 -5
  38. package/host/preset-reviewer.js +27 -0
  39. package/host/preset-worker.js +32 -0
  40. package/host/presets.js +17 -0
  41. package/host/tool-registry.js +21 -4
  42. package/index.js +17 -6
  43. package/locale/archive/en.json +6 -0
  44. package/locale/archive/zh.json +6 -0
  45. package/locale/batch/en.json +1 -1
  46. package/locale/batch/zh.json +1 -1
  47. package/locale/board/en.json +6 -0
  48. package/locale/board/zh.json +6 -0
  49. package/locale/en.json +41 -21
  50. package/locale/hmr/en.json +2 -2
  51. package/locale/hmr/zh.json +1 -1
  52. package/locale/import/en.json +1 -1
  53. package/locale/import/zh.json +1 -1
  54. package/locale/leases/en.json +2 -2
  55. package/locale/leases/zh.json +2 -2
  56. package/locale/preset-reviewer/en.json +6 -0
  57. package/locale/preset-reviewer/zh.json +6 -0
  58. package/locale/preset-worker/en.json +6 -0
  59. package/locale/preset-worker/zh.json +6 -0
  60. package/locale/skills/en.json +1 -1
  61. package/locale/skills/zh.json +1 -1
  62. package/locale/tests/en.json +2 -2
  63. package/locale/tests/zh.json +2 -2
  64. package/locale/zh.json +40 -20
  65. package/package.json +15 -2
  66. package/scripts/check-components.mjs +642 -42
  67. package/scripts/check-pack.mjs +7 -3
  68. package/scripts/component-surface.json +87 -29
  69. package/scripts/generate-capability-locale.mjs +100 -21
  70. package/scripts/mount-smoke/probe.mjs +33 -3
  71. package/scripts/mount-smoke/surface.mjs +144 -1
  72. package/scripts/mount-smoke.mjs +24 -4
  73. package/scripts/pack-manifest.txt +15 -0
  74. package/scripts/release-notes.mjs +256 -0
  75. package/scripts/surface-baseline.json +32 -3
  76. package/scripts/surface-baseline.mjs +15 -5
  77. package/skills.js +4 -1
  78. package/tools/complete.js +3 -1
  79. package/tools/spawn.js +5 -5
  80. package/tools/test.js +7 -7
  81. package/ui-bundle/locale/en.json +1 -1
  82. package/ui-bundle/locale/zh.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,57 @@
4
4
 
5
5
  ## 未发布(Unreleased)
6
6
 
7
+ ## 2.5.0(2026-11-01)
8
+
9
+ ### 新增
10
+
11
+ - **#133 追踪看板成为可选能力**:新增 `tower-board` 组件行,关闭后不再写 `MISSIONS.md`,
12
+ `tower_status`/`tower_doctor` 说明关闭原因;`docs.boardLocation` 仍是位置调节项。
13
+ 旧的 `docs.board` 配置键被删除,写入旧键会收到带迁移说明的 `config-unknown-key`。
14
+ - **#135 归档成为可选能力**:新增 `tower-archive` 组件行,关闭后终态任务保持热存储且不自动归档,
15
+ doctor 报告关闭后果;`archive.afterDays`/`archive.maxMissions` 仍是节奏调节项。
16
+ 旧的 `archive.enabled` 配置键被删除。
17
+ - **#134 `tower-ui` 进入组件源**:`ui-bundle/cordis.patch.yml` 的 UI 行现在被组件源、locale 覆盖与
18
+ 基线同时看到;门禁解析所有随包发布的 `cordis.patch.yml`。
19
+ - **#137 面向普通用户的双语标签与术语表**:新增 `GLOSSARY.md`(12 组中英术语 + 文案规则),
20
+ 重写全部可见组件行的标题、用途与关闭后果;不再出现原始配置键、工具 id、斜杠命令或模块说明符。
21
+
22
+ ### 变更
23
+
24
+ - **#138 组件门禁改为失败关闭**:键分类、行为开关、行覆盖、随包补丁与双向计数面全部机器校验;
25
+ 双向面的每次计数决策必须带日期;`OPERATIONAL_TOOLS` 直接导入真实注册表,不再靠正则猜测。
26
+ - **#136 删除 `tests.enabled` 第二开关**:`tower-tests` 组件行是唯一开关;`tower_test` 在行关闭时
27
+ 报告 `disabled by component tower-tests` 并给出修复方式,不再误报“未配置”。
28
+ - **#132 组行关闭后果可见**:`tower` 组行的“关闭等于卸载 Tower”现在显示在 Plugins 页面的描述里,
29
+ 不再只是不可达的 locale 文本。
30
+ - **#141 HMR 明确定位为开发工具**:`tower-hmr` 保持默认关闭,文案改为“开发热重载(开发工具)”。
31
+ - 配置面 55 → 53 键;可见组件 11 个;新增 `tower-board`/`tower-archive` 行 locale 与打包清单条目。
32
+
33
+ ### 修复
34
+
35
+ - **#131 租约组合提前拒绝**:`tower-leases` 关闭且 `spawn.requireLease` 非空时,配置/预检阶段直接
36
+ 拒绝并说明行与两项修复方式;`lease-busy` 只指向真实注册的工具,`tower_doctor` 交叉检查该组合。
37
+ - **#139 能力卸载不留残影**:skills provider 注册与 Tower 模式提示段都通过 `ctx.effect` 释放,
38
+ 热卸载后 `componentReport` 与真实挂载状态一致。
39
+ - **#140 禁用行原因一致**:`tests-missing` 合并阻塞项点名 `tower-tests` 行;机器输出的英文契约写入文档。
40
+ - **#142 `/tower` 预检不再误伤转发输入**:运行前置条件只在进入模式/切换时重新校验,
41
+ 转发指令与 `/tower off` 在无关行关闭时继续工作。
42
+ - **F16 作用域重叠判定补全**:`scopesOverlap` 在保留文档化近似的同时加入精确 token-NFA 交集,
43
+ 修掉 `a/b*` 与 `a/bc*` 被误判为不重叠的问题。
44
+ - **F17 配置解析一遍收敛**:`resolveTowerConfig` 现在对空输入也满足严格一遍幂等。
45
+ - **F23 术语表随包发布**:`GLOSSARY.md` 进入 `package.json` `files` 与打包清单(293 个文件)。
46
+
47
+ ## 2.4.2(2026-10-24)
48
+
49
+ ### 修复
50
+
51
+ - **两个 Agent 预设行在 Plugins 页面无法区分**:此前 `preset-tower-worker` 与 `preset-tower-reviewer`
52
+ 共用 `@deepseek-ai/dsh-agent-preset` 模块,页面按模块包取标签,两行都显示「Agent 预设定义」。
53
+ 现在两行分别由 Tower 自有的 import-free preset 声明模块承载(`dsh-tower/preset-worker` /
54
+ `dsh-tower/preset-reviewer`),通过注入的 `agentPresets.register(config)` 注册,
55
+ preset 的 id/name/description/order/plugins 保持不变;两个 specifier 各自提供生成的
56
+ `<specifier>/locale/{en,zh}.json`,页面显示「Tower Worker 预设」「Tower Reviewer 预设」
57
+ 及各自用途与关闭影响。真实 `smoke:mount` 在 pinned host 上确认 2 个 preset 均挂载且标签不同。
7
58
  ## 2.4.1(2026-10-24)
8
59
 
9
60
  ### 修复
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.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
+ | 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 |
8
10
  | 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
11
  | 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 |
10
12
  | 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` |
package/CONTRIBUTING.md CHANGED
@@ -26,7 +26,8 @@ The component axis — who may be switched off — is principle 11 and
26
26
  [scripts/component-surface.json](scripts/component-surface.json) into
27
27
  [docs/reference/component-surface.md](docs/reference/component-surface.md) and
28
28
  enforced by `npm run docs:check:components`: every visible component needs
29
- `title`/`summary`/`disables` in both locales, the host/health/recovery/review/
29
+ `title`/`summary`/`disables` in both locales (and every generated row label —
30
+ capability or preset — needs the same three strings), the host/health/recovery/review/
30
31
  merge-gate capabilities may not be gated by any row or boolean key (the
31
32
  `gateableTools` allowlist is empty since #122), and the component count — 8
32
33
  visible rows in 2.4.0 — is the fourth baselined surface.
@@ -101,7 +102,9 @@ cd dsh-tower
101
102
  npm test # no dependencies to install; Node's built-in test runner only
102
103
  ```
103
104
 
104
- Requirements: Node.js 22+, Git. No package installation is needed for the offline suite.
105
+ Requirements: Node.js 22+, Git. No package installation is needed for the offline example suite.
106
+
107
+ The property-based PR tier (`npm run test:property`) does need the dev dependencies: run `npm ci` once, then `npm run test:property`. It is part of `npm run check` and runs on every CI matrix leg; the nightly long tier is `npm run test:property:long`. See [tests/README.md](tests/README.md#property-based-tests-fast-check-130).
105
108
 
106
109
  The opt-in live suite is the exception: install it once with
107
110
  `npm ci --prefix tests/live && npx --prefix tests/live playwright install chromium`, then run it with `npm run test:live`
@@ -116,13 +119,14 @@ the push gates; CI schedules it nightly.
116
119
  - `index.js` — Host entry: `/tower` command, mode guard, dynamic prompt section, `tower_control`.
117
120
  - `host/` — the composition root and session/process glue (`tool-registry.js`, `config.js`, `hmr.js`, `modeStore.js`, `doctor.js`); `adapters/` — the only I/O implementations (store, git, archive, leases, locks); `app/` — use cases; `domain/` — the pure core.
118
121
  - `worker.js` — the `tower-worker` preset policy (tool surface + protocol text).
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`.
122
+ - `host/preset-worker.js` / `host/preset-reviewer.js` — the two Tower-owned preset declaration rows (`dsh-tower/preset-*`). They keep the row config in `cordis.patch.yml`, forward it to `ctx.agentPresets.register()`, and give each preset row its own Plugins-page label; do not mount the shared `@deepseek-ai/dsh-agent-preset` for these rows.
123
+ - `tests/` — add a regression test for every behavior change. The offline suite is `tests/*.test.mjs`; an invariant over a pure domain function belongs in `tests/property/*.prop.mjs` (fast-check) rather than a hand-rolled randomized loop, and `tests/no-random.test.mjs` enforces that. Live-runtime scripts are documented in `tests/README.md`.
120
124
  - `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
125
  - `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.
122
126
 
123
127
  ## Pull request checklist
124
128
 
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.
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.
126
130
  2. New behavior has a test. Bug fixes should add a test that fails before the fix.
127
131
  3. No secrets, tokens, personal paths, session logs, or generated artifacts are added. `.gitignore` covers local acceptance output; do not force-add it.
128
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.
@@ -139,7 +143,9 @@ the push gates; CI schedules it nightly.
139
143
  `docs/reference/component-surface.md` (`npm run docs:generate:components`), and must pass
140
144
  `npm run docs:check:components`. A new visible capability row also ships its own
141
145
  `<specifier>/locale/*.json` row label via `npm run docs:generate:locale` and passes
142
- `npm run docs:check:generated:locale` (see
146
+ `npm run docs:check:generated:locale`; a preset-declaring row does the same through
147
+ `PRESET_LOCALES`, and every generated row label (capability or preset) must keep
148
+ `title`/`summary`/`disables` in both locales (see
143
149
  [architecture/overview.md](docs/architecture/overview.md#row-display-metadata-121)).
144
150
 
145
151
  ## Mount smoke
package/GLOSSARY.md ADDED
@@ -0,0 +1,67 @@
1
+ # GLOSSARY
2
+
3
+ The canonical bilingual vocabulary for Tower's user-facing copy. Plain language
4
+ first: a component label says what a person can do, then what changes when the
5
+ component is off. The reader should not need to know a config key, a tool id or
6
+ an internal module name.
7
+
8
+ This file is enforced. `npm run docs:check:components` fails when a label field
9
+ in `locale/`, the generated `locale/<specifier>/` row labels or
10
+ `ui-bundle/locale/` uses a different Chinese term, leaves the English word
11
+ untranslated, or drifts from the table below. The gate that enforces it is
12
+ described in [ADR 0005](docs/adr/0005-component-surface.md) (wave-4 addendum,
13
+ #137).
14
+
15
+ ## Canonical vocabulary
16
+
17
+ One term per concept, in both languages. Use these in every label, summary and
18
+ off-state sentence.
19
+
20
+ | Concept | English | 中文 | Notes |
21
+ |---|---|---|---|
22
+ | mission | mission | 任务线 | The unit of work a Tower session plans, executes and merges. Reserve 任务 for a single task inside a mission. |
23
+ | task | task | 任务 | One checklist item inside a mission. Not a substitute for 任务线. |
24
+ | batch | batch | 批次 | A finite evidence batch. Do not mix 批量 into label copy; that word describes bulk actions, not the component. |
25
+ | doctor | doctor | 体检 | The read-only health check. Do not use 诊断 as a noun for the health tool. |
26
+ | board | board | 看板 | The human-readable mission board (`MISSIONS.md`). |
27
+ | archive | archive | 归档 | Moving finished missions out of the live state file. |
28
+ | preset | preset | 预设 | A shipped role definition. Never leave the English word in Chinese copy. |
29
+ | worktree | worktree | 工作树 | The isolated checkout a worker runs in. |
30
+ | review | review | 审查 | The review contract and the act of reviewing. Do not use 评审 in plugin-page copy. |
31
+ | lease | lease | 租约 | Advisory resource coordination. Never 咨询式租约: the lease coordinates, it does not consult. |
32
+ | worker | worker | 执行代理 | The role that executes a mission. "Tower Worker" is a product name and stays as-is. |
33
+ | reviewer | reviewer | 审查代理 | The role that independently reviews a mission. "Tower Reviewer" is a product name and stays as-is. |
34
+
35
+ ## Product names and other surfaces
36
+
37
+ Brand and product names are not translated: Tower, DSH, Tower Worker, Tower
38
+ Reviewer, Tower UI, MISSIONS.md. On first use in a sentence, gloss an unfamiliar
39
+ one parenthetically — for example "Tower Codex (Tower's earlier state format)".
40
+
41
+ The Web client has its own string catalogue (`ui-bundle/client.locales.js`).
42
+ When its wording disagrees with this table, this table wins and the client copy
43
+ is migrated separately under the same glossary.
44
+
45
+ ## Copy rules
46
+
47
+ 1. One sentence for the capability, one sentence for the off consequence.
48
+ 2. No config-key syntax (`gates.requireTests:true`), no bare tool ids
49
+ (`tower_batch`), no slash commands (`/tower reload`) and no module specifiers
50
+ in a label field. Describe the capability and the effect instead.
51
+ 3. Say "Tower mode", not just "模式", and "refused with an explanation", not
52
+ "locked": the user needs to know they will get the reason.
53
+ 4. The off sentence must be truthful. A lease is coordination only, but a worker
54
+ configured to require one cannot start while the lease row is off — planning
55
+ and Tower-mode entry are refused with an explanation until the setting is
56
+ cleared or the row is re-enabled. Say that, and say that everything else keeps
57
+ working.
58
+ 5. English and Chinese must agree field by field. Chinese copy reads as Chinese:
59
+ no untranslated English common nouns, no machine-translated calques — say
60
+ 关闭该批次, not 收口; 当前状态文件, not 在线状态文件; 某一具体工作, not
61
+ 一项命名工作; and 卡住的锁如何恢复, not 锁状态的恢复出口.
62
+ 6. When turning a row off makes an operation impossible under the current
63
+ settings, name that operation (planning, entering Tower mode, merging) and how
64
+ the user clears the requirement. Never claim a blocked operation keeps
65
+ working: a reviewer preset needed for merging is a precondition, not an
66
+ optional extra, and test evidence that cannot be recorded keeps merges blocked
67
+ until the requirement is cleared.
package/README.md CHANGED
@@ -125,6 +125,12 @@ tower_teardown repoRoot=D:\work\repo
125
125
 
126
126
  图片与文件附件会随指令一起作为一条完整的 `UserMessage` 转发。
127
127
 
128
+ 运行时前置条件(preset 行、组件行与配置的冲突)只在**进入**模式的路径上重新校验:
129
+ 裸 `/tower`、`/tower on`、`/tower on <指令>`,以及模式关闭时的 `/tower <指令>`。已经开启
130
+ 模式后的 `/tower <指令>`、`/tower off`、`/tower off <指令>` 与 `/tower ack` 不再重复校验,
131
+ 因此会话中途关掉某一行之后,人仍然退得出去、指令也仍然转发得出去;`tower_spawn`、规划与
132
+ 合并/预检各自保留自己的护栏。详见 [组件行与消息语言](docs/guide/quickstart.md#component-rows-runtime-preconditions-and-message-language)。
133
+
128
134
  ## 组件模型
129
135
 
130
136
  本 bundle 采用 **强制 host 锚点 + 可单独启停的可选行**(#121):`cordis.patch.yml` 插入顶层
package/app/archive.js CHANGED
@@ -94,6 +94,21 @@ export function archiveView(index) {
94
94
  }
95
95
  }
96
96
 
97
+ /**
98
+ * #135: archival is the `tower-archive` component row, not a config switch. The
99
+ * probe is supplied by the composition root. A Tower built without a component
100
+ * probe (unit fixtures, direct adapter use) keeps archiving: an absent probe means
101
+ * "not composed", not "row disabled".
102
+ *
103
+ * @param {any} tower
104
+ * @returns {boolean}
105
+ */
106
+ function archiveEnabled(tower) {
107
+ const components = tower.deps?.components
108
+ if (!components || typeof components.enabled !== 'function') return true
109
+ return components.enabled('tower-archive') === true
110
+ }
111
+
97
112
  /**
98
113
  * Move the eligible terminal missions out of `state` and into the archive.
99
114
  * Mutates `state` in memory (missions, linked reviews and closed findings) and
@@ -105,8 +120,8 @@ export function archiveView(index) {
105
120
  * @returns {Promise<{ archived: string[], written: boolean, files?: string[], error: string | null, year?: number }>}
106
121
  */
107
122
  export async function sweepArchive(tower, state, options = {}) {
123
+ if (!archiveEnabled(tower)) return { archived: [], written: false, error: null }
108
124
  const policy = tower.config.archive ?? {}
109
- if (policy.enabled === false) return { archived: [], written: false, error: null }
110
125
  const now = options.now ?? Date.now()
111
126
  const activity = options.activity ?? {}
112
127
 
package/app/board.js CHANGED
@@ -8,8 +8,23 @@ export function boardFile(tower) {
8
8
  : path.join(tower.paths.dir, 'MISSIONS.md')
9
9
  }
10
10
 
11
+ /**
12
+ * #133: the board is the `tower-board` component row, not a config switch. The
13
+ * probe is supplied by the composition root. A Tower built without a component
14
+ * probe (unit fixtures, direct adapter use) keeps writing the board: an absent
15
+ * probe means "not composed", not "row disabled".
16
+ *
17
+ * @param {any} tower
18
+ * @returns {boolean}
19
+ */
20
+ export function boardEnabled(tower) {
21
+ const components = tower.deps?.components
22
+ if (!components || typeof components.enabled !== 'function') return true
23
+ return components.enabled('tower-board') === true
24
+ }
25
+
11
26
  export async function writeBoard(tower) {
12
- if (tower.config.docs?.board === false) return null
27
+ if (!boardEnabled(tower)) return null
13
28
  try {
14
29
  const state = await tower.load()
15
30
  const index = await tower.archiveIndex()
package/app/gate.js CHANGED
@@ -91,6 +91,19 @@ export async function evaluateGate(tower, input) {
91
91
 
92
92
  const branch = worktree ? worktree.branch : null
93
93
  const latest = latestReview(reviewsForBranch(reviews, branch))
94
+ // #140: `mergeGateBlockers` is pure and may not read the component registry, so
95
+ // the composition root publishes `deps.components` and this layer snapshots the
96
+ // one fact the gate needs. Absent (unit tests, custom port bags) means unknown,
97
+ // and the blocker keeps its generic wording.
98
+ const components = /** @type {{ enabled?: (id: string) => boolean, reason?: (id: string) => string } | undefined} */ (
99
+ tower.deps?.components
100
+ )
101
+ const testsComponent = components
102
+ ? {
103
+ enabled: typeof components.enabled === 'function' ? components.enabled('tower-tests') === true : false,
104
+ reason: typeof components.reason === 'function' ? components.reason('tower-tests') : 'disabled by component tower-tests',
105
+ }
106
+ : null
94
107
  for (const blocker of mergeGateBlockers({
95
108
  mission,
96
109
  roster: state.roster ?? [],
@@ -102,6 +115,7 @@ export async function evaluateGate(tower, input) {
102
115
  parentStatus: input.parentStatus,
103
116
  inactiveStates: ROSTER_INACTIVE_STATES,
104
117
  rebaseWaived: latest ? tower.rebaseWaived(mission, latest) : false,
118
+ testsComponent,
105
119
  })) push(blocker.code, blocker.detail)
106
120
 
107
121
  const checkoutBranch = await tower.deps.vcs.currentBranch(tower.root)
@@ -132,10 +132,12 @@ export async function precheckWorkerOwnership({
132
132
  /**
133
133
  * Service/provider/preset/lease checks, run before provisioning.
134
134
  * `inspectLeases` is injected by the composition root so the app layer never
135
- * imports the lease adapter (#69).
136
- * @param {{ tower: any, ctx: any, config: any, preset: string, actor: any, exec: any, inspectLeases: any }} input
135
+ * imports the lease adapter (#69). `leaseComponent` is the matching probe for the
136
+ * `tower-leases` row (#131): when the row is off there is no `tower_resource`
137
+ * tool, so a missing lease cannot be fixed by following the old advice.
138
+ * @param {{ tower: any, ctx: any, config: any, preset: string, actor: any, exec: any, inspectLeases: any, leaseComponent?: () => { enabled: boolean, reason: string } }} input
137
139
  */
138
- export async function precheckSpawnServices({ tower, ctx, config, preset, actor, exec, inspectLeases }) {
140
+ export async function precheckSpawnServices({ tower, ctx, config, preset, actor, exec, inspectLeases, leaseComponent }) {
139
141
  const subagents = ctx.get('subagents')
140
142
  if (!subagents) throw new TowerToolError('spawn-unavailable', 'subagents service is unavailable; cannot spawn')
141
143
  const providers = subagents.list()
@@ -150,6 +152,19 @@ export async function precheckSpawnServices({ tower, ctx, config, preset, actor,
150
152
  }
151
153
  if (typeof config.spawn.requireLease === 'string' && config.spawn.requireLease.trim()) {
152
154
  const requiredLease = config.spawn.requireLease.trim()
155
+ // #131: registration-aware. Telling a caller to "acquire it with
156
+ // tower_resource" is useless when the tower-leases row is off and the tool is
157
+ // not registered; name the component and both remedies instead.
158
+ const leasesRow = typeof leaseComponent === 'function' ? leaseComponent() : null
159
+ if (leasesRow && leasesRow.enabled !== true) {
160
+ // Refuse before reading the ledger: a lease left over from when the row was
161
+ // on must not let a spawn through, and the ledger cannot be fixed without
162
+ // the unregistered tool (issue #131, evidence note 4).
163
+ throw new TowerToolError(
164
+ 'lease-busy',
165
+ `spawn.requireLease="${requiredLease}" cannot be satisfied: ${leasesRow.reason ?? 'the tower-leases component is disabled'}, so tower_resource is not registered; re-enable the tower-leases row or clear spawn.requireLease`,
166
+ )
167
+ }
153
168
  const info = await inspectLeases(tower.commonDir)
154
169
  const lease = info.leases.find((entry) => entry.name === requiredLease && entry.expired !== true)
155
170
  if (!lease) {
package/cordis.patch.yml CHANGED
@@ -8,11 +8,13 @@
8
8
  #
9
9
  # Policy: principle 11 in PRINCIPLES.md + docs/adr/0005-component-surface.md.
10
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.
11
+ # scripts/component-surface.json. `npm run docs:check:components` reads every
12
+ # shipped patch (this file and ui-bundle/cordis.patch.yml, which ships the
13
+ # tower-ui row) and refuses a row with no classification, a missing locale label,
14
+ # or a moved component count without a written decision. `tower_doctor` and
15
+ # `tower_recover` are part of the host and are never switches (#122); a capability
16
+ # row owns exactly one optional capability, and `tower_status` / `tower_doctor`
17
+ # name any row that is off.
16
18
  - insert:
17
19
  # Mandatory host anchor. Not an optional capability: off == uninstall.
18
20
  - id: tower
@@ -33,8 +35,12 @@
33
35
  interruptDefault: true
34
36
  spawn:
35
37
  staleMs: 15000
38
+ # Each preset row mounts its own Tower-owned declaration module, not the
39
+ # shared @deepseek-ai/dsh-agent-preset: DSH labels a row from
40
+ # <specifier>/locale/<lang>.json, so two rows sharing one module would
41
+ # render the same title. The row config is unchanged.
36
42
  - id: preset-tower-worker
37
- name: '@deepseek-ai/dsh-agent-preset'
43
+ name: 'dsh-tower/preset-worker'
38
44
  config:
39
45
  id: tower-worker
40
46
  name: Tower Worker
@@ -62,7 +68,20 @@
62
68
  name: 'dsh-tower/leases'
63
69
  - id: tower-tests
64
70
  name: 'dsh-tower/tests'
65
- # dev hot reload: shipped off. Enable the row for a linked dev profile.
71
+ # #133: the MISSIONS.md board is a capability, not the old `docs.board` key.
72
+ # Off writes no board and leaves an existing one to go stale; every tool and
73
+ # gate keeps working. `docs.boardLocation` stays genuine tuning.
74
+ - id: tower-board
75
+ name: 'dsh-tower/board'
76
+ # #135: archival is a capability, not the old `archive.enabled` key. Off keeps
77
+ # terminal missions hot in state.json, so the hot state keeps growing; merges,
78
+ # gates and every other capability keep working and nothing is deleted.
79
+ # `archive.afterDays` / `archive.maxMissions` stay genuine tuning.
80
+ - id: tower-archive
81
+ name: 'dsh-tower/archive'
82
+ # #141: developer tooling, not an end-user capability switch. It stays shipped
83
+ # off; the locale copy states that this row exists for linked-profile
84
+ # development only, so the plugin page does not read as a missing feature.
66
85
  - id: tower-hmr
67
86
  name: 'dsh-tower/hmr'
68
87
  disabled: true
@@ -70,7 +89,7 @@
70
89
  # review.requireReview is false; with the gate on planning/mode entry refuse
71
90
  # the combination with an explanatory error.
72
91
  - id: preset-tower-reviewer
73
- name: '@deepseek-ai/dsh-agent-preset'
92
+ name: 'dsh-tower/preset-reviewer'
74
93
  config:
75
94
  id: tower-reviewer
76
95
  name: Tower Reviewer
@@ -30,7 +30,9 @@ note for each niche key.
30
30
 
31
31
  - Growth is visible in a diff instead of arriving one reasonable-looking key at a
32
32
  time.
33
- - A reduction is cheap: the baseline can be ratcheted down with no ADR.
33
+ - A reduction is cheap for `sections` and `tools`: the baseline can be ratcheted
34
+ down with no ADR. Since #138 the config-key count is two-way like the component
35
+ count (see [ADR 0005](0005-component-surface.md)).
34
36
  - The classification table is auditable, not decorative: the check fails when a
35
37
  live key or tool is missing from it.
36
38
  - The numbers are three lines in `tower_doctor` output, not a new dashboard.
@@ -53,8 +55,10 @@ extends the budget with a fourth — the number of user-visible component rows i
53
55
  `cordis.patch.yml` — and records it as its own `components` baseline entry with a
54
56
  two-way decision rule (a contraction needs a decision just like growth). The
55
57
  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
+ component count is validated by `npm run docs:check:components`. ADR 0005's wave
59
+ 3 (#138) also makes `configKeys` two-way, so the cheap-shrink consequence above
60
+ now applies only to `sections` and `tools`. The historical decision above is
61
+ unchanged: "three surfaces" describes the 2.3.0 rule.
58
62
 
59
63
  ## Rejected alternatives
60
64
 
@@ -65,6 +69,8 @@ decision above is unchanged: "three surfaces" describes the 2.3.0 rule.
65
69
  - **A per-key count in each generated reference** — two references can disagree;
66
70
  one shared baseline plus one check keeps the counts equal.
67
71
  - **Only an ADR requirement** — a pure deletion would need paperwork even though it
68
- shrinks the surface; the matching-deletion path keeps reductions cheap.
72
+ shrinks the surface; the matching-deletion path keeps `sections`/`tools`
73
+ reductions cheap. The config-key surface is the exception since #138: a removed
74
+ switch is itself a capability decision, so it needs a dated record (ADR 0005).
69
75
  - **A GitHub issue label instead of a repo file** — the brake must run offline in
70
76
  `npm test` and in CI without a token.
@@ -128,3 +128,169 @@ this wave makes the shipped surface obey it instead of carrying debt.
128
128
  - **A GitHub issue label or a docs-only policy.** The brake must run offline in
129
129
  `npm test` and in CI without a token, and the policy must survive the closure
130
130
  of the issue that introduced it.
131
+
132
+ ## Addendum (wave 2): board, archive, UI and explicit development tooling
133
+
134
+ - Date: 2026-11-02
135
+ - Decisions: #132, #133, #134, #135, #141
136
+
137
+ Four components were component-shaped but sat outside the policy: `docs.board` and
138
+ `archive.enabled` were booleans in the config form, `tower-ui` shipped as a real
139
+ plugin-page row that no source-of-truth entry described, and the group rows never
140
+ stated their off consequence where a reader sees it. Wave 2 closes those gaps
141
+ without widening the mechanism.
142
+
143
+ - **`tower-board` (#133).** `docs.board` is deleted; the board is the optional
144
+ `tower-board` row with its own capability module. Its fallback: no `MISSIONS.md`
145
+ is written or refreshed, an existing board goes stale, and every state tool and
146
+ gate keeps working. `docs.boardLocation` stays a tuning key because it only
147
+ chooses a path. With the row off, the composition behaves like an all-Tower-off
148
+ profile for the board (nothing is written) while the rest of Tower keeps working.
149
+ - **`tower-archive` (#135).** `archive.enabled` is deleted; archival is the
150
+ optional `tower-archive` row. Its fallback is deliberately honest: terminal
151
+ missions stay hot in `state.json`, the hot state keeps growing, and `status` /
152
+ `doctor` keep loading their full history; merges and gates are unaffected and
153
+ nothing is deleted. `archive.afterDays` / `archive.maxMissions` stay tuning.
154
+ - **Removed keys fail loudly.** A config that still sets `docs.board` or
155
+ `archive.enabled` is rejected with `config-unknown-key` and an upgrade note that
156
+ names the replacing row and points here. There is no silent alias: a switch that
157
+ moved to the plugin page must not keep working from a config file.
158
+ - **`tower-ui` is classified (#134).** The UI plugin row is now a visible
159
+ component with a documented fallback (the web UI plugin does not load: no tool
160
+ cards, composer-dock summary or status strip; host tools, missions and the board
161
+ are unaffected). Because it ships in `ui-bundle/`, the gate reads **every**
162
+ shipped `cordis.patch.yml` (root and `ui-bundle/`), classifies every visible row
163
+ and enforces a label source for each: either a generated `<specifier>/locale`
164
+ row, or the row package's own `meta` label when the labels live there. A row
165
+ added to any shipped patch without a source-of-truth entry fails
166
+ `npm run docs:check:components`.
167
+ - **A group row states its consequence where it is read (#132).** The plugin page
168
+ renders the group row from the package `meta` block, so the off-equals-uninstall
169
+ consequence lives in the root `meta.description` in both languages, and the gate
170
+ pins it there instead of trusting only the `components` block. This is a label
171
+ fix, not a new mechanism.
172
+ - **`tower-hmr` stays shipped off (#141).** Moving it to a separate install path
173
+ would hide the switch and re-open the orphan-mount hazard; the row is kept as
174
+ explicit development tooling instead: its locale copy and its source-of-truth
175
+ fallback say that it exists for a linked dev profile, that a normal profile
176
+ behaves exactly as before, and that it is off on purpose.
177
+ - **Count decision.** The visible component count moved 8 → 9 → 10 → 11 across
178
+ #133, #135 and #134; the three dated steps are recorded in
179
+ `scripts/surface-baseline.json`. The config surface contracted 55 → 53, which the
180
+ other surfaces allow without a decision record.
181
+
182
+ ## Addendum (wave 3): close the gate's blind spots
183
+
184
+ - Date: 2026-11-03
185
+ - Decisions: #138
186
+
187
+ Wave 2 made the gate read every shipped row, but four ways to evade it remained:
188
+ a class was free text, the operational route table was scraped with a regex, only
189
+ the patches named in `COMPONENT_PATCH_FILES` were read, and only booleans were
190
+ reviewed. Wave 3 closes them without changing the shipped decisions: 53 config
191
+ keys, 11 visible components.
192
+
193
+ - **The class is checked against the key's shape (#138.1).** A boolean that
194
+ removes a capability cannot file as `optional-config` by assertion. Every live
195
+ boolean must be `core`, `optional-component` or `forbidden-to-disable`, or an
196
+ `optional-config` key carrying a dated `exemption` whose reason names its
197
+ tuning-only nature. The same rule covers the non-boolean behaviour knobs below.
198
+ An exemption is only legal on a reviewed boolean/knob, and its `ref` must exist.
199
+ - **The config-key count is two-way (#138.1).** Wave 2's closing note ("the config
200
+ surface contracted 55 → 53, which the other surfaces allow without a decision
201
+ record") is retracted. Removing a switch is a capability decision, so
202
+ `configKeys` now joins `components` in `SURFACE_TWO_WAY_KEYS`: any later increase
203
+ or decrease needs a dated decision in `scripts/surface-baseline.json`. The gate
204
+ requires the `date` field itself (`YYYY-MM-DD`) on every decision of a two-way
205
+ surface, so a bare from/to record fails even when its chain reaches the value
206
+ (review of #138). The 64 → 53 history is recorded as one dated decision there;
207
+ sections and tools keep the ADR 0004 cheap-shrink rule.
208
+ - **The route table is imported, not scraped (#138.2).** `host/tool-registry.js`
209
+ exports `OPERATIONAL_TOOLS`, and the gate imports that module instead of
210
+ regex-matching its source. A missing file, a failed import, or a missing/non-Map
211
+ export is `registry-unreadable` and fails the gate. An empty Map is still the
212
+ correct determined state (#122): "nothing is gated" is distinct from "could not
213
+ read".
214
+ - **Every shipped patch is discovered (#138.3).** The gate walks the package for
215
+ `cordis.patch.yml` and compares the result with `COMPONENT_PATCH_FILES` in both
216
+ directions. An unlisted patch fails `patch-unclassified`; a listed patch that is
217
+ missing fails `patch-missing`. The row checks stay bidirectional: every shipped
218
+ row needs a source-of-truth entry, and every source-of-truth row needs a shipped
219
+ row.
220
+ - **Behaviour knobs and row overrides are reviewed (#138.4).** `spawn.requireLease`
221
+ and `tests.admission` are recorded in `behaviorKnobs` and carry tuning-only
222
+ exemptions: the optional `tower-leases` / `tower-tests` row owns the capability,
223
+ while these keys choose the safety level or timing inside it. A row's scalar
224
+ `config:` overrides are now parsed and must name a live config key and be listed
225
+ in that component's `allowedOverrides`. A row that sets `docs.board`,
226
+ `archive.enabled` or `tests.enabled` therefore fails as `row-override-unknown`
227
+ (those capabilities are rows), a row that enables or disables a key it does not
228
+ own fails as `row-override-foreign`, and `allowedOverrides` may never name a
229
+ forbidden-to-disable key. Fixtures reproduce the #133/#135/#136 shapes and a
230
+ foreign override, proving they fail before the fix passes.
231
+
232
+ ## Addendum (wave 4): broad-audience labels and a copy gate
233
+
234
+ - Date: 2026-11-04
235
+ - Decisions: #137
236
+
237
+ Wave 3 made the gate fail closed, but every label it admitted still passed. The
238
+ plugin page leaked config keys (`gates.requireTests:true`, `review.requireReview`)
239
+ and bare tool ids (`tower_batch`, `tower_resource`) into the off-state copy, the
240
+ Chinese copy mixed 咨询式租约 / 重型工作 / untranslated `mission` and `preset`, and
241
+ the Web UI used 任务线 while the plugin page left `mission` in English. The counts
242
+ do not move: 53 config keys, 13 components, 11 visible.
243
+
244
+ - **The labels are rewritten for a reader, not for an operator (#137.1).** Every
245
+ visible row, the group row and the UI row now say what the capability is for
246
+ and what stops when it is off, in plain language. Config keys, tool ids, slash
247
+ commands and module specifiers are gone from `title`, `summary`, `disables` and
248
+ the two `meta` descriptions. The off copy says "refused with an explanation",
249
+ not "locked", and names Tower mode. The `tower-leases` fix also removes the F19
250
+ claim that "nothing else depends on a lease": a lease is coordination only, but
251
+ a worker configured to require one cannot start while the row is off; planning
252
+ and Tower-mode entry are refused with an explanation until the setting is
253
+ cleared or the row is re-enabled, and everything else keeps working.
254
+ - **One bilingual vocabulary, recorded (#137.2).** `GLOSSARY.md` is the single
255
+ table for mission/任务线, batch/批次, doctor/体检, board/看板, archive/归档,
256
+ preset/预设, worktree/工作树, review/审查, lease/租约, worker/执行代理 and
257
+ reviewer/审查代理. It also records the copy rules (two sentences, no
258
+ identifiers, truthful off effect, natural Chinese) and is a gate input: a label
259
+ that uses a term the table does not pair with its other-language field fails.
260
+ The Web client's own string catalogue shares this table but is migrated
261
+ separately; the runtime error copy in `host/presets.js` is likewise a follow-up.
262
+ - **The copy is checked mechanically (#137.3).** `checkLocaleCopy` runs over the
263
+ root locale pair, the ui-bundle meta pair and every generated row-locale pair.
264
+ It fails on config-key syntax, `tower_*` ids, `/tower` commands, `dsh-tower/*`
265
+ specifiers, empty fields, zh/en path drift and glossary mismatches in both
266
+ directions; the Chinese side may not leave glossary words untranslated.
267
+ `checkMetaRowCopy` pins the hand-written ui-bundle `meta.title` / `description`
268
+ to the root `components['tower-ui']` entry and the same composition the
269
+ generator renders, so the one row that ships its own label cannot drift.
270
+ `checkGlossaryFile` keeps `GLOSSARY.md` present and complete. Negative
271
+ fixtures in `tests/locale-copy.test.mjs` prove a raw key, a bare tool id and a
272
+ term mismatch fail in both languages before the fixed copy passes.
273
+ - **Review of #137 corrected three claims and hardened the guard.** The
274
+ `tower-tests` off copy promised that "merges … keep working", but
275
+ `domain/gate.js` emits the `tests-missing` merge blocker while the test
276
+ requirement is on and `host/presets.js` refuses planning and Tower-mode entry,
277
+ so the copy now says planning and entry are refused and merges stay blocked
278
+ until the requirement is cleared. The reviewer preset summary said "only used
279
+ when review before merging is required"; the row is usable while review is
280
+ optional and becomes a precondition for merging only when the requirement is
281
+ on, so the summary says that instead. The Chinese calques 锁状态的恢复出口,
282
+ 收口, 一项命名工作, 在线状态文件 and 关闭该行 were replaced with 卡住的锁如何
283
+ 恢复, 关闭该批次, 某一具体工作, 当前状态文件 and 关闭这一项. The guard now
284
+ normalizes full-width punctuation and matches case-insensitively, so
285
+ `Gates.requireTests`, `Tower_batch` and `gates。requireTests` fail; the worker
286
+ and reviewer role nouns accept an initial capital but exclude the product names
287
+ "Tower Worker" and "Tower Reviewer", in both the en→zh and zh→en directions.
288
+ `GLOSSARY.md` ships in the package (`package.json` `files`, pinned by
289
+ `scripts/pack-manifest.txt`) because it is a gate input, not repository-only
290
+ prose.
291
+ - **Rejected: a prose style guide with no gate.** It is what let wave 1–3 pass;
292
+ a reviewer's attention does not survive the next rename.
293
+ - **Rejected: translating the developer reference table.** The fallback text in
294
+ `scripts/component-surface.json` and the generated
295
+ `docs/reference/component-surface.md` is an operator reference for auditors, not
296
+ a plugin-page label. It may name keys and tools; the locale fields may not.