dsh-tower 2.0.3 → 2.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/COMPATIBILITY.md +38 -0
  3. package/CONTRIBUTING.md +150 -2
  4. package/README.en.md +59 -16
  5. package/README.md +57 -16
  6. package/SECURITY.md +31 -1
  7. package/adapters/default.js +10 -0
  8. package/adapters/store-fs.js +6 -0
  9. package/adapters/vcs-git.js +4 -0
  10. package/app/batch.js +79 -0
  11. package/app/board.js +25 -0
  12. package/app/checkCompletion.js +76 -0
  13. package/app/enrichedMissions.js +26 -0
  14. package/app/findings.js +78 -0
  15. package/app/gate.js +108 -0
  16. package/app/init.js +249 -0
  17. package/app/merge.js +567 -0
  18. package/app/messages.js +66 -0
  19. package/app/mission.js +35 -0
  20. package/app/missionContext.js +14 -0
  21. package/app/plan.js +42 -0
  22. package/app/provision.js +193 -0
  23. package/app/read.js +57 -0
  24. package/app/rebase.js +302 -0
  25. package/app/recording.js +72 -0
  26. package/app/review.js +145 -0
  27. package/app/roster.js +123 -0
  28. package/app/spawn.js +125 -0
  29. package/app/teardown.js +184 -0
  30. package/app/tower.js +524 -0
  31. package/cordis.patch.yml +61 -53
  32. package/docs/README.md +33 -1
  33. package/docs/architecture/overview.md +114 -14
  34. package/docs/board.md +72 -0
  35. package/docs/examples/first-mission.md +2 -1
  36. package/docs/faq.md +11 -7
  37. package/docs/guide/hot-reload.md +1 -1
  38. package/docs/guide/install-upgrade-migration.md +22 -6
  39. package/docs/guide/quickstart.md +12 -7
  40. package/docs/guide/release.md +125 -0
  41. package/docs/guide/scenarios-and-philosophy.md +86 -69
  42. package/docs/operations/security.md +65 -14
  43. package/docs/reference/blockers.md +11 -10
  44. package/docs/reference/config.generated.md +66 -0
  45. package/docs/reference/config.md +11 -7
  46. package/docs/reference/errors.generated.md +39 -0
  47. package/docs/reference/errors.md +10 -2
  48. package/docs/reference/events.md +7 -4
  49. package/docs/reference/testing.md +2 -1
  50. package/docs/reference/tools.generated.md +33 -0
  51. package/docs/reference/tools.md +8 -7
  52. package/docs/zh/README.md +4 -3
  53. package/docs/zh/faq.md +9 -6
  54. package/docs/zh/guide/hot-reload.md +1 -1
  55. package/docs/zh/guide/install-upgrade-migration.md +17 -5
  56. package/docs/zh/guide/quickstart.md +13 -8
  57. package/docs/zh/guide/scenarios-and-philosophy.md +55 -50
  58. package/docs/zh/operations/security.md +43 -7
  59. package/docs/zh/reference/blockers.md +10 -10
  60. package/docs/zh/reference/config.md +11 -7
  61. package/docs/zh/reference/errors.md +4 -1
  62. package/docs/zh/reference/events.md +6 -3
  63. package/docs/zh/reference/testing.md +3 -2
  64. package/docs/zh/reference/tools.md +8 -7
  65. package/domain/collab/batch.js +112 -0
  66. package/domain/collab/messages.js +127 -0
  67. package/domain/collab/roster.js +158 -0
  68. package/domain/collab/spawn.js +188 -0
  69. package/domain/config/schema.js +404 -0
  70. package/domain/contracts/outputs.js +279 -0
  71. package/domain/contracts/tool-inputs/collaboration.js +58 -0
  72. package/domain/contracts/tool-inputs/findings.js +29 -0
  73. package/domain/contracts/tool-inputs/index.js +94 -0
  74. package/domain/contracts/tool-inputs/integration.js +37 -0
  75. package/domain/contracts/tool-inputs/missions.js +94 -0
  76. package/domain/contracts/tool-inputs/ops.js +75 -0
  77. package/domain/contracts/tool-inputs/shared.js +25 -0
  78. package/domain/errors.js +96 -0
  79. package/domain/gate.js +125 -0
  80. package/domain/integration/gate.js +176 -0
  81. package/domain/integration/init.js +225 -0
  82. package/domain/integration/liveness.js +31 -0
  83. package/domain/integration/merge.js +377 -0
  84. package/domain/integration/provision.js +124 -0
  85. package/domain/integration/rebase.js +236 -0
  86. package/domain/integration/review.js +52 -0
  87. package/domain/integration/teardown.js +118 -0
  88. package/domain/mission.js +68 -0
  89. package/domain/missions/completion.js +114 -0
  90. package/domain/missions/findings.js +201 -0
  91. package/domain/missions/lifecycle.js +185 -0
  92. package/domain/missions/plan.js +149 -0
  93. package/domain/missions/review.js +179 -0
  94. package/domain/planning.js +118 -0
  95. package/domain/read/enrichedMissions.js +58 -0
  96. package/domain/read/missionContext.js +53 -0
  97. package/domain/read/views.js +128 -0
  98. package/domain/report.js +59 -0
  99. package/domain/roster.js +40 -0
  100. package/domain/scope.js +105 -0
  101. package/domain/state/schema.js +233 -0
  102. package/domain/state/semantics.js +117 -0
  103. package/domain/state.js +33 -0
  104. package/domain/summary.js +114 -0
  105. package/domain/vocabulary.js +179 -0
  106. package/index.js +86 -34
  107. package/lib/briefing.js +176 -0
  108. package/lib/compatibility.js +108 -0
  109. package/lib/config.js +54 -302
  110. package/lib/doctor.js +108 -9
  111. package/lib/git.js +93 -15
  112. package/lib/hmr.js +4 -3
  113. package/lib/leases.js +3 -2
  114. package/lib/legacy.js +20 -9
  115. package/lib/modeState.js +39 -0
  116. package/lib/modeStore.js +35 -33
  117. package/lib/persistence.js +349 -0
  118. package/lib/presets.js +35 -0
  119. package/lib/spawn-prechecks.js +168 -0
  120. package/lib/spawn-support.js +79 -0
  121. package/lib/store.js +57 -85
  122. package/lib/tests.js +46 -11
  123. package/lib/tool-registry.js +544 -0
  124. package/lib/workspaceLock.js +46 -22
  125. package/lib/worktreeOwnership.js +5 -4
  126. package/locale/en.json +23 -1
  127. package/locale/zh.json +22 -0
  128. package/package.json +33 -4
  129. package/ports/clock.js +6 -0
  130. package/ports/ids.js +7 -0
  131. package/ports/index.js +2 -0
  132. package/ports/store.js +11 -0
  133. package/ports/vcs.js +11 -0
  134. package/scripts/architecture.mjs +94 -0
  135. package/scripts/check-docs.mjs +44 -3
  136. package/scripts/check-pack.mjs +49 -0
  137. package/scripts/domain-metrics.mjs +48 -0
  138. package/scripts/generate-config-reference.mjs +46 -0
  139. package/scripts/generate-error-reference.mjs +29 -0
  140. package/scripts/generate-tools-reference.mjs +56 -0
  141. package/scripts/issue-audit.mjs +259 -0
  142. package/scripts/lib/cli.mjs +76 -0
  143. package/scripts/lib/tmp.mjs +20 -0
  144. package/scripts/mount-smoke/fixtures.mjs +162 -0
  145. package/scripts/mount-smoke/host.json +4 -0
  146. package/scripts/mount-smoke/probe.mjs +125 -0
  147. package/scripts/mount-smoke/surface.mjs +141 -0
  148. package/scripts/mount-smoke.mjs +270 -0
  149. package/scripts/pack-manifest.txt +203 -0
  150. package/scripts/release-check.mjs +303 -0
  151. package/skills/tower-recovery.md +7 -5
  152. package/tools/adopt.js +72 -0
  153. package/tools/batch.js +78 -0
  154. package/tools/complete.js +53 -0
  155. package/tools/diff.js +39 -0
  156. package/tools/doctor.js +50 -0
  157. package/tools/finding.js +40 -0
  158. package/tools/findings.js +39 -0
  159. package/tools/import-legacy.js +42 -0
  160. package/tools/inbox.js +39 -0
  161. package/tools/index.js +39 -0
  162. package/tools/init.js +63 -0
  163. package/tools/interrupt.js +42 -0
  164. package/tools/legacy-map.js +44 -0
  165. package/tools/merge.js +39 -0
  166. package/tools/mission.js +41 -0
  167. package/tools/plan.js +39 -0
  168. package/tools/preflight.js +44 -0
  169. package/tools/rebase.js +39 -0
  170. package/tools/recover.js +45 -0
  171. package/tools/reload.js +52 -0
  172. package/tools/resource.js +54 -0
  173. package/tools/review.js +45 -0
  174. package/tools/roster.js +60 -0
  175. package/tools/send.js +147 -0
  176. package/tools/spawn.js +196 -0
  177. package/tools/status.js +94 -0
  178. package/tools/teardown.js +42 -0
  179. package/tools/test.js +61 -0
  180. package/types/cordis.d.ts +6 -0
  181. package/types/index.d.ts +339 -0
  182. package/ui-bundle/README.md +111 -0
  183. package/ui-bundle/client.js +1980 -0
  184. package/ui-bundle/cordis.patch.yml +3 -0
  185. package/ui-bundle/icon.svg +6 -0
  186. package/ui-bundle/index.js +2 -0
  187. package/ui-bundle/locale/en.json +6 -0
  188. package/ui-bundle/locale/zh.json +6 -0
  189. package/ui-bundle/package.json +35 -0
  190. package/lib/errors.js +0 -111
  191. package/lib/tools.js +0 -2250
  192. package/lib/tower.js +0 -2689
  193. /package/{lib → domain}/batch.js +0 -0
@@ -0,0 +1,39 @@
1
+ // tower_findings: list, read, or update findings.
2
+ import { Tower } from '../app/tower.js'
3
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
4
+ import { FindingsInput } from '../domain/contracts/tool-inputs/index.js'
5
+ import { ROLE } from '../domain/vocabulary.js'
6
+
7
+ /**
8
+ * @param {any} ctx
9
+ * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireKnownActor: any }} deps
10
+ */
11
+ export function registerFindingsTool(ctx, deps) {
12
+ const { define, actorFor, requireKnownActor } = deps
13
+ define(ctx, {
14
+ name: 'tower_findings',
15
+ description:
16
+ 'List, read, or update findings for a repository. Without id it lists findings newest-first with counts by status (filters: status, type, severity, missionId, open, limit). With id it returns that finding; with id plus status and/or note it records a lifecycle transition. Statuses are open -> triaged -> fixed|wontfix, and fixed/wontfix can be reopened to open. Only the tower or a worker may change a finding; reviewers may only list and read. Read-only when no id is given.',
17
+ parameters: FindingsInput,
18
+ aliases: {"missionId":["mission_id"]},
19
+ output: output(SCHEMAS.findings),
20
+ async execute(args, exec) {
21
+ const tower = await Tower.open(args.repoRoot)
22
+ const actor = await actorFor(tower, exec)
23
+ requireKnownActor(actor, 'tower_findings')
24
+ const view = await tower.rosterView()
25
+ const self = view.roster.find((entry) => entry.agentId === String(exec.agent.id))
26
+ return tower.findings({
27
+ id: args.id,
28
+ status: args.status,
29
+ note: args.note,
30
+ type: args.type,
31
+ severity: args.severity,
32
+ missionId: args.missionId,
33
+ open: args.open,
34
+ limit: args.limit,
35
+ actor: { name: self ? self.name : ROLE.tower, role: self ? self.role : ROLE.tower },
36
+ })
37
+ },
38
+ })
39
+ }
@@ -0,0 +1,42 @@
1
+ // tower_import_legacy — import a legacy snapshot read-only.
2
+ //
3
+ // Input: ImportLegacyInput (sourcePath) from contracts.
4
+ // Output: snapshot summary; state is never activated or adopted.
5
+ // Authorization: tower only.
6
+ // Side effects: writes a content-addressed legacy snapshot only.
7
+ // Failure modes: invalid legacy payload, path inside tower state.
8
+ // Pure legacy validation lives in domain and never runs fs.
9
+ // See docs/reference/tools.generated.md for the public shape.
10
+ // tower_import_legacy: read-only legacy snapshot import.
11
+ import { Tower } from '../app/tower.js'
12
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
13
+ import { importLegacySnapshot } from '../lib/legacy.js'
14
+ import { ImportLegacyInput } from '../domain/contracts/tool-inputs/index.js'
15
+
16
+ /**
17
+ * @param {any} ctx
18
+ * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
19
+ */
20
+ export function registerImportLegacyTool(ctx, deps) {
21
+ const { define, actorFor, requireTowerActor } = deps
22
+ define(ctx, {
23
+ name: 'tower_import_legacy',
24
+ description:
25
+ 'Read-only import of a Tower Codex (schemaVersion 1) or DSH legacy state file. Stores the full payload under <common>/dsh-tower/legacy/<sha256>.json and records only a summary in state.legacySnapshots; never activates, adopts, or resumes paused missions.',
26
+ parameters: ImportLegacyInput,
27
+ board: true,
28
+ output: output(SCHEMAS.importLegacy),
29
+ async execute(args, exec) {
30
+ const tower = await Tower.open(args.repoRoot)
31
+ const actor = await actorFor(tower, exec)
32
+ requireTowerActor(actor, 'import a legacy snapshot')
33
+ const imported = await importLegacySnapshot({
34
+ repoRoot: tower.root,
35
+ commonDir: tower.commonDir,
36
+ sourcePath: args.sourcePath,
37
+ })
38
+ const recorded = await tower.recordLegacySnapshot(imported.snapshot)
39
+ return { ok: true, summary: imported.summary, snapshot: recorded.snapshot }
40
+ },
41
+ })
42
+ }
package/tools/inbox.js ADDED
@@ -0,0 +1,39 @@
1
+ // Acknowledged messages are filtered from later unread counts.
2
+ // Delivery results are persisted by the registry.
3
+ // tower_inbox — read and acknowledge mission messages.
4
+ //
5
+ // Input: InboxInput (name, limit) from domain contracts.
6
+ // Output: unread messages plus acknowledgement metadata.
7
+ // Authorization: the addressed roster agent or the tower.
8
+ // Side effects: marks delivered messages as read and persists state.
9
+ // Failure modes: unknown recipient, invalid limit.
10
+ // The unread calculation is a pure domain/collab projection.
11
+ // See docs/reference/tools.generated.md for the public shape.
12
+ // tower_inbox: read and acknowledge inbound messages.
13
+ import { Tower } from '../app/tower.js'
14
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
15
+ import { InboxInput } from '../domain/contracts/tool-inputs/index.js'
16
+ import { ROLE } from '../domain/vocabulary.js'
17
+
18
+ /**
19
+ * @param {any} ctx
20
+ * @param {{ define: import('../types/index.js').DefineTool }} deps
21
+ */
22
+ export function registerInboxTool(ctx, deps) {
23
+ const { define } = deps
24
+ define(ctx, {
25
+ name: 'tower_inbox',
26
+ description:
27
+ 'Read your tower inbox: messages addressed to you plus broadcasts, newest first. Only returned messages are acknowledged; the result reports unreadRemaining, so keep reading until it reaches 0. Set include_read for history. Unread messages block tower_complete.',
28
+ parameters: InboxInput,
29
+ aliases: {"include_read":["includeRead"]},
30
+ output: output(SCHEMAS.inbox),
31
+ async execute(args, exec) {
32
+ const tower = await Tower.open(args.repoRoot)
33
+ const view = await tower.rosterView()
34
+ const self = view.roster.find((entry) => entry.agentId === String(exec.agent.id))
35
+ const reader = self ? self.name : ROLE.tower
36
+ return tower.inbox({ reader, limit: args.limit, includeRead: args.include_read === true })
37
+ },
38
+ })
39
+ }
package/tools/index.js ADDED
@@ -0,0 +1,39 @@
1
+ // Public tool wiring for the DSH Host plugin.
2
+ //
3
+ // Registration contract:
4
+ // - define(ctx, definition) accepts a TypeBox parameters schema;
5
+ // - execute(args) is typed from that schema by Static<>;
6
+ // - aliases and requiresMissionId are honored by the registry;
7
+ // - operational switches decide whether a tool is registered;
8
+ // - errors are classified to stable [tower:code] values;
9
+ // - tools must not import node:fs or node:child_process directly.
10
+ //
11
+ // Tests import this module for the public surface; they may also
12
+ // import lib/tool-registry.js for low-level test hooks.
13
+ //
14
+ // Packaging: package.json ships tools/*.js and lib/tool-registry.js.
15
+ // The file intentionally stays tiny so handler modules remain the
16
+ // only place where a tool is defined.
17
+ // Keep this comment and the re-export list in sync when adding tools.
18
+ // See docs/reference/tools.generated.md for the generated list.
19
+ // Tower tool surface — thin public wiring (#56).
20
+ //
21
+ // The registration aggregator, guards, rate limits, spawn backoff,
22
+ // repository preparation, discovery helpers, and verb helpers live in
23
+ // lib/tool-registry.js. This module only re-exports the public entry
24
+ // points consumed by the Host plugin, tests, and docs generators.
25
+ //
26
+ // Design rules:
27
+ // - one handler module per tool under tools/;
28
+ // - tool schemas live in domain/contracts/tool-inputs;
29
+ // - vocabulary lives in domain/vocabulary.js;
30
+ // - no business logic in this file;
31
+ // - keep the surface stable for index.js and tests.
32
+ //
33
+ // See docs/architecture/overview.md and lib/tool-registry.js.
34
+ // Tower tool surface: thin public wiring (#56 P5).
35
+ // Registration and shared machinery live in lib/tool-registry.js; one handler
36
+ // module per tool lives under tools/. This file only exposes the public entry
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'
package/tools/init.js ADDED
@@ -0,0 +1,63 @@
1
+ // tower_init: repository initialization.
2
+ import { Tower } from '../app/tower.js'
3
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
4
+ import { TowerToolError } from '../domain/errors.js'
5
+ import { resolveRepo } from '../lib/git.js'
6
+ import { runtimeConfig } from '../lib/config.js'
7
+ import { InitInput } from '../domain/contracts/tool-inputs/index.js'
8
+ import { isTowerRole, isUnknownRole } from '../domain/vocabulary.js'
9
+
10
+ /**
11
+ * @param {any} ctx
12
+ * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, workspaceHook: any, ensureRepository: any }} deps
13
+ */
14
+ export function registerInitTool(ctx, deps) {
15
+ const { define, actorFor, requireTowerActor, workspaceHook, ensureRepository } = deps
16
+ define(ctx, {
17
+ name: 'tower_init',
18
+ 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.',
20
+ parameters: InitInput,
21
+ aliases: { auto_git_init: ['autoGitInit'], snapshot_dirty: ['snapshotDirty'] },
22
+ board: true,
23
+ output: output(SCHEMAS.init),
24
+ async execute(args, exec) {
25
+ const profileInit = runtimeConfig().init ?? {}
26
+ let repo = null
27
+ try {
28
+ repo = await resolveRepo(args.repoRoot)
29
+ } catch (error) {
30
+ const autoGitInit =
31
+ args.autoGitInit === true || (args.autoGitInit === undefined && profileInit.autoGitInit === true)
32
+ repo = await ensureRepository(args.repoRoot, autoGitInit)
33
+ }
34
+ const tower = await Tower.open(repo.root)
35
+ const initConfig = tower.config.init ?? {}
36
+ const snapshotDirty =
37
+ args.snapshotDirty === true || (args.snapshotDirty === undefined && initConfig.snapshotDirty === true)
38
+ const agentId = exec?.agent?.id !== undefined ? String(exec.agent.id) : null
39
+ if (!agentId) {
40
+ throw new TowerToolError('not-authorized', 'tower_init requires a caller agent id; refusing to treat an unidentified caller as the tower')
41
+ }
42
+ let state = null
43
+ try {
44
+ state = await tower.load()
45
+ } catch {
46
+ state = null
47
+ }
48
+ if (state?.towerAgentId) {
49
+ const actor = await actorFor(tower, exec)
50
+ requireTowerActor(actor, 'initialize Tower state')
51
+ } else if (state) {
52
+ // Legacy state predating owner binding: an explicit tower_init may
53
+ // claim ownership once. Other tools remain fail-closed until then.
54
+ const actor = await actorFor(tower, exec)
55
+ if (!isTowerRole(actor.role) && !isUnknownRole(actor.role)) {
56
+ requireTowerActor(actor, 'claim Tower ownership')
57
+ }
58
+ }
59
+ const base = args.base ?? workspaceHook?.basePreference?.(exec) ?? undefined
60
+ return tower.init({ base, snapshotDirty, reanchor: args.reanchor, towerAgentId: agentId })
61
+ },
62
+ })
63
+ }
@@ -0,0 +1,42 @@
1
+ // tower_interrupt — stop one running roster agent.
2
+ //
3
+ // Input: InterruptInput (name/agentId/id) from domain contracts.
4
+ // Output: interrupted agent and updated roster observation.
5
+ // Authorization: tower or the worker parent for its own child.
6
+ // Side effects: interrupts the subagent and persists state.
7
+ // Failure modes: unknown agent, not running, not authorized.
8
+ // Roster state policy is pure and lives in domain/collab.
9
+ // See docs/reference/tools.generated.md for the public shape.
10
+ // tower_interrupt: interrupt one running roster agent.
11
+ import { Tower } from '../app/tower.js'
12
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
13
+ import { TowerToolError } from '../domain/errors.js'
14
+ import { InterruptInput } from '../domain/contracts/tool-inputs/index.js'
15
+
16
+ /**
17
+ * @param {any} ctx
18
+ * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, interruptRosterAgent: any }} deps
19
+ */
20
+ export function registerInterruptTool(ctx, deps) {
21
+ const { define, actorFor, requireTowerActor, interruptRosterAgent } = deps
22
+ define(ctx, {
23
+ name: 'tower_interrupt',
24
+ description:
25
+ 'Interrupt one running tower roster agent (worker or reviewer) and record it as interrupted in the roster. Only the tower may interrupt. Idempotent: an agent that is not running returns interrupted:false. Use it before takeover after a runaway worker, or to stop work that is no longer wanted.',
26
+ parameters: InterruptInput,
27
+ aliases: { agentId: ['agent_id'] },
28
+ output: output(SCHEMAS.interrupt),
29
+ async execute(args, exec) {
30
+ const tower = await Tower.open(args.repoRoot)
31
+ const actor = await actorFor(tower, exec)
32
+ requireTowerActor(actor, 'interrupt tower agents')
33
+ if (!args.to && !args.agentId) throw new TowerToolError('invalid-argument', 'tower_interrupt requires to (roster name) or agentId')
34
+ const result = await interruptRosterAgent(ctx, tower, exec, {
35
+ to: args.to,
36
+ agentId: args.agentId,
37
+ reason: args.reason,
38
+ })
39
+ return { ok: true, ...result }
40
+ },
41
+ })
42
+ }
@@ -0,0 +1,44 @@
1
+ // tower_legacy_map — map a legacy snapshot to a mission.
2
+ //
3
+ // Input: LegacyMapInput (action, sourceHash, oldId) from contracts.
4
+ // Output: mapping record or mapping status.
5
+ // Authorization: tower only.
6
+ // Side effects: appends audit mapping to state; no adoption.
7
+ // Failure modes: unknown snapshot, duplicate mapping, invalid id.
8
+ // Pure mapping validation lives in the domain legacy layer.
9
+ // See docs/reference/tools.generated.md for the public shape.
10
+ // tower_legacy_map: audit-only legacy snapshot mapping.
11
+ import { Tower } from '../app/tower.js'
12
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
13
+ import { TowerToolError } from '../domain/errors.js'
14
+ import { validateLegacyMap } from '../lib/legacy.js'
15
+ import { LegacyMapInput } from '../domain/contracts/tool-inputs/index.js'
16
+ import { LEGACY_MAP_ACTION } from '../domain/vocabulary.js'
17
+
18
+ /**
19
+ * @param {any} ctx
20
+ * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
21
+ */
22
+ export function registerLegacyMapTool(ctx, deps) {
23
+ const { define, actorFor, requireTowerActor } = deps
24
+ define(ctx, {
25
+ name: 'tower_legacy_map',
26
+ description:
27
+ 'Audit-only mapping between a legacy snapshot id and a current mission. map records sourceHash/oldId/newMissionId; status lists mappings. Mapping never adopts or resumes a legacy mission.',
28
+ parameters: LegacyMapInput,
29
+ output: output(SCHEMAS.legacyMap),
30
+ async execute(args, exec) {
31
+ const tower = await Tower.open(args.repoRoot)
32
+ const actor = await actorFor(tower, exec)
33
+ const state = await tower.load()
34
+ if (args.action === LEGACY_MAP_ACTION.status) {
35
+ return { ok: true, maps: state.legacyMaps ?? [] }
36
+ }
37
+ requireTowerActor(actor, 'map a legacy mission')
38
+ const validation = validateLegacyMap(state, args)
39
+ const map = { ...validation.map, by: actor.name, at: Date.now() }
40
+ const recorded = await tower.recordLegacyMap(map)
41
+ return { ok: true, map: recorded.map, maps: [...(state.legacyMaps ?? []), recorded.map] }
42
+ },
43
+ })
44
+ }
package/tools/merge.js ADDED
@@ -0,0 +1,39 @@
1
+ // The merge command uses the exact reviewed commit, never a mutable branch name.
2
+ // tower_merge — integrate a reviewed mission branch.
3
+ //
4
+ // Input: MergeInput (missionId/id, timeoutMs) from domain contracts.
5
+ // Output: merged commit, parents, review evidence, blockers.
6
+ // Authorization: tower only; the gate is re-evaluated.
7
+ // Side effects: git merge, state/activity writes, cleanup on failure.
8
+ // Failure modes: merge-conflict, merge-hook-failed, merge-shape.
9
+ // The pure intent/shape decisions live in domain/integration.
10
+ // See docs/reference/tools.generated.md for the public shape.
11
+ // tower_merge: merge a mission branch into the recorded base.
12
+ import { Tower } from '../app/tower.js'
13
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
14
+ import { MergeInput } from '../domain/contracts/tool-inputs/index.js'
15
+
16
+ /**
17
+ * @param {any} ctx
18
+ * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, observeActivity: any }} deps
19
+ */
20
+ export function registerMergeTool(ctx, deps) {
21
+ const { define, actorFor, requireTowerActor, observeActivity } = deps
22
+ define(ctx, {
23
+ name: 'tower_merge',
24
+ description:
25
+ 'Merge a mission branch into the recorded base branch with --no-ff, after the hard gate passes. Verifies the two merge parents and records the mission as merged. If the merge commit already landed (manual merge, crashed or slow pre-merge hook), it detects the merge commit and records it as reconciled instead of merging again. A failed gate returns stable blocker codes without merging; a hook failure aborts cleanly and returns "merge-hook-failed" while a real content conflict returns "merge-conflict".',
26
+ parameters: MergeInput,
27
+ aliases: {"missionId":["id"],"timeoutMs":["timeout_ms"]},
28
+ requiresMissionId: true,
29
+ board: true,
30
+ output: output(SCHEMAS.merge),
31
+ async execute(args, exec) {
32
+ const tower = await Tower.open(args.repoRoot)
33
+ const actor = await actorFor(tower, exec)
34
+ requireTowerActor(actor, 'merge missions')
35
+ const { activity, parentStatus } = await observeActivity(ctx, tower)
36
+ return tower.merge({ missionId: args.missionId, activity, parentStatus, timeoutMs: args.timeoutMs })
37
+ },
38
+ })
39
+ }
@@ -0,0 +1,41 @@
1
+ // tower_mission: mission lifecycle updates.
2
+ import { Tower } from '../app/tower.js'
3
+ import { TowerToolError } from '../domain/errors.js'
4
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
5
+ import { MissionInput } from '../domain/contracts/tool-inputs/index.js'
6
+
7
+ /**
8
+ * @param {any} ctx
9
+ * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireKnownActor: any }} deps
10
+ */
11
+ export function registerMissionTool(ctx, deps) {
12
+ const { define, actorFor, requireKnownActor } = deps
13
+ define(ctx, {
14
+ name: 'tower_mission',
15
+ description:
16
+ 'Update one mission: lifecycle status, decision note, blocker, clear blockers, mark a task done, drop a task with a reason, or widen the scope (tower only). "merged" is never set here; only a future tower_merge records it. Merged and abandoned are terminal.',
17
+ parameters: MissionInput,
18
+ aliases: {"id":["missionId"],"task_done":["taskDone"],"task_drop":["taskDrop"],"task_drop_reason":["taskDropReason"],"clear_blockers":["clearBlockers"]},
19
+ board: true,
20
+ output: output(SCHEMAS.mission),
21
+ async execute(args, exec) {
22
+ const tower = await Tower.open(args.repoRoot)
23
+ const actor = await actorFor(tower, exec)
24
+ requireKnownActor(actor, 'tower_mission')
25
+ return tower.mission({
26
+ actor,
27
+ id: args.id,
28
+ status: args.status,
29
+ note: args.note,
30
+ blocker: args.blocker,
31
+ clear_blockers: args.clear_blockers,
32
+ task_done: args.task_done,
33
+ task_drop: args.task_drop,
34
+ task_drop_reason: args.task_drop_reason,
35
+ scope: args.scope,
36
+ pause_cause: args.pause_cause,
37
+ authorization: args.authorization,
38
+ })
39
+ },
40
+ })
41
+ }
package/tools/plan.js ADDED
@@ -0,0 +1,39 @@
1
+ // Mission ids are assigned monotonically by the domain planner.
2
+ // tower_plan — create missions and assign M1.. ids.
3
+ //
4
+ // Input: PlanInput from domain contracts (missions, scope, deps).
5
+ // Output: created mission summaries and board state.
6
+ // Authorization: tower only; presets are validated first.
7
+ // Side effects: persists state and optional board file.
8
+ // It never creates worktrees; spawn/provision do that later.
9
+ // Failure modes: invalid scope, cyclic deps, preset missing.
10
+ // Pure normalization and DAG policy live in domain/missions.
11
+ // See docs/reference/tools.generated.md for the public shape.
12
+ // tower_plan: create missions.
13
+ import { Tower } from '../app/tower.js'
14
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
15
+ import { assertPresetsAvailable } from '../lib/presets.js'
16
+ import { PlanInput } from '../domain/contracts/tool-inputs/index.js'
17
+
18
+ /**
19
+ * @param {any} ctx
20
+ * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
21
+ */
22
+ export function registerPlanTool(ctx, deps) {
23
+ const { define, actorFor, requireTowerActor } = deps
24
+ define(ctx, {
25
+ name: 'tower_plan',
26
+ 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
+ parameters: PlanInput,
29
+ board: true,
30
+ output: output(SCHEMAS.plan),
31
+ async execute(args, exec) {
32
+ const tower = await Tower.open(args.repoRoot)
33
+ const actor = await actorFor(tower, exec)
34
+ requireTowerActor(actor, 'plan missions')
35
+ await assertPresetsAvailable(ctx, tower.config)
36
+ return tower.plan({ missions: args.missions })
37
+ },
38
+ })
39
+ }
@@ -0,0 +1,44 @@
1
+ // tower_preflight: read-only merge gate digest.
2
+ import { Tower } from '../app/tower.js'
3
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
4
+ import { classifyError } from '../domain/errors.js'
5
+ import { PreflightInput } from '../domain/contracts/tool-inputs/index.js'
6
+
7
+ /**
8
+ * @param {any} ctx
9
+ * @param {{ define: import('../types/index.js').DefineTool, observeActivity: any }} deps
10
+ */
11
+ export function registerPreflightTool(ctx, deps) {
12
+ const { define, observeActivity } = deps
13
+ define(ctx, {
14
+ name: 'tower_preflight',
15
+ description:
16
+ 'Read-only merge gate for one mission: returns ok plus stable blocker codes (branch-missing, deps-unmerged, read-only-survey, stale-base, out-of-scope, base-dirty, branch-owned-by-closed-missions, and mission/worker guards). Review blockers (no-review, review-hold, not-clean, review-not-approved, tip-moved, review-mission-mismatch) apply only when review.requireReview is on. Never changes anything.',
17
+ parameters: PreflightInput,
18
+ aliases: {"missionId":["id"]},
19
+ requiresMissionId: true,
20
+ output: output(SCHEMAS.preflight),
21
+ async execute(args) {
22
+ const tower = await Tower.open(args.repoRoot)
23
+ const { activity, parentStatus } = await observeActivity(ctx, tower)
24
+ const gate = await tower.evaluateGate({ missionId: args.missionId, activity, parentStatus })
25
+ return {
26
+ ok: gate.ok,
27
+ blockers: gate.blockers,
28
+ missionId: args.missionId,
29
+ branchTip: gate.branchTip ?? null,
30
+ baseTip: gate.baseTip ?? null,
31
+ changedFiles: gate.changedFiles ?? [],
32
+ review: gate.review
33
+ ? {
34
+ round: gate.review.round,
35
+ status: gate.review.status,
36
+ mergeVerdict: gate.review.mergeVerdict,
37
+ reviewer: gate.review.reviewer,
38
+ reviewedCommit: gate.review.reviewedCommit,
39
+ }
40
+ : null,
41
+ }
42
+ },
43
+ })
44
+ }
@@ -0,0 +1,39 @@
1
+ // A clean rebase may waive re-review by binding the old reviewed tip.
2
+ // tower_rebase — rebase a mission branch onto the base.
3
+ //
4
+ // Input: RebaseInput (missionId/id) from domain contracts.
5
+ // Output: old/new tips, base tip, review-waiver decision.
6
+ // Authorization: tower only; ownership and cleanliness are checked.
7
+ // Side effects: git rebase, abort/cleanup, state/activity writes.
8
+ // Failure modes: conflict, stale-base, tip-moved, dirty worktree.
9
+ // Pure intent/waiver decisions live in domain/integration.
10
+ // See docs/reference/tools.generated.md for the public shape.
11
+ // tower_rebase: rebase a mission branch inside its worktree.
12
+ import { Tower } from '../app/tower.js'
13
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
14
+ import { RebaseInput } from '../domain/contracts/tool-inputs/index.js'
15
+
16
+ /**
17
+ * @param {any} ctx
18
+ * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, observeActivity: any }} deps
19
+ */
20
+ export function registerRebaseTool(ctx, deps) {
21
+ const { define, actorFor, requireTowerActor, observeActivity } = deps
22
+ define(ctx, {
23
+ name: 'tower_rebase',
24
+ description:
25
+ 'Rebase a mission branch onto the current base tip inside its own worktree. Refuses while its worker is running or the worktree is dirty. A clean conflict-free rebase waives re-review for the old review; a conflict is aborted and the mission is marked blocked. If the branch is already contained in the base, it returns alreadyMerged and leaves the branch untouched so tower_merge can record the landed merge.',
26
+ parameters: RebaseInput,
27
+ aliases: {"missionId":["id"]},
28
+ requiresMissionId: true,
29
+ board: true,
30
+ output: output(SCHEMAS.rebase),
31
+ async execute(args, exec) {
32
+ const tower = await Tower.open(args.repoRoot)
33
+ const actor = await actorFor(tower, exec)
34
+ requireTowerActor(actor, 'rebase missions')
35
+ const { activity } = await observeActivity(ctx, tower)
36
+ return tower.rebase({ missionId: args.missionId, activity })
37
+ },
38
+ })
39
+ }
@@ -0,0 +1,45 @@
1
+ // tower_recover: explicit stale state-lock diagnostics/repair.
2
+ import { Tower } from '../app/tower.js'
3
+ import { readStateLock } from '../lib/store.js'
4
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
5
+ import { TowerToolError } from '../domain/errors.js'
6
+ import { fsp } from '../adapters/store-fs.js'
7
+ import { RecoverInput } from '../domain/contracts/tool-inputs/index.js'
8
+ import { RECOVERY_ACTION, RECOVERY_CONFIRM, ROLE } from '../domain/vocabulary.js'
9
+
10
+ /**
11
+ * @param {any} ctx
12
+ * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
13
+ */
14
+ export function registerRecoverTool(ctx, deps) {
15
+ const { define, actorFor, requireTowerActor } = deps
16
+ define(ctx, {
17
+ name: 'tower_recover',
18
+ 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.',
20
+ parameters: RecoverInput,
21
+ output: output(SCHEMAS.recover),
22
+ async execute(args, exec) {
23
+ const tower = await Tower.open(args.repoRoot)
24
+ const actor = await actorFor(tower, exec)
25
+ requireTowerActor(actor, 'recover Tower state')
26
+ const lock = await readStateLock(tower.paths)
27
+ const publicLock = lock.record
28
+ ? { pid: lock.record.pid ?? null, startedAt: lock.record.startedAt ?? null, at: lock.record.at ?? null, alive: lock.alive }
29
+ : null
30
+ if (args.action === RECOVERY_ACTION.status) {
31
+ return { ok: true, action: RECOVERY_ACTION.status, stateLock: publicLock, cleared: false }
32
+ }
33
+ if (args.confirm !== RECOVERY_CONFIRM) {
34
+ throw new TowerToolError('invalid-argument', 'clear-stale-lock requires confirm:"CLEAR_STALE_LOCK"')
35
+ }
36
+ 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`)
39
+ }
40
+ 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 }
43
+ },
44
+ })
45
+ }
@@ -0,0 +1,52 @@
1
+ // tower_reload: dev-only HMR reload request.
2
+ import { Tower } from '../app/tower.js'
3
+ import { SCHEMAS, output } from '../domain/contracts/outputs.js'
4
+ import { TowerToolError } from '../domain/errors.js'
5
+ import { runtimeConfig } from '../lib/config.js'
6
+ import { createHmrBridge } from '../lib/hmr.js'
7
+ import { ReloadInput } from '../domain/contracts/tool-inputs/index.js'
8
+
9
+ /**
10
+ * @param {any} ctx
11
+ * @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, sharedHmrBridge: any }} deps
12
+ */
13
+ export function registerReloadTool(ctx, deps) {
14
+ const { define, actorFor, requireTowerActor, sharedHmrBridge } = deps
15
+ // Dev-only hot reload. The tool is registered whenever dev.hotReload is on;
16
+ // it reports reload-unavailable when the profile has no HMR service. When the
17
+ // Host passed a shared bridge, its status() reflects the command's reloads too.
18
+ const dev = runtimeConfig().dev ?? {}
19
+ if (dev.hotReload !== false) {
20
+ const bridge =
21
+ sharedHmrBridge ??
22
+ createHmrBridge(ctx, {
23
+ requiredIds: dev.reloadRequiredIds,
24
+ delayMs: dev.reloadDelayMs,
25
+ logger: ctx.logger ?? console,
26
+ })
27
+ define(ctx, {
28
+ name: 'tower_reload',
29
+ description:
30
+ 'Dev-only: ask DSH HMR to reconcile this profile and reload the Tower Host plugin from disk. The reload is scheduled a few milliseconds after this call returns (so the result reaches the model first). Use after editing the plugin source in a linked profile; plugin_manager install/update already triggers reconciliation by itself. Registered while dev.hotReload is on; fails with reload-unavailable when the HMR service is absent.',
31
+ parameters: ReloadInput,
32
+ output: output(SCHEMAS.reload),
33
+ async execute(args, exec) {
34
+ if (typeof args.repoRoot === 'string') {
35
+ const tower = await Tower.open(args.repoRoot)
36
+ const actor = await actorFor(tower, exec)
37
+ requireTowerActor(actor, 'reload the Tower plugin')
38
+ }
39
+ if (!bridge.available) {
40
+ throw new TowerToolError('reload-unavailable', 'this DSH profile has no HMR service; restart DSH instead')
41
+ }
42
+ const outcome = bridge.schedule()
43
+ return {
44
+ ok: true,
45
+ ...outcome,
46
+ ...(outcome.lastError ? { error: outcome.lastError } : {}),
47
+ restartHint: 'If the new code does not appear, restart DSH or check the profile hmr entry.',
48
+ }
49
+ },
50
+ })
51
+ }
52
+ }