dsh-tower 2.0.3 → 2.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +141 -0
- package/COMPATIBILITY.md +38 -0
- package/CONTRIBUTING.md +150 -2
- package/README.en.md +59 -16
- package/README.md +57 -16
- package/SECURITY.md +31 -1
- package/adapters/default.js +10 -0
- package/adapters/store-fs.js +6 -0
- package/adapters/vcs-git.js +4 -0
- package/app/batch.js +79 -0
- package/app/board.js +25 -0
- package/app/checkCompletion.js +76 -0
- package/app/enrichedMissions.js +26 -0
- package/app/findings.js +78 -0
- package/app/gate.js +108 -0
- package/app/init.js +249 -0
- package/app/merge.js +567 -0
- package/app/messages.js +66 -0
- package/app/mission.js +35 -0
- package/app/missionContext.js +14 -0
- package/app/plan.js +42 -0
- package/app/provision.js +193 -0
- package/app/read.js +57 -0
- package/app/rebase.js +302 -0
- package/app/recording.js +72 -0
- package/app/review.js +145 -0
- package/app/roster.js +123 -0
- package/app/spawn.js +125 -0
- package/app/teardown.js +184 -0
- package/app/tower.js +524 -0
- package/cordis.patch.yml +61 -53
- package/docs/README.md +33 -1
- package/docs/architecture/overview.md +114 -14
- package/docs/board.md +72 -0
- package/docs/examples/first-mission.md +2 -1
- package/docs/faq.md +11 -7
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/install-upgrade-migration.md +22 -6
- package/docs/guide/quickstart.md +12 -7
- package/docs/guide/release.md +125 -0
- package/docs/guide/scenarios-and-philosophy.md +86 -69
- package/docs/operations/security.md +65 -14
- package/docs/reference/blockers.md +11 -10
- package/docs/reference/config.generated.md +66 -0
- package/docs/reference/config.md +11 -7
- package/docs/reference/errors.generated.md +39 -0
- package/docs/reference/errors.md +10 -2
- package/docs/reference/events.md +7 -4
- package/docs/reference/testing.md +2 -1
- package/docs/reference/tools.generated.md +33 -0
- package/docs/reference/tools.md +8 -7
- package/docs/zh/README.md +4 -3
- package/docs/zh/faq.md +9 -6
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/guide/install-upgrade-migration.md +17 -5
- package/docs/zh/guide/quickstart.md +13 -8
- package/docs/zh/guide/scenarios-and-philosophy.md +55 -50
- package/docs/zh/operations/security.md +43 -7
- package/docs/zh/reference/blockers.md +10 -10
- package/docs/zh/reference/config.md +11 -7
- package/docs/zh/reference/errors.md +4 -1
- package/docs/zh/reference/events.md +6 -3
- package/docs/zh/reference/testing.md +3 -2
- package/docs/zh/reference/tools.md +8 -7
- package/domain/collab/batch.js +112 -0
- package/domain/collab/messages.js +127 -0
- package/domain/collab/roster.js +158 -0
- package/domain/collab/spawn.js +188 -0
- package/domain/config/schema.js +404 -0
- package/domain/contracts/outputs.js +279 -0
- package/domain/contracts/tool-inputs/collaboration.js +58 -0
- package/domain/contracts/tool-inputs/findings.js +29 -0
- package/domain/contracts/tool-inputs/index.js +94 -0
- package/domain/contracts/tool-inputs/integration.js +37 -0
- package/domain/contracts/tool-inputs/missions.js +94 -0
- package/domain/contracts/tool-inputs/ops.js +75 -0
- package/domain/contracts/tool-inputs/shared.js +25 -0
- package/domain/errors.js +96 -0
- package/domain/gate.js +125 -0
- package/domain/integration/gate.js +176 -0
- package/domain/integration/init.js +225 -0
- package/domain/integration/liveness.js +31 -0
- package/domain/integration/merge.js +377 -0
- package/domain/integration/provision.js +124 -0
- package/domain/integration/rebase.js +236 -0
- package/domain/integration/review.js +52 -0
- package/domain/integration/teardown.js +118 -0
- package/domain/mission.js +68 -0
- package/domain/missions/completion.js +114 -0
- package/domain/missions/findings.js +201 -0
- package/domain/missions/lifecycle.js +185 -0
- package/domain/missions/plan.js +149 -0
- package/domain/missions/review.js +179 -0
- package/domain/planning.js +118 -0
- package/domain/read/enrichedMissions.js +58 -0
- package/domain/read/missionContext.js +53 -0
- package/domain/read/views.js +128 -0
- package/domain/report.js +59 -0
- package/domain/roster.js +40 -0
- package/domain/scope.js +105 -0
- package/domain/state/schema.js +233 -0
- package/domain/state/semantics.js +117 -0
- package/domain/state.js +33 -0
- package/domain/summary.js +114 -0
- package/domain/vocabulary.js +179 -0
- package/index.js +86 -34
- package/lib/briefing.js +176 -0
- package/lib/compatibility.js +108 -0
- package/lib/config.js +54 -302
- package/lib/doctor.js +108 -9
- package/lib/git.js +93 -15
- package/lib/hmr.js +4 -3
- package/lib/leases.js +3 -2
- package/lib/legacy.js +20 -9
- package/lib/modeState.js +39 -0
- package/lib/modeStore.js +35 -33
- package/lib/persistence.js +349 -0
- package/lib/presets.js +35 -0
- package/lib/spawn-prechecks.js +168 -0
- package/lib/spawn-support.js +79 -0
- package/lib/store.js +57 -85
- package/lib/tests.js +46 -11
- package/lib/tool-registry.js +544 -0
- package/lib/workspaceLock.js +46 -22
- package/lib/worktreeOwnership.js +5 -4
- package/locale/en.json +23 -1
- package/locale/zh.json +22 -0
- package/package.json +33 -4
- package/ports/clock.js +6 -0
- package/ports/ids.js +7 -0
- package/ports/index.js +2 -0
- package/ports/store.js +11 -0
- package/ports/vcs.js +11 -0
- package/scripts/architecture.mjs +94 -0
- package/scripts/check-docs.mjs +44 -3
- package/scripts/check-pack.mjs +49 -0
- package/scripts/domain-metrics.mjs +48 -0
- package/scripts/generate-config-reference.mjs +46 -0
- package/scripts/generate-error-reference.mjs +29 -0
- package/scripts/generate-tools-reference.mjs +56 -0
- package/scripts/issue-audit.mjs +259 -0
- package/scripts/lib/cli.mjs +76 -0
- package/scripts/lib/tmp.mjs +20 -0
- package/scripts/mount-smoke/fixtures.mjs +162 -0
- package/scripts/mount-smoke/host.json +4 -0
- package/scripts/mount-smoke/probe.mjs +125 -0
- package/scripts/mount-smoke/surface.mjs +141 -0
- package/scripts/mount-smoke.mjs +270 -0
- package/scripts/pack-manifest.txt +203 -0
- package/scripts/release-check.mjs +303 -0
- package/skills/tower-recovery.md +7 -5
- package/tools/adopt.js +72 -0
- package/tools/batch.js +78 -0
- package/tools/complete.js +53 -0
- package/tools/diff.js +39 -0
- package/tools/doctor.js +50 -0
- package/tools/finding.js +40 -0
- package/tools/findings.js +39 -0
- package/tools/import-legacy.js +42 -0
- package/tools/inbox.js +39 -0
- package/tools/index.js +39 -0
- package/tools/init.js +63 -0
- package/tools/interrupt.js +42 -0
- package/tools/legacy-map.js +44 -0
- package/tools/merge.js +39 -0
- package/tools/mission.js +41 -0
- package/tools/plan.js +39 -0
- package/tools/preflight.js +44 -0
- package/tools/rebase.js +39 -0
- package/tools/recover.js +45 -0
- package/tools/reload.js +52 -0
- package/tools/resource.js +54 -0
- package/tools/review.js +45 -0
- package/tools/roster.js +60 -0
- package/tools/send.js +147 -0
- package/tools/spawn.js +196 -0
- package/tools/status.js +94 -0
- package/tools/teardown.js +42 -0
- package/tools/test.js +61 -0
- package/types/cordis.d.ts +6 -0
- package/types/index.d.ts +339 -0
- package/ui-bundle/README.md +111 -0
- package/ui-bundle/client.js +1980 -0
- package/ui-bundle/cordis.patch.yml +3 -0
- package/ui-bundle/icon.svg +6 -0
- package/ui-bundle/index.js +2 -0
- package/ui-bundle/locale/en.json +6 -0
- package/ui-bundle/locale/zh.json +6 -0
- package/ui-bundle/package.json +35 -0
- package/lib/errors.js +0 -111
- package/lib/tools.js +0 -2250
- package/lib/tower.js +0 -2689
- /package/{lib → domain}/batch.js +0 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// Pure Tower configuration schema and default resolution (#56 P2a).
|
|
2
|
+
//
|
|
3
|
+
// This module is pure: it declares the Schemastery schema, validates raw config
|
|
4
|
+
// objects, and materializes defaults. Profile and repository loading (fs and the
|
|
5
|
+
// process-local profile snapshot) live in lib/config.js, which re-exports
|
|
6
|
+
// `Config` and `resolveTowerConfig` for compatibility.
|
|
7
|
+
//
|
|
8
|
+
// Layers, lowest precedence first:
|
|
9
|
+
// 1. code defaults (the schemastery `.default(...)` values)
|
|
10
|
+
// 2. profile config (the Cordis patch passes it to `apply()`)
|
|
11
|
+
// 3. repository config: <git-common-dir>/dsh-tower/config.json
|
|
12
|
+
// 4. per-tool arguments (e.g. worktreeRoot, timeoutMs)
|
|
13
|
+
//
|
|
14
|
+
// Defaults are exactly the 1.2.0 behavior; the shipped cordis patch keeps
|
|
15
|
+
// `allowAgentControl: true` via the legacy top-level key.
|
|
16
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
17
|
+
import { TowerToolError } from '../errors.js'
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_DENY_TOOLS = [
|
|
20
|
+
'create_goal',
|
|
21
|
+
'todo_write',
|
|
22
|
+
'team_task_create',
|
|
23
|
+
'team_task_update',
|
|
24
|
+
'team_task_reopen',
|
|
25
|
+
'team_task_list',
|
|
26
|
+
'spawn_teammate',
|
|
27
|
+
'wait_agent',
|
|
28
|
+
'workflow',
|
|
29
|
+
'ralph',
|
|
30
|
+
'subagent',
|
|
31
|
+
'subagent_fork',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const LEGACY_KEYS = ['section', 'allowAgentControl']
|
|
35
|
+
const GROUP_KEYS = ['init', 'mode', 'review', 'merge', 'git', 'worktrees', 'spawn', 'send', 'tests', 'gates', 'docs', 'leases', 'dev', 'tools']
|
|
36
|
+
|
|
37
|
+
const SECTION_KEYS = {
|
|
38
|
+
init: ['autoGitInit', 'snapshotDirty', 'autoExclude'],
|
|
39
|
+
mode: [
|
|
40
|
+
'promptSection',
|
|
41
|
+
'sectionAppend',
|
|
42
|
+
'allowAgentControl',
|
|
43
|
+
'autoPauseGoal',
|
|
44
|
+
'requireResumeAuthorization',
|
|
45
|
+
'requireHumanAckOnFirstEntry',
|
|
46
|
+
'workspaceLock',
|
|
47
|
+
'workspaceLockScope',
|
|
48
|
+
'planModePolicy',
|
|
49
|
+
'workspaceStaleMs',
|
|
50
|
+
'heartbeatMs',
|
|
51
|
+
'denyTools',
|
|
52
|
+
'storePath',
|
|
53
|
+
'maxEntries',
|
|
54
|
+
'lockTimeoutMs',
|
|
55
|
+
],
|
|
56
|
+
review: ['requireReview', 'maxRoundsPerReviewer'],
|
|
57
|
+
merge: ['timeoutMs', 'maxBufferBytes'],
|
|
58
|
+
git: ['timeoutMs'],
|
|
59
|
+
worktrees: ['root'],
|
|
60
|
+
spawn: [
|
|
61
|
+
'workerPreset',
|
|
62
|
+
'reviewerPreset',
|
|
63
|
+
'maxDepth',
|
|
64
|
+
'provider',
|
|
65
|
+
'workerModel',
|
|
66
|
+
'reviewerModel',
|
|
67
|
+
'maxLiveWorkers',
|
|
68
|
+
'staleMs',
|
|
69
|
+
'maxWaitMs',
|
|
70
|
+
'requireLease',
|
|
71
|
+
],
|
|
72
|
+
send: ['maxPerMinute', 'interruptDefault'],
|
|
73
|
+
tests: ['enabled', 'admission', 'timeoutMs', 'maxBytes', 'autoChecks', 'allowPackageDiscovery'],
|
|
74
|
+
gates: ['requireChecks', 'requireTests', 'allowReviewExemption'],
|
|
75
|
+
docs: ['board', 'boardLocation'],
|
|
76
|
+
tools: ['exposeOperational', 'exposeBatch', 'exposeImport', 'exposeLeases', 'exposeTests', 'exposeRecovery', 'exposeDoctor'],
|
|
77
|
+
leases: ['ttlMs'],
|
|
78
|
+
dev: ['hotReload', 'reloadDelayMs', 'reloadRequiredIds'],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const MODEL_KEYS = ['provider', 'model', 'reasoningEffort', 'maxTokens']
|
|
82
|
+
|
|
83
|
+
const ModelSchema = () =>
|
|
84
|
+
Schema.object({
|
|
85
|
+
provider: Schema.string().description('Provider route override for this role.'),
|
|
86
|
+
model: Schema.string().description('Model id override for this role.'),
|
|
87
|
+
reasoningEffort: Schema.string().description('Optional reasoning effort override.'),
|
|
88
|
+
maxTokens: Schema.natural().description('Optional output token cap.'),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
export const Config = Schema.object({
|
|
92
|
+
section: Schema.string().description(
|
|
93
|
+
'Legacy alias for mode.promptSection: replaces the whole Tower-mode system prompt text.',
|
|
94
|
+
),
|
|
95
|
+
allowAgentControl: Schema.boolean().description(
|
|
96
|
+
'Legacy alias for mode.allowAgentControl: register tower_control so the model may toggle the mode.',
|
|
97
|
+
),
|
|
98
|
+
init: Schema.object({
|
|
99
|
+
autoGitInit: Schema.boolean()
|
|
100
|
+
.default(false)
|
|
101
|
+
.description('tower_init may run git init and create an empty initial commit when the directory is not a repository.'),
|
|
102
|
+
snapshotDirty: Schema.boolean()
|
|
103
|
+
.default(false)
|
|
104
|
+
.description('tower_init may snapshot uncommitted tracked changes into a dangling commit used as the mission baseline.'),
|
|
105
|
+
autoExclude: Schema.boolean()
|
|
106
|
+
.default(true)
|
|
107
|
+
.description('Add in-repo Tower artifacts (MISSIONS.md, in-repo worktree roots) to .git/info/exclude so they do not look like user dirt.'),
|
|
108
|
+
})
|
|
109
|
+
.default({})
|
|
110
|
+
.description('Repository initialization policy.'),
|
|
111
|
+
mode: Schema.object({
|
|
112
|
+
promptSection: Schema.string().description('Replacement Tower-mode system prompt text. Empty keeps the built-in reminder.'),
|
|
113
|
+
sectionAppend: Schema.string().description('Extra text appended to the Tower-mode system prompt section (team conventions).'),
|
|
114
|
+
allowAgentControl: Schema.boolean().description('Register tower_control so the model may enter/leave the mode with a reason.'),
|
|
115
|
+
autoPauseGoal: Schema.boolean().default(true).description('Pause an active goal when entering Tower mode.'),
|
|
116
|
+
requireResumeAuthorization: Schema.boolean()
|
|
117
|
+
.default(false)
|
|
118
|
+
.description('Require an authorization reference when resuming a mission paused with pauseCause=user.'),
|
|
119
|
+
requireHumanAckOnFirstEntry: Schema.boolean()
|
|
120
|
+
.default(false)
|
|
121
|
+
.description('Require a human /tower ack before the model may enter Tower mode for the first time in a session.'),
|
|
122
|
+
workspaceLock: Schema.boolean()
|
|
123
|
+
.default(false)
|
|
124
|
+
.description('Allow only one Tower session per repository (advisory <git-common-dir>/dsh-tower/tower.lock).'),
|
|
125
|
+
workspaceLockScope: Schema.union(['all', 'mutations'])
|
|
126
|
+
.default('all')
|
|
127
|
+
.description('all = every repo-scoped tool acquires the workspace lock; mutations = only tools that write state/worktrees do.'),
|
|
128
|
+
planModePolicy: Schema.union(['clear', 'refuse'])
|
|
129
|
+
.default('clear')
|
|
130
|
+
.description('On Tower entry: clear an active plan mode, or refuse to enter until the plan is exited.'),
|
|
131
|
+
workspaceStaleMs: Schema.natural()
|
|
132
|
+
.min(1000)
|
|
133
|
+
.default(300000)
|
|
134
|
+
.description('A workspace lock older than this is stale and can be taken over.'),
|
|
135
|
+
heartbeatMs: Schema.natural().min(1000).default(10000).description('Workspace-lock heartbeat interval while Tower mode is on.'),
|
|
136
|
+
denyTools: Schema.array(Schema.string())
|
|
137
|
+
.default(DEFAULT_DENY_TOOLS)
|
|
138
|
+
.description('Tools refused while Tower mode is active.'),
|
|
139
|
+
storePath: Schema.string().description('Directory for modes.json; defaults to $DSH_HOME/tower-mode.'),
|
|
140
|
+
maxEntries: Schema.natural().min(1).max(2000).default(200).description('Maximum persisted mode entries.'),
|
|
141
|
+
lockTimeoutMs: Schema.natural().min(10).max(60000).default(5000).description('How long saveModes waits for the cross-process modes.lock before giving up.'),
|
|
142
|
+
})
|
|
143
|
+
.default({})
|
|
144
|
+
.description('Tower-mode behavior and persistence.'),
|
|
145
|
+
review: Schema.object({
|
|
146
|
+
requireReview: Schema.boolean()
|
|
147
|
+
.default(false)
|
|
148
|
+
.description('Require an independent clean review before merge. Default false: review is optional; set true to enforce the review gate globally.'),
|
|
149
|
+
maxRoundsPerReviewer: Schema.natural().min(1).default(5).description('Review-round cap per reviewer per mission branch.'),
|
|
150
|
+
})
|
|
151
|
+
.default({})
|
|
152
|
+
.description('Review policy.'),
|
|
153
|
+
merge: Schema.object({
|
|
154
|
+
timeoutMs: Schema.natural().min(1000).default(900000).description('Default tower_merge timeout, including pre-merge hooks.'),
|
|
155
|
+
maxBufferBytes: Schema.natural().min(65536).default(67108864).description('Captured merge output buffer.'),
|
|
156
|
+
})
|
|
157
|
+
.default({})
|
|
158
|
+
.description('Merge execution policy.'),
|
|
159
|
+
git: Schema.object({
|
|
160
|
+
timeoutMs: Schema.natural().min(1000).default(15000).description('Default timeout for Git commands other than merge; merge uses merge.timeoutMs.'),
|
|
161
|
+
})
|
|
162
|
+
.default({})
|
|
163
|
+
.description('Git subprocess policy.'),
|
|
164
|
+
worktrees: Schema.object({
|
|
165
|
+
root: Schema.string().description('Default parent directory for mission worktrees; empty derives <main-checkout-parent>/.dsh-tower-worktrees/<hash>.'),
|
|
166
|
+
})
|
|
167
|
+
.default({})
|
|
168
|
+
.description('Worktree placement.'),
|
|
169
|
+
spawn: Schema.object({
|
|
170
|
+
workerPreset: Schema.string().default('tower-worker').description('Agent preset for worker missions.'),
|
|
171
|
+
reviewerPreset: Schema.string().default('tower-reviewer').description('Agent preset for reviewers.'),
|
|
172
|
+
maxDepth: Schema.natural().min(1).default(1).description('Subagent depth requested when spawning tower agents.'),
|
|
173
|
+
provider: Schema.string().description('Subagent provider id; empty picks spawn, then the first registered provider.'),
|
|
174
|
+
workerModel: ModelSchema().description('Model route for workers; omitted inherits the parent session.'),
|
|
175
|
+
reviewerModel: ModelSchema().description('Model route for reviewers; omitted inherits the parent session (recommended).'),
|
|
176
|
+
maxLiveWorkers: Schema.natural()
|
|
177
|
+
.min(1)
|
|
178
|
+
.max(64)
|
|
179
|
+
.default(4)
|
|
180
|
+
.description('Maximum live worker entries allowed per repository at once.'),
|
|
181
|
+
staleMs: Schema.natural()
|
|
182
|
+
.min(0)
|
|
183
|
+
.default(15000)
|
|
184
|
+
.description('Roster entries with no catalog activity after this age are marked stale and block merge (0 disables).'),
|
|
185
|
+
maxWaitMs: Schema.natural()
|
|
186
|
+
.min(1000)
|
|
187
|
+
.max(600000)
|
|
188
|
+
.default(90000)
|
|
189
|
+
.description('Upper bound for tower_spawn waitMs (default 90s, Kimi-compatible).'),
|
|
190
|
+
requireLease: Schema.string().description('Optional lease name tower_spawn must hold before starting a worker.'),
|
|
191
|
+
})
|
|
192
|
+
.default({})
|
|
193
|
+
.description('Worker and reviewer presets plus model routing.'),
|
|
194
|
+
send: Schema.object({
|
|
195
|
+
maxPerMinute: Schema.natural()
|
|
196
|
+
.min(0)
|
|
197
|
+
.default(120)
|
|
198
|
+
.description('Per-repository tower_send rate limit per minute (0 disables).'),
|
|
199
|
+
interruptDefault: Schema.boolean()
|
|
200
|
+
.default(false)
|
|
201
|
+
.description('Default value of tower_send.interrupt when the caller omits it.'),
|
|
202
|
+
})
|
|
203
|
+
.default({})
|
|
204
|
+
.description('Message delivery policy.'),
|
|
205
|
+
tests: Schema.object({
|
|
206
|
+
enabled: Schema.boolean().default(true).description('Run declared test discovery when a mission configures it.'),
|
|
207
|
+
admission: Schema.union(['spawn', 'complete', 'off'])
|
|
208
|
+
.default('off')
|
|
209
|
+
.description('Run discovery before spawning a worker, when completing, or only on explicit tower_test (default off: repository code runs only on an explicit tower_test).'),
|
|
210
|
+
timeoutMs: Schema.natural().min(1000).max(1800000).default(60000).description('Per discovery command timeout.'),
|
|
211
|
+
maxBytes: Schema.natural().min(1024).max(16 * 1024 * 1024).default(16 * 1024 * 1024).description('Maximum captured stdout bytes per command.'),
|
|
212
|
+
autoChecks: Schema.boolean().default(true).description('Attach a tests check to the completion report when discovery runs.'),
|
|
213
|
+
allowPackageDiscovery: Schema.boolean()
|
|
214
|
+
.default(true)
|
|
215
|
+
.description('Allow package.json tower.testDiscovery commands from the mission worktree (executes repository code; see security docs).'),
|
|
216
|
+
})
|
|
217
|
+
.default({})
|
|
218
|
+
.description('Declared test discovery and admission.'),
|
|
219
|
+
gates: Schema.object({
|
|
220
|
+
requireChecks: Schema.boolean()
|
|
221
|
+
.default(false)
|
|
222
|
+
.description('Require at least one completion check (tower_complete.checks) before a mission can merge.'),
|
|
223
|
+
requireTests: Schema.boolean()
|
|
224
|
+
.default(false)
|
|
225
|
+
.description('Require a passing test-discovery receipt for the reviewed worktree tip before merge.'),
|
|
226
|
+
allowReviewExemption: Schema.boolean()
|
|
227
|
+
.default(false)
|
|
228
|
+
.description('Allow reviewPolicy:none missions with a non-empty reviewExemptionReason to skip review blockers.'),
|
|
229
|
+
})
|
|
230
|
+
.default({})
|
|
231
|
+
.description('Merge-gate strictness.'),
|
|
232
|
+
docs: Schema.object({
|
|
233
|
+
board: Schema.boolean()
|
|
234
|
+
.default(true)
|
|
235
|
+
.description('Write a human-readable MISSIONS.md after mission changes.'),
|
|
236
|
+
boardLocation: Schema.union(['common', 'repo'])
|
|
237
|
+
.default('common')
|
|
238
|
+
.description('common = <git-common-dir>/dsh-tower/MISSIONS.md; repo = <repoRoot>/MISSIONS.md.'),
|
|
239
|
+
})
|
|
240
|
+
.default({})
|
|
241
|
+
.description('Human-readable artifacts.'),
|
|
242
|
+
tools: Schema.object({
|
|
243
|
+
exposeOperational: Schema.boolean()
|
|
244
|
+
.default(true)
|
|
245
|
+
.description('Master switch for batch/resource/import/legacy/recover/test/doctor tools.'),
|
|
246
|
+
exposeBatch: Schema.boolean().default(true).description('Register tower_batch.'),
|
|
247
|
+
exposeImport: Schema.boolean().default(true).description('Register tower_import_legacy/tower_legacy_map.'),
|
|
248
|
+
exposeLeases: Schema.boolean().default(true).description('Register tower_resource advisory leases.'),
|
|
249
|
+
exposeTests: Schema.boolean().default(true).description('Register tower_test; gates.requireTests requires it.'),
|
|
250
|
+
exposeRecovery: Schema.boolean().default(true).description('Register tower_recover (stale state-lock escape).'),
|
|
251
|
+
exposeDoctor: Schema.boolean().default(true).description('Register tower_doctor (read-only health contract).'),
|
|
252
|
+
})
|
|
253
|
+
.default({})
|
|
254
|
+
.description('Tool-surface policy.'),
|
|
255
|
+
leases: Schema.object({
|
|
256
|
+
ttlMs: Schema.natural().min(1000).max(86400000).default(900000).description('Default advisory lease lifetime; expiry never auto-releases.'),
|
|
257
|
+
})
|
|
258
|
+
.default({})
|
|
259
|
+
.description('Advisory resource leases.'),
|
|
260
|
+
dev: Schema.object({
|
|
261
|
+
hotReload: Schema.boolean().default(true).description('Register tower_reload and /tower reload when the DSH HMR service is available.'),
|
|
262
|
+
reloadDelayMs: Schema.natural().min(1).max(5000).default(50).description('Delay before a scheduled reload, so the tool result is delivered first.'),
|
|
263
|
+
reloadRequiredIds: Schema.array(Schema.string())
|
|
264
|
+
.default(['tower-bundle', 'tower', 'tower-skills', 'preset-tower-worker', 'preset-tower-reviewer'])
|
|
265
|
+
.description('Loader entry ids passed to reconcileProfile as required.'),
|
|
266
|
+
})
|
|
267
|
+
.default({})
|
|
268
|
+
.description('Development and hot-reload behavior.'),
|
|
269
|
+
}).description('DSH Tower configuration')
|
|
270
|
+
|
|
271
|
+
export function clone(value) {
|
|
272
|
+
return value === undefined ? undefined : JSON.parse(JSON.stringify(value))
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function isPlainObject(value) {
|
|
276
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function deepMerge(base, override) {
|
|
280
|
+
if (override === undefined) return clone(base) ?? {}
|
|
281
|
+
if (!isPlainObject(base) || !isPlainObject(override)) return clone(override)
|
|
282
|
+
const out = clone(base)
|
|
283
|
+
for (const [key, value] of Object.entries(override)) {
|
|
284
|
+
out[key] = value === undefined ? out[key] : deepMerge(base[key], value)
|
|
285
|
+
}
|
|
286
|
+
return out
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Move the legacy top-level aliases into their canonical group keys. */
|
|
290
|
+
export function normalizeLegacy(raw = {}) {
|
|
291
|
+
const out = clone(raw) ?? {}
|
|
292
|
+
if (out.section !== undefined) {
|
|
293
|
+
const mode = { ...(out.mode ?? {}) }
|
|
294
|
+
if (mode.promptSection === undefined) mode.promptSection = out.section
|
|
295
|
+
out.mode = mode
|
|
296
|
+
delete out.section
|
|
297
|
+
}
|
|
298
|
+
if (out.allowAgentControl !== undefined) {
|
|
299
|
+
const mode = { ...(out.mode ?? {}) }
|
|
300
|
+
if (mode.allowAgentControl === undefined) mode.allowAgentControl = out.allowAgentControl
|
|
301
|
+
out.mode = mode
|
|
302
|
+
delete out.allowAgentControl
|
|
303
|
+
}
|
|
304
|
+
return out
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function assertModelKeys(value, label) {
|
|
308
|
+
const bad = Object.keys(value).filter((key) => !MODEL_KEYS.includes(key))
|
|
309
|
+
if (bad.length > 0) {
|
|
310
|
+
throw new TowerToolError('config-unknown-key', `${label} has unknown key(s) ${bad.join(', ')}`)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function assertKnownKeys(raw, label) {
|
|
315
|
+
const unknown = Object.keys(raw).filter((key) => !LEGACY_KEYS.includes(key) && !GROUP_KEYS.includes(key))
|
|
316
|
+
if (unknown.length > 0) {
|
|
317
|
+
throw new TowerToolError(
|
|
318
|
+
'config-unknown-key',
|
|
319
|
+
`${label} has unknown key(s) ${unknown.join(', ')} — config is { section?, allowAgentControl?, init?, mode?, review?, merge?, git?, worktrees?, spawn?, send?, tests?, gates?, docs?, leases?, dev? }`,
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
for (const [group, allowed] of Object.entries(SECTION_KEYS)) {
|
|
323
|
+
const value = raw[group]
|
|
324
|
+
if (value === undefined) continue
|
|
325
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) continue
|
|
326
|
+
const bad = Object.keys(value).filter((key) => !allowed.includes(key))
|
|
327
|
+
if (bad.length > 0) {
|
|
328
|
+
throw new TowerToolError(
|
|
329
|
+
'config-unknown-key',
|
|
330
|
+
`${label} has unknown key(s) ${bad.map((key) => `${group}.${key}`).join(', ')}`,
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
if ((group === 'spawn') && value.workerModel && typeof value.workerModel === 'object') {
|
|
334
|
+
assertModelKeys(value.workerModel, `${label} spawn.workerModel`)
|
|
335
|
+
}
|
|
336
|
+
if ((group === 'spawn') && value.reviewerModel && typeof value.reviewerModel === 'object') {
|
|
337
|
+
assertModelKeys(value.reviewerModel, `${label} spawn.reviewerModel`)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
343
|
+
|
|
344
|
+
/** Reject prototype-pollution style keys anywhere in a repository config. */
|
|
345
|
+
export function assertSafeKeys(value, label) {
|
|
346
|
+
if (Array.isArray(value)) {
|
|
347
|
+
for (const item of value) assertSafeKeys(item, label)
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
if (!isPlainObject(value)) return
|
|
351
|
+
for (const [key, child] of Object.entries(value)) {
|
|
352
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
353
|
+
throw new TowerToolError('config-invalid', `${label} contains the forbidden key "${key}"`)
|
|
354
|
+
}
|
|
355
|
+
assertSafeKeys(child, label)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function resolveTowerConfig(rawConfig) {
|
|
360
|
+
const raw = rawConfig ?? {}
|
|
361
|
+
if (raw !== null && typeof raw !== 'object') throw new TowerToolError('config-invalid', 'Tower config must be an object')
|
|
362
|
+
assertKnownKeys(raw, 'Tower config')
|
|
363
|
+
let parsed
|
|
364
|
+
try {
|
|
365
|
+
parsed = Config(raw)
|
|
366
|
+
} catch (error) {
|
|
367
|
+
throw new TowerToolError('config-invalid', `Tower config is invalid: ${String(error?.message ?? error)}`)
|
|
368
|
+
}
|
|
369
|
+
const mode = clone(parsed.mode ?? {})
|
|
370
|
+
if (parsed.gates?.requireTests === true) {
|
|
371
|
+
const testsAvailable = parsed.tests?.enabled !== false && parsed.tools?.exposeOperational !== false && parsed.tools?.exposeTests !== false
|
|
372
|
+
if (!testsAvailable) {
|
|
373
|
+
throw new TowerToolError('config-invalid', 'Tower config is invalid: gates.requireTests is true but test discovery is disabled (tests.enabled/tools.exposeTests)')
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Canonical keys win; the legacy top-level aliases only fill gaps.
|
|
377
|
+
const rawMode = raw.mode ?? {}
|
|
378
|
+
if (rawMode.promptSection === undefined && raw.section !== undefined) mode.promptSection = raw.section
|
|
379
|
+
if (rawMode.allowAgentControl === undefined && raw.allowAgentControl !== undefined) {
|
|
380
|
+
mode.allowAgentControl = raw.allowAgentControl
|
|
381
|
+
}
|
|
382
|
+
// Apply the canonical default only after alias resolution: a validated
|
|
383
|
+
// Cordis config may omit this key, and an explicit legacy top-level
|
|
384
|
+
// `allowAgentControl:true` must not be shadowed by a schema default.
|
|
385
|
+
if (mode.allowAgentControl === undefined) mode.allowAgentControl = true
|
|
386
|
+
if (!Array.isArray(mode.denyTools) || mode.denyTools.length === 0) mode.denyTools = [...DEFAULT_DENY_TOOLS]
|
|
387
|
+
return {
|
|
388
|
+
section: mode.promptSection ?? '',
|
|
389
|
+
init: clone(parsed.init ?? {}),
|
|
390
|
+
mode,
|
|
391
|
+
review: clone(parsed.review ?? {}),
|
|
392
|
+
merge: clone(parsed.merge ?? {}),
|
|
393
|
+
git: clone(parsed.git ?? {}),
|
|
394
|
+
worktrees: clone(parsed.worktrees ?? {}),
|
|
395
|
+
spawn: clone(parsed.spawn ?? {}),
|
|
396
|
+
send: clone(parsed.send ?? {}),
|
|
397
|
+
tests: clone(parsed.tests ?? {}),
|
|
398
|
+
gates: clone(parsed.gates ?? {}),
|
|
399
|
+
docs: clone(parsed.docs ?? {}),
|
|
400
|
+
tools: clone(parsed.tools ?? {}),
|
|
401
|
+
leases: clone(parsed.leases ?? {}),
|
|
402
|
+
dev: clone(parsed.dev ?? {}),
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Tool schema primitives and output schemas.
|
|
2
|
+
|
|
3
|
+
const S = {
|
|
4
|
+
str: { type: 'string' },
|
|
5
|
+
int: { type: 'integer' },
|
|
6
|
+
bool: { type: 'boolean' },
|
|
7
|
+
arr: (items) => ({ type: 'array', items }),
|
|
8
|
+
obj: (properties = {}, required = []) => ({ type: 'object', properties, required, additionalProperties: true }),
|
|
9
|
+
any: {},
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const blockerSchema = S.obj({ code: S.str, detail: S.str, cleanup: S.str, timeout: S.bool }, ['code', 'detail'])
|
|
13
|
+
const worktreeSchema = S.obj({ path: S.str, branch: S.str, baseCommit: S.str })
|
|
14
|
+
const missionSummarySchema = S.obj({
|
|
15
|
+
id: S.str,
|
|
16
|
+
title: S.str,
|
|
17
|
+
kind: S.str,
|
|
18
|
+
status: S.str,
|
|
19
|
+
scope: S.arr(S.str),
|
|
20
|
+
deps: S.arr(S.str),
|
|
21
|
+
owner: S.any,
|
|
22
|
+
worktree: S.any,
|
|
23
|
+
blockers: S.arr(S.str),
|
|
24
|
+
tasks: S.any,
|
|
25
|
+
scopeGeneration: S.int,
|
|
26
|
+
mergedCommit: S.any,
|
|
27
|
+
mergedAt: S.any,
|
|
28
|
+
updatedAt: S.any,
|
|
29
|
+
})
|
|
30
|
+
const findingSummarySchema = S.obj({
|
|
31
|
+
id: S.str,
|
|
32
|
+
seq: S.int,
|
|
33
|
+
type: S.str,
|
|
34
|
+
title: S.str,
|
|
35
|
+
severity: S.any,
|
|
36
|
+
status: S.str,
|
|
37
|
+
missionId: S.any,
|
|
38
|
+
branch: S.any,
|
|
39
|
+
reportedBy: S.any,
|
|
40
|
+
at: S.any,
|
|
41
|
+
updatedAt: S.any,
|
|
42
|
+
closedAt: S.any,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Per-tool output schemas. `additionalProperties: true` keeps future fields
|
|
46
|
+
// additive; `required` lists only fields every successful path returns.
|
|
47
|
+
const SCHEMAS = {
|
|
48
|
+
init: S.obj(
|
|
49
|
+
{
|
|
50
|
+
created: S.bool,
|
|
51
|
+
repoRoot: S.str,
|
|
52
|
+
commonDir: S.str,
|
|
53
|
+
statePath: S.str,
|
|
54
|
+
base: S.str,
|
|
55
|
+
revision: S.int,
|
|
56
|
+
missions: S.int,
|
|
57
|
+
},
|
|
58
|
+
['created', 'repoRoot', 'commonDir', 'statePath', 'base', 'revision', 'missions'],
|
|
59
|
+
),
|
|
60
|
+
plan: S.obj({ revision: S.int, missions: S.arr(missionSummarySchema) }, ['revision', 'missions']),
|
|
61
|
+
mission: S.obj({ revision: S.int, mission: S.obj() }, ['revision', 'mission']),
|
|
62
|
+
status: S.obj(
|
|
63
|
+
{
|
|
64
|
+
repoRoot: S.str,
|
|
65
|
+
commonDir: S.str,
|
|
66
|
+
statePath: S.str,
|
|
67
|
+
base: S.str,
|
|
68
|
+
revision: S.int,
|
|
69
|
+
counts: S.obj(),
|
|
70
|
+
openTasks: S.int,
|
|
71
|
+
roster: S.int,
|
|
72
|
+
messages: S.int,
|
|
73
|
+
unreadTower: S.int,
|
|
74
|
+
findings: S.int,
|
|
75
|
+
findingsByStatus: S.obj(),
|
|
76
|
+
reviews: S.int,
|
|
77
|
+
config: S.obj({ path: S.any, repoConfigPresent: S.bool, spawn: S.obj() }),
|
|
78
|
+
recentFindings: S.arr(S.obj()),
|
|
79
|
+
missions: S.arr(S.obj()),
|
|
80
|
+
activity: S.arr(S.obj()),
|
|
81
|
+
mode: S.any,
|
|
82
|
+
spawn: S.any,
|
|
83
|
+
towerAgentId: S.any,
|
|
84
|
+
ownerMissing: S.bool,
|
|
85
|
+
},
|
|
86
|
+
['repoRoot', 'commonDir', 'statePath', 'base', 'revision', 'counts', 'missions', 'activity'],
|
|
87
|
+
),
|
|
88
|
+
spawn: S.obj(
|
|
89
|
+
{
|
|
90
|
+
name: S.str,
|
|
91
|
+
agentId: S.str,
|
|
92
|
+
role: S.str,
|
|
93
|
+
missionId: S.str,
|
|
94
|
+
preset: S.str,
|
|
95
|
+
provider: S.str,
|
|
96
|
+
maxDepth: S.int,
|
|
97
|
+
agentOptions: S.any,
|
|
98
|
+
worktree: worktreeSchema,
|
|
99
|
+
created: S.bool,
|
|
100
|
+
briefingChars: S.int,
|
|
101
|
+
rosterEntry: S.obj(),
|
|
102
|
+
stopReason: S.any,
|
|
103
|
+
timedOut: S.bool,
|
|
104
|
+
output: S.any,
|
|
105
|
+
diagnostic: S.any,
|
|
106
|
+
},
|
|
107
|
+
['name', 'agentId', 'role', 'missionId', 'preset', 'provider', 'worktree'],
|
|
108
|
+
),
|
|
109
|
+
roster: S.obj(
|
|
110
|
+
{ revision: S.int, roster: S.arr(S.obj()), parentStatus: S.obj(), missions: S.arr(S.obj()) },
|
|
111
|
+
['revision', 'roster', 'parentStatus', 'missions'],
|
|
112
|
+
),
|
|
113
|
+
diff: S.obj(
|
|
114
|
+
{
|
|
115
|
+
missionId: S.str,
|
|
116
|
+
branch: S.str,
|
|
117
|
+
baseTip: S.str,
|
|
118
|
+
branchTip: S.str,
|
|
119
|
+
mergeBase: S.str,
|
|
120
|
+
files: S.arr(S.str),
|
|
121
|
+
commits: S.arr(S.str),
|
|
122
|
+
truncated: S.bool,
|
|
123
|
+
patch: S.str,
|
|
124
|
+
},
|
|
125
|
+
['missionId', 'branch', 'baseTip', 'branchTip', 'mergeBase', 'files', 'commits', 'truncated', 'patch'],
|
|
126
|
+
),
|
|
127
|
+
adopt: S.obj(
|
|
128
|
+
{ retired: S.arr(S.str), skippedUnknownParents: S.arr(S.str), roster: S.arr(S.obj()), parentStatus: S.obj() },
|
|
129
|
+
['retired', 'skippedUnknownParents', 'roster', 'parentStatus'],
|
|
130
|
+
),
|
|
131
|
+
send: S.obj({ message: S.obj(), deliveries: S.arr(S.obj()) }, ['message', 'deliveries']),
|
|
132
|
+
inbox: S.obj(
|
|
133
|
+
{ reader: S.str, messages: S.arr(S.obj()), pageSize: S.int, unreadRemaining: S.int },
|
|
134
|
+
['reader', 'messages', 'pageSize', 'unreadRemaining'],
|
|
135
|
+
),
|
|
136
|
+
complete: S.obj({ mission: S.obj(), commits: S.int, unread: S.int }, ['mission', 'commits', 'unread']),
|
|
137
|
+
finding: S.obj(
|
|
138
|
+
{
|
|
139
|
+
id: S.str,
|
|
140
|
+
seq: S.int,
|
|
141
|
+
type: S.str,
|
|
142
|
+
title: S.str,
|
|
143
|
+
severity: S.any,
|
|
144
|
+
summary: S.str,
|
|
145
|
+
location: S.any,
|
|
146
|
+
details: S.str,
|
|
147
|
+
suggested_fix: S.str,
|
|
148
|
+
missionId: S.any,
|
|
149
|
+
branch: S.any,
|
|
150
|
+
reportedBy: S.any,
|
|
151
|
+
at: S.any,
|
|
152
|
+
status: S.str,
|
|
153
|
+
updatedAt: S.any,
|
|
154
|
+
closedAt: S.any,
|
|
155
|
+
notes: S.arr(S.obj()),
|
|
156
|
+
},
|
|
157
|
+
['id', 'type', 'title', 'summary', 'details', 'suggested_fix', 'status'],
|
|
158
|
+
),
|
|
159
|
+
findings: S.obj(
|
|
160
|
+
{
|
|
161
|
+
findings: S.arr(findingSummarySchema),
|
|
162
|
+
finding: S.obj(),
|
|
163
|
+
counts: S.obj(),
|
|
164
|
+
total: S.int,
|
|
165
|
+
filtered: S.int,
|
|
166
|
+
action: S.str,
|
|
167
|
+
},
|
|
168
|
+
['counts', 'total'],
|
|
169
|
+
),
|
|
170
|
+
review: S.obj(
|
|
171
|
+
{ review: S.obj(), branchTip: S.str, missionStatus: S.str, round: S.int, reviewerRounds: S.int },
|
|
172
|
+
['review', 'branchTip', 'missionStatus', 'round', 'reviewerRounds'],
|
|
173
|
+
),
|
|
174
|
+
preflight: S.obj(
|
|
175
|
+
{
|
|
176
|
+
ok: S.bool,
|
|
177
|
+
blockers: S.arr(blockerSchema),
|
|
178
|
+
missionId: S.str,
|
|
179
|
+
branchTip: S.any,
|
|
180
|
+
baseTip: S.any,
|
|
181
|
+
changedFiles: S.any,
|
|
182
|
+
review: S.any,
|
|
183
|
+
},
|
|
184
|
+
['ok', 'blockers', 'missionId'],
|
|
185
|
+
),
|
|
186
|
+
merge: S.obj(
|
|
187
|
+
{
|
|
188
|
+
ok: S.bool,
|
|
189
|
+
missionId: S.str,
|
|
190
|
+
mergedCommit: S.any,
|
|
191
|
+
baseTip: S.any,
|
|
192
|
+
branchTip: S.any,
|
|
193
|
+
parents: S.any,
|
|
194
|
+
blockers: S.arr(blockerSchema),
|
|
195
|
+
warnings: S.any,
|
|
196
|
+
alreadyMerged: S.bool,
|
|
197
|
+
recoveredFromFailure: S.bool,
|
|
198
|
+
},
|
|
199
|
+
['ok'],
|
|
200
|
+
),
|
|
201
|
+
rebase: S.obj(
|
|
202
|
+
{
|
|
203
|
+
ok: S.bool,
|
|
204
|
+
oldTip: S.any,
|
|
205
|
+
newTip: S.any,
|
|
206
|
+
baseTip: S.any,
|
|
207
|
+
alreadyMerged: S.bool,
|
|
208
|
+
alreadyUpToDate: S.bool,
|
|
209
|
+
skipped: S.bool,
|
|
210
|
+
reviewWaived: S.bool,
|
|
211
|
+
note: S.str,
|
|
212
|
+
blockers: S.arr(blockerSchema),
|
|
213
|
+
},
|
|
214
|
+
['ok'],
|
|
215
|
+
),
|
|
216
|
+
teardown: S.obj(
|
|
217
|
+
{ ok: S.bool, dryRun: S.bool, removed: S.arr(S.str), decisions: S.arr(S.obj()) },
|
|
218
|
+
['ok', 'dryRun', 'removed', 'decisions'],
|
|
219
|
+
),
|
|
220
|
+
interrupt: S.obj(
|
|
221
|
+
{
|
|
222
|
+
ok: S.bool,
|
|
223
|
+
interrupted: S.bool,
|
|
224
|
+
previousActivity: S.str,
|
|
225
|
+
entry: S.obj(),
|
|
226
|
+
reason: S.str,
|
|
227
|
+
},
|
|
228
|
+
['ok', 'interrupted'],
|
|
229
|
+
),
|
|
230
|
+
reload: S.obj(
|
|
231
|
+
{
|
|
232
|
+
ok: S.bool,
|
|
233
|
+
scheduled: S.bool,
|
|
234
|
+
delayMs: S.int,
|
|
235
|
+
requiredIds: S.arr(S.str),
|
|
236
|
+
error: S.str,
|
|
237
|
+
restartHint: S.str,
|
|
238
|
+
},
|
|
239
|
+
['ok', 'scheduled'],
|
|
240
|
+
),
|
|
241
|
+
doctor: S.obj({ ok: S.bool, checks: S.arr(S.obj()), environmentContract: S.obj() }, ['ok', 'checks']),
|
|
242
|
+
recover: S.obj(
|
|
243
|
+
{ ok: S.bool, action: S.str, stateLock: S.any, cleared: S.bool, backup: S.any, error: S.str },
|
|
244
|
+
['ok', 'action'],
|
|
245
|
+
),
|
|
246
|
+
test: S.obj(
|
|
247
|
+
{
|
|
248
|
+
ok: S.bool,
|
|
249
|
+
configured: S.bool,
|
|
250
|
+
treeTip: S.any,
|
|
251
|
+
tests: S.arr(S.obj()),
|
|
252
|
+
commands: S.arr(S.obj()),
|
|
253
|
+
error: S.str,
|
|
254
|
+
receipt: S.obj(),
|
|
255
|
+
checks: S.arr(S.obj()),
|
|
256
|
+
},
|
|
257
|
+
['ok', 'configured'],
|
|
258
|
+
),
|
|
259
|
+
batch: S.obj(
|
|
260
|
+
{ ok: S.bool, action: S.str, batch: S.obj(), batches: S.arr(S.obj()), blockers: S.arr(blockerSchema), summary: S.obj() },
|
|
261
|
+
['ok', 'action'],
|
|
262
|
+
),
|
|
263
|
+
resource: S.obj(
|
|
264
|
+
{ ok: S.bool, action: S.str, lease: S.obj(), leases: S.arr(S.obj()), released: S.bool },
|
|
265
|
+
['ok', 'action'],
|
|
266
|
+
),
|
|
267
|
+
importLegacy: S.obj({ ok: S.bool, summary: S.obj(), snapshot: S.obj() }, ['ok', 'summary']),
|
|
268
|
+
legacyMap: S.obj({ ok: S.bool, map: S.obj(), maps: S.arr(S.obj()) }, ['ok']),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function renderValue(_args, value) {
|
|
272
|
+
return [{ type: 'text', text: JSON.stringify(value, null, 2) }]
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function output(schema) {
|
|
276
|
+
return { schema, render: renderValue }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export { S, SCHEMAS, output }
|