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
package/lib/store.js
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
+
import { TowerToolError } from '../domain/errors.js'
|
|
1
2
|
// Durable repo-scoped store: exclusive lock + atomic JSON + append-only activity.
|
|
2
|
-
|
|
3
|
+
// The atomic/lock/JSON primitives are shared with the mode store (#56 P2b).
|
|
4
|
+
import { fsp } from '../adapters/store-fs.js'
|
|
5
|
+
import {
|
|
6
|
+
PROCESS_STARTED_AT,
|
|
7
|
+
acquireLock,
|
|
8
|
+
lockRecord,
|
|
9
|
+
readLock,
|
|
10
|
+
} from './persistence.js'
|
|
3
11
|
import path from 'node:path'
|
|
4
12
|
import crypto from 'node:crypto'
|
|
5
13
|
|
|
14
|
+
export {
|
|
15
|
+
readJson,
|
|
16
|
+
writeJsonAtomic,
|
|
17
|
+
writeJsonAtomicSync,
|
|
18
|
+
renameWithRetry,
|
|
19
|
+
appendJsonl,
|
|
20
|
+
} from './persistence.js'
|
|
21
|
+
|
|
6
22
|
export function pathsFor(commonDir) {
|
|
7
23
|
const dir = path.join(commonDir, 'dsh-tower')
|
|
8
24
|
return {
|
|
9
25
|
dir,
|
|
10
26
|
state: path.join(dir, 'state.json'),
|
|
11
27
|
lock: path.join(dir, 'state.lock'),
|
|
28
|
+
gitLock: path.join(dir, 'git.lock'),
|
|
12
29
|
activity: path.join(dir, 'activity.jsonl'),
|
|
13
30
|
}
|
|
14
31
|
}
|
|
@@ -26,55 +43,6 @@ export async function ensureDir(dir) {
|
|
|
26
43
|
await fsp.mkdir(dir, { recursive: true })
|
|
27
44
|
}
|
|
28
45
|
|
|
29
|
-
export async function readJson(file) {
|
|
30
|
-
const text = await fsp.readFile(file, 'utf8')
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(text)
|
|
33
|
-
} catch (error) {
|
|
34
|
-
throw new Error(`corrupt tower state at ${file}: ${error.message}`)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function writeJsonAtomic(file, value) {
|
|
39
|
-
const dir = path.dirname(file)
|
|
40
|
-
const tmp = path.join(dir, `.${path.basename(file)}.tmp-${process.pid}-${crypto.randomUUID()}`)
|
|
41
|
-
const handle = await fsp.open(tmp, 'w')
|
|
42
|
-
try {
|
|
43
|
-
await handle.writeFile(JSON.stringify(value, null, 2), 'utf8')
|
|
44
|
-
await handle.sync()
|
|
45
|
-
} finally {
|
|
46
|
-
await handle.close()
|
|
47
|
-
}
|
|
48
|
-
await renameWithRetry(tmp, file)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Windows can reject a rename while an indexer, antivirus, or a concurrent
|
|
53
|
-
* reader still holds the destination; retry briefly instead of failing a
|
|
54
|
-
* successful mutation. The temp file is removed when the rename never lands.
|
|
55
|
-
*/
|
|
56
|
-
async function renameWithRetry(tmp, file) {
|
|
57
|
-
for (let attempt = 0; ; attempt += 1) {
|
|
58
|
-
try {
|
|
59
|
-
await fsp.rename(tmp, file)
|
|
60
|
-
return
|
|
61
|
-
} catch (error) {
|
|
62
|
-
const transient = error?.code === 'EPERM' || error?.code === 'EBUSY' || error?.code === 'EACCES'
|
|
63
|
-
if (!transient || attempt >= 12) {
|
|
64
|
-
await fsp.unlink(tmp).catch(() => {})
|
|
65
|
-
throw error
|
|
66
|
-
}
|
|
67
|
-
await sleep(10 + attempt * 15)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export { renameWithRetry }
|
|
73
|
-
|
|
74
|
-
export async function appendJsonl(file, value) {
|
|
75
|
-
await fsp.appendFile(file, `${JSON.stringify(value)}\n`, 'utf8')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
46
|
export function shortHash(text) {
|
|
79
47
|
return crypto.createHash('sha256').update(String(text)).digest('hex').slice(0, 12)
|
|
80
48
|
}
|
|
@@ -94,53 +62,57 @@ export async function readActivityTail(file, limit = 12) {
|
|
|
94
62
|
return out
|
|
95
63
|
}
|
|
96
64
|
|
|
97
|
-
function
|
|
98
|
-
return
|
|
65
|
+
export async function readStateLock(paths) {
|
|
66
|
+
return readLock(paths.lock)
|
|
99
67
|
}
|
|
100
68
|
|
|
101
69
|
/**
|
|
102
|
-
* Hold the repo-wide tower lock for one mutation.
|
|
103
|
-
*
|
|
70
|
+
* Hold the repo-wide tower lock for one mutation. A lock whose recorded PID is
|
|
71
|
+
* provably dead is reclaimed atomically; a live lock is reported, never stolen.
|
|
104
72
|
*/
|
|
105
73
|
export async function withLock(paths, fn, options = {}) {
|
|
106
74
|
const timeoutMs = options.timeoutMs ?? 5000
|
|
107
75
|
const pollMs = options.pollMs ?? 25
|
|
108
76
|
const token = crypto.randomUUID()
|
|
109
|
-
const record =
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
let owner = null
|
|
120
|
-
try {
|
|
121
|
-
owner = await fsp.readFile(paths.lock, 'utf8')
|
|
122
|
-
} catch {
|
|
123
|
-
owner = null
|
|
124
|
-
}
|
|
125
|
-
throw new Error(
|
|
126
|
-
`tower state is locked by another writer${owner ? ` (${owner})` : ''}; retry after it finishes`,
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
await sleep(pollMs)
|
|
130
|
-
}
|
|
77
|
+
const record = lockRecord(token, { startedAt: PROCESS_STARTED_AT })
|
|
78
|
+
const held = await acquireLock(paths.lock, { token, record, timeoutMs, pollMs })
|
|
79
|
+
if (!held) {
|
|
80
|
+
const owner = await readStateLock(paths)
|
|
81
|
+
const ownerRecord = owner.record
|
|
82
|
+
const payload = owner.raw ?? (ownerRecord ? JSON.stringify(ownerRecord) : null)
|
|
83
|
+
throw new TowerToolError(
|
|
84
|
+
'state-locked',
|
|
85
|
+
`tower state is locked by another writer${payload ? ` (${payload})` : ''}; retry after it finishes`,
|
|
86
|
+
)
|
|
131
87
|
}
|
|
88
|
+
const startedAt = Date.now()
|
|
89
|
+
try {
|
|
90
|
+
return await fn(paths)
|
|
91
|
+
} finally {
|
|
92
|
+
await held.release()
|
|
93
|
+
await fsp
|
|
94
|
+
.appendFile(path.join(paths.dir, 'lock-holds.jsonl'), `${JSON.stringify({ at: Date.now(), ms: Date.now() - startedAt, pid: process.pid })}\n`, 'utf8')
|
|
95
|
+
.catch(() => {})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
132
98
|
|
|
99
|
+
/** Serialize git mutations without holding the state lock. */
|
|
100
|
+
export async function withGitLock(paths, fn, options = {}) {
|
|
101
|
+
const timeoutMs = options.timeoutMs ?? 60000
|
|
102
|
+
const pollMs = options.pollMs ?? 25
|
|
103
|
+
const lock = paths.gitLock
|
|
104
|
+
const token = crypto.randomUUID()
|
|
105
|
+
const startedAt = Date.now()
|
|
106
|
+
const held = await acquireLock(lock, { token, record: lockRecord(token, { kind: 'git' }), timeoutMs, pollMs })
|
|
107
|
+
if (!held) {
|
|
108
|
+
throw new TowerToolError('state-locked', 'another git mutation is in progress for this repository; retry after it finishes')
|
|
109
|
+
}
|
|
133
110
|
try {
|
|
134
111
|
return await fn(paths)
|
|
135
112
|
} finally {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (parsed && parsed.token === token) await fsp.unlink(paths.lock)
|
|
141
|
-
}
|
|
142
|
-
} catch {
|
|
143
|
-
// a lock left behind by a crash is reported by the next writer, not fixed here
|
|
144
|
-
}
|
|
113
|
+
await held.release()
|
|
114
|
+
await fsp
|
|
115
|
+
.appendFile(path.join(paths.dir, 'lock-holds.jsonl'), `${JSON.stringify({ at: Date.now(), ms: Date.now() - startedAt, pid: process.pid, phase: 'git' })}\n`, 'utf8')
|
|
116
|
+
.catch(() => {})
|
|
145
117
|
}
|
|
146
118
|
}
|
package/lib/tests.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TowerToolError } from '../domain/errors.js'
|
|
1
2
|
// Tower test discovery (2.1): fail-closed admission evidence from the
|
|
2
3
|
// repository's own `package.json` `tower.testDiscovery` block.
|
|
3
4
|
//
|
|
@@ -18,10 +19,10 @@
|
|
|
18
19
|
// minCount/required shortfall) returns `ok: false` in the receipt;
|
|
19
20
|
// - an invalid discovery configuration (or invalid call arguments) throws a
|
|
20
21
|
// plain `Error`, which the tool layer wraps as `tower:invalid-argument`.
|
|
21
|
-
import { spawn } from '
|
|
22
|
-
import { createWriteStream } from '
|
|
22
|
+
import { spawn } from '../adapters/vcs-git.js'
|
|
23
|
+
import { createWriteStream } from '../adapters/store-fs.js'
|
|
23
24
|
import crypto from 'node:crypto'
|
|
24
|
-
import fsp from '
|
|
25
|
+
import { fsp } from '../adapters/store-fs.js'
|
|
25
26
|
import path from 'node:path'
|
|
26
27
|
|
|
27
28
|
import { tryGit } from './git.js'
|
|
@@ -45,7 +46,7 @@ function nonEmptyString(value) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function normalizePositiveInteger(value, name) {
|
|
48
|
-
if (!Number.isInteger(value) || value <= 0) throw new
|
|
49
|
+
if (!Number.isInteger(value) || value <= 0) throw new TowerToolError('invalid-argument', `${name} must be an integer > 0`)
|
|
49
50
|
return value
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -253,6 +254,40 @@ export function parseJsonArray(stdout) {
|
|
|
253
254
|
return { tests, ok: failed === 0, count: tests.length, failed, error: null }
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
/** Test discovery is repository-controlled code: do not hand it secrets. */
|
|
258
|
+
const TEST_ENV_ALLOW = new Set([
|
|
259
|
+
'PATH',
|
|
260
|
+
'Path',
|
|
261
|
+
'PATHEXT',
|
|
262
|
+
'SystemRoot',
|
|
263
|
+
'WINDIR',
|
|
264
|
+
'ComSpec',
|
|
265
|
+
'HOME',
|
|
266
|
+
'USERPROFILE',
|
|
267
|
+
'TMP',
|
|
268
|
+
'TEMP',
|
|
269
|
+
'TMPDIR',
|
|
270
|
+
'LANG',
|
|
271
|
+
'LC_ALL',
|
|
272
|
+
'LC_CTYPE',
|
|
273
|
+
'SHELL',
|
|
274
|
+
'TERM',
|
|
275
|
+
'APPDATA',
|
|
276
|
+
'LOCALAPPDATA',
|
|
277
|
+
'ProgramFiles',
|
|
278
|
+
'ProgramFiles(x86)',
|
|
279
|
+
'NODE_PATH',
|
|
280
|
+
])
|
|
281
|
+
|
|
282
|
+
export function testProcessEnv(source = process.env) {
|
|
283
|
+
const env = {}
|
|
284
|
+
for (const [key, value] of Object.entries(source)) {
|
|
285
|
+
if (!TEST_ENV_ALLOW.has(key)) continue
|
|
286
|
+
env[key] = value
|
|
287
|
+
}
|
|
288
|
+
return env
|
|
289
|
+
}
|
|
290
|
+
|
|
256
291
|
/** Platform shell for discovery commands (PowerShell 7 on Windows, bash on POSIX). */
|
|
257
292
|
function shellSpec(platform = process.platform) {
|
|
258
293
|
if (platform === 'win32') {
|
|
@@ -379,7 +414,7 @@ function spawnToFiles({ command, cwd, timeoutMs, maxBytes, stdoutPath, stderrPat
|
|
|
379
414
|
windowsHide: true,
|
|
380
415
|
// POSIX: a process group lets a timeout kill grandchildren too.
|
|
381
416
|
detached: process.platform !== 'win32',
|
|
382
|
-
env:
|
|
417
|
+
env: testProcessEnv(),
|
|
383
418
|
})
|
|
384
419
|
} catch (error) {
|
|
385
420
|
spawnError = error
|
|
@@ -446,7 +481,7 @@ async function resolveDiagnosticsDir({ worktreePath, diagnosticsDir, missionId }
|
|
|
446
481
|
|
|
447
482
|
function normalizeDiscoveryInput(config) {
|
|
448
483
|
if (isPlainObject(config) && Object.prototype.hasOwnProperty.call(config, 'configured')) {
|
|
449
|
-
if (config.error) throw new
|
|
484
|
+
if (config.error) throw new TowerToolError('invalid-argument', `test discovery configuration is invalid: ${config.error}`)
|
|
450
485
|
if (config.configured === false) return { configured: false, config: null, error: null }
|
|
451
486
|
return { configured: true, config: config.config, error: null }
|
|
452
487
|
}
|
|
@@ -493,16 +528,16 @@ export async function runDiscovery(options = {}) {
|
|
|
493
528
|
} = options
|
|
494
529
|
const startedAt = new Date().toISOString()
|
|
495
530
|
|
|
496
|
-
if (!nonEmptyString(worktreePath)) throw new
|
|
497
|
-
if (!path.isAbsolute(worktreePath)) throw new
|
|
531
|
+
if (!nonEmptyString(worktreePath)) throw new TowerToolError('invalid-argument', 'worktreePath is required')
|
|
532
|
+
if (!path.isAbsolute(worktreePath)) throw new TowerToolError('invalid-argument', `worktreePath must be an absolute path: ${worktreePath}`)
|
|
498
533
|
const effectiveTimeout = normalizePositiveInteger(timeoutMs, 'timeoutMs')
|
|
499
534
|
const effectiveMaxBytes = normalizePositiveInteger(maxBytes, 'maxBytes')
|
|
500
535
|
if (runner !== undefined && runner !== null && typeof runner !== 'function') {
|
|
501
|
-
throw new
|
|
536
|
+
throw new TowerToolError('invalid-argument', 'runner must be a function')
|
|
502
537
|
}
|
|
503
538
|
if (diagnosticsDir !== undefined && diagnosticsDir !== null) {
|
|
504
539
|
if (!nonEmptyString(diagnosticsDir) || !path.isAbsolute(diagnosticsDir)) {
|
|
505
|
-
throw new
|
|
540
|
+
throw new TowerToolError('invalid-argument', `diagnosticsDir must be an absolute path: ${diagnosticsDir}`)
|
|
506
541
|
}
|
|
507
542
|
}
|
|
508
543
|
|
|
@@ -524,7 +559,7 @@ export async function runDiscovery(options = {}) {
|
|
|
524
559
|
|
|
525
560
|
if (!discovery.configured) return finishUnconfigured(discovery.error)
|
|
526
561
|
const normalized = normalizeDiscovery(discovery.config)
|
|
527
|
-
if (!normalized.ok) throw new
|
|
562
|
+
if (!normalized.ok) throw new TowerToolError('invalid-argument', `invalid tower.testDiscovery config: ${normalized.error}`)
|
|
528
563
|
|
|
529
564
|
const dir = await resolveDiagnosticsDir({ worktreePath, diagnosticsDir, missionId })
|
|
530
565
|
const commands = []
|