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/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
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -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.
|