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.
- package/CHANGELOG.md +137 -0
- package/COMPATIBILITY.md +6 -1
- package/CONTRIBUTING.md +78 -5
- package/PRINCIPLES.md +163 -0
- package/README.en.md +35 -19
- package/README.md +28 -18
- package/SECURITY.md +6 -0
- package/{lib → adapters}/archive.js +1 -1
- package/{lib → adapters}/caseSensitivity.js +1 -1
- package/adapters/default.js +51 -10
- package/{lib → adapters}/git.js +1 -1
- package/{lib → adapters}/leases.js +1 -1
- package/{lib → adapters}/legacy.js +1 -1
- package/adapters/persistence.js +543 -0
- package/adapters/store-fs.js +28 -1
- package/adapters/store.js +204 -0
- package/{lib → adapters}/tests.js +9 -5
- package/adapters/vcs-git.js +15 -1
- package/{lib → adapters}/workspaceLock.js +1 -1
- package/{lib → adapters}/worktreeOwnership.js +1 -1
- package/app/archive.js +4 -6
- package/app/batch.js +9 -10
- package/app/board.js +2 -4
- package/app/checkCompletion.js +9 -10
- package/app/enrichedMissions.js +2 -4
- package/app/findings.js +6 -7
- package/app/gate.js +1 -2
- package/app/init.js +24 -29
- package/app/merge.js +58 -51
- package/app/messages.js +7 -8
- package/app/mission.js +5 -5
- package/app/plan.js +8 -5
- package/app/provision.js +12 -14
- package/app/read.js +6 -3
- package/app/rebase.js +14 -16
- package/app/recording.js +7 -8
- package/app/review.js +15 -8
- package/app/roster.js +14 -15
- package/{lib → app}/spawn-prechecks.js +4 -3
- package/app/spawn.js +12 -13
- package/app/teardown.js +8 -9
- package/app/tower.js +89 -35
- package/cordis.patch.yml +56 -28
- package/docs/README.md +7 -2
- package/docs/adr/0001-ports-adapters-as-data.md +49 -0
- package/docs/adr/0002-single-state-json.md +50 -0
- package/docs/adr/0003-allow-agent-control-default-true.md +48 -0
- package/docs/adr/0004-surface-budget.md +70 -0
- package/docs/adr/0005-component-surface.md +130 -0
- package/docs/adr/README.md +52 -0
- package/docs/agents/workflow.md +51 -0
- package/docs/architecture/overview.md +192 -43
- package/docs/guide/hot-reload.md +5 -5
- package/docs/guide/install-upgrade-migration.md +13 -0
- package/docs/guide/release.md +1 -1
- package/docs/operations/incident.md +82 -0
- package/docs/operations/runbook.md +12 -3
- package/docs/operations/security.md +1 -1
- package/docs/reference/blockers.md +2 -2
- package/docs/reference/component-surface.md +135 -0
- package/docs/reference/config.generated.md +2 -9
- package/docs/reference/config.md +11 -9
- package/docs/reference/doctor.md +53 -2
- package/docs/reference/errors.generated.md +41 -37
- package/docs/reference/errors.md +53 -41
- package/docs/reference/events.md +38 -6
- package/docs/reference/surface-budget.md +157 -0
- package/docs/reference/tools.generated.md +8 -5
- package/docs/reference/tools.md +10 -7
- package/docs/zh/README.md +1 -1
- package/docs/zh/guide/hot-reload.md +4 -4
- package/docs/zh/guide/install-upgrade-migration.md +10 -0
- package/docs/zh/operations/security.md +1 -1
- package/docs/zh/reference/blockers.md +2 -2
- package/docs/zh/reference/config.md +9 -9
- package/docs/zh/reference/doctor.md +29 -1
- package/docs/zh/reference/tools.md +10 -8
- package/domain/batch.js +2 -2
- package/domain/components.js +641 -0
- package/domain/config/schema.js +62 -28
- package/domain/contracts/outputs.js +16 -2
- package/domain/contracts/tool-inputs/ops.js +1 -0
- package/domain/errors.js +308 -43
- package/domain/events.js +207 -0
- package/domain/integration/gate.js +17 -2
- package/domain/integration/merge.js +35 -0
- package/domain/lockBudget.js +30 -0
- package/domain/mission.js +12 -1
- package/domain/missions/completion.js +14 -0
- package/domain/missions/lifecycle.js +9 -3
- package/domain/missions/plan.js +14 -0
- package/domain/missions/review.js +90 -7
- package/domain/read/views.js +3 -0
- package/domain/state/archive.js +1 -1
- package/domain/state.js +1 -1
- package/domain/surface.js +347 -0
- package/host/capability-batch.js +15 -0
- package/host/capability-hmr.js +30 -0
- package/host/capability-import.js +11 -0
- package/host/capability-leases.js +11 -0
- package/host/capability-registry.js +149 -0
- package/host/capability-tests.js +16 -0
- package/{lib → host}/doctor.js +157 -18
- package/{lib → host}/hmr.js +2 -4
- package/{lib → host}/modeStore.js +1 -1
- package/host/presets.js +77 -0
- package/{lib → host}/tool-registry.js +235 -44
- package/host/tower.js +32 -0
- package/index.js +40 -25
- package/locale/en.json +31 -11
- package/locale/zh.json +30 -10
- package/package.json +24 -4
- package/ports/archive.js +18 -0
- package/ports/caseSensitivity.js +15 -0
- package/ports/clock.js +10 -2
- package/ports/config.js +19 -0
- package/ports/contract.js +38 -0
- package/ports/events.js +24 -0
- package/ports/ids.js +10 -2
- package/ports/index.js +54 -2
- package/ports/ownership.js +17 -0
- package/ports/state.js +16 -0
- package/ports/store.js +52 -3
- package/ports/vcs.js +25 -4
- package/scripts/architecture-waivers.json +19 -0
- package/scripts/architecture.mjs +634 -23
- package/scripts/check-components.mjs +235 -0
- package/scripts/check-docs.mjs +8 -35
- package/scripts/check-pack.mjs +68 -39
- package/scripts/check-philosophy.mjs +266 -0
- package/scripts/component-surface.json +395 -0
- package/scripts/domain-metrics.mjs +13 -4
- package/scripts/generate-component-surface.mjs +59 -0
- package/scripts/generate-config-reference.mjs +14 -1
- package/scripts/generate-error-reference.mjs +68 -14
- package/scripts/generate-events-reference.mjs +83 -0
- package/scripts/generate-tools-reference.mjs +18 -25
- package/scripts/generate-types.mjs +127 -0
- package/scripts/issue-audit.mjs +2 -2
- package/scripts/lib/surface.mjs +87 -0
- package/scripts/lib/tool-surface.mjs +85 -0
- package/scripts/mount-smoke/fixtures.mjs +3 -3
- package/scripts/mount-smoke/surface.mjs +96 -27
- package/scripts/mount-smoke.mjs +3 -1
- package/scripts/pack-manifest.txt +75 -22
- package/scripts/regression-audit.mjs +164 -0
- package/scripts/release-check.mjs +1 -1
- package/scripts/surface-baseline.json +52 -0
- package/scripts/surface-baseline.mjs +118 -0
- package/scripts/types-coverage.mjs +184 -0
- package/scripts/types-strict-manifest.txt +10 -0
- package/scripts/types-strict.mjs +141 -0
- package/skills.js +8 -1
- package/tools/adopt.js +2 -3
- package/tools/batch.js +2 -3
- package/tools/complete.js +2 -3
- package/tools/diff.js +3 -3
- package/tools/doctor.js +7 -10
- package/tools/finding.js +2 -3
- package/tools/findings.js +3 -3
- package/tools/import-legacy.js +3 -5
- package/tools/inbox.js +3 -3
- package/tools/index.js +9 -8
- package/tools/init.js +3 -6
- package/tools/interrupt.js +2 -3
- package/tools/legacy-map.js +3 -5
- package/tools/merge.js +3 -3
- package/tools/mission.js +2 -3
- package/tools/plan.js +6 -5
- package/tools/preflight.js +2 -3
- package/tools/rebase.js +3 -3
- package/tools/recover.js +38 -13
- package/tools/reload.js +36 -40
- package/tools/resource.js +3 -5
- package/tools/review.js +3 -4
- package/tools/roster.js +2 -3
- package/tools/send.js +2 -2
- package/tools/spawn.js +8 -5
- package/tools/status.js +11 -4
- package/tools/teardown.js +2 -3
- package/tools/test.js +2 -2
- package/types/index.d.ts +29 -13
- package/ui-bundle/README.md +37 -3
- package/ui-bundle/client.cards.js +356 -0
- package/ui-bundle/client.denied.js +142 -0
- package/ui-bundle/client.dock.js +333 -0
- package/ui-bundle/client.helpers.js +271 -0
- package/ui-bundle/client.js +79 -1747
- package/ui-bundle/client.locales.js +247 -0
- package/ui-bundle/client.mode.js +206 -0
- package/ui-bundle/client.overlay.js +64 -0
- package/ui-bundle/client.styles.js +151 -0
- package/ui-bundle/client.toolview.js +82 -0
- package/ui-bundle/package.json +1 -0
- package/lib/persistence.js +0 -356
- package/lib/presets.js +0 -35
- package/lib/store.js +0 -133
- /package/{lib → adapters}/stateFile.js +0 -0
- /package/{lib → app}/briefing.js +0 -0
- /package/{lib → app}/spawn-support.js +0 -0
- /package/{lib → host}/compatibility.js +0 -0
- /package/{lib → host}/config.js +0 -0
- /package/{lib → host}/modeState.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 '
|
|
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 { assertRuntimePreconditions } 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,17 +50,97 @@ 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 '
|
|
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'
|
|
56
|
+
import { COMPONENT_TOOLS, componentEnabled, componentReport, holdComponent, disabledReason } from './capability-registry.js'
|
|
48
57
|
import path from 'node:path'
|
|
49
58
|
import { fsp } from '../adapters/store-fs.js'
|
|
59
|
+
import { createDefaultDeps } from '../adapters/default.js'
|
|
50
60
|
import crypto from 'node:crypto'
|
|
51
61
|
|
|
52
62
|
// Mutable composition state is grouped in one const object: the registration
|
|
53
63
|
// aggregator is the composition root, not an ambient module-level binding.
|
|
64
|
+
/**
|
|
65
|
+
* The workspace-lock hook the Host entry injects (`index.js`). Optional
|
|
66
|
+
* members are only present when the entry supplies them, so the registry and
|
|
67
|
+
* the tools that receive it declare the same shape.
|
|
68
|
+
* @typedef {object} WorkspaceHook
|
|
69
|
+
* @property {(repoRoot: string, exec: any, toolName: string) => Promise<void>} ensure
|
|
70
|
+
* @property {(exec: any) => string | null} [basePreference]
|
|
71
|
+
* @property {(exec: any) => Record<string, any> | null} [modeInfo]
|
|
72
|
+
* @property {(entry: any, key: string) => Promise<Record<string, any>>} [status]
|
|
73
|
+
*/
|
|
74
|
+
/** @type {{ workspaceHook: WorkspaceHook | null, hmrBridge: ReturnType<typeof createHmrBridge> | null, deps: Record<string, any> | null, definedTools: Set<string> }} */
|
|
54
75
|
const runtimeState = {
|
|
55
76
|
workspaceHook: null,
|
|
56
77
|
hmrBridge: null,
|
|
78
|
+
// #69: the host entry builds the adapter set once and passes it in. The lazy
|
|
79
|
+
// fallback is for tests/embedders that call registerTowerTools() directly;
|
|
80
|
+
// it is still built once per process.
|
|
81
|
+
deps: null,
|
|
82
|
+
// #84/#118/#121: registered tool names are recorded before any component row
|
|
83
|
+
// decides whether its capability mounts, so the concept budget measures the
|
|
84
|
+
// full build surface (29 tools) rather than one profile's mounted subset (28
|
|
85
|
+
// with the shipped `tower-hmr` row off). `recordToolRegistration` is the one
|
|
86
|
+
// recorder: define() calls it for every definition, the Host entry calls it
|
|
87
|
+
// for the directly registered tower_mode/tower_control pair, and the host
|
|
88
|
+
// records the declared capability tools (COMPONENT_TOOLS) because those entry
|
|
89
|
+
// points only run when their row is on. The number is reported by
|
|
90
|
+
// tower_doctor and compared with the committed scripts/surface-baseline.json.
|
|
91
|
+
definedTools: new Set(),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Record one tool name in the concept-budget surface (#84, #118, #121).
|
|
96
|
+
*
|
|
97
|
+
* This is the single recorder for the *build* tool surface. `define()` calls it
|
|
98
|
+
* for every definition when a capability is mounted; the Host entry (`index.js`)
|
|
99
|
+
* calls it for the two tools it registers directly, `tower_mode` and
|
|
100
|
+
* `tower_control`; and `registerCoreTowerTools` records the declared capability
|
|
101
|
+
* tools up front so a row that ships off does not shrink the budget. One
|
|
102
|
+
* recorder means the count cannot silently miss a registration path again: #118
|
|
103
|
+
* found 27 of the 29 names because only the `define()` path was recorded.
|
|
104
|
+
*/
|
|
105
|
+
export function recordToolRegistration(name) {
|
|
106
|
+
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
107
|
+
throw new TypeError('recordToolRegistration requires a non-empty tool name')
|
|
108
|
+
}
|
|
109
|
+
runtimeState.definedTools.add(name.trim())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The full shipped tool surface (names, sorted). It covers every tool this build
|
|
114
|
+
* can register, including capabilities whose component row is currently off.
|
|
115
|
+
*/
|
|
116
|
+
export function definedToolNames() {
|
|
117
|
+
return [...runtimeState.definedTools].sort()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The number of tools this build ships, used by the concept budget (#84). */
|
|
121
|
+
export function definedToolCount() {
|
|
122
|
+
return runtimeState.definedTools.size
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Test hook: the recorded set is process state and must be reset per fixture. */
|
|
126
|
+
export function __testResetToolSurface() {
|
|
127
|
+
runtimeState.definedTools.clear()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The injected port set for the app layer (built once per process). */
|
|
131
|
+
function towerDeps() {
|
|
132
|
+
if (!runtimeState.deps) runtimeState.deps = createDefaultDeps()
|
|
133
|
+
return runtimeState.deps
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Compose the app Tower with the injected ports (never a bare adapter import). */
|
|
137
|
+
function openTower(repoRoot) {
|
|
138
|
+
return Tower.open(repoRoot, towerDeps())
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Build a Tower with an explicit config for the read-only doctor path. */
|
|
142
|
+
function createTower(repo, config) {
|
|
143
|
+
return new Tower(repo, config, towerDeps())
|
|
57
144
|
}
|
|
58
145
|
|
|
59
146
|
function normalizeToolArgs(args, aliases = {}) {
|
|
@@ -291,15 +378,7 @@ async function ensureRepository(repoRootInput, autoGitInit) {
|
|
|
291
378
|
}
|
|
292
379
|
}
|
|
293
380
|
|
|
294
|
-
const OPERATIONAL_TOOLS = new Map(
|
|
295
|
-
['tower_batch', 'exposeBatch'],
|
|
296
|
-
['tower_resource', 'exposeLeases'],
|
|
297
|
-
['tower_import_legacy', 'exposeImport'],
|
|
298
|
-
['tower_legacy_map', 'exposeImport'],
|
|
299
|
-
['tower_recover', 'exposeRecovery'],
|
|
300
|
-
['tower_test', 'exposeTests'],
|
|
301
|
-
['tower_doctor', 'exposeDoctor'],
|
|
302
|
-
])
|
|
381
|
+
const OPERATIONAL_TOOLS = new Map()
|
|
303
382
|
|
|
304
383
|
function plainSchema(value) {
|
|
305
384
|
return JSON.parse(JSON.stringify(value))
|
|
@@ -324,9 +403,10 @@ function plainSchema(value) {
|
|
|
324
403
|
*/
|
|
325
404
|
function define(ctx, definition) {
|
|
326
405
|
const { aliases, requiresMissionId, board, execute, ...rest } = definition
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
406
|
+
// Record the definition for the concept budget. The capability that owns the
|
|
407
|
+
// tool decides whether it is registered at all (#121): the component row is
|
|
408
|
+
// the switch, so `define()` no longer consults a `tools.expose*` boolean.
|
|
409
|
+
recordToolRegistration(rest.name)
|
|
330
410
|
ctx.effect(() =>
|
|
331
411
|
ctx.tools.register({
|
|
332
412
|
...rest,
|
|
@@ -364,7 +444,7 @@ function define(ctx, definition) {
|
|
|
364
444
|
}
|
|
365
445
|
if (board === true && typeof normalized.repoRoot === 'string') {
|
|
366
446
|
try {
|
|
367
|
-
const opened = await
|
|
447
|
+
const opened = await openTower(normalized.repoRoot)
|
|
368
448
|
await opened.writeBoard()
|
|
369
449
|
} catch {
|
|
370
450
|
// board output is best effort
|
|
@@ -386,6 +466,12 @@ function textOf(blocks) {
|
|
|
386
466
|
/** Read `package.json tower.testDiscovery` and run it for one mission worktree.
|
|
387
467
|
* Returns `{configured:false}` when the repository has no discovery block. */
|
|
388
468
|
async function runMissionDiscovery(tower, mission) {
|
|
469
|
+
// #121: test discovery is the tower-tests component. With the row off there is
|
|
470
|
+
// no tower_test tool and no receipt; never execute repository code silently
|
|
471
|
+
// because a config key still says admission complete/spawn.
|
|
472
|
+
if (!componentEnabled('tower-tests')) {
|
|
473
|
+
return { configured: false, disabledBy: 'tower-tests', reason: disabledReason('tower-tests') }
|
|
474
|
+
}
|
|
389
475
|
const testsConfig = tower.config.tests ?? {}
|
|
390
476
|
if (testsConfig.enabled === false || testsConfig.allowPackageDiscovery === false) {
|
|
391
477
|
return { configured: false }
|
|
@@ -448,9 +534,47 @@ async function observeActivity(ctx, tower) {
|
|
|
448
534
|
return { activity, parentStatus }
|
|
449
535
|
}
|
|
450
536
|
|
|
451
|
-
|
|
537
|
+
/**
|
|
538
|
+
* Register the mandatory Tower host surface (#121).
|
|
539
|
+
*
|
|
540
|
+
* The host owns planning, mode integration, the health contract
|
|
541
|
+
* (`tower_doctor`) and the recovery escape (`tower_recover`); neither is a
|
|
542
|
+
* component and neither can be switched off. The optional capabilities
|
|
543
|
+
* (batch/import/leases/tests/reload) are registered by their own entry points
|
|
544
|
+
* (`registerBatchCapability`, ...), so switching a component row off removes
|
|
545
|
+
* exactly that capability while the host keeps working.
|
|
546
|
+
*/
|
|
547
|
+
export function registerCoreTowerTools(ctx, options = {}) {
|
|
452
548
|
runtimeState.workspaceHook = options.workspace ?? null
|
|
453
549
|
runtimeState.hmrBridge = options.hmrBridge ?? null
|
|
550
|
+
// #121/#122: capability tools are part of this build even while their
|
|
551
|
+
// component row is off. A row's entry point only runs when the row is on, so
|
|
552
|
+
// record the declared capability names here — before any row gates
|
|
553
|
+
// registration — and the concept budget reports the full 29-tool build
|
|
554
|
+
// surface in every profile. `componentReport` is what names the subset that
|
|
555
|
+
// is actually mounted; this count never shrinks because a user switched a
|
|
556
|
+
// component off.
|
|
557
|
+
for (const toolIds of Object.values(COMPONENT_TOOLS)) {
|
|
558
|
+
for (const toolId of toolIds) recordToolRegistration(toolId)
|
|
559
|
+
}
|
|
560
|
+
// #69: the host entry (index.js) is the composition root. When it supplies the
|
|
561
|
+
// port set, adopt it — validated once — instead of constructing a second set
|
|
562
|
+
// here. Tests/embedders that omit deps fall back to one lazily built set.
|
|
563
|
+
// #75: the event port is bound here to the live Cordis emitter; emitting is
|
|
564
|
+
// best effort so a listener can never fail the tool that produced the fact.
|
|
565
|
+
const baseDeps = options.deps ? assertPorts(options.deps) : (runtimeState.deps ?? createDefaultDeps())
|
|
566
|
+
runtimeState.deps = {
|
|
567
|
+
...baseDeps,
|
|
568
|
+
events: {
|
|
569
|
+
emit(name, payload) {
|
|
570
|
+
try {
|
|
571
|
+
ctx.emit?.(name, payload)
|
|
572
|
+
} catch {
|
|
573
|
+
// events are an optional side channel; never fail a tool for them
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
}
|
|
454
578
|
// A lightweight roster watchdog: the authoritative activity comes from the
|
|
455
579
|
// subagents catalog; a missing observation after spawn.staleMs is marked
|
|
456
580
|
// stale (never silently treated as stopped) and blocks merge via the gate.
|
|
@@ -460,7 +584,7 @@ export function registerTowerTools(ctx, options = {}) {
|
|
|
460
584
|
try {
|
|
461
585
|
if (!ctx.get('subagents')) return
|
|
462
586
|
for (const repoRoot of [...WATCHED_REPOS]) {
|
|
463
|
-
const tower = await
|
|
587
|
+
const tower = await openTower(repoRoot)
|
|
464
588
|
const view = await tower.rosterView()
|
|
465
589
|
const live = view.roster.filter((entry) => !ROSTER_INACTIVE_STATES.includes(entry.state))
|
|
466
590
|
if (live.length === 0) { WATCHED_REPOS.delete(repoRoot); continue }
|
|
@@ -481,59 +605,126 @@ export function registerTowerTools(ctx, options = {}) {
|
|
|
481
605
|
}, watchMs)
|
|
482
606
|
timer.unref?.()
|
|
483
607
|
ctx.effect(() => () => clearInterval(timer))
|
|
484
|
-
registerInitTool(ctx, { define, actorFor, requireTowerActor, workspaceHook: runtimeState.workspaceHook, ensureRepository })
|
|
608
|
+
registerInitTool(ctx, { openTower, define, actorFor, requireTowerActor, workspaceHook: runtimeState.workspaceHook, ensureRepository, resolveRepo, runtimeConfig })
|
|
609
|
+
|
|
610
|
+
// #121: planning validates the mounted component rows as well as the presets,
|
|
611
|
+
// so a review gate without its reviewer row is refused before work starts.
|
|
612
|
+
registerPlanTool(ctx, { openTower, define, actorFor, requireTowerActor, assertPresetsAvailable: assertRuntimePreconditions })
|
|
485
613
|
|
|
486
|
-
|
|
614
|
+
registerMissionTool(ctx, { openTower, define, actorFor, requireKnownActor })
|
|
487
615
|
|
|
488
|
-
|
|
616
|
+
registerStatusTool(ctx, { openTower, define, workspaceHook: runtimeState.workspaceHook, spawnBackoffView, observeActivity, componentReport })
|
|
489
617
|
|
|
490
|
-
|
|
618
|
+
registerSpawnTool(ctx, { openTower, define, actorFor, requireTowerActor, enqueueSpawn, observeActivity, spawnBackoffState, recordSpawnThrottle, recordSpawnSuccess, runMissionDiscovery, textOf, inspectLeases })
|
|
491
619
|
|
|
492
|
-
|
|
620
|
+
registerRosterTool(ctx, { openTower, define })
|
|
493
621
|
|
|
494
|
-
|
|
622
|
+
registerInterruptTool(ctx, { openTower, define, actorFor, requireTowerActor, interruptRosterAgent })
|
|
495
623
|
|
|
496
|
-
|
|
624
|
+
registerDiffTool(ctx, { openTower, define })
|
|
497
625
|
|
|
498
|
-
|
|
626
|
+
registerAdoptTool(ctx, { openTower, define, actorFor, requireTowerActor })
|
|
499
627
|
|
|
500
|
-
|
|
628
|
+
registerSendTool(ctx, { openTower, define, actorFor, checkSendRate })
|
|
501
629
|
|
|
502
|
-
|
|
630
|
+
registerInboxTool(ctx, { openTower, define })
|
|
503
631
|
|
|
504
|
-
|
|
632
|
+
registerCompleteTool(ctx, { openTower, define, runMissionDiscovery })
|
|
505
633
|
|
|
506
|
-
|
|
634
|
+
registerFindingTool(ctx, { openTower, define, actorFor, requireKnownActor })
|
|
507
635
|
|
|
508
|
-
|
|
636
|
+
registerFindingsTool(ctx, { openTower, define, actorFor, requireKnownActor })
|
|
509
637
|
|
|
510
|
-
|
|
638
|
+
registerReviewTool(ctx, { openTower, define, actorFor })
|
|
511
639
|
|
|
512
|
-
|
|
640
|
+
registerPreflightTool(ctx, { openTower, define, observeActivity })
|
|
513
641
|
|
|
514
|
-
|
|
642
|
+
registerMergeTool(ctx, { openTower, define, actorFor, requireTowerActor, observeActivity })
|
|
515
643
|
|
|
516
|
-
|
|
644
|
+
registerRebaseTool(ctx, { openTower, define, actorFor, requireTowerActor, observeActivity })
|
|
517
645
|
|
|
518
|
-
|
|
646
|
+
registerTeardownTool(ctx, { openTower, define, actorFor, requireTowerActor, observeActivity })
|
|
519
647
|
|
|
520
|
-
|
|
648
|
+
registerDoctorTool(ctx, {
|
|
649
|
+
openTower,
|
|
650
|
+
define,
|
|
651
|
+
createTower,
|
|
652
|
+
resolveRepo,
|
|
653
|
+
runtimeConfig,
|
|
654
|
+
runDoctor,
|
|
655
|
+
// #84/#122: the doctor reports the full registered surface. It is always
|
|
656
|
+
// registered itself, and it is the reporter that names a disabled component.
|
|
657
|
+
surface: () => ({ tools: definedToolCount() }),
|
|
658
|
+
})
|
|
521
659
|
|
|
522
|
-
|
|
660
|
+
// #122: the recovery escape is registered for every configuration. The lock it
|
|
661
|
+
// repairs can be left behind by any crashed mutation, so an off state would be
|
|
662
|
+
// worse than uninstalling the plugin.
|
|
663
|
+
registerRecoverTool(ctx, { openTower, define, actorFor, requireTowerActor, readStateLock, lockTiming })
|
|
664
|
+
}
|
|
523
665
|
|
|
524
|
-
|
|
666
|
+
/** Evidence batches — the `tower-batch` component. */
|
|
667
|
+
export function registerBatchCapability(ctx) {
|
|
668
|
+
registerBatchTool(ctx, { openTower, define, actorFor, requireTowerActor, summarizeBatch, verifyBatchReceiptFiles })
|
|
669
|
+
}
|
|
525
670
|
|
|
526
|
-
|
|
671
|
+
/** Advisory leases — the `tower-leases` component. */
|
|
672
|
+
export function registerLeasesCapability(ctx) {
|
|
673
|
+
registerResourceTool(ctx, { openTower, define, actorFor, requireTowerActor, acquireLease, releaseLease, inspectLeases })
|
|
674
|
+
}
|
|
527
675
|
|
|
528
|
-
|
|
676
|
+
/** Legacy import and its audit map — the `tower-import` component. */
|
|
677
|
+
export function registerImportCapability(ctx) {
|
|
678
|
+
registerImportLegacyTool(ctx, { openTower, define, actorFor, requireTowerActor, importLegacySnapshot })
|
|
679
|
+
registerLegacyMapTool(ctx, { openTower, define, actorFor, requireTowerActor, validateLegacyMap })
|
|
680
|
+
}
|
|
529
681
|
|
|
530
|
-
|
|
682
|
+
/** Declared test discovery — the `tower-tests` component. */
|
|
683
|
+
export function registerTestsCapability(ctx) {
|
|
684
|
+
registerTestTool(ctx, { openTower, define, actorFor, requireKnownActor, runMissionDiscovery })
|
|
685
|
+
}
|
|
531
686
|
|
|
532
|
-
|
|
687
|
+
/**
|
|
688
|
+
* Dev hot reload — the `tower-hmr` component (shipped off).
|
|
689
|
+
* The returned bridge is the one `/tower reload` and `tower_mode` report.
|
|
690
|
+
*/
|
|
691
|
+
export function registerHmrCapability(ctx, options = {}) {
|
|
692
|
+
const dev = runtimeConfig().dev ?? {}
|
|
693
|
+
const bridge =
|
|
694
|
+
options.bridge ??
|
|
695
|
+
runtimeState.hmrBridge ??
|
|
696
|
+
createHmrBridge(ctx, {
|
|
697
|
+
requiredIds: dev.reloadRequiredIds,
|
|
698
|
+
delayMs: dev.reloadDelayMs,
|
|
699
|
+
logger: ctx.logger ?? console,
|
|
700
|
+
})
|
|
701
|
+
registerReloadTool(ctx, { openTower, define, actorFor, requireTowerActor, sharedHmrBridge: bridge, runtimeConfig, createHmrBridge })
|
|
702
|
+
return bridge
|
|
703
|
+
}
|
|
533
704
|
|
|
534
|
-
|
|
705
|
+
/**
|
|
706
|
+
* Register every optional capability in-process. This is the aggregate used by
|
|
707
|
+
* tests and embedders; the shipped profile mounts the capability rows instead,
|
|
708
|
+
* so one row can be off without removing the rest.
|
|
709
|
+
*/
|
|
710
|
+
export function registerAllCapabilities(ctx, options = {}) {
|
|
711
|
+
for (const id of ['tower-batch', 'tower-import', 'tower-leases', 'tower-tests', 'tower-hmr']) holdComponent(ctx, id)
|
|
712
|
+
registerBatchCapability(ctx)
|
|
713
|
+
registerImportCapability(ctx)
|
|
714
|
+
registerLeasesCapability(ctx)
|
|
715
|
+
registerTestsCapability(ctx)
|
|
716
|
+
registerHmrCapability(ctx, options)
|
|
717
|
+
}
|
|
535
718
|
|
|
536
|
-
|
|
719
|
+
/**
|
|
720
|
+
* Backwards-compatible aggregate: the mandatory host surface plus every
|
|
721
|
+
* optional capability. Tests and embedders call this; the shipped composition
|
|
722
|
+
* calls `registerCoreTowerTools` and lets the component rows register their own
|
|
723
|
+
* capability.
|
|
724
|
+
*/
|
|
725
|
+
export function registerTowerTools(ctx, options = {}) {
|
|
726
|
+
registerCoreTowerTools(ctx, options)
|
|
727
|
+
registerAllCapabilities(ctx, options)
|
|
537
728
|
}
|
|
538
729
|
|
|
539
730
|
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 {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
15
|
+
import { registerCoreTowerTools, recordToolRegistration } 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 './
|
|
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 './
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
28
|
+
} from './adapters/workspaceLock.js'
|
|
29
|
+
import { assertRuntimePreconditions } from './host/presets.js'
|
|
30
|
+
import { currentHmrBridge } from './host/capability-registry.js'
|
|
30
31
|
|
|
31
32
|
export const name = 'dsh-tower'
|
|
32
33
|
export const inject = ['tools', 'systemPrompt']
|
|
@@ -255,14 +256,6 @@ export function apply(ctx, rawConfig) {
|
|
|
255
256
|
modeState.replace(loadModes(config.mode.storePath || undefined))
|
|
256
257
|
if (typeof ctx.provide === 'function') ctx.provide('tower', modeState)
|
|
257
258
|
denyTools = new Set(config.mode.denyTools)
|
|
258
|
-
const hotReloadEnabled = config.dev?.hotReload !== false
|
|
259
|
-
const hmrBridge = hotReloadEnabled
|
|
260
|
-
? createHmrBridge(ctx, {
|
|
261
|
-
requiredIds: config.dev?.reloadRequiredIds,
|
|
262
|
-
delayMs: config.dev?.reloadDelayMs,
|
|
263
|
-
logger: ctx.logger ?? console,
|
|
264
|
-
})
|
|
265
|
-
: null
|
|
266
259
|
|
|
267
260
|
const workspace = {
|
|
268
261
|
/** Lazily acquire the advisory workspace lock on the first repo-scoped tool. */
|
|
@@ -343,12 +336,26 @@ export function apply(ctx, rawConfig) {
|
|
|
343
336
|
},
|
|
344
337
|
}
|
|
345
338
|
|
|
346
|
-
|
|
339
|
+
// #69: the host entry is the composition root — the adapters are constructed
|
|
340
|
+
// once here and injected into the tool layer and every Tower instance.
|
|
341
|
+
// #121: only the mandatory host surface is registered here. The optional
|
|
342
|
+
// capability rows (batch/import/leases/tests/hmr) are separate loader entries
|
|
343
|
+
// and register their own tools, so a disabled row removes exactly one
|
|
344
|
+
// capability.
|
|
345
|
+
registerCoreTowerTools(ctx, { workspace, deps: createDefaultDeps() })
|
|
346
|
+
|
|
347
|
+
// #118/#121: tower_mode/tower_control are registered directly here instead of
|
|
348
|
+
// through define(), so record them through the same recorder the registry
|
|
349
|
+
// uses. tower_control is recorded before the allowAgentControl gate: the
|
|
350
|
+
// concept budget measures the surface this build can register, not only the
|
|
351
|
+
// tools one profile exposes (#84). Together with the capability-tool names the
|
|
352
|
+
// registry records before row gating, these are the shipped 29.
|
|
353
|
+
recordToolRegistration('tower_mode')
|
|
354
|
+
recordToolRegistration('tower_control')
|
|
347
355
|
|
|
348
356
|
// On dispose (including an HMR reload) release advisory workspace locks this
|
|
349
357
|
// process still owns; a reloaded plugin restores the entries from modes.json.
|
|
350
358
|
ctx.effect(() => () => {
|
|
351
|
-
if (hmrBridge) hmrBridge.dispose?.()
|
|
352
359
|
for (const entry of modeState.values()) {
|
|
353
360
|
if (entry?.lock) {
|
|
354
361
|
releaseWorkspaceLock(entry.lock.commonDir, { token: entry.lock.token }).catch(() => {})
|
|
@@ -448,7 +455,12 @@ export function apply(ctx, rawConfig) {
|
|
|
448
455
|
deniedWhileActive: [...denyTools],
|
|
449
456
|
persistedAt: storePath(config.mode.storePath || undefined),
|
|
450
457
|
pluginVersion: readPluginVersion(),
|
|
451
|
-
hotReload:
|
|
458
|
+
hotReload: (() => {
|
|
459
|
+
// #121/#122: the tower-hmr component row owns the bridge. When the
|
|
460
|
+
// row is off the capability is reported, never silently absent.
|
|
461
|
+
const bridge = currentHmrBridge()
|
|
462
|
+
return bridge ? bridge.status() : { available: false, disabled: true, component: 'tower-hmr' }
|
|
463
|
+
})(),
|
|
452
464
|
planMode: (() => {
|
|
453
465
|
const state = planModeState(ctx, exec.agent)
|
|
454
466
|
return { available: state.available, active: state.active, pending: state.pending }
|
|
@@ -504,7 +516,7 @@ export function apply(ctx, rawConfig) {
|
|
|
504
516
|
}
|
|
505
517
|
const reason = args.reason.trim()
|
|
506
518
|
if (args.action === 'enter' && !modeOn(key)) {
|
|
507
|
-
await
|
|
519
|
+
await assertRuntimePreconditions(ctx, config)
|
|
508
520
|
}
|
|
509
521
|
const outcome =
|
|
510
522
|
args.action === 'enter'
|
|
@@ -555,20 +567,23 @@ export function apply(ctx, rawConfig) {
|
|
|
555
567
|
}
|
|
556
568
|
const parsed = parseTowerInput(rawInput)
|
|
557
569
|
if (parsed.action === 'reload') {
|
|
558
|
-
|
|
559
|
-
|
|
570
|
+
// #121: /tower reload is the tower-hmr component. The row is shipped
|
|
571
|
+
// off, so the error names the component instead of an invisible key.
|
|
572
|
+
const bridge = currentHmrBridge()
|
|
573
|
+
if (!bridge) {
|
|
574
|
+
return { kind: 'error', text: 'Tower reload is disabled by component tower-hmr; enable the tower-hmr row in the plugin page and restart DSH.' }
|
|
560
575
|
}
|
|
561
|
-
if (!
|
|
576
|
+
if (!bridge.available) {
|
|
562
577
|
return { kind: 'error', text: 'Tower reload is unavailable: this DSH profile has no HMR service. Restart DSH instead.' }
|
|
563
578
|
}
|
|
564
|
-
const outcome =
|
|
579
|
+
const outcome = bridge.schedule()
|
|
565
580
|
return {
|
|
566
581
|
kind: 'success',
|
|
567
582
|
text: `Tower reload scheduled in ${outcome.delayMs}ms (required entries: ${outcome.requiredIds.join(', ')}). If the new code does not appear, restart DSH.`,
|
|
568
583
|
}
|
|
569
584
|
}
|
|
570
585
|
const { active, message } = parsed
|
|
571
|
-
if (active) await
|
|
586
|
+
if (active) await assertRuntimePreconditions(ctx, config)
|
|
572
587
|
const outcome = active
|
|
573
588
|
? enterMode(ctx, key, agent, 'user', message || null)
|
|
574
589
|
: await exitMode(key, 'user', message || null)
|
package/locale/en.json
CHANGED
|
@@ -4,25 +4,45 @@
|
|
|
4
4
|
"description": "Mission planning, isolated worktrees, worker/reviewer agents, and review-gated merges."
|
|
5
5
|
},
|
|
6
6
|
"components": {
|
|
7
|
-
"tower
|
|
8
|
-
"title": "Tower",
|
|
9
|
-
"summary": "Mission planning,
|
|
10
|
-
"disables": "Turns off the
|
|
7
|
+
"tower": {
|
|
8
|
+
"title": "Tower host",
|
|
9
|
+
"summary": "Mission planning, worktree isolation, the mode guard, the health contract and the recovery escape.",
|
|
10
|
+
"disables": "Turns off the mandatory host; this equals uninstalling Tower: no tools, presets or skills remain."
|
|
11
11
|
},
|
|
12
12
|
"tower-skills": {
|
|
13
13
|
"title": "Tower skills",
|
|
14
14
|
"summary": "Playbooks for worker/reviewer agents and recovery after a blocked merge.",
|
|
15
|
-
"disables": "
|
|
15
|
+
"disables": "The three Tower skills leave the catalog; the worker/reviewer protocol sections and every mission tool keep working."
|
|
16
16
|
},
|
|
17
|
-
"
|
|
18
|
-
"title": "
|
|
19
|
-
"summary": "
|
|
20
|
-
"disables": "
|
|
17
|
+
"tower-batch": {
|
|
18
|
+
"title": "Evidence batches",
|
|
19
|
+
"summary": "Group missions into finite evidence batches and close them against required checks and receipts.",
|
|
20
|
+
"disables": "tower_batch is not registered; missions, gates and merges are unaffected."
|
|
21
|
+
},
|
|
22
|
+
"tower-import": {
|
|
23
|
+
"title": "Legacy import",
|
|
24
|
+
"summary": "Import a Tower Codex legacy state file and audit its snapshot-to-mission mapping.",
|
|
25
|
+
"disables": "tower_import_legacy and tower_legacy_map are not registered; new state is unaffected."
|
|
26
|
+
},
|
|
27
|
+
"tower-leases": {
|
|
28
|
+
"title": "Advisory leases",
|
|
29
|
+
"summary": "Coordinate heavy per-repository work with advisory resource leases.",
|
|
30
|
+
"disables": "tower_resource is not registered; nothing else depends on a lease."
|
|
31
|
+
},
|
|
32
|
+
"tower-tests": {
|
|
33
|
+
"title": "Test discovery",
|
|
34
|
+
"summary": "Run the worktree's declared test discovery and record a receipt for the mission tip.",
|
|
35
|
+
"disables": "tower_test is not registered and no receipt can be recorded; with gates.requireTests:true planning/mode entry is rejected with an explanation."
|
|
36
|
+
},
|
|
37
|
+
"tower-hmr": {
|
|
38
|
+
"title": "Dev hot reload",
|
|
39
|
+
"summary": "Register tower_reload and /tower reload for linked-profile development.",
|
|
40
|
+
"disables": "tower_reload and /tower reload are unavailable; a non-HMR profile behaves exactly as before. Shipped off."
|
|
21
41
|
},
|
|
22
42
|
"preset-tower-reviewer": {
|
|
23
43
|
"title": "Tower reviewer",
|
|
24
|
-
"summary": "Read-only
|
|
25
|
-
"disables": "
|
|
44
|
+
"summary": "Read-only independent review: inspect the mission diff, verify scope and acceptance, return a verdict.",
|
|
45
|
+
"disables": "The reviewer cannot be dispatched. Supported while review.requireReview is false; with the gate on planning/mode entry is locked with an explanatory error."
|
|
26
46
|
}
|
|
27
47
|
}
|
|
28
48
|
}
|