dsh-tower 2.0.4 → 2.1.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.
Files changed (184) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/COMPATIBILITY.md +39 -0
  3. package/CONTRIBUTING.md +157 -2
  4. package/README.en.md +46 -3
  5. package/README.md +44 -3
  6. package/SECURITY.md +27 -0
  7. package/adapters/default.js +12 -0
  8. package/adapters/store-fs.js +6 -0
  9. package/adapters/vcs-git.js +4 -0
  10. package/app/archive.js +190 -0
  11. package/app/batch.js +79 -0
  12. package/app/board.js +27 -0
  13. package/app/checkCompletion.js +89 -0
  14. package/app/enrichedMissions.js +26 -0
  15. package/app/findings.js +78 -0
  16. package/app/gate.js +126 -0
  17. package/app/init.js +291 -0
  18. package/app/merge.js +573 -0
  19. package/app/messages.js +66 -0
  20. package/app/mission.js +37 -0
  21. package/app/missionContext.js +14 -0
  22. package/app/plan.js +50 -0
  23. package/app/provision.js +193 -0
  24. package/app/read.js +113 -0
  25. package/app/rebase.js +302 -0
  26. package/app/recording.js +72 -0
  27. package/app/review.js +147 -0
  28. package/app/roster.js +123 -0
  29. package/app/spawn.js +125 -0
  30. package/app/teardown.js +205 -0
  31. package/app/tower.js +552 -0
  32. package/cordis.patch.yml +61 -53
  33. package/docs/README.md +3 -1
  34. package/docs/agents/domain.md +38 -0
  35. package/docs/agents/issue-tracker.md +46 -0
  36. package/docs/agents/triage-labels.md +15 -0
  37. package/docs/architecture/overview.md +114 -13
  38. package/docs/board.md +72 -0
  39. package/docs/guide/hot-reload.md +1 -1
  40. package/docs/guide/install-upgrade-migration.md +25 -1
  41. package/docs/guide/release.md +125 -0
  42. package/docs/operations/security.md +47 -2
  43. package/docs/reference/blockers.md +1 -1
  44. package/docs/reference/config.generated.md +70 -0
  45. package/docs/reference/config.md +8 -1
  46. package/docs/reference/doctor.md +58 -8
  47. package/docs/reference/errors.generated.md +41 -0
  48. package/docs/reference/errors.md +7 -3
  49. package/docs/reference/tools.generated.md +33 -0
  50. package/docs/reference/tools.md +7 -6
  51. package/docs/zh/README.md +3 -2
  52. package/docs/zh/guide/hot-reload.md +1 -1
  53. package/docs/zh/guide/install-upgrade-migration.md +18 -1
  54. package/docs/zh/operations/security.md +30 -1
  55. package/docs/zh/reference/blockers.md +1 -1
  56. package/docs/zh/reference/config.md +8 -1
  57. package/docs/zh/reference/doctor.md +35 -2
  58. package/docs/zh/reference/errors.md +6 -2
  59. package/docs/zh/reference/tools.md +3 -3
  60. package/domain/collab/batch.js +112 -0
  61. package/domain/collab/messages.js +127 -0
  62. package/domain/collab/roster.js +158 -0
  63. package/domain/collab/spawn.js +188 -0
  64. package/domain/config/schema.js +427 -0
  65. package/domain/contracts/outputs.js +305 -0
  66. package/domain/contracts/tool-inputs/collaboration.js +58 -0
  67. package/domain/contracts/tool-inputs/findings.js +29 -0
  68. package/domain/contracts/tool-inputs/index.js +94 -0
  69. package/domain/contracts/tool-inputs/integration.js +37 -0
  70. package/domain/contracts/tool-inputs/missions.js +95 -0
  71. package/domain/contracts/tool-inputs/ops.js +75 -0
  72. package/domain/contracts/tool-inputs/shared.js +25 -0
  73. package/domain/errors.js +98 -0
  74. package/domain/gate.js +125 -0
  75. package/domain/integration/gate.js +190 -0
  76. package/domain/integration/init.js +261 -0
  77. package/domain/integration/liveness.js +31 -0
  78. package/domain/integration/merge.js +377 -0
  79. package/domain/integration/provision.js +124 -0
  80. package/domain/integration/rebase.js +236 -0
  81. package/domain/integration/review.js +52 -0
  82. package/domain/integration/teardown.js +118 -0
  83. package/domain/mission.js +68 -0
  84. package/domain/missions/completion.js +167 -0
  85. package/domain/missions/findings.js +201 -0
  86. package/domain/missions/lifecycle.js +186 -0
  87. package/domain/missions/plan.js +149 -0
  88. package/domain/missions/review.js +179 -0
  89. package/domain/planning.js +121 -0
  90. package/domain/read/enrichedMissions.js +58 -0
  91. package/domain/read/missionContext.js +53 -0
  92. package/domain/read/views.js +141 -0
  93. package/domain/report.js +67 -0
  94. package/domain/roster.js +40 -0
  95. package/domain/scope.js +177 -0
  96. package/domain/state/archive.js +257 -0
  97. package/domain/state/migrations.js +145 -0
  98. package/domain/state/schema.js +239 -0
  99. package/domain/state/semantics.js +117 -0
  100. package/domain/state.js +183 -0
  101. package/domain/summary.js +116 -0
  102. package/domain/vocabulary.js +179 -0
  103. package/index.js +63 -20
  104. package/lib/archive.js +279 -0
  105. package/lib/briefing.js +176 -0
  106. package/lib/caseSensitivity.js +66 -0
  107. package/lib/compatibility.js +108 -0
  108. package/lib/config.js +33 -375
  109. package/lib/doctor.js +275 -4
  110. package/lib/git.js +60 -16
  111. package/lib/hmr.js +4 -3
  112. package/lib/leases.js +17 -7
  113. package/lib/legacy.js +39 -14
  114. package/lib/modeState.js +39 -0
  115. package/lib/modeStore.js +11 -43
  116. package/lib/persistence.js +356 -0
  117. package/lib/presets.js +35 -0
  118. package/lib/spawn-prechecks.js +168 -0
  119. package/lib/spawn-support.js +79 -0
  120. package/lib/stateFile.js +99 -0
  121. package/lib/store.js +67 -135
  122. package/lib/tests.js +17 -12
  123. package/lib/tool-registry.js +544 -0
  124. package/lib/workspaceLock.js +2 -2
  125. package/lib/worktreeOwnership.js +22 -10
  126. package/locale/en.json +23 -1
  127. package/locale/zh.json +22 -0
  128. package/package.json +32 -4
  129. package/ports/clock.js +6 -0
  130. package/ports/ids.js +7 -0
  131. package/ports/index.js +2 -0
  132. package/ports/store.js +11 -0
  133. package/ports/vcs.js +11 -0
  134. package/scripts/architecture.mjs +94 -0
  135. package/scripts/check-docs.mjs +9 -3
  136. package/scripts/check-pack.mjs +49 -0
  137. package/scripts/domain-metrics.mjs +48 -0
  138. package/scripts/generate-config-reference.mjs +46 -0
  139. package/scripts/generate-error-reference.mjs +29 -0
  140. package/scripts/generate-tools-reference.mjs +56 -0
  141. package/scripts/issue-audit.mjs +259 -0
  142. package/scripts/lib/cli.mjs +76 -0
  143. package/scripts/lib/tmp.mjs +20 -0
  144. package/scripts/mount-smoke/fixtures.mjs +162 -0
  145. package/scripts/mount-smoke/host.json +4 -0
  146. package/scripts/mount-smoke/probe.mjs +125 -0
  147. package/scripts/mount-smoke/surface.mjs +141 -0
  148. package/scripts/mount-smoke.mjs +270 -0
  149. package/scripts/pack-manifest.txt +212 -0
  150. package/scripts/release-check.mjs +303 -0
  151. package/tools/adopt.js +72 -0
  152. package/tools/batch.js +78 -0
  153. package/tools/complete.js +53 -0
  154. package/tools/diff.js +39 -0
  155. package/tools/doctor.js +50 -0
  156. package/tools/finding.js +40 -0
  157. package/tools/findings.js +39 -0
  158. package/tools/import-legacy.js +42 -0
  159. package/tools/inbox.js +39 -0
  160. package/tools/index.js +39 -0
  161. package/tools/init.js +63 -0
  162. package/tools/interrupt.js +42 -0
  163. package/tools/legacy-map.js +44 -0
  164. package/tools/merge.js +39 -0
  165. package/tools/mission.js +41 -0
  166. package/tools/plan.js +39 -0
  167. package/tools/preflight.js +45 -0
  168. package/tools/rebase.js +39 -0
  169. package/tools/recover.js +45 -0
  170. package/tools/reload.js +52 -0
  171. package/tools/resource.js +54 -0
  172. package/tools/review.js +45 -0
  173. package/tools/roster.js +60 -0
  174. package/tools/send.js +147 -0
  175. package/tools/spawn.js +196 -0
  176. package/tools/status.js +102 -0
  177. package/tools/teardown.js +42 -0
  178. package/tools/test.js +61 -0
  179. package/types/cordis.d.ts +6 -0
  180. package/types/index.d.ts +373 -0
  181. package/lib/errors.js +0 -123
  182. package/lib/tools.js +0 -2460
  183. package/lib/tower.js +0 -2912
  184. /package/{lib → domain}/batch.js +0 -0
@@ -0,0 +1,373 @@
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
+ /** UTF-8 byte length of `report`; absent on documents written before #89. */
103
+ reportBytes?: number
104
+ by?: string
105
+ at?: number
106
+ checks?: CompletionCheck[]
107
+ }
108
+
109
+ /** One recorded test-discovery receipt (lib/tests.js). */
110
+ export interface TestRun {
111
+ version?: number
112
+ ok?: boolean
113
+ treeTip?: string | null
114
+ treeTipAfter?: string | null
115
+ [key: string]: unknown
116
+ }
117
+
118
+ /** Record of the last successful rebase of a mission branch. */
119
+ export interface RebaseRecord {
120
+ at: number
121
+ oldTip: string
122
+ newTip?: string
123
+ baseTip?: string
124
+ }
125
+
126
+ export interface Mission {
127
+ id: string
128
+ title: string
129
+ status: MissionStatus
130
+ kind: MissionKind
131
+ scope: string[]
132
+ deps: string[]
133
+ /**
134
+ * Optional because the pure domain helpers tolerate partial/legacy mission
135
+ * snapshots (`mission.tasks ?? []`); a mission loaded through validateState
136
+ * always has an array.
137
+ */
138
+ tasks?: MissionTask[]
139
+ blockers?: string[]
140
+ reviewPolicy?: ReviewPolicy
141
+ reviewExemptionReason?: string | null
142
+ worktree?: MissionWorktree | null
143
+ owner?: string | null
144
+ notes?: string[]
145
+ completionReport?: CompletionReport | null
146
+ testRuns?: TestRun[]
147
+ rebase?: RebaseRecord | null
148
+ mergedCommit?: string | null
149
+ mergedAt?: number | null
150
+ scopeGeneration?: number
151
+ ownerGeneration?: number
152
+ pauseCause?: string | null
153
+ updatedAt?: number
154
+ references?: string[]
155
+ authorizationRef?: string | null
156
+ }
157
+
158
+ /**
159
+ * Compact archived-mission entry (`archive/index.json`). It keeps enough of the
160
+ * mission for ordering, dependency lookups and id allocation without loading the
161
+ * full year file, and it is what `tower_status includeArchived` pages from (#73).
162
+ */
163
+ export interface ArchiveIndexEntry {
164
+ id: string
165
+ title: string
166
+ status: MissionStatus
167
+ kind?: MissionKind
168
+ scope?: string[]
169
+ deps?: string[]
170
+ owner?: string | null
171
+ branch?: string | null
172
+ mergedCommit?: string | null
173
+ mergedAt?: number | null
174
+ updatedAt?: number | null
175
+ archivedAt: number
176
+ archivedYear: number
177
+ }
178
+
179
+ /** Full archived record (`archive/<year>.json`): mission + linked history. */
180
+ export interface ArchivedMissionRecord extends ArchiveIndexEntry {
181
+ mission: Mission
182
+ reviews: Review[]
183
+ findings: Finding[]
184
+ }
185
+
186
+ export interface Review {
187
+ id?: string
188
+ seq?: number
189
+ missionId: string
190
+ branch: string
191
+ round: number
192
+ status: string
193
+ mergeVerdict: MergeVerdict
194
+ reviewedCommit: string
195
+ at: number
196
+ findings?: string
197
+ checks?: string[]
198
+ decision?: string
199
+ reviewer?: string
200
+ reviewerRole?: string
201
+ breakGlassReason?: string
202
+ }
203
+
204
+ export interface Finding {
205
+ id: string
206
+ type: FindingType
207
+ title: string
208
+ seq?: number | null
209
+ severity?: FindingSeverity | null
210
+ status?: FindingStatus | null
211
+ missionId?: string | null
212
+ branch?: string | null
213
+ reportedBy?: string | null
214
+ at?: number | null
215
+ updatedAt?: number | null
216
+ closedAt?: number | null
217
+ summary?: string
218
+ details?: string
219
+ suggestedFix?: string
220
+ location?: string
221
+ }
222
+
223
+ export interface RosterEntry {
224
+ name: string
225
+ role: Role
226
+ state: string
227
+ /** Reserved rows may exist before an agent id is known. */
228
+ agentId?: string | null
229
+ missionId?: string | null
230
+ parentSessionId?: string | null
231
+ spawnTip?: string | null
232
+ stale?: boolean
233
+ [key: string]: unknown
234
+ }
235
+
236
+ /**
237
+ * Public state contract. This stays hand-written rather than derived from
238
+ * `Static<typeof StateSchema>` because the validator schema is deliberately
239
+ * tolerant of legacy rows (`additionalProperties`, optional collections) and
240
+ * checks status vocabularies/worktree records semantically after the shape
241
+ * pass. `tests/type-contracts.ts` pins the schema/public-type overlap.
242
+ */
243
+ export interface TowerState {
244
+ schemaVersion: number
245
+ revision: number
246
+ base: string
247
+ missions: Mission[]
248
+ repoRoot?: string
249
+ commonDir?: string
250
+ createdAt?: number
251
+ updatedAt?: number
252
+ dirtyBase?: boolean
253
+ /** Detected once at tower_init; null means "not detected yet" (#90). */
254
+ caseInsensitive?: boolean | null
255
+ towerAgentId?: string | null
256
+ snapshotCommit?: string | null
257
+ snapshotRef?: string | null
258
+ roster?: RosterEntry[]
259
+ reviews?: Review[]
260
+ findings?: Finding[]
261
+ messages?: Array<Record<string, unknown>>
262
+ batches?: Array<Record<string, unknown>>
263
+ legacySnapshots?: Array<Record<string, unknown>>
264
+ legacyMaps?: Array<Record<string, unknown>>
265
+ deliveries?: Array<Record<string, unknown>>
266
+ mergeIntent?: Record<string, unknown> | null
267
+ rebaseIntent?: Record<string, unknown> | null
268
+ messageSeq?: number
269
+ reviewSeq?: number
270
+ findingSeq?: number
271
+ batchSeq?: number
272
+ deliverySeq?: number
273
+ }
274
+
275
+ export type TowerConfig = import('@deepseek-ai/schemastery').TypeS<typeof import('../domain/config/schema.js').Config>
276
+
277
+ /** Counts rendered by mission summaries and the board. */
278
+ export interface TaskCounts {
279
+ total: number
280
+ done: number
281
+ open: number
282
+ dropped: number
283
+ }
284
+
285
+ /** Compact mission view returned by tower_plan/tower_status. */
286
+ export interface MissionSummary {
287
+ id: string
288
+ title: string
289
+ kind: MissionKind
290
+ status: MissionStatus
291
+ scope: string[]
292
+ deps: string[]
293
+ owner: string | null
294
+ worktree: MissionWorktree | null
295
+ blockers: string[]
296
+ tasks: TaskCounts
297
+ scopeGeneration: number
298
+ ownerGeneration: number
299
+ pauseCause: string | null
300
+ reviewPolicy: ReviewPolicy
301
+ mergedCommit: string | null
302
+ mergedAt: number | null
303
+ /** Size of the completion report without loading its body; null when absent. */
304
+ reportBytes: number | null
305
+ updatedAt?: number
306
+ }
307
+
308
+ /** Full mission view returned by mission-scoped use cases. */
309
+ export interface MissionDetail extends Omit<MissionSummary, 'tasks'> {
310
+ tasks: MissionTask[]
311
+ notes: string[]
312
+ completionReport: CompletionReport | null
313
+ authorizationRef: string | null
314
+ references: string[]
315
+ reviewExemptionReason: string
316
+ testRuns: TestRun[]
317
+ }
318
+
319
+ export interface FindingSummary {
320
+ id: string
321
+ seq: number | null
322
+ type: FindingType
323
+ title: string
324
+ severity: FindingSeverity | null
325
+ status: FindingStatus
326
+ missionId: string | null
327
+ branch: string | null
328
+ reportedBy: string | null
329
+ at: number | null
330
+ updatedAt: number | null
331
+ closedAt: number | null
332
+ }
333
+
334
+ export interface ToolDefinition {
335
+ name: string
336
+ description: string
337
+ parameters: { type: 'object'; properties: Record<string, unknown>; required?: string[] }
338
+ execute(args: Record<string, unknown>, exec: { agent?: { id?: string } }): Promise<unknown>
339
+ }
340
+
341
+ /** Owned Tower mode state exposed as the `tower` Cordis service. */
342
+ export interface ModeStateService {
343
+ has(key: string): boolean
344
+ get(key: string): unknown
345
+ set(key: string, value: unknown): this
346
+ delete(key: string): boolean
347
+ values(): IterableIterator<unknown>
348
+ entries(): IterableIterator<[string, unknown]>
349
+ replace(entries?: Iterable<[string, unknown]>): void
350
+ clear(): void
351
+ }
352
+
353
+ declare module '@deepseek-ai/cordis' {
354
+ interface Context {
355
+ tower?: ModeStateService
356
+ }
357
+ }
358
+
359
+ import type { Static, TSchema } from '@sinclair/typebox'
360
+
361
+ export type DefineTool = <S extends TSchema>(
362
+ ctx: any,
363
+ definition: {
364
+ name: string
365
+ description: string
366
+ parameters: S
367
+ execute(args: Static<S>, exec: any): Promise<unknown>
368
+ aliases?: any
369
+ requiresMissionId?: any
370
+ board?: any
371
+ output?: any
372
+ },
373
+ ) => 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
- }