dsh-tower 2.5.0 → 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 +32 -0
- package/COMPATIBILITY.md +1 -0
- package/CONTRIBUTING.md +1 -1
- package/GLOSSARY.md +6 -0
- package/PRINCIPLES.md +15 -7
- package/README.en.md +16 -15
- package/README.md +12 -10
- package/app/review.js +7 -0
- package/app/spawn-prechecks.js +12 -2
- package/app/tower.js +5 -5
- package/client.js +321 -0
- package/cordis.patch.yml +5 -3
- package/docs/adr/0005-component-surface.md +58 -13
- 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 +32 -7
- package/docs/guide/quickstart.md +20 -10
- 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 +14 -7
- package/docs/reference/config.generated.md +3 -2
- package/docs/reference/config.md +29 -3
- package/docs/reference/errors.generated.md +1 -0
- package/docs/reference/errors.md +2 -1
- package/docs/reference/surface-budget.md +20 -3
- package/docs/reference/testing.md +1 -1
- package/docs/reference/tools.generated.md +5 -5
- package/docs/reference/tools.md +3 -3
- package/docs/zh/faq.md +7 -5
- package/docs/zh/guide/install-upgrade-migration.md +25 -6
- 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 +21 -3
- package/docs/zh/reference/errors.md +2 -1
- package/docs/zh/reference/testing.md +2 -2
- package/docs/zh/reference/tools.md +3 -3
- package/domain/components.js +12 -4
- package/domain/config/schema.js +40 -5
- 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 +6 -3
- package/domain/integration/review.js +203 -9
- package/host/config.js +40 -3
- package/host/doctor.js +18 -7
- package/host/presets.js +9 -8
- package/host/tool-registry.js +9 -1
- package/package.json +7 -1
- package/scripts/check-components.mjs +120 -6
- package/scripts/check-pack.mjs +3 -0
- package/scripts/component-surface.json +16 -10
- package/scripts/pack-manifest.txt +1 -0
- package/scripts/surface-baseline.json +8 -1
- package/scripts/types-coverage.mjs +7 -0
- package/tools/preflight.js +1 -1
- package/tools/review.js +1 -1
- package/tools/spawn.js +2 -2
- package/tools/status.js +8 -1
- package/types/index.d.ts +10 -0
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
|
@@ -85,9 +85,11 @@
|
|
|
85
85
|
- id: tower-hmr
|
|
86
86
|
name: 'dsh-tower/hmr'
|
|
87
87
|
disabled: true
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
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.
|
|
91
93
|
- id: preset-tower-reviewer
|
|
92
94
|
name: 'dsh-tower/preset-reviewer'
|
|
93
95
|
config:
|
|
@@ -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
|
|
@@ -235,8 +239,9 @@ keys, 11 visible components.
|
|
|
235
239
|
- Decisions: #137
|
|
236
240
|
|
|
237
241
|
Wave 3 made the gate fail closed, but every label it admitted still passed. The
|
|
238
|
-
plugin page leaked config keys (`gates.requireTests:true`,
|
|
239
|
-
and bare tool ids (`tower_batch`, `tower_resource`)
|
|
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
|
|
240
245
|
Chinese copy mixed 咨询式租约 / 重型工作 / untranslated `mission` and `preset`, and
|
|
241
246
|
the Web UI used 任务线 while the plugin page left `mission` in English. The counts
|
|
242
247
|
do not move: 53 config keys, 13 components, 11 visible.
|
|
@@ -294,3 +299,43 @@ do not move: 53 config keys, 13 components, 11 visible.
|
|
|
294
299
|
`scripts/component-surface.json` and the generated
|
|
295
300
|
`docs/reference/component-surface.md` is an operator reference for auditors, not
|
|
296
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.
|
|
@@ -12,9 +12,14 @@ escape keep working.
|
|
|
12
12
|
|
|
13
13
|
[Principle 11](../../PRINCIPLES.md) and
|
|
14
14
|
[ADR 0005](../adr/0005-component-surface.md) say a component may exist only for
|
|
15
|
-
a capability whose off state leaves the rest intact, and that the host,
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
a capability whose off state leaves the rest intact, and that the host, the
|
|
16
|
+
health contract, the recovery escape and the merge gates are never switches.
|
|
17
|
+
The review policy is not one of them: `review.mode` is a single tri-state
|
|
18
|
+
control (`off`, `optional` or `required`) on the plugin-page configuration
|
|
19
|
+
form, not a component row and never one row per mode, and only the review gate
|
|
20
|
+
it turns on in `required` mode — the clean, tip-bound evidence and the
|
|
21
|
+
exemption path — is non-disableable. Which keys are allowed to be switches is a
|
|
22
|
+
table lookup in
|
|
18
23
|
[component-surface.md](../reference/component-surface.md), generated from
|
|
19
24
|
`scripts/component-surface.json`; the component count is baselined and checked by
|
|
20
25
|
`npm run docs:check:components`. That gate also refuses a `tools.expose*` route
|
|
@@ -236,9 +241,10 @@ set by `tower_merge`/reconciliation.
|
|
|
236
241
|
3. Survey missions must not change files; build missions must stay inside `scope`.
|
|
237
242
|
4. The main checkout must be clean for the files this merge would overwrite and on the base branch.
|
|
238
243
|
5. Dependency missions must be merged/abandoned, except a completed survey dependency which is its terminal success state (#107).
|
|
239
|
-
6. If `review.
|
|
240
|
-
clean, approve merge, and be bound to the tip (unless a clean
|
|
241
|
-
|
|
244
|
+
6. If the review policy is `required` (`review.mode:required`; the default is `optional`), a review
|
|
245
|
+
must exist, belong to this mission, be clean, approve merge, and be bound to the tip (unless a clean
|
|
246
|
+
rebase waived it). With `off`, review is intentionally unavailable and the gate ignores it. No
|
|
247
|
+
worker may be running.
|
|
242
248
|
|
|
243
249
|
## Merge
|
|
244
250
|
|
|
@@ -23,8 +23,9 @@ Wait for the worker's final report (delivered to this session) or check `tower_r
|
|
|
23
23
|
creates `src/greet.txt`, commits on `tower/m1-add-greeting`, marks both tasks done, and calls
|
|
24
24
|
`tower_complete`.
|
|
25
25
|
|
|
26
|
-
This example follows the strict workflow:
|
|
27
|
-
repository config
|
|
26
|
+
This example follows the strict workflow: set the review policy to `required` (`review.mode:required`
|
|
27
|
+
in the profile or repository config, or the one tri-state control on the Plugins page) before the
|
|
28
|
+
review step; the default is `optional`. Spawn the reviewer:
|
|
28
29
|
|
|
29
30
|
```json
|
|
30
31
|
{"repoRoot":"D:\\work\\demo","missionId":"M1","role":"reviewer"}
|
package/docs/faq.md
CHANGED
|
@@ -14,9 +14,9 @@ phase instead—use Workflow/Team to analyze and decide, then start Tower to exe
|
|
|
14
14
|
review) the repository change. See the [full guide](guide/scenarios-and-philosophy.md#4-how-tower-differs-from-other-dsh-workflow-patterns).
|
|
15
15
|
|
|
16
16
|
**Why did `tower_preflight` fail with `no-review` right after the worker finished?**
|
|
17
|
-
That happens only when `review.
|
|
18
|
-
independent review. Spawn a reviewer; after a clean review the gate turns green. With the
|
|
19
|
-
`review.
|
|
17
|
+
That happens only when the review policy is `required` (`review.mode:required`): a completed mission
|
|
18
|
+
awaits an independent review. Spawn a reviewer; after a clean review the gate turns green. With the
|
|
19
|
+
default `review.mode:optional`, review is optional and no `no-review` blocker is produced.
|
|
20
20
|
|
|
21
21
|
**Do I need to poll the worker?**
|
|
22
22
|
No. The worker's final report is delivered to this session; `tower_roster` shows live activity. Polling
|
|
@@ -40,10 +40,12 @@ Correct: the branch is already contained in the base. Call `tower_merge` to reco
|
|
|
40
40
|
instead of rebasing.
|
|
41
41
|
|
|
42
42
|
**Can I require independent review?**
|
|
43
|
-
Yes. Set
|
|
44
|
-
|
|
45
|
-
reason + `gates.allowReviewExemption:true` is the narrower
|
|
46
|
-
verdicts
|
|
43
|
+
Yes. Set the review policy to `required` — the one tri-state control on the Plugins page, or
|
|
44
|
+
`review.mode:required` in the profile or repository config (the default is `optional`). When required,
|
|
45
|
+
mission-level `reviewPolicy:"none"` + a reason + `gates.allowReviewExemption:true` is the narrower
|
|
46
|
+
exemption. `tower_review` works and records verdicts in `optional` mode too. The old boolean toggle is
|
|
47
|
+
a deprecated one-release alias; the mapping is in
|
|
48
|
+
[install, upgrade and migration](guide/install-upgrade-migration.md).
|
|
47
49
|
|
|
48
50
|
**Can a reviewer edit files?**
|
|
49
51
|
No. Reviewers use the read-only `tower-reviewer` preset (no write/edit/pwsh); a branch changed after the
|
|
@@ -29,6 +29,29 @@ The Host plugin loads at DSH startup: **restart DSH after install or update**.
|
|
|
29
29
|
|
|
30
30
|
## Migration notes
|
|
31
31
|
|
|
32
|
+
- **2.5.0 → 2.6.0 (#M39/#M40) — review modes replace the boolean**: the review policy is one
|
|
33
|
+
tri-state control on the Plugins page in the `dsh-tower` configuration section, and the config key
|
|
34
|
+
is `review.mode`: `off`, `optional` (the default) or `required`.
|
|
35
|
+
- `off` is supported and documented: `tower_review` is not registered, reviewer spawn is refused,
|
|
36
|
+
and the merge gate ignores review.
|
|
37
|
+
- `optional` keeps `tower_review` but never blocks a merge.
|
|
38
|
+
- `required` keeps the merge gate: a completed mission needs a clean review bound to the branch tip
|
|
39
|
+
(the existing per-mission exemption still applies).
|
|
40
|
+
`review.requireReview` is a deprecated one-release alias: `true` resolves to `required`, `false`
|
|
41
|
+
to `optional`, and leaving it unset means `optional`. A `true` next to an explicit non-`required`
|
|
42
|
+
mode is rejected with `config-invalid`; `false` never overrides an explicit mode. Saving from the
|
|
43
|
+
plugin page always writes `review.mode` and normalizes the alias to `false`. The policy is not a
|
|
44
|
+
component row and there are no mutually exclusive review rows: `preset-tower-reviewer` remains an
|
|
45
|
+
optional capability the policy may leave unused. The row may be off while `review.mode` is `off`
|
|
46
|
+
or `optional`; when `review.mode` is `required` and the reviewer row is off, planning and mode
|
|
47
|
+
entry refuse the combination with an explanatory `preset-missing` error.
|
|
48
|
+
A profile that starts with the policy `off` never registered `tower_review`; switching to
|
|
49
|
+
`optional` or `required` changes the merge gate immediately, but the tool appears only after a DSH
|
|
50
|
+
restart (or a profile reload), and the plugin page states that after such a save.
|
|
51
|
+
Upgrade: replace `review.requireReview: true|false` with `review.mode: required|optional`; keep the
|
|
52
|
+
alias only if the same config is shared with older 2.5 installations, and remove it after this
|
|
53
|
+
release. No state migration.
|
|
54
|
+
|
|
32
55
|
- **2.4.2 → 2.5.0 (#131/#133/#135/#136/#142)**: three component-shaped config
|
|
33
56
|
keys are gone — `tests.enabled`, `docs.board`, and `archive.enabled`. The
|
|
34
57
|
`tower-tests`, `tower-board`, and `tower-archive` component rows are the only
|
|
@@ -70,14 +93,14 @@ The Host plugin loads at DSH startup: **restart DSH after install or update**.
|
|
|
70
93
|
now ships the mandatory `tower` host anchor (the Host plugin and the worker preset are its internal
|
|
71
94
|
sections) plus individually switchable top-level rows: `tower-skills`, `tower-batch`, `tower-import`,
|
|
72
95
|
`tower-leases`, `tower-tests`, `tower-hmr` (shipped off) and `preset-tower-reviewer` (conditional on
|
|
73
|
-
`review.
|
|
74
|
-
still carries them is now rejected as an unknown key (`config-unknown-key`) and the host stops
|
|
96
|
+
the review policy, today `review.mode`). The `tools` config group and `dev.hotReload` were removed: a
|
|
97
|
+
profile that still carries them is now rejected as an unknown key (`config-unknown-key`) and the host stops
|
|
75
98
|
mounting until they are deleted, so no silent no-op switch remains. Move each capability to its row
|
|
76
99
|
in the plugin page instead; `tower-hmr` ships off, so `tower_reload` and `/tower reload` are reported
|
|
77
100
|
as "disabled by component tower-hmr" until you enable it for a linked dev profile. `tower_doctor` and
|
|
78
101
|
`tower_recover` are registered for every configuration and need no key. The reviewer row may be off
|
|
79
|
-
|
|
80
|
-
combination with an explanatory error. No state migration is needed.
|
|
102
|
+
while the policy is `off` or `optional`; with `review.mode:required`, planning and mode entry refuse
|
|
103
|
+
the combination with an explanatory error. No state migration is needed.
|
|
81
104
|
|
|
82
105
|
- **2.0.4 → 2.1.0-beta.1**: no config migration; the state file migrates automatically (v1/v2 → v3) as
|
|
83
106
|
described above. This is a **pre-release** published on the
|
|
@@ -89,10 +112,12 @@ The Host plugin loads at DSH startup: **restart DSH after install or update**.
|
|
|
89
112
|
runs `npm publish --tag next`, and `latest` stays on 2.0.4 until a stable release.
|
|
90
113
|
- **2.0.3 → 2.0.4**: no state/config migration. This is a security-hardening and integrity release
|
|
91
114
|
(authorization, merge SHA binding, dirty-worktree completion, stale-lock recovery, limited test/git
|
|
92
|
-
process environment, `tower_recover`, UI 0.2.2). Review `review.
|
|
93
|
-
worker tool permissions against your
|
|
115
|
+
process environment, `tower_recover`, UI 0.2.2). Review the review policy (`review.mode`, or the
|
|
116
|
+
deprecated `review.requireReview` alias), `tests.admission` and worker tool permissions against your
|
|
117
|
+
threat model after upgrading.
|
|
94
118
|
- **2.0.1 → 2.0.3**: no state migration. Independent review became **optional by default** in 2.0.3
|
|
95
|
-
(`review.
|
|
119
|
+
(today's `review.mode:optional`); set `review.mode:required` to keep the old review-gated merges.
|
|
120
|
+
2.0.1 added
|
|
96
121
|
`tower_batch` `abandon`, accepted merged members in batch close, made doctor skip torn-down worktrees,
|
|
97
122
|
and fixed terminal-mission spawn/preflight edge cases. The optional `ui-bundle` 0.2.1 adds the
|
|
98
123
|
persistent Tower-mode chip and mode-aware denied-tool rows.
|
package/docs/guide/quickstart.md
CHANGED
|
@@ -33,15 +33,25 @@ session. Use `tower_roster` to observe activity; do not poll in a loop.
|
|
|
33
33
|
|
|
34
34
|
## 4. Optional: review
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
The review policy is one tri-state control — `off`, `optional` (the default) or `required` — on the
|
|
37
|
+
Plugins page in the `dsh-tower` configuration section, or `review.mode` in the profile/repository
|
|
38
|
+
config. It is one control, not a component row and not three rows. Set it to `required` for the strict
|
|
39
|
+
workflow, then:
|
|
38
40
|
|
|
39
41
|
```
|
|
40
42
|
tower_spawn repoRoot=D:\work\repo missionId=M1 role=reviewer
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
Reviewers use the read-only `tower-reviewer` preset. They inspect `tower_diff` and submit
|
|
44
|
-
`tower_review`; with `review.
|
|
46
|
+
`tower_review`; with `review.mode:required`, a `clean` + `merge` verdict is required to merge.
|
|
47
|
+
`optional` records reviews but never blocks a merge, and `off` is a supported mode that does not
|
|
48
|
+
register `tower_review`.
|
|
49
|
+
|
|
50
|
+
The old boolean review toggle is a one-release alias (`true` → `required`, `false` → `optional`;
|
|
51
|
+
unset → `optional`). Prefer `review.mode`. A profile that started with
|
|
52
|
+
`off` only registers `tower_review` after a restart/reload, although the merge gate changes
|
|
53
|
+
immediately. See [install, upgrade and migration](install-upgrade-migration.md) for the full
|
|
54
|
+
migration.
|
|
45
55
|
|
|
46
56
|
## 5. Gate, merge, clean up
|
|
47
57
|
|
|
@@ -51,10 +61,10 @@ tower_merge repoRoot=D:\work\repo missionId=M1
|
|
|
51
61
|
tower_teardown repoRoot=D:\work\repo
|
|
52
62
|
```
|
|
53
63
|
|
|
54
|
-
With
|
|
55
|
-
review it should be `ok:true`. With the default (`
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
With `review.mode:required`, `tower_preflight` before review reports `no-review`, and after a clean
|
|
65
|
+
review it should be `ok:true`. With the default (`optional`), no review blocker is required.
|
|
66
|
+
`tower_merge` refuses while blockers remain. `tower_teardown` removes merged/abandoned worktrees and
|
|
67
|
+
branches.
|
|
58
68
|
|
|
59
69
|
```
|
|
60
70
|
/tower off
|
|
@@ -63,8 +73,8 @@ worktrees and branches.
|
|
|
63
73
|
## If something fails
|
|
64
74
|
|
|
65
75
|
Read the blocker code and use [the blocker reference](../reference/blockers.md). The most common cases:
|
|
66
|
-
`stale-base` (run `tower_rebase`), `no-review` (only with `review.
|
|
67
|
-
|
|
76
|
+
`stale-base` (run `tower_rebase`), `no-review` (only with `review.mode:required`; spawn a reviewer),
|
|
77
|
+
`merge-hook-failed` (retry, or raise `timeoutMs`), `tip-moved` (only when the policy is `required`;
|
|
68
78
|
the branch changed after review, review again).
|
|
69
79
|
|
|
70
80
|
Thrown tool failures carry a `[tower:<code>]` prefix; see [error codes](../reference/errors.md)
|
|
@@ -78,7 +88,7 @@ JSON file at `<git-common-dir>/dsh-tower/config.json` (usually `.git/dsh-tower/c
|
|
|
78
88
|
|
|
79
89
|
```json
|
|
80
90
|
{
|
|
81
|
-
"review": { "
|
|
91
|
+
"review": { "mode": "required", "maxRoundsPerReviewer": 3 },
|
|
82
92
|
"merge": { "timeoutMs": 1800000 },
|
|
83
93
|
"worktrees": { "root": "D:/tower-worktrees" }
|
|
84
94
|
}
|