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.
Files changed (193) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/COMPATIBILITY.md +38 -0
  3. package/CONTRIBUTING.md +150 -2
  4. package/README.en.md +59 -16
  5. package/README.md +57 -16
  6. package/SECURITY.md +31 -1
  7. package/adapters/default.js +10 -0
  8. package/adapters/store-fs.js +6 -0
  9. package/adapters/vcs-git.js +4 -0
  10. package/app/batch.js +79 -0
  11. package/app/board.js +25 -0
  12. package/app/checkCompletion.js +76 -0
  13. package/app/enrichedMissions.js +26 -0
  14. package/app/findings.js +78 -0
  15. package/app/gate.js +108 -0
  16. package/app/init.js +249 -0
  17. package/app/merge.js +567 -0
  18. package/app/messages.js +66 -0
  19. package/app/mission.js +35 -0
  20. package/app/missionContext.js +14 -0
  21. package/app/plan.js +42 -0
  22. package/app/provision.js +193 -0
  23. package/app/read.js +57 -0
  24. package/app/rebase.js +302 -0
  25. package/app/recording.js +72 -0
  26. package/app/review.js +145 -0
  27. package/app/roster.js +123 -0
  28. package/app/spawn.js +125 -0
  29. package/app/teardown.js +184 -0
  30. package/app/tower.js +524 -0
  31. package/cordis.patch.yml +61 -53
  32. package/docs/README.md +33 -1
  33. package/docs/architecture/overview.md +114 -14
  34. package/docs/board.md +72 -0
  35. package/docs/examples/first-mission.md +2 -1
  36. package/docs/faq.md +11 -7
  37. package/docs/guide/hot-reload.md +1 -1
  38. package/docs/guide/install-upgrade-migration.md +22 -6
  39. package/docs/guide/quickstart.md +12 -7
  40. package/docs/guide/release.md +125 -0
  41. package/docs/guide/scenarios-and-philosophy.md +86 -69
  42. package/docs/operations/security.md +65 -14
  43. package/docs/reference/blockers.md +11 -10
  44. package/docs/reference/config.generated.md +66 -0
  45. package/docs/reference/config.md +11 -7
  46. package/docs/reference/errors.generated.md +39 -0
  47. package/docs/reference/errors.md +10 -2
  48. package/docs/reference/events.md +7 -4
  49. package/docs/reference/testing.md +2 -1
  50. package/docs/reference/tools.generated.md +33 -0
  51. package/docs/reference/tools.md +8 -7
  52. package/docs/zh/README.md +4 -3
  53. package/docs/zh/faq.md +9 -6
  54. package/docs/zh/guide/hot-reload.md +1 -1
  55. package/docs/zh/guide/install-upgrade-migration.md +17 -5
  56. package/docs/zh/guide/quickstart.md +13 -8
  57. package/docs/zh/guide/scenarios-and-philosophy.md +55 -50
  58. package/docs/zh/operations/security.md +43 -7
  59. package/docs/zh/reference/blockers.md +10 -10
  60. package/docs/zh/reference/config.md +11 -7
  61. package/docs/zh/reference/errors.md +4 -1
  62. package/docs/zh/reference/events.md +6 -3
  63. package/docs/zh/reference/testing.md +3 -2
  64. package/docs/zh/reference/tools.md +8 -7
  65. package/domain/collab/batch.js +112 -0
  66. package/domain/collab/messages.js +127 -0
  67. package/domain/collab/roster.js +158 -0
  68. package/domain/collab/spawn.js +188 -0
  69. package/domain/config/schema.js +404 -0
  70. package/domain/contracts/outputs.js +279 -0
  71. package/domain/contracts/tool-inputs/collaboration.js +58 -0
  72. package/domain/contracts/tool-inputs/findings.js +29 -0
  73. package/domain/contracts/tool-inputs/index.js +94 -0
  74. package/domain/contracts/tool-inputs/integration.js +37 -0
  75. package/domain/contracts/tool-inputs/missions.js +94 -0
  76. package/domain/contracts/tool-inputs/ops.js +75 -0
  77. package/domain/contracts/tool-inputs/shared.js +25 -0
  78. package/domain/errors.js +96 -0
  79. package/domain/gate.js +125 -0
  80. package/domain/integration/gate.js +176 -0
  81. package/domain/integration/init.js +225 -0
  82. package/domain/integration/liveness.js +31 -0
  83. package/domain/integration/merge.js +377 -0
  84. package/domain/integration/provision.js +124 -0
  85. package/domain/integration/rebase.js +236 -0
  86. package/domain/integration/review.js +52 -0
  87. package/domain/integration/teardown.js +118 -0
  88. package/domain/mission.js +68 -0
  89. package/domain/missions/completion.js +114 -0
  90. package/domain/missions/findings.js +201 -0
  91. package/domain/missions/lifecycle.js +185 -0
  92. package/domain/missions/plan.js +149 -0
  93. package/domain/missions/review.js +179 -0
  94. package/domain/planning.js +118 -0
  95. package/domain/read/enrichedMissions.js +58 -0
  96. package/domain/read/missionContext.js +53 -0
  97. package/domain/read/views.js +128 -0
  98. package/domain/report.js +59 -0
  99. package/domain/roster.js +40 -0
  100. package/domain/scope.js +105 -0
  101. package/domain/state/schema.js +233 -0
  102. package/domain/state/semantics.js +117 -0
  103. package/domain/state.js +33 -0
  104. package/domain/summary.js +114 -0
  105. package/domain/vocabulary.js +179 -0
  106. package/index.js +86 -34
  107. package/lib/briefing.js +176 -0
  108. package/lib/compatibility.js +108 -0
  109. package/lib/config.js +54 -302
  110. package/lib/doctor.js +108 -9
  111. package/lib/git.js +93 -15
  112. package/lib/hmr.js +4 -3
  113. package/lib/leases.js +3 -2
  114. package/lib/legacy.js +20 -9
  115. package/lib/modeState.js +39 -0
  116. package/lib/modeStore.js +35 -33
  117. package/lib/persistence.js +349 -0
  118. package/lib/presets.js +35 -0
  119. package/lib/spawn-prechecks.js +168 -0
  120. package/lib/spawn-support.js +79 -0
  121. package/lib/store.js +57 -85
  122. package/lib/tests.js +46 -11
  123. package/lib/tool-registry.js +544 -0
  124. package/lib/workspaceLock.js +46 -22
  125. package/lib/worktreeOwnership.js +5 -4
  126. package/locale/en.json +23 -1
  127. package/locale/zh.json +22 -0
  128. package/package.json +33 -4
  129. package/ports/clock.js +6 -0
  130. package/ports/ids.js +7 -0
  131. package/ports/index.js +2 -0
  132. package/ports/store.js +11 -0
  133. package/ports/vcs.js +11 -0
  134. package/scripts/architecture.mjs +94 -0
  135. package/scripts/check-docs.mjs +44 -3
  136. package/scripts/check-pack.mjs +49 -0
  137. package/scripts/domain-metrics.mjs +48 -0
  138. package/scripts/generate-config-reference.mjs +46 -0
  139. package/scripts/generate-error-reference.mjs +29 -0
  140. package/scripts/generate-tools-reference.mjs +56 -0
  141. package/scripts/issue-audit.mjs +259 -0
  142. package/scripts/lib/cli.mjs +76 -0
  143. package/scripts/lib/tmp.mjs +20 -0
  144. package/scripts/mount-smoke/fixtures.mjs +162 -0
  145. package/scripts/mount-smoke/host.json +4 -0
  146. package/scripts/mount-smoke/probe.mjs +125 -0
  147. package/scripts/mount-smoke/surface.mjs +141 -0
  148. package/scripts/mount-smoke.mjs +270 -0
  149. package/scripts/pack-manifest.txt +203 -0
  150. package/scripts/release-check.mjs +303 -0
  151. package/skills/tower-recovery.md +7 -5
  152. package/tools/adopt.js +72 -0
  153. package/tools/batch.js +78 -0
  154. package/tools/complete.js +53 -0
  155. package/tools/diff.js +39 -0
  156. package/tools/doctor.js +50 -0
  157. package/tools/finding.js +40 -0
  158. package/tools/findings.js +39 -0
  159. package/tools/import-legacy.js +42 -0
  160. package/tools/inbox.js +39 -0
  161. package/tools/index.js +39 -0
  162. package/tools/init.js +63 -0
  163. package/tools/interrupt.js +42 -0
  164. package/tools/legacy-map.js +44 -0
  165. package/tools/merge.js +39 -0
  166. package/tools/mission.js +41 -0
  167. package/tools/plan.js +39 -0
  168. package/tools/preflight.js +44 -0
  169. package/tools/rebase.js +39 -0
  170. package/tools/recover.js +45 -0
  171. package/tools/reload.js +52 -0
  172. package/tools/resource.js +54 -0
  173. package/tools/review.js +45 -0
  174. package/tools/roster.js +60 -0
  175. package/tools/send.js +147 -0
  176. package/tools/spawn.js +196 -0
  177. package/tools/status.js +94 -0
  178. package/tools/teardown.js +42 -0
  179. package/tools/test.js +61 -0
  180. package/types/cordis.d.ts +6 -0
  181. package/types/index.d.ts +339 -0
  182. package/ui-bundle/README.md +111 -0
  183. package/ui-bundle/client.js +1980 -0
  184. package/ui-bundle/cordis.patch.yml +3 -0
  185. package/ui-bundle/icon.svg +6 -0
  186. package/ui-bundle/index.js +2 -0
  187. package/ui-bundle/locale/en.json +6 -0
  188. package/ui-bundle/locale/zh.json +6 -0
  189. package/ui-bundle/package.json +35 -0
  190. package/lib/errors.js +0 -111
  191. package/lib/tools.js +0 -2250
  192. package/lib/tower.js +0 -2689
  193. /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
+ }