dsh-tower 2.4.1 → 2.4.2
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 +11 -0
- package/COMPATIBILITY.md +1 -0
- package/CONTRIBUTING.md +7 -3
- package/cordis.patch.yml +6 -2
- package/docs/architecture/overview.md +47 -20
- package/docs/reference/component-surface.md +2 -2
- package/host/preset-reviewer.js +27 -0
- package/host/preset-worker.js +32 -0
- package/locale/en.json +7 -2
- package/locale/preset-reviewer/en.json +6 -0
- package/locale/preset-reviewer/zh.json +6 -0
- package/locale/preset-worker/en.json +6 -0
- package/locale/preset-worker/zh.json +6 -0
- package/locale/zh.json +7 -2
- package/package.json +5 -1
- package/scripts/check-components.mjs +61 -1
- package/scripts/check-pack.mjs +7 -3
- package/scripts/component-surface.json +2 -2
- package/scripts/generate-capability-locale.mjs +97 -21
- package/scripts/mount-smoke/probe.mjs +33 -3
- package/scripts/mount-smoke/surface.mjs +142 -1
- package/scripts/mount-smoke.mjs +24 -4
- package/scripts/pack-manifest.txt +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
## 未发布(Unreleased)
|
|
6
6
|
|
|
7
|
+
## 2.4.2(2026-10-24)
|
|
8
|
+
|
|
9
|
+
### 修复
|
|
10
|
+
|
|
11
|
+
- **两个 Agent 预设行在 Plugins 页面无法区分**:此前 `preset-tower-worker` 与 `preset-tower-reviewer`
|
|
12
|
+
共用 `@deepseek-ai/dsh-agent-preset` 模块,页面按模块包取标签,两行都显示「Agent 预设定义」。
|
|
13
|
+
现在两行分别由 Tower 自有的 import-free preset 声明模块承载(`dsh-tower/preset-worker` /
|
|
14
|
+
`dsh-tower/preset-reviewer`),通过注入的 `agentPresets.register(config)` 注册,
|
|
15
|
+
preset 的 id/name/description/order/plugins 保持不变;两个 specifier 各自提供生成的
|
|
16
|
+
`<specifier>/locale/{en,zh}.json`,页面显示「Tower Worker 预设」「Tower Reviewer 预设」
|
|
17
|
+
及各自用途与关闭影响。真实 `smoke:mount` 在 pinned host 上确认 2 个 preset 均挂载且标签不同。
|
|
7
18
|
## 2.4.1(2026-10-24)
|
|
8
19
|
|
|
9
20
|
### 修复
|
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.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
9
|
| 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
10
|
| 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
11
|
| 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
|
|
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.
|
|
@@ -116,13 +117,14 @@ the push gates; CI schedules it nightly.
|
|
|
116
117
|
- `index.js` — Host entry: `/tower` command, mode guard, dynamic prompt section, `tower_control`.
|
|
117
118
|
- `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
119
|
- `worker.js` — the `tower-worker` preset policy (tool surface + protocol text).
|
|
120
|
+
- `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.
|
|
119
121
|
- `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`.
|
|
120
122
|
- `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
123
|
- `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
124
|
|
|
123
125
|
## Pull request checklist
|
|
124
126
|
|
|
125
|
-
1. `npm test` passes locally (
|
|
127
|
+
1. `npm test` passes locally (631 tests today, including protocol end-to-end against a real temporary Git repository and mock spawn coverage) and `npm run docs:check` passes.
|
|
126
128
|
2. New behavior has a test. Bug fixes should add a test that fails before the fix.
|
|
127
129
|
3. No secrets, tokens, personal paths, session logs, or generated artifacts are added. `.gitignore` covers local acceptance output; do not force-add it.
|
|
128
130
|
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 +141,9 @@ the push gates; CI schedules it nightly.
|
|
|
139
141
|
`docs/reference/component-surface.md` (`npm run docs:generate:components`), and must pass
|
|
140
142
|
`npm run docs:check:components`. A new visible capability row also ships its own
|
|
141
143
|
`<specifier>/locale/*.json` row label via `npm run docs:generate:locale` and passes
|
|
142
|
-
`npm run docs:check:generated:locale
|
|
144
|
+
`npm run docs:check:generated:locale`; a preset-declaring row does the same through
|
|
145
|
+
`PRESET_LOCALES`, and every generated row label (capability or preset) must keep
|
|
146
|
+
`title`/`summary`/`disables` in both locales (see
|
|
143
147
|
[architecture/overview.md](docs/architecture/overview.md#row-display-metadata-121)).
|
|
144
148
|
|
|
145
149
|
## Mount smoke
|
package/cordis.patch.yml
CHANGED
|
@@ -33,8 +33,12 @@
|
|
|
33
33
|
interruptDefault: true
|
|
34
34
|
spawn:
|
|
35
35
|
staleMs: 15000
|
|
36
|
+
# Each preset row mounts its own Tower-owned declaration module, not the
|
|
37
|
+
# shared @deepseek-ai/dsh-agent-preset: DSH labels a row from
|
|
38
|
+
# <specifier>/locale/<lang>.json, so two rows sharing one module would
|
|
39
|
+
# render the same title. The row config is unchanged.
|
|
36
40
|
- id: preset-tower-worker
|
|
37
|
-
name: '
|
|
41
|
+
name: 'dsh-tower/preset-worker'
|
|
38
42
|
config:
|
|
39
43
|
id: tower-worker
|
|
40
44
|
name: Tower Worker
|
|
@@ -70,7 +74,7 @@
|
|
|
70
74
|
# review.requireReview is false; with the gate on planning/mode entry refuse
|
|
71
75
|
# the combination with an explanatory error.
|
|
72
76
|
- id: preset-tower-reviewer
|
|
73
|
-
name: '
|
|
77
|
+
name: 'dsh-tower/preset-reviewer'
|
|
74
78
|
config:
|
|
75
79
|
id: tower-reviewer
|
|
76
80
|
name: Tower Reviewer
|
|
@@ -23,7 +23,7 @@ to a tool that is not on the recorded allowlist (empty since #122) and any
|
|
|
23
23
|
|
|
24
24
|
### Row display metadata (#121)
|
|
25
25
|
|
|
26
|
-
The Plugins page renders
|
|
26
|
+
The Plugins page renders a row as `row.meta.title ?? row.moduleName`
|
|
27
27
|
plus `row.meta.description`. The Host fills `row.meta` by resolving
|
|
28
28
|
`<specifier>/locale/en.json` and then every sibling `<lang>.json` in that
|
|
29
29
|
directory, reading `meta.title` / `meta.description`; title falls back to the full
|
|
@@ -31,7 +31,7 @@ module specifier and description to empty when no resource resolves. That is why
|
|
|
31
31
|
the mandatory `tower` card explains itself (the package root ships
|
|
32
32
|
`locale/*.json`) while a bare subpath row would show only `dsh-tower/batch`.
|
|
33
33
|
|
|
34
|
-
Each optional capability therefore ships its own resolvable subpath resource:
|
|
34
|
+
Each optional capability row therefore ships its own resolvable subpath resource:
|
|
35
35
|
`dsh-tower/skills/locale/{en,zh}.json` and the same for `batch`, `import`,
|
|
36
36
|
`leases`, `tests` and `hmr`. `package.json` `exports` maps every
|
|
37
37
|
`./<capability>/locale/*.json` to `./locale/<capability>/*.json`, and
|
|
@@ -39,31 +39,56 @@ Each optional capability therefore ships its own resolvable subpath resource:
|
|
|
39
39
|
package. The root `./locale/*.json` export stays so the Tower group keeps its own
|
|
40
40
|
`meta`.
|
|
41
41
|
|
|
42
|
-
The
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
The two preset-declaring rows need the same treatment but cannot share a module.
|
|
43
|
+
DSH labels a row from its own `moduleName`, so while both rows mounted
|
|
44
|
+
`@deepseek-ai/dsh-agent-preset` they resolved one locale resource and rendered the
|
|
45
|
+
same title: the mandatory worker dependency and the conditional reviewer switch
|
|
46
|
+
were indistinguishable. That package is also supplied by the DSH profile, not by
|
|
47
|
+
this bundle, so Tower cannot add per-row locale files beside its label. Tower
|
|
48
|
+
therefore owns one tiny declaration module per preset row:
|
|
49
|
+
`host/preset-worker.js` (row specifier `dsh-tower/preset-worker`) and
|
|
50
|
+
`host/preset-reviewer.js` (`dsh-tower/preset-reviewer`). Each is a plain object
|
|
51
|
+
plugin with `inject: ['agentPresets']` whose async `apply` returns
|
|
52
|
+
`ctx.agentPresets.register(config)`; Cordis collects the resolved function as the
|
|
53
|
+
plugin's disposer, so disabling the row or an HMR unload unregisters the preset.
|
|
54
|
+
The registry still owns preset semantics — it mounts the definition's `plugins`
|
|
55
|
+
list itself — and the modules import nothing, so they are renderer-free and do
|
|
56
|
+
not depend on `@deepseek-ai/dsh-agent-preset`. In `cordis.patch.yml` only the two
|
|
57
|
+
`name:` lines changed; the id/name/description/order/plugins config blocks are
|
|
58
|
+
unchanged.
|
|
59
|
+
|
|
60
|
+
The row label files are generated, never hand-written. `npm run
|
|
61
|
+
docs:generate:locale` renders every row label — capability and preset — from the
|
|
62
|
+
`components` block of `locale/en.json` and `locale/zh.json` (the same source
|
|
63
|
+
`docs:check:components` audits): `meta.title` is the component `title`, and
|
|
64
|
+
`meta.description` joins the component `summary` (what the row provides) with its
|
|
65
|
+
`disables` text (what turning it off removes) behind a per-language "When off /
|
|
66
|
+
关闭后" label. The mandatory worker preset has no off state, so its description
|
|
67
|
+
uses a "Mandatory dependency, not a switch / 强制依赖,不是开关" label instead; the
|
|
68
|
+
conditional reviewer keeps the off-effect wording. The row is therefore decidable
|
|
69
|
+
from the page alone. `npm run docs:check:generated:locale` fails on drift, runs in
|
|
70
|
+
`npm run check`, and is its own step in the `test` CI job
|
|
71
|
+
(`.github/workflows/test.yml`). `npm run docs:check:components` also covers the
|
|
72
|
+
generated rows: a label whose `components` entry is missing, or whose `mandatory`
|
|
73
|
+
flag disagrees with the component scope, is refused there. The real
|
|
74
|
+
`npm run smoke:mount` calls the same host `readPluginMeta` the Plugins page uses
|
|
75
|
+
and fails unless the two preset rows resolve distinct title/description maps, so
|
|
76
|
+
the shared-label defect cannot return unnoticed.
|
|
77
|
+
|
|
78
|
+
To label a new row:
|
|
53
79
|
|
|
54
80
|
1. Classify it in `scripts/component-surface.json`, ship it in
|
|
55
81
|
`cordis.patch.yml`, and add `title` / `summary` / `disables` to the
|
|
56
82
|
`components` block of both `locale/en.json` and `locale/zh.json`.
|
|
57
|
-
2. Add `{ specifier, component }` to `CAPABILITY_LOCALES`
|
|
58
|
-
`
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
2. Add `{ specifier, component }` to `CAPABILITY_LOCALES` — or to
|
|
84
|
+
`PRESET_LOCALES` with `mandatory` matching the component scope for a
|
|
85
|
+
preset-declaring row — in `scripts/generate-capability-locale.mjs`, the
|
|
86
|
+
matching `./<specifier>/locale/*.json` export in `package.json`, and the row
|
|
87
|
+
to the coverage map in `tests/capability-locales.test.mjs`.
|
|
61
88
|
3. Run `npm run docs:generate:locale`, then regenerate
|
|
62
89
|
`scripts/pack-manifest.txt` with `npm run pack:check -- --write`.
|
|
63
90
|
|
|
64
|
-
The
|
|
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`.
|
|
91
|
+
The generated row files never touch the root locale `meta`.
|
|
67
92
|
|
|
68
93
|
```
|
|
69
94
|
Host plugin (index.js)
|
|
@@ -90,6 +115,8 @@ Host plugin (index.js)
|
|
|
90
115
|
host/modeStore.js per-agent Tower mode persistence
|
|
91
116
|
host/modeState.js owned in-memory mode state
|
|
92
117
|
host/presets.js cross-field preset validation before work starts
|
|
118
|
+
host/preset-worker.js mandatory worker preset declaration (dsh-tower/preset-worker)
|
|
119
|
+
host/preset-reviewer.js conditional reviewer preset declaration (dsh-tower/preset-reviewer)
|
|
93
120
|
host/compatibility.js COMPATIBILITY.md provenance
|
|
94
121
|
host/tower.js default-deps Tower binding for tests/embedders
|
|
95
122
|
ports (explicit runtime contracts, no implementation):
|
|
@@ -32,8 +32,8 @@ row is a section of its parent and is not independently switchable.
|
|
|
32
32
|
| `tower-leases` | visible | row | tower_resource is not registered; advisory leases are coordination only, so nothing else depends on them. |
|
|
33
33
|
| `tower-tests` | visible | row | tower_test is not registered and no test receipt can be recorded. With gates.requireTests:true planning/mode entry is rejected with an explanation, and the merge gate still reports tests-missing. |
|
|
34
34
|
| `tower-hmr` | visible | row | tower_reload and /tower reload are unavailable; a profile without HMR behaves exactly as before. Shipped disabled: enable the row for a linked dev profile. |
|
|
35
|
-
| `preset-tower-worker` | internal | row | tower_spawn fails without the worker preset. A mandatory dependency, never a switch. |
|
|
36
|
-
| `preset-tower-reviewer` | visible | row |
|
|
35
|
+
| `preset-tower-worker` | internal | row | Declared by dsh-tower/preset-worker: tower_spawn fails without the worker preset. A mandatory dependency, never a switch. |
|
|
36
|
+
| `preset-tower-reviewer` | visible | row | Declared by dsh-tower/preset-reviewer: the reviewer cannot be dispatched. Supported while review.requireReview is false (merge is unaffected); with the gate on planning/mode entry is locked with an explanatory error. |
|
|
37
37
|
|
|
38
38
|
## Forbidden to disable
|
|
39
39
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Tower reviewer preset declaration (#121): the read-only preset's own label.
|
|
2
|
+
//
|
|
3
|
+
// This is the reviewer half of the pair documented in `host/preset-worker.js`.
|
|
4
|
+
// The row used to mount `@deepseek-ai/dsh-agent-preset`, so DSH resolved that
|
|
5
|
+
// shared module's locale and both preset rows rendered the same title; the
|
|
6
|
+
// conditional reviewer switch was therefore indistinguishable from the
|
|
7
|
+
// mandatory worker dependency. A Tower-owned module gives each row its own
|
|
8
|
+
// `<specifier>/locale/<lang>.json` resource without importing the agent-preset
|
|
9
|
+
// package (which is not resolvable from this package anyway).
|
|
10
|
+
//
|
|
11
|
+
// The module forwards the unchanged row config to the injected `agentPresets`
|
|
12
|
+
// service and returns its disposer, so `register`/unregister lifecycle, the
|
|
13
|
+
// definition's `plugins` mount and the activation diagnostic all stay registry
|
|
14
|
+
// owned. It imports nothing and renders nothing.
|
|
15
|
+
export const name = 'dsh-tower/preset-reviewer'
|
|
16
|
+
|
|
17
|
+
export const inject = ['agentPresets']
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register the reviewer preset with the profile's preset registry.
|
|
21
|
+
* @param {{ agentPresets: { register: (config: any) => Promise<() => Promise<void> | void> } }} ctx
|
|
22
|
+
* @param {any} config the `cordis.patch.yml` row config (id/name/description/order/plugins)
|
|
23
|
+
* @returns {Promise<() => Promise<void> | void>} the registry's unregister disposer
|
|
24
|
+
*/
|
|
25
|
+
export async function apply(ctx, config) {
|
|
26
|
+
return ctx.agentPresets.register(config)
|
|
27
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Tower worker preset declaration (#121): one row, one Plugins-page label.
|
|
2
|
+
//
|
|
3
|
+
// DSH labels a row from `readPluginMeta(row.moduleName)`, which resolves
|
|
4
|
+
// `<specifier>/locale/en.json` and every sibling `<lang>.json` there. Both
|
|
5
|
+
// preset rows used to mount `@deepseek-ai/dsh-agent-preset`, so both shared that
|
|
6
|
+
// module's single label and the page could not tell the worker from the
|
|
7
|
+
// reviewer. That package is also not resolvable from this package, so Tower
|
|
8
|
+
// cannot add per-row locale files to it.
|
|
9
|
+
//
|
|
10
|
+
// This module is the tiny Tower-owned twin: it keeps the row config in
|
|
11
|
+
// `cordis.patch.yml` and forwards it to the injected `agentPresets` service. The
|
|
12
|
+
// service owns preset semantics — it mounts the definition's `plugins` list
|
|
13
|
+
// itself and returns the disposer that unmounts it — so this module stays
|
|
14
|
+
// renderer-free and imports nothing. Its specifier carries the worker label
|
|
15
|
+
// (`dsh-tower/preset-worker/locale/<lang>.json`).
|
|
16
|
+
//
|
|
17
|
+
// Shape: a plain object plugin (named exports) whose async `apply` returns the
|
|
18
|
+
// registry's disposer. Cordis collects the resolved function as the plugin's
|
|
19
|
+
// disposer, so disabling the row (or an HMR unload) unregisters the preset.
|
|
20
|
+
export const name = 'dsh-tower/preset-worker'
|
|
21
|
+
|
|
22
|
+
export const inject = ['agentPresets']
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register the worker preset with the profile's preset registry.
|
|
26
|
+
* @param {{ agentPresets: { register: (config: any) => Promise<() => Promise<void> | void> } }} ctx
|
|
27
|
+
* @param {any} config the `cordis.patch.yml` row config (id/name/description/order/plugins)
|
|
28
|
+
* @returns {Promise<() => Promise<void> | void>} the registry's unregister disposer
|
|
29
|
+
*/
|
|
30
|
+
export async function apply(ctx, config) {
|
|
31
|
+
return ctx.agentPresets.register(config)
|
|
32
|
+
}
|
package/locale/en.json
CHANGED
|
@@ -39,9 +39,14 @@
|
|
|
39
39
|
"summary": "Register tower_reload and /tower reload for linked-profile development.",
|
|
40
40
|
"disables": "tower_reload and /tower reload are unavailable; a non-HMR profile behaves exactly as before. Shipped off."
|
|
41
41
|
},
|
|
42
|
+
"preset-tower-worker": {
|
|
43
|
+
"title": "Tower Worker preset",
|
|
44
|
+
"summary": "Declares the Tower Worker preset: the mission protocol plus the filesystem, shell and Tower tools a worker runs inside its assigned worktree.",
|
|
45
|
+
"disables": "tower_spawn cannot start a worker without it."
|
|
46
|
+
},
|
|
42
47
|
"preset-tower-reviewer": {
|
|
43
|
-
"title": "Tower
|
|
44
|
-
"summary": "
|
|
48
|
+
"title": "Tower Reviewer preset",
|
|
49
|
+
"summary": "Declares the Tower Reviewer preset: read-only independent review that inspects the mission diff, verifies scope and acceptance, and returns a verdict.",
|
|
45
50
|
"disables": "The reviewer cannot be dispatched. Supported while review.requireReview is false; with the gate on planning/mode entry is locked with an explanatory error."
|
|
46
51
|
}
|
|
47
52
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "Tower Reviewer preset",
|
|
4
|
+
"description": "Declares the Tower Reviewer preset: read-only independent review that inspects the mission diff, verifies scope and acceptance, and returns a verdict. When off: The reviewer cannot be dispatched. Supported while review.requireReview is false; with the gate on planning/mode entry is locked with an explanatory error."
|
|
5
|
+
}
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "Tower Worker preset",
|
|
4
|
+
"description": "Declares the Tower Worker preset: the mission protocol plus the filesystem, shell and Tower tools a worker runs inside its assigned worktree. Mandatory dependency, not a switch: tower_spawn cannot start a worker without it."
|
|
5
|
+
}
|
|
6
|
+
}
|
package/locale/zh.json
CHANGED
|
@@ -39,9 +39,14 @@
|
|
|
39
39
|
"summary": "为联动开发 profile 注册 tower_reload 与 /tower reload。",
|
|
40
40
|
"disables": "tower_reload 与 /tower reload 不可用;非 HMR profile 的行为与之前完全相同。随包默认关闭。"
|
|
41
41
|
},
|
|
42
|
+
"preset-tower-worker": {
|
|
43
|
+
"title": "Tower Worker 预设",
|
|
44
|
+
"summary": "声明 Tower Worker 预设:任务协议,以及执行代理在其工作树内使用的文件系统、终端与 Tower 工具。",
|
|
45
|
+
"disables": "缺少它 tower_spawn 无法启动执行代理。"
|
|
46
|
+
},
|
|
42
47
|
"preset-tower-reviewer": {
|
|
43
|
-
"title": "Tower
|
|
44
|
-
"summary": "
|
|
48
|
+
"title": "Tower Reviewer 预设",
|
|
49
|
+
"summary": "声明 Tower Reviewer 预设:只读独立审查,检视任务差异、核对范围与验收,并返回结论。",
|
|
45
50
|
"disables": "无法派发审查代理。review.requireReview 为 false 时允许关闭;强制审查开启时规划/进入模式会被锁定并给出解释。"
|
|
46
51
|
}
|
|
47
52
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tower",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
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,12 +36,16 @@
|
|
|
36
36
|
"./leases": "./host/capability-leases.js",
|
|
37
37
|
"./tests": "./host/capability-tests.js",
|
|
38
38
|
"./hmr": "./host/capability-hmr.js",
|
|
39
|
+
"./preset-worker": "./host/preset-worker.js",
|
|
40
|
+
"./preset-reviewer": "./host/preset-reviewer.js",
|
|
39
41
|
"./skills/locale/*.json": "./locale/skills/*.json",
|
|
40
42
|
"./batch/locale/*.json": "./locale/batch/*.json",
|
|
41
43
|
"./import/locale/*.json": "./locale/import/*.json",
|
|
42
44
|
"./leases/locale/*.json": "./locale/leases/*.json",
|
|
43
45
|
"./tests/locale/*.json": "./locale/tests/*.json",
|
|
44
46
|
"./hmr/locale/*.json": "./locale/hmr/*.json",
|
|
47
|
+
"./preset-worker/locale/*.json": "./locale/preset-worker/*.json",
|
|
48
|
+
"./preset-reviewer/locale/*.json": "./locale/preset-reviewer/*.json",
|
|
45
49
|
"./locale/*.json": "./locale/*.json",
|
|
46
50
|
"./icon": "./icon.svg",
|
|
47
51
|
"./package.json": "./package.json"
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// gate is what makes that a rule instead of a slogan. It fails on:
|
|
6
6
|
//
|
|
7
7
|
// 1. a visible component without `title`, `summary` and `disables` in both
|
|
8
|
-
// locale/en.json and locale/zh.json
|
|
8
|
+
// locale/en.json and locale/zh.json — and the same three strings for every
|
|
9
|
+
// component that ships a generated row label, including the internal
|
|
10
|
+
// worker preset (#121);
|
|
9
11
|
// 2. a forbidden-to-disable capability (host, health, recovery, review
|
|
10
12
|
// contract, merge gates) that any group, row or boolean key can disable;
|
|
11
13
|
// 3. a component count that moved without a decision in
|
|
@@ -28,6 +30,7 @@ import { Config, configSurface } from '../domain/config/schema.js'
|
|
|
28
30
|
import {
|
|
29
31
|
COMPONENT_REFERENCE_FILE,
|
|
30
32
|
COMPONENT_SURFACE_FILE,
|
|
33
|
+
LOCALE_FIELDS,
|
|
31
34
|
LOCALE_FILES,
|
|
32
35
|
checkForbidden,
|
|
33
36
|
checkLocaleCoverage,
|
|
@@ -40,6 +43,7 @@ import {
|
|
|
40
43
|
import { validateSurfaceBaselineKey } from '../domain/surface.js'
|
|
41
44
|
import { readBaseline } from './lib/surface.mjs'
|
|
42
45
|
import { parseArgs, runMain } from './lib/cli.mjs'
|
|
46
|
+
import { ROW_LOCALES } from './generate-capability-locale.mjs'
|
|
43
47
|
|
|
44
48
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
45
49
|
const REGISTRY_FILE = 'host/tool-registry.js'
|
|
@@ -51,6 +55,7 @@ const USAGE = `usage: node scripts/check-components.mjs [--root <dir>] [--json]
|
|
|
51
55
|
--help print this text
|
|
52
56
|
|
|
53
57
|
Fails on a visible component without title/summary/disables in both locale files,
|
|
58
|
+
a generated row label (capability or preset) whose component entry is missing,
|
|
54
59
|
a forbidden-to-disable capability disabled by a group, row or boolean key, a
|
|
55
60
|
component count that moved without a scripts/surface-baseline.json decision,
|
|
56
61
|
an unclassified config key or component row, and a stale generated reference.
|
|
@@ -60,6 +65,57 @@ function finding(code, level, message) {
|
|
|
60
65
|
return { code, level, message }
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The generated row-label map is a coverage axis of its own (#121). A component
|
|
70
|
+
* that ships a `<specifier>/locale/<lang>.json` label must carry `title`,
|
|
71
|
+
* `summary` and `disables` in every shipped locale — including an internal
|
|
72
|
+
* component such as the mandatory worker preset, which the visible-only rule in
|
|
73
|
+
* `domain/components.js` deliberately does not cover.
|
|
74
|
+
*
|
|
75
|
+
* `rows` is the generator's declaration (`{ specifier, component, mandatory? }`).
|
|
76
|
+
* When a row records `mandatory`, it must agree with the component-surface
|
|
77
|
+
* scope: an internal component is a dependency, never a switch. That is how the
|
|
78
|
+
* worker's "mandatory dependency" description and the component policy stay one
|
|
79
|
+
* decision.
|
|
80
|
+
*
|
|
81
|
+
* @param {any} surface the parsed `scripts/component-surface.json`
|
|
82
|
+
* @param {Record<string, any>} locales file path -> parsed locale JSON
|
|
83
|
+
* @param {Array<{ specifier: string, component: string, mandatory?: boolean }>} rows
|
|
84
|
+
*/
|
|
85
|
+
export function checkRowLocaleCoverage(surface, locales, rows) {
|
|
86
|
+
const findings = []
|
|
87
|
+
const record = (code, message) => findings.push(finding(code, 'error', message))
|
|
88
|
+
const components = new Map(surface.components.map((component) => [component.id, component]))
|
|
89
|
+
for (const row of rows ?? []) {
|
|
90
|
+
const component = components.get(row?.component)
|
|
91
|
+
if (!component) {
|
|
92
|
+
record(
|
|
93
|
+
'row-locale-unknown',
|
|
94
|
+
`row locale "${row?.specifier}" names component "${row?.component}", which the component surface does not declare`,
|
|
95
|
+
)
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
if (typeof row.mandatory === 'boolean' && row.mandatory !== (component.scope === 'internal')) {
|
|
99
|
+
record(
|
|
100
|
+
'row-locale-scope',
|
|
101
|
+
`row locale "${row.specifier}" labels ${component.id} ${row.mandatory ? 'mandatory' : 'optional'}, but the component surface scope is ${component.scope}`,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
for (const file of LOCALE_FILES) {
|
|
105
|
+
const entry = locales?.[file]?.components?.[component.id]
|
|
106
|
+
if (!entry || typeof entry !== 'object') {
|
|
107
|
+
record('row-locale', `row locale ${component.id} has no entry in ${file}`)
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
for (const field of LOCALE_FIELDS) {
|
|
111
|
+
const value = typeof entry[field] === 'string' ? entry[field].trim() : ''
|
|
112
|
+
if (!value) record('row-locale', `row locale ${component.id} misses "${field}" in ${file}`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return findings
|
|
117
|
+
}
|
|
118
|
+
|
|
63
119
|
/** The live boolean config keys, derived from the schemastery schema. */
|
|
64
120
|
function liveBooleanKeys() {
|
|
65
121
|
const keys = []
|
|
@@ -141,6 +197,10 @@ export function checkComponents(root) {
|
|
|
141
197
|
}
|
|
142
198
|
findings.push(...validateComponentSurface(surface, { configKeys: live.configKeys, patch: live.patch }))
|
|
143
199
|
findings.push(...checkLocaleCoverage(surface, live.locales))
|
|
200
|
+
// #121: a row that ships a generated `<specifier>/locale/*.json` label needs
|
|
201
|
+
// the same three strings in both locales, including an internal row (the
|
|
202
|
+
// mandatory worker preset) that the visible-only rule above does not cover.
|
|
203
|
+
findings.push(...checkRowLocaleCoverage(surface, live.locales, ROW_LOCALES))
|
|
144
204
|
findings.push(...checkForbidden(surface, { booleans: live.booleans, registry: live.registry, patch: live.patch }))
|
|
145
205
|
|
|
146
206
|
// #122: an empty OPERATIONAL_TOOLS table is the expected state. Every route
|
package/scripts/check-pack.mjs
CHANGED
|
@@ -10,7 +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
|
+
import { capabilityLocalePaths, presetLocalePaths } from './generate-capability-locale.mjs'
|
|
14
14
|
|
|
15
15
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
16
16
|
const REQUIRED_FILES = [
|
|
@@ -18,9 +18,13 @@ const REQUIRED_FILES = [
|
|
|
18
18
|
'index.js',
|
|
19
19
|
'locale/en.json',
|
|
20
20
|
'locale/zh.json',
|
|
21
|
-
// #121: every optional capability row
|
|
22
|
-
// through a subpath locale resource, so those
|
|
21
|
+
// #121: every optional capability row and both preset-declaring rows explain
|
|
22
|
+
// themselves on the Plugins page through a subpath locale resource, so those
|
|
23
|
+
// generated files are required.
|
|
23
24
|
...capabilityLocalePaths(),
|
|
25
|
+
...presetLocalePaths(),
|
|
26
|
+
'host/preset-worker.js',
|
|
27
|
+
'host/preset-reviewer.js',
|
|
24
28
|
'ui-bundle/index.js',
|
|
25
29
|
'ui-bundle/package.json',
|
|
26
30
|
'ui-bundle/cordis.patch.yml',
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
"id": "preset-tower-worker",
|
|
62
62
|
"scope": "internal",
|
|
63
63
|
"kind": "row",
|
|
64
|
-
"fallback": "tower_spawn fails without the worker preset. A mandatory dependency, never a switch."
|
|
64
|
+
"fallback": "Declared by dsh-tower/preset-worker: tower_spawn fails without the worker preset. A mandatory dependency, never a switch."
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
67
|
"id": "preset-tower-reviewer",
|
|
68
68
|
"scope": "visible",
|
|
69
69
|
"kind": "row",
|
|
70
|
-
"fallback": "
|
|
70
|
+
"fallback": "Declared by dsh-tower/preset-reviewer: the reviewer cannot be dispatched. Supported while review.requireReview is false (merge is unaffected); with the gate on planning/mode entry is locked with an explanatory error."
|
|
71
71
|
}
|
|
72
72
|
],
|
|
73
73
|
"gateableTools": {},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Generate the per-
|
|
1
|
+
// Generate the per-row locale files (#121).
|
|
2
2
|
//
|
|
3
|
-
// The DSH Plugins page renders
|
|
3
|
+
// The DSH Plugins page renders a row as
|
|
4
4
|
// `row.meta.title ?? row.moduleName` plus `row.meta.description`, and the Host
|
|
5
5
|
// fills `row.meta` by resolving `<specifier>/locale/en.json` and then every
|
|
6
6
|
// sibling `<lang>.json` in that directory. A subpath row without such a resource
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
// Those files are generated, not hand-written: the single source of truth is the
|
|
11
11
|
// `components` block of `locale/<lang>.json` that `npm run docs:check:components`
|
|
12
12
|
// already audits. `meta.title` is the component `title`; `meta.description`
|
|
13
|
-
// composes that component's `summary` (what the
|
|
14
|
-
// `disables` text (what turning it off removes),
|
|
15
|
-
//
|
|
13
|
+
// composes that component's `summary` (what the row provides) with either its
|
|
14
|
+
// `disables` text (what turning it off removes) or, for the mandatory worker
|
|
15
|
+
// preset, an explicit "mandatory dependency, not a switch" label. The same
|
|
16
|
+
// generator covers capability rows and the two preset-declaring rows, so their
|
|
17
|
+
// labels cannot drift apart.
|
|
16
18
|
//
|
|
17
19
|
// Usage:
|
|
18
20
|
// node scripts/generate-capability-locale.mjs [--root <dir>] [--check]
|
|
@@ -28,6 +30,10 @@ const LANGUAGES = Object.keys(LANGUAGE_FILES)
|
|
|
28
30
|
// `disables` prose, so the check can prove every description names what the
|
|
29
31
|
// switch removes without depending on one English or Chinese phrasing.
|
|
30
32
|
const OFF_EFFECT_LABEL = { en: 'When off:', zh: '关闭后:' }
|
|
33
|
+
// The worker preset has no off state; it is a mandatory dependency of the Tower
|
|
34
|
+
// host. Its description says so with an explicit marker instead of a "When off"
|
|
35
|
+
// label that would make the row read like a switch.
|
|
36
|
+
const DEPENDENCY_LABEL = { en: 'Mandatory dependency, not a switch:', zh: '强制依赖,不是开关:' }
|
|
31
37
|
|
|
32
38
|
/**
|
|
33
39
|
* The optional capability rows that need their own subpath locale resource, in
|
|
@@ -43,6 +49,22 @@ export const CAPABILITY_LOCALES = [
|
|
|
43
49
|
{ specifier: 'hmr', component: 'tower-hmr' },
|
|
44
50
|
]
|
|
45
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The two preset-declaring rows. They used to mount the shared
|
|
54
|
+
* `@deepseek-ai/dsh-agent-preset`, so DSH resolved one locale resource for both
|
|
55
|
+
* and rendered the same title twice. Each now owns a Tower declaration module
|
|
56
|
+
* (`host/preset-*.js`) and therefore its own `<specifier>/locale/*.json` label.
|
|
57
|
+
* `mandatory` is the worker dependency; it is asserted against the component
|
|
58
|
+
* surface scope by the gate/tests so the flag cannot drift from the policy.
|
|
59
|
+
*/
|
|
60
|
+
export const PRESET_LOCALES = [
|
|
61
|
+
{ specifier: 'preset-worker', component: 'preset-tower-worker', mandatory: true },
|
|
62
|
+
{ specifier: 'preset-reviewer', component: 'preset-tower-reviewer', mandatory: false },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
/** Every row that ships a generated subpath locale resource. */
|
|
66
|
+
export const ROW_LOCALES = [...CAPABILITY_LOCALES, ...PRESET_LOCALES]
|
|
67
|
+
|
|
46
68
|
export const CAPABILITY_LOCALE_LANGUAGES = LANGUAGES
|
|
47
69
|
|
|
48
70
|
/** Repo-relative paths of every generated capability locale file. */
|
|
@@ -52,7 +74,19 @@ export function capabilityLocalePaths() {
|
|
|
52
74
|
)
|
|
53
75
|
}
|
|
54
76
|
|
|
55
|
-
/**
|
|
77
|
+
/** Repo-relative paths of every generated preset row locale file. */
|
|
78
|
+
export function presetLocalePaths() {
|
|
79
|
+
return PRESET_LOCALES.flatMap(({ specifier }) =>
|
|
80
|
+
LANGUAGES.map((lang) => `locale/${specifier}/${lang}.json`),
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Repo-relative paths of every generated row locale file (capability + preset). */
|
|
85
|
+
export function rowLocalePaths() {
|
|
86
|
+
return [...capabilityLocalePaths(), ...presetLocalePaths()]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The `package.json` exports entry that exposes one row's locale files. */
|
|
56
90
|
export function capabilityLocaleExport(specifier) {
|
|
57
91
|
return { key: `./${specifier}/locale/*.json`, target: `./locale/${specifier}/*.json` }
|
|
58
92
|
}
|
|
@@ -63,15 +97,22 @@ function requireText(value, label) {
|
|
|
63
97
|
return text
|
|
64
98
|
}
|
|
65
99
|
|
|
100
|
+
/** Normalize the three locale prose fields a generated row label needs. */
|
|
101
|
+
function rowText(entry, lang) {
|
|
102
|
+
return {
|
|
103
|
+
title: requireText(entry?.title, `${lang} row title`),
|
|
104
|
+
summary: requireText(entry?.summary, `${lang} row summary`).replace(/\s+/g, ' '),
|
|
105
|
+
disables: requireText(entry?.disables, `${lang} row disables`).replace(/\s+/g, ' '),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
66
109
|
/**
|
|
67
110
|
* Compose the display metadata for one capability row in one language.
|
|
68
111
|
* @param {{ title: string, summary: string, disables: string }} entry
|
|
69
112
|
* @param {'en' | 'zh'} lang
|
|
70
113
|
*/
|
|
71
114
|
export function renderCapabilityMeta(entry, lang) {
|
|
72
|
-
const 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, ' ')
|
|
115
|
+
const { title, summary, disables } = rowText(entry, lang)
|
|
75
116
|
// Chinese prose does not space CJK punctuation, so the label joins directly.
|
|
76
117
|
const space = lang === 'zh' ? '' : ' '
|
|
77
118
|
return {
|
|
@@ -80,23 +121,57 @@ export function renderCapabilityMeta(entry, lang) {
|
|
|
80
121
|
}
|
|
81
122
|
|
|
82
123
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
124
|
+
* Compose the display metadata for one preset-declaring row in one language.
|
|
125
|
+
* The mandatory worker dependency is labelled as a dependency, never as a
|
|
126
|
+
* switch; the conditional reviewer keeps the ordinary off-effect label.
|
|
127
|
+
* @param {{ title: string, summary: string, disables: string }} entry
|
|
128
|
+
* @param {'en' | 'zh'} lang
|
|
129
|
+
* @param {boolean} mandatory
|
|
86
130
|
*/
|
|
87
|
-
export function
|
|
131
|
+
export function renderPresetMeta(entry, lang, mandatory) {
|
|
132
|
+
const { title, summary, disables } = rowText(entry, lang)
|
|
133
|
+
const space = lang === 'zh' ? '' : ' '
|
|
134
|
+
const label = mandatory ? DEPENDENCY_LABEL[lang] : OFF_EFFECT_LABEL[lang]
|
|
135
|
+
return {
|
|
136
|
+
meta: { title, description: `${summary}${space}${label}${space}${disables}` },
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function readLocales(root) {
|
|
88
141
|
const locales = {}
|
|
89
142
|
for (const lang of LANGUAGES) {
|
|
90
143
|
const file = path.join(root, LANGUAGE_FILES[lang])
|
|
91
144
|
if (!fs.existsSync(file)) throw new Error(`${LANGUAGE_FILES[lang]} is missing under ${root}`)
|
|
92
145
|
locales[lang] = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
93
146
|
}
|
|
147
|
+
return locales
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Pull one row's components entry out of both locales, or throw a named error. */
|
|
151
|
+
function componentEntry(locales, lang, component) {
|
|
152
|
+
const entry = locales[lang]?.components?.[component]
|
|
153
|
+
if (!entry) throw new Error(`${LANGUAGE_FILES[lang]} has no components.${component} entry`)
|
|
154
|
+
return entry
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Render every generated file for one repository root.
|
|
159
|
+
* @param {string} root
|
|
160
|
+
* @returns {Map<string, string>} repo-relative path -> file text
|
|
161
|
+
*/
|
|
162
|
+
export function renderRowLocaleFiles(root) {
|
|
163
|
+
const locales = readLocales(root)
|
|
94
164
|
const files = new Map()
|
|
95
165
|
for (const { specifier, component } of CAPABILITY_LOCALES) {
|
|
96
166
|
for (const lang of LANGUAGES) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
167
|
+
const rendered = renderCapabilityMeta(componentEntry(locales, lang, component), lang)
|
|
168
|
+
files.set(`locale/${specifier}/${lang}.json`, `${JSON.stringify(rendered, null, 2)}\n`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const { specifier, component, mandatory } of PRESET_LOCALES) {
|
|
172
|
+
for (const lang of LANGUAGES) {
|
|
173
|
+
const rendered = renderPresetMeta(componentEntry(locales, lang, component), lang, mandatory)
|
|
174
|
+
files.set(`locale/${specifier}/${lang}.json`, `${JSON.stringify(rendered, null, 2)}\n`)
|
|
100
175
|
}
|
|
101
176
|
}
|
|
102
177
|
return files
|
|
@@ -116,11 +191,12 @@ function main(argv) {
|
|
|
116
191
|
--help print this text
|
|
117
192
|
|
|
118
193
|
The files are generated from the components block of locale/en.json and
|
|
119
|
-
locale/zh.json; do not edit locale/<
|
|
194
|
+
locale/zh.json; do not edit locale/<row>/<lang>.json by hand. Capability and
|
|
195
|
+
preset-declaring rows share this generator so their labels cannot drift.`)
|
|
120
196
|
return 0
|
|
121
197
|
}
|
|
122
198
|
const root = path.resolve(options.root)
|
|
123
|
-
const files =
|
|
199
|
+
const files = renderRowLocaleFiles(root)
|
|
124
200
|
if (options.check) {
|
|
125
201
|
const stale = []
|
|
126
202
|
for (const [relative, expected] of files) {
|
|
@@ -129,11 +205,11 @@ locale/zh.json; do not edit locale/<capability>/<lang>.json by hand.`)
|
|
|
129
205
|
if (actual !== expected) stale.push(relative)
|
|
130
206
|
}
|
|
131
207
|
if (stale.length > 0) {
|
|
132
|
-
console.error('
|
|
208
|
+
console.error('row locale files are stale; run npm run docs:generate:locale')
|
|
133
209
|
for (const relative of stale) console.error(` ${relative}`)
|
|
134
210
|
return 1
|
|
135
211
|
}
|
|
136
|
-
console.log(`
|
|
212
|
+
console.log(`row locales: ${files.size} files in sync (${ROW_LOCALES.length} specifiers, en/zh)`)
|
|
137
213
|
return 0
|
|
138
214
|
}
|
|
139
215
|
for (const [relative, text] of files) {
|
|
@@ -141,7 +217,7 @@ locale/zh.json; do not edit locale/<capability>/<lang>.json by hand.`)
|
|
|
141
217
|
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
142
218
|
fs.writeFileSync(target, text)
|
|
143
219
|
}
|
|
144
|
-
console.log(`wrote ${files.size}
|
|
220
|
+
console.log(`wrote ${files.size} row locale files (${ROW_LOCALES.length} specifiers, en/zh)`)
|
|
145
221
|
return 0
|
|
146
222
|
}
|
|
147
223
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import fs from 'node:fs'
|
|
12
12
|
import path from 'node:path'
|
|
13
13
|
import { pathToFileURL } from 'node:url'
|
|
14
|
+
import { PRESET_SPECIFIERS } from './surface.mjs'
|
|
14
15
|
|
|
15
16
|
function parseArgs(argv) {
|
|
16
17
|
const options = {}
|
|
@@ -45,7 +46,7 @@ for (const file of [profileBoot, appBoot]) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const { runProfile } = await import(pathToFileURL(profileBoot).href)
|
|
48
|
-
const { loadLayeredEnv } = await import(pathToFileURL(appBoot).href)
|
|
49
|
+
const { loadLayeredEnv, readPluginMeta } = await import(pathToFileURL(appBoot).href)
|
|
49
50
|
if (typeof runProfile !== 'function') throw new Error(`probe: host ${hostVersion} does not export runProfile`)
|
|
50
51
|
|
|
51
52
|
/** Run one surface read, keeping a missing service a reportable value. */
|
|
@@ -64,7 +65,7 @@ const { ctx, shutdown } = await runProfile({
|
|
|
64
65
|
args: [],
|
|
65
66
|
})
|
|
66
67
|
|
|
67
|
-
const surface = { host: { version: hostVersion }, profile: {}, tools: null, skills: null, presets: null, commands: null, tree: null }
|
|
68
|
+
const surface = { host: { version: hostVersion }, profile: {}, tools: null, skills: null, presets: null, labels: null, commands: null, tree: null }
|
|
68
69
|
try {
|
|
69
70
|
const profileContext = ctx.get('profileContext')
|
|
70
71
|
surface.profile = {
|
|
@@ -88,9 +89,38 @@ try {
|
|
|
88
89
|
|
|
89
90
|
const presets = ctx.get('agentPresets')
|
|
90
91
|
surface.presets = presets
|
|
91
|
-
? await read(async () =>
|
|
92
|
+
? await read(async () => {
|
|
93
|
+
// Both preset rows declare themselves through Tower-owned modules
|
|
94
|
+
// (`dsh-tower/preset-*`). Keep the display metadata and the activation
|
|
95
|
+
// diagnostic so the smoke can prove each row still mounts, not only
|
|
96
|
+
// that two ids exist (#121).
|
|
97
|
+
const list = await presets.list()
|
|
98
|
+
return {
|
|
99
|
+
ids: list.map((preset) => preset.id),
|
|
100
|
+
entries: list.map(({ id, name, description, order, broken }) => ({
|
|
101
|
+
id,
|
|
102
|
+
...name === undefined ? {} : { name },
|
|
103
|
+
...description === undefined ? {} : { description },
|
|
104
|
+
...order === undefined ? {} : { order },
|
|
105
|
+
...broken === undefined ? {} : { broken },
|
|
106
|
+
})),
|
|
107
|
+
}
|
|
108
|
+
})
|
|
92
109
|
: { error: 'the agentPresets service is not mounted (profile has no preset registry)' }
|
|
93
110
|
|
|
111
|
+
// The Plugins page labels a row through this exact call. Resolve both preset
|
|
112
|
+
// row specifiers from the installed profile so the smoke proves the two rows
|
|
113
|
+
// render distinct titles/descriptions instead of one shared label (#121).
|
|
114
|
+
surface.labels =
|
|
115
|
+
typeof readPluginMeta === 'function' && typeof profileContext?.dir === 'string'
|
|
116
|
+
? await read(() => {
|
|
117
|
+
const base = pathToFileURL(path.join(profileContext.dir, 'cordis.yml')).href
|
|
118
|
+
return Object.fromEntries(
|
|
119
|
+
PRESET_SPECIFIERS.map((specifier) => [specifier, readPluginMeta(specifier, base) ?? null]),
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
: { error: 'the host exports no readPluginMeta, so preset row labels cannot be read' }
|
|
123
|
+
|
|
94
124
|
const commands = ctx.get('commands')
|
|
95
125
|
surface.commands = commands
|
|
96
126
|
? await read(() => commands.list(undefined).map((command) => command.name))
|
|
@@ -55,11 +55,30 @@ export function defaultOffToolIds() {
|
|
|
55
55
|
return ids
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The two preset rows the bundle declares. Each row mounts a Tower-owned
|
|
60
|
+
* declaration module (`dsh-tower/preset-*`) whose config the agent-preset
|
|
61
|
+
* registry registers; the label and the read-only reviewer surface are part of
|
|
62
|
+
* the contract, so the smoke compares the loaded `agentPresets.list()` entries
|
|
63
|
+
* against this table (#121).
|
|
64
|
+
*/
|
|
65
|
+
export const PRESET_ROWS = Object.freeze([
|
|
66
|
+
Object.freeze({ id: 'tower-worker', specifier: 'preset-worker', name: 'Tower Worker', order: 60 }),
|
|
67
|
+
Object.freeze({ id: 'tower-reviewer', specifier: 'preset-reviewer', name: 'Tower Reviewer', order: 61 }),
|
|
68
|
+
])
|
|
69
|
+
|
|
58
70
|
/** Presets the bundle declares for its worker and reviewer roles. */
|
|
59
|
-
export const PRESET_IDS =
|
|
71
|
+
export const PRESET_IDS = PRESET_ROWS.map((row) => row.id).sort()
|
|
72
|
+
|
|
73
|
+
/** Module specifiers the two preset rows mount; each owns its Plugins-page label. */
|
|
74
|
+
export const PRESET_SPECIFIERS = PRESET_ROWS.map((row) => `dsh-tower/${row.specifier}`)
|
|
60
75
|
|
|
61
76
|
/** Cordis fiber state 2: mounted and active. */
|
|
62
77
|
const FIBER_ACTIVE = 2
|
|
78
|
+
/** Cordis fiber state 3: the plugin failed to activate. */
|
|
79
|
+
const FIBER_FAILED = 3
|
|
80
|
+
/** Cordis fiber state 1: activation is still in flight (the boot has not settled). */
|
|
81
|
+
const FIBER_PENDING = 1
|
|
63
82
|
|
|
64
83
|
const OWNED_ROW_IDS = new Set([BUNDLE_ID, ...BUNDLE_CHILD_IDS, ...OPTIONAL_ROW_IDS])
|
|
65
84
|
|
|
@@ -128,6 +147,128 @@ export function compareIds(label, expectedIds, loadedIds) {
|
|
|
128
147
|
return failures
|
|
129
148
|
}
|
|
130
149
|
|
|
150
|
+
/** Read one localized field: DSH reports `{ en, zh }` maps, a bare string, or nothing. */
|
|
151
|
+
function localizedField(value, lang) {
|
|
152
|
+
if (typeof value === 'string') return value.trim() || undefined
|
|
153
|
+
if (value && typeof value === 'object' && typeof value[lang] === 'string') return value[lang].trim() || undefined
|
|
154
|
+
return undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Assert the two preset rows resolve distinct Plugins-page labels. The defect
|
|
159
|
+
* this mission fixes was two rows sharing `@deepseek-ai/dsh-agent-preset`, so
|
|
160
|
+
* both rendered that module's single title; the smoke now reads the same
|
|
161
|
+
* `readPluginMeta` the page uses and requires a distinct title and description
|
|
162
|
+
* per specifier (#121).
|
|
163
|
+
* @param {Record<string, { title?: unknown, description?: unknown, error?: string } | null>} labels
|
|
164
|
+
* module specifier -> label metadata from `readPluginMeta`.
|
|
165
|
+
* @returns {string[]} one message per missing, failed or indistinguishable label.
|
|
166
|
+
*/
|
|
167
|
+
export function checkPresetLabels(labels) {
|
|
168
|
+
const failures = []
|
|
169
|
+
const byLanguage = { en: new Map(), zh: new Map() }
|
|
170
|
+
for (const row of PRESET_ROWS) {
|
|
171
|
+
const specifier = `dsh-tower/${row.specifier}`
|
|
172
|
+
const label = labels?.[specifier]
|
|
173
|
+
if (label === undefined) {
|
|
174
|
+
failures.push(`label: no label reported for ${specifier}`)
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
if (label === null) {
|
|
178
|
+
failures.push(`label: ${specifier} resolves no locale metadata`)
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
if (label.error) {
|
|
182
|
+
failures.push(`label: ${specifier}: ${label.error}`)
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
for (const lang of ['en', 'zh']) {
|
|
186
|
+
const title = localizedField(label.title, lang)
|
|
187
|
+
const description = localizedField(label.description, lang)
|
|
188
|
+
if (!title) {
|
|
189
|
+
failures.push(`label: ${specifier} has no ${lang} title`)
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
if (!description) failures.push(`label: ${specifier} has no ${lang} description`)
|
|
193
|
+
if (title === specifier) failures.push(`label: ${specifier} falls back to the module specifier as its ${lang} title`)
|
|
194
|
+
if (!title.includes(row.name)) failures.push(`label: ${specifier} ${lang} title does not name ${row.name}`)
|
|
195
|
+
byLanguage[lang].set(specifier, { title, description })
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const lang of ['en', 'zh']) {
|
|
199
|
+
const entries = [...byLanguage[lang].entries()]
|
|
200
|
+
if (entries.length < 2) continue
|
|
201
|
+
const [first] = entries
|
|
202
|
+
for (const [specifier, entry] of entries.slice(1)) {
|
|
203
|
+
if (entry.title === first[1].title) {
|
|
204
|
+
failures.push(`label: ${first[0]} and ${specifier} share the same ${lang} title`)
|
|
205
|
+
}
|
|
206
|
+
if (entry.description !== undefined && entry.description === first[1].description) {
|
|
207
|
+
failures.push(`label: ${first[0]} and ${specifier} share the same ${lang} description`)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return failures
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Compare the loaded preset definitions against {@link PRESET_ROWS}. A missing
|
|
216
|
+
* preset, a changed display name/order, or an activation diagnostic (`broken`)
|
|
217
|
+
* is a failed claim: a preset row that mounts but never activates is exactly the
|
|
218
|
+
* regression the smoke exists to catch.
|
|
219
|
+
* @param {Array<{ id?: string, name?: string, order?: number, broken?: string }>} entries
|
|
220
|
+
* @returns {string[]} one message per violated preset claim.
|
|
221
|
+
*/
|
|
222
|
+
export function checkPresetRows(entries) {
|
|
223
|
+
const list = Array.isArray(entries) ? entries : []
|
|
224
|
+
const byId = new Map(list.map((entry) => [String(entry?.id), entry]))
|
|
225
|
+
const failures = []
|
|
226
|
+
for (const row of PRESET_ROWS) {
|
|
227
|
+
const entry = byId.get(row.id)
|
|
228
|
+
if (!entry) {
|
|
229
|
+
failures.push(`preset: missing ${row.id}`)
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
if (entry.name !== row.name) {
|
|
233
|
+
failures.push(`preset: ${row.id} name ${JSON.stringify(entry.name)}, expected ${JSON.stringify(row.name)}`)
|
|
234
|
+
}
|
|
235
|
+
if (entry.order !== row.order) {
|
|
236
|
+
failures.push(`preset: ${row.id} order ${JSON.stringify(entry.order)}, expected ${row.order}`)
|
|
237
|
+
}
|
|
238
|
+
if (entry.broken) failures.push(`preset: ${row.id} failed to mount: ${entry.broken}`)
|
|
239
|
+
}
|
|
240
|
+
return failures
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Entry ids whose fiber reported an activation error (state 3). */
|
|
244
|
+
export function failedEntryStates(entryStates = {}) {
|
|
245
|
+
return Object.entries(entryStates)
|
|
246
|
+
.filter(([, state]) => state === FIBER_FAILED)
|
|
247
|
+
.map(([id]) => id)
|
|
248
|
+
.sort()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Entry ids still loading when the boot returned (state 1). */
|
|
252
|
+
export function pendingEntryStates(entryStates = {}) {
|
|
253
|
+
return Object.entries(entryStates)
|
|
254
|
+
.filter(([, state]) => state === FIBER_PENDING)
|
|
255
|
+
.map(([id]) => id)
|
|
256
|
+
.sort()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* No row may fail or stay pending in a green smoke: the bundle is supposed to
|
|
261
|
+
* compose a working profile, not merely register the ids it claims.
|
|
262
|
+
* @param {Record<string, number | null | undefined>} entryStates
|
|
263
|
+
* @returns {string[]} one message per failed or pending row.
|
|
264
|
+
*/
|
|
265
|
+
export function checkRowStates(entryStates = {}) {
|
|
266
|
+
const failures = []
|
|
267
|
+
for (const id of failedEntryStates(entryStates)) failures.push(`row: ${id} failed to mount (entry state 3)`)
|
|
268
|
+
for (const id of pendingEntryStates(entryStates)) failures.push(`row: ${id} is still pending (entry state 1)`)
|
|
269
|
+
return failures
|
|
270
|
+
}
|
|
271
|
+
|
|
131
272
|
/** Effective loader id for a nested row (`group:child`), falling back to the raw id. */
|
|
132
273
|
function stateKey(id, entryStates) {
|
|
133
274
|
if (entryStates[id] !== undefined) return id
|
package/scripts/mount-smoke.mjs
CHANGED
|
@@ -25,6 +25,9 @@ import {
|
|
|
25
25
|
INDEX_TOOL_IDS,
|
|
26
26
|
PRESET_IDS,
|
|
27
27
|
checkBundleRow,
|
|
28
|
+
checkPresetLabels,
|
|
29
|
+
checkPresetRows,
|
|
30
|
+
checkRowStates,
|
|
28
31
|
compareIds,
|
|
29
32
|
expectedSkillIds,
|
|
30
33
|
expectedToolIds,
|
|
@@ -163,13 +166,29 @@ function verifySurface(surface, root) {
|
|
|
163
166
|
if (loadedSkills) failures.push(...compareIds('skill', expectedSkills, loadedSkills.tower))
|
|
164
167
|
|
|
165
168
|
const loadedPresets = area(surface, 'presets', 'preset', failures)
|
|
166
|
-
if (loadedPresets)
|
|
169
|
+
if (loadedPresets) {
|
|
170
|
+
failures.push(...compareIds('preset', PRESET_IDS, loadedPresets.ids ?? []))
|
|
171
|
+
// Both preset rows must still activate: ids, display names/orders and no
|
|
172
|
+
// registry activation diagnostic (#121).
|
|
173
|
+
failures.push(...checkPresetRows(loadedPresets.entries ?? []))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The Plugins page reads the label through `readPluginMeta`; the two preset
|
|
177
|
+
// rows used to share one module and rendered the same title. Prove the
|
|
178
|
+
// installed package now resolves a distinct label per row (#121).
|
|
179
|
+
const loadedLabels = area(surface, 'labels', 'label', failures)
|
|
180
|
+
if (loadedLabels) failures.push(...checkPresetLabels(loadedLabels))
|
|
167
181
|
|
|
168
182
|
const loadedCommands = area(surface, 'commands', 'command', failures)
|
|
169
183
|
if (loadedCommands && !loadedCommands.includes('tower')) failures.push('command: /tower is not registered in the loaded command registry')
|
|
170
184
|
|
|
171
185
|
const tree = area(surface, 'tree', 'bundle', failures)
|
|
172
|
-
if (tree)
|
|
186
|
+
if (tree) {
|
|
187
|
+
failures.push(...checkBundleRow(tree.top, tree.states))
|
|
188
|
+
// The bundle was replaced row-by-row (#121); prove no row failed to activate
|
|
189
|
+
// or is still waiting, not only that the expected rows are active.
|
|
190
|
+
failures.push(...checkRowStates(tree.states))
|
|
191
|
+
}
|
|
173
192
|
|
|
174
193
|
return failures
|
|
175
194
|
}
|
|
@@ -179,10 +198,11 @@ function summarize(surface, root) {
|
|
|
179
198
|
const expected = expectedToolIds(toolModuleNames(root)).length
|
|
180
199
|
const tools = Array.isArray(surface?.tools?.value) ? surface.tools.value.filter((name) => name.startsWith('tower_')).length : '?'
|
|
181
200
|
const skills = Array.isArray(surface?.skills?.value?.tower) ? surface.skills.value.tower.length : '?'
|
|
182
|
-
const presets = Array.isArray(surface?.presets?.value) ? surface.presets.value.length : '?'
|
|
201
|
+
const presets = Array.isArray(surface?.presets?.value?.ids) ? surface.presets.value.ids.length : '?'
|
|
202
|
+
const labels = surface?.labels?.value && typeof surface.labels.value === 'object' ? Object.keys(surface.labels.value).length : '?'
|
|
183
203
|
const commands = Array.isArray(surface?.commands?.value) ? surface.commands.value.filter((name) => name === 'tower').length : '?'
|
|
184
204
|
const rows = Array.isArray(surface?.tree?.value?.top) ? surface.tree.value.top.filter((row) => row.id === BUNDLE_ID).length : '?'
|
|
185
|
-
return `${tools} of ${expected} tower tools (${modules} modules + ${INDEX_TOOL_IDS.length} entry tools; ${DEFAULT_OFF_ROW_IDS.join(', ')} off), ${skills} skills, ${presets} presets, /tower ${commands === 1 ? 'present' : 'missing'}, ${rows} top-level bundle row`
|
|
205
|
+
return `${tools} of ${expected} tower tools (${modules} modules + ${INDEX_TOOL_IDS.length} entry tools; ${DEFAULT_OFF_ROW_IDS.join(', ')} off), ${skills} skills, ${presets} presets (${labels} distinct labels), /tower ${commands === 1 ? 'present' : 'missing'}, ${rows} top-level bundle row`
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
function reportFailure(state, pin, options, error) {
|
|
@@ -157,6 +157,8 @@ host/doctor.js
|
|
|
157
157
|
host/hmr.js
|
|
158
158
|
host/modeState.js
|
|
159
159
|
host/modeStore.js
|
|
160
|
+
host/preset-reviewer.js
|
|
161
|
+
host/preset-worker.js
|
|
160
162
|
host/presets.js
|
|
161
163
|
host/tool-registry.js
|
|
162
164
|
host/tower.js
|
|
@@ -171,6 +173,10 @@ locale/import/en.json
|
|
|
171
173
|
locale/import/zh.json
|
|
172
174
|
locale/leases/en.json
|
|
173
175
|
locale/leases/zh.json
|
|
176
|
+
locale/preset-reviewer/en.json
|
|
177
|
+
locale/preset-reviewer/zh.json
|
|
178
|
+
locale/preset-worker/en.json
|
|
179
|
+
locale/preset-worker/zh.json
|
|
174
180
|
locale/skills/en.json
|
|
175
181
|
locale/skills/zh.json
|
|
176
182
|
locale/tests/en.json
|