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.
- package/CHANGELOG.md +141 -0
- package/COMPATIBILITY.md +38 -0
- package/CONTRIBUTING.md +150 -2
- package/README.en.md +59 -16
- package/README.md +57 -16
- package/SECURITY.md +31 -1
- package/adapters/default.js +10 -0
- package/adapters/store-fs.js +6 -0
- package/adapters/vcs-git.js +4 -0
- package/app/batch.js +79 -0
- package/app/board.js +25 -0
- package/app/checkCompletion.js +76 -0
- package/app/enrichedMissions.js +26 -0
- package/app/findings.js +78 -0
- package/app/gate.js +108 -0
- package/app/init.js +249 -0
- package/app/merge.js +567 -0
- package/app/messages.js +66 -0
- package/app/mission.js +35 -0
- package/app/missionContext.js +14 -0
- package/app/plan.js +42 -0
- package/app/provision.js +193 -0
- package/app/read.js +57 -0
- package/app/rebase.js +302 -0
- package/app/recording.js +72 -0
- package/app/review.js +145 -0
- package/app/roster.js +123 -0
- package/app/spawn.js +125 -0
- package/app/teardown.js +184 -0
- package/app/tower.js +524 -0
- package/cordis.patch.yml +61 -53
- package/docs/README.md +33 -1
- package/docs/architecture/overview.md +114 -14
- package/docs/board.md +72 -0
- package/docs/examples/first-mission.md +2 -1
- package/docs/faq.md +11 -7
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/install-upgrade-migration.md +22 -6
- package/docs/guide/quickstart.md +12 -7
- package/docs/guide/release.md +125 -0
- package/docs/guide/scenarios-and-philosophy.md +86 -69
- package/docs/operations/security.md +65 -14
- package/docs/reference/blockers.md +11 -10
- package/docs/reference/config.generated.md +66 -0
- package/docs/reference/config.md +11 -7
- package/docs/reference/errors.generated.md +39 -0
- package/docs/reference/errors.md +10 -2
- package/docs/reference/events.md +7 -4
- package/docs/reference/testing.md +2 -1
- package/docs/reference/tools.generated.md +33 -0
- package/docs/reference/tools.md +8 -7
- package/docs/zh/README.md +4 -3
- package/docs/zh/faq.md +9 -6
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/guide/install-upgrade-migration.md +17 -5
- package/docs/zh/guide/quickstart.md +13 -8
- package/docs/zh/guide/scenarios-and-philosophy.md +55 -50
- package/docs/zh/operations/security.md +43 -7
- package/docs/zh/reference/blockers.md +10 -10
- package/docs/zh/reference/config.md +11 -7
- package/docs/zh/reference/errors.md +4 -1
- package/docs/zh/reference/events.md +6 -3
- package/docs/zh/reference/testing.md +3 -2
- package/docs/zh/reference/tools.md +8 -7
- package/domain/collab/batch.js +112 -0
- package/domain/collab/messages.js +127 -0
- package/domain/collab/roster.js +158 -0
- package/domain/collab/spawn.js +188 -0
- package/domain/config/schema.js +404 -0
- package/domain/contracts/outputs.js +279 -0
- package/domain/contracts/tool-inputs/collaboration.js +58 -0
- package/domain/contracts/tool-inputs/findings.js +29 -0
- package/domain/contracts/tool-inputs/index.js +94 -0
- package/domain/contracts/tool-inputs/integration.js +37 -0
- package/domain/contracts/tool-inputs/missions.js +94 -0
- package/domain/contracts/tool-inputs/ops.js +75 -0
- package/domain/contracts/tool-inputs/shared.js +25 -0
- package/domain/errors.js +96 -0
- package/domain/gate.js +125 -0
- package/domain/integration/gate.js +176 -0
- package/domain/integration/init.js +225 -0
- package/domain/integration/liveness.js +31 -0
- package/domain/integration/merge.js +377 -0
- package/domain/integration/provision.js +124 -0
- package/domain/integration/rebase.js +236 -0
- package/domain/integration/review.js +52 -0
- package/domain/integration/teardown.js +118 -0
- package/domain/mission.js +68 -0
- package/domain/missions/completion.js +114 -0
- package/domain/missions/findings.js +201 -0
- package/domain/missions/lifecycle.js +185 -0
- package/domain/missions/plan.js +149 -0
- package/domain/missions/review.js +179 -0
- package/domain/planning.js +118 -0
- package/domain/read/enrichedMissions.js +58 -0
- package/domain/read/missionContext.js +53 -0
- package/domain/read/views.js +128 -0
- package/domain/report.js +59 -0
- package/domain/roster.js +40 -0
- package/domain/scope.js +105 -0
- package/domain/state/schema.js +233 -0
- package/domain/state/semantics.js +117 -0
- package/domain/state.js +33 -0
- package/domain/summary.js +114 -0
- package/domain/vocabulary.js +179 -0
- package/index.js +86 -34
- package/lib/briefing.js +176 -0
- package/lib/compatibility.js +108 -0
- package/lib/config.js +54 -302
- package/lib/doctor.js +108 -9
- package/lib/git.js +93 -15
- package/lib/hmr.js +4 -3
- package/lib/leases.js +3 -2
- package/lib/legacy.js +20 -9
- package/lib/modeState.js +39 -0
- package/lib/modeStore.js +35 -33
- package/lib/persistence.js +349 -0
- package/lib/presets.js +35 -0
- package/lib/spawn-prechecks.js +168 -0
- package/lib/spawn-support.js +79 -0
- package/lib/store.js +57 -85
- package/lib/tests.js +46 -11
- package/lib/tool-registry.js +544 -0
- package/lib/workspaceLock.js +46 -22
- package/lib/worktreeOwnership.js +5 -4
- package/locale/en.json +23 -1
- package/locale/zh.json +22 -0
- package/package.json +33 -4
- package/ports/clock.js +6 -0
- package/ports/ids.js +7 -0
- package/ports/index.js +2 -0
- package/ports/store.js +11 -0
- package/ports/vcs.js +11 -0
- package/scripts/architecture.mjs +94 -0
- package/scripts/check-docs.mjs +44 -3
- package/scripts/check-pack.mjs +49 -0
- package/scripts/domain-metrics.mjs +48 -0
- package/scripts/generate-config-reference.mjs +46 -0
- package/scripts/generate-error-reference.mjs +29 -0
- package/scripts/generate-tools-reference.mjs +56 -0
- package/scripts/issue-audit.mjs +259 -0
- package/scripts/lib/cli.mjs +76 -0
- package/scripts/lib/tmp.mjs +20 -0
- package/scripts/mount-smoke/fixtures.mjs +162 -0
- package/scripts/mount-smoke/host.json +4 -0
- package/scripts/mount-smoke/probe.mjs +125 -0
- package/scripts/mount-smoke/surface.mjs +141 -0
- package/scripts/mount-smoke.mjs +270 -0
- package/scripts/pack-manifest.txt +203 -0
- package/scripts/release-check.mjs +303 -0
- package/skills/tower-recovery.md +7 -5
- package/tools/adopt.js +72 -0
- package/tools/batch.js +78 -0
- package/tools/complete.js +53 -0
- package/tools/diff.js +39 -0
- package/tools/doctor.js +50 -0
- package/tools/finding.js +40 -0
- package/tools/findings.js +39 -0
- package/tools/import-legacy.js +42 -0
- package/tools/inbox.js +39 -0
- package/tools/index.js +39 -0
- package/tools/init.js +63 -0
- package/tools/interrupt.js +42 -0
- package/tools/legacy-map.js +44 -0
- package/tools/merge.js +39 -0
- package/tools/mission.js +41 -0
- package/tools/plan.js +39 -0
- package/tools/preflight.js +44 -0
- package/tools/rebase.js +39 -0
- package/tools/recover.js +45 -0
- package/tools/reload.js +52 -0
- package/tools/resource.js +54 -0
- package/tools/review.js +45 -0
- package/tools/roster.js +60 -0
- package/tools/send.js +147 -0
- package/tools/spawn.js +196 -0
- package/tools/status.js +94 -0
- package/tools/teardown.js +42 -0
- package/tools/test.js +61 -0
- package/types/cordis.d.ts +6 -0
- package/types/index.d.ts +339 -0
- package/ui-bundle/README.md +111 -0
- package/ui-bundle/client.js +1980 -0
- package/ui-bundle/cordis.patch.yml +3 -0
- package/ui-bundle/icon.svg +6 -0
- package/ui-bundle/index.js +2 -0
- package/ui-bundle/locale/en.json +6 -0
- package/ui-bundle/locale/zh.json +6 -0
- package/ui-bundle/package.json +35 -0
- package/lib/errors.js +0 -111
- package/lib/tools.js +0 -2250
- package/lib/tower.js +0 -2689
- /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
|
+
}
|
package/tools/mission.js
ADDED
|
@@ -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
|
+
}
|
package/tools/rebase.js
ADDED
|
@@ -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
|
+
}
|
package/tools/recover.js
ADDED
|
@@ -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
|
+
}
|
package/tools/reload.js
ADDED
|
@@ -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
|
+
}
|