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,141 @@
1
+ // The expected loaded surface of the dsh-tower bundle (#95).
2
+ //
3
+ // Pure derivation and comparison only: the mount smoke boots a throwaway
4
+ // profile, and these helpers turn the checked-in source of truth into the ids
5
+ // that boot must produce. Keeping them here — instead of inside the command —
6
+ // lets the offline suite pin the oracle without installing a host.
7
+ //
8
+ // The oracle is deliberately source-derived where the source is unambiguous:
9
+ // one tool id per tool module, one skill id per bundled skill file. The bundle
10
+ // children and presets come from `cordis.patch.yml` and are stated here as the
11
+ // contract the #66 invariant protects.
12
+
13
+ /** Tools registered by index.js rather than by a handler module under tools/. */
14
+ export const INDEX_TOOL_IDS = ['tower_control', 'tower_mode']
15
+
16
+ /** The single top-level row the bundle is allowed to contribute (#66). */
17
+ export const BUNDLE_ID = 'tower-bundle'
18
+
19
+ /** Rows that must stay nested inside {@link BUNDLE_ID}. */
20
+ export const BUNDLE_CHILD_IDS = ['preset-tower-reviewer', 'preset-tower-worker', 'tower', 'tower-skills']
21
+
22
+ /** Presets the bundle declares for its worker and reviewer roles. */
23
+ export const PRESET_IDS = ['tower-reviewer', 'tower-worker']
24
+
25
+ /** Cordis fiber state 2: mounted and active. */
26
+ const FIBER_ACTIVE = 2
27
+
28
+ const OWNED_ROW_IDS = new Set([BUNDLE_ID, ...BUNDLE_CHILD_IDS])
29
+
30
+ /**
31
+ * Map one tools/ file name to the tool id its module must register.
32
+ * `import-legacy.js` registers `tower_import_legacy`; `index.js` registers none.
33
+ * @param {string} fileName - a file name under tools/.
34
+ * @returns {string | null} the expected tool id, or null for non-tool files.
35
+ */
36
+ export function toolIdForModule(fileName) {
37
+ if (typeof fileName !== 'string' || !fileName.endsWith('.js')) return null
38
+ const base = fileName.slice(0, -'.js'.length)
39
+ if (base.length === 0 || base === 'index' || base.includes('/') || base.includes('\\')) return null
40
+ return `tower_${base.replaceAll('-', '_')}`
41
+ }
42
+
43
+ /**
44
+ * @param {string[]} moduleFiles - file names under tools/.
45
+ * @returns {string[]} sorted tool ids the loaded profile must register.
46
+ */
47
+ export function expectedToolIds(moduleFiles) {
48
+ const ids = new Set(INDEX_TOOL_IDS)
49
+ for (const file of moduleFiles) {
50
+ const id = toolIdForModule(file)
51
+ if (id !== null) ids.add(id)
52
+ }
53
+ return [...ids].sort()
54
+ }
55
+
56
+ /**
57
+ * @param {string[]} skillFiles - file names under skills/.
58
+ * @returns {string[]} sorted skill ids the bundled provider must publish.
59
+ */
60
+ export function expectedSkillIds(skillFiles) {
61
+ return skillFiles
62
+ .filter((file) => typeof file === 'string' && file.endsWith('.md'))
63
+ .map((file) => file.slice(0, -'.md'.length))
64
+ .sort()
65
+ }
66
+
67
+ /**
68
+ * Compare one loaded id list against the expected one.
69
+ * @param {string} label - diagnostic prefix (`tool`, `skill`, `preset`, ...).
70
+ * @param {string[]} expectedIds - ids the surface must contain.
71
+ * @param {string[]} loadedIds - ids the loaded profile registered.
72
+ * @returns {string[]} one message per missing, unexpected, or miscounted id.
73
+ */
74
+ export function compareIds(label, expectedIds, loadedIds) {
75
+ const expected = [...new Set(expectedIds)].sort()
76
+ const loaded = [...new Set(loadedIds)].sort()
77
+ const failures = []
78
+ for (const id of expected) if (!loaded.includes(id)) failures.push(`${label}: missing ${id}`)
79
+ for (const id of loaded) if (!expected.includes(id)) failures.push(`${label}: unexpected ${id}`)
80
+ if (loaded.length !== expected.length) failures.push(`${label}: loaded ${loaded.length} ids, expected ${expected.length}`)
81
+ return failures
82
+ }
83
+
84
+ /**
85
+ * Assert the bundle contributes exactly one top-level group row (#66) and that
86
+ * every composed section actually mounted.
87
+ * @param {Array<{ id?: string, name?: string, group?: boolean, config?: unknown[] }>} topRows -
88
+ * the composed rows at the root of the loaded profile tree.
89
+ * @param {Record<string, number | undefined>} [entryStates] - fiber state per entry id.
90
+ * @returns {string[]} one message per violated invariant.
91
+ */
92
+ export function checkBundleRow(topRows, entryStates = {}) {
93
+ const rows = Array.isArray(topRows) ? topRows.filter(Boolean) : []
94
+ const failures = []
95
+ const bundleRows = rows.filter((row) => row.id === BUNDLE_ID)
96
+ if (bundleRows.length !== 1) {
97
+ failures.push(
98
+ bundleRows.length === 0
99
+ ? `bundle: no top-level ${BUNDLE_ID} row; the bundle did not compose`
100
+ : `bundle: found ${bundleRows.length} top-level ${BUNDLE_ID} rows, expected exactly 1`,
101
+ )
102
+ if (bundleRows.length === 0) return failures
103
+ }
104
+
105
+ const leaked = new Set()
106
+ for (const row of rows) {
107
+ if (row.id === BUNDLE_ID) continue
108
+ const name = typeof row.name === 'string' ? row.name : ''
109
+ const ownedName = name === 'dsh-tower' || name.startsWith('dsh-tower/')
110
+ if (!OWNED_ROW_IDS.has(String(row.id)) && !ownedName) continue
111
+ if (leaked.has(String(row.id))) continue
112
+ leaked.add(String(row.id))
113
+ failures.push(`bundle: ${row.id} is a top-level row; it must stay nested in ${BUNDLE_ID}`)
114
+ }
115
+
116
+ const bundleRow = bundleRows[0]
117
+ if (bundleRow.name !== 'cordis:group' || bundleRow.group !== true) {
118
+ failures.push(
119
+ `bundle: ${BUNDLE_ID} is not a cordis:group row (name ${JSON.stringify(bundleRow.name)}, group ${JSON.stringify(bundleRow.group)})`,
120
+ )
121
+ }
122
+
123
+ const rawChildren = Array.isArray(bundleRow.childIds) ? bundleRow.childIds : Array.isArray(bundleRow.config) ? bundleRow.config : []
124
+ const children = rawChildren
125
+ .map((child) =>
126
+ child && typeof child === 'object' && child.id !== undefined ? String(child.id) : typeof child === 'string' ? child : null,
127
+ )
128
+ .filter(Boolean)
129
+ const expectedChildren = [...BUNDLE_CHILD_IDS].sort()
130
+ for (const id of expectedChildren) if (!children.includes(id)) failures.push(`bundle: ${BUNDLE_ID} is missing child ${id}`)
131
+ for (const id of children) if (!expectedChildren.includes(id)) failures.push(`bundle: ${BUNDLE_ID} has unexpected child ${id}`)
132
+ if (new Set(children).size !== children.length) failures.push(`bundle: ${BUNDLE_ID} has duplicate child rows`)
133
+
134
+ for (const id of [BUNDLE_ID, ...BUNDLE_CHILD_IDS]) {
135
+ const state = entryStates?.[id]
136
+ if (state !== FIBER_ACTIVE) {
137
+ failures.push(`bundle: entry ${id} is not active (entry state ${state === undefined ? 'missing' : state})`)
138
+ }
139
+ }
140
+ return failures
141
+ }
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ // Mount smoke (#95): does the *packed* bundle mount and register what it claims?
3
+ //
4
+ // npm run smoke:mount # pack, install the pinned host, boot, assert
5
+ // node scripts/mount-smoke.mjs --root <repo> --host-dir <dir> --keep
6
+ //
7
+ // The command packs this package, installs the host build pinned in
8
+ // scripts/mount-smoke/host.json into a throwaway directory, creates a throwaway
9
+ // DSH home and profile, adds the tarball through the plugin CLI, and boots the
10
+ // composed profile through the real loader (scripts/mount-smoke/probe.mjs).
11
+ // It then asserts the LOADED surface — registered tool ids, the bundled skills,
12
+ // both presets, the /tower command, and the single top-level bundle row — never
13
+ // the text of cordis.patch.yml. No model call and no browser.
14
+ //
15
+ // A red run prints the resolved profile path, the host build, the loaded counts
16
+ // and every failed claim, and keeps the temporary workspace for inspection.
17
+ import { spawnSync } from 'node:child_process'
18
+ import fs from 'node:fs'
19
+ import os from 'node:os'
20
+ import path from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+ import {
23
+ BUNDLE_ID,
24
+ INDEX_TOOL_IDS,
25
+ PRESET_IDS,
26
+ checkBundleRow,
27
+ compareIds,
28
+ expectedSkillIds,
29
+ expectedToolIds,
30
+ } from './mount-smoke/surface.mjs'
31
+ import { parseArgs, runMain } from './lib/cli.mjs'
32
+ import { removeTreeQuietly } from './lib/tmp.mjs'
33
+
34
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
35
+ const repoRoot = path.resolve(scriptDir, '..')
36
+ const probePath = path.join(scriptDir, 'mount-smoke', 'probe.mjs')
37
+ const pinPath = path.join(scriptDir, 'mount-smoke', 'host.json')
38
+
39
+ function parseOptions(argv) {
40
+ return parseArgs(argv, {
41
+ initial: { root: repoRoot, hostDir: undefined, profile: 'tower-smoke', keep: false },
42
+ values: {
43
+ '--root': { key: 'root', parse: path.resolve },
44
+ '--host-dir': { key: 'hostDir', parse: path.resolve },
45
+ '--profile': 'profile',
46
+ },
47
+ flags: { '--keep': 'keep' },
48
+ error: (message) => new Error(message),
49
+ optionLikeValues: false,
50
+ })
51
+ }
52
+
53
+ /** npm's JS entry, so no shell (and no quoting) is involved on any platform. */
54
+ function npmInvocation() {
55
+ const nodeDir = path.dirname(process.execPath)
56
+ const candidates = [
57
+ process.env.npm_execpath,
58
+ path.join(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
59
+ path.join(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
60
+ ].filter((candidate) => typeof candidate === 'string' && candidate.endsWith('.js'))
61
+ for (const candidate of candidates) {
62
+ if (fs.existsSync(candidate)) return { command: process.execPath, prefix: [candidate], shell: false }
63
+ }
64
+ return { command: process.platform === 'win32' ? 'npm.cmd' : 'npm', prefix: [], shell: process.platform === 'win32' }
65
+ }
66
+
67
+ function run(command, args, { cwd, env, timeout, label, shell = false }) {
68
+ const result = spawnSync(command, args, { cwd, env, timeout, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, shell })
69
+ const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim()
70
+ if (result.error) throw new Error(`${label} could not run: ${result.error.message}\n${output.slice(-2000)}`)
71
+ if (result.status !== 0) throw new Error(`${label} failed (exit ${result.status}):\n${output.slice(-4000)}`)
72
+ return result
73
+ }
74
+
75
+ function childEnv(home) {
76
+ return { ...process.env, DSH_HOME: home, DSH_TELEMETRY_DISABLED: '1' }
77
+ }
78
+
79
+ function readHostVersion(hostDir) {
80
+ const manifest = path.join(hostDir, 'node_modules', '@deepseek-ai', 'dsh', 'package.json')
81
+ if (!fs.existsSync(manifest)) return null
82
+ return JSON.parse(fs.readFileSync(manifest, 'utf8')).version
83
+ }
84
+
85
+ function ensureHost(hostDir, pin, npm, env) {
86
+ const existing = readHostVersion(hostDir)
87
+ if (existing !== null) {
88
+ if (existing !== pin.dsh) throw new Error(`host: ${hostDir} has @deepseek-ai/dsh@${existing}; the pin is ${pin.dsh}`)
89
+ return existing
90
+ }
91
+ fs.mkdirSync(hostDir, { recursive: true })
92
+ run(
93
+ npm.command,
94
+ [...npm.prefix, 'install', '--prefix', hostDir, '--no-audit', '--no-fund', '--ignore-scripts', '--loglevel=error', `@deepseek-ai/dsh@${pin.dsh}`],
95
+ { cwd: hostDir, env, timeout: 300_000, label: `host install (@deepseek-ai/dsh@${pin.dsh})`, shell: npm.shell },
96
+ )
97
+ const installed = readHostVersion(hostDir)
98
+ if (installed !== pin.dsh) throw new Error(`host install: installed @deepseek-ai/dsh@${installed}, expected pin ${pin.dsh}`)
99
+ return installed
100
+ }
101
+
102
+ function pack(root, packDir, npm, env) {
103
+ fs.mkdirSync(packDir, { recursive: true })
104
+ const result = run(npm.command, [...npm.prefix, 'pack', '--json', '--pack-destination', packDir], {
105
+ cwd: root,
106
+ env,
107
+ timeout: 120_000,
108
+ label: 'npm pack',
109
+ shell: npm.shell,
110
+ })
111
+ const stdout = result.stdout ?? ''
112
+ const start = stdout.indexOf('[')
113
+ if (start < 0) throw new Error(`npm pack: no JSON on stdout:\n${stdout.slice(-2000)}`)
114
+ const parsed = JSON.parse(stdout.slice(start))
115
+ const info = Array.isArray(parsed) ? parsed[0] : parsed
116
+ if (!info?.filename) throw new Error(`npm pack: no filename in ${JSON.stringify(info)}`)
117
+ return { tarball: path.join(packDir, info.filename), name: info.name, version: info.version }
118
+ }
119
+
120
+ /**
121
+ * The smoke profile is a bare @deepseek-ai/dsh-base profile; the bundle's two
122
+ * preset rows need the agent-preset registry that every shipped web profile
123
+ * mounts. Add that one host row to the throwaway profile's own patch layer so
124
+ * the preset claims are checked against the real registry, not row presence.
125
+ */
126
+ const PROFILE_OVERLAY = `# Added by scripts/mount-smoke.mjs (#95).
127
+ - insert:
128
+ - id: agent-preset-registry
129
+ name: '@deepseek-ai/dsh-agent-preset-registry'
130
+ config:
131
+ default: standard
132
+ selectedDefault: standard
133
+ `
134
+
135
+ /** Read one surface area, or report why it is unavailable. */
136
+ function area(surface, key, label, failures) {
137
+ const entry = surface?.[key]
138
+ if (!entry) {
139
+ failures.push(`${label}: the probe reported no ${key} surface`)
140
+ return null
141
+ }
142
+ if (entry.error) {
143
+ failures.push(`${label}: ${entry.error}`)
144
+ return null
145
+ }
146
+ return entry.value
147
+ }
148
+
149
+ function toolModuleNames(root) {
150
+ return fs.readdirSync(path.join(root, 'tools')).filter((name) => name.endsWith('.js'))
151
+ }
152
+
153
+ function verifySurface(surface, root) {
154
+ const failures = []
155
+ const expectedTools = expectedToolIds(toolModuleNames(root))
156
+ const expectedSkills = expectedSkillIds(fs.readdirSync(path.join(root, 'skills')))
157
+
158
+ const loadedTools = area(surface, 'tools', 'tool', failures)
159
+ if (loadedTools) failures.push(...compareIds('tool', expectedTools, loadedTools.filter((name) => name.startsWith('tower_'))))
160
+
161
+ const loadedSkills = area(surface, 'skills', 'skill', failures)
162
+ if (loadedSkills) failures.push(...compareIds('skill', expectedSkills, loadedSkills.tower))
163
+
164
+ const loadedPresets = area(surface, 'presets', 'preset', failures)
165
+ if (loadedPresets) failures.push(...compareIds('preset', PRESET_IDS, loadedPresets))
166
+
167
+ const loadedCommands = area(surface, 'commands', 'command', failures)
168
+ if (loadedCommands && !loadedCommands.includes('tower')) failures.push('command: /tower is not registered in the loaded command registry')
169
+
170
+ const tree = area(surface, 'tree', 'bundle', failures)
171
+ if (tree) failures.push(...checkBundleRow(tree.top, tree.states))
172
+
173
+ return failures
174
+ }
175
+
176
+ function summarize(surface, root) {
177
+ const modules = toolModuleNames(root).filter((name) => name !== 'index.js').length
178
+ const tools = Array.isArray(surface?.tools?.value) ? surface.tools.value.filter((name) => name.startsWith('tower_')).length : '?'
179
+ const skills = Array.isArray(surface?.skills?.value?.tower) ? surface.skills.value.tower.length : '?'
180
+ const presets = Array.isArray(surface?.presets?.value) ? surface.presets.value.length : '?'
181
+ const commands = Array.isArray(surface?.commands?.value) ? surface.commands.value.filter((name) => name === 'tower').length : '?'
182
+ const rows = Array.isArray(surface?.tree?.value?.top) ? surface.tree.value.top.filter((row) => row.id === BUNDLE_ID).length : '?'
183
+ return `${tools} tower tools (${modules} modules + ${INDEX_TOOL_IDS.length} entry tools), ${skills} skills, ${presets} presets, /tower ${commands === 1 ? 'present' : 'missing'}, ${rows} top-level bundle row`
184
+ }
185
+
186
+ function reportFailure(state, pin, options, error) {
187
+ const surface = state.surface
188
+ console.error('')
189
+ console.error(
190
+ `mount smoke failed: ${state.packed ? `${state.packed.name}@${state.packed.version}` : 'dsh-tower'} did not mount as claimed on @deepseek-ai/dsh@${pin.dsh}`,
191
+ )
192
+ if (error) console.error(` error: ${error.message}`)
193
+ console.error(` host: @deepseek-ai/dsh@${state.hostVersion ?? readHostVersion(state.hostDir) ?? pin.dsh} (${state.hostDir})`)
194
+ console.error(` profile: ${state.profileDir}`)
195
+ console.error(` tarball: ${state.tarball ?? '(not packed)'}`)
196
+ if (surface) console.error(` loaded: ${summarize(surface, options.root)}`)
197
+ for (const failure of state.failures) console.error(` - ${failure}`)
198
+ console.error(` workspace kept: ${state.tmp}`)
199
+ }
200
+
201
+ async function main() {
202
+ const options = parseOptions(process.argv.slice(2))
203
+ const root = options.root
204
+ if (!fs.existsSync(path.join(root, 'package.json'))) throw new Error(`mount smoke: ${root} is not a package root`)
205
+ const pin = JSON.parse(fs.readFileSync(pinPath, 'utf8'))
206
+ const npm = npmInvocation()
207
+ const started = Date.now()
208
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-tower-mount-'))
209
+ const home = path.join(tmp, 'home')
210
+ const hostDir = options.hostDir ?? path.join(tmp, 'host')
211
+ const profileDir = path.join(home, 'profiles', options.profile)
212
+ const state = { tmp, home, hostDir, profileDir, tarball: null, packed: null, surface: null, failures: [] }
213
+
214
+ let error = null
215
+ try {
216
+ const hostVersion = ensureHost(hostDir, pin, npm, childEnv(home))
217
+ state.hostVersion = hostVersion
218
+
219
+ state.packed = pack(root, path.join(tmp, 'pack'), npm, childEnv(home))
220
+ state.tarball = state.packed.tarball
221
+
222
+ fs.mkdirSync(home, { recursive: true })
223
+ const pluginBin = path.join(hostDir, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js')
224
+ run(process.execPath, [pluginBin, 'plugin', '--profile', options.profile, 'add', state.tarball], {
225
+ cwd: tmp,
226
+ env: childEnv(home),
227
+ timeout: 180_000,
228
+ label: `dsh plugin add (${path.basename(state.tarball)})`,
229
+ })
230
+
231
+ const profileManifestPath = path.join(profileDir, 'package.json')
232
+ if (!fs.existsSync(profileManifestPath)) throw new Error(`profile: plugin add did not create ${profileManifestPath}`)
233
+ const profileManifest = JSON.parse(fs.readFileSync(profileManifestPath, 'utf8'))
234
+ const bundles = profileManifest?.dsh?.profile?.bundles ?? []
235
+ if (!bundles.includes(state.packed.name)) {
236
+ throw new Error(`profile: plugin add did not add ${state.packed.name} to dsh.profile.bundles (got ${JSON.stringify(bundles)})`)
237
+ }
238
+ fs.writeFileSync(path.join(profileDir, 'cordis.patch.yml'), PROFILE_OVERLAY)
239
+
240
+ const surfacePath = path.join(tmp, 'surface.json')
241
+ run(process.execPath, [probePath, '--home', home, '--host', hostDir, '--profile', options.profile, '--out', surfacePath], {
242
+ cwd: tmp,
243
+ env: childEnv(home),
244
+ timeout: 180_000,
245
+ label: 'profile boot',
246
+ })
247
+ state.surface = JSON.parse(fs.readFileSync(surfacePath, 'utf8'))
248
+ state.failures = verifySurface(state.surface, root)
249
+ } catch (caught) {
250
+ error = caught
251
+ }
252
+
253
+ if (error || state.failures.length > 0) {
254
+ reportFailure(state, pin, options, error)
255
+ return 1
256
+ }
257
+
258
+ const seconds = ((Date.now() - started) / 1000).toFixed(1)
259
+ console.log(`mount smoke ok: ${state.packed.name}@${state.packed.version} mounts on @deepseek-ai/dsh@${state.hostVersion} (${seconds}s)`)
260
+ console.log(` profile: ${profileDir}`)
261
+ console.log(` loaded: ${summarize(state.surface, root)}`)
262
+ if (options.keep) {
263
+ console.log(` workspace kept: ${tmp}`)
264
+ } else {
265
+ removeTreeQuietly(tmp)
266
+ }
267
+ return 0
268
+ }
269
+
270
+ await runMain(import.meta.url, main, { errorPrefix: 'mount smoke failed' })
@@ -0,0 +1,212 @@
1
+ CHANGELOG.md
2
+ COMPATIBILITY.md
3
+ CONTRIBUTING.md
4
+ LICENSE
5
+ README.en.md
6
+ README.md
7
+ SECURITY.md
8
+ adapters/default.js
9
+ adapters/store-fs.js
10
+ adapters/vcs-git.js
11
+ app/archive.js
12
+ app/batch.js
13
+ app/board.js
14
+ app/checkCompletion.js
15
+ app/enrichedMissions.js
16
+ app/findings.js
17
+ app/gate.js
18
+ app/init.js
19
+ app/merge.js
20
+ app/messages.js
21
+ app/mission.js
22
+ app/missionContext.js
23
+ app/plan.js
24
+ app/provision.js
25
+ app/read.js
26
+ app/rebase.js
27
+ app/recording.js
28
+ app/review.js
29
+ app/roster.js
30
+ app/spawn.js
31
+ app/teardown.js
32
+ app/tower.js
33
+ cordis.patch.yml
34
+ docs/README.md
35
+ docs/agents/domain.md
36
+ docs/agents/issue-tracker.md
37
+ docs/agents/triage-labels.md
38
+ docs/architecture/overview.md
39
+ docs/board.md
40
+ docs/examples/first-mission.md
41
+ docs/faq.md
42
+ docs/guide/hot-reload.md
43
+ docs/guide/install-upgrade-migration.md
44
+ docs/guide/quickstart.md
45
+ docs/guide/release.md
46
+ docs/guide/scenarios-and-philosophy.md
47
+ docs/operations/runbook.md
48
+ docs/operations/security.md
49
+ docs/reference/blockers.md
50
+ docs/reference/config.generated.md
51
+ docs/reference/config.md
52
+ docs/reference/doctor.md
53
+ docs/reference/errors.generated.md
54
+ docs/reference/errors.md
55
+ docs/reference/events.md
56
+ docs/reference/operations.md
57
+ docs/reference/testing.md
58
+ docs/reference/tools.generated.md
59
+ docs/reference/tools.md
60
+ docs/zh/README.md
61
+ docs/zh/faq.md
62
+ docs/zh/guide/hot-reload.md
63
+ docs/zh/guide/install-upgrade-migration.md
64
+ docs/zh/guide/quickstart.md
65
+ docs/zh/guide/scenarios-and-philosophy.md
66
+ docs/zh/operations/security.md
67
+ docs/zh/reference/blockers.md
68
+ docs/zh/reference/config.md
69
+ docs/zh/reference/doctor.md
70
+ docs/zh/reference/errors.md
71
+ docs/zh/reference/events.md
72
+ docs/zh/reference/operations.md
73
+ docs/zh/reference/testing.md
74
+ docs/zh/reference/tools.md
75
+ domain/batch.js
76
+ domain/collab/batch.js
77
+ domain/collab/messages.js
78
+ domain/collab/roster.js
79
+ domain/collab/spawn.js
80
+ domain/config/schema.js
81
+ domain/contracts/outputs.js
82
+ domain/contracts/tool-inputs/collaboration.js
83
+ domain/contracts/tool-inputs/findings.js
84
+ domain/contracts/tool-inputs/index.js
85
+ domain/contracts/tool-inputs/integration.js
86
+ domain/contracts/tool-inputs/missions.js
87
+ domain/contracts/tool-inputs/ops.js
88
+ domain/contracts/tool-inputs/shared.js
89
+ domain/errors.js
90
+ domain/gate.js
91
+ domain/integration/gate.js
92
+ domain/integration/init.js
93
+ domain/integration/liveness.js
94
+ domain/integration/merge.js
95
+ domain/integration/provision.js
96
+ domain/integration/rebase.js
97
+ domain/integration/review.js
98
+ domain/integration/teardown.js
99
+ domain/mission.js
100
+ domain/missions/completion.js
101
+ domain/missions/findings.js
102
+ domain/missions/lifecycle.js
103
+ domain/missions/plan.js
104
+ domain/missions/review.js
105
+ domain/planning.js
106
+ domain/read/enrichedMissions.js
107
+ domain/read/missionContext.js
108
+ domain/read/views.js
109
+ domain/report.js
110
+ domain/roster.js
111
+ domain/scope.js
112
+ domain/state.js
113
+ domain/state/archive.js
114
+ domain/state/migrations.js
115
+ domain/state/schema.js
116
+ domain/state/semantics.js
117
+ domain/summary.js
118
+ domain/vocabulary.js
119
+ icon.svg
120
+ index.js
121
+ lib/archive.js
122
+ lib/briefing.js
123
+ lib/caseSensitivity.js
124
+ lib/compatibility.js
125
+ lib/config.js
126
+ lib/doctor.js
127
+ lib/git.js
128
+ lib/hmr.js
129
+ lib/leases.js
130
+ lib/legacy.js
131
+ lib/modeState.js
132
+ lib/modeStore.js
133
+ lib/persistence.js
134
+ lib/presets.js
135
+ lib/spawn-prechecks.js
136
+ lib/spawn-support.js
137
+ lib/stateFile.js
138
+ lib/store.js
139
+ lib/tests.js
140
+ lib/tool-registry.js
141
+ lib/workspaceLock.js
142
+ lib/worktreeOwnership.js
143
+ locale/en.json
144
+ locale/zh.json
145
+ package.json
146
+ ports/clock.js
147
+ ports/ids.js
148
+ ports/index.js
149
+ ports/store.js
150
+ ports/vcs.js
151
+ reviewer.js
152
+ scripts/architecture.mjs
153
+ scripts/check-docs.mjs
154
+ scripts/check-pack.mjs
155
+ scripts/domain-metrics.mjs
156
+ scripts/enable-dev-hmr.mjs
157
+ scripts/generate-config-reference.mjs
158
+ scripts/generate-error-reference.mjs
159
+ scripts/generate-tools-reference.mjs
160
+ scripts/issue-audit.mjs
161
+ scripts/lib/cli.mjs
162
+ scripts/lib/tmp.mjs
163
+ scripts/mount-smoke.mjs
164
+ scripts/mount-smoke/fixtures.mjs
165
+ scripts/mount-smoke/host.json
166
+ scripts/mount-smoke/probe.mjs
167
+ scripts/mount-smoke/surface.mjs
168
+ scripts/pack-manifest.txt
169
+ scripts/release-check.mjs
170
+ skills.js
171
+ skills/tower-recovery.md
172
+ skills/tower-review.md
173
+ skills/tower-worker-protocol.md
174
+ tools/adopt.js
175
+ tools/batch.js
176
+ tools/complete.js
177
+ tools/diff.js
178
+ tools/doctor.js
179
+ tools/finding.js
180
+ tools/findings.js
181
+ tools/import-legacy.js
182
+ tools/inbox.js
183
+ tools/index.js
184
+ tools/init.js
185
+ tools/interrupt.js
186
+ tools/legacy-map.js
187
+ tools/merge.js
188
+ tools/mission.js
189
+ tools/plan.js
190
+ tools/preflight.js
191
+ tools/rebase.js
192
+ tools/recover.js
193
+ tools/reload.js
194
+ tools/resource.js
195
+ tools/review.js
196
+ tools/roster.js
197
+ tools/send.js
198
+ tools/spawn.js
199
+ tools/status.js
200
+ tools/teardown.js
201
+ tools/test.js
202
+ types/cordis.d.ts
203
+ types/index.d.ts
204
+ ui-bundle/README.md
205
+ ui-bundle/client.js
206
+ ui-bundle/cordis.patch.yml
207
+ ui-bundle/icon.svg
208
+ ui-bundle/index.js
209
+ ui-bundle/locale/en.json
210
+ ui-bundle/locale/zh.json
211
+ ui-bundle/package.json
212
+ worker.js