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
package/skills.js
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
// the short system-prompt sections stay authoritative for safety rules.
|
|
4
4
|
import { readFile } from 'node:fs/promises'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { holdComponent } from './host/capability-registry.js'
|
|
6
7
|
|
|
7
8
|
export const name = 'dsh-tower/skills'
|
|
8
|
-
|
|
9
|
+
// #121: an optional capability row still depends on the host service, so a
|
|
10
|
+
// composition with the host group off cannot leave an orphan skill provider.
|
|
11
|
+
export const inject = ['skills', 'tower']
|
|
9
12
|
|
|
10
13
|
const ASSETS = fileURLToPath(new URL('./skills/', import.meta.url))
|
|
11
14
|
|
|
@@ -59,5 +62,9 @@ const provider = {
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
export function apply(ctx) {
|
|
65
|
+
// #121: tower-skills is an optional component row. Registering here is what
|
|
66
|
+
// lets tower_status/tower_doctor report "disabled by component tower-skills"
|
|
67
|
+
// when the row is off instead of leaving the missing catalog unexplained.
|
|
68
|
+
holdComponent(ctx, 'tower-skills')
|
|
62
69
|
ctx.skills.registerProvider(() => provider)
|
|
63
70
|
}
|
package/tools/adopt.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// tower_adopt: reconcile the roster with the subagent catalog.
|
|
2
|
-
import { Tower } from '../app/tower.js'
|
|
3
2
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
4
3
|
import { TowerToolError } from '../domain/errors.js'
|
|
5
4
|
import { AdoptInput } from '../domain/contracts/tool-inputs/index.js'
|
|
@@ -7,7 +6,7 @@ import { PARENT_STATUS_OK, ROSTER_ACTIVITY, ROSTER_STATE } from '../domain/vocab
|
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* @param {any} ctx
|
|
10
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
|
|
9
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any }} deps
|
|
11
10
|
*/
|
|
12
11
|
export function registerAdoptTool(ctx, deps) {
|
|
13
12
|
const { define, actorFor, requireTowerActor } = deps
|
|
@@ -19,7 +18,7 @@ export function registerAdoptTool(ctx, deps) {
|
|
|
19
18
|
aliases: { retire_missing: ['retireMissing'] },
|
|
20
19
|
output: output(SCHEMAS.adopt),
|
|
21
20
|
async execute(args, exec) {
|
|
22
|
-
const tower = await
|
|
21
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
23
22
|
const actor = await actorFor(tower, exec)
|
|
24
23
|
requireTowerActor(actor, 'adopt the roster')
|
|
25
24
|
const view = await tower.rosterView()
|
package/tools/batch.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// tower_batch: finite evidence batches.
|
|
2
|
-
import { Tower } from '../app/tower.js'
|
|
3
2
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
4
3
|
import { TowerToolError } from '../domain/errors.js'
|
|
5
4
|
import { BatchInput } from '../domain/contracts/tool-inputs/index.js'
|
|
@@ -7,7 +6,7 @@ import { BATCH_ACTION } from '../domain/vocabulary.js'
|
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* @param {any} ctx
|
|
10
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, summarizeBatch: any, verifyBatchReceiptFiles: any }} deps
|
|
9
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, summarizeBatch: any, verifyBatchReceiptFiles: any }} deps
|
|
11
10
|
*/
|
|
12
11
|
export function registerBatchTool(ctx, deps) {
|
|
13
12
|
const { define, actorFor, requireTowerActor, summarizeBatch, verifyBatchReceiptFiles } = deps
|
|
@@ -19,7 +18,7 @@ export function registerBatchTool(ctx, deps) {
|
|
|
19
18
|
board: true,
|
|
20
19
|
output: output(SCHEMAS.batch),
|
|
21
20
|
async execute(args, exec) {
|
|
22
|
-
const tower = await
|
|
21
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
23
22
|
const actor = await actorFor(tower, exec)
|
|
24
23
|
if (args.action === BATCH_ACTION.status) {
|
|
25
24
|
const state = await tower.load()
|
package/tools/complete.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// tower_complete: worker completion admission.
|
|
2
|
-
import { Tower } from '../app/tower.js'
|
|
3
2
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
4
3
|
import { TowerToolError } from '../domain/errors.js'
|
|
5
4
|
import { CompleteInput } from '../domain/contracts/tool-inputs/index.js'
|
|
@@ -7,7 +6,7 @@ import { isCompleteAdmission } from '../domain/vocabulary.js'
|
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* @param {any} ctx
|
|
10
|
-
* @param {{ define: import('../types/index.js').DefineTool, runMissionDiscovery: any }} deps
|
|
9
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, runMissionDiscovery: any }} deps
|
|
11
10
|
*/
|
|
12
11
|
export function registerCompleteTool(ctx, deps) {
|
|
13
12
|
const { define, runMissionDiscovery } = deps
|
|
@@ -21,7 +20,7 @@ export function registerCompleteTool(ctx, deps) {
|
|
|
21
20
|
board: true,
|
|
22
21
|
output: output(SCHEMAS.complete),
|
|
23
22
|
async execute(args, exec) {
|
|
24
|
-
const tower = await
|
|
23
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
25
24
|
// Validate authorization/state before executing repository-controlled
|
|
26
25
|
// test code or writing a test receipt.
|
|
27
26
|
await tower.checkCompletion({
|
package/tools/diff.js
CHANGED
|
@@ -13,15 +13,15 @@
|
|
|
13
13
|
// The patch cap is enforced by the pure read projection.
|
|
14
14
|
// See docs/reference/tools.generated.md for the public shape.
|
|
15
15
|
// tower_diff: read-only mission branch view.
|
|
16
|
-
import { Tower } from '../app/tower.js'
|
|
17
16
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
18
17
|
import { DiffInput } from '../domain/contracts/tool-inputs/index.js'
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
20
|
* @param {any} ctx
|
|
22
|
-
* @param {{ define: import('../types/index.js').DefineTool }} deps
|
|
21
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any }} deps
|
|
23
22
|
*/
|
|
24
23
|
export function registerDiffTool(ctx, deps) {
|
|
24
|
+
// openTower is injected by the composition root (#69).
|
|
25
25
|
const { define } = deps
|
|
26
26
|
define(ctx, {
|
|
27
27
|
name: 'tower_diff',
|
|
@@ -32,7 +32,7 @@ export function registerDiffTool(ctx, deps) {
|
|
|
32
32
|
requiresMissionId: true,
|
|
33
33
|
output: output(SCHEMAS.diff),
|
|
34
34
|
async execute(args) {
|
|
35
|
-
const tower = await
|
|
35
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
36
36
|
return tower.diff({ missionId: args.missionId, maxBytes: args.maxBytes })
|
|
37
37
|
},
|
|
38
38
|
})
|
package/tools/doctor.js
CHANGED
|
@@ -1,36 +1,33 @@
|
|
|
1
1
|
// tower_doctor: read-only health check.
|
|
2
|
-
import { Tower } from '../app/tower.js'
|
|
3
|
-
import { resolveRepo } from '../lib/git.js'
|
|
4
|
-
import { runtimeConfig } from '../lib/config.js'
|
|
5
|
-
import { runDoctor } from '../lib/doctor.js'
|
|
6
2
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
7
3
|
import { DoctorInput } from '../domain/contracts/tool-inputs/index.js'
|
|
8
4
|
import { DOCTOR_STATUS } from '../domain/vocabulary.js'
|
|
9
5
|
|
|
10
6
|
/**
|
|
11
7
|
* @param {any} ctx
|
|
12
|
-
* @param {import('../types/index.js').DefineTool}
|
|
8
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, createTower: any, resolveRepo: any, runtimeConfig: any, runDoctor: any, surface?: () => { tools?: number } }} deps
|
|
13
9
|
*/
|
|
14
|
-
export function registerDoctorTool(ctx,
|
|
10
|
+
export function registerDoctorTool(ctx, deps) {
|
|
11
|
+
const { define, createTower, resolveRepo, runtimeConfig, runDoctor } = deps
|
|
15
12
|
define(ctx, {
|
|
16
13
|
name: 'tower_doctor',
|
|
17
14
|
description:
|
|
18
|
-
'Read-only\ environment\ and\ repository\ health\ check:\ git,\ state\ schema,\ case-sensitivity\ detection\ \(caseInsensitive\ and\ the\ active\ scope\ comparison\ mode\),\ config,\ presets/subagents/HMR,\ workspace\ lock,\ roster\ staleness,\ worktree\ ownership,\ test\ discovery,\ leases,\ and\ the\ terminal-mission\ archive\ \(count,\ bytes,\ years,\ unreadable\ diagnostics\ and\ the\ configured\ afterDays/maxMissions\ policy\)\.\ Returns\ checks\ with\ pass/warn/fail\ and\ an\ environment\ contract\.\ Never\ writes\ state\ and\ never\ probes\ the\ filesystem\.',
|
|
15
|
+
'Read-only\ environment\ and\ repository\ health\ check:\ the\ concept-budget\ surface\ \(config\ keys/sections/tools\),\ git,\ state\ schema,\ case-sensitivity\ detection\ \(caseInsensitive\ and\ the\ active\ scope\ comparison\ mode\),\ config,\ presets/subagents/HMR,\ the\ state\ lock\ age/heartbeat\ freshness,\ the\ repo-scoped\ git\ lock\ holder\ \(gitLock:\ \{held,pid,phase,heldMs\}\),\ workspace\ lock,\ roster\ staleness,\ worktree\ ownership,\ test\ discovery,\ leases,\ and\ the\ terminal-mission\ archive\ \(count,\ bytes,\ years,\ unreadable\ diagnostics\ and\ the\ configured\ afterDays/maxMissions\ policy\)\.\ Returns\ checks\ with\ pass/warn/fail,\ state-lock\ and\ git-lock\ reports\ and\ an\ environment\ contract\.\ Never\ writes\ state\ and\ never\ probes\ the\ filesystem\.',
|
|
19
16
|
parameters: DoctorInput,
|
|
20
17
|
output: output(SCHEMAS.doctor),
|
|
21
18
|
async execute(args, exec) {
|
|
22
19
|
let tower
|
|
23
20
|
let configError = null
|
|
24
21
|
try {
|
|
25
|
-
tower = await
|
|
22
|
+
tower = await deps.openTower(args.repoRoot)
|
|
26
23
|
} catch (error) {
|
|
27
24
|
const message = String(error?.message ?? error)
|
|
28
25
|
if (error?.code !== 'config-invalid' && error?.code !== 'config-unknown-key') throw error
|
|
29
26
|
configError = message
|
|
30
27
|
const repo = await resolveRepo(args.repoRoot)
|
|
31
|
-
tower =
|
|
28
|
+
tower = deps.createTower(repo, { ...runtimeConfig(), configPath: null, repoConfigPresent: true })
|
|
32
29
|
}
|
|
33
|
-
const report = await runDoctor(/** @type {any} */ ({ tower, ctx, agent: exec.agent }))
|
|
30
|
+
const report = await runDoctor(/** @type {any} */ ({ tower, ctx, agent: exec.agent, surface: deps.surface?.() }))
|
|
34
31
|
if (!configError) return report
|
|
35
32
|
return {
|
|
36
33
|
...report,
|
package/tools/finding.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// tower_finding: file one structured finding.
|
|
2
|
-
import { Tower } from '../app/tower.js'
|
|
3
2
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
4
3
|
import { FindingInput } from '../domain/contracts/tool-inputs/index.js'
|
|
5
4
|
import { ROLE } from '../domain/vocabulary.js'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* @param {any} ctx
|
|
9
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireKnownActor: any }} deps
|
|
8
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireKnownActor: any }} deps
|
|
10
9
|
*/
|
|
11
10
|
export function registerFindingTool(ctx, deps) {
|
|
12
11
|
const { define, actorFor, requireKnownActor } = deps
|
|
@@ -19,7 +18,7 @@ export function registerFindingTool(ctx, deps) {
|
|
|
19
18
|
board: true,
|
|
20
19
|
output: output(SCHEMAS.finding),
|
|
21
20
|
async execute(args, exec) {
|
|
22
|
-
const tower = await
|
|
21
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
23
22
|
const actor = await actorFor(tower, exec)
|
|
24
23
|
requireKnownActor(actor, 'tower_finding')
|
|
25
24
|
const view = await tower.rosterView()
|
package/tools/findings.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// tower_findings: list, read, or update findings.
|
|
2
|
-
import { Tower } from '../app/tower.js'
|
|
3
2
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
4
3
|
import { FindingsInput } from '../domain/contracts/tool-inputs/index.js'
|
|
5
4
|
import { ROLE } from '../domain/vocabulary.js'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* @param {any} ctx
|
|
9
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireKnownActor: any }} deps
|
|
8
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireKnownActor: any }} deps
|
|
10
9
|
*/
|
|
11
10
|
export function registerFindingsTool(ctx, deps) {
|
|
11
|
+
// openTower is injected by the composition root (#69).
|
|
12
12
|
const { define, actorFor, requireKnownActor } = deps
|
|
13
13
|
define(ctx, {
|
|
14
14
|
name: 'tower_findings',
|
|
@@ -18,7 +18,7 @@ export function registerFindingsTool(ctx, deps) {
|
|
|
18
18
|
aliases: {"missionId":["mission_id"]},
|
|
19
19
|
output: output(SCHEMAS.findings),
|
|
20
20
|
async execute(args, exec) {
|
|
21
|
-
const tower = await
|
|
21
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
22
22
|
const actor = await actorFor(tower, exec)
|
|
23
23
|
requireKnownActor(actor, 'tower_findings')
|
|
24
24
|
const view = await tower.rosterView()
|
package/tools/import-legacy.js
CHANGED
|
@@ -8,17 +8,15 @@
|
|
|
8
8
|
// Pure legacy validation lives in domain and never runs fs.
|
|
9
9
|
// See docs/reference/tools.generated.md for the public shape.
|
|
10
10
|
// tower_import_legacy: read-only legacy snapshot import.
|
|
11
|
-
import { Tower } from '../app/tower.js'
|
|
12
11
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
13
|
-
import { importLegacySnapshot } from '../lib/legacy.js'
|
|
14
12
|
import { ImportLegacyInput } from '../domain/contracts/tool-inputs/index.js'
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
15
|
* @param {any} ctx
|
|
18
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
|
|
16
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, importLegacySnapshot: any }} deps
|
|
19
17
|
*/
|
|
20
18
|
export function registerImportLegacyTool(ctx, deps) {
|
|
21
|
-
const { define, actorFor, requireTowerActor } = deps
|
|
19
|
+
const { define, actorFor, requireTowerActor, importLegacySnapshot } = deps
|
|
22
20
|
define(ctx, {
|
|
23
21
|
name: 'tower_import_legacy',
|
|
24
22
|
description:
|
|
@@ -27,7 +25,7 @@ export function registerImportLegacyTool(ctx, deps) {
|
|
|
27
25
|
board: true,
|
|
28
26
|
output: output(SCHEMAS.importLegacy),
|
|
29
27
|
async execute(args, exec) {
|
|
30
|
-
const tower = await
|
|
28
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
31
29
|
const actor = await actorFor(tower, exec)
|
|
32
30
|
requireTowerActor(actor, 'import a legacy snapshot')
|
|
33
31
|
const imported = await importLegacySnapshot({
|
package/tools/inbox.js
CHANGED
|
@@ -10,16 +10,16 @@
|
|
|
10
10
|
// The unread calculation is a pure domain/collab projection.
|
|
11
11
|
// See docs/reference/tools.generated.md for the public shape.
|
|
12
12
|
// tower_inbox: read and acknowledge inbound messages.
|
|
13
|
-
import { Tower } from '../app/tower.js'
|
|
14
13
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
15
14
|
import { InboxInput } from '../domain/contracts/tool-inputs/index.js'
|
|
16
15
|
import { ROLE } from '../domain/vocabulary.js'
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* @param {any} ctx
|
|
20
|
-
* @param {{ define: import('../types/index.js').DefineTool }} deps
|
|
19
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any }} deps
|
|
21
20
|
*/
|
|
22
21
|
export function registerInboxTool(ctx, deps) {
|
|
22
|
+
// openTower is injected by the composition root (#69).
|
|
23
23
|
const { define } = deps
|
|
24
24
|
define(ctx, {
|
|
25
25
|
name: 'tower_inbox',
|
|
@@ -29,7 +29,7 @@ export function registerInboxTool(ctx, deps) {
|
|
|
29
29
|
aliases: {"include_read":["includeRead"]},
|
|
30
30
|
output: output(SCHEMAS.inbox),
|
|
31
31
|
async execute(args, exec) {
|
|
32
|
-
const tower = await
|
|
32
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
33
33
|
const view = await tower.rosterView()
|
|
34
34
|
const self = view.roster.find((entry) => entry.agentId === String(exec.agent.id))
|
|
35
35
|
const reader = self ? self.name : ROLE.tower
|
package/tools/index.js
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
// - define(ctx, definition) accepts a TypeBox parameters schema;
|
|
5
5
|
// - execute(args) is typed from that schema by Static<>;
|
|
6
6
|
// - aliases and requiresMissionId are honored by the registry;
|
|
7
|
-
// -
|
|
7
|
+
// - capability ownership decides whether a tool is registered; the component
|
|
8
|
+
// rows in cordis.patch.yml are the switches (#121);
|
|
8
9
|
// - errors are classified to stable [tower:code] values;
|
|
9
10
|
// - tools must not import node:fs or node:child_process directly.
|
|
10
11
|
//
|
|
11
12
|
// Tests import this module for the public surface; they may also
|
|
12
|
-
// import
|
|
13
|
+
// import host/tool-registry.js for low-level test hooks.
|
|
13
14
|
//
|
|
14
|
-
// Packaging: package.json ships tools/*.js and
|
|
15
|
+
// Packaging: package.json ships tools/*.js and host/tool-registry.js.
|
|
15
16
|
// The file intentionally stays tiny so handler modules remain the
|
|
16
17
|
// only place where a tool is defined.
|
|
17
18
|
// Keep this comment and the re-export list in sync when adding tools.
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
//
|
|
21
22
|
// The registration aggregator, guards, rate limits, spawn backoff,
|
|
22
23
|
// repository preparation, discovery helpers, and verb helpers live in
|
|
23
|
-
//
|
|
24
|
+
// host/tool-registry.js. This module only re-exports the public entry
|
|
24
25
|
// points consumed by the Host plugin, tests, and docs generators.
|
|
25
26
|
//
|
|
26
27
|
// Design rules:
|
|
@@ -30,10 +31,10 @@
|
|
|
30
31
|
// - no business logic in this file;
|
|
31
32
|
// - keep the surface stable for index.js and tests.
|
|
32
33
|
//
|
|
33
|
-
// See docs/architecture/overview.md and
|
|
34
|
+
// See docs/architecture/overview.md and host/tool-registry.js.
|
|
34
35
|
// Tower tool surface: thin public wiring (#56 P5).
|
|
35
|
-
// Registration and shared machinery live in
|
|
36
|
+
// Registration and shared machinery live in host/tool-registry.js; one handler
|
|
36
37
|
// module per tool lives under tools/. This file only exposes the public entry
|
|
37
38
|
// points consumed by the Host plugin, tests, and documentation generators.
|
|
38
|
-
export { registerTowerTools, __testSpawnBackoff } from '../
|
|
39
|
-
export { workerBriefing, reviewerBriefing } from '../
|
|
39
|
+
export { registerTowerTools, __testSpawnBackoff } from '../host/tool-registry.js'
|
|
40
|
+
export { workerBriefing, reviewerBriefing } from '../app/briefing.js'
|
package/tools/init.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
// tower_init: repository initialization.
|
|
2
|
-
import { Tower } from '../app/tower.js'
|
|
3
2
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
4
3
|
import { TowerToolError } from '../domain/errors.js'
|
|
5
|
-
import { resolveRepo } from '../lib/git.js'
|
|
6
|
-
import { runtimeConfig } from '../lib/config.js'
|
|
7
4
|
import { InitInput } from '../domain/contracts/tool-inputs/index.js'
|
|
8
5
|
import { isTowerRole, isUnknownRole } from '../domain/vocabulary.js'
|
|
9
6
|
|
|
10
7
|
/**
|
|
11
8
|
* @param {any} ctx
|
|
12
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, workspaceHook: any, ensureRepository: any }} deps
|
|
9
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, workspaceHook: any, ensureRepository: any, resolveRepo: any, runtimeConfig: any }} deps
|
|
13
10
|
*/
|
|
14
11
|
export function registerInitTool(ctx, deps) {
|
|
15
|
-
const { define, actorFor, requireTowerActor, workspaceHook, ensureRepository } = deps
|
|
12
|
+
const { define, actorFor, requireTowerActor, workspaceHook, ensureRepository, resolveRepo, runtimeConfig } = deps
|
|
16
13
|
define(ctx, {
|
|
17
14
|
name: 'tower_init',
|
|
18
15
|
description:
|
|
@@ -31,7 +28,7 @@ export function registerInitTool(ctx, deps) {
|
|
|
31
28
|
args.autoGitInit === true || (args.autoGitInit === undefined && profileInit.autoGitInit === true)
|
|
32
29
|
repo = await ensureRepository(args.repoRoot, autoGitInit)
|
|
33
30
|
}
|
|
34
|
-
const tower = await
|
|
31
|
+
const tower = await deps.openTower(repo.root)
|
|
35
32
|
const initConfig = tower.config.init ?? {}
|
|
36
33
|
const snapshotDirty =
|
|
37
34
|
args.snapshotDirty === true || (args.snapshotDirty === undefined && initConfig.snapshotDirty === true)
|
package/tools/interrupt.js
CHANGED
|
@@ -8,14 +8,13 @@
|
|
|
8
8
|
// Roster state policy is pure and lives in domain/collab.
|
|
9
9
|
// See docs/reference/tools.generated.md for the public shape.
|
|
10
10
|
// tower_interrupt: interrupt one running roster agent.
|
|
11
|
-
import { Tower } from '../app/tower.js'
|
|
12
11
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
13
12
|
import { TowerToolError } from '../domain/errors.js'
|
|
14
13
|
import { InterruptInput } from '../domain/contracts/tool-inputs/index.js'
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* @param {any} ctx
|
|
18
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, interruptRosterAgent: any }} deps
|
|
17
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, interruptRosterAgent: any }} deps
|
|
19
18
|
*/
|
|
20
19
|
export function registerInterruptTool(ctx, deps) {
|
|
21
20
|
const { define, actorFor, requireTowerActor, interruptRosterAgent } = deps
|
|
@@ -27,7 +26,7 @@ export function registerInterruptTool(ctx, deps) {
|
|
|
27
26
|
aliases: { agentId: ['agent_id'] },
|
|
28
27
|
output: output(SCHEMAS.interrupt),
|
|
29
28
|
async execute(args, exec) {
|
|
30
|
-
const tower = await
|
|
29
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
31
30
|
const actor = await actorFor(tower, exec)
|
|
32
31
|
requireTowerActor(actor, 'interrupt tower agents')
|
|
33
32
|
if (!args.to && !args.agentId) throw new TowerToolError('invalid-argument', 'tower_interrupt requires to (roster name) or agentId')
|
package/tools/legacy-map.js
CHANGED
|
@@ -8,19 +8,17 @@
|
|
|
8
8
|
// Pure mapping validation lives in the domain legacy layer.
|
|
9
9
|
// See docs/reference/tools.generated.md for the public shape.
|
|
10
10
|
// tower_legacy_map: audit-only legacy snapshot mapping.
|
|
11
|
-
import { Tower } from '../app/tower.js'
|
|
12
11
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
13
12
|
import { TowerToolError } from '../domain/errors.js'
|
|
14
|
-
import { validateLegacyMap } from '../lib/legacy.js'
|
|
15
13
|
import { LegacyMapInput } from '../domain/contracts/tool-inputs/index.js'
|
|
16
14
|
import { LEGACY_MAP_ACTION } from '../domain/vocabulary.js'
|
|
17
15
|
|
|
18
16
|
/**
|
|
19
17
|
* @param {any} ctx
|
|
20
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
|
|
18
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, validateLegacyMap: any }} deps
|
|
21
19
|
*/
|
|
22
20
|
export function registerLegacyMapTool(ctx, deps) {
|
|
23
|
-
const { define, actorFor, requireTowerActor } = deps
|
|
21
|
+
const { define, actorFor, requireTowerActor, validateLegacyMap } = deps
|
|
24
22
|
define(ctx, {
|
|
25
23
|
name: 'tower_legacy_map',
|
|
26
24
|
description:
|
|
@@ -28,7 +26,7 @@ export function registerLegacyMapTool(ctx, deps) {
|
|
|
28
26
|
parameters: LegacyMapInput,
|
|
29
27
|
output: output(SCHEMAS.legacyMap),
|
|
30
28
|
async execute(args, exec) {
|
|
31
|
-
const tower = await
|
|
29
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
32
30
|
const actor = await actorFor(tower, exec)
|
|
33
31
|
const state = await tower.load()
|
|
34
32
|
if (args.action === LEGACY_MAP_ACTION.status) {
|
package/tools/merge.js
CHANGED
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
// The pure intent/shape decisions live in domain/integration.
|
|
10
10
|
// See docs/reference/tools.generated.md for the public shape.
|
|
11
11
|
// tower_merge: merge a mission branch into the recorded base.
|
|
12
|
-
import { Tower } from '../app/tower.js'
|
|
13
12
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
14
13
|
import { MergeInput } from '../domain/contracts/tool-inputs/index.js'
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* @param {any} ctx
|
|
18
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, observeActivity: any }} deps
|
|
17
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, observeActivity: any }} deps
|
|
19
18
|
*/
|
|
20
19
|
export function registerMergeTool(ctx, deps) {
|
|
20
|
+
// openTower is injected by the composition root (#69).
|
|
21
21
|
const { define, actorFor, requireTowerActor, observeActivity } = deps
|
|
22
22
|
define(ctx, {
|
|
23
23
|
name: 'tower_merge',
|
|
@@ -29,7 +29,7 @@ export function registerMergeTool(ctx, deps) {
|
|
|
29
29
|
board: true,
|
|
30
30
|
output: output(SCHEMAS.merge),
|
|
31
31
|
async execute(args, exec) {
|
|
32
|
-
const tower = await
|
|
32
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
33
33
|
const actor = await actorFor(tower, exec)
|
|
34
34
|
requireTowerActor(actor, 'merge missions')
|
|
35
35
|
const { activity, parentStatus } = await observeActivity(ctx, tower)
|
package/tools/mission.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// tower_mission: mission lifecycle updates.
|
|
2
|
-
import { Tower } from '../app/tower.js'
|
|
3
2
|
import { TowerToolError } from '../domain/errors.js'
|
|
4
3
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
5
4
|
import { MissionInput } from '../domain/contracts/tool-inputs/index.js'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* @param {any} ctx
|
|
9
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireKnownActor: any }} deps
|
|
8
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireKnownActor: any }} deps
|
|
10
9
|
*/
|
|
11
10
|
export function registerMissionTool(ctx, deps) {
|
|
12
11
|
const { define, actorFor, requireKnownActor } = deps
|
|
@@ -19,7 +18,7 @@ export function registerMissionTool(ctx, deps) {
|
|
|
19
18
|
board: true,
|
|
20
19
|
output: output(SCHEMAS.mission),
|
|
21
20
|
async execute(args, exec) {
|
|
22
|
-
const tower = await
|
|
21
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
23
22
|
const actor = await actorFor(tower, exec)
|
|
24
23
|
requireKnownActor(actor, 'tower_mission')
|
|
25
24
|
return tower.mission({
|
package/tools/plan.js
CHANGED
|
@@ -10,17 +10,18 @@
|
|
|
10
10
|
// Pure normalization and DAG policy live in domain/missions.
|
|
11
11
|
// See docs/reference/tools.generated.md for the public shape.
|
|
12
12
|
// tower_plan: create missions.
|
|
13
|
-
import { Tower } from '../app/tower.js'
|
|
14
13
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
15
|
-
import { assertPresetsAvailable } from '../lib/presets.js'
|
|
16
14
|
import { PlanInput } from '../domain/contracts/tool-inputs/index.js'
|
|
17
15
|
|
|
18
16
|
/**
|
|
19
17
|
* @param {any} ctx
|
|
20
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
|
|
18
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, assertPresetsAvailable: any }} deps
|
|
21
19
|
*/
|
|
22
20
|
export function registerPlanTool(ctx, deps) {
|
|
23
|
-
|
|
21
|
+
// #70: preset validation is injected by the composition root, so this tool
|
|
22
|
+
// never imports host/ or adapters/ directly.
|
|
23
|
+
// openTower is injected by the composition root (#69).
|
|
24
|
+
const { define, actorFor, requireTowerActor, assertPresetsAvailable } = deps
|
|
24
25
|
define(ctx, {
|
|
25
26
|
name: 'tower_plan',
|
|
26
27
|
description:
|
|
@@ -29,7 +30,7 @@ export function registerPlanTool(ctx, deps) {
|
|
|
29
30
|
board: true,
|
|
30
31
|
output: output(SCHEMAS.plan),
|
|
31
32
|
async execute(args, exec) {
|
|
32
|
-
const tower = await
|
|
33
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
33
34
|
const actor = await actorFor(tower, exec)
|
|
34
35
|
requireTowerActor(actor, 'plan missions')
|
|
35
36
|
await assertPresetsAvailable(ctx, tower.config)
|
package/tools/preflight.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// tower_preflight: read-only merge gate digest.
|
|
2
|
-
import { Tower } from '../app/tower.js'
|
|
3
2
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
4
3
|
import { classifyError } from '../domain/errors.js'
|
|
5
4
|
import { PreflightInput } from '../domain/contracts/tool-inputs/index.js'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* @param {any} ctx
|
|
9
|
-
* @param {{ define: import('../types/index.js').DefineTool, observeActivity: any }} deps
|
|
8
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, observeActivity: any }} deps
|
|
10
9
|
*/
|
|
11
10
|
export function registerPreflightTool(ctx, deps) {
|
|
12
11
|
const { define, observeActivity } = deps
|
|
@@ -19,7 +18,7 @@ export function registerPreflightTool(ctx, deps) {
|
|
|
19
18
|
requiresMissionId: true,
|
|
20
19
|
output: output(SCHEMAS.preflight),
|
|
21
20
|
async execute(args) {
|
|
22
|
-
const tower = await
|
|
21
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
23
22
|
const { activity, parentStatus } = await observeActivity(ctx, tower)
|
|
24
23
|
const gate = await tower.evaluateGate({ missionId: args.missionId, activity, parentStatus })
|
|
25
24
|
return {
|
package/tools/rebase.js
CHANGED
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
// Pure intent/waiver decisions live in domain/integration.
|
|
10
10
|
// See docs/reference/tools.generated.md for the public shape.
|
|
11
11
|
// tower_rebase: rebase a mission branch inside its worktree.
|
|
12
|
-
import { Tower } from '../app/tower.js'
|
|
13
12
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
14
13
|
import { RebaseInput } from '../domain/contracts/tool-inputs/index.js'
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* @param {any} ctx
|
|
18
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, observeActivity: any }} deps
|
|
17
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, observeActivity: any }} deps
|
|
19
18
|
*/
|
|
20
19
|
export function registerRebaseTool(ctx, deps) {
|
|
20
|
+
// openTower is injected by the composition root (#69).
|
|
21
21
|
const { define, actorFor, requireTowerActor, observeActivity } = deps
|
|
22
22
|
define(ctx, {
|
|
23
23
|
name: 'tower_rebase',
|
|
@@ -29,7 +29,7 @@ export function registerRebaseTool(ctx, deps) {
|
|
|
29
29
|
board: true,
|
|
30
30
|
output: output(SCHEMAS.rebase),
|
|
31
31
|
async execute(args, exec) {
|
|
32
|
-
const tower = await
|
|
32
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
33
33
|
const actor = await actorFor(tower, exec)
|
|
34
34
|
requireTowerActor(actor, 'rebase missions')
|
|
35
35
|
const { activity } = await observeActivity(ctx, tower)
|
package/tools/recover.js
CHANGED
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
// tower_recover: explicit stale state-lock diagnostics/repair.
|
|
2
|
-
import { Tower } from '../app/tower.js'
|
|
3
|
-
import { readStateLock } from '../lib/store.js'
|
|
4
2
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
5
3
|
import { TowerToolError } from '../domain/errors.js'
|
|
6
|
-
import { fsp } from '../adapters/store-fs.js'
|
|
7
4
|
import { RecoverInput } from '../domain/contracts/tool-inputs/index.js'
|
|
8
5
|
import { RECOVERY_ACTION, RECOVERY_CONFIRM, ROLE } from '../domain/vocabulary.js'
|
|
9
6
|
|
|
10
7
|
/**
|
|
11
8
|
* @param {any} ctx
|
|
12
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
|
|
9
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, readStateLock: any, lockTiming: any }} deps
|
|
13
10
|
*/
|
|
14
11
|
export function registerRecoverTool(ctx, deps) {
|
|
15
|
-
const { define, actorFor, requireTowerActor } = deps
|
|
12
|
+
const { define, actorFor, requireTowerActor, readStateLock, lockTiming } = deps
|
|
16
13
|
define(ctx, {
|
|
17
14
|
name: 'tower_recover',
|
|
18
15
|
description:
|
|
19
|
-
'Read-only recovery diagnostics and an explicit stale-state-lock repair. status reports the state lock owner; clear-stale-lock requires confirm:"CLEAR_STALE_LOCK" and only moves a lock whose PID is provably dead to a backup file. It never resets Tower state or worktrees.',
|
|
16
|
+
'Read-only recovery diagnostics and an explicit stale-state-lock repair. status reports the state lock owner with its age, heartbeat age and staleness; clear-stale-lock requires confirm:"CLEAR_STALE_LOCK" and only moves a lock whose PID is provably dead to a backup file. force:true additionally clears a live-but-silent PID (a recycled PID) and still renames the record to a .recovered-<ts> backup. It never resets Tower state or worktrees.',
|
|
20
17
|
parameters: RecoverInput,
|
|
21
18
|
output: output(SCHEMAS.recover),
|
|
22
19
|
async execute(args, exec) {
|
|
23
|
-
const tower = await
|
|
20
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
24
21
|
const actor = await actorFor(tower, exec)
|
|
25
22
|
requireTowerActor(actor, 'recover Tower state')
|
|
26
|
-
const
|
|
23
|
+
const now = Date.now()
|
|
24
|
+
const lock = await readStateLock(tower.paths, { now })
|
|
25
|
+
const timing = lockTiming(lock.record, { now })
|
|
27
26
|
const publicLock = lock.record
|
|
28
|
-
? {
|
|
27
|
+
? {
|
|
28
|
+
pid: lock.record.pid ?? null,
|
|
29
|
+
startedAt: lock.record.startedAt ?? null,
|
|
30
|
+
at: lock.record.at ?? null,
|
|
31
|
+
alive: lock.alive,
|
|
32
|
+
ageMs: timing.ageMs,
|
|
33
|
+
heartbeatAgeMs: timing.heartbeatAgeMs,
|
|
34
|
+
staleAfterMs: timing.staleAfterMs,
|
|
35
|
+
stale: timing.stale,
|
|
36
|
+
}
|
|
29
37
|
: null
|
|
30
38
|
if (args.action === RECOVERY_ACTION.status) {
|
|
31
39
|
return { ok: true, action: RECOVERY_ACTION.status, stateLock: publicLock, cleared: false }
|
|
@@ -34,12 +42,29 @@ export function registerRecoverTool(ctx, deps) {
|
|
|
34
42
|
throw new TowerToolError('invalid-argument', 'clear-stale-lock requires confirm:"CLEAR_STALE_LOCK"')
|
|
35
43
|
}
|
|
36
44
|
if (!lock.record) return { ok: true, action: RECOVERY_ACTION.clearStaleLock, stateLock: null, cleared: false }
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
const forced = args.force === true
|
|
46
|
+
if (lock.alive !== false && !forced) {
|
|
47
|
+
// A live PID is not proof of a live writer (#92): report the heartbeat
|
|
48
|
+
// and require the explicit force opt-in before moving the record.
|
|
49
|
+
throw new TowerToolError(
|
|
50
|
+
'state-locked',
|
|
51
|
+
`state lock pid ${lock.record.pid ?? ROLE.unknown} is still alive (last heartbeat ${timing.heartbeatAgeMs ?? 'unknown'}ms ago, stale after ${timing.staleAfterMs}ms); pass force:true with confirm:"CLEAR_STALE_LOCK" to move it to a backup after confirming no writer is active`,
|
|
52
|
+
)
|
|
39
53
|
}
|
|
54
|
+
// Always rename, never unlink: the recovered bytes stay auditable. Use the
|
|
55
|
+
// plain port primitive, not `renameWithRetry`: the retrying helper has
|
|
56
|
+
// temp-file semantics and unlinks its source when the rename finally
|
|
57
|
+
// fails, which would destroy the record we are trying to preserve.
|
|
40
58
|
const backup = `${tower.paths.lock}.recovered-${Date.now()}`
|
|
41
|
-
await
|
|
42
|
-
return {
|
|
59
|
+
await tower.deps.store.rename(tower.paths.lock, backup)
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
action: RECOVERY_ACTION.clearStaleLock,
|
|
63
|
+
stateLock: publicLock,
|
|
64
|
+
cleared: true,
|
|
65
|
+
backup,
|
|
66
|
+
forced,
|
|
67
|
+
}
|
|
43
68
|
},
|
|
44
69
|
})
|
|
45
70
|
}
|