dsh-tower 2.1.0 → 2.3.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 +103 -0
- package/COMPATIBILITY.md +5 -1
- package/CONTRIBUTING.md +64 -5
- package/PRINCIPLES.md +145 -0
- package/README.en.md +7 -4
- package/README.md +7 -4
- 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/docs/README.md +6 -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 +50 -0
- package/docs/adr/README.md +51 -0
- package/docs/agents/workflow.md +51 -0
- package/docs/architecture/overview.md +167 -36
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/release.md +1 -1
- package/docs/operations/incident.md +82 -0
- package/docs/operations/runbook.md +12 -3
- package/docs/reference/blockers.md +2 -2
- package/docs/reference/config.generated.md +1 -0
- package/docs/reference/doctor.md +48 -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 +128 -0
- package/docs/reference/tools.generated.md +5 -4
- package/docs/reference/tools.md +4 -4
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/reference/blockers.md +2 -2
- package/docs/zh/reference/doctor.md +29 -1
- package/docs/zh/reference/tools.md +4 -4
- package/domain/batch.js +2 -2
- package/domain/config/schema.js +25 -1
- package/domain/contracts/outputs.js +6 -2
- package/domain/contracts/tool-inputs/ops.js +1 -0
- package/domain/errors.js +297 -42
- 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/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 +248 -0
- package/{lib → host}/doctor.js +116 -9
- package/{lib → host}/modeStore.js +1 -1
- package/{lib → host}/tool-registry.js +106 -31
- package/host/tower.js +32 -0
- package/index.js +12 -9
- package/package.json +10 -3
- 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-docs.mjs +8 -35
- package/scripts/check-pack.mjs +68 -39
- package/scripts/check-philosophy.mjs +266 -0
- package/scripts/domain-metrics.mjs +13 -4
- 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 +11 -0
- package/scripts/issue-audit.mjs +2 -2
- package/scripts/lib/surface.mjs +87 -0
- package/scripts/pack-manifest.txt +58 -22
- package/scripts/regression-audit.mjs +164 -0
- package/scripts/release-check.mjs +1 -1
- package/scripts/surface-baseline.json +21 -0
- package/scripts/surface-baseline.mjs +120 -0
- 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 +7 -7
- 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 +3 -6
- 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 +3 -3
- package/tools/teardown.js +2 -3
- package/tools/test.js +2 -2
- package/types/index.d.ts +1 -1
- 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/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}/hmr.js +0 -0
- /package/{lib → host}/modeState.js +0 -0
- /package/{lib → host}/presets.js +0 -0
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
|
}
|
package/tools/reload.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
// tower_reload: dev-only HMR reload request.
|
|
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 { runtimeConfig } from '../lib/config.js'
|
|
6
|
-
import { createHmrBridge } from '../lib/hmr.js'
|
|
7
4
|
import { ReloadInput } from '../domain/contracts/tool-inputs/index.js'
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
7
|
* @param {any} ctx
|
|
11
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, sharedHmrBridge: any }} deps
|
|
8
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, sharedHmrBridge: any, runtimeConfig: any, createHmrBridge: any }} deps
|
|
12
9
|
*/
|
|
13
10
|
export function registerReloadTool(ctx, deps) {
|
|
14
|
-
const { define, actorFor, requireTowerActor, sharedHmrBridge } = deps
|
|
11
|
+
const { define, actorFor, requireTowerActor, sharedHmrBridge, runtimeConfig, createHmrBridge } = deps
|
|
15
12
|
// Dev-only hot reload. The tool is registered whenever dev.hotReload is on;
|
|
16
13
|
// it reports reload-unavailable when the profile has no HMR service. When the
|
|
17
14
|
// Host passed a shared bridge, its status() reflects the command's reloads too.
|
|
@@ -32,7 +29,7 @@ export function registerReloadTool(ctx, deps) {
|
|
|
32
29
|
output: output(SCHEMAS.reload),
|
|
33
30
|
async execute(args, exec) {
|
|
34
31
|
if (typeof args.repoRoot === 'string') {
|
|
35
|
-
const tower = await
|
|
32
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
36
33
|
const actor = await actorFor(tower, exec)
|
|
37
34
|
requireTowerActor(actor, 'reload the Tower plugin')
|
|
38
35
|
}
|
package/tools/resource.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
// tower_resource: advisory resource leases.
|
|
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 { acquireLease, releaseLease, inspectLeases } from '../lib/leases.js'
|
|
6
4
|
import { ResourceInput } from '../domain/contracts/tool-inputs/index.js'
|
|
7
5
|
import { LEASE_ACTION } from '../domain/vocabulary.js'
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
8
|
* @param {any} ctx
|
|
11
|
-
* @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, acquireLease: any, releaseLease: any, inspectLeases: any }} deps
|
|
12
10
|
*/
|
|
13
11
|
export function registerResourceTool(ctx, deps) {
|
|
14
|
-
const { define, actorFor, requireTowerActor } = deps
|
|
12
|
+
const { define, actorFor, requireTowerActor, acquireLease, releaseLease, inspectLeases } = deps
|
|
15
13
|
define(ctx, {
|
|
16
14
|
name: 'tower_resource',
|
|
17
15
|
description:
|
|
@@ -19,7 +17,7 @@ export function registerResourceTool(ctx, deps) {
|
|
|
19
17
|
parameters: ResourceInput,
|
|
20
18
|
output: output(SCHEMAS.resource),
|
|
21
19
|
async execute(args, exec) {
|
|
22
|
-
const tower = await
|
|
20
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
23
21
|
const actor = await actorFor(tower, exec)
|
|
24
22
|
const redact = (leases) => leases.map(({ token, ...lease }) => lease)
|
|
25
23
|
if (args.action === LEASE_ACTION.status) {
|
package/tools/review.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// tower_review: independent review verdict.
|
|
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 { ReviewInput } from '../domain/contracts/tool-inputs/index.js'
|
|
@@ -7,21 +6,21 @@ import { isReviewerRole, isTowerRole } from '../domain/vocabulary.js'
|
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* @param {any} ctx
|
|
10
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any }} deps
|
|
9
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any }} deps
|
|
11
10
|
*/
|
|
12
11
|
export function registerReviewTool(ctx, deps) {
|
|
13
12
|
const { define, actorFor } = deps
|
|
14
13
|
define(ctx, {
|
|
15
14
|
name: 'tower_review',
|
|
16
15
|
description:
|
|
17
|
-
'Submit an independent review verdict
|
|
16
|
+
'Submit an independent review verdict bound to the current branch tip: a completed mission starts a review round, and while the mission is active a later route may still record a verdict for that same reviewed tip, appending to the same round without changing the status (a moved tip is rejected with an explanation). status is "clean", "p1-Nitems", or "p2-Nitems"; merge is "merge", "fix-then-merge", or "hold" (only clean + merge is later approval). Non-clean returns the mission to active. Reviewers are capped at 5 rounds per branch; a tower-submitted review is break-glass and requires breakGlassReason.',
|
|
18
17
|
parameters: ReviewInput,
|
|
19
18
|
aliases: {"missionId":["id"]},
|
|
20
19
|
requiresMissionId: true,
|
|
21
20
|
board: true,
|
|
22
21
|
output: output(SCHEMAS.review),
|
|
23
22
|
async execute(args, exec) {
|
|
24
|
-
const tower = await
|
|
23
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
25
24
|
const actor = await actorFor(tower, exec)
|
|
26
25
|
if (!isReviewerRole(actor.role) && !isTowerRole(actor.role)) {
|
|
27
26
|
throw new TowerToolError('not-authorized', `only an assigned reviewer or the tower may review; ${actor.name} is ${actor.role}`)
|
package/tools/roster.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// tower_roster: authoritative roster activity.
|
|
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 { RosterInput } from '../domain/contracts/tool-inputs/index.js'
|
|
@@ -7,7 +6,7 @@ import { PARENT_STATUS_OK, ROSTER_ACTIVITY, ROSTER_GONE_STATES } from '../domain
|
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* @param {any} ctx
|
|
10
|
-
* @param {{ define: import('../types/index.js').DefineTool }} deps
|
|
9
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any }} deps
|
|
11
10
|
*/
|
|
12
11
|
export function registerRosterTool(ctx, deps) {
|
|
13
12
|
const { define } = deps
|
|
@@ -18,7 +17,7 @@ export function registerRosterTool(ctx, deps) {
|
|
|
18
17
|
parameters: RosterInput,
|
|
19
18
|
output: output(SCHEMAS.roster),
|
|
20
19
|
async execute(args) {
|
|
21
|
-
const tower = await
|
|
20
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
22
21
|
const view = await tower.rosterView()
|
|
23
22
|
const subagents = ctx.get('subagents')
|
|
24
23
|
if (!subagents) throw new TowerToolError('spawn-unavailable', 'subagents service is unavailable; cannot observe roster')
|
package/tools/send.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// tower_send: durable inbox message delivery.
|
|
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 { SendInput } from '../domain/contracts/tool-inputs/index.js'
|
|
@@ -17,6 +16,7 @@ import {
|
|
|
17
16
|
* @param {any} ctx
|
|
18
17
|
* @param {{
|
|
19
18
|
* define: import('../types/index.js').DefineTool,
|
|
19
|
+
* openTower: any,
|
|
20
20
|
* actorFor: any,
|
|
21
21
|
* checkSendRate: any,
|
|
22
22
|
* }} deps
|
|
@@ -31,7 +31,7 @@ export function registerSendTool(ctx, deps) {
|
|
|
31
31
|
aliases: { consentRef: ['consent_ref'] },
|
|
32
32
|
output: output(SCHEMAS.send),
|
|
33
33
|
async execute(args, exec) {
|
|
34
|
-
const tower = await
|
|
34
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
35
35
|
checkSendRate(tower.config, args.repoRoot)
|
|
36
36
|
const actor = await actorFor(tower, exec)
|
|
37
37
|
const view = await tower.rosterView()
|
package/tools/spawn.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// tower_spawn: provision and start a worker/reviewer.
|
|
2
|
-
import { Tower } from '../app/tower.js'
|
|
3
2
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
4
3
|
import { TowerToolError, classifyError } from '../domain/errors.js'
|
|
5
4
|
import { SpawnInput } from '../domain/contracts/tool-inputs/index.js'
|
|
@@ -9,13 +8,14 @@ import {
|
|
|
9
8
|
precheckSpawnServices,
|
|
10
9
|
precheckWorkerOwnership,
|
|
11
10
|
resolveRosterName,
|
|
12
|
-
} from '../
|
|
13
|
-
import { buildAgentOptions, buildSpawnBriefing, waitForSpawnResult } from '../
|
|
11
|
+
} from '../app/spawn-prechecks.js'
|
|
12
|
+
import { buildAgentOptions, buildSpawnBriefing, waitForSpawnResult } from '../app/spawn-support.js'
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* @param {any} ctx
|
|
17
16
|
* @param {{
|
|
18
17
|
* define: import('../types/index.js').DefineTool,
|
|
18
|
+
* openTower: any,
|
|
19
19
|
* actorFor: any,
|
|
20
20
|
* requireTowerActor: any,
|
|
21
21
|
* enqueueSpawn: any,
|
|
@@ -25,6 +25,7 @@ import { buildAgentOptions, buildSpawnBriefing, waitForSpawnResult } from '../li
|
|
|
25
25
|
* recordSpawnSuccess: any,
|
|
26
26
|
* runMissionDiscovery: any,
|
|
27
27
|
* textOf: any,
|
|
28
|
+
* inspectLeases: any,
|
|
28
29
|
* }} deps
|
|
29
30
|
*/
|
|
30
31
|
export function registerSpawnTool(ctx, deps) {
|
|
@@ -43,7 +44,7 @@ export function registerSpawnTool(ctx, deps) {
|
|
|
43
44
|
board: true,
|
|
44
45
|
output: output(SCHEMAS.spawn),
|
|
45
46
|
async execute(args, exec) {
|
|
46
|
-
const tower = await
|
|
47
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
47
48
|
const actor = await actorFor(tower, exec)
|
|
48
49
|
requireTowerActor(actor, 'spawn tower agents')
|
|
49
50
|
const role = args.role ?? DEFAULT_ROLE
|
|
@@ -68,7 +69,9 @@ export function registerSpawnTool(ctx, deps) {
|
|
|
68
69
|
role, tower, ctx, view, missionId: args.missionId, takeover: args.takeover,
|
|
69
70
|
maxLiveWorkers: config.spawn.maxLiveWorkers, budget: backoff.budget, observeActivity,
|
|
70
71
|
})
|
|
71
|
-
const { subagents, provider } = await precheckSpawnServices({
|
|
72
|
+
const { subagents, provider } = await precheckSpawnServices({
|
|
73
|
+
tower, ctx, config, preset, actor, exec, inspectLeases: deps.inspectLeases,
|
|
74
|
+
})
|
|
72
75
|
|
|
73
76
|
const provisioned = await tower.provision({
|
|
74
77
|
missionId: args.missionId,
|
package/tools/status.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// tower_status: dashboard and mission detail.
|
|
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 { StatusInput } from '../domain/contracts/tool-inputs/index.js'
|
|
@@ -10,6 +9,7 @@ import { archiveView } from '../app/archive.js'
|
|
|
10
9
|
* @param {any} ctx
|
|
11
10
|
* @param {{
|
|
12
11
|
* define: import('../types/index.js').DefineTool,
|
|
12
|
+
* openTower: any,
|
|
13
13
|
* workspaceHook: any,
|
|
14
14
|
* spawnBackoffView: any,
|
|
15
15
|
* observeActivity: any,
|
|
@@ -20,12 +20,12 @@ export function registerStatusTool(ctx, deps) {
|
|
|
20
20
|
define(ctx, {
|
|
21
21
|
name: 'tower_status',
|
|
22
22
|
description:
|
|
23
|
-
'Read the Tower dashboard for a repository: state path, base branch, revision, mission status counts, paged mission summaries (scope/deps/tasks/worktree/owner), roster size, findings, archive count, config path and the activity tail. Pass missionId for one full mission (tasks, notes, reviews, findings, completion report; archived missions are loaded from their year file); offset/limit page the mission list (default 20, max 100); includeGate adds preflight gate digests for the page; includeArchived appends compact archived summaries after the active page; format:"markdown" returns the human-readable mission board (active missions plus an archived count). Read-only.',
|
|
23
|
+
'Read the Tower dashboard for a repository: state path, base branch, revision, mission status counts, paged mission summaries (scope/deps/tasks/worktree/owner), roster size, findings, archive count, config path and the activity tail. Also reports the repo-scoped git lock holder (`gitLock: {held,pid,phase,heldMs}`) so a merge holding the mutex is visible. Pass missionId for one full mission (tasks, notes, reviews, findings, completion report; archived missions are loaded from their year file); offset/limit page the mission list (default 20, max 100); includeGate adds preflight gate digests for the page; includeArchived appends compact archived summaries after the active page; format:"markdown" returns the human-readable mission board (active missions plus an archived count). Read-only.',
|
|
24
24
|
parameters: StatusInput,
|
|
25
25
|
aliases: { mission_id: ['missionId'], include_gate: ['includeGate'], include_archived: ['includeArchived'] },
|
|
26
26
|
output: output(SCHEMAS.status),
|
|
27
27
|
async execute(args, exec) {
|
|
28
|
-
const tower = await
|
|
28
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
29
29
|
const boardPath = tower.boardFile()
|
|
30
30
|
const mode = workspaceHook?.modeInfo?.(exec) ?? null
|
|
31
31
|
const statusState = await tower.load()
|
package/tools/teardown.js
CHANGED
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
// Pure eligibility decisions live in domain/integration.
|
|
9
9
|
// See docs/reference/tools.generated.md for the public shape.
|
|
10
10
|
// tower_teardown: remove eligible mission worktrees.
|
|
11
|
-
import { Tower } from '../app/tower.js'
|
|
12
11
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
13
12
|
import { TeardownInput } from '../domain/contracts/tool-inputs/index.js'
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* @param {any} ctx
|
|
17
|
-
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, observeActivity: any }} deps
|
|
16
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any, requireTowerActor: any, observeActivity: any }} deps
|
|
18
17
|
*/
|
|
19
18
|
export function registerTeardownTool(ctx, deps) {
|
|
20
19
|
const { define, actorFor, requireTowerActor, observeActivity } = deps
|
|
@@ -27,7 +26,7 @@ export function registerTeardownTool(ctx, deps) {
|
|
|
27
26
|
board: true,
|
|
28
27
|
output: output(SCHEMAS.teardown),
|
|
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, 'tear down worktrees')
|
|
33
32
|
const { activity } = await observeActivity(ctx, tower)
|
package/tools/test.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// tower_test: run declared worktree test discovery.
|
|
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 { TestInput } from '../domain/contracts/tool-inputs/index.js'
|
|
@@ -9,6 +8,7 @@ import { isReviewerRole, isWorkerRole } from '../domain/vocabulary.js'
|
|
|
9
8
|
* @param {any} ctx
|
|
10
9
|
* @param {{
|
|
11
10
|
* define: import('../types/index.js').DefineTool,
|
|
11
|
+
* openTower: any,
|
|
12
12
|
* actorFor: any,
|
|
13
13
|
* requireKnownActor: any,
|
|
14
14
|
* runMissionDiscovery: any,
|
|
@@ -25,7 +25,7 @@ export function registerTestTool(ctx, deps) {
|
|
|
25
25
|
requiresMissionId: true,
|
|
26
26
|
output: output(SCHEMAS.test),
|
|
27
27
|
async execute(args, exec) {
|
|
28
|
-
const tower = await
|
|
28
|
+
const tower = await deps.openTower(args.repoRoot)
|
|
29
29
|
const actor = await actorFor(tower, exec)
|
|
30
30
|
requireKnownActor(actor, 'tower_test')
|
|
31
31
|
const context = await tower.missionContext(args.missionId)
|
package/types/index.d.ts
CHANGED
|
@@ -106,7 +106,7 @@ export interface CompletionReport {
|
|
|
106
106
|
checks?: CompletionCheck[]
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/** One recorded test-discovery receipt (
|
|
109
|
+
/** One recorded test-discovery receipt (adapters/tests.js). */
|
|
110
110
|
export interface TestRun {
|
|
111
111
|
version?: number
|
|
112
112
|
ok?: boolean
|
package/ui-bundle/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Tower UI (optional Client bundle)
|
|
2
2
|
|
|
3
|
-
An **optional** Web Client bundle for the Host `dsh-tower` plugin. It never touches Tower state directly: everything it renders is derived from the conversation the user is already looking at, or from the tool results already in that conversation. There is no Host → Client remote channel, no `host.call`, no DOM manipulation, no iframe, and no import of Harness Client packages (the only module it requires is `react`
|
|
3
|
+
An **optional** Web Client bundle for the Host `dsh-tower` plugin. It never touches Tower state directly: everything it renders is derived from the conversation the user is already looking at, or from the tool results already in that conversation. There is no Host → Client remote channel, no `host.call`, no DOM manipulation, no iframe, and no import of Harness Client packages (the only browser module-table word it requires is `react`).
|
|
4
4
|
|
|
5
5
|
Version: **0.2.2** (private, not published). The Host plugin version is unchanged.
|
|
6
6
|
|
|
@@ -20,6 +20,39 @@ The cards are deliberately compact: mission status/counts, task checklist progre
|
|
|
20
20
|
|
|
21
21
|
The overlay strip is `pointer-events: none` end-to-end and uses `role="status"` / `aria-live="polite"`; it never blocks the composer or the page.
|
|
22
22
|
|
|
23
|
+
## Source layout (#71)
|
|
24
|
+
|
|
25
|
+
The bundle is split along the surfaces above. `client.js` is the entry: it owns
|
|
26
|
+
the diagnostics seam and the registrations, and loads the package-local chunks
|
|
27
|
+
through the DSH client module system's `require.async("./client.<surface>.js")`
|
|
28
|
+
(bundles execute as classic scripts, so the entry cannot require a sibling
|
|
29
|
+
synchronously). Every chunk registers through the same `window.__ModuleLoader__`
|
|
30
|
+
surface as `{ id: 'dsh-tower-ui', chunk: 'client.<surface>.js', factory }` and
|
|
31
|
+
receives the shared helpers/styles/locales through the `env` the entry composes;
|
|
32
|
+
no chunk reaches the Host or another chunk.
|
|
33
|
+
|
|
34
|
+
| File | Surface |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `client.js` | entry: diagnostics, `apply`, the five registrations |
|
|
37
|
+
| `client.toolview.js` | toolview card: lifecycle + `TowerToolCard` |
|
|
38
|
+
| `client.cards.js` | the per-tool card models behind the card |
|
|
39
|
+
| `client.dock.js` | composer dock: mode chip, unknown state, latest summary |
|
|
40
|
+
| `client.mode.js` | history-derived mode model + observable |
|
|
41
|
+
| `client.denied.js` | mode-aware `Tower mode · disabled` rows |
|
|
42
|
+
| `client.overlay.js` | transient `shell.overlay` status strip + notice store |
|
|
43
|
+
| `client.locales.js` | `en`/`zh` dictionaries and the `t`-seat helpers |
|
|
44
|
+
| `client.styles.js` | the `--dsw-alias-*` inline style table |
|
|
45
|
+
| `client.helpers.js` | tool vocabulary + defensive payload helpers |
|
|
46
|
+
| `index.js` | Host half (empty `apply`) |
|
|
47
|
+
|
|
48
|
+
The client tier is gated by `scripts/architecture.mjs` like any other source
|
|
49
|
+
root: `client-no-node` (no `node:*` imports; DOM globals are fine),
|
|
50
|
+
`client-size` (800-line ceiling per file) and `client-ambient` (no module-level
|
|
51
|
+
`let`; state lives in the `create*` closure), each with a seeded fixture in
|
|
52
|
+
`tests/architecture.test.mjs`. Every client source file stays under 400 lines
|
|
53
|
+
and the split preserves the pre-split line total within 10%
|
|
54
|
+
(`tests/ui-bundle.test.mjs`).
|
|
55
|
+
|
|
23
56
|
## Persistent Tower-mode indicator (#11)
|
|
24
57
|
|
|
25
58
|
Tower mode is a Host-side session stance and there is no Client RPC for it. The dock derives it from the durable session history that the Chat projection exposes in `useChat(snapshot.legacy)`:
|
|
@@ -38,7 +71,7 @@ The Host guard returns a plain tool error like `tower mode: <tool> is disabled w
|
|
|
38
71
|
|
|
39
72
|
While the displayed session is in Tower mode the bundle dynamically registers `tool.call.toolview` entries for:
|
|
40
73
|
|
|
41
|
-
- `tower_mode.deniedWhileActive` when the latest `tower_mode` output lists it, otherwise `
|
|
74
|
+
- `tower_mode.deniedWhileActive` when the latest `tower_mode` output lists it, otherwise `host/config.js` `DEFAULT_DENY_TOOLS` (`create_goal`, `todo_write`, `team_task_create`, `team_task_update`, `team_task_reopen`, `team_task_list`, `spawn_teammate`, `wait_agent`, `workflow`, `ralph`, `subagent`, `subagent_fork`);
|
|
42
75
|
- `update_goal` unconditionally, because the guard special-cases its `action: "resume"`.
|
|
43
76
|
|
|
44
77
|
Those entries register with `priority: -10`. The slot core renders the lowest priority for a key, so this shadows the native keyed views without replacing them; disposing the entries (mode off) restores the native views exactly. A denial result renders as a distinct **`Tower mode · disabled`** row (warn marker, label, the preserved denial message, and the `/tower off` hint) — not the generic error text styling. `update_goal` resume denials get the same row. A non-denial result that lands in one of those slots while the mode is on renders a defensive generic row (there is no native view to delegate to while the registration shadows it); ordinary tool errors are not misclassified as Tower-mode denials.
|
|
@@ -56,7 +89,7 @@ Verified **statically** against the installed DSH **0.2.1-rc.2** sources:
|
|
|
56
89
|
- Locale: `ctx.locale.register(ns, { en, zh })`, dictionaries are flat `key → template` maps with `{param}` interpolation; a slot declaration's `locale` option supplies the bound `t` seat.
|
|
57
90
|
- The bundle's `dsh.client.inject` orders activation after `ui-conversation` and `ui-tool`, the two packages that declare the slots it targets. `ctx.slots.inject` only installs a registration while its owner declaration exists; each injection is guarded and reported in diagnostics instead of crashing the whole Client half.
|
|
58
91
|
|
|
59
|
-
Automated offline coverage: `tests/ui-bundle.test.mjs` loads `client.js` through a fake `window.__ModuleLoader__
|
|
92
|
+
Automated offline coverage: `tests/ui-bundle.test.mjs` loads `client.js` through a fake `window.__ModuleLoader__` (and each split chunk through the same loader when the entry calls `require.async`), asserts the 12 keys / dock id / overlay id, en–zh dictionary parity, theme-token discipline, every file's loader format and the split-size budget, and renders every card plus the dock/overlay components with fake hooks. It also drives fake chat history through the dock to assert the off hint, the on chip (including refresh re-derivation), `tower_mode`/`tower_control`/command updates, dynamic registration for default and configured deny lists, disposal on mode off, `update_goal`, denial-row treatment, generic fallbacks, and locale parity — no browser required.
|
|
60
93
|
|
|
61
94
|
Live/browser verification is **opt-in** (`tests/live/ui-slots.cjs`) because it needs an isolated profile with the bundle installed. From a teammate/subagent session the Client `Slots` inspect query times out by design (the page answers only the agent whose session it displays), so this phase did not run a live inspection; the source-level contract above is the evidence. When the script runs against an installed profile it binds a session with a bare `/tower` command and asserts, through the read-only `window.__TOWER_UI_DIAG__` report, that apply() ran without errors, that all 12 toolview keys and the dock/overlay entries registered, that the dock outlet is visible, and — since the bind command turns the Host mode on — that the history-derived `Tower · on` chip rendered (#11). The chip can be inspected visually in the same run (`data-tower-ui="dock-mode"`).
|
|
62
95
|
|
|
@@ -65,6 +98,7 @@ Live/browser verification is **opt-in** (`tests/live/ui-slots.cjs`) because it n
|
|
|
65
98
|
```powershell
|
|
66
99
|
# offline, no browser
|
|
67
100
|
node --check ui-bundle/client.js
|
|
101
|
+
Get-ChildItem ui-bundle -Filter 'client*.js' | ForEach-Object { node --check $_.FullName }
|
|
68
102
|
node --test tests/ui-bundle.test.mjs
|
|
69
103
|
|
|
70
104
|
# opt-in live check (isolated profile only)
|