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.
Files changed (193) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/COMPATIBILITY.md +38 -0
  3. package/CONTRIBUTING.md +150 -2
  4. package/README.en.md +59 -16
  5. package/README.md +57 -16
  6. package/SECURITY.md +31 -1
  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 +33 -1
  33. package/docs/architecture/overview.md +114 -14
  34. package/docs/board.md +72 -0
  35. package/docs/examples/first-mission.md +2 -1
  36. package/docs/faq.md +11 -7
  37. package/docs/guide/hot-reload.md +1 -1
  38. package/docs/guide/install-upgrade-migration.md +22 -6
  39. package/docs/guide/quickstart.md +12 -7
  40. package/docs/guide/release.md +125 -0
  41. package/docs/guide/scenarios-and-philosophy.md +86 -69
  42. package/docs/operations/security.md +65 -14
  43. package/docs/reference/blockers.md +11 -10
  44. package/docs/reference/config.generated.md +66 -0
  45. package/docs/reference/config.md +11 -7
  46. package/docs/reference/errors.generated.md +39 -0
  47. package/docs/reference/errors.md +10 -2
  48. package/docs/reference/events.md +7 -4
  49. package/docs/reference/testing.md +2 -1
  50. package/docs/reference/tools.generated.md +33 -0
  51. package/docs/reference/tools.md +8 -7
  52. package/docs/zh/README.md +4 -3
  53. package/docs/zh/faq.md +9 -6
  54. package/docs/zh/guide/hot-reload.md +1 -1
  55. package/docs/zh/guide/install-upgrade-migration.md +17 -5
  56. package/docs/zh/guide/quickstart.md +13 -8
  57. package/docs/zh/guide/scenarios-and-philosophy.md +55 -50
  58. package/docs/zh/operations/security.md +43 -7
  59. package/docs/zh/reference/blockers.md +10 -10
  60. package/docs/zh/reference/config.md +11 -7
  61. package/docs/zh/reference/errors.md +4 -1
  62. package/docs/zh/reference/events.md +6 -3
  63. package/docs/zh/reference/testing.md +3 -2
  64. package/docs/zh/reference/tools.md +8 -7
  65. package/domain/collab/batch.js +112 -0
  66. package/domain/collab/messages.js +127 -0
  67. package/domain/collab/roster.js +158 -0
  68. package/domain/collab/spawn.js +188 -0
  69. package/domain/config/schema.js +404 -0
  70. package/domain/contracts/outputs.js +279 -0
  71. package/domain/contracts/tool-inputs/collaboration.js +58 -0
  72. package/domain/contracts/tool-inputs/findings.js +29 -0
  73. package/domain/contracts/tool-inputs/index.js +94 -0
  74. package/domain/contracts/tool-inputs/integration.js +37 -0
  75. package/domain/contracts/tool-inputs/missions.js +94 -0
  76. package/domain/contracts/tool-inputs/ops.js +75 -0
  77. package/domain/contracts/tool-inputs/shared.js +25 -0
  78. package/domain/errors.js +96 -0
  79. package/domain/gate.js +125 -0
  80. package/domain/integration/gate.js +176 -0
  81. package/domain/integration/init.js +225 -0
  82. package/domain/integration/liveness.js +31 -0
  83. package/domain/integration/merge.js +377 -0
  84. package/domain/integration/provision.js +124 -0
  85. package/domain/integration/rebase.js +236 -0
  86. package/domain/integration/review.js +52 -0
  87. package/domain/integration/teardown.js +118 -0
  88. package/domain/mission.js +68 -0
  89. package/domain/missions/completion.js +114 -0
  90. package/domain/missions/findings.js +201 -0
  91. package/domain/missions/lifecycle.js +185 -0
  92. package/domain/missions/plan.js +149 -0
  93. package/domain/missions/review.js +179 -0
  94. package/domain/planning.js +118 -0
  95. package/domain/read/enrichedMissions.js +58 -0
  96. package/domain/read/missionContext.js +53 -0
  97. package/domain/read/views.js +128 -0
  98. package/domain/report.js +59 -0
  99. package/domain/roster.js +40 -0
  100. package/domain/scope.js +105 -0
  101. package/domain/state/schema.js +233 -0
  102. package/domain/state/semantics.js +117 -0
  103. package/domain/state.js +33 -0
  104. package/domain/summary.js +114 -0
  105. package/domain/vocabulary.js +179 -0
  106. package/index.js +86 -34
  107. package/lib/briefing.js +176 -0
  108. package/lib/compatibility.js +108 -0
  109. package/lib/config.js +54 -302
  110. package/lib/doctor.js +108 -9
  111. package/lib/git.js +93 -15
  112. package/lib/hmr.js +4 -3
  113. package/lib/leases.js +3 -2
  114. package/lib/legacy.js +20 -9
  115. package/lib/modeState.js +39 -0
  116. package/lib/modeStore.js +35 -33
  117. package/lib/persistence.js +349 -0
  118. package/lib/presets.js +35 -0
  119. package/lib/spawn-prechecks.js +168 -0
  120. package/lib/spawn-support.js +79 -0
  121. package/lib/store.js +57 -85
  122. package/lib/tests.js +46 -11
  123. package/lib/tool-registry.js +544 -0
  124. package/lib/workspaceLock.js +46 -22
  125. package/lib/worktreeOwnership.js +5 -4
  126. package/locale/en.json +23 -1
  127. package/locale/zh.json +22 -0
  128. package/package.json +33 -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 +44 -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 +203 -0
  150. package/scripts/release-check.mjs +303 -0
  151. package/skills/tower-recovery.md +7 -5
  152. package/tools/adopt.js +72 -0
  153. package/tools/batch.js +78 -0
  154. package/tools/complete.js +53 -0
  155. package/tools/diff.js +39 -0
  156. package/tools/doctor.js +50 -0
  157. package/tools/finding.js +40 -0
  158. package/tools/findings.js +39 -0
  159. package/tools/import-legacy.js +42 -0
  160. package/tools/inbox.js +39 -0
  161. package/tools/index.js +39 -0
  162. package/tools/init.js +63 -0
  163. package/tools/interrupt.js +42 -0
  164. package/tools/legacy-map.js +44 -0
  165. package/tools/merge.js +39 -0
  166. package/tools/mission.js +41 -0
  167. package/tools/plan.js +39 -0
  168. package/tools/preflight.js +44 -0
  169. package/tools/rebase.js +39 -0
  170. package/tools/recover.js +45 -0
  171. package/tools/reload.js +52 -0
  172. package/tools/resource.js +54 -0
  173. package/tools/review.js +45 -0
  174. package/tools/roster.js +60 -0
  175. package/tools/send.js +147 -0
  176. package/tools/spawn.js +196 -0
  177. package/tools/status.js +94 -0
  178. package/tools/teardown.js +42 -0
  179. package/tools/test.js +61 -0
  180. package/types/cordis.d.ts +6 -0
  181. package/types/index.d.ts +339 -0
  182. package/ui-bundle/README.md +111 -0
  183. package/ui-bundle/client.js +1980 -0
  184. package/ui-bundle/cordis.patch.yml +3 -0
  185. package/ui-bundle/icon.svg +6 -0
  186. package/ui-bundle/index.js +2 -0
  187. package/ui-bundle/locale/en.json +6 -0
  188. package/ui-bundle/locale/zh.json +6 -0
  189. package/ui-bundle/package.json +35 -0
  190. package/lib/errors.js +0 -111
  191. package/lib/tools.js +0 -2250
  192. package/lib/tower.js +0 -2689
  193. /package/{lib → domain}/batch.js +0 -0
package/tools/test.js ADDED
@@ -0,0 +1,61 @@
1
+ // tower_test: run declared worktree test discovery.
2
+ import { Tower } from '../app/tower.js'
3
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
4
+ import { TowerToolError } from '../domain/errors.js'
5
+ import { TestInput } from '../domain/contracts/tool-inputs/index.js'
6
+ import { isReviewerRole, isWorkerRole } from '../domain/vocabulary.js'
7
+
8
+ /**
9
+ * @param {any} ctx
10
+ * @param {{
11
+ * define: import('../types/index.js').DefineTool,
12
+ * actorFor: any,
13
+ * requireKnownActor: any,
14
+ * runMissionDiscovery: any,
15
+ * }} deps
16
+ */
17
+ export function registerTestTool(ctx, deps) {
18
+ const { define, actorFor, requireKnownActor, runMissionDiscovery } = deps
19
+ define(ctx, {
20
+ name: 'tower_test',
21
+ description:
22
+ 'Run the mission worktree\'s declared tower.testDiscovery (package.json) and record the receipt. Returns ok:false with diagnostics when discovery fails; does not change mission status. Use tower_complete for automatic admission checks.',
23
+ parameters: TestInput,
24
+ aliases: { missionId: ['id'] },
25
+ requiresMissionId: true,
26
+ output: output(SCHEMAS.test),
27
+ async execute(args, exec) {
28
+ const tower = await Tower.open(args.repoRoot)
29
+ const actor = await actorFor(tower, exec)
30
+ requireKnownActor(actor, 'tower_test')
31
+ const context = await tower.missionContext(args.missionId)
32
+ if (!context.mission.worktree) throw new TowerToolError('invalid-transition', `mission ${args.missionId} has no worktree`)
33
+ if (isReviewerRole(actor.role)) throw new TowerToolError('not-authorized', 'reviewers may not run test discovery')
34
+ if (isWorkerRole(actor.role) && context.mission.owner && context.mission.owner !== actor.name) {
35
+ throw new TowerToolError('not-authorized', `worker ${actor.name} does not own mission ${args.missionId}`)
36
+ }
37
+ await tower.assertWorktree(context.mission.worktree)
38
+ const discovery = await runMissionDiscovery(tower, context.mission)
39
+ if (!discovery.configured) {
40
+ return {
41
+ ok: false,
42
+ configured: false,
43
+ error: discovery.error ?? 'tower.testDiscovery is not configured in the worktree package.json',
44
+ tests: [],
45
+ commands: [],
46
+ }
47
+ }
48
+ const receipt = discovery.receipt
49
+ return {
50
+ ok: receipt.ok === true,
51
+ configured: true,
52
+ treeTip: receipt.treeTip ?? null,
53
+ tests: receipt.tests ?? [],
54
+ commands: receipt.commands ?? [],
55
+ error: receipt.error ?? undefined,
56
+ receipt,
57
+ checks: discovery.check ? [discovery.check] : [],
58
+ }
59
+ },
60
+ })
61
+ }
@@ -0,0 +1,6 @@
1
+ // Ambient augmentation for the Tower mode service (#50).
2
+ declare module '@deepseek-ai/cordis' {
3
+ interface Context {
4
+ tower?: import('./index.js').ModeStateService
5
+ }
6
+ }
@@ -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
@@ -0,0 +1,111 @@
1
+ # Tower UI (optional Client bundle)
2
+
3
+ An **optional** Web Client bundle for the Host `dsh-tower` plugin. It never touches Tower state directly: everything it renders is derived from the conversation the user is already looking at, or from the tool results already in that conversation. There is no Host → Client remote channel, no `host.call`, no DOM manipulation, no iframe, and no import of Harness Client packages (the only module it requires is `react` from the browser module table).
4
+
5
+ Version: **0.2.2** (private, not published). The Host plugin version is unchanged.
6
+
7
+ This revision fixes GitHub **#11** (persistent, history-derived Tower-mode indicator in the composer dock) and **#12** (mode-aware `Tower mode · disabled` rows for guard-denied tools), see below.
8
+
9
+ ## What it registers
10
+
11
+ | Surface | Slot | Registration | Data source |
12
+ |---|---|---|---|
13
+ | Tower tool cards | `tool.call.toolview` (keyed, session) | 12 entries, one per `key`: `tower_status`, `tower_roster`, `tower_spawn`, `tower_mission`, `tower_complete`, `tower_review`, `tower_preflight`, `tower_merge`, `tower_teardown`, `tower_interrupt`, `tower_doctor`, `tower_batch` | The Chat tool-call tree's owner props: `toolName`, `phase` (`preparing` / `start` / `result`), `block` (streaming start block or settled `tool/result` block), `callId`. |
14
+ | Composer dock | `conversation.composer.dock` (list, session) | `id: "tower-hint"`, `order: 6` | `useChat` standard selector hook (the durable Chat legacy slice). In Tower mode: a `Tower · on` chip. Otherwise, a Tower tool result in the conversation: a compact latest-status line. Otherwise: the static `/tower` hint. |
15
+ | Denied-tool rows | `tool.call.toolview` (keyed, session) | dynamic entries for the Tower-mode deny list, `priority: -10`, plus `update_goal` | The same history-derived Tower mode. Registered only while the displayed session is in Tower mode and disposed when it leaves, so the native toolviews (priority 0) return. |
16
+ | Overlay status strip | `shell.overlay` (list, root) | `id: "tower-status-strip"`, `order: 20` | The overlay slot is root-scoped, so it receives no chat hooks. The chat-driven toolview components publish a start/result notice into a module-local observable that this entry injects as `useTowerNotice`; a single timer auto-dismisses it after 4.5 s and is disposed by `ctx.effect`. |
17
+ | Locale | Client locale service | namespace `tower-ui` (`en`, `zh`) | Bound `t` seats come from each registration's `locale` option. |
18
+
19
+ The cards are deliberately compact: mission status/counts, task checklist progress, blockers, roster live/stale counts, review verdict, merge/preflight state, and check/test evidence (exit codes included). All of it is rendered as React elements with inline styles; **theme references are restricted to `--dsw-alias-*` tokens**, so light/dark and token renames degrade gracefully.
20
+
21
+ The overlay strip is `pointer-events: none` end-to-end and uses `role="status"` / `aria-live="polite"`; it never blocks the composer or the page.
22
+
23
+ ## Persistent Tower-mode indicator (#11)
24
+
25
+ Tower mode is a Host-side session stance and there is no Client RPC for it. The dock derives it from the durable session history that the Chat projection exposes in `useChat(snapshot.legacy)`:
26
+
27
+ - the `/tower on|off` command node (`kind: "command"`, `name: "tower"`, `args`, `outcome.kind`); bare `/tower` and `/tower <message>` mean "ensure on", `/tower reload` and `/tower base <branch>` leave the stance unchanged, and a failed command is ignored;
28
+ - `tower_mode` results (`mode: "on" | "off" | "unknown"`, `deniedWhileActive`, `workspaceLock`, `lastChange`);
29
+ - `tower_control` results (`action: enter|exit`, `mode`, `changedBy`, `reason`).
30
+
31
+ The last recognized event wins. A page refresh re-derives the same stance from the loaded history; when that history window no longer contains a mode event, the dock falls back to a browser-local last-known value for the same session and labels it as such. An old value expires after seven days and never drives the denied-tool shadowing by itself.
32
+
33
+ While the mode is on the dock shows a `Tower · on` chip with `role="status"` / `aria-live="polite"`, and its tooltip carries the actionable hint plus, when known, the repository basename, workspace-lock state (`held`/`free`), and who changed the mode last. The chip is **deliberately non-interactive**: the only existing standard affordance is `inputActions.setDraft` + `inputActions.submit`, which submits the composer draft and would clobber what the user is typing. The tooltip tells the user to run `/tower off`; the bundle itself does not toggle the mode (the model's supported path is the Host `tower_control` tool when `allowAgentControl` is enabled).
34
+
35
+ ## Mode-aware denied-tool rows (#12)
36
+
37
+ The Host guard returns a plain tool error like `tower mode: <tool> is disabled while Tower mode is active; use the Tower tools or /tower off` (and `tower mode: goals cannot be resumed while Tower mode is active; use /tower off first` for `update_goal` resume).
38
+
39
+ While the displayed session is in Tower mode the bundle dynamically registers `tool.call.toolview` entries for:
40
+
41
+ - `tower_mode.deniedWhileActive` when the latest `tower_mode` output lists it, otherwise `lib/config.js` `DEFAULT_DENY_TOOLS` (`create_goal`, `todo_write`, `team_task_create`, `team_task_update`, `team_task_reopen`, `team_task_list`, `spawn_teammate`, `wait_agent`, `workflow`, `ralph`, `subagent`, `subagent_fork`);
42
+ - `update_goal` unconditionally, because the guard special-cases its `action: "resume"`.
43
+
44
+ Those entries register with `priority: -10`. The slot core renders the lowest priority for a key, so this shadows the native keyed views without replacing them; disposing the entries (mode off) restores the native views exactly. A denial result renders as a distinct **`Tower mode · disabled`** row (warn marker, label, the preserved denial message, and the `/tower off` hint) — not the generic error text styling. `update_goal` resume denials get the same row. A non-denial result that lands in one of those slots while the mode is on renders a defensive generic row (there is no native view to delegate to while the registration shadows it); ordinary tool errors are not misclassified as Tower-mode denials.
45
+
46
+ ## Verification status
47
+
48
+ Verified **statically** against the installed DSH **0.2.1-rc.2** sources:
49
+
50
+ - `tool.call.toolview`: declared by `@deepseek-ai/dsh-client-ui-tool` as `{ kind: "keyed", scope: "session" }`; the Chat tool-call tree dispatches with `entryKey = toolName` and passes `{ callId, toolName, ...call, openFile, cwd, home, loadImage, useDisclosure, inspect }`, where `call` is `{ phase, block }`. A keyed hit replaces the generic tool row; a miss falls back to it.
51
+ - Keyed priority/shadowing: the Client slot core (in the web shell bundle) allows a second keyed registration only at a different `priority` and resolves a key with the **lowest** priority first — `register at a different priority to shadow it (lowest renders)`. Native views use the default priority 0.
52
+ - `conversation.composer.dock`: declared by `@deepseek-ai/dsh-client-ui-conversation` as `{ kind: "list", scope: "session" }`, rendered as `div[data-composer-dock]`. Owner props are empty; session-scope standard props (including `useSession`, `useChat`, `useConversation`) are supplied by the renderer.
53
+ - `shell.overlay`: declared by `@deepseek-ai/dsh-client-ui-layout` as `{ kind: "list", scope: "root" }`, rendered inside `[data-shell-overlay]`. Root standard props are `useSessions`, `useSessionStatus`, `useWorkspaces`, `usePanelInfo` and keyed access hooks — **no chat hook**; hence the toolview-published notice design above.
54
+ - `/tower` history: DSH logs `command/run` with `{ name, args: parsed.rawInput, source }` and `command/done` with `{ kind, text }`, which the Chat command node exposes as `{ kind: "command", name, args, outcome }` in the legacy slice.
55
+ - Guard denial shape: the Host tools pipeline materializes a denial as `{ content: [{ type: "text", text: "Error: <reason>" }], isError: true, error: { message: <reason> } }`.
56
+ - Locale: `ctx.locale.register(ns, { en, zh })`, dictionaries are flat `key → template` maps with `{param}` interpolation; a slot declaration's `locale` option supplies the bound `t` seat.
57
+ - The bundle's `dsh.client.inject` orders activation after `ui-conversation` and `ui-tool`, the two packages that declare the slots it targets. `ctx.slots.inject` only installs a registration while its owner declaration exists; each injection is guarded and reported in diagnostics instead of crashing the whole Client half.
58
+
59
+ Automated offline coverage: `tests/ui-bundle.test.mjs` loads `client.js` through a fake `window.__ModuleLoader__`, asserts the 12 keys / dock id / overlay id, en–zh dictionary parity, theme-token discipline, and renders every card plus the dock/overlay components with fake hooks. It also drives fake chat history through the dock to assert the off hint, the on chip (including refresh re-derivation), `tower_mode`/`tower_control`/command updates, dynamic registration for default and configured deny lists, disposal on mode off, `update_goal`, denial-row treatment, generic fallbacks, and locale parity — no browser required.
60
+
61
+ Live/browser verification is **opt-in** (`tests/live/ui-slots.cjs`) because it needs an isolated profile with the bundle installed. From a teammate/subagent session the Client `Slots` inspect query times out by design (the page answers only the agent whose session it displays), so this phase did not run a live inspection; the source-level contract above is the evidence. When the script runs against an installed profile it binds a session with a bare `/tower` command and asserts, through the read-only `window.__TOWER_UI_DIAG__` report, that apply() ran without errors, that all 12 toolview keys and the dock/overlay entries registered, that the dock outlet is visible, and — since the bind command turns the Host mode on — that the history-derived `Tower · on` chip rendered (#11). The chip can be inspected visually in the same run (`data-tower-ui="dock-mode"`).
62
+
63
+ ## Tests
64
+
65
+ ```powershell
66
+ # offline, no browser
67
+ node --check ui-bundle/client.js
68
+ node --test tests/ui-bundle.test.mjs
69
+
70
+ # opt-in live check (isolated profile only)
71
+ node tests/live/ui-slots.cjs
72
+ ```
73
+
74
+ `tests/live/ui-slots.cjs` prints `SKIP` and exits 0 when `DSH_TEST_URL` (or Playwright) is missing; it never installs anything and never opens the desktop profile. Set `DSH_TEST_EXPECT_HINT=1` when the bundle is installed so a missing client module or missing dock entry fails instead of skipping the assertion, and `DSH_TEST_EXPECT_MODE=1` to require the history-derived `Tower · on` chip after the script's `/tower` bind step (otherwise that check is a warning, since the bind step is best-effort).
75
+
76
+ ## Why it is a separate bundle
77
+
78
+ It is not installed by default and does not affect installing or running the Host `dsh-tower` plugin. The Host plugin is fully functional without it; the UI half only decorates the conversation that is already on screen.
79
+
80
+ ## Install (isolated profile only)
81
+
82
+ ```text
83
+ # throwaway profile, not the desktop profile
84
+ dsh --profile towertest --from-default-profile web --dump-config
85
+ dsh plugin --profile towertest add <absolute path to this ui-bundle>
86
+ dsh --profile towertest --port 59001 --no-open
87
+ # open the tokenized URL printed at boot, then verify:
88
+ $env:DSH_TEST_URL='http://127.0.0.1:59001/?token=<token>'
89
+ $env:DSH_TEST_EXPECT_HINT='1'; node tests/live/ui-slots.cjs
90
+ ```
91
+
92
+ From a session in that profile, `plugin_manager({ action: "install_bundle", target: "<absolute path to this ui-bundle>" })` performs the same add + enable. Do not install it into the live desktop profile without the user's explicit request.
93
+
94
+ ## Remove
95
+
96
+ ```text
97
+ dsh plugin --profile towertest remove dsh-tower-ui
98
+ ```
99
+
100
+ Without HMR a bundle that has been running since process start may need a DSH restart for removal; with HMR a reconcile can unload it. A page refresh (or hard refresh) is enough to update the Client half after a linked-directory change.
101
+
102
+ ## Known limitations
103
+
104
+ - **No live Host state / no new RPC.** The mode chip is inferred from the **loaded history window** of the displayed session. If the `/tower on` command (or a `tower_mode` result) sits outside that window, the dock falls back to the static hint and the mode-aware rows are not registered; a later `/tower`, `tower_control`, or `tower_mode` result corrects it. The dock summary is likewise the latest visible Tower tool result, not a dashboard, and does not poll.
105
+ - **Mode-driven registration is slot-global.** The dock follows the displayed session. While that session is in Tower mode, the denied-tool shadow entries also apply to other sessions' rows for those tools (they render the defensive generic row); leaving the mode disposes them globally and restores the native views.
106
+ - **A custom `mode.denyTools` list needs a `tower_mode` result in the visible history.** The dock learns `deniedWhileActive` only from a `tower_mode` output; if the mode was inferred from `/tower on` alone (or the `tower_mode` result is outside the loaded window), the bundle shadows the default list instead, and a custom denied tool falls back to the native error row until a `tower_mode` output appears.
107
+ - **The on-chip is not a button.** `inputActions.submit()` submits the composer draft, so programmatically sending `/tower off` would overwrite the user's draft; the actionable tooltip asks the user to run it. There is no click handler and no automatic toggle from the bundle; the Host `tower_control` tool remains the model's supported path.
108
+ - **The overlay is root-scoped.** It cannot read chat state directly, so a result that lands while every Tower toolview is unmounted (for example the user switches sessions before the result renders) shows no strip. The strip itself carries no actionable content by design.
109
+ - A tool result whose payload is not JSON (older/foreign shape) renders as a plain one-line summary rather than a card; errors render as an error card.
110
+ - Client bundle updates are not auto-rebuilt: edit → reconcile/link → **refresh the page**. Profile patch or manifest changes may need a restart.
111
+ - `window.__TOWER_UI_DIAG__` is a read-only diagnostic seam for tests. It contains no session content — only plugin version, applied flag, slot/toolview names, current mode summary, dynamic deny keys, and guarded registration errors.