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
@@ -14,8 +14,15 @@ import {
14
14
  isTowerRole,
15
15
  isUnknownRole,
16
16
  } from '../domain/vocabulary.js'
17
- import { git, resolveRepo, hasAnyCommit } from './git.js'
17
+ import { git, resolveRepo, hasAnyCommit } from '../adapters/git.js'
18
18
  import { runtimeConfig } from './config.js'
19
+ import { runDoctor } from './doctor.js'
20
+ import { createHmrBridge } from './hmr.js'
21
+ import { assertPresetsAvailable } from './presets.js'
22
+ import { readStateLock } from '../adapters/store.js'
23
+ import { lockTiming } from '../adapters/persistence.js'
24
+ import { acquireLease, releaseLease, inspectLeases } from '../adapters/leases.js'
25
+ import { importLegacySnapshot, validateLegacyMap } from '../adapters/legacy.js'
19
26
  import { registerDoctorTool } from '../tools/doctor.js'
20
27
  import { registerRecoverTool } from '../tools/recover.js'
21
28
  import { registerTestTool } from '../tools/test.js'
@@ -43,10 +50,12 @@ import { registerSpawnTool } from '../tools/spawn.js'
43
50
  import { registerCompleteTool } from '../tools/complete.js'
44
51
  import { registerFindingTool } from '../tools/finding.js'
45
52
  import { registerFindingsTool } from '../tools/findings.js'
46
- import { readDiscoveryConfig, normalizeDiscovery, runDiscovery } from './tests.js'
53
+ import { readDiscoveryConfig, normalizeDiscovery, runDiscovery } from '../adapters/tests.js'
47
54
  import { summarize as summarizeBatch } from '../domain/batch.js'
55
+ import { assertPorts } from '../ports/index.js'
48
56
  import path from 'node:path'
49
57
  import { fsp } from '../adapters/store-fs.js'
58
+ import { createDefaultDeps } from '../adapters/default.js'
50
59
  import crypto from 'node:crypto'
51
60
 
52
61
  // Mutable composition state is grouped in one const object: the registration
@@ -54,6 +63,41 @@ import crypto from 'node:crypto'
54
63
  const runtimeState = {
55
64
  workspaceHook: null,
56
65
  hmrBridge: null,
66
+ // #69: the host entry builds the adapter set once and passes it in. The lazy
67
+ // fallback is for tests/embedders that call registerTowerTools() directly;
68
+ // it is still built once per process.
69
+ deps: null,
70
+ // #84: the defined tool names are recorded before the exposure switches run,
71
+ // so the concept budget measures the full surface rather than one profile's
72
+ // subset. The number is reported by tower_doctor and compared with the
73
+ // committed scripts/surface-baseline.json.
74
+ definedTools: new Set(),
75
+ }
76
+
77
+ /** The full defined tool surface (names, sorted), independent of exposure flags. */
78
+ export function definedToolNames() {
79
+ return [...runtimeState.definedTools].sort()
80
+ }
81
+
82
+ /** The number of tools defined by this build, used by the concept budget (#84). */
83
+ export function definedToolCount() {
84
+ return runtimeState.definedTools.size
85
+ }
86
+
87
+ /** The injected port set for the app layer (built once per process). */
88
+ function towerDeps() {
89
+ if (!runtimeState.deps) runtimeState.deps = createDefaultDeps()
90
+ return runtimeState.deps
91
+ }
92
+
93
+ /** Compose the app Tower with the injected ports (never a bare adapter import). */
94
+ function openTower(repoRoot) {
95
+ return Tower.open(repoRoot, towerDeps())
96
+ }
97
+
98
+ /** Build a Tower with an explicit config for the read-only doctor path. */
99
+ function createTower(repo, config) {
100
+ return new Tower(repo, config, towerDeps())
57
101
  }
58
102
 
59
103
  function normalizeToolArgs(args, aliases = {}) {
@@ -324,6 +368,9 @@ function plainSchema(value) {
324
368
  */
325
369
  function define(ctx, definition) {
326
370
  const { aliases, requiresMissionId, board, execute, ...rest } = definition
371
+ // Record the definition before the exposure gate: the concept budget counts
372
+ // the tools this build can register, not only the ones a profile exposes.
373
+ runtimeState.definedTools.add(rest.name)
327
374
  const toolsConfig = runtimeConfig().tools ?? {}
328
375
  const flag = OPERATIONAL_TOOLS.get(rest.name)
329
376
  if (flag && (toolsConfig.exposeOperational === false || toolsConfig[flag] === false)) return
@@ -364,7 +411,7 @@ function define(ctx, definition) {
364
411
  }
365
412
  if (board === true && typeof normalized.repoRoot === 'string') {
366
413
  try {
367
- const opened = await Tower.open(normalized.repoRoot)
414
+ const opened = await openTower(normalized.repoRoot)
368
415
  await opened.writeBoard()
369
416
  } catch {
370
417
  // board output is best effort
@@ -451,6 +498,24 @@ async function observeActivity(ctx, tower) {
451
498
  export function registerTowerTools(ctx, options = {}) {
452
499
  runtimeState.workspaceHook = options.workspace ?? null
453
500
  runtimeState.hmrBridge = options.hmrBridge ?? null
501
+ // #69: the host entry (index.js) is the composition root. When it supplies the
502
+ // port set, adopt it — validated once — instead of constructing a second set
503
+ // here. Tests/embedders that omit deps fall back to one lazily built set.
504
+ // #75: the event port is bound here to the live Cordis emitter; emitting is
505
+ // best effort so a listener can never fail the tool that produced the fact.
506
+ const baseDeps = options.deps ? assertPorts(options.deps) : (runtimeState.deps ?? createDefaultDeps())
507
+ runtimeState.deps = {
508
+ ...baseDeps,
509
+ events: {
510
+ emit(name, payload) {
511
+ try {
512
+ ctx.emit?.(name, payload)
513
+ } catch {
514
+ // events are an optional side channel; never fail a tool for them
515
+ }
516
+ },
517
+ },
518
+ }
454
519
  // A lightweight roster watchdog: the authoritative activity comes from the
455
520
  // subagents catalog; a missing observation after spawn.staleMs is marked
456
521
  // stale (never silently treated as stopped) and blocks merge via the gate.
@@ -460,7 +525,7 @@ export function registerTowerTools(ctx, options = {}) {
460
525
  try {
461
526
  if (!ctx.get('subagents')) return
462
527
  for (const repoRoot of [...WATCHED_REPOS]) {
463
- const tower = await Tower.open(repoRoot)
528
+ const tower = await openTower(repoRoot)
464
529
  const view = await tower.rosterView()
465
530
  const live = view.roster.filter((entry) => !ROSTER_INACTIVE_STATES.includes(entry.state))
466
531
  if (live.length === 0) { WATCHED_REPOS.delete(repoRoot); continue }
@@ -481,59 +546,69 @@ export function registerTowerTools(ctx, options = {}) {
481
546
  }, watchMs)
482
547
  timer.unref?.()
483
548
  ctx.effect(() => () => clearInterval(timer))
484
- registerInitTool(ctx, { define, actorFor, requireTowerActor, workspaceHook: runtimeState.workspaceHook, ensureRepository })
549
+ registerInitTool(ctx, { openTower, define, actorFor, requireTowerActor, workspaceHook: runtimeState.workspaceHook, ensureRepository, resolveRepo, runtimeConfig })
485
550
 
486
- registerPlanTool(ctx, { define, actorFor, requireTowerActor })
551
+ registerPlanTool(ctx, { openTower, define, actorFor, requireTowerActor, assertPresetsAvailable })
487
552
 
488
- registerMissionTool(ctx, { define, actorFor, requireKnownActor })
553
+ registerMissionTool(ctx, { openTower, define, actorFor, requireKnownActor })
489
554
 
490
- registerStatusTool(ctx, { define, workspaceHook: runtimeState.workspaceHook, spawnBackoffView, observeActivity })
555
+ registerStatusTool(ctx, { openTower, define, workspaceHook: runtimeState.workspaceHook, spawnBackoffView, observeActivity })
491
556
 
492
- registerSpawnTool(ctx, { define, actorFor, requireTowerActor, enqueueSpawn, observeActivity, spawnBackoffState, spawnBackoffView, recordSpawnThrottle, recordSpawnSuccess, runMissionDiscovery, textOf })
557
+ registerSpawnTool(ctx, { openTower, define, actorFor, requireTowerActor, enqueueSpawn, observeActivity, spawnBackoffState, spawnBackoffView, recordSpawnThrottle, recordSpawnSuccess, runMissionDiscovery, textOf, inspectLeases })
493
558
 
494
- registerRosterTool(ctx, { define, workspaceHook: runtimeState.workspaceHook, observeActivity, actorFor, requireTowerActor, interruptRosterAgent })
559
+ registerRosterTool(ctx, { openTower, define, workspaceHook: runtimeState.workspaceHook, observeActivity, actorFor, requireTowerActor, interruptRosterAgent })
495
560
 
496
- registerInterruptTool(ctx, { define, actorFor, requireTowerActor, interruptRosterAgent })
561
+ registerInterruptTool(ctx, { openTower, define, actorFor, requireTowerActor, interruptRosterAgent })
497
562
 
498
- registerDiffTool(ctx, { define, actorFor, requireTowerActor })
563
+ registerDiffTool(ctx, { openTower, define, actorFor, requireTowerActor })
499
564
 
500
- registerAdoptTool(ctx, { define, actorFor, requireTowerActor })
565
+ registerAdoptTool(ctx, { openTower, define, actorFor, requireTowerActor })
501
566
 
502
- registerSendTool(ctx, { define, actorFor, checkSendRate })
567
+ registerSendTool(ctx, { openTower, define, actorFor, checkSendRate })
503
568
 
504
- registerInboxTool(ctx, { define })
569
+ registerInboxTool(ctx, { openTower, define })
505
570
 
506
- registerCompleteTool(ctx, { define, runMissionDiscovery })
571
+ registerCompleteTool(ctx, { openTower, define, runMissionDiscovery })
507
572
 
508
- registerFindingTool(ctx, { define, actorFor, requireKnownActor })
573
+ registerFindingTool(ctx, { openTower, define, actorFor, requireKnownActor })
509
574
 
510
- registerFindingsTool(ctx, { define, actorFor, requireKnownActor })
575
+ registerFindingsTool(ctx, { openTower, define, actorFor, requireKnownActor })
511
576
 
512
- registerReviewTool(ctx, { define, actorFor })
577
+ registerReviewTool(ctx, { openTower, define, actorFor })
513
578
 
514
- registerPreflightTool(ctx, { define, observeActivity })
579
+ registerPreflightTool(ctx, { openTower, define, observeActivity })
515
580
 
516
- registerMergeTool(ctx, { define, actorFor, requireTowerActor, observeActivity })
581
+ registerMergeTool(ctx, { openTower, define, actorFor, requireTowerActor, observeActivity })
517
582
 
518
- registerRebaseTool(ctx, { define, actorFor, requireTowerActor, observeActivity })
583
+ registerRebaseTool(ctx, { openTower, define, actorFor, requireTowerActor, observeActivity })
519
584
 
520
- registerTeardownTool(ctx, { define, actorFor, requireTowerActor, observeActivity })
585
+ registerTeardownTool(ctx, { openTower, define, actorFor, requireTowerActor, observeActivity })
521
586
 
522
- registerDoctorTool(ctx, define)
587
+ registerDoctorTool(ctx, {
588
+ openTower,
589
+ define,
590
+ createTower,
591
+ resolveRepo,
592
+ runtimeConfig,
593
+ runDoctor,
594
+ // #84: the doctor reports the live surface; the count is the full defined
595
+ // tool surface, independent of which operational switches this profile uses.
596
+ surface: () => ({ tools: definedToolCount() }),
597
+ })
523
598
 
524
- registerRecoverTool(ctx, { define, actorFor, requireTowerActor })
599
+ registerRecoverTool(ctx, { openTower, define, actorFor, requireTowerActor, readStateLock, lockTiming })
525
600
 
526
- registerTestTool(ctx, { define, actorFor, requireKnownActor, runMissionDiscovery })
601
+ registerTestTool(ctx, { openTower, define, actorFor, requireKnownActor, runMissionDiscovery })
527
602
 
528
- registerBatchTool(ctx, { define, actorFor, requireTowerActor, summarizeBatch, verifyBatchReceiptFiles })
603
+ registerBatchTool(ctx, { openTower, define, actorFor, requireTowerActor, summarizeBatch, verifyBatchReceiptFiles })
529
604
 
530
- registerResourceTool(ctx, { define, actorFor, requireTowerActor })
605
+ registerResourceTool(ctx, { openTower, define, actorFor, requireTowerActor, acquireLease, releaseLease, inspectLeases })
531
606
 
532
- registerImportLegacyTool(ctx, { define, actorFor, requireTowerActor })
607
+ registerImportLegacyTool(ctx, { openTower, define, actorFor, requireTowerActor, importLegacySnapshot })
533
608
 
534
- registerLegacyMapTool(ctx, { define, actorFor, requireTowerActor })
609
+ registerLegacyMapTool(ctx, { openTower, define, actorFor, requireTowerActor, validateLegacyMap })
535
610
 
536
- registerReloadTool(ctx, { define, actorFor, requireTowerActor, sharedHmrBridge: runtimeState.hmrBridge })
611
+ registerReloadTool(ctx, { openTower, define, actorFor, requireTowerActor, sharedHmrBridge: runtimeState.hmrBridge, runtimeConfig, createHmrBridge })
537
612
  }
538
613
 
539
614
  export const __testSpawnBackoff = {
package/host/tower.js ADDED
@@ -0,0 +1,32 @@
1
+ // Default-deps binding of the Tower app service (#69).
2
+ //
3
+ // The composition root (index.js / host/tool-registry.js) builds one deps object
4
+ // and injects it into the app layer. This module is the convenience binding for
5
+ // embedders and tests that want the process-default adapters; it is the only
6
+ // host/ module besides tool-registry.js that knows the default adapter set.
7
+ //
8
+ // Object layout supplied here is memoised in adapters/default.js, so every
9
+ // caller shares the same adapter instances.
10
+ import { Tower as InjectedTower } from '../app/tower.js'
11
+ import { defaultDeps } from '../adapters/default.js'
12
+
13
+ export class Tower extends InjectedTower {
14
+ /**
15
+ * @param {{ root: string, commonDir: string, branch?: string | null }} repo
16
+ * @param {import('../types/index.js').TowerConfig} [config]
17
+ * @param {Record<string, any>} [deps]
18
+ */
19
+ constructor(repo, config, deps = defaultDeps) {
20
+ super(repo, config, deps)
21
+ }
22
+
23
+ /**
24
+ * @param {string} repoRoot
25
+ * @param {Record<string, any>} [deps]
26
+ */
27
+ static async open(repoRoot, deps = defaultDeps) {
28
+ const repo = await deps.vcs.resolveRepo(repoRoot)
29
+ const config = await deps.config.resolveRepoConfig(repo)
30
+ return new Tower(repo, config, deps)
31
+ }
32
+ }
package/index.js CHANGED
@@ -12,21 +12,22 @@
12
12
  // plugin from disk when the HMR service is present (dev/linked installs).
13
13
  import fs from 'node:fs'
14
14
  import { randomUUID } from 'node:crypto'
15
- import { registerTowerTools } from './lib/tool-registry.js'
16
- import { loadModes, saveModes, storePath } from './lib/modeStore.js'
17
- import { ModeState } from './lib/modeState.js'
18
- import { Config as ConfigSchema, DEFAULT_DENY_TOOLS, configureRuntime, runtimeConfig } from './lib/config.js'
15
+ import { registerTowerTools } from './host/tool-registry.js'
16
+ import { createDefaultDeps } from './adapters/default.js'
17
+ import { loadModes, saveModes, storePath } from './host/modeStore.js'
18
+ import { ModeState } from './host/modeState.js'
19
+ import { Config as ConfigSchema, DEFAULT_DENY_TOOLS, configureRuntime, runtimeConfig } from './host/config.js'
19
20
  import { TowerToolError } from './domain/errors.js'
20
- import { resolveRepo } from './lib/git.js'
21
+ import { resolveRepo } from './adapters/git.js'
21
22
  import {
22
23
  acquireWorkspaceLock,
23
24
  inspectWorkspaceLock,
24
25
  releaseWorkspaceLock,
25
26
  touchWorkspaceLock,
26
27
  workspaceLockToken,
27
- } from './lib/workspaceLock.js'
28
- import { createHmrBridge } from './lib/hmr.js'
29
- import { assertPresetsAvailable } from './lib/presets.js'
28
+ } from './adapters/workspaceLock.js'
29
+ import { createHmrBridge } from './host/hmr.js'
30
+ import { assertPresetsAvailable } from './host/presets.js'
30
31
 
31
32
  export const name = 'dsh-tower'
32
33
  export const inject = ['tools', 'systemPrompt']
@@ -343,7 +344,9 @@ export function apply(ctx, rawConfig) {
343
344
  },
344
345
  }
345
346
 
346
- registerTowerTools(ctx, { workspace, hmrBridge })
347
+ // #69: the host entry is the composition root — the adapters are constructed
348
+ // once here and injected into the tool layer and every Tower instance.
349
+ registerTowerTools(ctx, { workspace, hmrBridge, deps: createDefaultDeps() })
347
350
 
348
351
  // On dispose (including an HMR reload) release advisory workspace locks this
349
352
  // process still owns; a reloaded plugin restores the entries from modes.json.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tower",
3
- "version": "2.1.0-beta.1",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "DSH-native Tower: mission planning, worktree isolation, review-gated merges, and a human-owned Tower mode.",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  "reviewer.js",
42
42
  "skills.js",
43
43
  "skills/",
44
- "lib/*.js",
44
+ "host/*.js",
45
45
  "domain/**/*.js",
46
46
  "app/*.js",
47
47
  "tools/*.js",
@@ -58,6 +58,7 @@
58
58
  "LICENSE",
59
59
  "CONTRIBUTING.md",
60
60
  "SECURITY.md",
61
+ "PRINCIPLES.md",
61
62
  "docs/",
62
63
  "ui-bundle/",
63
64
  "ports/*.js"
@@ -71,22 +72,28 @@
71
72
  "test": "node --test tests/*.test.mjs",
72
73
  "test:watch": "node --test --watch tests/*.test.mjs",
73
74
  "test:coverage": "node --test --experimental-test-coverage tests/*.test.mjs",
75
+ "test:live": "node tests/live/run.cjs",
74
76
  "smoke:mount": "node scripts/mount-smoke.mjs",
75
77
  "smoke:fixtures": "node scripts/mount-smoke/fixtures.mjs",
76
78
  "release:check": "node scripts/release-check.mjs",
77
79
  "audit:issues": "node scripts/issue-audit.mjs",
80
+ "audit:regressions": "node scripts/regression-audit.mjs",
81
+ "surface:check": "node scripts/surface-baseline.mjs",
78
82
  "dev:hmr": "node scripts/enable-dev-hmr.mjs",
79
83
  "docs:check": "node scripts/check-docs.mjs",
84
+ "docs:check:philosophy": "node scripts/check-philosophy.mjs",
80
85
  "docs:generate:errors": "node scripts/generate-error-reference.mjs",
81
86
  "docs:check:generated": "node scripts/generate-error-reference.mjs --check",
82
87
  "docs:generate:tools": "node scripts/generate-tools-reference.mjs",
83
88
  "docs:check:generated:tools": "node scripts/generate-tools-reference.mjs --check",
84
89
  "docs:generate:config": "node scripts/generate-config-reference.mjs",
85
90
  "docs:check:generated:config": "node scripts/generate-config-reference.mjs --check",
91
+ "docs:generate:events": "node scripts/generate-events-reference.mjs",
92
+ "docs:check:generated:events": "node scripts/generate-events-reference.mjs --check",
86
93
  "pack:check": "node scripts/check-pack.mjs",
87
94
  "architecture:check": "node scripts/architecture.mjs",
88
95
  "types:check": "tsc --noEmit",
89
- "check": "npm test && npm run docs:check && npm run pack:check && npm run architecture:check && npm run types:check && npm run domain:check",
96
+ "check": "npm test && npm run docs:check && npm run docs:check:philosophy && npm run docs:check:generated && npm run docs:check:generated:tools && npm run docs:check:generated:config && npm run docs:check:generated:events && npm run surface:check && npm run pack:check && npm run architecture:check && npm run types:check && npm run domain:check",
90
97
  "domain:metrics": "node scripts/domain-metrics.mjs",
91
98
  "domain:check": "node scripts/domain-metrics.mjs --check"
92
99
  },
@@ -0,0 +1,18 @@
1
+ // Terminal-mission archive port (#69). The readers/writers are store-agnostic:
2
+ // they take the store port and the Tower paths, so the same archive logic runs
3
+ // on the filesystem adapter and on an in-memory store.
4
+ import { assertPort, definePort } from './contract.js'
5
+
6
+ /**
7
+ * @typedef {object} ArchivePort
8
+ * @property {(store: unknown, paths: { dir: string }) => Promise<{ ok: boolean, file: string, missions: any[], error?: string, exists: boolean }>} readIndex
9
+ * @property {(store: unknown, paths: { dir: string }, missionId: string) => Promise<any>} readEntry
10
+ * @property {(store: unknown, paths: { dir: string }, input: { entries: any[], now?: number }) => Promise<any>} writeEntries
11
+ */
12
+
13
+ export const ARCHIVE_PORT = definePort('ArchivePort', ['readIndex', 'readEntry', 'writeEntries'])
14
+
15
+ /** @param {unknown} impl */
16
+ export function assertArchivePort(impl) {
17
+ return /** @type {ArchivePort} */ (assertPort(impl, ARCHIVE_PORT, 'archive'))
18
+ }
@@ -0,0 +1,15 @@
1
+ // Case-sensitivity probe port (#69). tower_init probes the checkout filesystem
2
+ // once; a composition without a real filesystem injects its own answer.
3
+ import { assertPort, definePort } from './contract.js'
4
+
5
+ /**
6
+ * @typedef {object} CaseSensitivityPort
7
+ * @property {(root: string) => Promise<boolean | null>} detect
8
+ */
9
+
10
+ export const CASE_SENSITIVITY_PORT = definePort('CaseSensitivityPort', ['detect'])
11
+
12
+ /** @param {unknown} impl */
13
+ export function assertCaseSensitivityPort(impl) {
14
+ return /** @type {CaseSensitivityPort} */ (assertPort(impl, CASE_SENSITIVITY_PORT, 'caseSensitivity'))
15
+ }
package/ports/clock.js CHANGED
@@ -1,6 +1,14 @@
1
- // Clock port (#56).
1
+ // Clock port (#69).
2
+ import { assertPort, definePort } from './contract.js'
3
+
2
4
  /**
3
5
  * @typedef {object} ClockPort
4
6
  * @property {() => number} now
5
7
  */
6
- export {}
8
+
9
+ export const CLOCK_PORT = definePort('ClockPort', ['now'])
10
+
11
+ /** @param {unknown} impl */
12
+ export function assertClockPort(impl) {
13
+ return /** @type {ClockPort} */ (assertPort(impl, CLOCK_PORT, 'clock'))
14
+ }
@@ -0,0 +1,19 @@
1
+ // Config port (#69).
2
+ //
3
+ // App use cases read the effective Tower config through this port. They do not
4
+ // import host/config.js, so the process-level config store stays a property of
5
+ // the composition root rather than a module-global dependency of the app.
6
+ import { assertPort, definePort } from './contract.js'
7
+
8
+ /**
9
+ * @typedef {object} ConfigPort
10
+ * @property {() => import('../types/index.js').TowerConfig} runtimeConfig
11
+ * @property {(repo: { root: string, commonDir: string, branch?: string|null }) => Promise<import('../types/index.js').TowerConfig>} resolveRepoConfig
12
+ */
13
+
14
+ export const CONFIG_PORT = definePort('ConfigPort', ['runtimeConfig', 'resolveRepoConfig'])
15
+
16
+ /** @param {unknown} impl */
17
+ export function assertConfigPort(impl) {
18
+ return /** @type {ConfigPort} */ (assertPort(impl, CONFIG_PORT, 'config'))
19
+ }
@@ -0,0 +1,38 @@
1
+ // Port contract primitives (#69).
2
+ //
3
+ // A port is a runtime-checkable contract, not only a JSDoc typedef: the
4
+ // contract object is importable, and `assertPort` rejects a dependency object
5
+ // that does not implement it. That is what makes the seam substitutable at
6
+ // runtime instead of merely documented.
7
+ export function definePort(name, methods) {
8
+ return Object.freeze({ name, methods: Object.freeze([...methods]) })
9
+ }
10
+
11
+ /**
12
+ * @param {unknown} impl
13
+ * @param {{ name: string, methods: readonly string[] }} contract
14
+ * @param {string} [label]
15
+ */
16
+ export function assertPort(impl, contract, label = contract.name) {
17
+ if (!impl || typeof impl !== 'object') {
18
+ throw new TypeError(`${label}: expected an object implementing ${contract.name}`)
19
+ }
20
+ const missing = contract.methods.filter((method) => typeof (/** @type {any} */ (impl))[method] !== 'function')
21
+ if (missing.length > 0) {
22
+ throw new TypeError(`${label} does not implement ${contract.name}; missing: ${missing.join(', ')}`)
23
+ }
24
+ return impl
25
+ }
26
+
27
+ /**
28
+ * @param {unknown} impl
29
+ * @param {{ name: string, methods: readonly string[] }} contract
30
+ */
31
+ export function implementsPort(impl, contract) {
32
+ try {
33
+ assertPort(impl, contract)
34
+ return true
35
+ } catch {
36
+ return false
37
+ }
38
+ }
@@ -0,0 +1,24 @@
1
+ // Domain-event port (#75).
2
+ //
3
+ // The app layer persists a transition and then emits the domain events it
4
+ // described through this port. The host binds it to Cordis `ctx.emit`; tests
5
+ // bind it to a recorder. `emit` is best effort by contract: a listener (or a
6
+ // missing service) must never fail the tool that produced the fact.
7
+ import { assertPort, definePort } from './contract.js'
8
+
9
+ /**
10
+ * @typedef {object} EventPort
11
+ * @property {(name: string, payload: Record<string, any>) => void} emit
12
+ */
13
+
14
+ export const EVENT_PORT = definePort('EventPort', ['emit'])
15
+
16
+ /** @param {unknown} impl */
17
+ export function assertEventPort(impl) {
18
+ return /** @type {EventPort} */ (assertPort(impl, EVENT_PORT, 'events'))
19
+ }
20
+
21
+ /** The no-op default: embedders without an event channel still get a Tower. */
22
+ export const NOOP_EVENT_PORT = Object.freeze({
23
+ emit() {},
24
+ })
package/ports/ids.js CHANGED
@@ -1,7 +1,15 @@
1
- // Ids port (#56).
1
+ // Ids port (#69).
2
+ import { assertPort, definePort } from './contract.js'
3
+
2
4
  /**
3
5
  * @typedef {object} IdsPort
4
6
  * @property {() => string} uuid
5
7
  * @property {(text: string) => string} shortHash
6
8
  */
7
- export {}
9
+
10
+ export const IDS_PORT = definePort('IdsPort', ['uuid', 'shortHash'])
11
+
12
+ /** @param {unknown} impl */
13
+ export function assertIdsPort(impl) {
14
+ return /** @type {IdsPort} */ (assertPort(impl, IDS_PORT, 'ids'))
15
+ }
package/ports/index.js CHANGED
@@ -1,2 +1,54 @@
1
- // Explicit ports consumed by app use cases (#56).
2
- export {}
1
+ // Explicit ports consumed by app use cases (#69).
2
+ //
3
+ // `assertPorts` is the injection check the app layer runs on the dependency
4
+ // object it receives: a composition root that forgets a port fails at
5
+ // construction, not in the middle of a mission.
6
+ import { assertPort } from './contract.js'
7
+ import { STORE_PORT, assertStorePort } from './store.js'
8
+ import { VCS_PORT, assertVcsPort } from './vcs.js'
9
+ import { CLOCK_PORT, assertClockPort } from './clock.js'
10
+ import { IDS_PORT, assertIdsPort } from './ids.js'
11
+ import { CONFIG_PORT, assertConfigPort } from './config.js'
12
+ import { STATE_PORT, assertStatePort } from './state.js'
13
+ import { OWNERSHIP_PORT, assertOwnershipPort } from './ownership.js'
14
+ import { CASE_SENSITIVITY_PORT, assertCaseSensitivityPort } from './caseSensitivity.js'
15
+ import { ARCHIVE_PORT, assertArchivePort } from './archive.js'
16
+ import { EVENT_PORT, assertEventPort } from './events.js'
17
+
18
+ /** Every port the Tower app layer depends on, keyed by deps member. */
19
+ export const PORTS = Object.freeze({
20
+ store: { contract: STORE_PORT, assert: assertStorePort },
21
+ vcs: { contract: VCS_PORT, assert: assertVcsPort },
22
+ clock: { contract: CLOCK_PORT, assert: assertClockPort },
23
+ ids: { contract: IDS_PORT, assert: assertIdsPort },
24
+ config: { contract: CONFIG_PORT, assert: assertConfigPort },
25
+ state: { contract: STATE_PORT, assert: assertStatePort },
26
+ ownership: { contract: OWNERSHIP_PORT, assert: assertOwnershipPort },
27
+ caseSensitivity: { contract: CASE_SENSITIVITY_PORT, assert: assertCaseSensitivityPort },
28
+ archive: { contract: ARCHIVE_PORT, assert: assertArchivePort },
29
+ events: { contract: EVENT_PORT, assert: assertEventPort },
30
+ })
31
+
32
+ /** @param {unknown} deps */
33
+ export function assertPorts(deps) {
34
+ if (!deps || typeof deps !== 'object') {
35
+ throw new TypeError('Tower requires an injected deps object assembled by the composition root (see adapters/default.js)')
36
+ }
37
+ for (const [name, port] of Object.entries(PORTS)) {
38
+ if (typeof port.assert === 'function') port.assert(deps[name])
39
+ else assertPort(deps[name], port.contract, name)
40
+ }
41
+ return deps
42
+ }
43
+
44
+ export { STORE_PORT, assertStorePort } from './store.js'
45
+ export { VCS_PORT, assertVcsPort } from './vcs.js'
46
+ export { CLOCK_PORT, assertClockPort } from './clock.js'
47
+ export { IDS_PORT, assertIdsPort } from './ids.js'
48
+ export { CONFIG_PORT, assertConfigPort } from './config.js'
49
+ export { STATE_PORT, assertStatePort } from './state.js'
50
+ export { OWNERSHIP_PORT, assertOwnershipPort } from './ownership.js'
51
+ export { CASE_SENSITIVITY_PORT, assertCaseSensitivityPort } from './caseSensitivity.js'
52
+ export { ARCHIVE_PORT, assertArchivePort } from './archive.js'
53
+ export { EVENT_PORT, assertEventPort, NOOP_EVENT_PORT } from './events.js'
54
+ export { definePort, assertPort, implementsPort } from './contract.js'
@@ -0,0 +1,17 @@
1
+ // Worktree-ownership port (#69). The default implementation verifies a stored
2
+ // worktree against the real filesystem and Git; an in-memory composition can
3
+ // substitute a verifier that matches its own virtual paths.
4
+ import { assertPort, definePort } from './contract.js'
5
+
6
+ /**
7
+ * @typedef {object} OwnershipPort
8
+ * @property {(input: { storedPath: string, storedRealPath?: string, branch: string, commonDir: string, missionId?: string }) => Promise<{ path: string, realPath: string }>} assertOwnership
9
+ * @property {(target: string) => Promise<string>} canonicalPath
10
+ */
11
+
12
+ export const OWNERSHIP_PORT = definePort('OwnershipPort', ['assertOwnership', 'canonicalPath'])
13
+
14
+ /** @param {unknown} impl */
15
+ export function assertOwnershipPort(impl) {
16
+ return /** @type {OwnershipPort} */ (assertPort(impl, OWNERSHIP_PORT, 'ownership'))
17
+ }
package/ports/state.js ADDED
@@ -0,0 +1,16 @@
1
+ // State-load port (#69): schema inspection, migration, and the one persisted
2
+ // migration result. The implementation is store-agnostic and receives the
3
+ // store port, so an in-memory store can drive the same load path.
4
+ import { assertPort, definePort } from './contract.js'
5
+
6
+ /**
7
+ * @typedef {object} StatePort
8
+ * @property {(input: { store: unknown, paths: unknown, now?: number, persist?: boolean, migrations?: readonly unknown[] }) => Promise<any>} loadStateFile
9
+ */
10
+
11
+ export const STATE_PORT = definePort('StatePort', ['loadStateFile'])
12
+
13
+ /** @param {unknown} impl */
14
+ export function assertStatePort(impl) {
15
+ return /** @type {StatePort} */ (assertPort(impl, STATE_PORT, 'state'))
16
+ }