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/{lib → host}/doctor.js
RENAMED
|
@@ -2,19 +2,33 @@
|
|
|
2
2
|
import { execFile } from '../adapters/vcs-git.js'
|
|
3
3
|
import { promisify } from 'node:util'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
5
6
|
import { fsp } from '../adapters/store-fs.js'
|
|
6
|
-
import { git } from '
|
|
7
|
-
import { readStateLock } from '
|
|
8
|
-
import {
|
|
7
|
+
import { git } from '../adapters/git.js'
|
|
8
|
+
import { readStateLock, readGitLock, gitLockView } from '../adapters/store.js'
|
|
9
|
+
import { lockTiming } from '../adapters/persistence.js'
|
|
10
|
+
import { inspectArchive } from '../adapters/archive.js'
|
|
9
11
|
import { inspectStateVersion } from '../domain/state.js'
|
|
10
12
|
import { SCHEMA_VERSION } from '../domain/mission.js'
|
|
11
13
|
import { DEFAULT_MAX_REPORT_BYTES, completionReportBytes } from '../domain/missions/completion.js'
|
|
12
14
|
import { comparisonMode } from '../domain/scope.js'
|
|
15
|
+
import { configSurface } from '../domain/config/schema.js'
|
|
16
|
+
import { parseBaseline, surfaceLine, SURFACE_BASELINE_FILE } from '../domain/surface.js'
|
|
13
17
|
import { assessHostVersion, detectHostVersion, readCompatibility } from './compatibility.js'
|
|
14
18
|
import { profileConfig } from './config.js'
|
|
15
|
-
import { inspectWorkspaceLock } from '
|
|
19
|
+
import { inspectWorkspaceLock } from '../adapters/workspaceLock.js'
|
|
16
20
|
|
|
17
21
|
const execFileAsync = promisify(execFile)
|
|
22
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
23
|
+
|
|
24
|
+
/** Read the committed concept-budget baseline read-only; null when absent. */
|
|
25
|
+
async function readSurfaceBaseline() {
|
|
26
|
+
try {
|
|
27
|
+
return parseBaseline(await fsp.readFile(path.join(PACKAGE_ROOT, SURFACE_BASELINE_FILE), 'utf8'))
|
|
28
|
+
} catch {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
18
32
|
|
|
19
33
|
async function exists(target) {
|
|
20
34
|
try {
|
|
@@ -38,6 +52,12 @@ function check(id, status, detail, action) {
|
|
|
38
52
|
return { id, status, detail, ...(action ? { action } : {}) }
|
|
39
53
|
}
|
|
40
54
|
|
|
55
|
+
/** Render a lock age the same way the state-lock checker does. */
|
|
56
|
+
function durationText(ms) {
|
|
57
|
+
if (!Number.isFinite(ms)) return 'an unknown duration'
|
|
58
|
+
return `${Math.max(0, Math.round(ms / 1000))} s`
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
/** Read the on-disk state version without loading, migrating, or locking. */
|
|
42
62
|
async function inspectStateSchema(paths) {
|
|
43
63
|
try {
|
|
@@ -116,9 +136,10 @@ function stateSchemaReport(info) {
|
|
|
116
136
|
}
|
|
117
137
|
|
|
118
138
|
/** Run every read-only diagnostic; never writes state or takes the state lock. */
|
|
119
|
-
export async function runDoctor({ tower, ctx, agent, hostVersion, compatibilityFile, loaderRows }) {
|
|
139
|
+
export async function runDoctor({ tower, ctx, agent, hostVersion, compatibilityFile, loaderRows, surface }) {
|
|
120
140
|
const checks = []
|
|
121
141
|
let reportsReport = null
|
|
142
|
+
let stateLockReport = null
|
|
122
143
|
const version = await gitVersion()
|
|
123
144
|
checks.push(version ? check('git', 'pass', version) : check('git', 'fail', 'git is not available', 'install Git and retry'))
|
|
124
145
|
|
|
@@ -167,6 +188,19 @@ export async function runDoctor({ tower, ctx, agent, hostVersion, compatibilityF
|
|
|
167
188
|
}
|
|
168
189
|
|
|
169
190
|
checks.push(check('repository', 'pass', `${tower.root} (common ${tower.commonDir})`))
|
|
191
|
+
// The repo-scoped git mutex can legally be held for a whole merge (15 min by
|
|
192
|
+
// default). Report the holder so a stalled tower has a visible cause (#93).
|
|
193
|
+
const gitLock = gitLockView(await readGitLock(tower.paths))
|
|
194
|
+
checks.push(
|
|
195
|
+
gitLock.held
|
|
196
|
+
? check(
|
|
197
|
+
'git-lock',
|
|
198
|
+
'warn',
|
|
199
|
+
`held by pid ${gitLock.pid ?? 'unknown'}${gitLock.phase ? ` (${gitLock.phase})` : ''} for ${durationText(gitLock.heldMs)}; the repo-scoped git mutex is busy`,
|
|
200
|
+
'wait for the git mutation to finish; tower_status reports the same holder',
|
|
201
|
+
)
|
|
202
|
+
: check('git-lock', 'pass', 'free'),
|
|
203
|
+
)
|
|
170
204
|
const schemaInfo = await inspectStateSchema(tower.paths)
|
|
171
205
|
let state = null
|
|
172
206
|
try {
|
|
@@ -226,6 +260,32 @@ export async function runDoctor({ tower, ctx, agent, hostVersion, compatibilityF
|
|
|
226
260
|
'optional: add <git-common-dir>/dsh-tower/config.json',
|
|
227
261
|
),
|
|
228
262
|
)
|
|
263
|
+
// #84: report the concept-budget surface instead of implying it. The counts
|
|
264
|
+
// are the live schema/tool surface; the date is the committed baseline, so a
|
|
265
|
+
// reader sees both the number and the decision it was frozen at.
|
|
266
|
+
const configCounts = configSurface()
|
|
267
|
+
const baseline = await readSurfaceBaseline()
|
|
268
|
+
const toolCount = Number.isInteger(surface?.tools) ? surface.tools : null
|
|
269
|
+
const surfaceCounts = {
|
|
270
|
+
configKeys: configCounts.count,
|
|
271
|
+
sections: configCounts.sections.length,
|
|
272
|
+
tools: toolCount,
|
|
273
|
+
}
|
|
274
|
+
const surfaceReport = {
|
|
275
|
+
...surfaceCounts,
|
|
276
|
+
baselineDate: baseline?.date ?? null,
|
|
277
|
+
line: surfaceLine(surfaceCounts),
|
|
278
|
+
}
|
|
279
|
+
checks.push(
|
|
280
|
+
toolCount === null
|
|
281
|
+
? check('surface', 'warn', `${surfaceReport.line} (tool count unavailable; pass the registered surface)`)
|
|
282
|
+
: check(
|
|
283
|
+
'surface',
|
|
284
|
+
'pass',
|
|
285
|
+
`${surfaceReport.line}${baseline?.date ? ` (baseline ${baseline.date})` : ''}`,
|
|
286
|
+
baseline?.date ? undefined : `commit ${SURFACE_BASELINE_FILE} so growth is compared against a decision`,
|
|
287
|
+
),
|
|
288
|
+
)
|
|
229
289
|
const presets = ctx.get?.('agentPresets')
|
|
230
290
|
if (presets && typeof presets.list === 'function') {
|
|
231
291
|
try {
|
|
@@ -251,12 +311,45 @@ export async function runDoctor({ tower, ctx, agent, hostVersion, compatibilityF
|
|
|
251
311
|
|
|
252
312
|
if (state) {
|
|
253
313
|
const lock = await readStateLock(tower.paths)
|
|
314
|
+
const timing = lockTiming(lock.record)
|
|
315
|
+
stateLockReport = {
|
|
316
|
+
held: Boolean(lock.record),
|
|
317
|
+
pid: lock.record?.pid ?? null,
|
|
318
|
+
startedAt: lock.record?.startedAt ?? null,
|
|
319
|
+
at: lock.record?.at ?? null,
|
|
320
|
+
alive: lock.alive,
|
|
321
|
+
ageMs: timing.ageMs,
|
|
322
|
+
heartbeatAgeMs: timing.heartbeatAgeMs,
|
|
323
|
+
staleAfterMs: timing.staleAfterMs,
|
|
324
|
+
stale: timing.stale,
|
|
325
|
+
}
|
|
326
|
+
// The PID alone cannot distinguish a live writer from a recycled PID
|
|
327
|
+
// (#92); report the heartbeat age so a stuck tower is diagnosable without
|
|
328
|
+
// reading the file.
|
|
329
|
+
const heartbeatText =
|
|
330
|
+
timing.heartbeatAgeMs === null
|
|
331
|
+
? 'no heartbeat timestamp'
|
|
332
|
+
: `last heartbeat ${timing.heartbeatAgeMs}ms ago (stale after ${timing.staleAfterMs}ms)`
|
|
254
333
|
checks.push(
|
|
255
334
|
!lock.record
|
|
256
335
|
? check('state-lock', 'pass', 'no state lock file')
|
|
257
336
|
: lock.alive === false
|
|
258
|
-
? check(
|
|
259
|
-
|
|
337
|
+
? check(
|
|
338
|
+
'state-lock',
|
|
339
|
+
'warn',
|
|
340
|
+
`stale state lock (pid ${lock.record.pid ?? 'unknown'} is gone; ${heartbeatText}); the next mutation reclaims it atomically`,
|
|
341
|
+
)
|
|
342
|
+
: timing.stale
|
|
343
|
+
? check(
|
|
344
|
+
'state-lock',
|
|
345
|
+
'warn',
|
|
346
|
+
`state lock pid ${lock.record.pid ?? 'unknown'} is alive but silent (${heartbeatText}); the next mutation reclaims it after byte verification, or tower_recover clear-stale-lock force:true moves it to a backup`,
|
|
347
|
+
)
|
|
348
|
+
: check(
|
|
349
|
+
'state-lock',
|
|
350
|
+
'warn',
|
|
351
|
+
`state lock held by pid ${lock.record.pid ?? 'unknown'}; ${heartbeatText}; retry after it finishes`,
|
|
352
|
+
),
|
|
260
353
|
)
|
|
261
354
|
const lockDir = path.join(tower.commonDir, 'dsh-tower', 'tower.lock')
|
|
262
355
|
try {
|
|
@@ -342,7 +435,7 @@ export async function runDoctor({ tower, ctx, agent, hostVersion, compatibilityF
|
|
|
342
435
|
}
|
|
343
436
|
|
|
344
437
|
try {
|
|
345
|
-
const { inspectLeases } = await import('
|
|
438
|
+
const { inspectLeases } = await import('../adapters/leases.js')
|
|
346
439
|
const leases = await inspectLeases(tower.commonDir)
|
|
347
440
|
checks.push(check('leases', leases.leases?.length ? 'warn' : 'pass', `${leases.leases?.length ?? 0} active lease(s)`))
|
|
348
441
|
} catch (error) {
|
|
@@ -359,7 +452,18 @@ export async function runDoctor({ tower, ctx, agent, hostVersion, compatibilityF
|
|
|
359
452
|
|
|
360
453
|
try {
|
|
361
454
|
const text = await fsp.readFile(path.join(tower.paths.dir, 'lock-holds.jsonl'), 'utf8')
|
|
362
|
-
|
|
455
|
+
// These percentiles are about the state lock. Git-mutex holds are logged
|
|
456
|
+
// too (kind: 'git' / the pre-#93 phase: 'git' form) and can legitimately
|
|
457
|
+
// span a merge, so they must not be counted as slow state mutations.
|
|
458
|
+
const samples = text
|
|
459
|
+
.split('\n')
|
|
460
|
+
.filter(Boolean)
|
|
461
|
+
.slice(-200)
|
|
462
|
+
.map((line) => JSON.parse(line))
|
|
463
|
+
.filter((hold) => hold.kind !== 'git' && hold.phase !== 'git')
|
|
464
|
+
.map((hold) => hold.ms)
|
|
465
|
+
.filter((ms) => Number.isFinite(ms))
|
|
466
|
+
.sort((a, b) => a - b)
|
|
363
467
|
if (samples.length === 0) {
|
|
364
468
|
checks.push(check('lock-holds', 'pass', 'no lock-hold samples yet'))
|
|
365
469
|
} else {
|
|
@@ -410,9 +514,12 @@ export async function runDoctor({ tower, ctx, agent, hostVersion, compatibilityF
|
|
|
410
514
|
return {
|
|
411
515
|
ok: failed === 0,
|
|
412
516
|
checks,
|
|
517
|
+
stateLock: stateLockReport,
|
|
518
|
+
gitLock,
|
|
413
519
|
stateSchema: schemaReport.stateSchema,
|
|
414
520
|
// reports (from #89) and archive (from #73) are both kept
|
|
415
521
|
reports: reportsReport,
|
|
522
|
+
surface: surfaceReport,
|
|
416
523
|
archive: {
|
|
417
524
|
missions: state ? state.missions.length : null,
|
|
418
525
|
archivedMissions: archiveInventory.missions,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { fs } from '../adapters/store-fs.js'
|
|
7
7
|
import os from 'node:os'
|
|
8
8
|
import path from 'node:path'
|
|
9
|
-
import { acquireLockSync, readJsonSync, writeJsonAtomicSync } from '
|
|
9
|
+
import { acquireLockSync, readJsonSync, writeJsonAtomicSync } from '../adapters/persistence.js'
|
|
10
10
|
|
|
11
11
|
const DEFAULT_MAX_ENTRIES = 200
|
|
12
12
|
|
|
@@ -14,8 +14,15 @@ import {
|
|
|
14
14
|
isTowerRole,
|
|
15
15
|
isUnknownRole,
|
|
16
16
|
} from '../domain/vocabulary.js'
|
|
17
|
-
import { git, resolveRepo, hasAnyCommit } from '
|
|
17
|
+
import { git, resolveRepo, hasAnyCommit } from '../adapters/git.js'
|
|
18
18
|
import { runtimeConfig } from './config.js'
|
|
19
|
+
import { runDoctor } from './doctor.js'
|
|
20
|
+
import { createHmrBridge } from './hmr.js'
|
|
21
|
+
import { assertPresetsAvailable } from './presets.js'
|
|
22
|
+
import { readStateLock } from '../adapters/store.js'
|
|
23
|
+
import { lockTiming } from '../adapters/persistence.js'
|
|
24
|
+
import { acquireLease, releaseLease, inspectLeases } from '../adapters/leases.js'
|
|
25
|
+
import { importLegacySnapshot, validateLegacyMap } from '../adapters/legacy.js'
|
|
19
26
|
import { registerDoctorTool } from '../tools/doctor.js'
|
|
20
27
|
import { registerRecoverTool } from '../tools/recover.js'
|
|
21
28
|
import { registerTestTool } from '../tools/test.js'
|
|
@@ -43,10 +50,12 @@ import { registerSpawnTool } from '../tools/spawn.js'
|
|
|
43
50
|
import { registerCompleteTool } from '../tools/complete.js'
|
|
44
51
|
import { registerFindingTool } from '../tools/finding.js'
|
|
45
52
|
import { registerFindingsTool } from '../tools/findings.js'
|
|
46
|
-
import { readDiscoveryConfig, normalizeDiscovery, runDiscovery } from '
|
|
53
|
+
import { readDiscoveryConfig, normalizeDiscovery, runDiscovery } from '../adapters/tests.js'
|
|
47
54
|
import { summarize as summarizeBatch } from '../domain/batch.js'
|
|
55
|
+
import { assertPorts } from '../ports/index.js'
|
|
48
56
|
import path from 'node:path'
|
|
49
57
|
import { fsp } from '../adapters/store-fs.js'
|
|
58
|
+
import { createDefaultDeps } from '../adapters/default.js'
|
|
50
59
|
import crypto from 'node:crypto'
|
|
51
60
|
|
|
52
61
|
// Mutable composition state is grouped in one const object: the registration
|
|
@@ -54,6 +63,41 @@ import crypto from 'node:crypto'
|
|
|
54
63
|
const runtimeState = {
|
|
55
64
|
workspaceHook: null,
|
|
56
65
|
hmrBridge: null,
|
|
66
|
+
// #69: the host entry builds the adapter set once and passes it in. The lazy
|
|
67
|
+
// fallback is for tests/embedders that call registerTowerTools() directly;
|
|
68
|
+
// it is still built once per process.
|
|
69
|
+
deps: null,
|
|
70
|
+
// #84: the defined tool names are recorded before the exposure switches run,
|
|
71
|
+
// so the concept budget measures the full surface rather than one profile's
|
|
72
|
+
// subset. The number is reported by tower_doctor and compared with the
|
|
73
|
+
// committed scripts/surface-baseline.json.
|
|
74
|
+
definedTools: new Set(),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The full defined tool surface (names, sorted), independent of exposure flags. */
|
|
78
|
+
export function definedToolNames() {
|
|
79
|
+
return [...runtimeState.definedTools].sort()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The number of tools defined by this build, used by the concept budget (#84). */
|
|
83
|
+
export function definedToolCount() {
|
|
84
|
+
return runtimeState.definedTools.size
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The injected port set for the app layer (built once per process). */
|
|
88
|
+
function towerDeps() {
|
|
89
|
+
if (!runtimeState.deps) runtimeState.deps = createDefaultDeps()
|
|
90
|
+
return runtimeState.deps
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Compose the app Tower with the injected ports (never a bare adapter import). */
|
|
94
|
+
function openTower(repoRoot) {
|
|
95
|
+
return Tower.open(repoRoot, towerDeps())
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Build a Tower with an explicit config for the read-only doctor path. */
|
|
99
|
+
function createTower(repo, config) {
|
|
100
|
+
return new Tower(repo, config, towerDeps())
|
|
57
101
|
}
|
|
58
102
|
|
|
59
103
|
function normalizeToolArgs(args, aliases = {}) {
|
|
@@ -324,6 +368,9 @@ function plainSchema(value) {
|
|
|
324
368
|
*/
|
|
325
369
|
function define(ctx, definition) {
|
|
326
370
|
const { aliases, requiresMissionId, board, execute, ...rest } = definition
|
|
371
|
+
// Record the definition before the exposure gate: the concept budget counts
|
|
372
|
+
// the tools this build can register, not only the ones a profile exposes.
|
|
373
|
+
runtimeState.definedTools.add(rest.name)
|
|
327
374
|
const toolsConfig = runtimeConfig().tools ?? {}
|
|
328
375
|
const flag = OPERATIONAL_TOOLS.get(rest.name)
|
|
329
376
|
if (flag && (toolsConfig.exposeOperational === false || toolsConfig[flag] === false)) return
|
|
@@ -364,7 +411,7 @@ function define(ctx, definition) {
|
|
|
364
411
|
}
|
|
365
412
|
if (board === true && typeof normalized.repoRoot === 'string') {
|
|
366
413
|
try {
|
|
367
|
-
const opened = await
|
|
414
|
+
const opened = await openTower(normalized.repoRoot)
|
|
368
415
|
await opened.writeBoard()
|
|
369
416
|
} catch {
|
|
370
417
|
// board output is best effort
|
|
@@ -451,6 +498,24 @@ async function observeActivity(ctx, tower) {
|
|
|
451
498
|
export function registerTowerTools(ctx, options = {}) {
|
|
452
499
|
runtimeState.workspaceHook = options.workspace ?? null
|
|
453
500
|
runtimeState.hmrBridge = options.hmrBridge ?? null
|
|
501
|
+
// #69: the host entry (index.js) is the composition root. When it supplies the
|
|
502
|
+
// port set, adopt it — validated once — instead of constructing a second set
|
|
503
|
+
// here. Tests/embedders that omit deps fall back to one lazily built set.
|
|
504
|
+
// #75: the event port is bound here to the live Cordis emitter; emitting is
|
|
505
|
+
// best effort so a listener can never fail the tool that produced the fact.
|
|
506
|
+
const baseDeps = options.deps ? assertPorts(options.deps) : (runtimeState.deps ?? createDefaultDeps())
|
|
507
|
+
runtimeState.deps = {
|
|
508
|
+
...baseDeps,
|
|
509
|
+
events: {
|
|
510
|
+
emit(name, payload) {
|
|
511
|
+
try {
|
|
512
|
+
ctx.emit?.(name, payload)
|
|
513
|
+
} catch {
|
|
514
|
+
// events are an optional side channel; never fail a tool for them
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
}
|
|
454
519
|
// A lightweight roster watchdog: the authoritative activity comes from the
|
|
455
520
|
// subagents catalog; a missing observation after spawn.staleMs is marked
|
|
456
521
|
// stale (never silently treated as stopped) and blocks merge via the gate.
|
|
@@ -460,7 +525,7 @@ export function registerTowerTools(ctx, options = {}) {
|
|
|
460
525
|
try {
|
|
461
526
|
if (!ctx.get('subagents')) return
|
|
462
527
|
for (const repoRoot of [...WATCHED_REPOS]) {
|
|
463
|
-
const tower = await
|
|
528
|
+
const tower = await openTower(repoRoot)
|
|
464
529
|
const view = await tower.rosterView()
|
|
465
530
|
const live = view.roster.filter((entry) => !ROSTER_INACTIVE_STATES.includes(entry.state))
|
|
466
531
|
if (live.length === 0) { WATCHED_REPOS.delete(repoRoot); continue }
|
|
@@ -481,59 +546,69 @@ export function registerTowerTools(ctx, options = {}) {
|
|
|
481
546
|
}, watchMs)
|
|
482
547
|
timer.unref?.()
|
|
483
548
|
ctx.effect(() => () => clearInterval(timer))
|
|
484
|
-
registerInitTool(ctx, { define, actorFor, requireTowerActor, workspaceHook: runtimeState.workspaceHook, ensureRepository })
|
|
549
|
+
registerInitTool(ctx, { openTower, define, actorFor, requireTowerActor, workspaceHook: runtimeState.workspaceHook, ensureRepository, resolveRepo, runtimeConfig })
|
|
485
550
|
|
|
486
|
-
registerPlanTool(ctx, { define, actorFor, requireTowerActor })
|
|
551
|
+
registerPlanTool(ctx, { openTower, define, actorFor, requireTowerActor, assertPresetsAvailable })
|
|
487
552
|
|
|
488
|
-
registerMissionTool(ctx, { define, actorFor, requireKnownActor })
|
|
553
|
+
registerMissionTool(ctx, { openTower, define, actorFor, requireKnownActor })
|
|
489
554
|
|
|
490
|
-
registerStatusTool(ctx, { define, workspaceHook: runtimeState.workspaceHook, spawnBackoffView, observeActivity })
|
|
555
|
+
registerStatusTool(ctx, { openTower, define, workspaceHook: runtimeState.workspaceHook, spawnBackoffView, observeActivity })
|
|
491
556
|
|
|
492
|
-
registerSpawnTool(ctx, { define, actorFor, requireTowerActor, enqueueSpawn, observeActivity, spawnBackoffState, spawnBackoffView, recordSpawnThrottle, recordSpawnSuccess, runMissionDiscovery, textOf })
|
|
557
|
+
registerSpawnTool(ctx, { openTower, define, actorFor, requireTowerActor, enqueueSpawn, observeActivity, spawnBackoffState, spawnBackoffView, recordSpawnThrottle, recordSpawnSuccess, runMissionDiscovery, textOf, inspectLeases })
|
|
493
558
|
|
|
494
|
-
registerRosterTool(ctx, { define, workspaceHook: runtimeState.workspaceHook, observeActivity, actorFor, requireTowerActor, interruptRosterAgent })
|
|
559
|
+
registerRosterTool(ctx, { openTower, define, workspaceHook: runtimeState.workspaceHook, observeActivity, actorFor, requireTowerActor, interruptRosterAgent })
|
|
495
560
|
|
|
496
|
-
registerInterruptTool(ctx, { define, actorFor, requireTowerActor, interruptRosterAgent })
|
|
561
|
+
registerInterruptTool(ctx, { openTower, define, actorFor, requireTowerActor, interruptRosterAgent })
|
|
497
562
|
|
|
498
|
-
registerDiffTool(ctx, { define, actorFor, requireTowerActor })
|
|
563
|
+
registerDiffTool(ctx, { openTower, define, actorFor, requireTowerActor })
|
|
499
564
|
|
|
500
|
-
registerAdoptTool(ctx, { define, actorFor, requireTowerActor })
|
|
565
|
+
registerAdoptTool(ctx, { openTower, define, actorFor, requireTowerActor })
|
|
501
566
|
|
|
502
|
-
registerSendTool(ctx, { define, actorFor, checkSendRate })
|
|
567
|
+
registerSendTool(ctx, { openTower, define, actorFor, checkSendRate })
|
|
503
568
|
|
|
504
|
-
registerInboxTool(ctx, { define })
|
|
569
|
+
registerInboxTool(ctx, { openTower, define })
|
|
505
570
|
|
|
506
|
-
registerCompleteTool(ctx, { define, runMissionDiscovery })
|
|
571
|
+
registerCompleteTool(ctx, { openTower, define, runMissionDiscovery })
|
|
507
572
|
|
|
508
|
-
registerFindingTool(ctx, { define, actorFor, requireKnownActor })
|
|
573
|
+
registerFindingTool(ctx, { openTower, define, actorFor, requireKnownActor })
|
|
509
574
|
|
|
510
|
-
registerFindingsTool(ctx, { define, actorFor, requireKnownActor })
|
|
575
|
+
registerFindingsTool(ctx, { openTower, define, actorFor, requireKnownActor })
|
|
511
576
|
|
|
512
|
-
registerReviewTool(ctx, { define, actorFor })
|
|
577
|
+
registerReviewTool(ctx, { openTower, define, actorFor })
|
|
513
578
|
|
|
514
|
-
registerPreflightTool(ctx, { define, observeActivity })
|
|
579
|
+
registerPreflightTool(ctx, { openTower, define, observeActivity })
|
|
515
580
|
|
|
516
|
-
registerMergeTool(ctx, { define, actorFor, requireTowerActor, observeActivity })
|
|
581
|
+
registerMergeTool(ctx, { openTower, define, actorFor, requireTowerActor, observeActivity })
|
|
517
582
|
|
|
518
|
-
registerRebaseTool(ctx, { define, actorFor, requireTowerActor, observeActivity })
|
|
583
|
+
registerRebaseTool(ctx, { openTower, define, actorFor, requireTowerActor, observeActivity })
|
|
519
584
|
|
|
520
|
-
registerTeardownTool(ctx, { define, actorFor, requireTowerActor, observeActivity })
|
|
585
|
+
registerTeardownTool(ctx, { openTower, define, actorFor, requireTowerActor, observeActivity })
|
|
521
586
|
|
|
522
|
-
registerDoctorTool(ctx,
|
|
587
|
+
registerDoctorTool(ctx, {
|
|
588
|
+
openTower,
|
|
589
|
+
define,
|
|
590
|
+
createTower,
|
|
591
|
+
resolveRepo,
|
|
592
|
+
runtimeConfig,
|
|
593
|
+
runDoctor,
|
|
594
|
+
// #84: the doctor reports the live surface; the count is the full defined
|
|
595
|
+
// tool surface, independent of which operational switches this profile uses.
|
|
596
|
+
surface: () => ({ tools: definedToolCount() }),
|
|
597
|
+
})
|
|
523
598
|
|
|
524
|
-
registerRecoverTool(ctx, { define, actorFor, requireTowerActor })
|
|
599
|
+
registerRecoverTool(ctx, { openTower, define, actorFor, requireTowerActor, readStateLock, lockTiming })
|
|
525
600
|
|
|
526
|
-
registerTestTool(ctx, { define, actorFor, requireKnownActor, runMissionDiscovery })
|
|
601
|
+
registerTestTool(ctx, { openTower, define, actorFor, requireKnownActor, runMissionDiscovery })
|
|
527
602
|
|
|
528
|
-
registerBatchTool(ctx, { define, actorFor, requireTowerActor, summarizeBatch, verifyBatchReceiptFiles })
|
|
603
|
+
registerBatchTool(ctx, { openTower, define, actorFor, requireTowerActor, summarizeBatch, verifyBatchReceiptFiles })
|
|
529
604
|
|
|
530
|
-
registerResourceTool(ctx, { define, actorFor, requireTowerActor })
|
|
605
|
+
registerResourceTool(ctx, { openTower, define, actorFor, requireTowerActor, acquireLease, releaseLease, inspectLeases })
|
|
531
606
|
|
|
532
|
-
registerImportLegacyTool(ctx, { define, actorFor, requireTowerActor })
|
|
607
|
+
registerImportLegacyTool(ctx, { openTower, define, actorFor, requireTowerActor, importLegacySnapshot })
|
|
533
608
|
|
|
534
|
-
registerLegacyMapTool(ctx, { define, actorFor, requireTowerActor })
|
|
609
|
+
registerLegacyMapTool(ctx, { openTower, define, actorFor, requireTowerActor, validateLegacyMap })
|
|
535
610
|
|
|
536
|
-
registerReloadTool(ctx, { define, actorFor, requireTowerActor, sharedHmrBridge: runtimeState.hmrBridge })
|
|
611
|
+
registerReloadTool(ctx, { openTower, define, actorFor, requireTowerActor, sharedHmrBridge: runtimeState.hmrBridge, runtimeConfig, createHmrBridge })
|
|
537
612
|
}
|
|
538
613
|
|
|
539
614
|
export const __testSpawnBackoff = {
|
package/host/tower.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Default-deps binding of the Tower app service (#69).
|
|
2
|
+
//
|
|
3
|
+
// The composition root (index.js / host/tool-registry.js) builds one deps object
|
|
4
|
+
// and injects it into the app layer. This module is the convenience binding for
|
|
5
|
+
// embedders and tests that want the process-default adapters; it is the only
|
|
6
|
+
// host/ module besides tool-registry.js that knows the default adapter set.
|
|
7
|
+
//
|
|
8
|
+
// Object layout supplied here is memoised in adapters/default.js, so every
|
|
9
|
+
// caller shares the same adapter instances.
|
|
10
|
+
import { Tower as InjectedTower } from '../app/tower.js'
|
|
11
|
+
import { defaultDeps } from '../adapters/default.js'
|
|
12
|
+
|
|
13
|
+
export class Tower extends InjectedTower {
|
|
14
|
+
/**
|
|
15
|
+
* @param {{ root: string, commonDir: string, branch?: string | null }} repo
|
|
16
|
+
* @param {import('../types/index.js').TowerConfig} [config]
|
|
17
|
+
* @param {Record<string, any>} [deps]
|
|
18
|
+
*/
|
|
19
|
+
constructor(repo, config, deps = defaultDeps) {
|
|
20
|
+
super(repo, config, deps)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} repoRoot
|
|
25
|
+
* @param {Record<string, any>} [deps]
|
|
26
|
+
*/
|
|
27
|
+
static async open(repoRoot, deps = defaultDeps) {
|
|
28
|
+
const repo = await deps.vcs.resolveRepo(repoRoot)
|
|
29
|
+
const config = await deps.config.resolveRepoConfig(repo)
|
|
30
|
+
return new Tower(repo, config, deps)
|
|
31
|
+
}
|
|
32
|
+
}
|
package/index.js
CHANGED
|
@@ -12,21 +12,22 @@
|
|
|
12
12
|
// plugin from disk when the HMR service is present (dev/linked installs).
|
|
13
13
|
import fs from 'node:fs'
|
|
14
14
|
import { randomUUID } from 'node:crypto'
|
|
15
|
-
import { registerTowerTools } from './
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
15
|
+
import { registerTowerTools } from './host/tool-registry.js'
|
|
16
|
+
import { createDefaultDeps } from './adapters/default.js'
|
|
17
|
+
import { loadModes, saveModes, storePath } from './host/modeStore.js'
|
|
18
|
+
import { ModeState } from './host/modeState.js'
|
|
19
|
+
import { Config as ConfigSchema, DEFAULT_DENY_TOOLS, configureRuntime, runtimeConfig } from './host/config.js'
|
|
19
20
|
import { TowerToolError } from './domain/errors.js'
|
|
20
|
-
import { resolveRepo } from './
|
|
21
|
+
import { resolveRepo } from './adapters/git.js'
|
|
21
22
|
import {
|
|
22
23
|
acquireWorkspaceLock,
|
|
23
24
|
inspectWorkspaceLock,
|
|
24
25
|
releaseWorkspaceLock,
|
|
25
26
|
touchWorkspaceLock,
|
|
26
27
|
workspaceLockToken,
|
|
27
|
-
} from './
|
|
28
|
-
import { createHmrBridge } from './
|
|
29
|
-
import { assertPresetsAvailable } from './
|
|
28
|
+
} from './adapters/workspaceLock.js'
|
|
29
|
+
import { createHmrBridge } from './host/hmr.js'
|
|
30
|
+
import { assertPresetsAvailable } from './host/presets.js'
|
|
30
31
|
|
|
31
32
|
export const name = 'dsh-tower'
|
|
32
33
|
export const inject = ['tools', 'systemPrompt']
|
|
@@ -343,7 +344,9 @@ export function apply(ctx, rawConfig) {
|
|
|
343
344
|
},
|
|
344
345
|
}
|
|
345
346
|
|
|
346
|
-
|
|
347
|
+
// #69: the host entry is the composition root — the adapters are constructed
|
|
348
|
+
// once here and injected into the tool layer and every Tower instance.
|
|
349
|
+
registerTowerTools(ctx, { workspace, hmrBridge, deps: createDefaultDeps() })
|
|
347
350
|
|
|
348
351
|
// On dispose (including an HMR reload) release advisory workspace locks this
|
|
349
352
|
// process still owns; a reloaded plugin restores the entries from modes.json.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tower",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH-native Tower: mission planning, worktree isolation, review-gated merges, and a human-owned Tower mode.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"reviewer.js",
|
|
42
42
|
"skills.js",
|
|
43
43
|
"skills/",
|
|
44
|
-
"
|
|
44
|
+
"host/*.js",
|
|
45
45
|
"domain/**/*.js",
|
|
46
46
|
"app/*.js",
|
|
47
47
|
"tools/*.js",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"LICENSE",
|
|
59
59
|
"CONTRIBUTING.md",
|
|
60
60
|
"SECURITY.md",
|
|
61
|
+
"PRINCIPLES.md",
|
|
61
62
|
"docs/",
|
|
62
63
|
"ui-bundle/",
|
|
63
64
|
"ports/*.js"
|
|
@@ -71,22 +72,28 @@
|
|
|
71
72
|
"test": "node --test tests/*.test.mjs",
|
|
72
73
|
"test:watch": "node --test --watch tests/*.test.mjs",
|
|
73
74
|
"test:coverage": "node --test --experimental-test-coverage tests/*.test.mjs",
|
|
75
|
+
"test:live": "node tests/live/run.cjs",
|
|
74
76
|
"smoke:mount": "node scripts/mount-smoke.mjs",
|
|
75
77
|
"smoke:fixtures": "node scripts/mount-smoke/fixtures.mjs",
|
|
76
78
|
"release:check": "node scripts/release-check.mjs",
|
|
77
79
|
"audit:issues": "node scripts/issue-audit.mjs",
|
|
80
|
+
"audit:regressions": "node scripts/regression-audit.mjs",
|
|
81
|
+
"surface:check": "node scripts/surface-baseline.mjs",
|
|
78
82
|
"dev:hmr": "node scripts/enable-dev-hmr.mjs",
|
|
79
83
|
"docs:check": "node scripts/check-docs.mjs",
|
|
84
|
+
"docs:check:philosophy": "node scripts/check-philosophy.mjs",
|
|
80
85
|
"docs:generate:errors": "node scripts/generate-error-reference.mjs",
|
|
81
86
|
"docs:check:generated": "node scripts/generate-error-reference.mjs --check",
|
|
82
87
|
"docs:generate:tools": "node scripts/generate-tools-reference.mjs",
|
|
83
88
|
"docs:check:generated:tools": "node scripts/generate-tools-reference.mjs --check",
|
|
84
89
|
"docs:generate:config": "node scripts/generate-config-reference.mjs",
|
|
85
90
|
"docs:check:generated:config": "node scripts/generate-config-reference.mjs --check",
|
|
91
|
+
"docs:generate:events": "node scripts/generate-events-reference.mjs",
|
|
92
|
+
"docs:check:generated:events": "node scripts/generate-events-reference.mjs --check",
|
|
86
93
|
"pack:check": "node scripts/check-pack.mjs",
|
|
87
94
|
"architecture:check": "node scripts/architecture.mjs",
|
|
88
95
|
"types:check": "tsc --noEmit",
|
|
89
|
-
"check": "npm test && npm run docs:check && npm run pack:check && npm run architecture:check && npm run types:check && npm run domain:check",
|
|
96
|
+
"check": "npm test && npm run docs:check && npm run docs:check:philosophy && npm run docs:check:generated && npm run docs:check:generated:tools && npm run docs:check:generated:config && npm run docs:check:generated:events && npm run surface:check && npm run pack:check && npm run architecture:check && npm run types:check && npm run domain:check",
|
|
90
97
|
"domain:metrics": "node scripts/domain-metrics.mjs",
|
|
91
98
|
"domain:check": "node scripts/domain-metrics.mjs --check"
|
|
92
99
|
},
|
package/ports/archive.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Terminal-mission archive port (#69). The readers/writers are store-agnostic:
|
|
2
|
+
// they take the store port and the Tower paths, so the same archive logic runs
|
|
3
|
+
// on the filesystem adapter and on an in-memory store.
|
|
4
|
+
import { assertPort, definePort } from './contract.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} ArchivePort
|
|
8
|
+
* @property {(store: unknown, paths: { dir: string }) => Promise<{ ok: boolean, file: string, missions: any[], error?: string, exists: boolean }>} readIndex
|
|
9
|
+
* @property {(store: unknown, paths: { dir: string }, missionId: string) => Promise<any>} readEntry
|
|
10
|
+
* @property {(store: unknown, paths: { dir: string }, input: { entries: any[], now?: number }) => Promise<any>} writeEntries
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const ARCHIVE_PORT = definePort('ArchivePort', ['readIndex', 'readEntry', 'writeEntries'])
|
|
14
|
+
|
|
15
|
+
/** @param {unknown} impl */
|
|
16
|
+
export function assertArchivePort(impl) {
|
|
17
|
+
return /** @type {ArchivePort} */ (assertPort(impl, ARCHIVE_PORT, 'archive'))
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Case-sensitivity probe port (#69). tower_init probes the checkout filesystem
|
|
2
|
+
// once; a composition without a real filesystem injects its own answer.
|
|
3
|
+
import { assertPort, definePort } from './contract.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} CaseSensitivityPort
|
|
7
|
+
* @property {(root: string) => Promise<boolean | null>} detect
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const CASE_SENSITIVITY_PORT = definePort('CaseSensitivityPort', ['detect'])
|
|
11
|
+
|
|
12
|
+
/** @param {unknown} impl */
|
|
13
|
+
export function assertCaseSensitivityPort(impl) {
|
|
14
|
+
return /** @type {CaseSensitivityPort} */ (assertPort(impl, CASE_SENSITIVITY_PORT, 'caseSensitivity'))
|
|
15
|
+
}
|
package/ports/clock.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
// Clock port (#
|
|
1
|
+
// Clock port (#69).
|
|
2
|
+
import { assertPort, definePort } from './contract.js'
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* @typedef {object} ClockPort
|
|
4
6
|
* @property {() => number} now
|
|
5
7
|
*/
|
|
6
|
-
|
|
8
|
+
|
|
9
|
+
export const CLOCK_PORT = definePort('ClockPort', ['now'])
|
|
10
|
+
|
|
11
|
+
/** @param {unknown} impl */
|
|
12
|
+
export function assertClockPort(impl) {
|
|
13
|
+
return /** @type {ClockPort} */ (assertPort(impl, CLOCK_PORT, 'clock'))
|
|
14
|
+
}
|
package/ports/config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Config port (#69).
|
|
2
|
+
//
|
|
3
|
+
// App use cases read the effective Tower config through this port. They do not
|
|
4
|
+
// import host/config.js, so the process-level config store stays a property of
|
|
5
|
+
// the composition root rather than a module-global dependency of the app.
|
|
6
|
+
import { assertPort, definePort } from './contract.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} ConfigPort
|
|
10
|
+
* @property {() => import('../types/index.js').TowerConfig} runtimeConfig
|
|
11
|
+
* @property {(repo: { root: string, commonDir: string, branch?: string|null }) => Promise<import('../types/index.js').TowerConfig>} resolveRepoConfig
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const CONFIG_PORT = definePort('ConfigPort', ['runtimeConfig', 'resolveRepoConfig'])
|
|
15
|
+
|
|
16
|
+
/** @param {unknown} impl */
|
|
17
|
+
export function assertConfigPort(impl) {
|
|
18
|
+
return /** @type {ConfigPort} */ (assertPort(impl, CONFIG_PORT, 'config'))
|
|
19
|
+
}
|