dsh-tower 2.0.4 → 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.
Files changed (171) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/COMPATIBILITY.md +38 -0
  3. package/CONTRIBUTING.md +150 -2
  4. package/README.en.md +40 -3
  5. package/README.md +38 -3
  6. package/SECURITY.md +27 -0
  7. package/adapters/default.js +10 -0
  8. package/adapters/store-fs.js +6 -0
  9. package/adapters/vcs-git.js +4 -0
  10. package/app/batch.js +79 -0
  11. package/app/board.js +25 -0
  12. package/app/checkCompletion.js +76 -0
  13. package/app/enrichedMissions.js +26 -0
  14. package/app/findings.js +78 -0
  15. package/app/gate.js +108 -0
  16. package/app/init.js +249 -0
  17. package/app/merge.js +567 -0
  18. package/app/messages.js +66 -0
  19. package/app/mission.js +35 -0
  20. package/app/missionContext.js +14 -0
  21. package/app/plan.js +42 -0
  22. package/app/provision.js +193 -0
  23. package/app/read.js +57 -0
  24. package/app/rebase.js +302 -0
  25. package/app/recording.js +72 -0
  26. package/app/review.js +145 -0
  27. package/app/roster.js +123 -0
  28. package/app/spawn.js +125 -0
  29. package/app/teardown.js +184 -0
  30. package/app/tower.js +524 -0
  31. package/cordis.patch.yml +61 -53
  32. package/docs/README.md +3 -1
  33. package/docs/architecture/overview.md +111 -12
  34. package/docs/board.md +72 -0
  35. package/docs/guide/hot-reload.md +1 -1
  36. package/docs/guide/install-upgrade-migration.md +8 -1
  37. package/docs/guide/release.md +125 -0
  38. package/docs/operations/security.md +47 -2
  39. package/docs/reference/config.generated.md +66 -0
  40. package/docs/reference/config.md +4 -1
  41. package/docs/reference/errors.generated.md +39 -0
  42. package/docs/reference/errors.md +4 -2
  43. package/docs/reference/tools.generated.md +33 -0
  44. package/docs/reference/tools.md +1 -1
  45. package/docs/zh/README.md +3 -2
  46. package/docs/zh/guide/hot-reload.md +1 -1
  47. package/docs/zh/guide/install-upgrade-migration.md +6 -1
  48. package/docs/zh/operations/security.md +30 -1
  49. package/docs/zh/reference/config.md +4 -1
  50. package/docs/zh/reference/errors.md +3 -1
  51. package/docs/zh/reference/tools.md +1 -1
  52. package/domain/collab/batch.js +112 -0
  53. package/domain/collab/messages.js +127 -0
  54. package/domain/collab/roster.js +158 -0
  55. package/domain/collab/spawn.js +188 -0
  56. package/domain/config/schema.js +404 -0
  57. package/domain/contracts/outputs.js +279 -0
  58. package/domain/contracts/tool-inputs/collaboration.js +58 -0
  59. package/domain/contracts/tool-inputs/findings.js +29 -0
  60. package/domain/contracts/tool-inputs/index.js +94 -0
  61. package/domain/contracts/tool-inputs/integration.js +37 -0
  62. package/domain/contracts/tool-inputs/missions.js +94 -0
  63. package/domain/contracts/tool-inputs/ops.js +75 -0
  64. package/domain/contracts/tool-inputs/shared.js +25 -0
  65. package/domain/errors.js +96 -0
  66. package/domain/gate.js +125 -0
  67. package/domain/integration/gate.js +176 -0
  68. package/domain/integration/init.js +225 -0
  69. package/domain/integration/liveness.js +31 -0
  70. package/domain/integration/merge.js +377 -0
  71. package/domain/integration/provision.js +124 -0
  72. package/domain/integration/rebase.js +236 -0
  73. package/domain/integration/review.js +52 -0
  74. package/domain/integration/teardown.js +118 -0
  75. package/domain/mission.js +68 -0
  76. package/domain/missions/completion.js +114 -0
  77. package/domain/missions/findings.js +201 -0
  78. package/domain/missions/lifecycle.js +185 -0
  79. package/domain/missions/plan.js +149 -0
  80. package/domain/missions/review.js +179 -0
  81. package/domain/planning.js +118 -0
  82. package/domain/read/enrichedMissions.js +58 -0
  83. package/domain/read/missionContext.js +53 -0
  84. package/domain/read/views.js +128 -0
  85. package/domain/report.js +59 -0
  86. package/domain/roster.js +40 -0
  87. package/domain/scope.js +105 -0
  88. package/domain/state/schema.js +233 -0
  89. package/domain/state/semantics.js +117 -0
  90. package/domain/state.js +33 -0
  91. package/domain/summary.js +114 -0
  92. package/domain/vocabulary.js +179 -0
  93. package/index.js +63 -20
  94. package/lib/briefing.js +176 -0
  95. package/lib/compatibility.js +108 -0
  96. package/lib/config.js +33 -375
  97. package/lib/doctor.js +97 -4
  98. package/lib/git.js +60 -16
  99. package/lib/hmr.js +4 -3
  100. package/lib/leases.js +3 -2
  101. package/lib/legacy.js +20 -9
  102. package/lib/modeState.js +39 -0
  103. package/lib/modeStore.js +11 -43
  104. package/lib/persistence.js +349 -0
  105. package/lib/presets.js +35 -0
  106. package/lib/spawn-prechecks.js +168 -0
  107. package/lib/spawn-support.js +79 -0
  108. package/lib/store.js +54 -137
  109. package/lib/tests.js +11 -10
  110. package/lib/tool-registry.js +544 -0
  111. package/lib/workspaceLock.js +2 -2
  112. package/lib/worktreeOwnership.js +5 -4
  113. package/locale/en.json +23 -1
  114. package/locale/zh.json +22 -0
  115. package/package.json +32 -4
  116. package/ports/clock.js +6 -0
  117. package/ports/ids.js +7 -0
  118. package/ports/index.js +2 -0
  119. package/ports/store.js +11 -0
  120. package/ports/vcs.js +11 -0
  121. package/scripts/architecture.mjs +94 -0
  122. package/scripts/check-docs.mjs +9 -3
  123. package/scripts/check-pack.mjs +49 -0
  124. package/scripts/domain-metrics.mjs +48 -0
  125. package/scripts/generate-config-reference.mjs +46 -0
  126. package/scripts/generate-error-reference.mjs +29 -0
  127. package/scripts/generate-tools-reference.mjs +56 -0
  128. package/scripts/issue-audit.mjs +259 -0
  129. package/scripts/lib/cli.mjs +76 -0
  130. package/scripts/lib/tmp.mjs +20 -0
  131. package/scripts/mount-smoke/fixtures.mjs +162 -0
  132. package/scripts/mount-smoke/host.json +4 -0
  133. package/scripts/mount-smoke/probe.mjs +125 -0
  134. package/scripts/mount-smoke/surface.mjs +141 -0
  135. package/scripts/mount-smoke.mjs +270 -0
  136. package/scripts/pack-manifest.txt +203 -0
  137. package/scripts/release-check.mjs +303 -0
  138. package/tools/adopt.js +72 -0
  139. package/tools/batch.js +78 -0
  140. package/tools/complete.js +53 -0
  141. package/tools/diff.js +39 -0
  142. package/tools/doctor.js +50 -0
  143. package/tools/finding.js +40 -0
  144. package/tools/findings.js +39 -0
  145. package/tools/import-legacy.js +42 -0
  146. package/tools/inbox.js +39 -0
  147. package/tools/index.js +39 -0
  148. package/tools/init.js +63 -0
  149. package/tools/interrupt.js +42 -0
  150. package/tools/legacy-map.js +44 -0
  151. package/tools/merge.js +39 -0
  152. package/tools/mission.js +41 -0
  153. package/tools/plan.js +39 -0
  154. package/tools/preflight.js +44 -0
  155. package/tools/rebase.js +39 -0
  156. package/tools/recover.js +45 -0
  157. package/tools/reload.js +52 -0
  158. package/tools/resource.js +54 -0
  159. package/tools/review.js +45 -0
  160. package/tools/roster.js +60 -0
  161. package/tools/send.js +147 -0
  162. package/tools/spawn.js +196 -0
  163. package/tools/status.js +94 -0
  164. package/tools/teardown.js +42 -0
  165. package/tools/test.js +61 -0
  166. package/types/cordis.d.ts +6 -0
  167. package/types/index.d.ts +339 -0
  168. package/lib/errors.js +0 -123
  169. package/lib/tools.js +0 -2460
  170. package/lib/tower.js +0 -2912
  171. /package/{lib → domain}/batch.js +0 -0
@@ -0,0 +1,339 @@
1
+ // Public type contract for the Tower bundle. This is a declaration-only file:
2
+ // the package remains zero-build and no .js is emitted.
3
+ //
4
+ // This is the single vocabulary shared by the published API and the pure
5
+ // domain modules under domain/, which are machine-checked against it by
6
+ // `tsc --noEmit` (checkJs). Keep this file free of value imports so consumers
7
+ // can load it without `allowJs`.
8
+
9
+ export type MissionStatus = 'planned' | 'active' | 'completed' | 'blocked' | 'paused' | 'merged' | 'abandoned'
10
+ export type MissionKind = 'build' | 'survey'
11
+ export type ReviewStatus = 'clean' | 'p1-1items' | 'p1-2items' | 'p1-3items' | 'p1-4items' | 'p1-5items' | 'p2-1items' | 'p2-2items' | 'p2-3items' | 'p2-4items' | 'p2-5items'
12
+ export type MergeVerdict = 'merge' | 'fix-then-merge' | 'hold'
13
+ export type ReviewPolicy = 'required-clean' | 'none'
14
+ export type FindingType = 'bug' | 'improve' | 'vuln' | 'idea'
15
+ export type FindingSeverity = 'low' | 'medium' | 'high' | 'critical'
16
+ export type FindingStatus = 'open' | 'triaged' | 'fixed' | 'wontfix'
17
+ export type Role = 'worker' | 'reviewer'
18
+
19
+ export type ErrorCode =
20
+ | 'invalid-argument'
21
+ | 'repo-invalid'
22
+ | 'not-initialized'
23
+ | 'mission-not-found'
24
+ | 'finding-not-found'
25
+ | 'invalid-transition'
26
+ | 'state-locked'
27
+ | 'state-corrupt'
28
+ | 'config-invalid'
29
+ | 'config-unknown-key'
30
+ | 'git-timeout'
31
+ | 'merge-conflict'
32
+ | 'merge-hook-failed'
33
+ | 'merge-intent-pending'
34
+ | 'base-dirty'
35
+ | 'stale-base'
36
+ | 'tip-moved'
37
+ | 'out-of-scope'
38
+ | 'worker-busy'
39
+ | 'worker-stale'
40
+ | 'spawn-unavailable'
41
+ | 'provider-missing'
42
+ | 'provider-cooldown'
43
+ | 'preset-missing'
44
+ | 'reviewer-prerequisite'
45
+ | 'worktree-dirty'
46
+ | 'workspace-busy'
47
+ | 'worker-limit'
48
+ | 'rate-limited'
49
+ | 'takeover-refused'
50
+ | 'reload-unavailable'
51
+ | 'reload-failed'
52
+ | 'tests-failed'
53
+ | 'tests-timeout'
54
+ | 'lease-busy'
55
+ | 'lease-token'
56
+ | 'batch-invalid'
57
+ | 'legacy-invalid'
58
+ | 'tool-internal'
59
+
60
+ /** One machine-readable gate/completion blocker: a stable code plus a detail. */
61
+ export interface Blocker {
62
+ code: string
63
+ detail: string
64
+ }
65
+
66
+ /** Snapshot inputs for the merge/preflight review, roster and test gates. */
67
+ export interface MergeGateOptions {
68
+ mission: Mission
69
+ roster?: RosterEntry[]
70
+ branch?: string | null
71
+ branchTip?: string | null
72
+ review?: Review | null
73
+ config?: TowerConfig
74
+ activity?: Record<string, string | undefined>
75
+ parentStatus?: Record<string, string | null> | null
76
+ inactiveStates?: Iterable<string>
77
+ rebaseWaived?: boolean
78
+ }
79
+
80
+ export interface MissionTask {
81
+ text: string
82
+ status: 'open' | 'done' | 'dropped'
83
+ note?: string
84
+ }
85
+
86
+ export interface MissionWorktree {
87
+ path: string
88
+ branch: string
89
+ baseCommit: string
90
+ }
91
+
92
+ export interface CompletionCheck {
93
+ name?: string
94
+ command?: string
95
+ exitCode?: number
96
+ evidence?: string
97
+ [key: string]: unknown
98
+ }
99
+
100
+ export interface CompletionReport {
101
+ report?: string
102
+ by?: string
103
+ at?: number
104
+ checks?: CompletionCheck[]
105
+ }
106
+
107
+ /** One recorded test-discovery receipt (lib/tests.js). */
108
+ export interface TestRun {
109
+ version?: number
110
+ ok?: boolean
111
+ treeTip?: string | null
112
+ treeTipAfter?: string | null
113
+ [key: string]: unknown
114
+ }
115
+
116
+ /** Record of the last successful rebase of a mission branch. */
117
+ export interface RebaseRecord {
118
+ at: number
119
+ oldTip: string
120
+ newTip?: string
121
+ baseTip?: string
122
+ }
123
+
124
+ export interface Mission {
125
+ id: string
126
+ title: string
127
+ status: MissionStatus
128
+ kind: MissionKind
129
+ scope: string[]
130
+ deps: string[]
131
+ /**
132
+ * Optional because the pure domain helpers tolerate partial/legacy mission
133
+ * snapshots (`mission.tasks ?? []`); a mission loaded through validateState
134
+ * always has an array.
135
+ */
136
+ tasks?: MissionTask[]
137
+ blockers?: string[]
138
+ reviewPolicy?: ReviewPolicy
139
+ reviewExemptionReason?: string | null
140
+ worktree?: MissionWorktree | null
141
+ owner?: string | null
142
+ notes?: string[]
143
+ completionReport?: CompletionReport | null
144
+ testRuns?: TestRun[]
145
+ rebase?: RebaseRecord | null
146
+ mergedCommit?: string | null
147
+ mergedAt?: number | null
148
+ scopeGeneration?: number
149
+ ownerGeneration?: number
150
+ pauseCause?: string | null
151
+ updatedAt?: number
152
+ references?: string[]
153
+ authorizationRef?: string | null
154
+ }
155
+
156
+ export interface Review {
157
+ id?: string
158
+ seq?: number
159
+ missionId: string
160
+ branch: string
161
+ round: number
162
+ status: string
163
+ mergeVerdict: MergeVerdict
164
+ reviewedCommit: string
165
+ at: number
166
+ findings?: string
167
+ checks?: string[]
168
+ decision?: string
169
+ reviewer?: string
170
+ reviewerRole?: string
171
+ breakGlassReason?: string
172
+ }
173
+
174
+ export interface Finding {
175
+ id: string
176
+ type: FindingType
177
+ title: string
178
+ seq?: number | null
179
+ severity?: FindingSeverity | null
180
+ status?: FindingStatus | null
181
+ missionId?: string | null
182
+ branch?: string | null
183
+ reportedBy?: string | null
184
+ at?: number | null
185
+ updatedAt?: number | null
186
+ closedAt?: number | null
187
+ summary?: string
188
+ details?: string
189
+ suggestedFix?: string
190
+ location?: string
191
+ }
192
+
193
+ export interface RosterEntry {
194
+ name: string
195
+ role: Role
196
+ state: string
197
+ /** Reserved rows may exist before an agent id is known. */
198
+ agentId?: string | null
199
+ missionId?: string | null
200
+ parentSessionId?: string | null
201
+ spawnTip?: string | null
202
+ stale?: boolean
203
+ [key: string]: unknown
204
+ }
205
+
206
+ /**
207
+ * Public state contract. This stays hand-written rather than derived from
208
+ * `Static<typeof StateSchema>` because the validator schema is deliberately
209
+ * tolerant of legacy rows (`additionalProperties`, optional collections) and
210
+ * checks status vocabularies/worktree records semantically after the shape
211
+ * pass. `tests/type-contracts.ts` pins the schema/public-type overlap.
212
+ */
213
+ export interface TowerState {
214
+ schemaVersion: number
215
+ revision: number
216
+ base: string
217
+ missions: Mission[]
218
+ repoRoot?: string
219
+ commonDir?: string
220
+ createdAt?: number
221
+ updatedAt?: number
222
+ dirtyBase?: boolean
223
+ towerAgentId?: string | null
224
+ snapshotCommit?: string | null
225
+ snapshotRef?: string | null
226
+ roster?: RosterEntry[]
227
+ reviews?: Review[]
228
+ findings?: Finding[]
229
+ messages?: Array<Record<string, unknown>>
230
+ batches?: Array<Record<string, unknown>>
231
+ legacySnapshots?: Array<Record<string, unknown>>
232
+ legacyMaps?: Array<Record<string, unknown>>
233
+ deliveries?: Array<Record<string, unknown>>
234
+ mergeIntent?: Record<string, unknown> | null
235
+ rebaseIntent?: Record<string, unknown> | null
236
+ messageSeq?: number
237
+ reviewSeq?: number
238
+ findingSeq?: number
239
+ batchSeq?: number
240
+ deliverySeq?: number
241
+ }
242
+
243
+ export type TowerConfig = import('@deepseek-ai/schemastery').TypeS<typeof import('../domain/config/schema.js').Config>
244
+
245
+ /** Counts rendered by mission summaries and the board. */
246
+ export interface TaskCounts {
247
+ total: number
248
+ done: number
249
+ open: number
250
+ dropped: number
251
+ }
252
+
253
+ /** Compact mission view returned by tower_plan/tower_status. */
254
+ export interface MissionSummary {
255
+ id: string
256
+ title: string
257
+ kind: MissionKind
258
+ status: MissionStatus
259
+ scope: string[]
260
+ deps: string[]
261
+ owner: string | null
262
+ worktree: MissionWorktree | null
263
+ blockers: string[]
264
+ tasks: TaskCounts
265
+ scopeGeneration: number
266
+ ownerGeneration: number
267
+ pauseCause: string | null
268
+ reviewPolicy: ReviewPolicy
269
+ mergedCommit: string | null
270
+ mergedAt: number | null
271
+ updatedAt?: number
272
+ }
273
+
274
+ /** Full mission view returned by mission-scoped use cases. */
275
+ export interface MissionDetail extends Omit<MissionSummary, 'tasks'> {
276
+ tasks: MissionTask[]
277
+ notes: string[]
278
+ completionReport: CompletionReport | null
279
+ authorizationRef: string | null
280
+ references: string[]
281
+ reviewExemptionReason: string
282
+ testRuns: TestRun[]
283
+ }
284
+
285
+ export interface FindingSummary {
286
+ id: string
287
+ seq: number | null
288
+ type: FindingType
289
+ title: string
290
+ severity: FindingSeverity | null
291
+ status: FindingStatus
292
+ missionId: string | null
293
+ branch: string | null
294
+ reportedBy: string | null
295
+ at: number | null
296
+ updatedAt: number | null
297
+ closedAt: number | null
298
+ }
299
+
300
+ export interface ToolDefinition {
301
+ name: string
302
+ description: string
303
+ parameters: { type: 'object'; properties: Record<string, unknown>; required?: string[] }
304
+ execute(args: Record<string, unknown>, exec: { agent?: { id?: string } }): Promise<unknown>
305
+ }
306
+
307
+ /** Owned Tower mode state exposed as the `tower` Cordis service. */
308
+ export interface ModeStateService {
309
+ has(key: string): boolean
310
+ get(key: string): unknown
311
+ set(key: string, value: unknown): this
312
+ delete(key: string): boolean
313
+ values(): IterableIterator<unknown>
314
+ entries(): IterableIterator<[string, unknown]>
315
+ replace(entries?: Iterable<[string, unknown]>): void
316
+ clear(): void
317
+ }
318
+
319
+ declare module '@deepseek-ai/cordis' {
320
+ interface Context {
321
+ tower?: ModeStateService
322
+ }
323
+ }
324
+
325
+ import type { Static, TSchema } from '@sinclair/typebox'
326
+
327
+ export type DefineTool = <S extends TSchema>(
328
+ ctx: any,
329
+ definition: {
330
+ name: string
331
+ description: string
332
+ parameters: S
333
+ execute(args: Static<S>, exec: any): Promise<unknown>
334
+ aliases?: any
335
+ requiresMissionId?: any
336
+ board?: any
337
+ output?: any
338
+ },
339
+ ) => void
package/lib/errors.js DELETED
@@ -1,123 +0,0 @@
1
- // Stable tool error codes.
2
- //
3
- // Everything a Tower tool throws reaches the model as
4
- // [tower:<code>] <original message>
5
- // so callers (and tests) can branch on the code instead of matching prose.
6
- // The catalog is documented in docs/reference/errors.md; scripts/check-docs.mjs
7
- // keeps the two in sync.
8
- import { GitError } from './git.js'
9
-
10
- export const ERROR_CODES = {
11
- 'invalid-argument': 'A tool argument is missing, malformed, or not allowed.',
12
- 'repo-invalid': 'repoRoot is missing, relative, or not inside a Git repository.',
13
- 'not-initialized': 'Tower is not initialized for this repository; run tower_init.',
14
- 'mission-not-found': 'No mission with that id exists.',
15
- 'finding-not-found': 'No finding with that id exists.',
16
- 'invalid-transition': 'The requested state transition is not allowed.',
17
- 'state-locked': 'Another Tower writer holds the state lock.',
18
- 'state-corrupt': 'The Tower state file is unreadable or invalid.',
19
- 'config-invalid': 'Tower configuration is invalid.',
20
- 'config-unknown-key': 'Tower configuration has an unknown top-level key.',
21
- 'git-failed': 'A git command failed.',
22
- 'git-timeout': 'A git command timed out or exceeded its output buffer.',
23
- 'worktree-dirty': 'The mission worktree has uncommitted changes and cannot complete.',
24
- 'spawn-unavailable': 'The subagents service is unavailable.',
25
- 'provider-missing': 'No subagent provider is registered.',
26
- 'preset-missing': 'The configured agent preset is not registered.',
27
- 'reviewer-prerequisite': 'The reviewer spawn prerequisite is not met.',
28
- 'not-authorized': 'The calling agent role may not perform this action.',
29
- 'workspace-busy': 'Another Tower session owns this repository workspace lock.',
30
- 'worker-limit': 'The repository reached spawn.maxLiveWorkers live workers.',
31
- 'rate-limited': 'The tower_send per-minute rate limit was reached.',
32
- 'takeover-refused': 'The mission already has a live worker that was not stopped first.',
33
- 'reload-unavailable': 'The DSH HMR service is not available in this profile.',
34
- 'reload-failed': 'The HMR reload failed; check the DSH log and restart if needed.',
35
- 'tests-failed': 'Declared test discovery failed or did not meet its admission requirements.',
36
- 'tests-timeout': 'Test discovery exceeded its time or output budget.',
37
- 'lease-busy': 'Another holder owns the requested advisory lease.',
38
- 'lease-token': 'The lease release token does not match the current holder.',
39
- 'batch-invalid': 'The batch request or its close evidence is invalid.',
40
- 'legacy-invalid': 'The legacy snapshot or mapping is invalid.',
41
- 'tool-internal': 'An unexpected failure; read the message and the activity log.',
42
- }
43
-
44
- export class TowerToolError extends Error {
45
- constructor(code, message, options = {}) {
46
- super(`[tower:${code}] ${message}`, options.cause ? { cause: options.cause } : undefined)
47
- this.name = 'TowerToolError'
48
- this.code = code
49
- this.retryable = options.retryable === true
50
- this.nextAction = options.nextAction ?? null
51
- this.details = options.details ?? null
52
- }
53
- }
54
-
55
- // Ordered rules; the first match wins. Keep the specific patterns above the
56
- // generic argument fallback so `no open task matching` never becomes internal.
57
- const RULES = [
58
- ['not-initialized', /tower is not initialized/i],
59
- ['mission-not-found', /^unknown mission "/i],
60
- ['finding-not-found', /^unknown finding "/i],
61
- ['state-locked', /locked by another writer/i],
62
- [
63
- 'state-corrupt',
64
- /corrupt tower state|^tower state |^unsupported tower state|^invalid (?:mission|roster|message|finding|review) entry|^mission entry without id|^duplicate (?:mission|roster|message|finding|review)|^mission .* (?:has unknown status|has no tasks array|has no deps array)|^message .* has no readBy array|^finding .* has unknown status|^review .* has unknown (?:status|merge verdict)/i,
65
- ],
66
- ['config-unknown-key', /unknown key|unknown top-level key/i],
67
- ['config-invalid', /config (?:is invalid|must be an object)|repo config/i],
68
- ['lease-busy', /lease .* (?:held|owned)|another holder owns/i],
69
- ['lease-token', /lease .*token|token does not match/i],
70
- ['tests-timeout', /test discovery .*(?:timed out|exceeded|too large)/i],
71
- ['tests-failed', /test discovery (?:failed|did not)|tests? (?:failed|missing|did not pass)/i],
72
- ['batch-invalid', /batch (?:is invalid|close|member|receipt)/i],
73
- ['legacy-invalid', /legacy (?:snapshot|map|source)|unsupported legacy/i],
74
- ['spawn-unavailable', /subagents service is unavailable/i],
75
- ['provider-missing', /no subagent provider/i],
76
- ['preset-missing', /agent preset .*is not registered/i],
77
- ['reviewer-prerequisite', /reviewer needs an existing|reviewer requires a completed/i],
78
- ['repo-invalid', /repoRoot is required|repoRoot must be an absolute path/i],
79
- ['workspace-busy', /workspace .*lock|another tower session owns/i],
80
- ['worker-limit', /live worker limit|too many live workers|worker limit reached/i],
81
- ['rate-limited', /rate limit|too many tower messages/i],
82
- ['takeover-refused', /takeover refused|cannot take over/i],
83
- ['not-authorized', /only the tower|only an assigned reviewer|only the tower or a worker|does not own mission|not authorized|may not perform this action/i],
84
- [
85
- 'invalid-transition',
86
- /cannot move from|cannot be completed from that state|is already completed|merged is recorded only|can only be widened|only a completed mission|reviewers may list and read|still has \d+ open task|has unresolved blockers|has no worktree|has no commits since|cannot complete with \d+ unread inbox|not assigned to mission|is a [a-z]+, not a worker|cannot reanchor base|worktree is missing|worktree .* is not a Git worktree|belongs to a different repository|is on .* expected|has \d+ live worker|already has a live worker|cannot reopen a terminal mission|stopped worker cannot report completion|can only be spawned|violates? (?:the )?mission (?:state|transition)/i,
87
- ],
88
- [
89
- 'invalid-argument',
90
- /requires |is required|must be (?:a|an|one|an integer|non-empty|printable)|must not be|must be one of|no open task matching|unknown mission status|unknown finding severity|unknown finding type|not an array|is not an array|scope must|title must|checks must|role must|timeoutMs must|repoRoot is required|only a list filter|snapshotDirty was requested|refusing to send a message to yourself|mission has \d+ live worker|roster name already used|is already in the roster|depends on unknown mission/i,
91
- ],
92
- ]
93
-
94
- function originalMessage(error) {
95
- return String(error?.message ?? error)
96
- }
97
-
98
- /** Wrap any thrown value into a TowerToolError with a stable code. */
99
- export function classifyError(error) {
100
- if (error instanceof TowerToolError) return error
101
- if (error && typeof error.code === 'string' && ERROR_CODES[error.code]) {
102
- return new TowerToolError(error.code, originalMessage(error), {
103
- cause: error,
104
- retryable: error.retryable === true,
105
- nextAction: error.nextAction ?? null,
106
- details: error.details ?? null,
107
- })
108
- }
109
- if (error instanceof GitError || error?.name === 'GitError') {
110
- const message = originalMessage(error)
111
- if (/not a git repository/i.test(message)) return new TowerToolError('repo-invalid', message, { cause: error })
112
- const code = error?.timedOut === true || error?.bufferOverflow === true ? 'git-timeout' : 'git-failed'
113
- return new TowerToolError(code, message, { cause: error })
114
- }
115
- const message = originalMessage(error)
116
- const RETRYABLE = new Set(['state-locked', 'rate-limited', 'git-timeout', 'workspace-busy', 'merge-intent-pending'])
117
- for (const [code, pattern] of RULES) {
118
- if (pattern.test(message)) {
119
- return new TowerToolError(code, message, { cause: error, retryable: RETRYABLE.has(code) })
120
- }
121
- }
122
- return new TowerToolError('tool-internal', message, { cause: error })
123
- }