dsh-tower 2.1.0 → 2.4.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 (203) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/COMPATIBILITY.md +6 -1
  3. package/CONTRIBUTING.md +78 -5
  4. package/PRINCIPLES.md +163 -0
  5. package/README.en.md +35 -19
  6. package/README.md +28 -18
  7. package/SECURITY.md +6 -0
  8. package/{lib → adapters}/archive.js +1 -1
  9. package/{lib → adapters}/caseSensitivity.js +1 -1
  10. package/adapters/default.js +51 -10
  11. package/{lib → adapters}/git.js +1 -1
  12. package/{lib → adapters}/leases.js +1 -1
  13. package/{lib → adapters}/legacy.js +1 -1
  14. package/adapters/persistence.js +543 -0
  15. package/adapters/store-fs.js +28 -1
  16. package/adapters/store.js +204 -0
  17. package/{lib → adapters}/tests.js +9 -5
  18. package/adapters/vcs-git.js +15 -1
  19. package/{lib → adapters}/workspaceLock.js +1 -1
  20. package/{lib → adapters}/worktreeOwnership.js +1 -1
  21. package/app/archive.js +4 -6
  22. package/app/batch.js +9 -10
  23. package/app/board.js +2 -4
  24. package/app/checkCompletion.js +9 -10
  25. package/app/enrichedMissions.js +2 -4
  26. package/app/findings.js +6 -7
  27. package/app/gate.js +1 -2
  28. package/app/init.js +24 -29
  29. package/app/merge.js +58 -51
  30. package/app/messages.js +7 -8
  31. package/app/mission.js +5 -5
  32. package/app/plan.js +8 -5
  33. package/app/provision.js +12 -14
  34. package/app/read.js +6 -3
  35. package/app/rebase.js +14 -16
  36. package/app/recording.js +7 -8
  37. package/app/review.js +15 -8
  38. package/app/roster.js +14 -15
  39. package/{lib → app}/spawn-prechecks.js +4 -3
  40. package/app/spawn.js +12 -13
  41. package/app/teardown.js +8 -9
  42. package/app/tower.js +89 -35
  43. package/cordis.patch.yml +56 -28
  44. package/docs/README.md +7 -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 +70 -0
  49. package/docs/adr/0005-component-surface.md +130 -0
  50. package/docs/adr/README.md +52 -0
  51. package/docs/agents/workflow.md +51 -0
  52. package/docs/architecture/overview.md +192 -43
  53. package/docs/guide/hot-reload.md +5 -5
  54. package/docs/guide/install-upgrade-migration.md +13 -0
  55. package/docs/guide/release.md +1 -1
  56. package/docs/operations/incident.md +82 -0
  57. package/docs/operations/runbook.md +12 -3
  58. package/docs/operations/security.md +1 -1
  59. package/docs/reference/blockers.md +2 -2
  60. package/docs/reference/component-surface.md +135 -0
  61. package/docs/reference/config.generated.md +2 -9
  62. package/docs/reference/config.md +11 -9
  63. package/docs/reference/doctor.md +53 -2
  64. package/docs/reference/errors.generated.md +41 -37
  65. package/docs/reference/errors.md +53 -41
  66. package/docs/reference/events.md +38 -6
  67. package/docs/reference/surface-budget.md +157 -0
  68. package/docs/reference/tools.generated.md +8 -5
  69. package/docs/reference/tools.md +10 -7
  70. package/docs/zh/README.md +1 -1
  71. package/docs/zh/guide/hot-reload.md +4 -4
  72. package/docs/zh/guide/install-upgrade-migration.md +10 -0
  73. package/docs/zh/operations/security.md +1 -1
  74. package/docs/zh/reference/blockers.md +2 -2
  75. package/docs/zh/reference/config.md +9 -9
  76. package/docs/zh/reference/doctor.md +29 -1
  77. package/docs/zh/reference/tools.md +10 -8
  78. package/domain/batch.js +2 -2
  79. package/domain/components.js +641 -0
  80. package/domain/config/schema.js +62 -28
  81. package/domain/contracts/outputs.js +16 -2
  82. package/domain/contracts/tool-inputs/ops.js +1 -0
  83. package/domain/errors.js +308 -43
  84. package/domain/events.js +207 -0
  85. package/domain/integration/gate.js +17 -2
  86. package/domain/integration/merge.js +35 -0
  87. package/domain/lockBudget.js +30 -0
  88. package/domain/mission.js +12 -1
  89. package/domain/missions/completion.js +14 -0
  90. package/domain/missions/lifecycle.js +9 -3
  91. package/domain/missions/plan.js +14 -0
  92. package/domain/missions/review.js +90 -7
  93. package/domain/read/views.js +3 -0
  94. package/domain/state/archive.js +1 -1
  95. package/domain/state.js +1 -1
  96. package/domain/surface.js +347 -0
  97. package/host/capability-batch.js +15 -0
  98. package/host/capability-hmr.js +30 -0
  99. package/host/capability-import.js +11 -0
  100. package/host/capability-leases.js +11 -0
  101. package/host/capability-registry.js +149 -0
  102. package/host/capability-tests.js +16 -0
  103. package/{lib → host}/doctor.js +157 -18
  104. package/{lib → host}/hmr.js +2 -4
  105. package/{lib → host}/modeStore.js +1 -1
  106. package/host/presets.js +77 -0
  107. package/{lib → host}/tool-registry.js +235 -44
  108. package/host/tower.js +32 -0
  109. package/index.js +40 -25
  110. package/locale/en.json +31 -11
  111. package/locale/zh.json +30 -10
  112. package/package.json +24 -4
  113. package/ports/archive.js +18 -0
  114. package/ports/caseSensitivity.js +15 -0
  115. package/ports/clock.js +10 -2
  116. package/ports/config.js +19 -0
  117. package/ports/contract.js +38 -0
  118. package/ports/events.js +24 -0
  119. package/ports/ids.js +10 -2
  120. package/ports/index.js +54 -2
  121. package/ports/ownership.js +17 -0
  122. package/ports/state.js +16 -0
  123. package/ports/store.js +52 -3
  124. package/ports/vcs.js +25 -4
  125. package/scripts/architecture-waivers.json +19 -0
  126. package/scripts/architecture.mjs +634 -23
  127. package/scripts/check-components.mjs +235 -0
  128. package/scripts/check-docs.mjs +8 -35
  129. package/scripts/check-pack.mjs +68 -39
  130. package/scripts/check-philosophy.mjs +266 -0
  131. package/scripts/component-surface.json +395 -0
  132. package/scripts/domain-metrics.mjs +13 -4
  133. package/scripts/generate-component-surface.mjs +59 -0
  134. package/scripts/generate-config-reference.mjs +14 -1
  135. package/scripts/generate-error-reference.mjs +68 -14
  136. package/scripts/generate-events-reference.mjs +83 -0
  137. package/scripts/generate-tools-reference.mjs +18 -25
  138. package/scripts/generate-types.mjs +127 -0
  139. package/scripts/issue-audit.mjs +2 -2
  140. package/scripts/lib/surface.mjs +87 -0
  141. package/scripts/lib/tool-surface.mjs +85 -0
  142. package/scripts/mount-smoke/fixtures.mjs +3 -3
  143. package/scripts/mount-smoke/surface.mjs +96 -27
  144. package/scripts/mount-smoke.mjs +3 -1
  145. package/scripts/pack-manifest.txt +75 -22
  146. package/scripts/regression-audit.mjs +164 -0
  147. package/scripts/release-check.mjs +1 -1
  148. package/scripts/surface-baseline.json +52 -0
  149. package/scripts/surface-baseline.mjs +118 -0
  150. package/scripts/types-coverage.mjs +184 -0
  151. package/scripts/types-strict-manifest.txt +10 -0
  152. package/scripts/types-strict.mjs +141 -0
  153. package/skills.js +8 -1
  154. package/tools/adopt.js +2 -3
  155. package/tools/batch.js +2 -3
  156. package/tools/complete.js +2 -3
  157. package/tools/diff.js +3 -3
  158. package/tools/doctor.js +7 -10
  159. package/tools/finding.js +2 -3
  160. package/tools/findings.js +3 -3
  161. package/tools/import-legacy.js +3 -5
  162. package/tools/inbox.js +3 -3
  163. package/tools/index.js +9 -8
  164. package/tools/init.js +3 -6
  165. package/tools/interrupt.js +2 -3
  166. package/tools/legacy-map.js +3 -5
  167. package/tools/merge.js +3 -3
  168. package/tools/mission.js +2 -3
  169. package/tools/plan.js +6 -5
  170. package/tools/preflight.js +2 -3
  171. package/tools/rebase.js +3 -3
  172. package/tools/recover.js +38 -13
  173. package/tools/reload.js +36 -40
  174. package/tools/resource.js +3 -5
  175. package/tools/review.js +3 -4
  176. package/tools/roster.js +2 -3
  177. package/tools/send.js +2 -2
  178. package/tools/spawn.js +8 -5
  179. package/tools/status.js +11 -4
  180. package/tools/teardown.js +2 -3
  181. package/tools/test.js +2 -2
  182. package/types/index.d.ts +29 -13
  183. package/ui-bundle/README.md +37 -3
  184. package/ui-bundle/client.cards.js +356 -0
  185. package/ui-bundle/client.denied.js +142 -0
  186. package/ui-bundle/client.dock.js +333 -0
  187. package/ui-bundle/client.helpers.js +271 -0
  188. package/ui-bundle/client.js +79 -1747
  189. package/ui-bundle/client.locales.js +247 -0
  190. package/ui-bundle/client.mode.js +206 -0
  191. package/ui-bundle/client.overlay.js +64 -0
  192. package/ui-bundle/client.styles.js +151 -0
  193. package/ui-bundle/client.toolview.js +82 -0
  194. package/ui-bundle/package.json +1 -0
  195. package/lib/persistence.js +0 -356
  196. package/lib/presets.js +0 -35
  197. package/lib/store.js +0 -133
  198. /package/{lib → adapters}/stateFile.js +0 -0
  199. /package/{lib → app}/briefing.js +0 -0
  200. /package/{lib → app}/spawn-support.js +0 -0
  201. /package/{lib → host}/compatibility.js +0 -0
  202. /package/{lib → host}/config.js +0 -0
  203. /package/{lib → host}/modeState.js +0 -0
package/skills.js CHANGED
@@ -3,9 +3,12 @@
3
3
  // the short system-prompt sections stay authoritative for safety rules.
4
4
  import { readFile } from 'node:fs/promises'
5
5
  import { fileURLToPath } from 'node:url'
6
+ import { holdComponent } from './host/capability-registry.js'
6
7
 
7
8
  export const name = 'dsh-tower/skills'
8
- export const inject = ['skills']
9
+ // #121: an optional capability row still depends on the host service, so a
10
+ // composition with the host group off cannot leave an orphan skill provider.
11
+ export const inject = ['skills', 'tower']
9
12
 
10
13
  const ASSETS = fileURLToPath(new URL('./skills/', import.meta.url))
11
14
 
@@ -59,5 +62,9 @@ const provider = {
59
62
  }
60
63
 
61
64
  export function apply(ctx) {
65
+ // #121: tower-skills is an optional component row. Registering here is what
66
+ // lets tower_status/tower_doctor report "disabled by component tower-skills"
67
+ // when the row is off instead of leaving the missing catalog unexplained.
68
+ holdComponent(ctx, 'tower-skills')
62
69
  ctx.skills.registerProvider(() => provider)
63
70
  }
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,7 +6,7 @@ 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
@@ -21,7 +20,7 @@ export function registerCompleteTool(ctx, deps) {
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,\ case-sensitivity\ detection\ \(caseInsensitive\ and\ the\ active\ scope\ comparison\ mode\),\ config,\ presets/subagents/HMR,\ 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\ and\ an\ environment\ contract\.\ Never\ writes\ state\ and\ never\ probes\ the\ filesystem\.',
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
@@ -4,14 +4,15 @@
4
4
  // - define(ctx, definition) accepts a TypeBox parameters schema;
5
5
  // - execute(args) is typed from that schema by Static<>;
6
6
  // - aliases and requiresMissionId are honored by the registry;
7
- // - operational switches decide whether a tool is registered;
7
+ // - capability ownership decides whether a tool is registered; the component
8
+ // rows in cordis.patch.yml are the switches (#121);
8
9
  // - errors are classified to stable [tower:code] values;
9
10
  // - tools must not import node:fs or node:child_process directly.
10
11
  //
11
12
  // Tests import this module for the public surface; they may also
12
- // import lib/tool-registry.js for low-level test hooks.
13
+ // import host/tool-registry.js for low-level test hooks.
13
14
  //
14
- // Packaging: package.json ships tools/*.js and lib/tool-registry.js.
15
+ // Packaging: package.json ships tools/*.js and host/tool-registry.js.
15
16
  // The file intentionally stays tiny so handler modules remain the
16
17
  // only place where a tool is defined.
17
18
  // Keep this comment and the re-export list in sync when adding tools.
@@ -20,7 +21,7 @@
20
21
  //
21
22
  // The registration aggregator, guards, rate limits, spawn backoff,
22
23
  // repository preparation, discovery helpers, and verb helpers live in
23
- // lib/tool-registry.js. This module only re-exports the public entry
24
+ // host/tool-registry.js. This module only re-exports the public entry
24
25
  // points consumed by the Host plugin, tests, and docs generators.
25
26
  //
26
27
  // Design rules:
@@ -30,10 +31,10 @@
30
31
  // - no business logic in this file;
31
32
  // - keep the surface stable for index.js and tests.
32
33
  //
33
- // See docs/architecture/overview.md and lib/tool-registry.js.
34
+ // See docs/architecture/overview.md and host/tool-registry.js.
34
35
  // Tower tool surface: thin public wiring (#56 P5).
35
- // Registration and shared machinery live in lib/tool-registry.js; one handler
36
+ // Registration and shared machinery live in host/tool-registry.js; one handler
36
37
  // module per tool lives under tools/. This file only exposes the public entry
37
38
  // 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'
39
+ export { registerTowerTools, __testSpawnBackoff } from '../host/tool-registry.js'
40
+ export { workerBriefing, reviewerBriefing } from '../app/briefing.js'
package/tools/init.js CHANGED
@@ -1,18 +1,15 @@
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:
@@ -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,17 +10,18 @@
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:
@@ -29,7 +30,7 @@ export function registerPlanTool(ctx, deps) {
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 {
package/tools/rebase.js CHANGED
@@ -9,15 +9,15 @@
9
9
  // Pure intent/waiver decisions live in domain/integration.
10
10
  // See docs/reference/tools.generated.md for the public shape.
11
11
  // tower_rebase: rebase a mission branch inside its worktree.
12
- import { Tower } from '../app/tower.js'
13
12
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
14
13
  import { RebaseInput } 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 registerRebaseTool(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_rebase',
@@ -29,7 +29,7 @@ export function registerRebaseTool(ctx, deps) {
29
29
  board: true,
30
30
  output: output(SCHEMAS.rebase),
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, 'rebase missions')
35
35
  const { activity } = await observeActivity(ctx, tower)
package/tools/recover.js CHANGED
@@ -1,31 +1,39 @@
1
1
  // tower_recover: explicit stale state-lock diagnostics/repair.
2
- import { Tower } from '../app/tower.js'
3
- import { readStateLock } from '../lib/store.js'
4
2
  import { SCHEMAS, output } from '../domain/contracts/outputs.js'
5
3
  import { TowerToolError } from '../domain/errors.js'
6
- import { fsp } from '../adapters/store-fs.js'
7
4
  import { RecoverInput } from '../domain/contracts/tool-inputs/index.js'
8
5
  import { RECOVERY_ACTION, RECOVERY_CONFIRM, ROLE } 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 }} deps
9
+ * @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, readStateLock: any, lockTiming: any }} deps
13
10
  */
14
11
  export function registerRecoverTool(ctx, deps) {
15
- const { define, actorFor, requireTowerActor } = deps
12
+ const { define, actorFor, requireTowerActor, readStateLock, lockTiming } = deps
16
13
  define(ctx, {
17
14
  name: 'tower_recover',
18
15
  description:
19
- 'Read-only recovery diagnostics and an explicit stale-state-lock repair. status reports the state lock owner; clear-stale-lock requires confirm:"CLEAR_STALE_LOCK" and only moves a lock whose PID is provably dead to a backup file. It never resets Tower state or worktrees.',
16
+ 'Read-only recovery diagnostics and an explicit stale-state-lock repair. status reports the state lock owner with its age, heartbeat age and staleness; clear-stale-lock requires confirm:"CLEAR_STALE_LOCK" and only moves a lock whose PID is provably dead to a backup file. force:true additionally clears a live-but-silent PID (a recycled PID) and still renames the record to a .recovered-<ts> backup. It never resets Tower state or worktrees.',
20
17
  parameters: RecoverInput,
21
18
  output: output(SCHEMAS.recover),
22
19
  async execute(args, exec) {
23
- const tower = await Tower.open(args.repoRoot)
20
+ const tower = await deps.openTower(args.repoRoot)
24
21
  const actor = await actorFor(tower, exec)
25
22
  requireTowerActor(actor, 'recover Tower state')
26
- const lock = await readStateLock(tower.paths)
23
+ const now = Date.now()
24
+ const lock = await readStateLock(tower.paths, { now })
25
+ const timing = lockTiming(lock.record, { now })
27
26
  const publicLock = lock.record
28
- ? { pid: lock.record.pid ?? null, startedAt: lock.record.startedAt ?? null, at: lock.record.at ?? null, alive: lock.alive }
27
+ ? {
28
+ pid: lock.record.pid ?? null,
29
+ startedAt: lock.record.startedAt ?? null,
30
+ at: lock.record.at ?? null,
31
+ alive: lock.alive,
32
+ ageMs: timing.ageMs,
33
+ heartbeatAgeMs: timing.heartbeatAgeMs,
34
+ staleAfterMs: timing.staleAfterMs,
35
+ stale: timing.stale,
36
+ }
29
37
  : null
30
38
  if (args.action === RECOVERY_ACTION.status) {
31
39
  return { ok: true, action: RECOVERY_ACTION.status, stateLock: publicLock, cleared: false }
@@ -34,12 +42,29 @@ export function registerRecoverTool(ctx, deps) {
34
42
  throw new TowerToolError('invalid-argument', 'clear-stale-lock requires confirm:"CLEAR_STALE_LOCK"')
35
43
  }
36
44
  if (!lock.record) return { ok: true, action: RECOVERY_ACTION.clearStaleLock, stateLock: null, cleared: false }
37
- if (lock.alive !== false) {
38
- throw new TowerToolError('state-locked', `state lock pid ${lock.record.pid ?? ROLE.unknown} is still alive; refusing to clear it`)
45
+ const forced = args.force === true
46
+ if (lock.alive !== false && !forced) {
47
+ // A live PID is not proof of a live writer (#92): report the heartbeat
48
+ // and require the explicit force opt-in before moving the record.
49
+ throw new TowerToolError(
50
+ 'state-locked',
51
+ `state lock pid ${lock.record.pid ?? ROLE.unknown} is still alive (last heartbeat ${timing.heartbeatAgeMs ?? 'unknown'}ms ago, stale after ${timing.staleAfterMs}ms); pass force:true with confirm:"CLEAR_STALE_LOCK" to move it to a backup after confirming no writer is active`,
52
+ )
39
53
  }
54
+ // Always rename, never unlink: the recovered bytes stay auditable. Use the
55
+ // plain port primitive, not `renameWithRetry`: the retrying helper has
56
+ // temp-file semantics and unlinks its source when the rename finally
57
+ // fails, which would destroy the record we are trying to preserve.
40
58
  const backup = `${tower.paths.lock}.recovered-${Date.now()}`
41
- await fsp.rename(tower.paths.lock, backup)
42
- return { ok: true, action: RECOVERY_ACTION.clearStaleLock, stateLock: publicLock, cleared: true, backup }
59
+ await tower.deps.store.rename(tower.paths.lock, backup)
60
+ return {
61
+ ok: true,
62
+ action: RECOVERY_ACTION.clearStaleLock,
63
+ stateLock: publicLock,
64
+ cleared: true,
65
+ backup,
66
+ forced,
67
+ }
43
68
  },
44
69
  })
45
70
  }