cli-swarm 7.0.18 → 7.0.25

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.
@@ -0,0 +1,500 @@
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import { coordinatorError, identifier, readCoordinationState,
3
+ withCoordinationState, writeSignedLease } from './swarm-coordinator-fs.mjs'
4
+ import { coordinationMessage, detectWaitCycles, locksConflict, normalizeLockRequest,
5
+ normalizeTaskCard, normalizeWait, requireString, requireTaskStatus,
6
+ scanTaskConflicts, validateInputShape } from './swarm-coordinator-model.mjs'
7
+
8
+ const LOCAL_SCHEMA = 'swarm.coordinator-local/1.0'
9
+ const OPERATIONS = Object.freeze([
10
+ 'capabilities', 'register-task', 'conflict-scan', 'lock-acquire', 'lock-renew',
11
+ 'lock-release', 'baseline-handshake', 'dependency-wait', 'event-publish',
12
+ 'task-status', 'tick', 'resolve-human', 'status',
13
+ ])
14
+
15
+ const objectSchema = (required, properties) => ({
16
+ type: 'object', additionalProperties: false, required, properties,
17
+ })
18
+ const string = { type: 'string', minLength: 1 }
19
+ const stringArray = { type: 'array', items: string }
20
+ const nullableString = { type: ['string', 'null'] }
21
+ const TASK_CARD_SCHEMA = objectSchema(
22
+ ['taskId', 'agentId', 'chainId', 'taskScope', 'plannedActions', 'deployTarget', 'eta', 'baselineHash', 'archConstraints'],
23
+ { taskId: string, agentId: string, chainId: string, taskScope: stringArray,
24
+ plannedActions: stringArray, deployTarget: nullableString, eta: string,
25
+ baselineHash: string, archConstraints: stringArray },
26
+ )
27
+ const LOCK_SCHEMA = objectSchema(
28
+ ['taskId', 'agentId', 'chainId', 'lockType', 'resource', 'paths', 'ttlSeconds', 'queueOnConflict', 'baselineHandshakeId'],
29
+ { taskId: string, agentId: string, chainId: string,
30
+ lockType: { enum: ['file', 'build', 'deploy'] }, resource: nullableString, paths: stringArray,
31
+ ttlSeconds: { type: 'integer', minimum: 1, maximum: 3600 }, queueOnConflict: { type: 'boolean' },
32
+ baselineHandshakeId: nullableString },
33
+ )
34
+ const WAIT_SCHEMA = objectSchema(
35
+ ['taskId', 'chainId', 'waiter', 'waitFor', 'event', 'purpose', 'expectedWithinMs', 'onEvent', 'onTimeout', 'refetchPaths'],
36
+ { taskId: string, chainId: string, waiter: string, waitFor: string, event: string, purpose: string,
37
+ expectedWithinMs: { type: 'integer', minimum: 1 }, onEvent: { const: 'wake-with-package' },
38
+ onTimeout: { enum: ['escalate-need-human', 'abandon-wait', 'continue-after-timeout'] },
39
+ refetchPaths: stringArray },
40
+ )
41
+ const OPERATION_SCHEMAS = Object.freeze({
42
+ capabilities: objectSchema([], {}),
43
+ 'register-task': TASK_CARD_SCHEMA,
44
+ 'conflict-scan': objectSchema(['taskId'], { taskId: string }),
45
+ 'lock-acquire': LOCK_SCHEMA,
46
+ 'lock-renew': objectSchema(['lockId', 'taskId', 'agentId', 'chainId', 'ttlSeconds'],
47
+ { lockId: string, taskId: string, agentId: string, chainId: string,
48
+ ttlSeconds: { type: 'integer', minimum: 1, maximum: 3600 } }),
49
+ 'lock-release': objectSchema(['lockId', 'taskId', 'agentId', 'chainId'],
50
+ { lockId: string, taskId: string, agentId: string, chainId: string }),
51
+ 'baseline-handshake': objectSchema(['taskId', 'agentId', 'chainId', 'observedBaselineHash', 'refetchPaths'],
52
+ { taskId: string, agentId: string, chainId: string, observedBaselineHash: string, refetchPaths: stringArray }),
53
+ 'dependency-wait': WAIT_SCHEMA,
54
+ 'event-publish': objectSchema(['publisher', 'event', 'payload'],
55
+ { publisher: string, event: string, payload: { type: 'object' } }),
56
+ 'task-status': objectSchema(['taskId', 'agentId', 'chainId', 'status'],
57
+ { taskId: string, agentId: string, chainId: string,
58
+ status: { enum: ['active', 'waiting', 'completed', 'failed', 'reclaimed'] } }),
59
+ tick: objectSchema(['now'], { now: { type: 'string', format: 'date-time' } }),
60
+ 'resolve-human': objectSchema(['decisionId', 'answer', 'actorId'],
61
+ { decisionId: string, answer: string, actorId: string }),
62
+ status: objectSchema([], {}),
63
+ })
64
+
65
+ function sha256(value) { return createHash('sha256').update(value).digest('hex') }
66
+
67
+ function findTask(state, input) {
68
+ const taskId = identifier(input.taskId, 'taskId')
69
+ const task = state.tasks.find((item) => item.taskId === taskId)
70
+ if (!task) coordinatorError('SWARM_COORD_TASK_NOT_FOUND', `task ${taskId} is not registered`)
71
+ if (task.agentId !== identifier(input.agentId, 'agentId')
72
+ || task.chainId !== identifier(input.chainId, 'chainId')) {
73
+ coordinatorError('SWARM_COORD_TASK_AUTHORITY_DENIED', 'task ownership does not match')
74
+ }
75
+ return task
76
+ }
77
+
78
+ function addMessage(state, type, from, to, payload, now) {
79
+ const message = coordinationMessage(type, from, to, payload, now)
80
+ state.messages.push(message)
81
+ return message
82
+ }
83
+
84
+ function eventRecord(publisher, event, payload, now) {
85
+ return {
86
+ schemaVersion: 'swarm.coord-event/1.0', eventId: `event-${randomUUID()}`,
87
+ publisher, event, payload, publishedAt: now,
88
+ }
89
+ }
90
+
91
+ function wakeWait(state, wait, resolution, event, now) {
92
+ wait.status = resolution
93
+ wait.resolvedAt = now
94
+ wait.resolution = event
95
+ const task = state.tasks.find((item) => item.taskId === wait.taskId)
96
+ if (task && task.status === 'waiting') {
97
+ task.status = 'active'
98
+ task.updatedAt = now
99
+ }
100
+ const wakePackage = {
101
+ schemaVersion: 'swarm.wake-package/1.0', waitId: wait.waitId, reason: resolution,
102
+ event, refetchPaths: wait.refetchPaths, createdAt: now,
103
+ }
104
+ addMessage(state, 'dependency-wait', 'coordinator', wait.waiter, wakePackage, now)
105
+ return wakePackage
106
+ }
107
+
108
+ function routeEvent(state, event) {
109
+ return state.waits.filter((wait) => wait.status === 'active'
110
+ && wait.waitFor === event.publisher && wait.event === event.event)
111
+ .map((wait) => wakeWait(state, wait, 'event-received', event, event.publishedAt))
112
+ }
113
+
114
+ function confirmationRequest(decision) {
115
+ const options = decision.agents.map((agentId) => ({
116
+ id: `resume:${agentId}`, label: `先恢复 ${agentId}`, hint: '唤醒该智能体先解除依赖',
117
+ }))
118
+ options.push({ id: 'abort', label: '终止等待', hint: '终止相关等待并保持任务阻塞' })
119
+ return {
120
+ schemaVersion: 'confirm-protocol.skill.request/1.0',
121
+ requestId: `confirm-${decision.decisionId}`,
122
+ operation: 'interaction-request',
123
+ input: { interaction: {
124
+ schemaVersion: 'confirm.interaction/1.0', requestId: decision.decisionId,
125
+ type: 'choice', question: decision.question, options, default: null, timeout: null,
126
+ timeoutAction: 'wait', risk: 'high', riskDescription: decision.riskDescription,
127
+ rememberable: false, memoryKey: '',
128
+ callback: { operation: 'resolve-human', payload: { decisionId: decision.decisionId } },
129
+ } },
130
+ }
131
+ }
132
+
133
+ function createDecision(state, kind, agents, waitIds, question, riskDescription, now) {
134
+ const decision = {
135
+ schemaVersion: 'swarm.coord-decision/1.0', decisionId: `decision-${randomUUID()}`,
136
+ kind, agents: [...new Set(agents)], waitIds: [...new Set(waitIds)], question,
137
+ riskDescription, status: 'pending', answer: null, actorId: null,
138
+ createdAt: now, resolvedAt: null,
139
+ }
140
+ state.decisions.push(decision)
141
+ addMessage(state, 'need-human', 'coordinator', 'human', { decisionId: decision.decisionId, kind, agents, waitIds }, now)
142
+ return { decision, confirmProtocolRequest: confirmationRequest(decision) }
143
+ }
144
+
145
+ async function registerTask(repositoryRoot, input) {
146
+ return withCoordinationState(repositoryRoot, async (state) => {
147
+ const card = normalizeTaskCard(input)
148
+ if (state.tasks.some((task) => task.taskId === card.taskId)) {
149
+ coordinatorError('SWARM_COORD_TASK_EXISTS', `task ${card.taskId} is already registered`)
150
+ }
151
+ state.tasks.push(card)
152
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, task: card },
153
+ audit: [{ event: 'range-declare', taskId: card.taskId, agentId: card.agentId, taskScope: card.taskScope }] }
154
+ })
155
+ }
156
+
157
+ async function conflictScan(repositoryRoot, input) {
158
+ return withCoordinationState(repositoryRoot, async (state) => {
159
+ const taskId = identifier(input.taskId, 'taskId')
160
+ const task = state.tasks.find((item) => item.taskId === taskId)
161
+ if (!task) coordinatorError('SWARM_COORD_TASK_NOT_FOUND', `task ${taskId} is not registered`)
162
+ const conflicts = scanTaskConflicts(task, state.tasks)
163
+ const messages = conflicts.filter((conflict) => conflict.type !== 'requirement')
164
+ .map((conflict) => addMessage(state, 'conflict-alert', 'coordinator', task.agentId, conflict))
165
+ const decisions = conflicts.filter((conflict) => conflict.type === 'requirement').map((conflict) => {
166
+ const other = state.tasks.find((item) => item.taskId === conflict.taskId)
167
+ if (!other) coordinatorError('SWARM_COORD_TASK_NOT_FOUND', `task ${conflict.taskId} is not registered`)
168
+ task.status = 'waiting'
169
+ other.status = 'waiting'
170
+ return createDecision(state, 'requirement-conflict', [task.agentId, other.agentId], [],
171
+ `任务 ${task.taskId} 与 ${conflict.taskId} 的需求声明冲突,请选择先恢复的智能体。`,
172
+ '矛盾需求同时执行会产生不可预测的覆盖,必须由真人裁决。', new Date().toISOString())
173
+ })
174
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, taskId, conflicts, messages,
175
+ decisions, zeroConflict: conflicts.length === 0 }, audit: [{ event: 'conflict-scan', taskId, conflictCount: conflicts.length }] }
176
+ })
177
+ }
178
+
179
+ async function grantLock(state, root, request, now) {
180
+ const issuedAt = now
181
+ const lockId = `lock-${randomUUID()}`
182
+ const leaseId = `lease-${randomUUID()}`
183
+ const expiresAt = new Date(Date.parse(now) + request.ttlSeconds * 1_000).toISOString()
184
+ const lease = await writeSignedLease(root, {
185
+ leaseId, lockId, chainId: request.chainId, agentId: request.agentId,
186
+ lockType: request.lockType, resource: request.resource, paths: request.paths,
187
+ issuedAt, expiresAt, nonce: randomUUID(),
188
+ }, sha256)
189
+ const lock = { schemaVersion: 'swarm.coord-lock/1.0', lockId, leaseId,
190
+ leasePath: lease.relativeLeasePath, taskId: request.taskId, chainId: request.chainId,
191
+ agentId: request.agentId, lockType: request.lockType, resource: request.resource,
192
+ paths: request.paths, status: 'active', issuedAt, expiresAt, releasedAt: null }
193
+ state.locks.push(lock)
194
+ addMessage(state, 'lock-granted', 'coordinator', request.agentId, { lockId, leaseId, expiresAt }, now)
195
+ return { lock, leasePath: lease.relativeLeasePath, lease: lease.signedLease }
196
+ }
197
+
198
+ function validHandshake(task, request) {
199
+ if (request.lockType === 'file') return request.baselineHandshakeId === null
200
+ return task.baselineHandshake !== null
201
+ && request.baselineHandshakeId === task.baselineHandshake.handshakeId
202
+ }
203
+
204
+ async function acquireLock(repositoryRoot, input) {
205
+ return withCoordinationState(repositoryRoot, async (state, root) => {
206
+ const request = normalizeLockRequest(input)
207
+ const task = findTask(state, request)
208
+ if (!validHandshake(task, request)) {
209
+ coordinatorError('SWARM_COORD_BASELINE_HANDSHAKE_REQUIRED', 'build and deploy locks require the current baseline handshake')
210
+ }
211
+ const conflicts = state.locks.filter((lock) => locksConflict(request, lock))
212
+ if (!conflicts.length) {
213
+ const granted = await grantLock(state, root, request, new Date().toISOString())
214
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, status: 'granted', ...granted },
215
+ audit: [{ event: 'lock-granted', lockId: granted.lock.lockId, taskId: request.taskId }] }
216
+ }
217
+ if (!request.queueOnConflict) coordinatorError('SWARM_COORD_LOCK_DENIED', 'the requested lock conflicts with an active lock')
218
+ const queued = { schemaVersion: 'swarm.coord-queue/1.0', queueId: `queue-${randomUUID()}`,
219
+ request, blockingLockIds: conflicts.map((lock) => lock.lockId), status: 'queued',
220
+ enqueuedAt: new Date().toISOString(), resolvedAt: null }
221
+ state.queue.push(queued)
222
+ addMessage(state, 'lock-denied', 'coordinator', request.agentId,
223
+ { queueId: queued.queueId, blockingLockIds: queued.blockingLockIds }, queued.enqueuedAt)
224
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, status: 'queued', queued },
225
+ audit: [{ event: 'lock-queued', queueId: queued.queueId, taskId: request.taskId }] }
226
+ })
227
+ }
228
+
229
+ async function renewLock(repositoryRoot, input) {
230
+ return withCoordinationState(repositoryRoot, async (state, root) => {
231
+ const task = findTask(state, input)
232
+ const lockId = identifier(input.lockId, 'lockId')
233
+ const lock = state.locks.find((item) => item.lockId === lockId)
234
+ if (!lock || lock.status !== 'active' || lock.taskId !== task.taskId) {
235
+ coordinatorError('SWARM_COORD_LOCK_NOT_ACTIVE', 'lock is not active for this task')
236
+ }
237
+ if (!Number.isInteger(input.ttlSeconds) || input.ttlSeconds < 1 || input.ttlSeconds > 3_600) {
238
+ coordinatorError('SWARM_COORD_LOCK_TTL_INVALID', 'ttlSeconds must be 1..3600')
239
+ }
240
+ lock.status = 'renewed'
241
+ const request = { ...lock, ttlSeconds: input.ttlSeconds }
242
+ const granted = await grantLock(state, root, request, new Date().toISOString())
243
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, status: 'renewed', ...granted },
244
+ audit: [{ event: 'lock-renewed', previousLockId: lockId, lockId: granted.lock.lockId }] }
245
+ })
246
+ }
247
+
248
+ async function promoteQueue(state, root, now) {
249
+ const promoted = []
250
+ for (const queued of state.queue.filter((item) => item.status === 'queued')) {
251
+ const conflicts = state.locks.filter((lock) => locksConflict(queued.request, lock))
252
+ if (conflicts.length) continue
253
+ queued.status = 'granted'
254
+ queued.resolvedAt = now
255
+ promoted.push(await grantLock(state, root, queued.request, now))
256
+ }
257
+ return promoted
258
+ }
259
+
260
+ async function releaseLock(repositoryRoot, input) {
261
+ return withCoordinationState(repositoryRoot, async (state, root) => {
262
+ const task = findTask(state, input)
263
+ const lockId = identifier(input.lockId, 'lockId')
264
+ const lock = state.locks.find((item) => item.lockId === lockId)
265
+ if (!lock || lock.status !== 'active' || lock.taskId !== task.taskId) {
266
+ coordinatorError('SWARM_COORD_LOCK_NOT_ACTIVE', 'lock is not active for this task')
267
+ }
268
+ const now = new Date().toISOString()
269
+ lock.status = 'released'
270
+ lock.releasedAt = now
271
+ const event = eventRecord(lock.agentId, 'lock-released', { lockId, resource: lock.resource }, now)
272
+ state.events.push(event)
273
+ const wakePackages = routeEvent(state, event)
274
+ const promoted = await promoteQueue(state, root, now)
275
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, released: lock, promoted, wakePackages },
276
+ audit: [{ event: 'lock-released', lockId, taskId: task.taskId }] }
277
+ })
278
+ }
279
+
280
+ async function baselineHandshake(repositoryRoot, input) {
281
+ return withCoordinationState(repositoryRoot, async (state) => {
282
+ const task = findTask(state, input)
283
+ const observed = requireString(input.observedBaselineHash, 'observedBaselineHash')
284
+ if (!/^[0-9a-f]{64}$/.test(observed)) coordinatorError('SWARM_COORD_BASELINE_INVALID', 'observedBaselineHash must be SHA-256')
285
+ if (!Array.isArray(input.refetchPaths)) coordinatorError('SWARM_COORD_REFETCH_REQUIRED', 'refetchPaths must be an array')
286
+ const now = new Date().toISOString()
287
+ const matched = observed === task.baselineHash
288
+ task.baselineHandshake = matched ? {
289
+ handshakeId: `handshake-${randomUUID()}`, baselineHash: observed, checkedAt: now,
290
+ } : null
291
+ if (!matched) task.status = 'waiting'
292
+ const message = addMessage(state, 'baseline-handshake', 'coordinator', task.agentId, {
293
+ matched, expectedBaselineHash: task.baselineHash, observedBaselineHash: observed,
294
+ refetchPaths: input.refetchPaths,
295
+ }, now)
296
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, allowed: matched,
297
+ handshake: task.baselineHandshake, message,
298
+ requiredAction: matched ? null : 'refetch-baseline' },
299
+ audit: [{ event: 'baseline-handshake', taskId: task.taskId, matched }] }
300
+ })
301
+ }
302
+
303
+ function applyCycles(state, now) {
304
+ const cycles = detectWaitCycles(state.waits)
305
+ const requests = []
306
+ for (const cycle of cycles) {
307
+ const agents = cycle.slice(0, -1)
308
+ const waits = state.waits.filter((wait) => wait.status === 'active'
309
+ && agents.includes(wait.waiter) && agents.includes(wait.waitFor))
310
+ for (const wait of waits) wakeWait(state, wait, 'deadlock-interrupted', { cycle }, now)
311
+ requests.push(createDecision(state, 'dependency-cycle', agents, waits.map((wait) => wait.waitId),
312
+ `检测到依赖等待成环:${agents.join(' → ')}。请选择先恢复的智能体。`,
313
+ '依赖成环会导致全部相关任务无限等待,必须由真人决定执行顺序。', now))
314
+ }
315
+ return requests
316
+ }
317
+
318
+ async function dependencyWait(repositoryRoot, input) {
319
+ return withCoordinationState(repositoryRoot, async (state) => {
320
+ const task = findTask(state, { taskId: input.taskId, agentId: input.waiter, chainId: input.chainId })
321
+ const wait = normalizeWait(input)
322
+ if (!state.tasks.some((item) => item.agentId === wait.waitFor)) {
323
+ coordinatorError('SWARM_COORD_WAIT_TARGET_UNKNOWN', 'waitFor must identify a registered agent')
324
+ }
325
+ task.status = 'waiting'
326
+ task.updatedAt = wait.startedAt
327
+ state.waits.push(wait)
328
+ addMessage(state, 'dependency-wait', wait.waiter, wait.waitFor, {
329
+ waitId: wait.waitId, event: wait.event, deadlineAt: wait.deadlineAt, purpose: wait.purpose,
330
+ }, wait.startedAt)
331
+ const decisions = applyCycles(state, wait.startedAt)
332
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, wait, suspended: wait.status === 'active', decisions },
333
+ audit: [{ event: 'dependency-wait', waitId: wait.waitId, waiter: wait.waiter, waitFor: wait.waitFor }] }
334
+ })
335
+ }
336
+
337
+ async function publishEvent(repositoryRoot, input) {
338
+ return withCoordinationState(repositoryRoot, async (state) => {
339
+ const publisher = identifier(input.publisher, 'publisher')
340
+ const eventName = identifier(input.event, 'event')
341
+ if (!input.payload || typeof input.payload !== 'object' || Array.isArray(input.payload)) {
342
+ coordinatorError('SWARM_COORD_EVENT_PAYLOAD_INVALID', 'payload must be an object')
343
+ }
344
+ const event = eventRecord(publisher, eventName, input.payload, new Date().toISOString())
345
+ state.events.push(event)
346
+ const wakePackages = routeEvent(state, event)
347
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, event, wakePackages },
348
+ audit: [{ event: 'event-published', eventId: event.eventId, publisher, eventName }] }
349
+ })
350
+ }
351
+
352
+ function notifyDeadTask(state, task, now) {
353
+ return state.waits.filter((wait) => wait.status === 'active' && wait.waitFor === task.agentId)
354
+ .map((wait) => wakeWait(state, wait, 'dependency-terminated', {
355
+ event: 'task-terminated', publisher: task.agentId, payload: { taskId: task.taskId, status: task.status },
356
+ publishedAt: now,
357
+ }, now))
358
+ }
359
+
360
+ async function taskStatus(repositoryRoot, input) {
361
+ return withCoordinationState(repositoryRoot, async (state) => {
362
+ const task = findTask(state, input)
363
+ const status = requireTaskStatus(input.status)
364
+ const now = new Date().toISOString()
365
+ task.status = status
366
+ task.updatedAt = now
367
+ const undeclaredWait = status === 'waiting' && !state.waits.some((wait) => (
368
+ wait.status === 'active' && wait.taskId === task.taskId
369
+ ))
370
+ let declarationMessage = null
371
+ if (undeclaredWait) declarationMessage = addMessage(state, 'dependency-wait', 'coordinator', task.agentId,
372
+ { status: 'declaration-required', taskId: task.taskId }, now)
373
+ const wakePackages = status === 'failed' || status === 'reclaimed'
374
+ ? notifyDeadTask(state, task, now) : []
375
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, task, undeclaredWait,
376
+ declarationMessage, wakePackages }, audit: [{ event: 'task-status', taskId: task.taskId, status }] }
377
+ })
378
+ }
379
+
380
+ function updateTimeoutCount(state, waiter) {
381
+ let entry = state.timeoutCounts.find((item) => item.waiter === waiter)
382
+ if (!entry) {
383
+ entry = { waiter, count: 0 }
384
+ state.timeoutCounts.push(entry)
385
+ }
386
+ entry.count += 1
387
+ return entry.count
388
+ }
389
+
390
+ function applyTimeouts(state, now) {
391
+ const timedOut = state.waits.filter((wait) => wait.status === 'active'
392
+ && Date.parse(wait.deadlineAt) <= Date.parse(now))
393
+ const decisions = []
394
+ const wakePackages = []
395
+ for (const wait of timedOut) {
396
+ const count = updateTimeoutCount(state, wait.waiter)
397
+ if (wait.onTimeout === 'escalate-need-human' || count >= 2) {
398
+ wakePackages.push(wakeWait(state, wait, 'timeout-interrupted', { timeoutCount: count }, now))
399
+ decisions.push(createDecision(state, 'dependency-timeout', [wait.waiter, wait.waitFor], [wait.waitId],
400
+ `${wait.waiter} 等待 ${wait.waitFor} 的 ${wait.event} 已超时,请选择后续动作。`,
401
+ '依赖事件未在声明期限内到达,继续静默等待可能导致任务停滞。', now))
402
+ } else {
403
+ const resolution = wait.onTimeout === 'abandon-wait' ? 'timeout-abandoned' : 'timeout-continued'
404
+ wakePackages.push(wakeWait(state, wait, resolution, { timeoutCount: count }, now))
405
+ }
406
+ }
407
+ return { timedOut, decisions, wakePackages }
408
+ }
409
+
410
+ async function tick(repositoryRoot, input) {
411
+ return withCoordinationState(repositoryRoot, async (state, root) => {
412
+ const nowMs = Date.parse(input.now)
413
+ if (!Number.isFinite(nowMs)) coordinatorError('SWARM_COORD_NOW_INVALID', 'now must be an ISO date-time')
414
+ const now = new Date(nowMs).toISOString()
415
+ const expiredLocks = state.locks.filter((lock) => lock.status === 'active'
416
+ && Date.parse(lock.expiresAt) <= nowMs)
417
+ for (const lock of expiredLocks) {
418
+ lock.status = 'expired'
419
+ lock.releasedAt = now
420
+ }
421
+ const promoted = await promoteQueue(state, root, now)
422
+ const timeouts = applyTimeouts(state, now)
423
+ const cycles = applyCycles(state, now)
424
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, expiredLocks, promoted,
425
+ timedOutWaits: timeouts.timedOut, wakePackages: timeouts.wakePackages,
426
+ decisions: [...timeouts.decisions, ...cycles] },
427
+ audit: [{ event: 'coordination-tick', expiredLocks: expiredLocks.length,
428
+ timedOutWaits: timeouts.timedOut.length, deadlocks: cycles.length }] }
429
+ })
430
+ }
431
+
432
+ async function resolveHuman(repositoryRoot, input) {
433
+ return withCoordinationState(repositoryRoot, async (state) => {
434
+ const decisionId = identifier(input.decisionId, 'decisionId')
435
+ const decision = state.decisions.find((item) => item.decisionId === decisionId)
436
+ if (!decision || decision.status !== 'pending') {
437
+ coordinatorError('SWARM_COORD_DECISION_NOT_PENDING', 'decision is not pending')
438
+ }
439
+ const answer = requireString(input.answer, 'answer')
440
+ const allowed = new Set([...decision.agents.map((agent) => `resume:${agent}`), 'abort'])
441
+ if (!allowed.has(answer)) coordinatorError('SWARM_COORD_DECISION_ANSWER_INVALID', 'answer is not a declared option')
442
+ const now = new Date().toISOString()
443
+ decision.status = 'resolved'
444
+ decision.answer = answer
445
+ decision.actorId = identifier(input.actorId, 'actorId')
446
+ decision.resolvedAt = now
447
+ if (answer.startsWith('resume:')) {
448
+ const agent = answer.slice('resume:'.length)
449
+ for (const task of state.tasks.filter((item) => item.agentId === agent && item.status === 'waiting')) {
450
+ task.status = 'active'
451
+ task.updatedAt = now
452
+ }
453
+ }
454
+ return { state, output: { schemaVersion: LOCAL_SCHEMA, decision },
455
+ audit: [{ event: 'decision-resolved', decisionId, answer, actorId: decision.actorId }] }
456
+ })
457
+ }
458
+
459
+ async function coordinatorStatus(repositoryRoot) {
460
+ const state = await readCoordinationState(repositoryRoot)
461
+ return { schemaVersion: LOCAL_SCHEMA, state }
462
+ }
463
+
464
+ const HANDLERS = Object.freeze({
465
+ 'register-task': registerTask,
466
+ 'conflict-scan': conflictScan,
467
+ 'lock-acquire': acquireLock,
468
+ 'lock-renew': renewLock,
469
+ 'lock-release': releaseLock,
470
+ 'baseline-handshake': baselineHandshake,
471
+ 'dependency-wait': dependencyWait,
472
+ 'event-publish': publishEvent,
473
+ 'task-status': taskStatus,
474
+ tick,
475
+ 'resolve-human': resolveHuman,
476
+ status: coordinatorStatus,
477
+ })
478
+
479
+ async function executeCoordinatorOperation(operation, repositoryRoot, input) {
480
+ if (operation === 'capabilities') return {
481
+ schemaVersion: LOCAL_SCHEMA, operations: OPERATIONS, operationSchemas: OPERATION_SCHEMAS,
482
+ roles: ['board', 'dispatcher', 'ops', 'security-guard', 'coordinator'],
483
+ stateBoundary: '.coord persistent ledger; conversation context is never authoritative',
484
+ writeBoundary: 'Aimlock guarded-write verifies the signed active coordination lease',
485
+ }
486
+ const handler = HANDLERS[operation]
487
+ if (!handler) coordinatorError('SWARM_COORD_OPERATION_UNSUPPORTED', `unsupported operation: ${operation}`)
488
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
489
+ coordinatorError('SWARM_COORD_INPUT_INVALID', 'input must be an object')
490
+ }
491
+ validateInputShape(input, OPERATION_SCHEMAS[operation])
492
+ return handler(repositoryRoot, input)
493
+ }
494
+
495
+ export {
496
+ LOCAL_SCHEMA,
497
+ OPERATION_SCHEMAS,
498
+ OPERATIONS,
499
+ executeCoordinatorOperation,
500
+ }