dsh-tower 2.1.0-beta.1 → 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 +154 -0
- package/COMPATIBILITY.md +6 -1
- package/CONTRIBUTING.md +71 -5
- package/PRINCIPLES.md +145 -0
- package/README.en.md +14 -5
- package/README.md +14 -5
- package/SECURITY.md +6 -0
- package/adapters/archive.js +279 -0
- package/adapters/caseSensitivity.js +66 -0
- package/adapters/default.js +51 -8
- package/{lib → adapters}/git.js +1 -1
- package/{lib → adapters}/leases.js +15 -6
- package/{lib → adapters}/legacy.js +20 -6
- package/adapters/persistence.js +543 -0
- package/adapters/stateFile.js +99 -0
- package/adapters/store-fs.js +28 -1
- package/adapters/store.js +204 -0
- package/{lib → adapters}/tests.js +15 -7
- package/adapters/vcs-git.js +15 -1
- package/{lib → adapters}/workspaceLock.js +1 -1
- package/{lib → adapters}/worktreeOwnership.js +18 -7
- package/app/archive.js +188 -0
- package/app/batch.js +9 -10
- package/app/board.js +4 -4
- package/app/checkCompletion.js +23 -11
- package/app/enrichedMissions.js +2 -4
- package/app/findings.js +6 -7
- package/app/gate.js +32 -15
- package/app/init.js +59 -22
- package/app/merge.js +64 -51
- package/app/messages.js +7 -8
- package/app/mission.js +7 -5
- package/app/plan.js +20 -9
- package/app/provision.js +12 -14
- package/app/read.js +65 -6
- package/app/rebase.js +14 -16
- package/app/recording.js +7 -8
- package/app/review.js +17 -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 +28 -8
- package/app/tower.js +121 -39
- 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/domain.md +38 -0
- package/docs/agents/issue-tracker.md +46 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/agents/workflow.md +51 -0
- package/docs/architecture/overview.md +170 -37
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/install-upgrade-migration.md +18 -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 +3 -3
- package/docs/reference/config.generated.md +5 -0
- package/docs/reference/config.md +4 -0
- package/docs/reference/doctor.md +104 -8
- package/docs/reference/errors.generated.md +41 -35
- package/docs/reference/errors.md +54 -40
- package/docs/reference/events.md +38 -6
- package/docs/reference/surface-budget.md +128 -0
- package/docs/reference/tools.generated.md +9 -8
- package/docs/reference/tools.md +9 -8
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/guide/install-upgrade-migration.md +13 -1
- package/docs/zh/reference/blockers.md +3 -3
- package/docs/zh/reference/config.md +4 -0
- package/docs/zh/reference/doctor.md +63 -2
- package/docs/zh/reference/errors.md +4 -2
- package/docs/zh/reference/tools.md +6 -6
- package/domain/batch.js +2 -2
- package/domain/config/schema.js +51 -4
- package/domain/contracts/outputs.js +33 -3
- package/domain/contracts/tool-inputs/missions.js +2 -1
- package/domain/contracts/tool-inputs/ops.js +1 -0
- package/domain/errors.js +297 -40
- package/domain/events.js +207 -0
- package/domain/integration/gate.js +41 -12
- package/domain/integration/init.js +40 -4
- package/domain/integration/merge.js +35 -0
- package/domain/lockBudget.js +30 -0
- package/domain/mission.js +1 -1
- package/domain/missions/completion.js +72 -5
- package/domain/missions/lifecycle.js +11 -4
- package/domain/missions/plan.js +14 -0
- package/domain/missions/review.js +90 -7
- package/domain/planning.js +6 -3
- package/domain/read/views.js +17 -1
- package/domain/report.js +9 -1
- package/domain/scope.js +83 -11
- package/domain/state/archive.js +257 -0
- package/domain/state/migrations.js +145 -0
- package/domain/state/schema.js +6 -0
- package/domain/state.js +154 -4
- package/domain/summary.js +2 -0
- package/domain/surface.js +248 -0
- package/host/doctor.js +555 -0
- 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 +64 -19
- 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 +3 -4
- 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 +4 -7
- 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 +7 -6
- package/tools/preflight.js +3 -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 +16 -8
- package/tools/teardown.js +3 -4
- package/tools/test.js +2 -2
- package/types/index.d.ts +35 -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/doctor.js +0 -270
- package/lib/persistence.js +0 -349
- package/lib/store.js +0 -118
- /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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// dsh-tower-ui 0.2.2 — the toolview card surface (one keyed view per tool).
|
|
2
|
+
//
|
|
3
|
+
// The chat-driven card publishes lifecycle transitions into the notice store
|
|
4
|
+
// the root-scoped overlay injects. Package-local chunk (#71).
|
|
5
|
+
window.__ModuleLoader__.load({
|
|
6
|
+
id: 'dsh-tower-ui',
|
|
7
|
+
chunk: 'client.toolview.js',
|
|
8
|
+
factory() {
|
|
9
|
+
function createToolview(env) {
|
|
10
|
+
const { React, h, translate, toolLabel, seatT, ST, dotStyle, helpers, cards, notice } = env
|
|
11
|
+
const { isTowerTool, parsePayload, isPlainObject, truncate } = helpers
|
|
12
|
+
const { rowElement, cardModel } = cards
|
|
13
|
+
const publishNotice = notice.publish
|
|
14
|
+
|
|
15
|
+
// Mounting an already-settled result (history) stays silent; a live
|
|
16
|
+
// preparing/start row and a start→result transition publish.
|
|
17
|
+
function useToolviewLifecycle(props) {
|
|
18
|
+
const phase = props ? props.phase : undefined
|
|
19
|
+
const toolName = props ? props.toolName : undefined
|
|
20
|
+
const callId = props ? props.callId : undefined
|
|
21
|
+
const failed = Boolean(props && props.block && (props.block.isError === true || isPlainObject(props.block.error)))
|
|
22
|
+
const previous = React.useRef ? React.useRef(null) : { current: null }
|
|
23
|
+
React.useEffect(
|
|
24
|
+
function () {
|
|
25
|
+
const before = previous.current
|
|
26
|
+
previous.current = { phase, callId }
|
|
27
|
+
if (!isTowerTool(toolName)) return
|
|
28
|
+
const sameCall = before !== null && before.callId === callId
|
|
29
|
+
if (phase !== 'result') {
|
|
30
|
+
if (!sameCall || before.phase !== phase) publishNotice({ tool: toolName, running: true })
|
|
31
|
+
} else if (sameCall && before.phase !== 'result') {
|
|
32
|
+
publishNotice({ tool: toolName, running: false, ok: failed !== true })
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
[phase, callId, toolName, failed],
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function TowerToolCard(props) {
|
|
40
|
+
const t = seatT(props)
|
|
41
|
+
const toolName = props && typeof props.toolName === 'string' ? props.toolName : ''
|
|
42
|
+
const phase = props && typeof props.phase === 'string' ? props.phase : 'start'
|
|
43
|
+
const running = phase !== 'result'
|
|
44
|
+
useToolviewLifecycle(props)
|
|
45
|
+
let payload = null
|
|
46
|
+
if (!running) payload = parsePayload(props ? props.block : undefined)
|
|
47
|
+
const model = running
|
|
48
|
+
? { tone: 'idle', headline: translate(t, 'card.running'), rows: [] }
|
|
49
|
+
: cardModel(toolName, payload, t)
|
|
50
|
+
const title =
|
|
51
|
+
toolLabel(t, toolName) +
|
|
52
|
+
' · ' +
|
|
53
|
+
(running ? translate(t, 'card.running') : model.headline)
|
|
54
|
+
return h(
|
|
55
|
+
'div',
|
|
56
|
+
{
|
|
57
|
+
'data-tower-toolview': toolName === '' ? 'tower_unknown' : toolName,
|
|
58
|
+
'data-tower-phase': phase,
|
|
59
|
+
style: ST.card,
|
|
60
|
+
title,
|
|
61
|
+
},
|
|
62
|
+
h(
|
|
63
|
+
'div',
|
|
64
|
+
{ style: ST.header },
|
|
65
|
+
h('span', { 'aria-hidden': true, style: dotStyle(running ? 'idle' : model.tone) }),
|
|
66
|
+
h('span', { style: ST.title }, translate(t, 'card.title')),
|
|
67
|
+
h('span', { style: ST.dim }, '·'),
|
|
68
|
+
h('span', { style: ST.title }, toolLabel(t, toolName)),
|
|
69
|
+
h('span', { style: running ? ST.dim : model.tone === 'error' ? ST.rowError : ST.dim }, truncate(model.headline, 100)),
|
|
70
|
+
),
|
|
71
|
+
model.rows.slice(0, 6).map(function (row, index) {
|
|
72
|
+
return rowElement(t, index, row)
|
|
73
|
+
}),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { TowerToolCard }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { createToolview }
|
|
81
|
+
},
|
|
82
|
+
})
|
package/ui-bundle/package.json
CHANGED
package/lib/doctor.js
DELETED
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
// Read-only environment contract and health checks for a Tower repository.
|
|
2
|
-
import { execFile } from '../adapters/vcs-git.js'
|
|
3
|
-
import { promisify } from 'node:util'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { fsp } from '../adapters/store-fs.js'
|
|
6
|
-
import { git } from './git.js'
|
|
7
|
-
import { readStateLock } from './store.js'
|
|
8
|
-
import { assessHostVersion, detectHostVersion, readCompatibility } from './compatibility.js'
|
|
9
|
-
import { profileConfig } from './config.js'
|
|
10
|
-
import { inspectWorkspaceLock } from './workspaceLock.js'
|
|
11
|
-
|
|
12
|
-
const execFileAsync = promisify(execFile)
|
|
13
|
-
|
|
14
|
-
async function exists(target) {
|
|
15
|
-
try {
|
|
16
|
-
await fsp.stat(target)
|
|
17
|
-
return true
|
|
18
|
-
} catch {
|
|
19
|
-
return false
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function gitVersion() {
|
|
24
|
-
try {
|
|
25
|
-
const { stdout } = await execFileAsync('git', ['--version'], { windowsHide: true })
|
|
26
|
-
return String(stdout).trim()
|
|
27
|
-
} catch {
|
|
28
|
-
return null
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function check(id, status, detail, action) {
|
|
33
|
-
return { id, status, detail, ...(action ? { action } : {}) }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Run every read-only diagnostic; never writes state or takes the state lock. */
|
|
37
|
-
export async function runDoctor({ tower, ctx, agent, hostVersion, compatibilityFile, loaderRows }) {
|
|
38
|
-
const checks = []
|
|
39
|
-
const version = await gitVersion()
|
|
40
|
-
checks.push(version ? check('git', 'pass', version) : check('git', 'fail', 'git is not available', 'install Git and retry'))
|
|
41
|
-
|
|
42
|
-
const detectedHost = hostVersion ?? detectHostVersion(ctx)
|
|
43
|
-
const compatibilityRows = readCompatibility(compatibilityFile)
|
|
44
|
-
const hostAssessment = assessHostVersion(detectedHost, compatibilityRows)
|
|
45
|
-
checks.push(
|
|
46
|
-
check(
|
|
47
|
-
'host-version',
|
|
48
|
-
hostAssessment.verdict === 'verified' ? 'pass' : 'warn',
|
|
49
|
-
detectedHost
|
|
50
|
-
? `${detectedHost}: ${hostAssessment.detail}`
|
|
51
|
-
: `unknown host version; ${hostAssessment.detail}. Include host version + COMPATIBILITY.md row in bug reports.`,
|
|
52
|
-
'see COMPATIBILITY.md for verified host builds',
|
|
53
|
-
),
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
const rows = Array.isArray(loaderRows)
|
|
57
|
-
? loaderRows
|
|
58
|
-
: (() => {
|
|
59
|
-
try {
|
|
60
|
-
const loader = ctx?.get?.('loader') ?? ctx?.loader
|
|
61
|
-
const source = loader?.entries ?? loader?.rows ?? loader?.store?.entries
|
|
62
|
-
if (Array.isArray(source)) return source
|
|
63
|
-
if (source && typeof source === 'object') return Object.values(source)
|
|
64
|
-
} catch {
|
|
65
|
-
// loader inventory is optional
|
|
66
|
-
}
|
|
67
|
-
return null
|
|
68
|
-
})()
|
|
69
|
-
if (rows === null) {
|
|
70
|
-
checks.push(check('loader-conflicts', 'warn', 'loader inventory unavailable; cannot detect co-installed Tower packages'))
|
|
71
|
-
} else {
|
|
72
|
-
const towerRows = rows.filter((row) => row && row.name === 'dsh-tower')
|
|
73
|
-
const foreign = towerRows.filter((row) => row.id !== 'tower')
|
|
74
|
-
checks.push(
|
|
75
|
-
towerRows.length > 1 || foreign.length > 0
|
|
76
|
-
? check(
|
|
77
|
-
'loader-conflicts',
|
|
78
|
-
'fail',
|
|
79
|
-
`another loader row resolves to "dsh-tower" (${towerRows.map((row) => row.id ?? '?').join(', ')}); uninstall the conflicting dsh-plugins-tower row`,
|
|
80
|
-
'see README "Known conflicts"',
|
|
81
|
-
)
|
|
82
|
-
: check('loader-conflicts', 'pass', 'no conflicting dsh-tower loader rows'),
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
checks.push(check('repository', 'pass', `${tower.root} (common ${tower.commonDir})`))
|
|
87
|
-
let state = null
|
|
88
|
-
try {
|
|
89
|
-
state = await tower.load()
|
|
90
|
-
checks.push(check('state', 'pass', `${tower.paths.state} revision ${state.revision}`))
|
|
91
|
-
checks.push(
|
|
92
|
-
state.towerAgentId
|
|
93
|
-
? check('tower-owner', 'pass', `bound to ${String(state.towerAgentId)}`)
|
|
94
|
-
: check('tower-owner', 'warn', 'state has no towerAgentId; write tools will fail closed', 'run tower_init with the restored session to re-claim ownership'),
|
|
95
|
-
)
|
|
96
|
-
} catch (error) {
|
|
97
|
-
checks.push(check('state', 'fail', String(error?.message ?? error), 'run tower_init or restore the state file'))
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const config = tower.config ?? {}
|
|
101
|
-
const profile = profileConfig()
|
|
102
|
-
const staleDefaults = []
|
|
103
|
-
if (profile.review && profile.review.requireReview === undefined) {
|
|
104
|
-
staleDefaults.push('review.requireReview now defaults to false (2.0.3)')
|
|
105
|
-
}
|
|
106
|
-
if (profile.mode && profile.mode.allowAgentControl === undefined) {
|
|
107
|
-
staleDefaults.push('mode.allowAgentControl now defaults to true (2.0.4)')
|
|
108
|
-
}
|
|
109
|
-
checks.push(
|
|
110
|
-
staleDefaults.length > 0
|
|
111
|
-
? check('default-reversals', 'warn', staleDefaults.join('; '), 'pin the key explicitly in the profile if the old posture is required')
|
|
112
|
-
: check('default-reversals', 'pass', 'profile does not rely on a reversed default'),
|
|
113
|
-
)
|
|
114
|
-
checks.push(
|
|
115
|
-
check(
|
|
116
|
-
'config',
|
|
117
|
-
config.configPath && config.repoConfigPresent ? 'pass' : 'warn',
|
|
118
|
-
config.repoConfigPresent ? `repo config ${config.configPath}` : `repo config not present (${config.configPath ?? 'unknown'})`,
|
|
119
|
-
'optional: add <git-common-dir>/dsh-tower/config.json',
|
|
120
|
-
),
|
|
121
|
-
)
|
|
122
|
-
const presets = ctx.get?.('agentPresets')
|
|
123
|
-
if (presets && typeof presets.list === 'function') {
|
|
124
|
-
try {
|
|
125
|
-
const available = (await presets.list()).map((entry) => String(entry.id))
|
|
126
|
-
const missing = [config.spawn?.workerPreset ?? 'tower-worker', config.spawn?.reviewerPreset ?? 'tower-reviewer']
|
|
127
|
-
.filter((id) => !available.includes(id))
|
|
128
|
-
checks.push(
|
|
129
|
-
missing.length === 0
|
|
130
|
-
? check('presets', 'pass', `worker=${config.spawn?.workerPreset ?? 'tower-worker'} reviewer=${config.spawn?.reviewerPreset ?? 'tower-reviewer'}`)
|
|
131
|
-
: check('presets', 'fail', `missing preset(s): ${missing.join(', ')}`, 'enable the Tower preset bundle and retry'),
|
|
132
|
-
)
|
|
133
|
-
} catch (error) {
|
|
134
|
-
checks.push(check('presets', 'warn', `preset registry unavailable: ${String(error?.message ?? error)}`))
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
checks.push(check('presets', 'warn', 'agentPresets service is unavailable'))
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const subagents = ctx.get?.('subagents')
|
|
141
|
-
checks.push(subagents ? check('subagents', 'pass', 'service mounted') : check('subagents', 'fail', 'subagents service is unavailable', 'enable the subagent bundle'))
|
|
142
|
-
const hmr = ctx.get?.('hmr')
|
|
143
|
-
checks.push(hmr ? check('hmr', 'pass', 'service mounted') : check('hmr', 'warn', 'HMR is not mounted; /tower reload is unavailable'))
|
|
144
|
-
|
|
145
|
-
if (state) {
|
|
146
|
-
const lock = await readStateLock(tower.paths)
|
|
147
|
-
checks.push(
|
|
148
|
-
!lock.record
|
|
149
|
-
? check('state-lock', 'pass', 'no state lock file')
|
|
150
|
-
: lock.alive === false
|
|
151
|
-
? check('state-lock', 'warn', `stale state lock (pid ${lock.record.pid ?? 'unknown'} is gone); the next mutation reclaims it atomically`)
|
|
152
|
-
: check('state-lock', 'warn', `state lock held by pid ${lock.record.pid ?? 'unknown'}; retry after it finishes`),
|
|
153
|
-
)
|
|
154
|
-
const lockDir = path.join(tower.commonDir, 'dsh-tower', 'tower.lock')
|
|
155
|
-
try {
|
|
156
|
-
const lock = await inspectWorkspaceLock(tower.commonDir, {
|
|
157
|
-
staleMs: Number(config.mode?.workspaceStaleMs) || 300000,
|
|
158
|
-
})
|
|
159
|
-
checks.push(lock.held ? check('workspace-lock', lock.stale ? 'warn' : 'pass', `held by ${lock.holder?.sessionId ?? 'unknown'}`) : check('workspace-lock', 'pass', 'free'))
|
|
160
|
-
} catch {
|
|
161
|
-
checks.push(check('workspace-lock', 'warn', `lock file ${lockDir} could not be inspected`))
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const live = (state.roster ?? []).filter((entry) => !['retired', 'failed', 'interrupted', 'missing', 'stale', 'inactive'].includes(entry.state))
|
|
165
|
-
checks.push(
|
|
166
|
-
live.length > 0
|
|
167
|
-
? check('roster', 'pass', `${live.length} live roster entr${live.length === 1 ? 'y' : 'ies'}`)
|
|
168
|
-
: check('roster', 'pass', 'no live workers'),
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
let missingWorktrees = 0
|
|
172
|
-
let dirtyWorktrees = 0
|
|
173
|
-
let removedWorktrees = 0
|
|
174
|
-
for (const mission of state.missions ?? []) {
|
|
175
|
-
if (!mission.worktree) continue
|
|
176
|
-
if (mission.worktreeRemovedAt) {
|
|
177
|
-
removedWorktrees += 1
|
|
178
|
-
continue
|
|
179
|
-
}
|
|
180
|
-
try {
|
|
181
|
-
await tower.assertWorktree(mission.worktree)
|
|
182
|
-
if (await exists(mission.worktree.path)) {
|
|
183
|
-
const dirty = (await git(mission.worktree.path, ['status', '--porcelain'])).trim()
|
|
184
|
-
if (dirty) dirtyWorktrees += 1
|
|
185
|
-
} else {
|
|
186
|
-
missingWorktrees += 1
|
|
187
|
-
}
|
|
188
|
-
} catch {
|
|
189
|
-
missingWorktrees += 1
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
const removedNote = removedWorktrees > 0 ? ` (${removedWorktrees} removed by teardown)` : ''
|
|
193
|
-
checks.push(
|
|
194
|
-
missingWorktrees > 0
|
|
195
|
-
? check('worktrees', 'fail', `${missingWorktrees} mission worktree(s) are missing or invalid${removedNote}`, 'inspect tower_status, restore the worktree/branch, then re-run tower_doctor')
|
|
196
|
-
: dirtyWorktrees > 0
|
|
197
|
-
? check('worktrees', 'warn', `${dirtyWorktrees} worktree(s) have uncommitted changes${removedNote}`)
|
|
198
|
-
: check('worktrees', 'pass', `all registered worktrees are valid${removedNote}`),
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
const testConfigured = state.missions?.some((mission) => mission.testRuns !== undefined)
|
|
202
|
-
checks.push(
|
|
203
|
-
check(
|
|
204
|
-
'tests',
|
|
205
|
-
config.tests?.enabled === false ? 'warn' : 'pass',
|
|
206
|
-
config.tests?.enabled === false ? 'test discovery disabled' : `admission=${config.tests?.admission ?? 'complete'} timeoutMs=${config.tests?.timeoutMs ?? 60000}${testConfigured ? '; receipts present' : ''}`,
|
|
207
|
-
),
|
|
208
|
-
)
|
|
209
|
-
checks.push(check('legacy', 'pass', `${state.legacySnapshots?.length ?? 0} legacy snapshot(s)`))
|
|
210
|
-
checks.push(check('batches', 'pass', `${state.batches?.length ?? 0} batch(es)`))
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
try {
|
|
214
|
-
const { inspectLeases } = await import('./leases.js')
|
|
215
|
-
const leases = await inspectLeases(tower.commonDir)
|
|
216
|
-
checks.push(check('leases', leases.leases?.length ? 'warn' : 'pass', `${leases.leases?.length ?? 0} active lease(s)`))
|
|
217
|
-
} catch (error) {
|
|
218
|
-
const corrupt = error?.code === 'state-corrupt'
|
|
219
|
-
checks.push(
|
|
220
|
-
check(
|
|
221
|
-
'leases',
|
|
222
|
-
corrupt ? 'fail' : 'warn',
|
|
223
|
-
corrupt ? `lease ledger is corrupt: ${String(error?.message ?? error)}` : 'lease module unavailable',
|
|
224
|
-
corrupt ? 'inspect <git-common-dir>/dsh-tower/leases.json and repair or remove it' : undefined,
|
|
225
|
-
),
|
|
226
|
-
)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
const text = await fsp.readFile(path.join(tower.paths.dir, 'lock-holds.jsonl'), 'utf8')
|
|
231
|
-
const samples = text.split('\n').filter(Boolean).slice(-200).map((line) => JSON.parse(line).ms).filter((ms) => Number.isFinite(ms)).sort((a, b) => a - b)
|
|
232
|
-
if (samples.length === 0) {
|
|
233
|
-
checks.push(check('lock-holds', 'pass', 'no lock-hold samples yet'))
|
|
234
|
-
} else {
|
|
235
|
-
const p = (q) => samples[Math.min(samples.length - 1, Math.floor(q * samples.length))]
|
|
236
|
-
const p50 = p(0.5)
|
|
237
|
-
const p95 = p(0.95)
|
|
238
|
-
checks.push(
|
|
239
|
-
check(
|
|
240
|
-
'lock-holds',
|
|
241
|
-
p95 > 1000 ? 'warn' : 'pass',
|
|
242
|
-
`${samples.length} sample(s): p50 ${p50}ms, p95 ${p95}ms`,
|
|
243
|
-
p95 > 1000 ? 'a mutation is holding the state lock across slow work; inspect the activity/lock-holds log' : undefined,
|
|
244
|
-
),
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
} catch {
|
|
248
|
-
checks.push(check('lock-holds', 'pass', 'no lock-hold samples yet'))
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const failed = checks.filter((item) => item.status === 'fail').length
|
|
252
|
-
return {
|
|
253
|
-
ok: failed === 0,
|
|
254
|
-
checks,
|
|
255
|
-
environmentContract: {
|
|
256
|
-
platform: process.platform,
|
|
257
|
-
gitVersion: version,
|
|
258
|
-
repoRoot: tower.root,
|
|
259
|
-
commonDir: tower.commonDir,
|
|
260
|
-
stateDir: tower.paths.dir,
|
|
261
|
-
gitTimeoutMs: Number(tower.config.git?.timeoutMs) || 15000,
|
|
262
|
-
mergeTimeoutMs: Number(tower.config.merge?.timeoutMs) || 900000,
|
|
263
|
-
testsAdmission: tower.config.tests?.admission ?? 'off',
|
|
264
|
-
hostVersion: detectedHost,
|
|
265
|
-
hostCompatibility: hostAssessment.verdict,
|
|
266
|
-
hostCompatibilityDetail: hostAssessment.detail,
|
|
267
|
-
},
|
|
268
|
-
mode: agent ? 'ready' : 'unknown',
|
|
269
|
-
}
|
|
270
|
-
}
|