dsh-tower 2.4.2 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/COMPATIBILITY.md +2 -0
- package/CONTRIBUTING.md +5 -3
- package/GLOSSARY.md +73 -0
- package/PRINCIPLES.md +15 -7
- package/README.en.md +16 -15
- package/README.md +18 -10
- package/app/archive.js +16 -1
- package/app/board.js +16 -1
- package/app/gate.js +14 -0
- package/app/review.js +7 -0
- package/app/spawn-prechecks.js +30 -5
- package/app/tower.js +5 -5
- package/client.js +321 -0
- package/cordis.patch.yml +26 -9
- package/docs/adr/0004-surface-budget.md +10 -4
- package/docs/adr/0005-component-surface.md +222 -11
- package/docs/architecture/overview.md +12 -6
- package/docs/examples/first-mission.md +3 -2
- package/docs/faq.md +9 -7
- package/docs/guide/install-upgrade-migration.md +53 -7
- package/docs/guide/quickstart.md +91 -10
- package/docs/guide/release.md +60 -19
- package/docs/guide/scenarios-and-philosophy.md +35 -28
- package/docs/operations/security.md +11 -10
- package/docs/reference/blockers.md +12 -7
- package/docs/reference/component-surface.md +40 -21
- package/docs/reference/config.generated.md +6 -8
- package/docs/reference/config.md +36 -8
- package/docs/reference/doctor.md +11 -5
- package/docs/reference/errors.generated.md +1 -0
- package/docs/reference/errors.md +2 -1
- package/docs/reference/surface-budget.md +47 -12
- package/docs/reference/testing.md +1 -1
- package/docs/reference/tools.generated.md +5 -5
- package/docs/reference/tools.md +5 -5
- package/docs/zh/faq.md +7 -5
- package/docs/zh/guide/install-upgrade-migration.md +26 -7
- package/docs/zh/guide/quickstart.md +17 -10
- package/docs/zh/guide/scenarios-and-philosophy.md +23 -15
- package/docs/zh/operations/security.md +9 -8
- package/docs/zh/reference/blockers.md +11 -7
- package/docs/zh/reference/config.md +28 -8
- package/docs/zh/reference/doctor.md +7 -2
- package/docs/zh/reference/errors.md +2 -1
- package/docs/zh/reference/testing.md +2 -2
- package/docs/zh/reference/tools.md +6 -5
- package/domain/component-rules.js +216 -0
- package/domain/components.js +181 -53
- package/domain/config/schema.js +86 -28
- package/domain/contracts/outputs.js +2 -0
- package/domain/contracts/tool-inputs/missions.js +1 -1
- package/domain/errors.js +6 -0
- package/domain/gate.js +23 -5
- package/domain/integration/review.js +203 -9
- package/domain/scope.js +176 -5
- package/domain/surface.js +21 -7
- package/host/capability-archive.js +18 -0
- package/host/capability-board.js +18 -0
- package/host/capability-registry.js +4 -0
- package/host/config.js +40 -3
- package/host/doctor.js +68 -12
- package/host/presets.js +26 -8
- package/host/tool-registry.js +30 -5
- package/index.js +17 -6
- package/locale/archive/en.json +6 -0
- package/locale/archive/zh.json +6 -0
- package/locale/batch/en.json +1 -1
- package/locale/batch/zh.json +1 -1
- package/locale/board/en.json +6 -0
- package/locale/board/zh.json +6 -0
- package/locale/en.json +37 -22
- package/locale/hmr/en.json +2 -2
- package/locale/hmr/zh.json +1 -1
- package/locale/import/en.json +1 -1
- package/locale/import/zh.json +1 -1
- package/locale/leases/en.json +2 -2
- package/locale/leases/zh.json +2 -2
- package/locale/preset-reviewer/en.json +1 -1
- package/locale/preset-reviewer/zh.json +1 -1
- package/locale/preset-worker/en.json +1 -1
- package/locale/preset-worker/zh.json +1 -1
- package/locale/skills/en.json +1 -1
- package/locale/skills/zh.json +1 -1
- package/locale/tests/en.json +2 -2
- package/locale/tests/zh.json +2 -2
- package/locale/zh.json +36 -21
- package/package.json +17 -2
- package/scripts/check-components.mjs +698 -44
- package/scripts/check-pack.mjs +3 -0
- package/scripts/component-surface.json +101 -37
- package/scripts/generate-capability-locale.mjs +3 -0
- package/scripts/mount-smoke/surface.mjs +2 -0
- package/scripts/pack-manifest.txt +10 -0
- package/scripts/release-notes.mjs +256 -0
- package/scripts/surface-baseline.json +39 -3
- package/scripts/surface-baseline.mjs +15 -5
- package/scripts/types-coverage.mjs +7 -0
- package/skills.js +4 -1
- package/tools/complete.js +3 -1
- package/tools/preflight.js +1 -1
- package/tools/review.js +1 -1
- package/tools/spawn.js +7 -7
- package/tools/status.js +8 -1
- package/tools/test.js +7 -7
- package/types/index.d.ts +10 -0
- package/ui-bundle/locale/en.json +1 -1
- package/ui-bundle/locale/zh.json +1 -1
package/client.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// dsh-tower — core Web Client half.
|
|
2
|
+
//
|
|
3
|
+
// The Host half of the bundle declares the profile config schema; this module
|
|
4
|
+
// adds the one editable control the Plugins page needs: a section in the
|
|
5
|
+
// `plugins.bundle.config` keyed slot for the `dsh-tower` bundle. The section is
|
|
6
|
+
// a single three-option radio group (off / optional / required) rendered from
|
|
7
|
+
// the Host-owned settings form. There is deliberately no component row for the
|
|
8
|
+
// review policy: rows are on/off capabilities, while the policy is one value
|
|
9
|
+
// with three states.
|
|
10
|
+
//
|
|
11
|
+
// Saving goes through `ctx.configForms.get('tower-host')` (the Host entry id in
|
|
12
|
+
// cordis.patch.yml). One `mutate([...])` call writes both review keys:
|
|
13
|
+
// `review.mode` becomes the selected value and the deprecated scalar alias
|
|
14
|
+
// `review.requireReview` is normalized to `false`, so the stored profile patch
|
|
15
|
+
// can never contain a contradiction. The Host schema marks both keys volatile, so
|
|
16
|
+
// the accepted values are reflected in place without reloading the plugin.
|
|
17
|
+
//
|
|
18
|
+
// The displayed selection is the *resolved* policy, not just `review.mode`: the
|
|
19
|
+
// alias decides when `mode` is absent, exactly like `resolveReviewPolicy` in
|
|
20
|
+
// domain/integration/review.js.
|
|
21
|
+
//
|
|
22
|
+
// No build step, no new dependency, no Host/Client package imports: React comes
|
|
23
|
+
// from the browser module table and theme colors use `--dsw-alias-*` tokens.
|
|
24
|
+
window.__ModuleLoader__.load({
|
|
25
|
+
id: 'dsh-tower',
|
|
26
|
+
factory(require) {
|
|
27
|
+
const React = require('react')
|
|
28
|
+
const h = React.createElement
|
|
29
|
+
const NS = 'dsh-tower'
|
|
30
|
+
const HOST_ENTRY = 'tower-host'
|
|
31
|
+
const MODES = ['off', 'optional', 'required']
|
|
32
|
+
const DEFAULT_MODE = 'optional'
|
|
33
|
+
|
|
34
|
+
const EN = {
|
|
35
|
+
'config.title': 'Review policy',
|
|
36
|
+
'config.intro': 'Choose how Tower treats review for this profile. This choice is saved immediately.',
|
|
37
|
+
'config.current': 'Current choice: {mode}',
|
|
38
|
+
'config.loading': 'Loading the saved review policy…',
|
|
39
|
+
'config.unavailable': 'The review policy cannot be changed here right now. Try again later, or open the Tower host settings.',
|
|
40
|
+
'config.readonly': 'This review policy is read-only in the current profile.',
|
|
41
|
+
'config.saving': 'Saving…',
|
|
42
|
+
'config.saved': 'Saved.',
|
|
43
|
+
'config.saveFailed': 'The new choice could not be saved; the previous policy is still in effect.',
|
|
44
|
+
'config.off.label': 'Off',
|
|
45
|
+
'config.off.description': 'Review is unavailable: no review can be recorded, and merges never wait for one.',
|
|
46
|
+
'config.optional.label': 'Optional',
|
|
47
|
+
'config.optional.description': 'Review is available, but it never blocks a merge.',
|
|
48
|
+
'config.required.label': 'Required',
|
|
49
|
+
'config.required.description': 'A clean review at the branch tip blocks merge; the reviewer preset is required.',
|
|
50
|
+
'config.restartHint':
|
|
51
|
+
'This profile started with review off, so the merge gate already uses the new choice but the review tool appears only after a restart (or a profile reload).',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ZH = {
|
|
55
|
+
'config.title': '审查策略',
|
|
56
|
+
'config.intro': '选择 Tower 为此配置处理审查的方式,选择后会立即保存。',
|
|
57
|
+
'config.current': '当前选择:{mode}',
|
|
58
|
+
'config.loading': '正在加载已保存的审查策略…',
|
|
59
|
+
'config.unavailable': '暂时无法在此修改审查策略,请稍后再试,或打开 Tower 主机设置。',
|
|
60
|
+
'config.readonly': '当前配置下审查策略为只读。',
|
|
61
|
+
'config.saving': '正在保存…',
|
|
62
|
+
'config.saved': '已保存。',
|
|
63
|
+
'config.saveFailed': '新选择未能保存,仍使用之前的审查策略。',
|
|
64
|
+
'config.off.label': '关闭',
|
|
65
|
+
'config.off.description': '审查不可用:无法记录审查,合并也不会等待审查。',
|
|
66
|
+
'config.optional.label': '可选',
|
|
67
|
+
'config.optional.description': '审查可用,但不会阻止合并。',
|
|
68
|
+
'config.required.label': '必需',
|
|
69
|
+
'config.required.description': '分支末端的干净审查会阻止合并;需要审查代理预设。',
|
|
70
|
+
'config.restartHint':
|
|
71
|
+
'此配置启动时审查为关闭状态:合并门禁已使用新的选择,但审查工具需要重启(或重新加载配置)后才会出现。',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fill(text, params) {
|
|
75
|
+
return String(text ?? '').replace(/\{([A-Za-z0-9_]+)\}/g, (match, key) =>
|
|
76
|
+
params && params[key] !== undefined ? String(params[key]) : match,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeT(props) {
|
|
81
|
+
const seat = props && typeof props.t === 'function' ? props.t : null
|
|
82
|
+
return function t(key, params) {
|
|
83
|
+
if (seat) {
|
|
84
|
+
try {
|
|
85
|
+
const value = seat(key, params)
|
|
86
|
+
if (typeof value === 'string') return value
|
|
87
|
+
} catch {
|
|
88
|
+
/* fall back to the bundled English dictionary */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return fill(EN[key] !== undefined ? EN[key] : key, params)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readForm(props) {
|
|
96
|
+
return props && props.form && typeof props.form.getSnapshot === 'function' ? props.form : null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function safeSnapshot(form) {
|
|
100
|
+
if (!form) return null
|
|
101
|
+
try {
|
|
102
|
+
return form.getSnapshot() ?? null
|
|
103
|
+
} catch {
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The resolved policy: an explicit `mode` wins; otherwise the deprecated
|
|
109
|
+
// boolean alias decides (true -> required, false -> optional), like
|
|
110
|
+
// `resolveReviewPolicy`. Only a literal `true` can raise the mode, which is
|
|
111
|
+
// also the only value the resolver accepts beside a non-required mode.
|
|
112
|
+
function currentMode(snapshot) {
|
|
113
|
+
const review = (snapshot && snapshot.value && snapshot.value.review) || {}
|
|
114
|
+
if (MODES.includes(review.mode)) return review.mode
|
|
115
|
+
return review.requireReview === true ? 'required' : DEFAULT_MODE
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Both review keys are written together: the selected mode, plus the alias
|
|
119
|
+
// normalized to `false`. An explicit `false` is required — leaving the key
|
|
120
|
+
// unset would restore the inherited default, and a stale `true` would
|
|
121
|
+
// contradict a non-required mode and make `resolveTowerConfig` throw.
|
|
122
|
+
function reviewOps(mode) {
|
|
123
|
+
return [
|
|
124
|
+
{ op: 'set', path: ['review', 'mode'], value: mode },
|
|
125
|
+
{ op: 'set', path: ['review', 'requireReview'], value: false },
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function ReviewPolicySection(props) {
|
|
130
|
+
const t = makeT(props)
|
|
131
|
+
const form = readForm(props)
|
|
132
|
+
const [snapshot, setSnapshot] = React.useState(function () {
|
|
133
|
+
return safeSnapshot(form)
|
|
134
|
+
})
|
|
135
|
+
const [pending, setPending] = React.useState(null)
|
|
136
|
+
const [notice, setNotice] = React.useState(null)
|
|
137
|
+
// The policy this page first saw. A session whose profile started with
|
|
138
|
+
// review off never registered the review tool, so switching to
|
|
139
|
+
// optional/required takes effect at the merge gate immediately but needs a
|
|
140
|
+
// restart (or profile reload) before the tool exists. The control states
|
|
141
|
+
// that limitation instead of pretending the tool appeared.
|
|
142
|
+
const startMode = React.useRef(null)
|
|
143
|
+
|
|
144
|
+
React.useEffect(
|
|
145
|
+
function () {
|
|
146
|
+
if (!form) return undefined
|
|
147
|
+
const update = function () {
|
|
148
|
+
const next = safeSnapshot(form)
|
|
149
|
+
if (startMode.current === null && next && next.status === 'ready') {
|
|
150
|
+
startMode.current = currentMode(next)
|
|
151
|
+
}
|
|
152
|
+
setSnapshot(next)
|
|
153
|
+
}
|
|
154
|
+
update()
|
|
155
|
+
let unsubscribe
|
|
156
|
+
try {
|
|
157
|
+
unsubscribe = form.subscribe(update)
|
|
158
|
+
} catch {
|
|
159
|
+
unsubscribe = undefined
|
|
160
|
+
}
|
|
161
|
+
return function () {
|
|
162
|
+
if (typeof unsubscribe === 'function') unsubscribe()
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
[form],
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
const status = snapshot && typeof snapshot.status === 'string' ? snapshot.status : 'unavailable'
|
|
169
|
+
const writable = status === 'ready' && snapshot.writable !== false
|
|
170
|
+
const selection = currentMode(snapshot)
|
|
171
|
+
const restartNeeded = notice === 'saved' && startMode.current === 'off' && selection !== 'off'
|
|
172
|
+
|
|
173
|
+
function choose(next) {
|
|
174
|
+
if (!form || !writable || pending || next === selection) return
|
|
175
|
+
setPending(next)
|
|
176
|
+
setNotice(null)
|
|
177
|
+
let result
|
|
178
|
+
try {
|
|
179
|
+
result = form.mutate(reviewOps(next), snapshot ? snapshot.revision : undefined)
|
|
180
|
+
} catch {
|
|
181
|
+
setPending(null)
|
|
182
|
+
setNotice('saveFailed')
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
Promise.resolve(result).then(
|
|
186
|
+
function (accepted) {
|
|
187
|
+
setPending(null)
|
|
188
|
+
setSnapshot(safeSnapshot(form))
|
|
189
|
+
setNotice(accepted === false ? 'saveFailed' : 'saved')
|
|
190
|
+
},
|
|
191
|
+
function () {
|
|
192
|
+
setPending(null)
|
|
193
|
+
setSnapshot(safeSnapshot(form))
|
|
194
|
+
setNotice('saveFailed')
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const statusText =
|
|
200
|
+
status === 'loading'
|
|
201
|
+
? t('config.loading')
|
|
202
|
+
: status !== 'ready'
|
|
203
|
+
? t('config.unavailable')
|
|
204
|
+
: snapshot && snapshot.writable === false
|
|
205
|
+
? t('config.readonly')
|
|
206
|
+
: ''
|
|
207
|
+
|
|
208
|
+
return h(
|
|
209
|
+
'section',
|
|
210
|
+
{
|
|
211
|
+
'data-dsh-tower-review-policy': '',
|
|
212
|
+
style: {
|
|
213
|
+
display: 'grid',
|
|
214
|
+
gap: '10px',
|
|
215
|
+
padding: '16px',
|
|
216
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
217
|
+
borderRadius: '8px',
|
|
218
|
+
background: 'var(--dsw-alias-bg-module-platform)',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
h('div', { style: { color: 'var(--dsw-alias-label-primary)', fontWeight: 600 } }, t('config.title')),
|
|
222
|
+
h('p', { style: { margin: 0, color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, t('config.intro')),
|
|
223
|
+
h(
|
|
224
|
+
'div',
|
|
225
|
+
{
|
|
226
|
+
role: 'radiogroup',
|
|
227
|
+
'aria-label': t('config.title'),
|
|
228
|
+
style: { display: 'grid', gap: '8px' },
|
|
229
|
+
},
|
|
230
|
+
MODES.map(function (mode) {
|
|
231
|
+
const active = selection === mode
|
|
232
|
+
return h(
|
|
233
|
+
'label',
|
|
234
|
+
{
|
|
235
|
+
key: mode,
|
|
236
|
+
'data-mode': mode,
|
|
237
|
+
style: {
|
|
238
|
+
display: 'grid',
|
|
239
|
+
gridTemplateColumns: 'auto 1fr',
|
|
240
|
+
gap: '8px',
|
|
241
|
+
alignItems: 'start',
|
|
242
|
+
padding: '8px',
|
|
243
|
+
borderRadius: '6px',
|
|
244
|
+
cursor: writable && !pending ? 'pointer' : 'default',
|
|
245
|
+
border: active ? '1px solid var(--dsw-alias-label-primary)' : '1px solid var(--dsw-alias-border-l2)',
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
h('input', {
|
|
249
|
+
type: 'radio',
|
|
250
|
+
name: 'dsh-tower-review-policy',
|
|
251
|
+
value: mode,
|
|
252
|
+
checked: active,
|
|
253
|
+
disabled: !writable || Boolean(pending),
|
|
254
|
+
onChange: function () {
|
|
255
|
+
choose(mode)
|
|
256
|
+
},
|
|
257
|
+
}),
|
|
258
|
+
h(
|
|
259
|
+
'span',
|
|
260
|
+
{ style: { display: 'grid', gap: '2px' } },
|
|
261
|
+
h('span', { style: { color: 'var(--dsw-alias-label-primary)', fontWeight: 500 } }, t(`config.${mode}.label`)),
|
|
262
|
+
h('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, t(`config.${mode}.description`)),
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
}),
|
|
266
|
+
),
|
|
267
|
+
statusText
|
|
268
|
+
? h('p', { style: { margin: 0, color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, statusText)
|
|
269
|
+
: null,
|
|
270
|
+
h(
|
|
271
|
+
'p',
|
|
272
|
+
{
|
|
273
|
+
role: 'status',
|
|
274
|
+
'aria-live': 'polite',
|
|
275
|
+
style: { margin: 0, color: 'var(--dsw-alias-label-secondary)', fontSize: '13px', minHeight: '18px' },
|
|
276
|
+
},
|
|
277
|
+
pending ? t('config.saving') : notice ? t(`config.${notice}`) : t('config.current', { mode: t(`config.${selection}.label`) }),
|
|
278
|
+
),
|
|
279
|
+
restartNeeded
|
|
280
|
+
? h(
|
|
281
|
+
'p',
|
|
282
|
+
{
|
|
283
|
+
'data-dsh-tower-restart-hint': '',
|
|
284
|
+
style: { margin: 0, color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' },
|
|
285
|
+
},
|
|
286
|
+
t('config.restartHint'),
|
|
287
|
+
)
|
|
288
|
+
: null,
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
inject: ['slots', 'locale'],
|
|
294
|
+
async apply(ctx) {
|
|
295
|
+
ctx.effect(function () {
|
|
296
|
+
return ctx.locale.register(NS, { en: EN, zh: ZH })
|
|
297
|
+
}, 'dsh-tower: review policy settings')
|
|
298
|
+
ctx.slots.inject('plugins.bundle.config', function () {
|
|
299
|
+
return ctx.slots.register(
|
|
300
|
+
{
|
|
301
|
+
name: 'plugins.bundle.config',
|
|
302
|
+
key: 'dsh-tower',
|
|
303
|
+
locale: NS,
|
|
304
|
+
inject: function () {
|
|
305
|
+
let forms
|
|
306
|
+
try {
|
|
307
|
+
forms = typeof ctx.get === 'function' ? ctx.get('configForms') : undefined
|
|
308
|
+
} catch {
|
|
309
|
+
forms = undefined
|
|
310
|
+
}
|
|
311
|
+
const form = forms && typeof forms.get === 'function' ? forms.get(HOST_ENTRY) : undefined
|
|
312
|
+
return { form }
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
ReviewPolicySection,
|
|
316
|
+
)
|
|
317
|
+
})
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
})
|
package/cordis.patch.yml
CHANGED
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
#
|
|
9
9
|
# Policy: principle 11 in PRINCIPLES.md + docs/adr/0005-component-surface.md.
|
|
10
10
|
# The per-key answer is docs/reference/component-surface.md, generated from
|
|
11
|
-
# scripts/component-surface.json. `npm run docs:check:components`
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# `
|
|
11
|
+
# scripts/component-surface.json. `npm run docs:check:components` reads every
|
|
12
|
+
# shipped patch (this file and ui-bundle/cordis.patch.yml, which ships the
|
|
13
|
+
# tower-ui row) and refuses a row with no classification, a missing locale label,
|
|
14
|
+
# or a moved component count without a written decision. `tower_doctor` and
|
|
15
|
+
# `tower_recover` are part of the host and are never switches (#122); a capability
|
|
16
|
+
# row owns exactly one optional capability, and `tower_status` / `tower_doctor`
|
|
17
|
+
# name any row that is off.
|
|
16
18
|
- insert:
|
|
17
19
|
# Mandatory host anchor. Not an optional capability: off == uninstall.
|
|
18
20
|
- id: tower
|
|
@@ -66,13 +68,28 @@
|
|
|
66
68
|
name: 'dsh-tower/leases'
|
|
67
69
|
- id: tower-tests
|
|
68
70
|
name: 'dsh-tower/tests'
|
|
69
|
-
#
|
|
71
|
+
# #133: the MISSIONS.md board is a capability, not the old `docs.board` key.
|
|
72
|
+
# Off writes no board and leaves an existing one to go stale; every tool and
|
|
73
|
+
# gate keeps working. `docs.boardLocation` stays genuine tuning.
|
|
74
|
+
- id: tower-board
|
|
75
|
+
name: 'dsh-tower/board'
|
|
76
|
+
# #135: archival is a capability, not the old `archive.enabled` key. Off keeps
|
|
77
|
+
# terminal missions hot in state.json, so the hot state keeps growing; merges,
|
|
78
|
+
# gates and every other capability keep working and nothing is deleted.
|
|
79
|
+
# `archive.afterDays` / `archive.maxMissions` stay genuine tuning.
|
|
80
|
+
- id: tower-archive
|
|
81
|
+
name: 'dsh-tower/archive'
|
|
82
|
+
# #141: developer tooling, not an end-user capability switch. It stays shipped
|
|
83
|
+
# off; the locale copy states that this row exists for linked-profile
|
|
84
|
+
# development only, so the plugin page does not read as a missing feature.
|
|
70
85
|
- id: tower-hmr
|
|
71
86
|
name: 'dsh-tower/hmr'
|
|
72
87
|
disabled: true
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
88
|
+
# Optional capability row, usable while the review policy (`review.mode`) is
|
|
89
|
+
# off or optional. With `review.mode:required`, planning/mode entry refuse this
|
|
90
|
+
# row being off with an explanatory error. The row is not mutually exclusive
|
|
91
|
+
# with the policy: the policy is one tri-state control on the plugin page,
|
|
92
|
+
# not a component row.
|
|
76
93
|
- id: preset-tower-reviewer
|
|
77
94
|
name: 'dsh-tower/preset-reviewer'
|
|
78
95
|
config:
|
|
@@ -30,7 +30,9 @@ note for each niche key.
|
|
|
30
30
|
|
|
31
31
|
- Growth is visible in a diff instead of arriving one reasonable-looking key at a
|
|
32
32
|
time.
|
|
33
|
-
- A reduction is cheap
|
|
33
|
+
- A reduction is cheap for `sections` and `tools`: the baseline can be ratcheted
|
|
34
|
+
down with no ADR. Since #138 the config-key count is two-way like the component
|
|
35
|
+
count (see [ADR 0005](0005-component-surface.md)).
|
|
34
36
|
- The classification table is auditable, not decorative: the check fails when a
|
|
35
37
|
live key or tool is missing from it.
|
|
36
38
|
- The numbers are three lines in `tower_doctor` output, not a new dashboard.
|
|
@@ -53,8 +55,10 @@ extends the budget with a fourth — the number of user-visible component rows i
|
|
|
53
55
|
`cordis.patch.yml` — and records it as its own `components` baseline entry with a
|
|
54
56
|
two-way decision rule (a contraction needs a decision just like growth). The
|
|
55
57
|
three original counts are still measured by `npm run surface:check`; the
|
|
56
|
-
component count is validated by `npm run docs:check:components`.
|
|
57
|
-
|
|
58
|
+
component count is validated by `npm run docs:check:components`. ADR 0005's wave
|
|
59
|
+
3 (#138) also makes `configKeys` two-way, so the cheap-shrink consequence above
|
|
60
|
+
now applies only to `sections` and `tools`. The historical decision above is
|
|
61
|
+
unchanged: "three surfaces" describes the 2.3.0 rule.
|
|
58
62
|
|
|
59
63
|
## Rejected alternatives
|
|
60
64
|
|
|
@@ -65,6 +69,8 @@ decision above is unchanged: "three surfaces" describes the 2.3.0 rule.
|
|
|
65
69
|
- **A per-key count in each generated reference** — two references can disagree;
|
|
66
70
|
one shared baseline plus one check keeps the counts equal.
|
|
67
71
|
- **Only an ADR requirement** — a pure deletion would need paperwork even though it
|
|
68
|
-
shrinks the surface; the matching-deletion path keeps
|
|
72
|
+
shrinks the surface; the matching-deletion path keeps `sections`/`tools`
|
|
73
|
+
reductions cheap. The config-key surface is the exception since #138: a removed
|
|
74
|
+
switch is itself a capability decision, so it needs a dated record (ADR 0005).
|
|
69
75
|
- **A GitHub issue label instead of a repo file** — the brake must run offline in
|
|
70
76
|
`npm test` and in CI without a token.
|
|
@@ -42,14 +42,17 @@ Adopt this shape and record it as principle 11:
|
|
|
42
42
|
a documented fallback stating what still works while it is off. A capability
|
|
43
43
|
qualifies only when its off state leaves the rest of Tower intact and is not
|
|
44
44
|
worse than uninstalling.
|
|
45
|
-
- **The safety nets are never switches.** The health contract, the recovery
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
- **The safety nets are never switches.** The health contract, the recovery escape
|
|
46
|
+
and the merge gates are configuration or code; no group, row or boolean key may
|
|
47
|
+
gate them. The review policy is configuration too — one tri-state control
|
|
48
|
+
(`off`/`optional`/`required`) on the plugin-page form, not a component row and
|
|
49
|
+
never one row per mode — while the review gate in required mode (clean,
|
|
50
|
+
tip-bound evidence and the exemption path) is contract and cannot be switched
|
|
51
|
+
off by a row. Enforcement covers both routes: a `cordis.patch.yml` row that
|
|
52
|
+
defaults any key classified `forbidden-to-disable` to `false` is refused, and
|
|
53
|
+
every `tools.expose*` route in `host/tool-registry.js` must match the
|
|
54
|
+
`gateableTools` allowlist in the source of truth, so a new route to a review,
|
|
55
|
+
merge or host tool fails even when that capability lists no tools of its own.
|
|
53
56
|
- **Config keys stay on the component axis.** Every config key is classified
|
|
54
57
|
`core`, `optional-component`, `optional-config` or `forbidden-to-disable` in
|
|
55
58
|
`docs/reference/component-surface.md`, generated from the single source of
|
|
@@ -80,9 +83,10 @@ The one `tower-bundle` group is gone. `cordis.patch.yml` now ships the mandatory
|
|
|
80
83
|
`tower` host anchor (the Host plugin and the worker preset are its internal
|
|
81
84
|
sections) plus the individually switchable top-level rows `tower-skills`,
|
|
82
85
|
`tower-batch`, `tower-import`, `tower-leases`, `tower-tests`, `tower-hmr`
|
|
83
|
-
(shipped off) and `preset-tower-reviewer` (conditional on
|
|
84
|
-
|
|
85
|
-
as a dated decision in
|
|
86
|
+
(shipped off) and `preset-tower-reviewer` (conditional on the review policy;
|
|
87
|
+
today the reviewer row may be off unless `review.mode` is `required`). The
|
|
88
|
+
visible component count moved 1 → 8 and is recorded as a dated decision in
|
|
89
|
+
`scripts/surface-baseline.json`.
|
|
86
90
|
|
|
87
91
|
The capability switches became rows: the seven `tools.expose*` keys and
|
|
88
92
|
`dev.hotReload` were deleted from the schema and are now rejected as unknown
|
|
@@ -128,3 +132,210 @@ this wave makes the shipped surface obey it instead of carrying debt.
|
|
|
128
132
|
- **A GitHub issue label or a docs-only policy.** The brake must run offline in
|
|
129
133
|
`npm test` and in CI without a token, and the policy must survive the closure
|
|
130
134
|
of the issue that introduced it.
|
|
135
|
+
|
|
136
|
+
## Addendum (wave 2): board, archive, UI and explicit development tooling
|
|
137
|
+
|
|
138
|
+
- Date: 2026-11-02
|
|
139
|
+
- Decisions: #132, #133, #134, #135, #141
|
|
140
|
+
|
|
141
|
+
Four components were component-shaped but sat outside the policy: `docs.board` and
|
|
142
|
+
`archive.enabled` were booleans in the config form, `tower-ui` shipped as a real
|
|
143
|
+
plugin-page row that no source-of-truth entry described, and the group rows never
|
|
144
|
+
stated their off consequence where a reader sees it. Wave 2 closes those gaps
|
|
145
|
+
without widening the mechanism.
|
|
146
|
+
|
|
147
|
+
- **`tower-board` (#133).** `docs.board` is deleted; the board is the optional
|
|
148
|
+
`tower-board` row with its own capability module. Its fallback: no `MISSIONS.md`
|
|
149
|
+
is written or refreshed, an existing board goes stale, and every state tool and
|
|
150
|
+
gate keeps working. `docs.boardLocation` stays a tuning key because it only
|
|
151
|
+
chooses a path. With the row off, the composition behaves like an all-Tower-off
|
|
152
|
+
profile for the board (nothing is written) while the rest of Tower keeps working.
|
|
153
|
+
- **`tower-archive` (#135).** `archive.enabled` is deleted; archival is the
|
|
154
|
+
optional `tower-archive` row. Its fallback is deliberately honest: terminal
|
|
155
|
+
missions stay hot in `state.json`, the hot state keeps growing, and `status` /
|
|
156
|
+
`doctor` keep loading their full history; merges and gates are unaffected and
|
|
157
|
+
nothing is deleted. `archive.afterDays` / `archive.maxMissions` stay tuning.
|
|
158
|
+
- **Removed keys fail loudly.** A config that still sets `docs.board` or
|
|
159
|
+
`archive.enabled` is rejected with `config-unknown-key` and an upgrade note that
|
|
160
|
+
names the replacing row and points here. There is no silent alias: a switch that
|
|
161
|
+
moved to the plugin page must not keep working from a config file.
|
|
162
|
+
- **`tower-ui` is classified (#134).** The UI plugin row is now a visible
|
|
163
|
+
component with a documented fallback (the web UI plugin does not load: no tool
|
|
164
|
+
cards, composer-dock summary or status strip; host tools, missions and the board
|
|
165
|
+
are unaffected). Because it ships in `ui-bundle/`, the gate reads **every**
|
|
166
|
+
shipped `cordis.patch.yml` (root and `ui-bundle/`), classifies every visible row
|
|
167
|
+
and enforces a label source for each: either a generated `<specifier>/locale`
|
|
168
|
+
row, or the row package's own `meta` label when the labels live there. A row
|
|
169
|
+
added to any shipped patch without a source-of-truth entry fails
|
|
170
|
+
`npm run docs:check:components`.
|
|
171
|
+
- **A group row states its consequence where it is read (#132).** The plugin page
|
|
172
|
+
renders the group row from the package `meta` block, so the off-equals-uninstall
|
|
173
|
+
consequence lives in the root `meta.description` in both languages, and the gate
|
|
174
|
+
pins it there instead of trusting only the `components` block. This is a label
|
|
175
|
+
fix, not a new mechanism.
|
|
176
|
+
- **`tower-hmr` stays shipped off (#141).** Moving it to a separate install path
|
|
177
|
+
would hide the switch and re-open the orphan-mount hazard; the row is kept as
|
|
178
|
+
explicit development tooling instead: its locale copy and its source-of-truth
|
|
179
|
+
fallback say that it exists for a linked dev profile, that a normal profile
|
|
180
|
+
behaves exactly as before, and that it is off on purpose.
|
|
181
|
+
- **Count decision.** The visible component count moved 8 → 9 → 10 → 11 across
|
|
182
|
+
#133, #135 and #134; the three dated steps are recorded in
|
|
183
|
+
`scripts/surface-baseline.json`. The config surface contracted 55 → 53, which the
|
|
184
|
+
other surfaces allow without a decision record.
|
|
185
|
+
|
|
186
|
+
## Addendum (wave 3): close the gate's blind spots
|
|
187
|
+
|
|
188
|
+
- Date: 2026-11-03
|
|
189
|
+
- Decisions: #138
|
|
190
|
+
|
|
191
|
+
Wave 2 made the gate read every shipped row, but four ways to evade it remained:
|
|
192
|
+
a class was free text, the operational route table was scraped with a regex, only
|
|
193
|
+
the patches named in `COMPONENT_PATCH_FILES` were read, and only booleans were
|
|
194
|
+
reviewed. Wave 3 closes them without changing the shipped decisions: 53 config
|
|
195
|
+
keys, 11 visible components.
|
|
196
|
+
|
|
197
|
+
- **The class is checked against the key's shape (#138.1).** A boolean that
|
|
198
|
+
removes a capability cannot file as `optional-config` by assertion. Every live
|
|
199
|
+
boolean must be `core`, `optional-component` or `forbidden-to-disable`, or an
|
|
200
|
+
`optional-config` key carrying a dated `exemption` whose reason names its
|
|
201
|
+
tuning-only nature. The same rule covers the non-boolean behaviour knobs below.
|
|
202
|
+
An exemption is only legal on a reviewed boolean/knob, and its `ref` must exist.
|
|
203
|
+
- **The config-key count is two-way (#138.1).** Wave 2's closing note ("the config
|
|
204
|
+
surface contracted 55 → 53, which the other surfaces allow without a decision
|
|
205
|
+
record") is retracted. Removing a switch is a capability decision, so
|
|
206
|
+
`configKeys` now joins `components` in `SURFACE_TWO_WAY_KEYS`: any later increase
|
|
207
|
+
or decrease needs a dated decision in `scripts/surface-baseline.json`. The gate
|
|
208
|
+
requires the `date` field itself (`YYYY-MM-DD`) on every decision of a two-way
|
|
209
|
+
surface, so a bare from/to record fails even when its chain reaches the value
|
|
210
|
+
(review of #138). The 64 → 53 history is recorded as one dated decision there;
|
|
211
|
+
sections and tools keep the ADR 0004 cheap-shrink rule.
|
|
212
|
+
- **The route table is imported, not scraped (#138.2).** `host/tool-registry.js`
|
|
213
|
+
exports `OPERATIONAL_TOOLS`, and the gate imports that module instead of
|
|
214
|
+
regex-matching its source. A missing file, a failed import, or a missing/non-Map
|
|
215
|
+
export is `registry-unreadable` and fails the gate. An empty Map is still the
|
|
216
|
+
correct determined state (#122): "nothing is gated" is distinct from "could not
|
|
217
|
+
read".
|
|
218
|
+
- **Every shipped patch is discovered (#138.3).** The gate walks the package for
|
|
219
|
+
`cordis.patch.yml` and compares the result with `COMPONENT_PATCH_FILES` in both
|
|
220
|
+
directions. An unlisted patch fails `patch-unclassified`; a listed patch that is
|
|
221
|
+
missing fails `patch-missing`. The row checks stay bidirectional: every shipped
|
|
222
|
+
row needs a source-of-truth entry, and every source-of-truth row needs a shipped
|
|
223
|
+
row.
|
|
224
|
+
- **Behaviour knobs and row overrides are reviewed (#138.4).** `spawn.requireLease`
|
|
225
|
+
and `tests.admission` are recorded in `behaviorKnobs` and carry tuning-only
|
|
226
|
+
exemptions: the optional `tower-leases` / `tower-tests` row owns the capability,
|
|
227
|
+
while these keys choose the safety level or timing inside it. A row's scalar
|
|
228
|
+
`config:` overrides are now parsed and must name a live config key and be listed
|
|
229
|
+
in that component's `allowedOverrides`. A row that sets `docs.board`,
|
|
230
|
+
`archive.enabled` or `tests.enabled` therefore fails as `row-override-unknown`
|
|
231
|
+
(those capabilities are rows), a row that enables or disables a key it does not
|
|
232
|
+
own fails as `row-override-foreign`, and `allowedOverrides` may never name a
|
|
233
|
+
forbidden-to-disable key. Fixtures reproduce the #133/#135/#136 shapes and a
|
|
234
|
+
foreign override, proving they fail before the fix passes.
|
|
235
|
+
|
|
236
|
+
## Addendum (wave 4): broad-audience labels and a copy gate
|
|
237
|
+
|
|
238
|
+
- Date: 2026-11-04
|
|
239
|
+
- Decisions: #137
|
|
240
|
+
|
|
241
|
+
Wave 3 made the gate fail closed, but every label it admitted still passed. The
|
|
242
|
+
plugin page leaked config keys (`gates.requireTests:true`, the deprecated
|
|
243
|
+
`review.requireReview` alias) and bare tool ids (`tower_batch`, `tower_resource`)
|
|
244
|
+
into the off-state copy, the
|
|
245
|
+
Chinese copy mixed 咨询式租约 / 重型工作 / untranslated `mission` and `preset`, and
|
|
246
|
+
the Web UI used 任务线 while the plugin page left `mission` in English. The counts
|
|
247
|
+
do not move: 53 config keys, 13 components, 11 visible.
|
|
248
|
+
|
|
249
|
+
- **The labels are rewritten for a reader, not for an operator (#137.1).** Every
|
|
250
|
+
visible row, the group row and the UI row now say what the capability is for
|
|
251
|
+
and what stops when it is off, in plain language. Config keys, tool ids, slash
|
|
252
|
+
commands and module specifiers are gone from `title`, `summary`, `disables` and
|
|
253
|
+
the two `meta` descriptions. The off copy says "refused with an explanation",
|
|
254
|
+
not "locked", and names Tower mode. The `tower-leases` fix also removes the F19
|
|
255
|
+
claim that "nothing else depends on a lease": a lease is coordination only, but
|
|
256
|
+
a worker configured to require one cannot start while the row is off; planning
|
|
257
|
+
and Tower-mode entry are refused with an explanation until the setting is
|
|
258
|
+
cleared or the row is re-enabled, and everything else keeps working.
|
|
259
|
+
- **One bilingual vocabulary, recorded (#137.2).** `GLOSSARY.md` is the single
|
|
260
|
+
table for mission/任务线, batch/批次, doctor/体检, board/看板, archive/归档,
|
|
261
|
+
preset/预设, worktree/工作树, review/审查, lease/租约, worker/执行代理 and
|
|
262
|
+
reviewer/审查代理. It also records the copy rules (two sentences, no
|
|
263
|
+
identifiers, truthful off effect, natural Chinese) and is a gate input: a label
|
|
264
|
+
that uses a term the table does not pair with its other-language field fails.
|
|
265
|
+
The Web client's own string catalogue shares this table but is migrated
|
|
266
|
+
separately; the runtime error copy in `host/presets.js` is likewise a follow-up.
|
|
267
|
+
- **The copy is checked mechanically (#137.3).** `checkLocaleCopy` runs over the
|
|
268
|
+
root locale pair, the ui-bundle meta pair and every generated row-locale pair.
|
|
269
|
+
It fails on config-key syntax, `tower_*` ids, `/tower` commands, `dsh-tower/*`
|
|
270
|
+
specifiers, empty fields, zh/en path drift and glossary mismatches in both
|
|
271
|
+
directions; the Chinese side may not leave glossary words untranslated.
|
|
272
|
+
`checkMetaRowCopy` pins the hand-written ui-bundle `meta.title` / `description`
|
|
273
|
+
to the root `components['tower-ui']` entry and the same composition the
|
|
274
|
+
generator renders, so the one row that ships its own label cannot drift.
|
|
275
|
+
`checkGlossaryFile` keeps `GLOSSARY.md` present and complete. Negative
|
|
276
|
+
fixtures in `tests/locale-copy.test.mjs` prove a raw key, a bare tool id and a
|
|
277
|
+
term mismatch fail in both languages before the fixed copy passes.
|
|
278
|
+
- **Review of #137 corrected three claims and hardened the guard.** The
|
|
279
|
+
`tower-tests` off copy promised that "merges … keep working", but
|
|
280
|
+
`domain/gate.js` emits the `tests-missing` merge blocker while the test
|
|
281
|
+
requirement is on and `host/presets.js` refuses planning and Tower-mode entry,
|
|
282
|
+
so the copy now says planning and entry are refused and merges stay blocked
|
|
283
|
+
until the requirement is cleared. The reviewer preset summary said "only used
|
|
284
|
+
when review before merging is required"; the row is usable while review is
|
|
285
|
+
optional and becomes a precondition for merging only when the requirement is
|
|
286
|
+
on, so the summary says that instead. The Chinese calques 锁状态的恢复出口,
|
|
287
|
+
收口, 一项命名工作, 在线状态文件 and 关闭该行 were replaced with 卡住的锁如何
|
|
288
|
+
恢复, 关闭该批次, 某一具体工作, 当前状态文件 and 关闭这一项. The guard now
|
|
289
|
+
normalizes full-width punctuation and matches case-insensitively, so
|
|
290
|
+
`Gates.requireTests`, `Tower_batch` and `gates。requireTests` fail; the worker
|
|
291
|
+
and reviewer role nouns accept an initial capital but exclude the product names
|
|
292
|
+
"Tower Worker" and "Tower Reviewer", in both the en→zh and zh→en directions.
|
|
293
|
+
`GLOSSARY.md` ships in the package (`package.json` `files`, pinned by
|
|
294
|
+
`scripts/pack-manifest.txt`) because it is a gate input, not repository-only
|
|
295
|
+
prose.
|
|
296
|
+
- **Rejected: a prose style guide with no gate.** It is what let wave 1–3 pass;
|
|
297
|
+
a reviewer's attention does not survive the next rename.
|
|
298
|
+
- **Rejected: translating the developer reference table.** The fallback text in
|
|
299
|
+
`scripts/component-surface.json` and the generated
|
|
300
|
+
`docs/reference/component-surface.md` is an operator reference for auditors, not
|
|
301
|
+
a plugin-page label. It may name keys and tools; the locale fields may not.
|
|
302
|
+
|
|
303
|
+
## Addendum (wave 5): the review policy is a tri-state control, not a component row
|
|
304
|
+
|
|
305
|
+
- Date: 2026-11-01
|
|
306
|
+
- Decisions: #M39, #M40, #M42
|
|
307
|
+
|
|
308
|
+
Waves 1–4 listed "the review contract" among the things that are never switches.
|
|
309
|
+
That wording was honest while review was a bare boolean, but it became wrong when
|
|
310
|
+
#M39 made the policy a three-value mode and #M40 put one control for it on the
|
|
311
|
+
plugin page. Wave 5 states the actual contract:
|
|
312
|
+
|
|
313
|
+
- **`review.mode` is the policy, not a capability row.** It has exactly three
|
|
314
|
+
values — `off`, `optional` (the default) and `required` — and the plugin-pages
|
|
315
|
+
control is one tri-state radio group on the `dsh-tower` configuration form.
|
|
316
|
+
There is no review-policy component row, and there are no three mutually
|
|
317
|
+
exclusive rows. `off` is supported and documented: `tower_review` is not
|
|
318
|
+
registered, reviewer spawn is refused, and the merge gate ignores review.
|
|
319
|
+
`optional` keeps `tower_review` and never blocks a merge. `required` keeps the
|
|
320
|
+
merge gate: a completed mission needs a clean review bound to the branch tip,
|
|
321
|
+
with the existing exemption path.
|
|
322
|
+
- **`review.requireReview` is a deprecated one-release alias.** It is classified
|
|
323
|
+
`core` (not `forbidden-to-disable`) because it is policy configuration and a
|
|
324
|
+
compatibility alias, not a switch a row may own. `true` resolves to `required`,
|
|
325
|
+
`false` to `optional`, and leaving it unset means `optional`. `true` next to an
|
|
326
|
+
explicit non-required mode is rejected; `false` never overrides an explicit
|
|
327
|
+
mode. The alias is removed after the 2.6 window.
|
|
328
|
+
- **What stays non-disableable is the gate in `required` mode.** The merge gate's
|
|
329
|
+
review evidence and tip binding, and the exemption path, may not be removed by
|
|
330
|
+
any group, row or boolean. The reviewer preset row stays an optional
|
|
331
|
+
capability: it may be off while the policy is `off` or `optional`, and under
|
|
332
|
+
`required` planning and mode entry refuse that combination with an explanation.
|
|
333
|
+
It is not a mutually exclusive row — the policy is not a row at all.
|
|
334
|
+
- **The gate enforces the shape.** A review-policy-shaped component row, a
|
|
335
|
+
component `allowedOverrides` or shipped row `config:` that names `review.mode`
|
|
336
|
+
(or the alias), a missing required-mode gate record, or a forbidden capability
|
|
337
|
+
that lists `preset-tower-reviewer` all fail
|
|
338
|
+
`npm run docs:check:components`.
|
|
339
|
+
- **Counts do not move.** 54 config keys, 11 visible components; only the class of
|
|
340
|
+
one deprecated key changes. The source of truth carries the decision date
|
|
341
|
+
(`2026-11-01`) and this addendum is its written decision.
|