dsh-tower 2.0.3 → 2.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +141 -0
- package/COMPATIBILITY.md +38 -0
- package/CONTRIBUTING.md +150 -2
- package/README.en.md +59 -16
- package/README.md +57 -16
- package/SECURITY.md +31 -1
- package/adapters/default.js +10 -0
- package/adapters/store-fs.js +6 -0
- package/adapters/vcs-git.js +4 -0
- package/app/batch.js +79 -0
- package/app/board.js +25 -0
- package/app/checkCompletion.js +76 -0
- package/app/enrichedMissions.js +26 -0
- package/app/findings.js +78 -0
- package/app/gate.js +108 -0
- package/app/init.js +249 -0
- package/app/merge.js +567 -0
- package/app/messages.js +66 -0
- package/app/mission.js +35 -0
- package/app/missionContext.js +14 -0
- package/app/plan.js +42 -0
- package/app/provision.js +193 -0
- package/app/read.js +57 -0
- package/app/rebase.js +302 -0
- package/app/recording.js +72 -0
- package/app/review.js +145 -0
- package/app/roster.js +123 -0
- package/app/spawn.js +125 -0
- package/app/teardown.js +184 -0
- package/app/tower.js +524 -0
- package/cordis.patch.yml +61 -53
- package/docs/README.md +33 -1
- package/docs/architecture/overview.md +114 -14
- package/docs/board.md +72 -0
- package/docs/examples/first-mission.md +2 -1
- package/docs/faq.md +11 -7
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/install-upgrade-migration.md +22 -6
- package/docs/guide/quickstart.md +12 -7
- package/docs/guide/release.md +125 -0
- package/docs/guide/scenarios-and-philosophy.md +86 -69
- package/docs/operations/security.md +65 -14
- package/docs/reference/blockers.md +11 -10
- package/docs/reference/config.generated.md +66 -0
- package/docs/reference/config.md +11 -7
- package/docs/reference/errors.generated.md +39 -0
- package/docs/reference/errors.md +10 -2
- package/docs/reference/events.md +7 -4
- package/docs/reference/testing.md +2 -1
- package/docs/reference/tools.generated.md +33 -0
- package/docs/reference/tools.md +8 -7
- package/docs/zh/README.md +4 -3
- package/docs/zh/faq.md +9 -6
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/guide/install-upgrade-migration.md +17 -5
- package/docs/zh/guide/quickstart.md +13 -8
- package/docs/zh/guide/scenarios-and-philosophy.md +55 -50
- package/docs/zh/operations/security.md +43 -7
- package/docs/zh/reference/blockers.md +10 -10
- package/docs/zh/reference/config.md +11 -7
- package/docs/zh/reference/errors.md +4 -1
- package/docs/zh/reference/events.md +6 -3
- package/docs/zh/reference/testing.md +3 -2
- package/docs/zh/reference/tools.md +8 -7
- package/domain/collab/batch.js +112 -0
- package/domain/collab/messages.js +127 -0
- package/domain/collab/roster.js +158 -0
- package/domain/collab/spawn.js +188 -0
- package/domain/config/schema.js +404 -0
- package/domain/contracts/outputs.js +279 -0
- package/domain/contracts/tool-inputs/collaboration.js +58 -0
- package/domain/contracts/tool-inputs/findings.js +29 -0
- package/domain/contracts/tool-inputs/index.js +94 -0
- package/domain/contracts/tool-inputs/integration.js +37 -0
- package/domain/contracts/tool-inputs/missions.js +94 -0
- package/domain/contracts/tool-inputs/ops.js +75 -0
- package/domain/contracts/tool-inputs/shared.js +25 -0
- package/domain/errors.js +96 -0
- package/domain/gate.js +125 -0
- package/domain/integration/gate.js +176 -0
- package/domain/integration/init.js +225 -0
- package/domain/integration/liveness.js +31 -0
- package/domain/integration/merge.js +377 -0
- package/domain/integration/provision.js +124 -0
- package/domain/integration/rebase.js +236 -0
- package/domain/integration/review.js +52 -0
- package/domain/integration/teardown.js +118 -0
- package/domain/mission.js +68 -0
- package/domain/missions/completion.js +114 -0
- package/domain/missions/findings.js +201 -0
- package/domain/missions/lifecycle.js +185 -0
- package/domain/missions/plan.js +149 -0
- package/domain/missions/review.js +179 -0
- package/domain/planning.js +118 -0
- package/domain/read/enrichedMissions.js +58 -0
- package/domain/read/missionContext.js +53 -0
- package/domain/read/views.js +128 -0
- package/domain/report.js +59 -0
- package/domain/roster.js +40 -0
- package/domain/scope.js +105 -0
- package/domain/state/schema.js +233 -0
- package/domain/state/semantics.js +117 -0
- package/domain/state.js +33 -0
- package/domain/summary.js +114 -0
- package/domain/vocabulary.js +179 -0
- package/index.js +86 -34
- package/lib/briefing.js +176 -0
- package/lib/compatibility.js +108 -0
- package/lib/config.js +54 -302
- package/lib/doctor.js +108 -9
- package/lib/git.js +93 -15
- package/lib/hmr.js +4 -3
- package/lib/leases.js +3 -2
- package/lib/legacy.js +20 -9
- package/lib/modeState.js +39 -0
- package/lib/modeStore.js +35 -33
- package/lib/persistence.js +349 -0
- package/lib/presets.js +35 -0
- package/lib/spawn-prechecks.js +168 -0
- package/lib/spawn-support.js +79 -0
- package/lib/store.js +57 -85
- package/lib/tests.js +46 -11
- package/lib/tool-registry.js +544 -0
- package/lib/workspaceLock.js +46 -22
- package/lib/worktreeOwnership.js +5 -4
- package/locale/en.json +23 -1
- package/locale/zh.json +22 -0
- package/package.json +33 -4
- package/ports/clock.js +6 -0
- package/ports/ids.js +7 -0
- package/ports/index.js +2 -0
- package/ports/store.js +11 -0
- package/ports/vcs.js +11 -0
- package/scripts/architecture.mjs +94 -0
- package/scripts/check-docs.mjs +44 -3
- package/scripts/check-pack.mjs +49 -0
- package/scripts/domain-metrics.mjs +48 -0
- package/scripts/generate-config-reference.mjs +46 -0
- package/scripts/generate-error-reference.mjs +29 -0
- package/scripts/generate-tools-reference.mjs +56 -0
- package/scripts/issue-audit.mjs +259 -0
- package/scripts/lib/cli.mjs +76 -0
- package/scripts/lib/tmp.mjs +20 -0
- package/scripts/mount-smoke/fixtures.mjs +162 -0
- package/scripts/mount-smoke/host.json +4 -0
- package/scripts/mount-smoke/probe.mjs +125 -0
- package/scripts/mount-smoke/surface.mjs +141 -0
- package/scripts/mount-smoke.mjs +270 -0
- package/scripts/pack-manifest.txt +203 -0
- package/scripts/release-check.mjs +303 -0
- package/skills/tower-recovery.md +7 -5
- package/tools/adopt.js +72 -0
- package/tools/batch.js +78 -0
- package/tools/complete.js +53 -0
- package/tools/diff.js +39 -0
- package/tools/doctor.js +50 -0
- package/tools/finding.js +40 -0
- package/tools/findings.js +39 -0
- package/tools/import-legacy.js +42 -0
- package/tools/inbox.js +39 -0
- package/tools/index.js +39 -0
- package/tools/init.js +63 -0
- package/tools/interrupt.js +42 -0
- package/tools/legacy-map.js +44 -0
- package/tools/merge.js +39 -0
- package/tools/mission.js +41 -0
- package/tools/plan.js +39 -0
- package/tools/preflight.js +44 -0
- package/tools/rebase.js +39 -0
- package/tools/recover.js +45 -0
- package/tools/reload.js +52 -0
- package/tools/resource.js +54 -0
- package/tools/review.js +45 -0
- package/tools/roster.js +60 -0
- package/tools/send.js +147 -0
- package/tools/spawn.js +196 -0
- package/tools/status.js +94 -0
- package/tools/teardown.js +42 -0
- package/tools/test.js +61 -0
- package/types/cordis.d.ts +6 -0
- package/types/index.d.ts +339 -0
- package/ui-bundle/README.md +111 -0
- package/ui-bundle/client.js +1980 -0
- package/ui-bundle/cordis.patch.yml +3 -0
- package/ui-bundle/icon.svg +6 -0
- package/ui-bundle/index.js +2 -0
- package/ui-bundle/locale/en.json +6 -0
- package/ui-bundle/locale/zh.json +6 -0
- package/ui-bundle/package.json +35 -0
- package/lib/errors.js +0 -111
- package/lib/tools.js +0 -2250
- package/lib/tower.js +0 -2689
- /package/{lib → domain}/batch.js +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Pure preflight/gate blocker computation from state snapshots plus the Git
|
|
2
|
+
// facts the app already read (tips, changed files, checkout branch). The app
|
|
3
|
+
// performs every read and owns the result assembly.
|
|
4
|
+
// @ts-check
|
|
5
|
+
import { fileInScope } from '../scope.js'
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('../../types/index.js').Blocker} Blocker */
|
|
8
|
+
/** @typedef {import('../../types/index.js').Mission} Mission */
|
|
9
|
+
/** @typedef {import('../../types/index.js').TowerState} TowerState */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} missionId
|
|
13
|
+
* @returns {object}
|
|
14
|
+
*/
|
|
15
|
+
export function missingMissionGateResult(missionId) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
blockers: [{ code: 'mission-missing', detail: `unknown mission "${missionId}"` }],
|
|
19
|
+
missionId,
|
|
20
|
+
branchTip: null,
|
|
21
|
+
baseTip: null,
|
|
22
|
+
changedFiles: [],
|
|
23
|
+
review: null,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {{ missionId: string, startedAt?: number }} mergeIntent
|
|
29
|
+
* @returns {Blocker}
|
|
30
|
+
*/
|
|
31
|
+
export function mergeIntentPendingBlocker(mergeIntent) {
|
|
32
|
+
return {
|
|
33
|
+
code: 'merge-intent-pending',
|
|
34
|
+
detail: `a merge intent for ${mergeIntent.missionId} was recorded at ${new Date(Number(mergeIntent.startedAt) || 0).toISOString()}; run tower_merge to reconcile it`,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} missionId
|
|
40
|
+
* @returns {Blocker}
|
|
41
|
+
*/
|
|
42
|
+
export function noWorktreeBlocker(missionId) {
|
|
43
|
+
return { code: 'branch-missing', detail: `mission ${missionId} has no worktree or branch` }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {object} input
|
|
48
|
+
* @param {string} input.branch
|
|
49
|
+
* @param {string} input.base
|
|
50
|
+
* @param {string} input.baseTip
|
|
51
|
+
* @returns {Blocker}
|
|
52
|
+
*/
|
|
53
|
+
export function staleBaseBlocker({ branch, base, baseTip }) {
|
|
54
|
+
return {
|
|
55
|
+
code: 'stale-base',
|
|
56
|
+
detail: `${branch} does not contain base "${base}" tip ${baseTip.slice(0, 12)}; run tower_rebase first`,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {object} input
|
|
62
|
+
* @param {string} input.missionId
|
|
63
|
+
* @param {string[]} input.changedFiles
|
|
64
|
+
* @returns {Blocker | null}
|
|
65
|
+
*/
|
|
66
|
+
export function surveyChangedFilesBlocker({ missionId, changedFiles }) {
|
|
67
|
+
return changedFiles.length > 0
|
|
68
|
+
? {
|
|
69
|
+
code: 'read-only-survey',
|
|
70
|
+
detail: `survey ${missionId} is read-only but ${changedFiles.length} file(s) changed: ${changedFiles.slice(0, 5).join(', ')}`,
|
|
71
|
+
}
|
|
72
|
+
: null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {object} input
|
|
77
|
+
* @param {Mission} input.mission
|
|
78
|
+
* @param {string[]} input.changedFiles
|
|
79
|
+
* @returns {Blocker | null}
|
|
80
|
+
*/
|
|
81
|
+
export function outOfScopeBlocker({ mission, changedFiles }) {
|
|
82
|
+
const outOfScope = changedFiles.filter((file) => !fileInScope(mission.scope, file))
|
|
83
|
+
return outOfScope.length > 0
|
|
84
|
+
? {
|
|
85
|
+
code: 'out-of-scope',
|
|
86
|
+
detail: `changed files outside scope (${mission.scope.join(', ')}): ${outOfScope.slice(0, 8).join(', ')}`,
|
|
87
|
+
}
|
|
88
|
+
: null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {string[]} changedFiles
|
|
93
|
+
* @param {string[]} dirty
|
|
94
|
+
* @returns {string[]}
|
|
95
|
+
*/
|
|
96
|
+
export function overwrittenPaths(changedFiles, dirty) {
|
|
97
|
+
return changedFiles.filter((file) => dirty.includes(file))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string[]} wouldOverwrite
|
|
102
|
+
* @returns {Blocker | null}
|
|
103
|
+
*/
|
|
104
|
+
export function baseDirtyBlocker(wouldOverwrite) {
|
|
105
|
+
return wouldOverwrite.length > 0
|
|
106
|
+
? {
|
|
107
|
+
code: 'base-dirty',
|
|
108
|
+
detail: `main checkout has uncommitted changes this merge would overwrite: ${wouldOverwrite.slice(0, 8).join(', ')}`,
|
|
109
|
+
}
|
|
110
|
+
: null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {object} input
|
|
115
|
+
* @param {string[]} input.deps
|
|
116
|
+
* @param {TowerState} input.state
|
|
117
|
+
* @returns {Blocker[]}
|
|
118
|
+
*/
|
|
119
|
+
export function dependencyBlockers({ deps = [], state }) {
|
|
120
|
+
/** @type {Blocker[]} */
|
|
121
|
+
const blockers = []
|
|
122
|
+
for (const depId of deps) {
|
|
123
|
+
const dep = state.missions.find((entry) => entry.id === depId)
|
|
124
|
+
if (!dep || !['merged', 'abandoned'].includes(dep.status)) {
|
|
125
|
+
blockers.push({ code: 'deps-unmerged', detail: `${depId} is ${dep ? dep.status : 'missing'}` })
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return blockers
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {object} input
|
|
133
|
+
* @param {string | null} input.checkoutBranch
|
|
134
|
+
* @param {string} input.base
|
|
135
|
+
* @returns {Blocker | null}
|
|
136
|
+
*/
|
|
137
|
+
export function baseWrongBranchBlocker({ checkoutBranch, base }) {
|
|
138
|
+
return checkoutBranch !== base
|
|
139
|
+
? { code: 'base-wrong-branch', detail: `main checkout is on ${checkoutBranch ?? 'detached HEAD'}, expected "${base}"` }
|
|
140
|
+
: null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Append every blocker produced by a decision helper.
|
|
145
|
+
* @param {Blocker[]} blockers
|
|
146
|
+
* @param {Blocker | null} blocker
|
|
147
|
+
* @returns {Blocker[]}
|
|
148
|
+
*/
|
|
149
|
+
export function pushBlocker(blockers, blocker) {
|
|
150
|
+
if (blocker) blockers.push(blocker)
|
|
151
|
+
return blockers
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {object} input
|
|
156
|
+
* @param {object} input.mission full mission projection
|
|
157
|
+
* @param {object | null} input.worktree
|
|
158
|
+
* @param {string | null} input.branchTip
|
|
159
|
+
* @param {string | null} input.baseTip
|
|
160
|
+
* @param {string[]} input.changedFiles
|
|
161
|
+
* @param {object | null} input.review
|
|
162
|
+
* @param {Blocker[]} input.blockers
|
|
163
|
+
* @returns {object}
|
|
164
|
+
*/
|
|
165
|
+
export function gateResult({ mission, worktree, branchTip, baseTip, changedFiles, review, blockers }) {
|
|
166
|
+
return {
|
|
167
|
+
ok: blockers.length === 0,
|
|
168
|
+
blockers,
|
|
169
|
+
mission,
|
|
170
|
+
worktree,
|
|
171
|
+
branchTip,
|
|
172
|
+
baseTip,
|
|
173
|
+
changedFiles,
|
|
174
|
+
review,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Pure init decisions: explicit/fallback base selection, ownership and reanchor
|
|
2
|
+
// policy, snapshot decisions, and the initial/existing result projections. All
|
|
3
|
+
// fs/git work and state persistence stay in app/init.js.
|
|
4
|
+
// @ts-check
|
|
5
|
+
import { SCHEMA_VERSION, TERMINAL_STATUSES } from '../mission.js'
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('../../types/index.js').Mission} Mission */
|
|
8
|
+
/** @typedef {import('../../types/index.js').TowerState} TowerState */
|
|
9
|
+
|
|
10
|
+
export const SNAPSHOT_REF_PREFIX = 'refs/tower/snapshot/'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} commit
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
export function snapshotRefFor(commit) {
|
|
17
|
+
return `${SNAPSHOT_REF_PREFIX}${commit}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {unknown} value
|
|
22
|
+
* @returns {string | null}
|
|
23
|
+
*/
|
|
24
|
+
export function explicitBase(value) {
|
|
25
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An explicit base must be a local branch name, never a raw ref or remote ref.
|
|
30
|
+
* @param {string} base
|
|
31
|
+
* @returns {string | null} the failure message, or null when valid
|
|
32
|
+
*/
|
|
33
|
+
export function invalidBaseMessage(base) {
|
|
34
|
+
return /^refs\//.test(base) || base.startsWith('origin/') ? `base must be a local branch name, not ${base}` : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Without an explicit base, use the checked-out branch and fall back to `main`
|
|
39
|
+
* only for an unborn repository.
|
|
40
|
+
* @param {object} input
|
|
41
|
+
* @param {string | null} [input.branch]
|
|
42
|
+
* @param {boolean} input.committed
|
|
43
|
+
* @returns {{ base: string } | { error: string }}
|
|
44
|
+
*/
|
|
45
|
+
export function fallbackBase({ branch, committed }) {
|
|
46
|
+
let base = branch
|
|
47
|
+
if (!base && !committed) base = 'main'
|
|
48
|
+
if (!base) return { error: 'repository is in detached HEAD; pass an explicit local base branch' }
|
|
49
|
+
return { base }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string | null} owner
|
|
54
|
+
* @param {string | null} caller
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
export function ownedByOther(owner, caller) {
|
|
58
|
+
return Boolean(owner && caller && owner !== caller)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string | null} owner
|
|
63
|
+
* @param {string | null} caller
|
|
64
|
+
* @returns {string | null} the caller that claims an unowned state
|
|
65
|
+
*/
|
|
66
|
+
export function claimCaller(owner, caller) {
|
|
67
|
+
return !owner && caller ? caller : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {Mission[]} missions
|
|
72
|
+
* @returns {string | null} refusal message, or null when reanchor may proceed
|
|
73
|
+
*/
|
|
74
|
+
export function reanchorBlockedMessage(missions) {
|
|
75
|
+
const active = missions.filter((mission) => !TERMINAL_STATUSES.has(mission.status))
|
|
76
|
+
return active.length > 0 ? `cannot reanchor base while ${active.length} mission(s) are not merged or abandoned` : null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {object} input
|
|
81
|
+
* @param {boolean} input.reanchor
|
|
82
|
+
* @param {string | null} input.reanchorBase
|
|
83
|
+
* @param {string} input.currentBase
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function shouldReanchor({ reanchor, reanchorBase, currentBase }) {
|
|
87
|
+
return reanchor === true && reanchorBase !== currentBase
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {object} input
|
|
92
|
+
* @param {boolean} input.snapshotDirty
|
|
93
|
+
* @param {boolean} input.dirty
|
|
94
|
+
* @returns {boolean}
|
|
95
|
+
*/
|
|
96
|
+
export function shouldSnapshot({ snapshotDirty, dirty }) {
|
|
97
|
+
return snapshotDirty === true && dirty
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {object} input
|
|
102
|
+
* @param {TowerState} input.state
|
|
103
|
+
* @param {string} input.statePath
|
|
104
|
+
* @param {boolean} input.dirty
|
|
105
|
+
* @returns {object}
|
|
106
|
+
*/
|
|
107
|
+
export function existingInitResult({ state, statePath, dirty }) {
|
|
108
|
+
return {
|
|
109
|
+
created: false,
|
|
110
|
+
repoRoot: state.repoRoot,
|
|
111
|
+
commonDir: state.commonDir,
|
|
112
|
+
statePath,
|
|
113
|
+
base: state.base,
|
|
114
|
+
revision: state.revision,
|
|
115
|
+
missions: state.missions.length,
|
|
116
|
+
reanchored: false,
|
|
117
|
+
dirty,
|
|
118
|
+
snapshotCommit: state.snapshotCommit ?? null,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {object} input
|
|
124
|
+
* @param {string} input.repoRoot
|
|
125
|
+
* @param {string} input.commonDir
|
|
126
|
+
* @param {string} input.base
|
|
127
|
+
* @param {boolean} input.dirty
|
|
128
|
+
* @param {string | null} [input.towerAgentId]
|
|
129
|
+
* @param {string | null} [input.snapshotCommit]
|
|
130
|
+
* @param {string | null} [input.snapshotRef]
|
|
131
|
+
* @param {number} input.now
|
|
132
|
+
* @returns {TowerState}
|
|
133
|
+
*/
|
|
134
|
+
export function initialState({ repoRoot, commonDir, base, dirty, towerAgentId = null, snapshotCommit = null, snapshotRef = null, now }) {
|
|
135
|
+
return {
|
|
136
|
+
schemaVersion: SCHEMA_VERSION,
|
|
137
|
+
repoRoot,
|
|
138
|
+
commonDir,
|
|
139
|
+
base,
|
|
140
|
+
revision: 0,
|
|
141
|
+
createdAt: now,
|
|
142
|
+
updatedAt: now,
|
|
143
|
+
dirtyBase: dirty,
|
|
144
|
+
...(towerAgentId ? { towerAgentId: String(towerAgentId) } : {}),
|
|
145
|
+
...(snapshotCommit ? { snapshotCommit, snapshotRef } : {}),
|
|
146
|
+
missions: [],
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {object} input
|
|
152
|
+
* @param {string} input.repoRoot
|
|
153
|
+
* @param {string} input.commonDir
|
|
154
|
+
* @param {string} input.statePath
|
|
155
|
+
* @param {string} input.base
|
|
156
|
+
* @param {boolean} input.dirty
|
|
157
|
+
* @param {string | null} input.snapshotCommit
|
|
158
|
+
* @param {string | null} input.snapshotRef
|
|
159
|
+
* @returns {object}
|
|
160
|
+
*/
|
|
161
|
+
export function createdInitResult({ repoRoot, commonDir, statePath, base, dirty, snapshotCommit, snapshotRef }) {
|
|
162
|
+
return {
|
|
163
|
+
created: true,
|
|
164
|
+
repoRoot,
|
|
165
|
+
commonDir,
|
|
166
|
+
statePath,
|
|
167
|
+
base,
|
|
168
|
+
revision: 0,
|
|
169
|
+
missions: 0,
|
|
170
|
+
reanchored: false,
|
|
171
|
+
dirty,
|
|
172
|
+
snapshotCommit,
|
|
173
|
+
snapshotRef,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {object} input
|
|
179
|
+
* @param {TowerState} input.state
|
|
180
|
+
* @param {string} input.statePath
|
|
181
|
+
* @param {boolean} input.dirty
|
|
182
|
+
* @param {boolean} input.reanchored
|
|
183
|
+
* @returns {object}
|
|
184
|
+
*/
|
|
185
|
+
export function existingInitWriteResult({ state, statePath, dirty, reanchored }) {
|
|
186
|
+
return {
|
|
187
|
+
created: false,
|
|
188
|
+
repoRoot: state.repoRoot,
|
|
189
|
+
commonDir: state.commonDir,
|
|
190
|
+
statePath,
|
|
191
|
+
base: state.base,
|
|
192
|
+
revision: state.revision,
|
|
193
|
+
missions: state.missions.length,
|
|
194
|
+
reanchored,
|
|
195
|
+
dirty,
|
|
196
|
+
snapshotCommit: state.snapshotCommit ?? null,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {object} input
|
|
202
|
+
* @param {string} input.base
|
|
203
|
+
* @param {string | null} [input.snapshotCommit]
|
|
204
|
+
* @param {boolean} input.dirty
|
|
205
|
+
* @returns {string}
|
|
206
|
+
*/
|
|
207
|
+
export function initActivityText({ base, snapshotCommit = null, dirty }) {
|
|
208
|
+
return `base ${base}${snapshotCommit ? ` snapshot ${snapshotCommit.slice(0, 12)}` : ''}${dirty ? ' (dirty base)' : ''}`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {string} caller
|
|
213
|
+
* @returns {string}
|
|
214
|
+
*/
|
|
215
|
+
export function ownerClaimActivityText(caller) {
|
|
216
|
+
return `tower owner claimed by ${caller}`
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @param {string} base
|
|
221
|
+
* @returns {string}
|
|
222
|
+
*/
|
|
223
|
+
export function reanchorActivityText(base) {
|
|
224
|
+
return `base reanchored to ${base}`
|
|
225
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Pure roster-liveness decisions shared by rebase/teardown guards. A live
|
|
2
|
+
// worker is a roster row for the mission that is neither inactive nor finished
|
|
3
|
+
// and whose observed activity is "running".
|
|
4
|
+
// @ts-check
|
|
5
|
+
import { ROSTER_INACTIVE } from '../roster.js'
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('../../types/index.js').RosterEntry} RosterEntry */
|
|
8
|
+
/** @typedef {import('../../types/index.js').TowerState} TowerState */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {TowerState} state
|
|
12
|
+
* @param {string} missionId
|
|
13
|
+
* @param {Record<string, string | undefined>} activity
|
|
14
|
+
* @returns {RosterEntry | undefined}
|
|
15
|
+
*/
|
|
16
|
+
export function liveAgentFor(state, missionId, activity) {
|
|
17
|
+
return (state.roster ?? []).find(
|
|
18
|
+
(entry) =>
|
|
19
|
+
entry.missionId === missionId && !ROSTER_INACTIVE.has(entry.state) && activity[String(entry.agentId)] === 'running',
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {TowerState} state
|
|
25
|
+
* @param {string} missionId
|
|
26
|
+
* @param {Record<string, string | undefined>} activity
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
export function hasLiveAgent(state, missionId, activity) {
|
|
30
|
+
return liveAgentFor(state, missionId, activity) !== undefined
|
|
31
|
+
}
|