dsh-tower 2.3.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/COMPATIBILITY.md +2 -0
- package/CONTRIBUTING.md +20 -3
- package/PRINCIPLES.md +18 -0
- package/README.en.md +29 -16
- package/README.md +22 -15
- package/cordis.patch.yml +56 -28
- package/docs/README.md +2 -1
- package/docs/adr/0004-surface-budget.md +20 -0
- package/docs/adr/0005-component-surface.md +130 -0
- package/docs/adr/README.md +1 -0
- package/docs/agents/workflow.md +1 -1
- package/docs/architecture/overview.md +69 -7
- package/docs/guide/hot-reload.md +4 -4
- package/docs/guide/install-upgrade-migration.md +13 -0
- package/docs/operations/security.md +1 -1
- package/docs/reference/component-surface.md +135 -0
- package/docs/reference/config.generated.md +2 -10
- package/docs/reference/config.md +11 -9
- package/docs/reference/doctor.md +14 -9
- package/docs/reference/surface-budget.md +52 -23
- package/docs/reference/tools.generated.md +4 -2
- package/docs/reference/tools.md +6 -3
- package/docs/zh/README.md +1 -1
- package/docs/zh/guide/hot-reload.md +3 -3
- package/docs/zh/guide/install-upgrade-migration.md +10 -0
- package/docs/zh/operations/security.md +1 -1
- package/docs/zh/reference/config.md +9 -9
- package/docs/zh/reference/tools.md +6 -4
- package/domain/components.js +641 -0
- package/domain/config/schema.js +37 -27
- package/domain/contracts/outputs.js +10 -0
- package/domain/errors.js +11 -1
- package/domain/mission.js +12 -1
- package/domain/surface.js +154 -55
- package/host/capability-batch.js +15 -0
- package/host/capability-hmr.js +30 -0
- package/host/capability-import.js +11 -0
- package/host/capability-leases.js +11 -0
- package/host/capability-registry.js +149 -0
- package/host/capability-tests.js +16 -0
- package/host/doctor.js +41 -9
- package/host/hmr.js +2 -4
- package/host/presets.js +51 -9
- package/host/tool-registry.js +150 -34
- package/index.js +32 -20
- package/locale/batch/en.json +6 -0
- package/locale/batch/zh.json +6 -0
- package/locale/en.json +31 -11
- package/locale/hmr/en.json +6 -0
- package/locale/hmr/zh.json +6 -0
- package/locale/import/en.json +6 -0
- package/locale/import/zh.json +6 -0
- package/locale/leases/en.json +6 -0
- package/locale/leases/zh.json +6 -0
- package/locale/skills/en.json +6 -0
- package/locale/skills/zh.json +6 -0
- package/locale/tests/en.json +6 -0
- package/locale/tests/zh.json +6 -0
- package/locale/zh.json +30 -10
- package/package.json +25 -4
- package/scripts/check-components.mjs +235 -0
- package/scripts/check-pack.mjs +4 -0
- package/scripts/component-surface.json +395 -0
- package/scripts/generate-capability-locale.mjs +148 -0
- package/scripts/generate-component-surface.mjs +59 -0
- package/scripts/generate-tools-reference.mjs +7 -25
- package/scripts/generate-types.mjs +127 -0
- package/scripts/lib/tool-surface.mjs +85 -0
- package/scripts/mount-smoke/fixtures.mjs +3 -3
- package/scripts/mount-smoke/surface.mjs +96 -27
- package/scripts/mount-smoke.mjs +3 -1
- package/scripts/pack-manifest.txt +30 -0
- package/scripts/surface-baseline.json +35 -4
- package/scripts/surface-baseline.mjs +32 -34
- package/scripts/types-coverage.mjs +184 -0
- package/scripts/types-strict-manifest.txt +10 -0
- package/scripts/types-strict.mjs +141 -0
- package/skills.js +8 -1
- package/tools/index.js +2 -1
- package/tools/reload.js +34 -35
- package/tools/status.js +8 -1
- package/types/index.d.ts +28 -12
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"date": "2026-10-23",
|
|
4
|
+
"adr": "docs/adr/0005-component-surface.md",
|
|
5
|
+
"classes": {
|
|
6
|
+
"core": "Removing it breaks the Tower contract. It is never a switch.",
|
|
7
|
+
"optional-component": "A capability whose off state leaves the rest of Tower intact. It must be a user-visible switch with a documented fallback.",
|
|
8
|
+
"optional-config": "A tuning knob, not a capability. It stays in the config form and never hides a component.",
|
|
9
|
+
"forbidden-to-disable": "The host, the health contract, the recovery escape, the review contract and the merge gates. No group, row or boolean key may turn it off."
|
|
10
|
+
},
|
|
11
|
+
"components": [
|
|
12
|
+
{
|
|
13
|
+
"id": "tower",
|
|
14
|
+
"scope": "visible",
|
|
15
|
+
"kind": "group",
|
|
16
|
+
"fallback": "Off equals uninstalling Tower: no tools, presets or skills remain. That is the accepted all-off boundary of the mandatory host anchor, not an optional capability."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "tower-host",
|
|
20
|
+
"scope": "internal",
|
|
21
|
+
"kind": "row",
|
|
22
|
+
"fallback": "The Host plugin itself, nested under the mandatory tower anchor. A mandatory dependency, never its own switch."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "tower-skills",
|
|
26
|
+
"scope": "visible",
|
|
27
|
+
"kind": "row",
|
|
28
|
+
"fallback": "The 3 skills leave the catalog; the worker/reviewer PROTOCOL sections still ship through systemPrompt.section, so only the tower-recovery guidance is lost."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "tower-batch",
|
|
32
|
+
"scope": "visible",
|
|
33
|
+
"kind": "row",
|
|
34
|
+
"fallback": "tower_batch is not registered; missions, gates and merges are unaffected because no state depends on a batch."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "tower-import",
|
|
38
|
+
"scope": "visible",
|
|
39
|
+
"kind": "row",
|
|
40
|
+
"fallback": "tower_import_legacy and tower_legacy_map are not registered; new repository state is unaffected."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "tower-leases",
|
|
44
|
+
"scope": "visible",
|
|
45
|
+
"kind": "row",
|
|
46
|
+
"fallback": "tower_resource is not registered; advisory leases are coordination only, so nothing else depends on them."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "tower-tests",
|
|
50
|
+
"scope": "visible",
|
|
51
|
+
"kind": "row",
|
|
52
|
+
"fallback": "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."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "tower-hmr",
|
|
56
|
+
"scope": "visible",
|
|
57
|
+
"kind": "row",
|
|
58
|
+
"fallback": "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."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "preset-tower-worker",
|
|
62
|
+
"scope": "internal",
|
|
63
|
+
"kind": "row",
|
|
64
|
+
"fallback": "tower_spawn fails without the worker preset. A mandatory dependency, never a switch."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "preset-tower-reviewer",
|
|
68
|
+
"scope": "visible",
|
|
69
|
+
"kind": "row",
|
|
70
|
+
"fallback": "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
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"gateableTools": {},
|
|
74
|
+
"keys": [
|
|
75
|
+
{
|
|
76
|
+
"name": "section",
|
|
77
|
+
"class": "core",
|
|
78
|
+
"note": "Legacy alias for mode.promptSection: the Tower-mode prompt contract, not a capability."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "allowAgentControl",
|
|
82
|
+
"class": "core",
|
|
83
|
+
"note": "Legacy alias for mode.allowAgentControl: the human-owned mode rule (ADR 0003), not a component."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "init.autoGitInit",
|
|
87
|
+
"class": "optional-config",
|
|
88
|
+
"note": "Bootstrap convenience; off only means tower_init will not run git init."
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "init.snapshotDirty",
|
|
92
|
+
"class": "optional-config",
|
|
93
|
+
"note": "Dirty-work baseline convenience; no capability is removed."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "init.autoExclude",
|
|
97
|
+
"class": "optional-config",
|
|
98
|
+
"note": "Keeps Tower artifacts out of .git/info/exclude; safe either way."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "mode.promptSection",
|
|
102
|
+
"class": "core",
|
|
103
|
+
"note": "Replaces the whole Tower-mode prompt text; contract text, not a capability."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "mode.sectionAppend",
|
|
107
|
+
"class": "optional-config",
|
|
108
|
+
"note": "Appends team conventions to the prompt."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "mode.allowAgentControl",
|
|
112
|
+
"class": "core",
|
|
113
|
+
"note": "Decides whether the model may toggle the mode (ADR 0003). A governance rule, never a component."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "mode.autoPauseGoal",
|
|
117
|
+
"class": "core",
|
|
118
|
+
"note": "Entering the mode pauses an active goal; exit never resumes it."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "mode.requireResumeAuthorization",
|
|
122
|
+
"class": "core",
|
|
123
|
+
"note": "Guards resuming a user-paused mission."
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "mode.requireHumanAckOnFirstEntry",
|
|
127
|
+
"class": "core",
|
|
128
|
+
"note": "First entry can require an explicit human ack."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "mode.workspaceLock",
|
|
132
|
+
"class": "core",
|
|
133
|
+
"note": "One Tower session per repository is a safety contract; a row may not turn it off."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "mode.workspaceLockScope",
|
|
137
|
+
"class": "optional-config",
|
|
138
|
+
"note": "Chooses whether read-only tools take the workspace lock."
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"name": "mode.planModePolicy",
|
|
142
|
+
"class": "core",
|
|
143
|
+
"note": "Plan mode and Tower mode stay mutually exclusive."
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "mode.workspaceStaleMs",
|
|
147
|
+
"class": "optional-config",
|
|
148
|
+
"note": "Stale window for the advisory workspace lock."
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "mode.heartbeatMs",
|
|
152
|
+
"class": "optional-config",
|
|
153
|
+
"note": "Heartbeat interval while the mode is on."
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "mode.denyTools",
|
|
157
|
+
"class": "core",
|
|
158
|
+
"note": "The denied-tool list is part of the mode guard."
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"name": "mode.storePath",
|
|
162
|
+
"class": "optional-config",
|
|
163
|
+
"note": "Custom mode-store location; no capability is removed."
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "mode.maxEntries",
|
|
167
|
+
"class": "optional-config",
|
|
168
|
+
"note": "Bounds the persisted mode store."
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "mode.lockTimeoutMs",
|
|
172
|
+
"class": "optional-config",
|
|
173
|
+
"note": "Bounded wait for the cross-process mode lock."
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"name": "review.requireReview",
|
|
177
|
+
"class": "forbidden-to-disable",
|
|
178
|
+
"note": "Turns independent review into a merge blocker; no component may switch it off. With it on, the preset-tower-reviewer row may not be off (planning/mode entry refuse the combination)."
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"name": "review.maxRoundsPerReviewer",
|
|
182
|
+
"class": "forbidden-to-disable",
|
|
183
|
+
"note": "Caps review rounds; part of the review contract."
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"name": "merge.timeoutMs",
|
|
187
|
+
"class": "optional-config",
|
|
188
|
+
"note": "Default merge timeout; a per-call value wins."
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "merge.maxBufferBytes",
|
|
192
|
+
"class": "optional-config",
|
|
193
|
+
"note": "Captured merge output buffer."
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "git.timeoutMs",
|
|
197
|
+
"class": "optional-config",
|
|
198
|
+
"note": "Default Git command timeout outside merge."
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"name": "worktrees.root",
|
|
202
|
+
"class": "optional-config",
|
|
203
|
+
"note": "Default worktree placement."
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"name": "spawn.workerPreset",
|
|
207
|
+
"class": "core",
|
|
208
|
+
"note": "Chooses the worker tool surface; a dependency, never a switch."
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "spawn.reviewerPreset",
|
|
212
|
+
"class": "core",
|
|
213
|
+
"note": "Chooses the read-only reviewer surface; a dependency, never a switch."
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"name": "spawn.maxDepth",
|
|
217
|
+
"class": "optional-config",
|
|
218
|
+
"note": "Subagent depth requested at spawn."
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"name": "spawn.provider",
|
|
222
|
+
"class": "optional-config",
|
|
223
|
+
"note": "Provider route for spawned agents."
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "spawn.workerModel",
|
|
227
|
+
"class": "optional-config",
|
|
228
|
+
"note": "Optional model route for workers."
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"name": "spawn.reviewerModel",
|
|
232
|
+
"class": "optional-config",
|
|
233
|
+
"note": "Optional independent model route for reviewers."
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"name": "spawn.maxLiveWorkers",
|
|
237
|
+
"class": "core",
|
|
238
|
+
"note": "Resource-safety cap on live workers."
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"name": "spawn.staleMs",
|
|
242
|
+
"class": "optional-config",
|
|
243
|
+
"note": "Roster staleness window before merge is blocked."
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"name": "spawn.maxWaitMs",
|
|
247
|
+
"class": "optional-config",
|
|
248
|
+
"note": "Upper bound for a blocking spawn wait."
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"name": "spawn.requireLease",
|
|
252
|
+
"class": "optional-config",
|
|
253
|
+
"note": "Advisory-lease gate before a worker starts."
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"name": "send.maxPerMinute",
|
|
257
|
+
"class": "optional-config",
|
|
258
|
+
"note": "Message rate limit; zero disables it."
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"name": "send.interruptDefault",
|
|
262
|
+
"class": "optional-config",
|
|
263
|
+
"note": "Default for the interrupt-before-deliver flag."
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"name": "tests.enabled",
|
|
267
|
+
"class": "optional-component",
|
|
268
|
+
"note": "Tuning inside the optional tower-tests component: it decides whether discovery runs when the row is on. Off with gates.requireTests:true is rejected at config resolution."
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"name": "tests.admission",
|
|
272
|
+
"class": "optional-config",
|
|
273
|
+
"note": "Chooses when discovery runs inside the optional tests capability."
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"name": "tests.timeoutMs",
|
|
277
|
+
"class": "optional-config",
|
|
278
|
+
"note": "Per-command discovery timeout."
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"name": "tests.maxBytes",
|
|
282
|
+
"class": "optional-config",
|
|
283
|
+
"note": "Captured discovery output cap."
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"name": "tests.autoChecks",
|
|
287
|
+
"class": "optional-config",
|
|
288
|
+
"note": "Attaches a tests check to completion when discovery runs."
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"name": "tests.allowPackageDiscovery",
|
|
292
|
+
"class": "core",
|
|
293
|
+
"note": "Allows repository code to execute; a security contract, not a component."
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"name": "gates.requireChecks",
|
|
297
|
+
"class": "forbidden-to-disable",
|
|
298
|
+
"note": "Requires completion evidence before merge; a merge gate."
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"name": "gates.requireTests",
|
|
302
|
+
"class": "forbidden-to-disable",
|
|
303
|
+
"note": "Requires a test receipt before merge; a merge gate. With the tower-tests row off, planning/mode entry refuse the combination."
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"name": "gates.allowReviewExemption",
|
|
307
|
+
"class": "forbidden-to-disable",
|
|
308
|
+
"note": "The review-exemption path is part of the review and merge contract."
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"name": "gates.maxReportBytes",
|
|
312
|
+
"class": "forbidden-to-disable",
|
|
313
|
+
"note": "The report cap is part of the completion contract; the refusal itself is never configurable away."
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"name": "docs.board",
|
|
317
|
+
"class": "optional-config",
|
|
318
|
+
"note": "Writes the human-readable board; no capability is removed."
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"name": "docs.boardLocation",
|
|
322
|
+
"class": "optional-config",
|
|
323
|
+
"note": "Chooses the board location."
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"name": "archive.enabled",
|
|
327
|
+
"class": "optional-config",
|
|
328
|
+
"note": "Archival is maintenance, not a tool capability; off only leaves terminal missions hot."
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"name": "archive.afterDays",
|
|
332
|
+
"class": "optional-config",
|
|
333
|
+
"note": "Archive age threshold."
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"name": "archive.maxMissions",
|
|
337
|
+
"class": "optional-config",
|
|
338
|
+
"note": "Hot terminal-mission cap."
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"name": "leases.ttlMs",
|
|
342
|
+
"class": "optional-config",
|
|
343
|
+
"note": "Tuning inside the optional tower-leases component."
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"name": "dev.reloadDelayMs",
|
|
347
|
+
"class": "optional-config",
|
|
348
|
+
"note": "Delay so a tool result is delivered before a scheduled reload inside the optional tower-hmr component."
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"name": "dev.reloadRequiredIds",
|
|
352
|
+
"class": "optional-config",
|
|
353
|
+
"note": "Loader ids passed to reconcileProfile as required by the tower-hmr component."
|
|
354
|
+
}
|
|
355
|
+
],
|
|
356
|
+
"forbidden": [
|
|
357
|
+
{
|
|
358
|
+
"id": "host",
|
|
359
|
+
"capability": "the Tower host",
|
|
360
|
+
"reason": "Without the host the plugin registers nothing; there is no off state that leaves the rest working, and no config boolean may remove it. The visible `tower` anchor is the all-off boundary, and the host section nested under it may never become its own switch.",
|
|
361
|
+
"tools": [],
|
|
362
|
+
"rowIds": ["tower-host"]
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"id": "health",
|
|
366
|
+
"capability": "tower_doctor (read-only health contract)",
|
|
367
|
+
"reason": "Diagnosis must always exist so a failure message can point at it. #122 removed exposeDoctor and the exposeOperational master switch; the tool is registered for every configuration.",
|
|
368
|
+
"tools": ["tower_doctor"],
|
|
369
|
+
"rowIds": ["tower-doctor"]
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"id": "recovery",
|
|
373
|
+
"capability": "tower_recover (stale state-lock escape)",
|
|
374
|
+
"reason": "The escape hatch must exist whenever the lock it repairs can be left behind. #122 removed exposeRecovery and the exposeOperational master switch; the tool is registered for every configuration.",
|
|
375
|
+
"tools": ["tower_recover"],
|
|
376
|
+
"rowIds": ["tower-recover"]
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"id": "review",
|
|
380
|
+
"capability": "the review contract",
|
|
381
|
+
"reason": "review.requireReview is a merge blocker, not a component; a row may not switch it off. The reviewer row is conditional, not a way to weaken the gate.",
|
|
382
|
+
"tools": ["tower_diff", "tower_preflight", "tower_review"],
|
|
383
|
+
"rowIds": ["tower-review"],
|
|
384
|
+
"gatedBy": ["review.requireReview", "review.maxRoundsPerReviewer"]
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"id": "merge-gates",
|
|
388
|
+
"capability": "the merge gates",
|
|
389
|
+
"reason": "gates.* decides delivery; a component may not bypass it.",
|
|
390
|
+
"tools": ["tower_complete", "tower_merge"],
|
|
391
|
+
"rowIds": ["tower-gates", "tower-merge-gates"],
|
|
392
|
+
"gatedBy": ["gates.requireChecks", "gates.requireTests", "gates.allowReviewExemption", "gates.maxReportBytes"]
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
}
|
|
@@ -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' })
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Generate the component-surface reference from the single source of truth
|
|
2
|
+
// (#120). The table is a rendering of `scripts/component-surface.json`; editing
|
|
3
|
+
// the markdown by hand fails `npm run docs:check:generated:components`.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node scripts/generate-component-surface.mjs [--root <dir>] [--check]
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import {
|
|
11
|
+
COMPONENT_REFERENCE_FILE,
|
|
12
|
+
COMPONENT_SURFACE_FILE,
|
|
13
|
+
parseComponentSurface,
|
|
14
|
+
renderComponentSurface,
|
|
15
|
+
} from '../domain/components.js'
|
|
16
|
+
import { readBaseline } from './lib/surface.mjs'
|
|
17
|
+
import { parseArgs, runMain } from './lib/cli.mjs'
|
|
18
|
+
|
|
19
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
20
|
+
const USAGE = `usage: node scripts/generate-component-surface.mjs [--root <dir>] [--check]
|
|
21
|
+
|
|
22
|
+
--root repository root to read and write (default: this package)
|
|
23
|
+
--check fail when the committed reference differs instead of writing it
|
|
24
|
+
--help print this text
|
|
25
|
+
|
|
26
|
+
The table is generated from ${COMPONENT_SURFACE_FILE}; do not edit
|
|
27
|
+
${COMPONENT_REFERENCE_FILE} by hand.`
|
|
28
|
+
|
|
29
|
+
function main(argv) {
|
|
30
|
+
const options = parseArgs(argv, {
|
|
31
|
+
initial: { root: PACKAGE_ROOT, check: false, help: false },
|
|
32
|
+
values: { '--root': { key: 'root', parse: path.resolve } },
|
|
33
|
+
flags: { '--check': 'check', '--help': 'help', '-h': 'help' },
|
|
34
|
+
})
|
|
35
|
+
if (options.help) {
|
|
36
|
+
console.log(USAGE)
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
const root = path.resolve(options.root)
|
|
40
|
+
const surface = parseComponentSurface(fs.readFileSync(path.join(root, COMPONENT_SURFACE_FILE), 'utf8'))
|
|
41
|
+
const baseline = readBaseline(root)
|
|
42
|
+
const expected = renderComponentSurface(surface, { baselineDate: baseline.date })
|
|
43
|
+
const target = path.join(root, COMPONENT_REFERENCE_FILE)
|
|
44
|
+
if (options.check) {
|
|
45
|
+
const actual = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
|
|
46
|
+
if (actual !== expected) {
|
|
47
|
+
console.error(`component reference is stale; run npm run docs:generate:components (${COMPONENT_REFERENCE_FILE})`)
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
console.log(`component reference: ${surface.keys.length} keys in sync (${COMPONENT_REFERENCE_FILE})`)
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
54
|
+
fs.writeFileSync(target, expected)
|
|
55
|
+
console.log(`wrote ${COMPONENT_REFERENCE_FILE} (${surface.keys.length} keys, ${surface.components.length} components)`)
|
|
56
|
+
return 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await runMain(import.meta.url, () => main(process.argv.slice(2)), { errorPrefix: 'components', usage: USAGE })
|
|
@@ -1,38 +1,19 @@
|
|
|
1
1
|
// Generate the tools overview from the registered tool definitions (#46).
|
|
2
|
+
//
|
|
3
|
+
// #118: use the shipped Host entry, not only the define() path, so the table and
|
|
4
|
+
// its header count include tower_mode/tower_control as well.
|
|
2
5
|
import fs from 'node:fs'
|
|
3
6
|
import path from 'node:path'
|
|
4
7
|
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import { registerTowerTools } from '../tools/index.js'
|
|
6
8
|
import { SURFACE_BASELINE_FILE, surfaceLine } from '../domain/surface.js'
|
|
9
|
+
import { loadShippedToolSurface } from './lib/tool-surface.mjs'
|
|
7
10
|
import { readBaseline } from './lib/surface.mjs'
|
|
8
11
|
|
|
9
12
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
10
13
|
const target = path.join(root, 'docs', 'reference', 'tools.generated.md')
|
|
11
14
|
const baseline = readBaseline(root)
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
get() {
|
|
15
|
-
return undefined
|
|
16
|
-
},
|
|
17
|
-
effect(fn) {
|
|
18
|
-
const dispose = fn()
|
|
19
|
-
return typeof dispose === 'function' ? dispose : () => {}
|
|
20
|
-
},
|
|
21
|
-
systemPrompt: { section() {} },
|
|
22
|
-
tools: {
|
|
23
|
-
register(definition) {
|
|
24
|
-
tools.set(definition.name, definition)
|
|
25
|
-
return () => {}
|
|
26
|
-
},
|
|
27
|
-
guard() {
|
|
28
|
-
return () => {}
|
|
29
|
-
},
|
|
30
|
-
restrict() {
|
|
31
|
-
return () => {}
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
}
|
|
35
|
-
registerTowerTools(ctx)
|
|
15
|
+
const surface = loadShippedToolSurface()
|
|
16
|
+
const tools = surface.definitions
|
|
36
17
|
const rows = [...tools.values()]
|
|
37
18
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
38
19
|
.map((tool) => `| \`${tool.name}\` | ${String(tool.description ?? '').replace(/\s+/g, ' ').trim()} |`)
|
|
@@ -52,6 +33,7 @@ const expected = `# Tools overview (generated)
|
|
|
52
33
|
|---|---|
|
|
53
34
|
${rows.join('\n')}
|
|
54
35
|
`
|
|
36
|
+
surface.dispose()
|
|
55
37
|
if (process.argv.includes('--check')) {
|
|
56
38
|
const actual = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
|
|
57
39
|
if (actual !== expected) {
|