dsh-tower 2.5.0 → 2.7.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 +114 -0
- package/COMPATIBILITY.md +2 -0
- package/CONTRIBUTING.md +1 -1
- package/GLOSSARY.md +6 -0
- package/PRINCIPLES.md +15 -7
- package/README.en.md +18 -16
- package/README.md +18 -11
- package/adapters/leases.js +20 -6
- package/adapters/legacy.js +149 -37
- package/adapters/stateFile.js +11 -1
- package/app/archive.js +30 -5
- package/app/briefing.js +95 -12
- package/app/checkCompletion.js +107 -18
- package/app/enrichedMissions.js +1 -1
- package/app/freshness.js +77 -0
- package/app/gate.js +26 -12
- package/app/merge.js +7 -4
- package/app/mission.js +24 -1
- package/app/missionContext.js +2 -1
- package/app/plan.js +64 -1
- package/app/provision.js +133 -22
- package/app/read.js +38 -5
- package/app/rebase.js +10 -8
- package/app/review.js +104 -61
- package/app/reviewCheckout.js +133 -0
- package/app/sandbox.js +121 -0
- package/app/sharedCheckout.js +90 -0
- package/app/spawn-prechecks.js +31 -20
- package/app/spawn-support.js +59 -8
- package/app/spawn.js +3 -0
- package/app/teardown.js +119 -2
- package/app/tower.js +34 -6
- package/client.js +321 -0
- package/cordis.patch.yml +55 -3
- package/docs/adr/0005-component-surface.md +58 -13
- package/docs/adr/0006-mission-shapes.md +306 -0
- package/docs/architecture/overview.md +18 -10
- package/docs/examples/first-mission.md +3 -2
- package/docs/faq.md +9 -7
- package/docs/guide/install-upgrade-migration.md +58 -7
- package/docs/guide/quickstart.md +20 -10
- package/docs/guide/scenarios-and-philosophy.md +35 -28
- package/docs/operations/runbook.md +1 -1
- package/docs/operations/security.md +11 -10
- package/docs/reference/blockers.md +20 -11
- package/docs/reference/component-surface.md +20 -8
- package/docs/reference/config.generated.md +8 -5
- package/docs/reference/config.md +30 -5
- package/docs/reference/doctor.md +7 -7
- package/docs/reference/errors.generated.md +1 -0
- package/docs/reference/errors.md +2 -1
- package/docs/reference/operations.md +1 -1
- package/docs/reference/shapes.md +123 -0
- package/docs/reference/surface-budget.md +53 -4
- package/docs/reference/testing.md +5 -3
- package/docs/reference/tools.generated.md +7 -7
- package/docs/reference/tools.md +5 -5
- 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 +13 -9
- package/docs/zh/reference/config.md +22 -5
- package/docs/zh/reference/errors.md +2 -1
- package/docs/zh/reference/testing.md +5 -4
- package/docs/zh/reference/tools.md +3 -3
- package/domain/batch.js +75 -105
- package/domain/collab/batch.js +30 -10
- package/domain/collab/spawn.js +53 -16
- package/domain/components.js +12 -4
- package/domain/config/schema.js +87 -22
- package/domain/contracts/outputs.js +2 -1
- package/domain/contracts/tool-inputs/missions.js +10 -2
- package/domain/errors.js +6 -0
- package/domain/evidence.js +283 -0
- package/domain/gate.js +17 -34
- package/domain/integration/gate.js +17 -43
- package/domain/integration/init.js +3 -2
- package/domain/integration/provision.js +51 -0
- package/domain/integration/rebase.js +16 -1
- package/domain/integration/review.js +203 -9
- package/domain/mission.js +16 -33
- package/domain/missions/completion.js +46 -8
- package/domain/missions/findings.js +2 -1
- package/domain/missions/lifecycle.js +131 -35
- package/domain/missions/plan.js +30 -6
- package/domain/missions/review.js +2 -18
- package/domain/planning.js +5 -5
- package/domain/read/missionContext.js +17 -3
- package/domain/read/views.js +5 -1
- package/domain/roles.js +222 -0
- package/domain/roster.js +9 -3
- package/domain/scope.js +51 -0
- package/domain/shapes/build.js +254 -0
- package/domain/shapes/index.js +171 -0
- package/domain/shapes/survey.js +445 -0
- package/domain/state/archive.js +28 -4
- package/domain/state/migrations.js +111 -4
- package/domain/state/schema.js +69 -7
- package/domain/state/semantics.js +93 -13
- package/domain/state.js +5 -1
- package/domain/summary.js +5 -11
- package/domain/vocabulary.js +15 -33
- package/host/capability-registry.js +25 -9
- package/host/config.js +40 -3
- package/host/doctor.js +20 -8
- package/host/preset-survey-sandbox.js +19 -0
- package/host/preset-survey.js +18 -0
- package/host/presets.js +17 -11
- package/host/tool-registry.js +23 -13
- package/locale/en.json +16 -1
- package/locale/preset-survey/en.json +6 -0
- package/locale/preset-survey/zh.json +6 -0
- package/locale/preset-survey-sandbox/en.json +6 -0
- package/locale/preset-survey-sandbox/zh.json +6 -0
- package/locale/zh.json +16 -1
- package/package.json +19 -2
- package/ports/state.js +8 -1
- package/scripts/architecture-waivers.json +3 -3
- package/scripts/check-components.mjs +120 -6
- package/scripts/check-docs.mjs +1 -1
- package/scripts/check-pack.mjs +5 -0
- package/scripts/component-surface.json +83 -29
- package/scripts/generate-capability-locale.mjs +5 -0
- package/scripts/generate-shapes-reference.mjs +171 -0
- package/scripts/mount-smoke/surface.mjs +3 -0
- package/scripts/pack-manifest.txt +22 -0
- package/scripts/shape-audit.mjs +361 -0
- package/scripts/surface-baseline.json +75 -4
- package/scripts/types-coverage.mjs +7 -0
- package/survey-sandbox.js +48 -0
- package/survey.js +40 -0
- package/tools/complete.js +5 -5
- package/tools/diff.js +5 -3
- package/tools/plan.js +1 -1
- package/tools/preflight.js +3 -2
- package/tools/review.js +1 -1
- package/tools/spawn.js +48 -50
- package/tools/status.js +8 -1
- package/tools/test.js +2 -1
- package/types/index.d.ts +76 -5
package/app/tower.js
CHANGED
|
@@ -8,6 +8,7 @@ import { assertPorts } from '../ports/index.js'
|
|
|
8
8
|
import { validateCreate as validateBatchCreate, validateClose as validateBatchClose, summarize as summarizeBatch } from '../domain/batch.js'
|
|
9
9
|
|
|
10
10
|
import { rebaseWaived } from '../domain/gate.js'
|
|
11
|
+
import { isReviewExempt } from '../domain/integration/review.js'
|
|
11
12
|
import { DEFAULT_MERGE_TIMEOUT_MS, gitLockTimeoutMs } from '../domain/lockBudget.js'
|
|
12
13
|
export { ROSTER_INACTIVE_STATES } from '../domain/roster.js'
|
|
13
14
|
|
|
@@ -27,6 +28,7 @@ import { writeBoard, boardFile } from './board.js'
|
|
|
27
28
|
import { archiveIndex, archivedMission } from './archive.js'
|
|
28
29
|
import { recordSpawnOutcome, recordInterrupt, reserveRoster, recordSpawn, rosterView } from './spawn.js'
|
|
29
30
|
import { recordTestRun, recordLegacySnapshot, recordLegacyMap } from './recording.js'
|
|
31
|
+
import { resolveLegacySurveyRead } from './sharedCheckout.js'
|
|
30
32
|
|
|
31
33
|
export const ROLES = ['worker', 'reviewer']
|
|
32
34
|
const MAX_REVIEW_ROUNDS = 5
|
|
@@ -98,6 +100,7 @@ import { rebase } from './rebase.js'
|
|
|
98
100
|
import { merge } from './merge.js'
|
|
99
101
|
import { init } from './init.js'
|
|
100
102
|
import { provision } from './provision.js'
|
|
103
|
+
import { provisionReviewCheckout } from './reviewCheckout.js'
|
|
101
104
|
import { addFinding, readFindings, findings } from './findings.js'
|
|
102
105
|
import { recordMessage, recordDelivery, inbox } from './messages.js'
|
|
103
106
|
import { createBatch, closeBatch, abandonBatch, batchStatus } from './batch.js'
|
|
@@ -169,6 +172,20 @@ export class Tower {
|
|
|
169
172
|
return (await this.git(cwd, ['rev-parse', ref])).trim()
|
|
170
173
|
}
|
|
171
174
|
|
|
175
|
+
/**
|
|
176
|
+
* The commit a mission branch points at, resolved from the branch ref. Never
|
|
177
|
+
* reads a live worktree checkout: a checkout is a view, the ref is the fact
|
|
178
|
+
* (#149 W3 review evidence and every tip comparison use this).
|
|
179
|
+
* @param {{ id?: string, worktree?: { branch?: string } | null }} mission
|
|
180
|
+
*/
|
|
181
|
+
async branchTip(mission) {
|
|
182
|
+
const branch = mission?.worktree?.branch
|
|
183
|
+
if (typeof branch !== 'string' || branch.length === 0) {
|
|
184
|
+
fail(`mission ${mission?.id ?? ''} has no branch to resolve`.replace(' ', ' '), 'invalid-argument')
|
|
185
|
+
}
|
|
186
|
+
return this.revParse(this.root, `refs/heads/${branch}`)
|
|
187
|
+
}
|
|
188
|
+
|
|
172
189
|
async isClean(cwd) {
|
|
173
190
|
return (await this.git(cwd, ['status', '--porcelain'])).trim().length === 0
|
|
174
191
|
}
|
|
@@ -192,6 +209,9 @@ export class Tower {
|
|
|
192
209
|
paths: this.paths,
|
|
193
210
|
now: this.deps.clock?.now ? this.deps.clock.now() : Date.now(),
|
|
194
211
|
persist: options.persist,
|
|
212
|
+
// A legacy survey planned but never provisioned recorded no base; the
|
|
213
|
+
// loader resolves it once, from this checkout, before the pure migration.
|
|
214
|
+
resolveLegacySurvey: (rawState) => resolveLegacySurveyRead(this, rawState),
|
|
195
215
|
})
|
|
196
216
|
this.stateLoad = { ...loaded.version, migration: loaded.migration }
|
|
197
217
|
return loaded.state
|
|
@@ -278,6 +298,15 @@ export class Tower {
|
|
|
278
298
|
return provision(this, input)
|
|
279
299
|
}
|
|
280
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Provision the disposable detached review checkout for a reviewer (#149 W3).
|
|
303
|
+
* The commit it returns is the reviewer's evidence for every later read and
|
|
304
|
+
* the verdict stamp.
|
|
305
|
+
*/
|
|
306
|
+
async provisionReviewCheckout(input = {}) {
|
|
307
|
+
return provisionReviewCheckout(this, input)
|
|
308
|
+
}
|
|
309
|
+
|
|
281
310
|
async missionContext(missionId) {
|
|
282
311
|
return missionContext(this, missionId)
|
|
283
312
|
}
|
|
@@ -508,11 +537,10 @@ export class Tower {
|
|
|
508
537
|
if (!mission.worktree) return null
|
|
509
538
|
const branch = mission.worktree.branch
|
|
510
539
|
const reviews = (state.reviews ?? []).filter((review) => review.branch === branch)
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
String(mission.reviewExemptionReason ?? '').trim().length > 0)
|
|
540
|
+
// #M39: reconciliation follows the same policy as the gate — only
|
|
541
|
+
// `review.mode:required` demands review evidence; `off`/`optional` reconcile
|
|
542
|
+
// a landed branch without one.
|
|
543
|
+
const exempt = isReviewExempt(mission, this.config)
|
|
516
544
|
if (reviews.length === 0 && !exempt) return null
|
|
517
545
|
const latest = reviews.length > 0 ? reviews.reduce((a, b) => (a.round >= b.round ? a : b)) : null
|
|
518
546
|
if (latest && !exempt && (latest.status !== 'clean' || latest.mergeVerdict !== 'merge')) return null
|
|
@@ -521,7 +549,7 @@ export class Tower {
|
|
|
521
549
|
let branchTip
|
|
522
550
|
try {
|
|
523
551
|
baseTip = await this.revParse(this.root, state.base)
|
|
524
|
-
branchTip = await this.
|
|
552
|
+
branchTip = await this.branchTip(mission)
|
|
525
553
|
} catch {
|
|
526
554
|
return null
|
|
527
555
|
}
|
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:
|
|
@@ -108,3 +110,53 @@
|
|
|
108
110
|
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
109
111
|
config:
|
|
110
112
|
sampleOverCapGlobResults: false
|
|
113
|
+
# #148 D7/W2: one visible survey capability row. The group is the switch:
|
|
114
|
+
# both the read-only preset and the detached-sandbox preset are nested
|
|
115
|
+
# dependencies of it, so the sandbox placement is tuning and adds no second
|
|
116
|
+
# visible component. Off means a survey cannot be planned (tower_plan refuses
|
|
117
|
+
# with an actionable code) and no survey agent can be dispatched; build
|
|
118
|
+
# missions are unaffected. Read-only remains structural for the shared
|
|
119
|
+
# placement (the preset allow-list has no write, edit, or shell tool); the
|
|
120
|
+
# sandbox preset exists only for `survey.sandbox: 'detached'`.
|
|
121
|
+
- id: tower-survey
|
|
122
|
+
name: cordis:group
|
|
123
|
+
group: true
|
|
124
|
+
config:
|
|
125
|
+
- id: preset-tower-survey
|
|
126
|
+
name: 'dsh-tower/preset-survey'
|
|
127
|
+
config:
|
|
128
|
+
id: tower-survey
|
|
129
|
+
name: Tower Survey
|
|
130
|
+
description: Read-only inspection of one recorded commit in the shared checkout.
|
|
131
|
+
order: 62
|
|
132
|
+
plugins:
|
|
133
|
+
- id: tower-survey-policy
|
|
134
|
+
name: 'dsh-tower/survey'
|
|
135
|
+
- id: tower-survey-fs
|
|
136
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
137
|
+
config:
|
|
138
|
+
write: false
|
|
139
|
+
edit: false
|
|
140
|
+
readImage: false
|
|
141
|
+
- id: tower-survey-fs-search
|
|
142
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
143
|
+
config:
|
|
144
|
+
sampleOverCapGlobResults: false
|
|
145
|
+
- id: preset-tower-survey-sandbox
|
|
146
|
+
name: 'dsh-tower/preset-survey-sandbox'
|
|
147
|
+
config:
|
|
148
|
+
id: tower-survey-sandbox
|
|
149
|
+
name: Tower Survey Sandbox
|
|
150
|
+
description: Disposable detached checkout for a survey that may write or run commands while reading one recorded commit.
|
|
151
|
+
order: 63
|
|
152
|
+
plugins:
|
|
153
|
+
- id: tower-survey-sandbox-policy
|
|
154
|
+
name: 'dsh-tower/survey-sandbox'
|
|
155
|
+
- id: tower-survey-sandbox-fs
|
|
156
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
157
|
+
- id: tower-survey-sandbox-fs-search
|
|
158
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
159
|
+
config:
|
|
160
|
+
sampleOverCapGlobResults: false
|
|
161
|
+
- id: tower-survey-sandbox-pwsh
|
|
162
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
@@ -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.
|