dsh-tower 2.1.0-beta.1 → 2.3.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 (188) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/COMPATIBILITY.md +6 -1
  3. package/CONTRIBUTING.md +71 -5
  4. package/PRINCIPLES.md +145 -0
  5. package/README.en.md +14 -5
  6. package/README.md +14 -5
  7. package/SECURITY.md +6 -0
  8. package/adapters/archive.js +279 -0
  9. package/adapters/caseSensitivity.js +66 -0
  10. package/adapters/default.js +51 -8
  11. package/{lib → adapters}/git.js +1 -1
  12. package/{lib → adapters}/leases.js +15 -6
  13. package/{lib → adapters}/legacy.js +20 -6
  14. package/adapters/persistence.js +543 -0
  15. package/adapters/stateFile.js +99 -0
  16. package/adapters/store-fs.js +28 -1
  17. package/adapters/store.js +204 -0
  18. package/{lib → adapters}/tests.js +15 -7
  19. package/adapters/vcs-git.js +15 -1
  20. package/{lib → adapters}/workspaceLock.js +1 -1
  21. package/{lib → adapters}/worktreeOwnership.js +18 -7
  22. package/app/archive.js +188 -0
  23. package/app/batch.js +9 -10
  24. package/app/board.js +4 -4
  25. package/app/checkCompletion.js +23 -11
  26. package/app/enrichedMissions.js +2 -4
  27. package/app/findings.js +6 -7
  28. package/app/gate.js +32 -15
  29. package/app/init.js +59 -22
  30. package/app/merge.js +64 -51
  31. package/app/messages.js +7 -8
  32. package/app/mission.js +7 -5
  33. package/app/plan.js +20 -9
  34. package/app/provision.js +12 -14
  35. package/app/read.js +65 -6
  36. package/app/rebase.js +14 -16
  37. package/app/recording.js +7 -8
  38. package/app/review.js +17 -8
  39. package/app/roster.js +14 -15
  40. package/{lib → app}/spawn-prechecks.js +4 -3
  41. package/app/spawn.js +12 -13
  42. package/app/teardown.js +28 -8
  43. package/app/tower.js +121 -39
  44. package/docs/README.md +6 -2
  45. package/docs/adr/0001-ports-adapters-as-data.md +49 -0
  46. package/docs/adr/0002-single-state-json.md +50 -0
  47. package/docs/adr/0003-allow-agent-control-default-true.md +48 -0
  48. package/docs/adr/0004-surface-budget.md +50 -0
  49. package/docs/adr/README.md +51 -0
  50. package/docs/agents/domain.md +38 -0
  51. package/docs/agents/issue-tracker.md +46 -0
  52. package/docs/agents/triage-labels.md +15 -0
  53. package/docs/agents/workflow.md +51 -0
  54. package/docs/architecture/overview.md +170 -37
  55. package/docs/guide/hot-reload.md +1 -1
  56. package/docs/guide/install-upgrade-migration.md +18 -1
  57. package/docs/guide/release.md +1 -1
  58. package/docs/operations/incident.md +82 -0
  59. package/docs/operations/runbook.md +12 -3
  60. package/docs/reference/blockers.md +3 -3
  61. package/docs/reference/config.generated.md +5 -0
  62. package/docs/reference/config.md +4 -0
  63. package/docs/reference/doctor.md +104 -8
  64. package/docs/reference/errors.generated.md +41 -35
  65. package/docs/reference/errors.md +54 -40
  66. package/docs/reference/events.md +38 -6
  67. package/docs/reference/surface-budget.md +128 -0
  68. package/docs/reference/tools.generated.md +9 -8
  69. package/docs/reference/tools.md +9 -8
  70. package/docs/zh/guide/hot-reload.md +1 -1
  71. package/docs/zh/guide/install-upgrade-migration.md +13 -1
  72. package/docs/zh/reference/blockers.md +3 -3
  73. package/docs/zh/reference/config.md +4 -0
  74. package/docs/zh/reference/doctor.md +63 -2
  75. package/docs/zh/reference/errors.md +4 -2
  76. package/docs/zh/reference/tools.md +6 -6
  77. package/domain/batch.js +2 -2
  78. package/domain/config/schema.js +51 -4
  79. package/domain/contracts/outputs.js +33 -3
  80. package/domain/contracts/tool-inputs/missions.js +2 -1
  81. package/domain/contracts/tool-inputs/ops.js +1 -0
  82. package/domain/errors.js +297 -40
  83. package/domain/events.js +207 -0
  84. package/domain/integration/gate.js +41 -12
  85. package/domain/integration/init.js +40 -4
  86. package/domain/integration/merge.js +35 -0
  87. package/domain/lockBudget.js +30 -0
  88. package/domain/mission.js +1 -1
  89. package/domain/missions/completion.js +72 -5
  90. package/domain/missions/lifecycle.js +11 -4
  91. package/domain/missions/plan.js +14 -0
  92. package/domain/missions/review.js +90 -7
  93. package/domain/planning.js +6 -3
  94. package/domain/read/views.js +17 -1
  95. package/domain/report.js +9 -1
  96. package/domain/scope.js +83 -11
  97. package/domain/state/archive.js +257 -0
  98. package/domain/state/migrations.js +145 -0
  99. package/domain/state/schema.js +6 -0
  100. package/domain/state.js +154 -4
  101. package/domain/summary.js +2 -0
  102. package/domain/surface.js +248 -0
  103. package/host/doctor.js +555 -0
  104. package/{lib → host}/modeStore.js +1 -1
  105. package/{lib → host}/tool-registry.js +106 -31
  106. package/host/tower.js +32 -0
  107. package/index.js +12 -9
  108. package/package.json +10 -3
  109. package/ports/archive.js +18 -0
  110. package/ports/caseSensitivity.js +15 -0
  111. package/ports/clock.js +10 -2
  112. package/ports/config.js +19 -0
  113. package/ports/contract.js +38 -0
  114. package/ports/events.js +24 -0
  115. package/ports/ids.js +10 -2
  116. package/ports/index.js +54 -2
  117. package/ports/ownership.js +17 -0
  118. package/ports/state.js +16 -0
  119. package/ports/store.js +52 -3
  120. package/ports/vcs.js +25 -4
  121. package/scripts/architecture-waivers.json +19 -0
  122. package/scripts/architecture.mjs +634 -23
  123. package/scripts/check-docs.mjs +8 -35
  124. package/scripts/check-pack.mjs +68 -39
  125. package/scripts/check-philosophy.mjs +266 -0
  126. package/scripts/domain-metrics.mjs +13 -4
  127. package/scripts/generate-config-reference.mjs +14 -1
  128. package/scripts/generate-error-reference.mjs +68 -14
  129. package/scripts/generate-events-reference.mjs +83 -0
  130. package/scripts/generate-tools-reference.mjs +11 -0
  131. package/scripts/issue-audit.mjs +2 -2
  132. package/scripts/lib/surface.mjs +87 -0
  133. package/scripts/pack-manifest.txt +64 -19
  134. package/scripts/regression-audit.mjs +164 -0
  135. package/scripts/release-check.mjs +1 -1
  136. package/scripts/surface-baseline.json +21 -0
  137. package/scripts/surface-baseline.mjs +120 -0
  138. package/tools/adopt.js +2 -3
  139. package/tools/batch.js +2 -3
  140. package/tools/complete.js +3 -4
  141. package/tools/diff.js +3 -3
  142. package/tools/doctor.js +7 -10
  143. package/tools/finding.js +2 -3
  144. package/tools/findings.js +3 -3
  145. package/tools/import-legacy.js +3 -5
  146. package/tools/inbox.js +3 -3
  147. package/tools/index.js +7 -7
  148. package/tools/init.js +4 -7
  149. package/tools/interrupt.js +2 -3
  150. package/tools/legacy-map.js +3 -5
  151. package/tools/merge.js +3 -3
  152. package/tools/mission.js +2 -3
  153. package/tools/plan.js +7 -6
  154. package/tools/preflight.js +3 -3
  155. package/tools/rebase.js +3 -3
  156. package/tools/recover.js +38 -13
  157. package/tools/reload.js +3 -6
  158. package/tools/resource.js +3 -5
  159. package/tools/review.js +3 -4
  160. package/tools/roster.js +2 -3
  161. package/tools/send.js +2 -2
  162. package/tools/spawn.js +8 -5
  163. package/tools/status.js +16 -8
  164. package/tools/teardown.js +3 -4
  165. package/tools/test.js +2 -2
  166. package/types/index.d.ts +35 -1
  167. package/ui-bundle/README.md +37 -3
  168. package/ui-bundle/client.cards.js +356 -0
  169. package/ui-bundle/client.denied.js +142 -0
  170. package/ui-bundle/client.dock.js +333 -0
  171. package/ui-bundle/client.helpers.js +271 -0
  172. package/ui-bundle/client.js +79 -1747
  173. package/ui-bundle/client.locales.js +247 -0
  174. package/ui-bundle/client.mode.js +206 -0
  175. package/ui-bundle/client.overlay.js +64 -0
  176. package/ui-bundle/client.styles.js +151 -0
  177. package/ui-bundle/client.toolview.js +82 -0
  178. package/ui-bundle/package.json +1 -0
  179. package/lib/doctor.js +0 -270
  180. package/lib/persistence.js +0 -349
  181. package/lib/store.js +0 -118
  182. /package/{lib → app}/briefing.js +0 -0
  183. /package/{lib → app}/spawn-support.js +0 -0
  184. /package/{lib → host}/compatibility.js +0 -0
  185. /package/{lib → host}/config.js +0 -0
  186. /package/{lib → host}/hmr.js +0 -0
  187. /package/{lib → host}/modeState.js +0 -0
  188. /package/{lib → host}/presets.js +0 -0
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ // Concept-budget gate (#84): compare the live surface with the committed
3
+ // baseline and require evidence for growth.
4
+ //
5
+ // The surface is three counts the user has to hold at once:
6
+ // config keys leaf keys in domain/config/schema.js (one level deep)
7
+ // sections object groups in that schema
8
+ // tools tool definitions registered by host/tool-registry.js
9
+ //
10
+ // `scripts/surface-baseline.json` freezes those counts. A PR that grows a count
11
+ // fails here until the baseline carries a decision with either an ADR under
12
+ // docs/adr/ or deletions that no longer exist in the live surface. The same
13
+ // file supplies the date and counts printed in the generated config/tools
14
+ // reference headers, so growth is visible in a diff.
15
+ //
16
+ // Usage:
17
+ // node scripts/surface-baseline.mjs [--root <dir>] [--json]
18
+ import fs from 'node:fs'
19
+ import path from 'node:path'
20
+ import { fileURLToPath } from 'node:url'
21
+ import { configSurface } from '../domain/config/schema.js'
22
+ import { SURFACE_CLASSIFICATION_FILE, SURFACE_BASELINE_FILE, surfaceLine } from '../domain/surface.js'
23
+ import { registerTowerTools } from '../tools/index.js'
24
+ import { definedToolNames } from '../host/tool-registry.js'
25
+ import { checkSurfaceBudget } from './lib/surface.mjs'
26
+ import { parseArgs, runMain } from './lib/cli.mjs'
27
+
28
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
29
+ const USAGE = `usage: node scripts/surface-baseline.mjs [--root <dir>] [--json]
30
+
31
+ --root repository root to check (default: this package)
32
+ --json print the report as JSON
33
+ --help print this text
34
+
35
+ Growth is refused by default. To grow a count, update ${SURFACE_BASELINE_FILE}
36
+ with a decision that names an existing docs/adr/ ADR or deletions that are no
37
+ longer in the live surface. The classification table in
38
+ ${SURFACE_CLASSIFICATION_FILE} must cover every key and tool exactly once.`
39
+
40
+ /** Register the real tool surface against a minimal mock context. */
41
+ function liveToolNames() {
42
+ const ctx = {
43
+ get() {
44
+ return undefined
45
+ },
46
+ effect(fn) {
47
+ const dispose = fn()
48
+ return typeof dispose === 'function' ? dispose : () => {}
49
+ },
50
+ systemPrompt: { section() {} },
51
+ tools: {
52
+ register() {
53
+ return () => {}
54
+ },
55
+ guard() {
56
+ return () => {}
57
+ },
58
+ restrict() {
59
+ return () => {}
60
+ },
61
+ },
62
+ }
63
+ registerTowerTools(ctx)
64
+ return definedToolNames()
65
+ }
66
+
67
+ /**
68
+ * The live surface used by the gate. Exported so tests can compare the counts
69
+ * with the generated references without spawning the CLI.
70
+ */
71
+ export function liveSurface() {
72
+ const config = configSurface()
73
+ const tools = liveToolNames()
74
+ return {
75
+ configKeys: config.count,
76
+ sections: config.sections.length,
77
+ tools: tools.length,
78
+ liveNames: {
79
+ configKeys: config.keys,
80
+ sections: config.sections,
81
+ tools,
82
+ },
83
+ }
84
+ }
85
+
86
+ function main(argv) {
87
+ const options = parseArgs(argv, {
88
+ initial: { root: PACKAGE_ROOT, json: false, help: false },
89
+ values: { '--root': { key: 'root', parse: path.resolve } },
90
+ flags: { '--json': 'json', '--help': 'help', '-h': 'help' },
91
+ })
92
+ if (options.help) {
93
+ console.log(USAGE)
94
+ return 0
95
+ }
96
+ const root = path.resolve(options.root)
97
+ if (!fs.existsSync(path.join(root, SURFACE_BASELINE_FILE))) {
98
+ console.error(`surface: ${SURFACE_BASELINE_FILE} is missing under ${root}`)
99
+ return 2
100
+ }
101
+ const current = liveSurface()
102
+ const result = checkSurfaceBudget({ root, current })
103
+ if (options.json) {
104
+ console.log(JSON.stringify({ root, current, ...result }, null, 2))
105
+ return result.ok ? 0 : 1
106
+ }
107
+ console.log(`surface: ${surfaceLine(current)}`)
108
+ if (result.baseline) console.log(`surface: baseline ${result.baseline.date} (${SURFACE_BASELINE_FILE})`)
109
+ if (result.classification) {
110
+ const configRows = result.classification.entries.filter((entry) => entry.kind === 'config').length
111
+ const toolRows = result.classification.entries.filter((entry) => entry.kind === 'tools').length
112
+ console.log(`surface: classified ${configRows} config keys and ${toolRows} tools (${SURFACE_CLASSIFICATION_FILE})`)
113
+ }
114
+ for (const finding of result.findings) console.log(`surface: ${finding.level} [${finding.code}] ${finding.message}`)
115
+ if (result.ok) console.log('surface: ok — counts match the committed baseline and every entry is classified')
116
+ else console.log(`surface: ${result.findings.length} problem(s); growth needs an ADR or a matching deletion`)
117
+ return result.ok ? 0 : 1
118
+ }
119
+
120
+ await runMain(import.meta.url, () => main(process.argv.slice(2)), { errorPrefix: 'surface', usage: USAGE })
package/tools/adopt.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // tower_adopt: reconcile the roster with the subagent catalog.
2
- import { Tower } from '../app/tower.js'
3
2
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
4
3
  import { TowerToolError } from '../domain/errors.js'
5
4
  import { AdoptInput } from '../domain/contracts/tool-inputs/index.js'
@@ -7,7 +6,7 @@ import { PARENT_STATUS_OK, ROSTER_ACTIVITY, ROSTER_STATE } from '../domain/vocab
7
6
 
8
7
  /**
9
8
  * @param {any} ctx
10
- * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
9
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any }} deps
11
10
  */
12
11
  export function registerAdoptTool(ctx, deps) {
13
12
  const { define, actorFor, requireTowerActor } = deps
@@ -19,7 +18,7 @@ export function registerAdoptTool(ctx, deps) {
19
18
  aliases: { retire_missing: ['retireMissing'] },
20
19
  output: output(SCHEMAS.adopt),
21
20
  async execute(args, exec) {
22
- const tower = await Tower.open(args.repoRoot)
21
+ const tower = await deps.openTower(args.repoRoot)
23
22
  const actor = await actorFor(tower, exec)
24
23
  requireTowerActor(actor, 'adopt the roster')
25
24
  const view = await tower.rosterView()
package/tools/batch.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // tower_batch: finite evidence batches.
2
- import { Tower } from '../app/tower.js'
3
2
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
4
3
  import { TowerToolError } from '../domain/errors.js'
5
4
  import { BatchInput } from '../domain/contracts/tool-inputs/index.js'
@@ -7,7 +6,7 @@ import { BATCH_ACTION } from '../domain/vocabulary.js'
7
6
 
8
7
  /**
9
8
  * @param {any} ctx
10
- * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, summarizeBatch: any, verifyBatchReceiptFiles: any }} deps
9
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, summarizeBatch: any, verifyBatchReceiptFiles: any }} deps
11
10
  */
12
11
  export function registerBatchTool(ctx, deps) {
13
12
  const { define, actorFor, requireTowerActor, summarizeBatch, verifyBatchReceiptFiles } = deps
@@ -19,7 +18,7 @@ export function registerBatchTool(ctx, deps) {
19
18
  board: true,
20
19
  output: output(SCHEMAS.batch),
21
20
  async execute(args, exec) {
22
- const tower = await Tower.open(args.repoRoot)
21
+ const tower = await deps.openTower(args.repoRoot)
23
22
  const actor = await actorFor(tower, exec)
24
23
  if (args.action === BATCH_ACTION.status) {
25
24
  const state = await tower.load()
package/tools/complete.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // tower_complete: worker completion admission.
2
- import { Tower } from '../app/tower.js'
3
2
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
4
3
  import { TowerToolError } from '../domain/errors.js'
5
4
  import { CompleteInput } from '../domain/contracts/tool-inputs/index.js'
@@ -7,21 +6,21 @@ import { isCompleteAdmission } from '../domain/vocabulary.js'
7
6
 
8
7
  /**
9
8
  * @param {any} ctx
10
- * @param {{ define: import('../types/index.js').DefineTool, runMissionDiscovery: any }} deps
9
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, runMissionDiscovery: any }} deps
11
10
  */
12
11
  export function registerCompleteTool(ctx, deps) {
13
12
  const { define, runMissionDiscovery } = deps
14
13
  define(ctx, {
15
14
  name: 'tower_complete',
16
15
  description:
17
- 'Worker-only: report mission completion. Enforced by the store: every task done or dropped, no blockers, the current owner, the mission branch has at least one commit since its spawn base, and no unread inbox messages. checks records verifiable evidence (name/command/exitCode/evidence); gates.requireChecks can require at least one before merge. Stores the full report; completion means ready for preflight and optional review, not merged.',
16
+ 'Worker-only: report mission completion. Enforced by the store: every task done or dropped, no blockers, the current owner, the mission branch has at least one commit since its spawn base, and no unread inbox messages. checks records verifiable evidence (name/command/exitCode/evidence); gates.requireChecks can require at least one before merge. Stores the full report, capped at gates.maxReportBytes (default 32768 UTF-8 bytes); a larger report is rejected with invalid-argument and an actionable nextAction, never truncated. Persists reportBytes on the stored report. completion means ready for preflight and optional review, not merged.',
18
17
  parameters: CompleteInput,
19
18
  aliases: {"missionId":["id"]},
20
19
  requiresMissionId: true,
21
20
  board: true,
22
21
  output: output(SCHEMAS.complete),
23
22
  async execute(args, exec) {
24
- const tower = await Tower.open(args.repoRoot)
23
+ const tower = await deps.openTower(args.repoRoot)
25
24
  // Validate authorization/state before executing repository-controlled
26
25
  // test code or writing a test receipt.
27
26
  await tower.checkCompletion({
package/tools/diff.js CHANGED
@@ -13,15 +13,15 @@
13
13
  // The patch cap is enforced by the pure read projection.
14
14
  // See docs/reference/tools.generated.md for the public shape.
15
15
  // tower_diff: read-only mission branch view.
16
- import { Tower } from '../app/tower.js'
17
16
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
18
17
  import { DiffInput } from '../domain/contracts/tool-inputs/index.js'
19
18
 
20
19
  /**
21
20
  * @param {any} ctx
22
- * @param {{ define: import('../types/index.js').DefineTool }} deps
21
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any }} deps
23
22
  */
24
23
  export function registerDiffTool(ctx, deps) {
24
+ // openTower is injected by the composition root (#69).
25
25
  const { define } = deps
26
26
  define(ctx, {
27
27
  name: 'tower_diff',
@@ -32,7 +32,7 @@ export function registerDiffTool(ctx, deps) {
32
32
  requiresMissionId: true,
33
33
  output: output(SCHEMAS.diff),
34
34
  async execute(args) {
35
- const tower = await Tower.open(args.repoRoot)
35
+ const tower = await deps.openTower(args.repoRoot)
36
36
  return tower.diff({ missionId: args.missionId, maxBytes: args.maxBytes })
37
37
  },
38
38
  })
package/tools/doctor.js CHANGED
@@ -1,36 +1,33 @@
1
1
  // tower_doctor: read-only health check.
2
- import { Tower } from '../app/tower.js'
3
- import { resolveRepo } from '../lib/git.js'
4
- import { runtimeConfig } from '../lib/config.js'
5
- import { runDoctor } from '../lib/doctor.js'
6
2
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
7
3
  import { DoctorInput } from '../domain/contracts/tool-inputs/index.js'
8
4
  import { DOCTOR_STATUS } from '../domain/vocabulary.js'
9
5
 
10
6
  /**
11
7
  * @param {any} ctx
12
- * @param {import('../types/index.js').DefineTool} define
8
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, createTower: any, resolveRepo: any, runtimeConfig: any, runDoctor: any, surface?: () => { tools?: number } }} deps
13
9
  */
14
- export function registerDoctorTool(ctx, define) {
10
+ export function registerDoctorTool(ctx, deps) {
11
+ const { define, createTower, resolveRepo, runtimeConfig, runDoctor } = deps
15
12
  define(ctx, {
16
13
  name: 'tower_doctor',
17
14
  description:
18
- 'Read-only environment and repository health check: git, state schema, config, presets/subagents/HMR, workspace lock, roster staleness, worktree ownership, test discovery, leases. Returns checks with pass/warn/fail and an environment contract. Never writes state.',
15
+ 'Read-only\ environment\ and\ repository\ health\ check:\ the\ concept-budget\ surface\ \(config\ keys/sections/tools\),\ git,\ state\ schema,\ case-sensitivity\ detection\ \(caseInsensitive\ and\ the\ active\ scope\ comparison\ mode\),\ config,\ presets/subagents/HMR,\ the\ state\ lock\ age/heartbeat\ freshness,\ the\ repo-scoped\ git\ lock\ holder\ \(gitLock:\ \{held,pid,phase,heldMs\}\),\ workspace\ lock,\ roster\ staleness,\ worktree\ ownership,\ test\ discovery,\ leases,\ and\ the\ terminal-mission\ archive\ \(count,\ bytes,\ years,\ unreadable\ diagnostics\ and\ the\ configured\ afterDays/maxMissions\ policy\)\.\ Returns\ checks\ with\ pass/warn/fail,\ state-lock\ and\ git-lock\ reports\ and\ an\ environment\ contract\.\ Never\ writes\ state\ and\ never\ probes\ the\ filesystem\.',
19
16
  parameters: DoctorInput,
20
17
  output: output(SCHEMAS.doctor),
21
18
  async execute(args, exec) {
22
19
  let tower
23
20
  let configError = null
24
21
  try {
25
- tower = await Tower.open(args.repoRoot)
22
+ tower = await deps.openTower(args.repoRoot)
26
23
  } catch (error) {
27
24
  const message = String(error?.message ?? error)
28
25
  if (error?.code !== 'config-invalid' && error?.code !== 'config-unknown-key') throw error
29
26
  configError = message
30
27
  const repo = await resolveRepo(args.repoRoot)
31
- tower = new Tower(repo, { ...runtimeConfig(), configPath: null, repoConfigPresent: true })
28
+ tower = deps.createTower(repo, { ...runtimeConfig(), configPath: null, repoConfigPresent: true })
32
29
  }
33
- const report = await runDoctor(/** @type {any} */ ({ tower, ctx, agent: exec.agent }))
30
+ const report = await runDoctor(/** @type {any} */ ({ tower, ctx, agent: exec.agent, surface: deps.surface?.() }))
34
31
  if (!configError) return report
35
32
  return {
36
33
  ...report,
package/tools/finding.js CHANGED
@@ -1,12 +1,11 @@
1
1
  // tower_finding: file one structured finding.
2
- import { Tower } from '../app/tower.js'
3
2
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
4
3
  import { FindingInput } from '../domain/contracts/tool-inputs/index.js'
5
4
  import { ROLE } from '../domain/vocabulary.js'
6
5
 
7
6
  /**
8
7
  * @param {any} ctx
9
- * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireKnownActor: any }} deps
8
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireKnownActor: any }} deps
10
9
  */
11
10
  export function registerFindingTool(ctx, deps) {
12
11
  const { define, actorFor, requireKnownActor } = deps
@@ -19,7 +18,7 @@ export function registerFindingTool(ctx, deps) {
19
18
  board: true,
20
19
  output: output(SCHEMAS.finding),
21
20
  async execute(args, exec) {
22
- const tower = await Tower.open(args.repoRoot)
21
+ const tower = await deps.openTower(args.repoRoot)
23
22
  const actor = await actorFor(tower, exec)
24
23
  requireKnownActor(actor, 'tower_finding')
25
24
  const view = await tower.rosterView()
package/tools/findings.js CHANGED
@@ -1,14 +1,14 @@
1
1
  // tower_findings: list, read, or update findings.
2
- import { Tower } from '../app/tower.js'
3
2
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
4
3
  import { FindingsInput } from '../domain/contracts/tool-inputs/index.js'
5
4
  import { ROLE } from '../domain/vocabulary.js'
6
5
 
7
6
  /**
8
7
  * @param {any} ctx
9
- * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireKnownActor: any }} deps
8
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireKnownActor: any }} deps
10
9
  */
11
10
  export function registerFindingsTool(ctx, deps) {
11
+ // openTower is injected by the composition root (#69).
12
12
  const { define, actorFor, requireKnownActor } = deps
13
13
  define(ctx, {
14
14
  name: 'tower_findings',
@@ -18,7 +18,7 @@ export function registerFindingsTool(ctx, deps) {
18
18
  aliases: {"missionId":["mission_id"]},
19
19
  output: output(SCHEMAS.findings),
20
20
  async execute(args, exec) {
21
- const tower = await Tower.open(args.repoRoot)
21
+ const tower = await deps.openTower(args.repoRoot)
22
22
  const actor = await actorFor(tower, exec)
23
23
  requireKnownActor(actor, 'tower_findings')
24
24
  const view = await tower.rosterView()
@@ -8,17 +8,15 @@
8
8
  // Pure legacy validation lives in domain and never runs fs.
9
9
  // See docs/reference/tools.generated.md for the public shape.
10
10
  // tower_import_legacy: read-only legacy snapshot import.
11
- import { Tower } from '../app/tower.js'
12
11
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
13
- import { importLegacySnapshot } from '../lib/legacy.js'
14
12
  import { ImportLegacyInput } from '../domain/contracts/tool-inputs/index.js'
15
13
 
16
14
  /**
17
15
  * @param {any} ctx
18
- * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
16
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, importLegacySnapshot: any }} deps
19
17
  */
20
18
  export function registerImportLegacyTool(ctx, deps) {
21
- const { define, actorFor, requireTowerActor } = deps
19
+ const { define, actorFor, requireTowerActor, importLegacySnapshot } = deps
22
20
  define(ctx, {
23
21
  name: 'tower_import_legacy',
24
22
  description:
@@ -27,7 +25,7 @@ export function registerImportLegacyTool(ctx, deps) {
27
25
  board: true,
28
26
  output: output(SCHEMAS.importLegacy),
29
27
  async execute(args, exec) {
30
- const tower = await Tower.open(args.repoRoot)
28
+ const tower = await deps.openTower(args.repoRoot)
31
29
  const actor = await actorFor(tower, exec)
32
30
  requireTowerActor(actor, 'import a legacy snapshot')
33
31
  const imported = await importLegacySnapshot({
package/tools/inbox.js CHANGED
@@ -10,16 +10,16 @@
10
10
  // The unread calculation is a pure domain/collab projection.
11
11
  // See docs/reference/tools.generated.md for the public shape.
12
12
  // tower_inbox: read and acknowledge inbound messages.
13
- import { Tower } from '../app/tower.js'
14
13
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
15
14
  import { InboxInput } from '../domain/contracts/tool-inputs/index.js'
16
15
  import { ROLE } from '../domain/vocabulary.js'
17
16
 
18
17
  /**
19
18
  * @param {any} ctx
20
- * @param {{ define: import('../types/index.js').DefineTool }} deps
19
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any }} deps
21
20
  */
22
21
  export function registerInboxTool(ctx, deps) {
22
+ // openTower is injected by the composition root (#69).
23
23
  const { define } = deps
24
24
  define(ctx, {
25
25
  name: 'tower_inbox',
@@ -29,7 +29,7 @@ export function registerInboxTool(ctx, deps) {
29
29
  aliases: {"include_read":["includeRead"]},
30
30
  output: output(SCHEMAS.inbox),
31
31
  async execute(args, exec) {
32
- const tower = await Tower.open(args.repoRoot)
32
+ const tower = await deps.openTower(args.repoRoot)
33
33
  const view = await tower.rosterView()
34
34
  const self = view.roster.find((entry) => entry.agentId === String(exec.agent.id))
35
35
  const reader = self ? self.name : ROLE.tower
package/tools/index.js CHANGED
@@ -9,9 +9,9 @@
9
9
  // - tools must not import node:fs or node:child_process directly.
10
10
  //
11
11
  // Tests import this module for the public surface; they may also
12
- // import lib/tool-registry.js for low-level test hooks.
12
+ // import host/tool-registry.js for low-level test hooks.
13
13
  //
14
- // Packaging: package.json ships tools/*.js and lib/tool-registry.js.
14
+ // Packaging: package.json ships tools/*.js and host/tool-registry.js.
15
15
  // The file intentionally stays tiny so handler modules remain the
16
16
  // only place where a tool is defined.
17
17
  // Keep this comment and the re-export list in sync when adding tools.
@@ -20,7 +20,7 @@
20
20
  //
21
21
  // The registration aggregator, guards, rate limits, spawn backoff,
22
22
  // repository preparation, discovery helpers, and verb helpers live in
23
- // lib/tool-registry.js. This module only re-exports the public entry
23
+ // host/tool-registry.js. This module only re-exports the public entry
24
24
  // points consumed by the Host plugin, tests, and docs generators.
25
25
  //
26
26
  // Design rules:
@@ -30,10 +30,10 @@
30
30
  // - no business logic in this file;
31
31
  // - keep the surface stable for index.js and tests.
32
32
  //
33
- // See docs/architecture/overview.md and lib/tool-registry.js.
33
+ // See docs/architecture/overview.md and host/tool-registry.js.
34
34
  // Tower tool surface: thin public wiring (#56 P5).
35
- // Registration and shared machinery live in lib/tool-registry.js; one handler
35
+ // Registration and shared machinery live in host/tool-registry.js; one handler
36
36
  // module per tool lives under tools/. This file only exposes the public entry
37
37
  // points consumed by the Host plugin, tests, and documentation generators.
38
- export { registerTowerTools, __testSpawnBackoff } from '../lib/tool-registry.js'
39
- export { workerBriefing, reviewerBriefing } from '../lib/briefing.js'
38
+ export { registerTowerTools, __testSpawnBackoff } from '../host/tool-registry.js'
39
+ export { workerBriefing, reviewerBriefing } from '../app/briefing.js'
package/tools/init.js CHANGED
@@ -1,22 +1,19 @@
1
1
  // tower_init: repository initialization.
2
- import { Tower } from '../app/tower.js'
3
2
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
4
3
  import { TowerToolError } from '../domain/errors.js'
5
- import { resolveRepo } from '../lib/git.js'
6
- import { runtimeConfig } from '../lib/config.js'
7
4
  import { InitInput } from '../domain/contracts/tool-inputs/index.js'
8
5
  import { isTowerRole, isUnknownRole } from '../domain/vocabulary.js'
9
6
 
10
7
  /**
11
8
  * @param {any} ctx
12
- * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, workspaceHook: any, ensureRepository: any }} deps
9
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, workspaceHook: any, ensureRepository: any, resolveRepo: any, runtimeConfig: any }} deps
13
10
  */
14
11
  export function registerInitTool(ctx, deps) {
15
- const { define, actorFor, requireTowerActor, workspaceHook, ensureRepository } = deps
12
+ const { define, actorFor, requireTowerActor, workspaceHook, ensureRepository, resolveRepo, runtimeConfig } = deps
16
13
  define(ctx, {
17
14
  name: 'tower_init',
18
15
  description:
19
- 'Initialize Tower mission state for a repository (absolute repoRoot). Creates <git-common-dir>/dsh-tower/state.json and an activity log; idempotent on an existing workspace. Options: autoGitInit (git init + empty initial commit when the directory is not a repository), snapshotDirty (snapshot uncommitted work into a dangling commit used as the mission baseline), reanchor (change base when every mission is merged/abandoned). Only the tower may initialize.',
16
+ 'Initialize Tower mission state for a repository (absolute repoRoot). Creates <git-common-dir>/dsh-tower/state.json and an activity log; idempotent on an existing workspace. Probes whether the checkout filesystem folds case and records caseInsensitive once, so scope matching follows the real filesystem (a skipped probe records null and keeps the safe case-sensitive comparison). Options: autoGitInit (git init + empty initial commit when the directory is not a repository), snapshotDirty (snapshot uncommitted work into a dangling commit used as the mission baseline), reanchor (change base when every mission is merged/abandoned). Only the tower may initialize.',
20
17
  parameters: InitInput,
21
18
  aliases: { auto_git_init: ['autoGitInit'], snapshot_dirty: ['snapshotDirty'] },
22
19
  board: true,
@@ -31,7 +28,7 @@ export function registerInitTool(ctx, deps) {
31
28
  args.autoGitInit === true || (args.autoGitInit === undefined && profileInit.autoGitInit === true)
32
29
  repo = await ensureRepository(args.repoRoot, autoGitInit)
33
30
  }
34
- const tower = await Tower.open(repo.root)
31
+ const tower = await deps.openTower(repo.root)
35
32
  const initConfig = tower.config.init ?? {}
36
33
  const snapshotDirty =
37
34
  args.snapshotDirty === true || (args.snapshotDirty === undefined && initConfig.snapshotDirty === true)
@@ -8,14 +8,13 @@
8
8
  // Roster state policy is pure and lives in domain/collab.
9
9
  // See docs/reference/tools.generated.md for the public shape.
10
10
  // tower_interrupt: interrupt one running roster agent.
11
- import { Tower } from '../app/tower.js'
12
11
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
13
12
  import { TowerToolError } from '../domain/errors.js'
14
13
  import { InterruptInput } from '../domain/contracts/tool-inputs/index.js'
15
14
 
16
15
  /**
17
16
  * @param {any} ctx
18
- * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, interruptRosterAgent: any }} deps
17
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, interruptRosterAgent: any }} deps
19
18
  */
20
19
  export function registerInterruptTool(ctx, deps) {
21
20
  const { define, actorFor, requireTowerActor, interruptRosterAgent } = deps
@@ -27,7 +26,7 @@ export function registerInterruptTool(ctx, deps) {
27
26
  aliases: { agentId: ['agent_id'] },
28
27
  output: output(SCHEMAS.interrupt),
29
28
  async execute(args, exec) {
30
- const tower = await Tower.open(args.repoRoot)
29
+ const tower = await deps.openTower(args.repoRoot)
31
30
  const actor = await actorFor(tower, exec)
32
31
  requireTowerActor(actor, 'interrupt tower agents')
33
32
  if (!args.to && !args.agentId) throw new TowerToolError('invalid-argument', 'tower_interrupt requires to (roster name) or agentId')
@@ -8,19 +8,17 @@
8
8
  // Pure mapping validation lives in the domain legacy layer.
9
9
  // See docs/reference/tools.generated.md for the public shape.
10
10
  // tower_legacy_map: audit-only legacy snapshot mapping.
11
- import { Tower } from '../app/tower.js'
12
11
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
13
12
  import { TowerToolError } from '../domain/errors.js'
14
- import { validateLegacyMap } from '../lib/legacy.js'
15
13
  import { LegacyMapInput } from '../domain/contracts/tool-inputs/index.js'
16
14
  import { LEGACY_MAP_ACTION } from '../domain/vocabulary.js'
17
15
 
18
16
  /**
19
17
  * @param {any} ctx
20
- * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
18
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, validateLegacyMap: any }} deps
21
19
  */
22
20
  export function registerLegacyMapTool(ctx, deps) {
23
- const { define, actorFor, requireTowerActor } = deps
21
+ const { define, actorFor, requireTowerActor, validateLegacyMap } = deps
24
22
  define(ctx, {
25
23
  name: 'tower_legacy_map',
26
24
  description:
@@ -28,7 +26,7 @@ export function registerLegacyMapTool(ctx, deps) {
28
26
  parameters: LegacyMapInput,
29
27
  output: output(SCHEMAS.legacyMap),
30
28
  async execute(args, exec) {
31
- const tower = await Tower.open(args.repoRoot)
29
+ const tower = await deps.openTower(args.repoRoot)
32
30
  const actor = await actorFor(tower, exec)
33
31
  const state = await tower.load()
34
32
  if (args.action === LEGACY_MAP_ACTION.status) {
package/tools/merge.js CHANGED
@@ -9,15 +9,15 @@
9
9
  // The pure intent/shape decisions live in domain/integration.
10
10
  // See docs/reference/tools.generated.md for the public shape.
11
11
  // tower_merge: merge a mission branch into the recorded base.
12
- import { Tower } from '../app/tower.js'
13
12
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
14
13
  import { MergeInput } from '../domain/contracts/tool-inputs/index.js'
15
14
 
16
15
  /**
17
16
  * @param {any} ctx
18
- * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, observeActivity: any }} deps
17
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, observeActivity: any }} deps
19
18
  */
20
19
  export function registerMergeTool(ctx, deps) {
20
+ // openTower is injected by the composition root (#69).
21
21
  const { define, actorFor, requireTowerActor, observeActivity } = deps
22
22
  define(ctx, {
23
23
  name: 'tower_merge',
@@ -29,7 +29,7 @@ export function registerMergeTool(ctx, deps) {
29
29
  board: true,
30
30
  output: output(SCHEMAS.merge),
31
31
  async execute(args, exec) {
32
- const tower = await Tower.open(args.repoRoot)
32
+ const tower = await deps.openTower(args.repoRoot)
33
33
  const actor = await actorFor(tower, exec)
34
34
  requireTowerActor(actor, 'merge missions')
35
35
  const { activity, parentStatus } = await observeActivity(ctx, tower)
package/tools/mission.js CHANGED
@@ -1,12 +1,11 @@
1
1
  // tower_mission: mission lifecycle updates.
2
- import { Tower } from '../app/tower.js'
3
2
  import { TowerToolError } from '../domain/errors.js'
4
3
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
5
4
  import { MissionInput } from '../domain/contracts/tool-inputs/index.js'
6
5
 
7
6
  /**
8
7
  * @param {any} ctx
9
- * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireKnownActor: any }} deps
8
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireKnownActor: any }} deps
10
9
  */
11
10
  export function registerMissionTool(ctx, deps) {
12
11
  const { define, actorFor, requireKnownActor } = deps
@@ -19,7 +18,7 @@ export function registerMissionTool(ctx, deps) {
19
18
  board: true,
20
19
  output: output(SCHEMAS.mission),
21
20
  async execute(args, exec) {
22
- const tower = await Tower.open(args.repoRoot)
21
+ const tower = await deps.openTower(args.repoRoot)
23
22
  const actor = await actorFor(tower, exec)
24
23
  requireKnownActor(actor, 'tower_mission')
25
24
  return tower.mission({
package/tools/plan.js CHANGED
@@ -10,26 +10,27 @@
10
10
  // Pure normalization and DAG policy live in domain/missions.
11
11
  // See docs/reference/tools.generated.md for the public shape.
12
12
  // tower_plan: create missions.
13
- import { Tower } from '../app/tower.js'
14
13
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
15
- import { assertPresetsAvailable } from '../lib/presets.js'
16
14
  import { PlanInput } from '../domain/contracts/tool-inputs/index.js'
17
15
 
18
16
  /**
19
17
  * @param {any} ctx
20
- * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
18
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, assertPresetsAvailable: any }} deps
21
19
  */
22
20
  export function registerPlanTool(ctx, deps) {
23
- const { define, actorFor, requireTowerActor } = deps
21
+ // #70: preset validation is injected by the composition root, so this tool
22
+ // never imports host/ or adapters/ directly.
23
+ // openTower is injected by the composition root (#69).
24
+ const { define, actorFor, requireTowerActor, assertPresetsAvailable } = deps
24
25
  define(ctx, {
25
26
  name: 'tower_plan',
26
27
  description:
27
- 'Plan one or more missions in an initialized repository. Each mission has a printable-ASCII title, relative scope globs, optional task checklist, optional mission-id dependencies, and kind build|survey. Assigns M1.. ids and auto-adds a dependency when build scopes overlap (serializes merges); rejects dependency cycles.',
28
+ 'Plan one or more missions in an initialized repository. Each mission has a printable-Unicode title (NFC-normalized, at most 120 characters), relative scope globs, optional task checklist, optional mission-id dependencies, and kind build|survey. Assigns M1.. ids and auto-adds a dependency when build scopes overlap (serializes merges); rejects dependency cycles.',
28
29
  parameters: PlanInput,
29
30
  board: true,
30
31
  output: output(SCHEMAS.plan),
31
32
  async execute(args, exec) {
32
- const tower = await Tower.open(args.repoRoot)
33
+ const tower = await deps.openTower(args.repoRoot)
33
34
  const actor = await actorFor(tower, exec)
34
35
  requireTowerActor(actor, 'plan missions')
35
36
  await assertPresetsAvailable(ctx, tower.config)
@@ -1,12 +1,11 @@
1
1
  // tower_preflight: read-only merge gate digest.
2
- import { Tower } from '../app/tower.js'
3
2
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
4
3
  import { classifyError } from '../domain/errors.js'
5
4
  import { PreflightInput } from '../domain/contracts/tool-inputs/index.js'
6
5
 
7
6
  /**
8
7
  * @param {any} ctx
9
- * @param {{ define: import('../types/index.js').DefineTool, observeActivity: any }} deps
8
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, observeActivity: any }} deps
10
9
  */
11
10
  export function registerPreflightTool(ctx, deps) {
12
11
  const { define, observeActivity } = deps
@@ -19,7 +18,7 @@ export function registerPreflightTool(ctx, deps) {
19
18
  requiresMissionId: true,
20
19
  output: output(SCHEMAS.preflight),
21
20
  async execute(args) {
22
- const tower = await Tower.open(args.repoRoot)
21
+ const tower = await deps.openTower(args.repoRoot)
23
22
  const { activity, parentStatus } = await observeActivity(ctx, tower)
24
23
  const gate = await tower.evaluateGate({ missionId: args.missionId, activity, parentStatus })
25
24
  return {
@@ -38,6 +37,7 @@ export function registerPreflightTool(ctx, deps) {
38
37
  reviewedCommit: gate.review.reviewedCommit,
39
38
  }
40
39
  : null,
40
+ ...(gate.archived ? { archived: true } : {}),
41
41
  }
42
42
  },
43
43
  })