dsh-tower 2.4.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@
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
+
7
17
  ## 2.4.0(2026-10-24)
8
18
 
9
19
  ### 新增
package/COMPATIBILITY.md CHANGED
@@ -5,6 +5,7 @@ is shipped in the npm package; doctor reads it locally and never contacts a regi
5
5
 
6
6
  | dsh version | tag / commit | verified on | what was run |
7
7
  |---|---|---|---|
8
+ | 0.2.1-rc.2 | v2.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 |
8
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 |
9
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` |
10
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 |
package/CONTRIBUTING.md CHANGED
@@ -122,7 +122,7 @@ the push gates; CI schedules it nightly.
122
122
 
123
123
  ## Pull request checklist
124
124
 
125
- 1. `npm test` passes locally (612 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.
126
126
  2. New behavior has a test. Bug fixes should add a test that fails before the fix.
127
127
  3. No secrets, tokens, personal paths, session logs, or generated artifacts are added. `.gitignore` covers local acceptance output; do not force-add it.
128
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.
@@ -137,7 +137,10 @@ the push gates; CI schedules it nightly.
137
137
  same PR, and `npm run surface:check` passes. A visible component change also updates
138
138
  `scripts/surface-baseline.json` (`components`), `scripts/component-surface.json` and
139
139
  `docs/reference/component-surface.md` (`npm run docs:generate:components`), and must pass
140
- `npm run docs:check:components`.
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)).
141
144
 
142
145
  ## Mount smoke
143
146
 
@@ -21,6 +21,50 @@ allowed to be switches is a table lookup in
21
21
  to a tool that is not on the recorded allowlist (empty since #122) and any
22
22
  `cordis.patch.yml` row that defaults a forbidden key off.
23
23
 
24
+ ### Row display metadata (#121)
25
+
26
+ The Plugins page renders an optional row as `row.meta.title ?? row.moduleName`
27
+ plus `row.meta.description`. The Host fills `row.meta` by resolving
28
+ `<specifier>/locale/en.json` and then every sibling `<lang>.json` in that
29
+ directory, reading `meta.title` / `meta.description`; title falls back to the full
30
+ module specifier and description to empty when no resource resolves. That is why
31
+ the mandatory `tower` card explains itself (the package root ships
32
+ `locale/*.json`) while a bare subpath row would show only `dsh-tower/batch`.
33
+
34
+ Each optional capability therefore ships its own resolvable subpath resource:
35
+ `dsh-tower/skills/locale/{en,zh}.json` and the same for `batch`, `import`,
36
+ `leases`, `tests` and `hmr`. `package.json` `exports` maps every
37
+ `./<capability>/locale/*.json` to `./locale/<capability>/*.json`, and
38
+ `files: ["locale/"]` ships the nested tree, so the lookup matches the installed
39
+ package. The root `./locale/*.json` export stays so the Tower group keeps its own
40
+ `meta`.
41
+
42
+ The capability files are generated, never hand-written. `npm run
43
+ docs:generate:locale` renders them from the `components` block of
44
+ `locale/en.json` and `locale/zh.json` (the same source `docs:check:components`
45
+ audits): `meta.title` is the component `title`, and `meta.description` joins the
46
+ component `summary` (what the capability enables) with its `disables` text (what
47
+ turning it off removes) behind a per-language "When off / 关闭后" label. The
48
+ row is therefore decidable from the page alone. `npm run
49
+ docs:check:generated:locale` fails on drift, runs in `npm run check`, and is its
50
+ own step in the `test` CI job (`.github/workflows/test.yml`).
51
+
52
+ To label a new capability row:
53
+
54
+ 1. Classify it in `scripts/component-surface.json`, ship it in
55
+ `cordis.patch.yml`, and add `title` / `summary` / `disables` to the
56
+ `components` block of both `locale/en.json` and `locale/zh.json`.
57
+ 2. Add `{ specifier, component }` to `CAPABILITY_LOCALES` in
58
+ `scripts/generate-capability-locale.mjs`, the matching
59
+ `./<specifier>/locale/*.json` export in `package.json`, and the row to the
60
+ coverage map in `tests/capability-locales.test.mjs`.
61
+ 3. Run `npm run docs:generate:locale`, then regenerate
62
+ `scripts/pack-manifest.txt` with `npm run pack:check -- --write`.
63
+
64
+ The reviewer preset row is the one visible component that is not a package
65
+ specifier: its label comes from the `cordis.patch.yml` preset `config.name` /
66
+ `description`. The generated capability files never touch the root locale `meta`.
67
+
24
68
  ```
25
69
  Host plugin (index.js)
26
70
  ├─ /tower command + Tower mode (guard, prompt section, tower_mode/tower_control)
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Evidence batches",
4
+ "description": "Group missions into finite evidence batches and close them against required checks and receipts. When off: tower_batch is not registered; missions, gates and merges are unaffected."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "证据批次",
4
+ "description": "把 mission 归入有限证据批次,并按必需检查与回执关闭批次。关闭后:不再注册 tower_batch;mission、门禁与合并不受影响。"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Dev hot reload",
4
+ "description": "Register tower_reload and /tower reload for linked-profile development. When off: tower_reload and /tower reload are unavailable; a non-HMR profile behaves exactly as before. Shipped off."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "开发热重载",
4
+ "description": "为联动开发 profile 注册 tower_reload 与 /tower reload。关闭后:tower_reload 与 /tower reload 不可用;非 HMR profile 的行为与之前完全相同。随包默认关闭。"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Legacy import",
4
+ "description": "Import a Tower Codex legacy state file and audit its snapshot-to-mission mapping. When off: tower_import_legacy and tower_legacy_map are not registered; new state is unaffected."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "旧版导入",
4
+ "description": "导入 Tower Codex 旧状态文件,并审计快照到 mission 的映射。关闭后:不再注册 tower_import_legacy 与 tower_legacy_map;新状态不受影响。"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Advisory leases",
4
+ "description": "Coordinate heavy per-repository work with advisory resource leases. When off: tower_resource is not registered; nothing else depends on a lease."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "咨询式租约",
4
+ "description": "用咨询式资源租约协调每个仓库中的重型工作。关闭后:不再注册 tower_resource;其他能力不依赖租约。"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Tower skills",
4
+ "description": "Playbooks for worker/reviewer agents and recovery after a blocked merge. When off: The three Tower skills leave the catalog; the worker/reviewer protocol sections and every mission tool keep working."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Tower 技能包",
4
+ "description": "执行与审查代理的操作手册,以及失败后的恢复指南。关闭后:三个 Tower 技能从目录中移除;执行/审查协议段落与所有 mission 工具继续工作。"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Test discovery",
4
+ "description": "Run the worktree's declared test discovery and record a receipt for the mission tip. When off: tower_test is not registered and no receipt can be recorded; with gates.requireTests:true planning/mode entry is rejected with an explanation."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "测试发现",
4
+ "description": "运行 worktree 声明的测试发现,并为 mission tip 记录回执。关闭后:不再注册 tower_test,也无法记录回执;若 gates.requireTests:true,规划/进入模式会以解释性错误拒绝。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tower",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "type": "module",
5
5
  "description": "DSH-native Tower: mission planning, worktree isolation, review-gated merges, and a human-owned Tower mode.",
6
6
  "license": "MIT",
@@ -36,6 +36,12 @@
36
36
  "./leases": "./host/capability-leases.js",
37
37
  "./tests": "./host/capability-tests.js",
38
38
  "./hmr": "./host/capability-hmr.js",
39
+ "./skills/locale/*.json": "./locale/skills/*.json",
40
+ "./batch/locale/*.json": "./locale/batch/*.json",
41
+ "./import/locale/*.json": "./locale/import/*.json",
42
+ "./leases/locale/*.json": "./locale/leases/*.json",
43
+ "./tests/locale/*.json": "./locale/tests/*.json",
44
+ "./hmr/locale/*.json": "./locale/hmr/*.json",
39
45
  "./locale/*.json": "./locale/*.json",
40
46
  "./icon": "./icon.svg",
41
47
  "./package.json": "./package.json"
@@ -54,7 +60,7 @@
54
60
  "types/",
55
61
  "cordis.patch.yml",
56
62
  "scripts/",
57
- "locale/*.json",
63
+ "locale/",
58
64
  "icon.svg",
59
65
  "README.md",
60
66
  "README.en.md",
@@ -99,6 +105,8 @@
99
105
  "docs:check:generated:types": "node scripts/generate-types.mjs --check",
100
106
  "docs:generate:components": "node scripts/generate-component-surface.mjs",
101
107
  "docs:check:generated:components": "node scripts/generate-component-surface.mjs --check",
108
+ "docs:generate:locale": "node scripts/generate-capability-locale.mjs",
109
+ "docs:check:generated:locale": "node scripts/generate-capability-locale.mjs --check",
102
110
  "docs:check:components": "node scripts/check-components.mjs",
103
111
  "pack:check": "node scripts/check-pack.mjs",
104
112
  "architecture:check": "node scripts/architecture.mjs",
@@ -106,7 +114,7 @@
106
114
  "types:coverage": "node scripts/types-coverage.mjs",
107
115
  "types:strict:check": "node scripts/types-strict.mjs",
108
116
  "types:strict:write": "node scripts/types-strict.mjs --write",
109
- "check": "npm test && npm run docs:check && npm run docs:check:philosophy && npm run docs:check:generated && npm run docs:check:generated:tools && npm run docs:check:generated:config && npm run docs:check:generated:events && npm run docs:check:generated:types && npm run docs:check:generated:components && npm run docs:check:components && npm run surface:check && npm run pack:check && npm run architecture:check && npm run types:coverage && npm run types:strict:check && npm run types:check && npm run domain:check",
117
+ "check": "npm test && npm run docs:check && npm run docs:check:philosophy && npm run docs:check:generated && npm run docs:check:generated:tools && npm run docs:check:generated:config && npm run docs:check:generated:events && npm run docs:check:generated:types && npm run docs:check:generated:components && npm run docs:check:generated:locale && npm run docs:check:components && npm run surface:check && npm run pack:check && npm run architecture:check && npm run types:coverage && npm run types:strict:check && npm run types:check && npm run domain:check",
110
118
  "domain:metrics": "node scripts/domain-metrics.mjs",
111
119
  "domain:check": "node scripts/domain-metrics.mjs --check"
112
120
  },
@@ -10,6 +10,7 @@ import { execFileSync } from 'node:child_process'
10
10
  import fs from 'node:fs'
11
11
  import path from 'node:path'
12
12
  import { fileURLToPath } from 'node:url'
13
+ import { capabilityLocalePaths } from './generate-capability-locale.mjs'
13
14
 
14
15
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
15
16
  const REQUIRED_FILES = [
@@ -17,6 +18,9 @@ const REQUIRED_FILES = [
17
18
  'index.js',
18
19
  'locale/en.json',
19
20
  'locale/zh.json',
21
+ // #121: every optional capability row explains itself on the Plugins page
22
+ // through a subpath locale resource, so those generated files are required.
23
+ ...capabilityLocalePaths(),
20
24
  'ui-bundle/index.js',
21
25
  'ui-bundle/package.json',
22
26
  'ui-bundle/cordis.patch.yml',
@@ -0,0 +1,148 @@
1
+ // Generate the per-capability row locale files (#121).
2
+ //
3
+ // The DSH Plugins page renders an optional row as
4
+ // `row.meta.title ?? row.moduleName` plus `row.meta.description`, and the Host
5
+ // fills `row.meta` by resolving `<specifier>/locale/en.json` and then every
6
+ // sibling `<lang>.json` in that directory. A subpath row without such a resource
7
+ // therefore shows only `dsh-tower/batch`, and the switch is not decidable from
8
+ // the page.
9
+ //
10
+ // Those files are generated, not hand-written: the single source of truth is the
11
+ // `components` block of `locale/<lang>.json` that `npm run docs:check:components`
12
+ // already audits. `meta.title` is the component `title`; `meta.description`
13
+ // composes that component's `summary` (what the capability enables) with its
14
+ // `disables` text (what turning it off removes), so the row explains the switch
15
+ // in both directions.
16
+ //
17
+ // Usage:
18
+ // node scripts/generate-capability-locale.mjs [--root <dir>] [--check]
19
+ import fs from 'node:fs'
20
+ import path from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+ import { parseArgs, runMain } from './lib/cli.mjs'
23
+
24
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
25
+ const LANGUAGE_FILES = { en: 'locale/en.json', zh: 'locale/zh.json' }
26
+ const LANGUAGES = Object.keys(LANGUAGE_FILES)
27
+ // The off-effect marker is generated, not recovered from the hand-written
28
+ // `disables` prose, so the check can prove every description names what the
29
+ // switch removes without depending on one English or Chinese phrasing.
30
+ const OFF_EFFECT_LABEL = { en: 'When off:', zh: '关闭后:' }
31
+
32
+ /**
33
+ * The optional capability rows that need their own subpath locale resource, in
34
+ * the row order of `cordis.patch.yml`. The specifier is the module subpath the
35
+ * row mounts; the component id is the key in the locale `components` block.
36
+ */
37
+ export const CAPABILITY_LOCALES = [
38
+ { specifier: 'skills', component: 'tower-skills' },
39
+ { specifier: 'batch', component: 'tower-batch' },
40
+ { specifier: 'import', component: 'tower-import' },
41
+ { specifier: 'leases', component: 'tower-leases' },
42
+ { specifier: 'tests', component: 'tower-tests' },
43
+ { specifier: 'hmr', component: 'tower-hmr' },
44
+ ]
45
+
46
+ export const CAPABILITY_LOCALE_LANGUAGES = LANGUAGES
47
+
48
+ /** Repo-relative paths of every generated capability locale file. */
49
+ export function capabilityLocalePaths() {
50
+ return CAPABILITY_LOCALES.flatMap(({ specifier }) =>
51
+ LANGUAGES.map((lang) => `locale/${specifier}/${lang}.json`),
52
+ )
53
+ }
54
+
55
+ /** The `package.json` exports entry that exposes one capability's locale files. */
56
+ export function capabilityLocaleExport(specifier) {
57
+ return { key: `./${specifier}/locale/*.json`, target: `./locale/${specifier}/*.json` }
58
+ }
59
+
60
+ function requireText(value, label) {
61
+ const text = typeof value === 'string' ? value.trim() : ''
62
+ if (!text) throw new Error(`${label} must be a non-empty string`)
63
+ return text
64
+ }
65
+
66
+ /**
67
+ * Compose the display metadata for one capability row in one language.
68
+ * @param {{ title: string, summary: string, disables: string }} entry
69
+ * @param {'en' | 'zh'} lang
70
+ */
71
+ export function renderCapabilityMeta(entry, lang) {
72
+ const title = requireText(entry?.title, `${lang} capability title`)
73
+ const summary = requireText(entry?.summary, `${lang} capability summary`).replace(/\s+/g, ' ')
74
+ const disables = requireText(entry?.disables, `${lang} capability disables`).replace(/\s+/g, ' ')
75
+ // Chinese prose does not space CJK punctuation, so the label joins directly.
76
+ const space = lang === 'zh' ? '' : ' '
77
+ return {
78
+ meta: { title, description: `${summary}${space}${OFF_EFFECT_LABEL[lang]}${space}${disables}` },
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Render every generated file for one repository root.
84
+ * @param {string} root
85
+ * @returns {Map<string, string>} repo-relative path -> file text
86
+ */
87
+ export function renderCapabilityLocaleFiles(root) {
88
+ const locales = {}
89
+ for (const lang of LANGUAGES) {
90
+ const file = path.join(root, LANGUAGE_FILES[lang])
91
+ if (!fs.existsSync(file)) throw new Error(`${LANGUAGE_FILES[lang]} is missing under ${root}`)
92
+ locales[lang] = JSON.parse(fs.readFileSync(file, 'utf8'))
93
+ }
94
+ const files = new Map()
95
+ for (const { specifier, component } of CAPABILITY_LOCALES) {
96
+ for (const lang of LANGUAGES) {
97
+ const entry = locales[lang]?.components?.[component]
98
+ if (!entry) throw new Error(`${LANGUAGE_FILES[lang]} has no components.${component} entry`)
99
+ files.set(`locale/${specifier}/${lang}.json`, `${JSON.stringify(renderCapabilityMeta(entry, lang), null, 2)}\n`)
100
+ }
101
+ }
102
+ return files
103
+ }
104
+
105
+ function main(argv) {
106
+ const options = parseArgs(argv, {
107
+ initial: { root: PACKAGE_ROOT, check: false, help: false },
108
+ values: { '--root': { key: 'root', parse: path.resolve } },
109
+ flags: { '--check': 'check', '--help': 'help', '-h': 'help' },
110
+ })
111
+ if (options.help) {
112
+ console.log(`usage: node scripts/generate-capability-locale.mjs [--root <dir>] [--check]
113
+
114
+ --root repository root to read and write (default: this package)
115
+ --check fail when a committed file differs instead of writing it
116
+ --help print this text
117
+
118
+ The files are generated from the components block of locale/en.json and
119
+ locale/zh.json; do not edit locale/<capability>/<lang>.json by hand.`)
120
+ return 0
121
+ }
122
+ const root = path.resolve(options.root)
123
+ const files = renderCapabilityLocaleFiles(root)
124
+ if (options.check) {
125
+ const stale = []
126
+ for (const [relative, expected] of files) {
127
+ const target = path.join(root, relative)
128
+ const actual = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
129
+ if (actual !== expected) stale.push(relative)
130
+ }
131
+ if (stale.length > 0) {
132
+ console.error('capability locale files are stale; run npm run docs:generate:locale')
133
+ for (const relative of stale) console.error(` ${relative}`)
134
+ return 1
135
+ }
136
+ console.log(`capability row locales: ${files.size} files in sync (${CAPABILITY_LOCALES.length} specifiers, en/zh)`)
137
+ return 0
138
+ }
139
+ for (const [relative, text] of files) {
140
+ const target = path.join(root, relative)
141
+ fs.mkdirSync(path.dirname(target), { recursive: true })
142
+ fs.writeFileSync(target, text)
143
+ }
144
+ console.log(`wrote ${files.size} capability row locale files (${CAPABILITY_LOCALES.length} specifiers, en/zh)`)
145
+ return 0
146
+ }
147
+
148
+ await runMain(import.meta.url, () => main(process.argv.slice(2)), { errorPrefix: 'capability-locales' })
@@ -162,7 +162,19 @@ host/tool-registry.js
162
162
  host/tower.js
163
163
  icon.svg
164
164
  index.js
165
+ locale/batch/en.json
166
+ locale/batch/zh.json
165
167
  locale/en.json
168
+ locale/hmr/en.json
169
+ locale/hmr/zh.json
170
+ locale/import/en.json
171
+ locale/import/zh.json
172
+ locale/leases/en.json
173
+ locale/leases/zh.json
174
+ locale/skills/en.json
175
+ locale/skills/zh.json
176
+ locale/tests/en.json
177
+ locale/tests/zh.json
166
178
  locale/zh.json
167
179
  package.json
168
180
  ports/archive.js
@@ -187,6 +199,7 @@ scripts/check-philosophy.mjs
187
199
  scripts/component-surface.json
188
200
  scripts/domain-metrics.mjs
189
201
  scripts/enable-dev-hmr.mjs
202
+ scripts/generate-capability-locale.mjs
190
203
  scripts/generate-component-surface.mjs
191
204
  scripts/generate-config-reference.mjs
192
205
  scripts/generate-error-reference.mjs