@shapeshift-labs/frontier-swarm 0.2.0 → 0.4.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/dist/index.js CHANGED
@@ -8,22 +8,65 @@ export const FRONTIER_SWARM_RUN_KIND = 'frontier.swarm.run';
8
8
  export const FRONTIER_SWARM_RUN_VERSION = 1;
9
9
  export const FRONTIER_SWARM_EVENT_KIND = 'frontier.swarm.event';
10
10
  export const FRONTIER_SWARM_EVENT_VERSION = 1;
11
+ export const FRONTIER_SWARM_EVENT_STREAM_KIND = 'frontier.swarm.event-stream';
12
+ export const FRONTIER_SWARM_EVENT_STREAM_VERSION = 1;
13
+ export const FRONTIER_SWARM_MAILBOX_KIND = 'frontier.swarm.mailbox';
14
+ export const FRONTIER_SWARM_MAILBOX_VERSION = 1;
11
15
  export const FRONTIER_SWARM_PROOF_KIND = 'frontier.swarm.proof';
12
16
  export const FRONTIER_SWARM_PROOF_VERSION = 1;
13
17
  export const FRONTIER_SWARM_SCHEDULE_KIND = 'frontier.swarm.schedule';
14
18
  export const FRONTIER_SWARM_SCHEDULE_VERSION = 1;
15
19
  export const FRONTIER_SWARM_LEASE_KIND = 'frontier.swarm.lease';
16
20
  export const FRONTIER_SWARM_LEASE_VERSION = 1;
21
+ export const FRONTIER_SWARM_QUEUE_SNAPSHOT_KIND = 'frontier.swarm.queue-snapshot';
22
+ export const FRONTIER_SWARM_QUEUE_SNAPSHOT_VERSION = 1;
23
+ export const FRONTIER_SWARM_RUN_CHECKPOINT_KIND = 'frontier.swarm.run-checkpoint';
24
+ export const FRONTIER_SWARM_RUN_CHECKPOINT_VERSION = 1;
17
25
  export const FRONTIER_SWARM_ARTIFACT_INDEX_KIND = 'frontier.swarm.artifact-index';
18
26
  export const FRONTIER_SWARM_ARTIFACT_INDEX_VERSION = 1;
19
27
  export const FRONTIER_SWARM_REVIEW_PLAN_KIND = 'frontier.swarm.review-plan';
20
28
  export const FRONTIER_SWARM_REVIEW_PLAN_VERSION = 1;
21
29
  export const FRONTIER_SWARM_MERGE_PLAN_KIND = 'frontier.swarm.merge-plan';
22
30
  export const FRONTIER_SWARM_MERGE_PLAN_VERSION = 1;
31
+ export const FRONTIER_SWARM_MERGE_BUNDLE_KIND = 'frontier.swarm.merge-bundle';
32
+ export const FRONTIER_SWARM_MERGE_BUNDLE_VERSION = 1;
33
+ export const FRONTIER_SWARM_QUEUE_OVERLAY_KIND = 'frontier.swarm.queue-overlay';
34
+ export const FRONTIER_SWARM_QUEUE_OVERLAY_VERSION = 1;
35
+ export const FRONTIER_SWARM_MERGE_INDEX_KIND = 'frontier.swarm.merge-index';
36
+ export const FRONTIER_SWARM_MERGE_INDEX_VERSION = 1;
37
+ export const FRONTIER_SWARM_HOTSPOT_REPORT_KIND = 'frontier.swarm.hotspot-report';
38
+ export const FRONTIER_SWARM_HOTSPOT_REPORT_VERSION = 1;
39
+ export const FRONTIER_SWARM_REVIEWER_LANE_PLAN_KIND = 'frontier.swarm.reviewer-lane-plan';
40
+ export const FRONTIER_SWARM_REVIEWER_LANE_PLAN_VERSION = 1;
41
+ export const FRONTIER_SWARM_RUN_STORE_SHARDS_KIND = 'frontier.swarm.run-store-shards';
42
+ export const FRONTIER_SWARM_RUN_STORE_SHARDS_VERSION = 1;
43
+ export const FRONTIER_SWARM_MERGE_ADMISSION_KIND = 'frontier.swarm.merge-admission';
44
+ export const FRONTIER_SWARM_MERGE_ADMISSION_VERSION = 1;
45
+ export const FRONTIER_SWARM_CONTEXT_PACK_KIND = 'frontier.swarm.context-pack';
46
+ export const FRONTIER_SWARM_CONTEXT_PACK_VERSION = 1;
47
+ export const FRONTIER_SWARM_ORACLE_CORPUS_KIND = 'frontier.swarm.oracle-corpus';
48
+ export const FRONTIER_SWARM_ORACLE_CORPUS_VERSION = 1;
49
+ export const FRONTIER_SWARM_LANE_PLAYBOOK_KIND = 'frontier.swarm.lane-playbook';
50
+ export const FRONTIER_SWARM_LANE_PLAYBOOK_VERSION = 1;
51
+ export const FRONTIER_SWARM_PATCH_STACK_PLAN_KIND = 'frontier.swarm.patch-stack-plan';
52
+ export const FRONTIER_SWARM_PATCH_STACK_PLAN_VERSION = 1;
23
53
  export const FRONTIER_SWARM_DEFAULT_CODEX_COMPUTE_ID = 'codex.gpt-5.5.xhigh';
24
54
  export const FRONTIER_SWARM_DEFAULT_MODEL = 'gpt-5.5';
25
55
  export const FRONTIER_SWARM_DEFAULT_REASONING_EFFORT = 'xhigh';
26
56
  const DEFAULT_COMPLETED_STATUSES = ['completed', 'verified', 'done', 'verified-local-harness'];
57
+ const DEFAULT_SWARM_EVENT_TYPES = [
58
+ 'swarm.started',
59
+ 'swarm.finished',
60
+ 'agent.scheduled',
61
+ 'agent.finished',
62
+ 'agent.handoff',
63
+ 'agent.blocked',
64
+ 'agent.ownership-request',
65
+ 'agent.evidence',
66
+ 'review.requested',
67
+ 'review.completed',
68
+ 'merge.proposed'
69
+ ];
27
70
  export function defineSwarmManifest(input = {}) {
28
71
  return createSwarmManifest(input);
29
72
  }
@@ -158,6 +201,41 @@ export function createSwarmPlan(manifestInput, taskInput, options = {}) {
158
201
  ...(toJsonObject(options.metadata) ? { metadata: toJsonObject(options.metadata) } : {})
159
202
  };
160
203
  }
204
+ export function createSwarmTaskSelection(manifestInput, taskInput, options = {}) {
205
+ const manifest = compileSwarm(manifestInput).manifest;
206
+ const tasks = normalizeTaskList(taskInput);
207
+ const lanes = new Set(options.lanes ?? []);
208
+ const layers = new Set(options.layers ?? []);
209
+ const statuses = new Set(options.statuses ?? []);
210
+ const workKinds = new Set(options.workKinds ?? []);
211
+ const selectors = (options.selectors ?? []).map((selector) => selector.toLowerCase());
212
+ const completed = new Set(manifest.policy.completedStatuses);
213
+ const limit = options.limit === undefined ? tasks.length : Math.max(0, Math.floor(options.limit));
214
+ const candidates = tasks
215
+ .filter((task) => !task.lane || manifest.lanes.some((lane) => lane.id === task.lane))
216
+ .filter((task) => lanes.size === 0 || (task.lane !== undefined && lanes.has(task.lane)))
217
+ .filter((task) => layers.size === 0 || taskLayer(manifest, task) !== undefined && layers.has(taskLayer(manifest, task)))
218
+ .filter((task) => statuses.size === 0 || statuses.has(task.status))
219
+ .filter((task) => workKinds.size === 0 || workKinds.has(task.workKind))
220
+ .filter((task) => options.includeCompleted || !completed.has(task.status))
221
+ .filter((task) => selectors.length === 0 || selectors.some((selector) => searchableTask(task).includes(selector)))
222
+ .map((task) => createSelectionEntry(manifest, task, options.priority))
223
+ .filter((entry) => options.includeOwnershipWarnings || entry.ownershipWarnings.length === 0)
224
+ .sort((left, right) => (left.selectionPriority - right.selectionPriority
225
+ || left.task.priority - right.task.priority
226
+ || left.task.id.localeCompare(right.task.id)));
227
+ const ordered = options.spreadLanes ? roundRobinSelectionByLane(candidates) : candidates;
228
+ const entries = ordered.slice(0, limit).map((entry, index) => {
229
+ if (!options.assignSelectionPriority)
230
+ return entry;
231
+ return { ...entry, task: { ...entry.task, priority: index } };
232
+ });
233
+ return {
234
+ tasks: entries.map((entry) => entry.task),
235
+ entries,
236
+ summary: summarizeTaskSelection(entries)
237
+ };
238
+ }
161
239
  export function createSwarmRun(input) {
162
240
  const results = (input.results ?? []).map(normalizeResult);
163
241
  const events = (input.events ?? []).map((event) => normalizeEvent({ ...event, runId: event.runId ?? input.id ?? input.plan.runId }));
@@ -182,6 +260,67 @@ export function recordSwarmEvent(runInput, eventInput) {
182
260
  run.events = run.events.concat(normalizeEvent({ ...eventInput, runId: eventInput.runId ?? run.id }));
183
261
  return run;
184
262
  }
263
+ export function createSwarmMailbox(input = {}) {
264
+ const scope = input.scope ?? (input.lane ? 'lane' : input.jobId ? 'job' : 'global');
265
+ const eventTypes = uniqueStrings(input.eventTypes ?? DEFAULT_SWARM_EVENT_TYPES);
266
+ return {
267
+ kind: FRONTIER_SWARM_MAILBOX_KIND,
268
+ version: FRONTIER_SWARM_MAILBOX_VERSION,
269
+ id: input.id ?? 'swarm-mailbox:' + stableHash([input.runId, scope, input.lane, input.jobId, input.path, eventTypes]),
270
+ ...(input.runId ? { runId: input.runId } : {}),
271
+ scope,
272
+ ...(input.lane ? { lane: input.lane } : {}),
273
+ ...(input.jobId ? { jobId: input.jobId } : {}),
274
+ ...(input.path ? { path: input.path } : {}),
275
+ eventTypes,
276
+ appendOnly: input.appendOnly ?? true,
277
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
278
+ };
279
+ }
280
+ export function createSwarmEventStream(input = {}) {
281
+ const laneIds = uniqueStrings((input.lanes ?? []).map(readLaneId));
282
+ const eventTypes = uniqueStrings(input.eventTypes ?? DEFAULT_SWARM_EVENT_TYPES);
283
+ const appendOnly = input.appendOnly ?? true;
284
+ const global = createSwarmMailbox({
285
+ runId: input.runId,
286
+ scope: 'global',
287
+ path: input.root ? joinPathParts(input.root, 'global.jsonl') : undefined,
288
+ eventTypes,
289
+ appendOnly
290
+ });
291
+ const lanes = Object.fromEntries(laneIds.map((lane) => [lane, createSwarmMailbox({
292
+ runId: input.runId,
293
+ scope: 'lane',
294
+ lane,
295
+ path: input.root ? joinPathParts(input.root, 'lanes', `${lane}.jsonl`) : undefined,
296
+ eventTypes,
297
+ appendOnly
298
+ })]));
299
+ return {
300
+ kind: FRONTIER_SWARM_EVENT_STREAM_KIND,
301
+ version: FRONTIER_SWARM_EVENT_STREAM_VERSION,
302
+ id: input.id ?? 'swarm-event-stream:' + stableHash([input.runId, input.root, laneIds, eventTypes]),
303
+ ...(input.runId ? { runId: input.runId } : {}),
304
+ ...(input.root ? { root: input.root } : {}),
305
+ appendOnly,
306
+ global,
307
+ lanes,
308
+ eventTypes,
309
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {}),
310
+ summary: {
311
+ mailboxCount: 1 + laneIds.length,
312
+ laneCount: laneIds.length,
313
+ eventTypeCount: eventTypes.length
314
+ }
315
+ };
316
+ }
317
+ export function routeSwarmEventToMailboxes(stream, eventInput) {
318
+ const event = isSwarmEvent(eventInput) ? eventInput : normalizeEvent(eventInput);
319
+ const mailboxes = [stream.global];
320
+ if (event.lane && stream.lanes[event.lane])
321
+ mailboxes.push(stream.lanes[event.lane]);
322
+ return mailboxes;
323
+ }
185
324
  export function completeSwarmJob(runInput, resultInput) {
186
325
  const run = cloneJsonValue(runInput);
187
326
  const result = normalizeResult(resultInput);
@@ -211,6 +350,672 @@ export function checkSwarmOwnership(job, changedPaths) {
211
350
  violations
212
351
  };
213
352
  }
353
+ export function resolveSwarmChangedRegions(job, changedPaths) {
354
+ const changed = uniqueStrings(changedPaths);
355
+ const regions = new Set(job.changedRegions);
356
+ for (const region of job.ownershipRegions) {
357
+ if (region.globs.some((glob) => changed.some((file) => matchesGlob(file, glob))))
358
+ regions.add(region.id);
359
+ for (const selector of region.selectors) {
360
+ if (changed.includes(selector))
361
+ regions.add(region.id);
362
+ }
363
+ }
364
+ return Array.from(regions).sort();
365
+ }
366
+ export function classifySwarmMergeReadiness(result) {
367
+ if (result.mergeReadiness)
368
+ return result.mergeReadiness;
369
+ if (result.status === 'blocked')
370
+ return 'blocked';
371
+ if (result.status === 'failed' || result.exitCode !== undefined && result.exitCode !== 0)
372
+ return 'rejected';
373
+ const changedPaths = result.changedPaths ?? [];
374
+ if (changedPaths.length === 0)
375
+ return 'discovery-only';
376
+ const ownershipViolations = result.ownershipViolations ?? [];
377
+ if (ownershipViolations.length)
378
+ return 'rejected';
379
+ const verification = result.verification ?? [];
380
+ const failedRequired = verification.some((entry) => entry.required !== false && entry.status !== 0);
381
+ if (failedRequired)
382
+ return 'patch-candidate';
383
+ return verification.length > 0 || result.status === 'verified' ? 'verified-patch' : 'patch-candidate';
384
+ }
385
+ export function classifySwarmMergeDisposition(result, input = {}) {
386
+ if (result.mergeDisposition)
387
+ return result.mergeDisposition;
388
+ if (input.staleAgainstHead)
389
+ return 'stale-against-head';
390
+ const readiness = classifySwarmMergeReadiness(result);
391
+ if (readiness === 'discovery-only')
392
+ return 'discovery-only';
393
+ if (readiness === 'blocked')
394
+ return 'blocked';
395
+ if (readiness === 'rejected')
396
+ return 'rejected';
397
+ if (readiness === 'verified-patch')
398
+ return 'auto-mergeable';
399
+ return 'needs-port';
400
+ }
401
+ export function createSwarmMergeBundle(input) {
402
+ const generatedAt = input.generatedAt ?? Date.now();
403
+ const result = isSwarmJobResult(input.result) ? cloneJsonValue(input.result) : normalizeResult(input.result);
404
+ const job = input.job;
405
+ const changedPaths = uniqueStrings(result.changedPaths);
406
+ const changedRegions = uniqueStrings([
407
+ ...result.changedRegions,
408
+ ...(job ? resolveSwarmChangedRegions(job, changedPaths) : [])
409
+ ]);
410
+ const evidencePaths = uniqueStrings([...(result.evidencePaths ?? []), ...(input.evidencePaths ?? [])]);
411
+ const queueItemIds = uniqueStrings([...(result.queueItemIds ?? []), ...(input.queueItemIds ?? []), ...(job ? [job.taskId] : [])]);
412
+ const disposition = input.disposition ?? classifySwarmMergeDisposition(result, { staleAgainstHead: input.staleAgainstHead });
413
+ const commandsPassed = result.verification.filter((entry) => entry.status === 0 || entry.required === false && entry.status === undefined);
414
+ const commandsFailed = result.verification.filter((entry) => entry.status !== undefined && entry.status !== 0 && entry.required !== false);
415
+ const ownedFilesTouched = job ? changedPaths.filter((file) => job.allowedWrites.some((glob) => matchesGlob(file, glob))) : changedPaths;
416
+ const reasons = mergeBundleReasons(result, disposition, input.staleAgainstHead ?? false);
417
+ return {
418
+ kind: FRONTIER_SWARM_MERGE_BUNDLE_KIND,
419
+ version: FRONTIER_SWARM_MERGE_BUNDLE_VERSION,
420
+ id: input.id ?? 'swarm-merge-bundle:' + stableHash([input.runId, input.planId, result.jobId, changedPaths, changedRegions, disposition, generatedAt]),
421
+ ...(input.runId ? { runId: input.runId } : {}),
422
+ ...(input.planId ? { planId: input.planId } : {}),
423
+ jobId: result.jobId,
424
+ ...(job ? { taskId: job.taskId, lane: job.lane, title: job.title } : {}),
425
+ generatedAt,
426
+ status: result.status,
427
+ mergeReadiness: result.mergeReadiness,
428
+ disposition,
429
+ riskLevel: input.riskLevel ?? result.riskLevel ?? inferMergeRisk(result, disposition),
430
+ autoMergeable: disposition === 'auto-mergeable' && reasons.length === 0,
431
+ changedPaths,
432
+ changedRegions,
433
+ ownedFilesTouched,
434
+ allowedWrites: job ? [...job.allowedWrites] : [],
435
+ ownershipViolations: [...result.ownershipViolations],
436
+ ...(input.patchPath ?? result.patchPath ? { patchPath: input.patchPath ?? result.patchPath } : {}),
437
+ ...(input.patchHash ? { patchHash: input.patchHash } : {}),
438
+ evidencePaths,
439
+ commandsPassed,
440
+ commandsFailed,
441
+ queueItemIds,
442
+ ...(input.branchName ? { branchName: input.branchName } : {}),
443
+ ...(input.commit ? { commit: input.commit } : {}),
444
+ staleAgainstHead: input.staleAgainstHead ?? false,
445
+ reasons,
446
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
447
+ };
448
+ }
449
+ export function createSwarmQueueOverlay(input = {}) {
450
+ const generatedAt = input.generatedAt ?? Date.now();
451
+ const entries = [];
452
+ for (const bundle of input.bundles ?? []) {
453
+ const status = queueOverlayStatusFromBundle(bundle);
454
+ const queueItemIds = bundle.queueItemIds.length ? bundle.queueItemIds : [bundle.taskId ?? bundle.jobId];
455
+ for (const queueItemId of queueItemIds) {
456
+ entries.push({
457
+ queueItemId,
458
+ jobId: bundle.jobId,
459
+ status,
460
+ mergeReadiness: bundle.mergeReadiness,
461
+ disposition: bundle.disposition,
462
+ riskLevel: bundle.riskLevel,
463
+ ...(bundle.patchPath ? { patchPath: bundle.patchPath } : {}),
464
+ evidencePaths: [...bundle.evidencePaths],
465
+ changedPaths: [...bundle.changedPaths],
466
+ changedRegions: [...bundle.changedRegions],
467
+ reasons: [...bundle.reasons],
468
+ generatedAt: bundle.generatedAt
469
+ });
470
+ }
471
+ }
472
+ for (const raw of input.results ?? []) {
473
+ const result = isSwarmJobResult(raw) ? cloneJsonValue(raw) : normalizeResult(raw);
474
+ const queueItemIds = result.queueItemIds.length ? result.queueItemIds : [result.jobId];
475
+ for (const queueItemId of queueItemIds) {
476
+ entries.push({
477
+ queueItemId,
478
+ jobId: result.jobId,
479
+ status: queueOverlayStatusFromResult(result),
480
+ mergeReadiness: result.mergeReadiness,
481
+ disposition: result.mergeDisposition,
482
+ riskLevel: result.riskLevel,
483
+ ...(result.patchPath ? { patchPath: result.patchPath } : {}),
484
+ evidencePaths: [...result.evidencePaths],
485
+ changedPaths: [...result.changedPaths],
486
+ changedRegions: [...result.changedRegions],
487
+ reasons: result.error ? [result.error] : [],
488
+ generatedAt
489
+ });
490
+ }
491
+ }
492
+ const byQueueItemId = groupOverlayEntries(entries);
493
+ return {
494
+ kind: FRONTIER_SWARM_QUEUE_OVERLAY_KIND,
495
+ version: FRONTIER_SWARM_QUEUE_OVERLAY_VERSION,
496
+ id: input.id ?? 'swarm-queue-overlay:' + stableHash([input.runId, entries, generatedAt]),
497
+ ...(input.runId ? { runId: input.runId } : {}),
498
+ generatedAt,
499
+ entries,
500
+ byQueueItemId,
501
+ summary: {
502
+ entryCount: entries.length,
503
+ queueItemCount: Object.keys(byQueueItemId).length,
504
+ readyToApplyCount: entries.filter((entry) => entry.status === 'ready-to-apply').length,
505
+ needsHumanPortCount: entries.filter((entry) => entry.status === 'needs-human-port').length,
506
+ failedEvidenceCount: entries.filter((entry) => entry.status === 'failed-evidence').length,
507
+ staleAgainstHeadCount: entries.filter((entry) => entry.status === 'stale-against-head').length,
508
+ discoveryOnlyCount: entries.filter((entry) => entry.status === 'discovery-only').length
509
+ },
510
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
511
+ };
512
+ }
513
+ export function deriveSwarmQueueStatus(input) {
514
+ const generatedAt = input.generatedAt ?? Date.now();
515
+ const latestByQueueItem = new Map();
516
+ for (const overlay of input.overlays ?? []) {
517
+ for (const entry of overlay.entries) {
518
+ const existing = latestByQueueItem.get(entry.queueItemId);
519
+ if (!existing || entry.generatedAt >= existing.generatedAt)
520
+ latestByQueueItem.set(entry.queueItemId, entry);
521
+ }
522
+ }
523
+ const jobs = input.snapshot.jobs.map((job) => {
524
+ const overlay = latestByQueueItem.get(job.taskId ?? job.jobId) ?? latestByQueueItem.get(job.jobId);
525
+ if (!overlay)
526
+ return cloneJsonValue(job);
527
+ return {
528
+ ...cloneJsonValue(job),
529
+ status: queueJobStatusFromOverlay(overlay),
530
+ lastError: overlay.status === 'failed-evidence' || overlay.status === 'stale-against-head' ? overlay.reasons.join(', ') : job.lastError,
531
+ metadata: toJsonObject({
532
+ ...(job.metadata ?? {}),
533
+ overlayStatus: overlay.status,
534
+ mergeDisposition: overlay.disposition,
535
+ mergeReadiness: overlay.mergeReadiness,
536
+ evidencePaths: overlay.evidencePaths
537
+ })
538
+ };
539
+ });
540
+ const byStatus = groupIds(jobs, (job) => job.status);
541
+ return {
542
+ generatedAt,
543
+ jobs,
544
+ byStatus,
545
+ summary: {
546
+ jobCount: jobs.length,
547
+ leaseCount: input.snapshot.leases.length,
548
+ readyCount: byStatus.ready?.length ?? 0,
549
+ leasedCount: byStatus.leased?.length ?? 0,
550
+ completedCount: byStatus.completed?.length ?? 0,
551
+ failedCount: byStatus.failed?.length ?? 0,
552
+ deadLetterCount: byStatus['dead-letter']?.length ?? 0
553
+ }
554
+ };
555
+ }
556
+ export function createSwarmMergeIndex(input) {
557
+ const generatedAt = input.generatedAt ?? Date.now();
558
+ const entries = input.bundles.map((bundle) => {
559
+ const patchStatus = input.patchStatuses?.[bundle.jobId] ?? (bundle.staleAgainstHead ? 'stale' : bundle.patchPath ? 'unknown' : 'missing');
560
+ const staleAgainstHead = bundle.staleAgainstHead || patchStatus === 'stale' || patchStatus === 'failed-check';
561
+ return {
562
+ jobId: bundle.jobId,
563
+ ...(bundle.taskId ? { taskId: bundle.taskId } : {}),
564
+ ...(bundle.lane ? { lane: bundle.lane } : {}),
565
+ ...(bundle.title ? { title: bundle.title } : {}),
566
+ status: bundle.status,
567
+ mergeReadiness: bundle.mergeReadiness,
568
+ disposition: staleAgainstHead ? 'stale-against-head' : bundle.disposition,
569
+ riskLevel: bundle.riskLevel,
570
+ patchStatus,
571
+ staleAgainstHead,
572
+ autoMergeable: bundle.autoMergeable && !staleAgainstHead,
573
+ changedPaths: [...bundle.changedPaths],
574
+ changedRegions: [...bundle.changedRegions],
575
+ conflictKeys: mergeIndexConflictKeys(bundle),
576
+ conflictingJobIds: [],
577
+ ownedFilesTouched: [...bundle.ownedFilesTouched],
578
+ ownershipViolations: [...bundle.ownershipViolations],
579
+ ...(bundle.patchPath ? { patchPath: bundle.patchPath } : {}),
580
+ ...(bundle.patchHash ? { patchHash: bundle.patchHash } : {}),
581
+ evidencePaths: [...bundle.evidencePaths],
582
+ queueItemIds: [...bundle.queueItemIds],
583
+ reasons: uniqueStrings([...bundle.reasons, ...(staleAgainstHead ? ['stale-against-head'] : [])]),
584
+ generatedAt: bundle.generatedAt
585
+ };
586
+ });
587
+ const conflicts = createMergeIndexConflicts(entries);
588
+ const conflictsByJob = new Map();
589
+ for (const conflict of conflicts) {
590
+ for (const jobId of conflict.jobIds) {
591
+ const set = conflictsByJob.get(jobId) ?? new Set();
592
+ for (const other of conflict.jobIds)
593
+ if (other !== jobId)
594
+ set.add(other);
595
+ conflictsByJob.set(jobId, set);
596
+ }
597
+ }
598
+ const indexed = entries.map((entry) => ({
599
+ ...entry,
600
+ conflictingJobIds: Array.from(conflictsByJob.get(entry.jobId) ?? []).sort()
601
+ }));
602
+ const byDisposition = groupJobIdsBy(indexed, (entry) => entry.disposition);
603
+ const byPath = groupJobIdsByMany(indexed, (entry) => entry.changedPaths);
604
+ const byRegion = groupJobIdsByMany(indexed, (entry) => entry.changedRegions);
605
+ return {
606
+ kind: FRONTIER_SWARM_MERGE_INDEX_KIND,
607
+ version: FRONTIER_SWARM_MERGE_INDEX_VERSION,
608
+ id: input.id ?? 'swarm-merge-index:' + stableHash([input.runId, input.planId, indexed, conflicts, generatedAt]),
609
+ ...(input.runId ? { runId: input.runId } : {}),
610
+ ...(input.planId ? { planId: input.planId } : {}),
611
+ generatedAt,
612
+ entries: indexed,
613
+ conflicts,
614
+ byDisposition,
615
+ byPath,
616
+ byRegion,
617
+ summary: {
618
+ entryCount: indexed.length,
619
+ readyToApplyCount: indexed.filter((entry) => entry.disposition === 'auto-mergeable' && entry.autoMergeable && !entry.conflictingJobIds.length).length,
620
+ needsHumanPortCount: indexed.filter((entry) => entry.disposition === 'needs-port').length,
621
+ failedEvidenceCount: indexed.filter((entry) => entry.disposition === 'rejected' || entry.disposition === 'blocked' || entry.ownershipViolations.length > 0).length,
622
+ staleAgainstHeadCount: indexed.filter((entry) => entry.staleAgainstHead || entry.disposition === 'stale-against-head').length,
623
+ discoveryOnlyCount: indexed.filter((entry) => entry.disposition === 'discovery-only').length,
624
+ conflictCount: conflicts.length,
625
+ conflictedJobCount: conflictsByJob.size
626
+ },
627
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
628
+ };
629
+ }
630
+ export function checkSwarmRegionOwnership(job, input = {}) {
631
+ const changedPaths = uniqueStrings(input.changedPaths ?? []);
632
+ const resolvedRegions = resolveSwarmChangedRegions(job, changedPaths);
633
+ const changedRegions = uniqueStrings([...(input.changedRegions ?? []), ...resolvedRegions]);
634
+ const ownedRegions = new Set(job.ownedRegions);
635
+ const regionViolations = changedRegions.filter((region) => !ownedRegions.has(region));
636
+ const classifiedPaths = new Set();
637
+ for (const region of job.ownershipRegions) {
638
+ for (const file of changedPaths) {
639
+ if (region.globs.some((glob) => matchesGlob(file, glob)))
640
+ classifiedPaths.add(file);
641
+ }
642
+ }
643
+ const unclassifiedChangedPaths = changedPaths.filter((file) => !classifiedPaths.has(file));
644
+ return {
645
+ ok: regionViolations.length === 0 && (job.ownershipRegions.length === 0 || unclassifiedChangedPaths.length === 0),
646
+ jobId: job.id,
647
+ changedPaths,
648
+ changedRegions,
649
+ ownedRegions: [...job.ownedRegions],
650
+ regionViolations,
651
+ unclassifiedChangedPaths
652
+ };
653
+ }
654
+ export function createSwarmHotspotReport(input = {}) {
655
+ const generatedAt = input.generatedAt ?? Date.now();
656
+ const threshold = Math.max(2, Math.floor(input.threshold ?? 3));
657
+ const byPath = new Map();
658
+ for (const bundle of input.bundles ?? []) {
659
+ for (const file of bundle.changedPaths) {
660
+ const current = byPath.get(file) ?? {
661
+ path: file,
662
+ touchCount: 0,
663
+ jobIds: [],
664
+ regions: [],
665
+ dispositions: [],
666
+ riskLevels: []
667
+ };
668
+ current.touchCount += 1;
669
+ current.jobIds = uniqueStrings([...current.jobIds, bundle.jobId]);
670
+ current.regions = uniqueStrings([...current.regions, ...bundle.changedRegions]);
671
+ current.dispositions = uniqueStrings([...current.dispositions, bundle.disposition]);
672
+ current.riskLevels = uniqueStrings([...current.riskLevels, bundle.riskLevel]);
673
+ byPath.set(file, current);
674
+ }
675
+ }
676
+ for (const raw of input.results ?? []) {
677
+ const result = isSwarmJobResult(raw) ? raw : normalizeResult(raw);
678
+ for (const file of result.changedPaths) {
679
+ const current = byPath.get(file) ?? {
680
+ path: file,
681
+ touchCount: 0,
682
+ jobIds: [],
683
+ regions: [],
684
+ dispositions: [],
685
+ riskLevels: []
686
+ };
687
+ current.touchCount += 1;
688
+ current.jobIds = uniqueStrings([...current.jobIds, result.jobId]);
689
+ current.regions = uniqueStrings([...current.regions, ...result.changedRegions]);
690
+ current.dispositions = uniqueStrings([...current.dispositions, result.mergeDisposition]);
691
+ current.riskLevels = uniqueStrings([...current.riskLevels, result.riskLevel]);
692
+ byPath.set(file, current);
693
+ }
694
+ }
695
+ const entries = Array.from(byPath.values()).sort((left, right) => right.touchCount - left.touchCount || left.path.localeCompare(right.path));
696
+ const recommendations = entries
697
+ .filter((entry) => entry.touchCount >= threshold || entry.regions.length > 1)
698
+ .map((entry) => ({
699
+ path: entry.path,
700
+ reason: entry.regions.length > 1 ? 'region-overlap' : 'hot-file',
701
+ suggestedModuleId: suggestedModuleId(entry.path),
702
+ suggestedOwnershipRegions: entry.regions.length ? entry.regions : [`${suggestedModuleId(entry.path)}.*`],
703
+ jobIds: [...entry.jobIds]
704
+ }));
705
+ return {
706
+ kind: FRONTIER_SWARM_HOTSPOT_REPORT_KIND,
707
+ version: FRONTIER_SWARM_HOTSPOT_REPORT_VERSION,
708
+ id: input.id ?? 'swarm-hotspot-report:' + stableHash([entries, threshold, generatedAt]),
709
+ generatedAt,
710
+ threshold,
711
+ entries,
712
+ recommendations,
713
+ summary: {
714
+ pathCount: entries.length,
715
+ hotspotCount: entries.filter((entry) => entry.touchCount >= threshold).length,
716
+ recommendationCount: recommendations.length
717
+ },
718
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
719
+ };
720
+ }
721
+ export function createSwarmReviewerLanePlan(input) {
722
+ const generatedAt = input.generatedAt ?? Date.now();
723
+ const reviewerLane = input.reviewerLane ?? 'review';
724
+ const reviewers = uniqueStrings(input.reviewers ?? []);
725
+ const deferralsByJob = new Map((input.admission?.deferred ?? []).map((entry) => [entry.jobId, entry.reasons]));
726
+ const candidates = input.index.entries.filter((entry) => input.includeAutoMergeable
727
+ || deferralsByJob.has(entry.jobId)
728
+ || entry.conflictingJobIds.length > 0
729
+ || entry.riskLevel === 'high'
730
+ || entry.disposition !== 'auto-mergeable'
731
+ || !entry.autoMergeable);
732
+ const assignments = candidates.map((entry) => ({
733
+ jobId: entry.jobId,
734
+ reviewers: selectReviewers(reviewers, reviewers.length ? 1 : 0, entry.jobId),
735
+ required: deferralsByJob.has(entry.jobId) || entry.conflictingJobIds.length > 0 || entry.riskLevel === 'high' || entry.disposition !== 'auto-mergeable',
736
+ reasons: uniqueStrings([...reviewerLaneReasons(entry), ...(deferralsByJob.get(entry.jobId) ?? [])])
737
+ }));
738
+ const tasks = candidates.map((entry) => ({
739
+ id: `review-${slug(entry.jobId)}`,
740
+ lane: reviewerLane,
741
+ kind: 'review',
742
+ title: `Review ${entry.title ?? entry.jobId}`,
743
+ objective: `Review swarm merge bundle ${entry.jobId}.`,
744
+ sourceRefs: entry.evidencePaths,
745
+ targetRefs: entry.changedPaths,
746
+ ownedRegions: entry.changedRegions,
747
+ acceptance: [
748
+ 'Review evidence, patch applicability, ownership, conflicts, and risk.',
749
+ `Merge disposition: ${entry.disposition}.`
750
+ ],
751
+ metadata: {
752
+ mergeJobId: entry.jobId,
753
+ conflictingJobIds: entry.conflictingJobIds,
754
+ reasons: uniqueStrings([...reviewerLaneReasons(entry), ...(deferralsByJob.get(entry.jobId) ?? [])])
755
+ }
756
+ }));
757
+ return {
758
+ kind: FRONTIER_SWARM_REVIEWER_LANE_PLAN_KIND,
759
+ version: FRONTIER_SWARM_REVIEWER_LANE_PLAN_VERSION,
760
+ id: input.id ?? 'swarm-reviewer-lane-plan:' + stableHash([input.index.id, assignments, generatedAt]),
761
+ mergeIndexId: input.index.id,
762
+ generatedAt,
763
+ reviewerLane,
764
+ assignments,
765
+ tasks,
766
+ summary: {
767
+ assignmentCount: assignments.length,
768
+ taskCount: tasks.length
769
+ },
770
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
771
+ };
772
+ }
773
+ export function createSwarmRunStoreShards(input = {}) {
774
+ const generatedAt = input.generatedAt ?? Date.now();
775
+ const root = input.root ?? 'agent-runs/shards';
776
+ const shardSize = Math.max(1, Math.floor(input.shardSize ?? 100));
777
+ const groupBy = input.groupBy ?? 'lane';
778
+ const jobs = input.run?.jobs ?? input.plan?.jobs ?? [];
779
+ const groups = new Map();
780
+ for (const job of jobs) {
781
+ const key = groupBy === 'none' ? 'all' : groupBy === 'hash' ? String(hashBucket(job.id, shardSize)) : job.lane;
782
+ groups.set(key, [...(groups.get(key) ?? []), job]);
783
+ }
784
+ const shards = [];
785
+ for (const [group, groupJobs] of Array.from(groups.entries()).sort((left, right) => left[0].localeCompare(right[0]))) {
786
+ for (let index = 0; index < groupJobs.length; index += shardSize) {
787
+ const slice = groupJobs.slice(index, index + shardSize);
788
+ const suffix = `${slug(group)}-${Math.floor(index / shardSize)}`;
789
+ const shardRoot = joinPathParts(root, suffix);
790
+ shards.push({
791
+ id: 'swarm-run-store-shard:' + stableHash([input.run?.id, input.plan?.id, group, index, slice.map((job) => job.id)]),
792
+ ...(groupBy === 'lane' ? { lane: group } : {}),
793
+ path: shardRoot,
794
+ eventPath: joinPathParts(shardRoot, 'events.jsonl'),
795
+ resultPath: joinPathParts(shardRoot, 'results.jsonl'),
796
+ checkpointPath: joinPathParts(shardRoot, 'checkpoint.json'),
797
+ jobIds: slice.map((job) => job.id)
798
+ });
799
+ }
800
+ }
801
+ return {
802
+ kind: FRONTIER_SWARM_RUN_STORE_SHARDS_KIND,
803
+ version: FRONTIER_SWARM_RUN_STORE_SHARDS_VERSION,
804
+ id: input.id ?? 'swarm-run-store-shards:' + stableHash([input.run?.id, input.plan?.id, root, shardSize, groupBy, shards, generatedAt]),
805
+ ...(input.run ? { runId: input.run.id } : {}),
806
+ ...(input.plan ? { planId: input.plan.id } : {}),
807
+ root,
808
+ generatedAt,
809
+ groupBy,
810
+ shardSize,
811
+ shards,
812
+ summary: {
813
+ shardCount: shards.length,
814
+ jobCount: jobs.length
815
+ },
816
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
817
+ };
818
+ }
819
+ export function createSwarmMergeAdmission(input) {
820
+ const generatedAt = input.generatedAt ?? Date.now();
821
+ const maxReady = Math.max(0, Math.floor(input.maxReady ?? input.index.entries.length));
822
+ const maxChangedPaths = input.maxChangedPaths === undefined ? undefined : Math.max(0, Math.floor(input.maxChangedPaths));
823
+ const maxChangedRegions = input.maxChangedRegions === undefined ? undefined : Math.max(0, Math.floor(input.maxChangedRegions));
824
+ const maxHighRisk = input.maxHighRisk === undefined ? undefined : Math.max(0, Math.floor(input.maxHighRisk));
825
+ const allowRisks = uniqueStrings(input.allowRisks ?? ['low', 'medium']);
826
+ const admitted = [];
827
+ const deferred = [];
828
+ const usedPaths = new Set();
829
+ const usedRegions = new Set();
830
+ let highRiskCount = 0;
831
+ for (const entry of input.index.entries) {
832
+ const reasons = [];
833
+ if (entry.disposition !== 'auto-mergeable' || !entry.autoMergeable)
834
+ reasons.push('not-auto-mergeable');
835
+ if (entry.staleAgainstHead)
836
+ reasons.push('stale-against-head');
837
+ if (entry.conflictingJobIds.length)
838
+ reasons.push('conflicting-changes');
839
+ if (!allowRisks.includes(entry.riskLevel))
840
+ reasons.push('risk-not-admitted');
841
+ if (admitted.length >= maxReady)
842
+ reasons.push('max-ready');
843
+ const nextPaths = new Set([...usedPaths, ...entry.changedPaths]);
844
+ const nextRegions = new Set([...usedRegions, ...entry.changedRegions]);
845
+ const nextHighRiskCount = highRiskCount + (entry.riskLevel === 'high' ? 1 : 0);
846
+ if (maxChangedPaths !== undefined && nextPaths.size > maxChangedPaths)
847
+ reasons.push('max-changed-paths');
848
+ if (maxChangedRegions !== undefined && nextRegions.size > maxChangedRegions)
849
+ reasons.push('max-changed-regions');
850
+ if (maxHighRisk !== undefined && nextHighRiskCount > maxHighRisk)
851
+ reasons.push('max-high-risk');
852
+ if (reasons.length) {
853
+ deferred.push({ jobId: entry.jobId, reasons: uniqueStrings(reasons) });
854
+ continue;
855
+ }
856
+ admitted.push(entry.jobId);
857
+ for (const file of entry.changedPaths)
858
+ usedPaths.add(file);
859
+ for (const region of entry.changedRegions)
860
+ usedRegions.add(region);
861
+ highRiskCount = nextHighRiskCount;
862
+ }
863
+ return {
864
+ kind: FRONTIER_SWARM_MERGE_ADMISSION_KIND,
865
+ version: FRONTIER_SWARM_MERGE_ADMISSION_VERSION,
866
+ id: input.id ?? 'swarm-merge-admission:' + stableHash([input.index.id, admitted, deferred, generatedAt]),
867
+ mergeIndexId: input.index.id,
868
+ generatedAt,
869
+ admitted,
870
+ deferred,
871
+ budget: {
872
+ maxReady,
873
+ ...(maxChangedPaths !== undefined ? { maxChangedPaths } : {}),
874
+ ...(maxChangedRegions !== undefined ? { maxChangedRegions } : {}),
875
+ ...(maxHighRisk !== undefined ? { maxHighRisk } : {}),
876
+ allowRisks
877
+ },
878
+ summary: {
879
+ admittedCount: admitted.length,
880
+ deferredCount: deferred.length,
881
+ changedPathCount: usedPaths.size,
882
+ changedRegionCount: usedRegions.size,
883
+ highRiskCount
884
+ },
885
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
886
+ };
887
+ }
888
+ export function createSwarmContextPack(input = {}) {
889
+ const generatedAt = input.generatedAt ?? Date.now();
890
+ const task = input.job?.task ?? (input.task ? isSwarmTask(input.task) ? input.task : normalizeTask(input.task) : undefined);
891
+ const files = uniqueStrings([
892
+ ...(input.files ?? []),
893
+ ...(input.job?.task.sourceRefs ?? []),
894
+ ...(input.job?.task.targetRefs ?? []),
895
+ ...(task?.sourceRefs ?? []),
896
+ ...(task?.targetRefs ?? [])
897
+ ]);
898
+ const apiMap = Object.fromEntries(Object.entries(input.apiMap ?? {}).map(([key, values]) => [key, uniqueStrings(values)]));
899
+ const commands = normalizeCommands([
900
+ ...(input.commands ?? []),
901
+ ...(input.oracleCommands ?? []),
902
+ ...(input.job?.verification ?? [])
903
+ ]);
904
+ const expectedEvidence = uniqueStrings([
905
+ ...(input.expectedEvidence ?? []),
906
+ ...(input.job?.evidencePrefix ? [joinPathParts(input.job.evidencePrefix, 'evidence.json')] : [])
907
+ ]);
908
+ return {
909
+ kind: FRONTIER_SWARM_CONTEXT_PACK_KIND,
910
+ version: FRONTIER_SWARM_CONTEXT_PACK_VERSION,
911
+ id: input.id ?? 'swarm-context-pack:' + stableHash([input.job?.id, task?.id, files, apiMap, generatedAt]),
912
+ ...(input.job ? { jobId: input.job.id } : {}),
913
+ ...(task ? { taskId: task.id } : {}),
914
+ ...(input.job?.lane ?? task?.lane ? { lane: input.job?.lane ?? task?.lane } : {}),
915
+ title: input.title ?? input.job?.title ?? task?.title ?? 'Swarm Context Pack',
916
+ generatedAt,
917
+ files,
918
+ apiMap,
919
+ knownFailures: uniqueStrings(input.knownFailures ?? []),
920
+ commands,
921
+ oracleCommands: commands,
922
+ ...(input.evidenceSchema !== undefined ? { evidenceSchema: toJsonValue(input.evidenceSchema) } : {}),
923
+ expectedEvidence,
924
+ exclusions: uniqueStrings(input.exclusions ?? []),
925
+ avoidInvestigating: uniqueStrings(input.avoidInvestigating ?? []),
926
+ playbookIds: uniqueStrings(input.playbookIds ?? []),
927
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
928
+ };
929
+ }
930
+ export function createSwarmOracleCorpus(input = {}) {
931
+ const generatedAt = input.generatedAt ?? Date.now();
932
+ const artifacts = (input.artifacts ?? []).map(normalizeOracleArtifact).sort((left, right) => left.id.localeCompare(right.id));
933
+ const byKind = groupArtifactIdsBy(artifacts, (artifact) => [artifact.kind]);
934
+ const byTag = groupArtifactIdsBy(artifacts, (artifact) => artifact.tags);
935
+ return {
936
+ kind: FRONTIER_SWARM_ORACLE_CORPUS_KIND,
937
+ version: FRONTIER_SWARM_ORACLE_CORPUS_VERSION,
938
+ id: input.id ?? 'swarm-oracle-corpus:' + stableHash([artifacts, generatedAt]),
939
+ title: input.title ?? titleFromId(input.id ?? 'oracle corpus'),
940
+ generatedAt,
941
+ artifacts,
942
+ byKind,
943
+ byTag,
944
+ summary: {
945
+ artifactCount: artifacts.length,
946
+ kindCount: Object.keys(byKind).length,
947
+ tagCount: Object.keys(byTag).length
948
+ },
949
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
950
+ };
951
+ }
952
+ export function createSwarmLanePlaybook(input) {
953
+ const generatedAt = input.generatedAt ?? Date.now();
954
+ const successful = (input.successfulBundles ?? []).filter((bundle) => bundle.status === 'completed' || bundle.status === 'verified' || bundle.autoMergeable);
955
+ const hotPaths = createSwarmHotspotReport({ bundles: successful, threshold: 2, generatedAt }).entries
956
+ .filter((entry) => entry.touchCount >= 2)
957
+ .map((entry) => entry.path);
958
+ return {
959
+ kind: FRONTIER_SWARM_LANE_PLAYBOOK_KIND,
960
+ version: FRONTIER_SWARM_LANE_PLAYBOOK_VERSION,
961
+ id: input.id ?? 'swarm-lane-playbook:' + stableHash([input.lane, successful.map((bundle) => bundle.jobId), input.notes, generatedAt]),
962
+ lane: normalizeId(input.lane, 'playbook lane'),
963
+ title: input.title ?? `${titleFromId(input.lane)} Playbook`,
964
+ generatedAt,
965
+ notes: uniqueStrings(input.notes ?? []),
966
+ commands: normalizeCommands(input.commands ?? []),
967
+ avoidInvestigating: uniqueStrings(input.avoidInvestigating ?? []),
968
+ evidencePatterns: uniqueStrings(input.evidencePatterns ?? successful.flatMap((bundle) => bundle.evidencePaths)),
969
+ successfulJobIds: uniqueStrings(successful.map((bundle) => bundle.jobId)),
970
+ hotPaths,
971
+ changedRegions: uniqueStrings(successful.flatMap((bundle) => bundle.changedRegions)),
972
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
973
+ };
974
+ }
975
+ export function createSwarmPatchStackPlan(input) {
976
+ const generatedAt = input.generatedAt ?? Date.now();
977
+ const maxStackSize = Math.max(1, Math.floor(input.maxStackSize ?? 8));
978
+ const groups = new Map();
979
+ for (const entry of input.index.entries) {
980
+ const key = patchStackKey(entry);
981
+ groups.set(key, [...(groups.get(key) ?? []), entry]);
982
+ }
983
+ const stacks = [];
984
+ for (const [key, entries] of Array.from(groups.entries()).sort((left, right) => left[0].localeCompare(right[0]))) {
985
+ const sorted = [...entries].sort((left, right) => riskRank(left.riskLevel) - riskRank(right.riskLevel) || left.jobId.localeCompare(right.jobId));
986
+ for (let index = 0; index < sorted.length; index += maxStackSize) {
987
+ const slice = sorted.slice(index, index + maxStackSize);
988
+ const jobIds = slice.map((entry) => entry.jobId);
989
+ const conflicts = input.index.conflicts.filter((conflict) => conflict.jobIds.some((jobId) => jobIds.includes(jobId)));
990
+ stacks.push({
991
+ id: 'swarm-patch-stack:' + stableHash([input.index.id, key, index, jobIds]),
992
+ title: titleFromId(key),
993
+ ...(slice[0]?.lane ? { lane: slice[0].lane } : {}),
994
+ jobIds,
995
+ changedPaths: uniqueStrings(slice.flatMap((entry) => entry.changedPaths)),
996
+ changedRegions: uniqueStrings(slice.flatMap((entry) => entry.changedRegions)),
997
+ riskLevels: uniqueStrings(slice.map((entry) => entry.riskLevel)),
998
+ dispositions: uniqueStrings(slice.map((entry) => entry.disposition)),
999
+ conflicts,
1000
+ gateHints: uniqueStrings(slice.flatMap((entry) => entry.evidencePaths.filter((file) => file.endsWith('.json') || file.endsWith('.jsonl'))))
1001
+ });
1002
+ }
1003
+ }
1004
+ return {
1005
+ kind: FRONTIER_SWARM_PATCH_STACK_PLAN_KIND,
1006
+ version: FRONTIER_SWARM_PATCH_STACK_PLAN_VERSION,
1007
+ id: input.id ?? 'swarm-patch-stack-plan:' + stableHash([input.index.id, stacks, generatedAt]),
1008
+ mergeIndexId: input.index.id,
1009
+ generatedAt,
1010
+ stacks,
1011
+ summary: {
1012
+ stackCount: stacks.length,
1013
+ jobCount: input.index.entries.length,
1014
+ conflictedStackCount: stacks.filter((stack) => stack.conflicts.length > 0).length
1015
+ },
1016
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
1017
+ };
1018
+ }
214
1019
  export function resolveSwarmCompute(manifestInput, taskInput) {
215
1020
  const compiled = compileSwarm(manifestInput);
216
1021
  const task = isSwarmTask(taskInput) ? taskInput : normalizeTask(taskInput);
@@ -248,7 +1053,14 @@ export function createSwarmSchedule(input) {
248
1053
  }
249
1054
  const runningJobs = (run?.jobs ?? plan.jobs)
250
1055
  .filter((job) => job.status === 'running')
251
- .map((job) => ({ jobId: job.id, lane: job.lane, compute: job.compute.id, concurrencyKey: job.concurrencyKey }));
1056
+ .map((job) => ({
1057
+ jobId: job.id,
1058
+ lane: job.lane,
1059
+ compute: job.compute.id,
1060
+ concurrencyKey: job.concurrencyKey,
1061
+ capabilities: [...job.capabilities],
1062
+ ...(job.resourceRequirements ? { resourceRequirements: cloneJsonValue(job.resourceRequirements) } : {})
1063
+ }));
252
1064
  const runningByLane = countBy(runningJobs.map((job) => job.lane));
253
1065
  const runningByKey = countBy(runningJobs.map((job) => job.concurrencyKey));
254
1066
  const runningByCompute = countBy(runningJobs.map((job) => job.compute));
@@ -330,6 +1142,65 @@ export function createSwarmLeases(input) {
330
1142
  status: 'active'
331
1143
  }));
332
1144
  }
1145
+ export function renewSwarmLease(input) {
1146
+ const now = input.now ?? Date.now();
1147
+ const leaseMs = Math.max(1, Math.floor(input.leaseMs ?? Math.max(1, input.lease.expiresAt - input.lease.leasedAt)));
1148
+ return {
1149
+ ...cloneJsonValue(input.lease),
1150
+ leasedAt: now,
1151
+ expiresAt: now + leaseMs,
1152
+ status: input.status ?? 'active'
1153
+ };
1154
+ }
1155
+ export function createSwarmQueueSnapshot(input) {
1156
+ const generatedAt = input.generatedAt ?? Date.now();
1157
+ const leases = [...(input.leases ?? [])].map((lease) => cloneJsonValue(lease));
1158
+ const jobs = (input.jobs ? input.jobs.map(normalizeQueueJob) : queueJobsFromPlan(input.plan, input.run, leases)).sort((left, right) => (left.priority - right.priority
1159
+ || left.jobId.localeCompare(right.jobId)));
1160
+ const byStatus = groupIds(jobs, (job) => job.status);
1161
+ const byLane = groupIds(jobs, (job) => job.lane ?? 'unassigned');
1162
+ return {
1163
+ kind: FRONTIER_SWARM_QUEUE_SNAPSHOT_KIND,
1164
+ version: FRONTIER_SWARM_QUEUE_SNAPSHOT_VERSION,
1165
+ id: input.id ?? 'swarm-queue-snapshot:' + stableHash([input.plan.id, input.run?.id, jobs, leases, generatedAt]),
1166
+ planId: input.plan.id,
1167
+ runId: input.run?.id ?? input.plan.runId,
1168
+ generatedAt,
1169
+ jobs,
1170
+ byStatus,
1171
+ byLane,
1172
+ leases,
1173
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {}),
1174
+ summary: {
1175
+ jobCount: jobs.length,
1176
+ leaseCount: leases.length,
1177
+ readyCount: byStatus.ready?.length ?? 0,
1178
+ leasedCount: byStatus.leased?.length ?? 0,
1179
+ completedCount: byStatus.completed?.length ?? 0,
1180
+ failedCount: byStatus.failed?.length ?? 0,
1181
+ deadLetterCount: byStatus['dead-letter']?.length ?? 0
1182
+ }
1183
+ };
1184
+ }
1185
+ export function createSwarmRunCheckpoint(input) {
1186
+ const run = 'kind' in input ? input : input.run;
1187
+ const sequence = 'kind' in input ? run.events.length + run.results.length : input.sequence ?? run.events.length + run.results.length;
1188
+ const savedAt = 'kind' in input ? Date.now() : input.savedAt ?? Date.now();
1189
+ return {
1190
+ kind: FRONTIER_SWARM_RUN_CHECKPOINT_KIND,
1191
+ version: FRONTIER_SWARM_RUN_CHECKPOINT_VERSION,
1192
+ id: 'swarm-run-checkpoint:' + stableHash([run.id, sequence, savedAt, run.summary]),
1193
+ runId: run.id,
1194
+ planId: run.planId,
1195
+ sequence,
1196
+ savedAt,
1197
+ status: run.status,
1198
+ eventCount: run.events.length,
1199
+ resultCount: run.results.length,
1200
+ hash: stableHash(run),
1201
+ ...(!('kind' in input) && toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
1202
+ };
1203
+ }
333
1204
  export function checkSwarmBudget(job, usageInput) {
334
1205
  const usage = normalizeUsage(usageInput);
335
1206
  const budget = job.budget;
@@ -524,7 +1395,8 @@ function createSwarmJobGraph(jobs) {
524
1395
  function normalizeScheduleLimits(manifest, options) {
525
1396
  const maxLaneConcurrency = {};
526
1397
  for (const lane of manifest.lanes) {
527
- const value = options.maxLaneConcurrency?.[lane.id] ?? lane.maxConcurrency ?? manifest.policy.defaultConcurrency;
1398
+ const browserMax = lane.resourceRequirements?.browser?.maxConcurrency;
1399
+ const value = options.maxLaneConcurrency?.[lane.id] ?? lane.maxConcurrency ?? browserMax ?? manifest.policy.defaultConcurrency;
528
1400
  maxLaneConcurrency[lane.id] = Math.max(1, Math.floor(value));
529
1401
  }
530
1402
  return {
@@ -550,7 +1422,9 @@ function scheduleJob(job, dependsOn = job.dependsOn) {
550
1422
  compute: job.compute.id,
551
1423
  concurrencyKey: job.concurrencyKey,
552
1424
  priority: job.priority,
553
- dependsOn: [...dependsOn]
1425
+ dependsOn: [...dependsOn],
1426
+ capabilities: [...job.capabilities],
1427
+ ...(job.resourceRequirements ? { resourceRequirements: cloneJsonValue(job.resourceRequirements) } : {})
554
1428
  };
555
1429
  }
556
1430
  function normalizeBudget(input = {}) {
@@ -616,28 +1490,188 @@ function selectReviewers(pool, required, salt) {
616
1490
  return sorted.slice(0, Math.min(required, sorted.length));
617
1491
  }
618
1492
  function conflictMap(results) {
619
- const byPath = new Map();
620
- for (const result of results) {
621
- for (const file of result.changedPaths) {
622
- const list = byPath.get(file) ?? [];
623
- list.push(result.jobId);
624
- byPath.set(file, list);
1493
+ const conflicts = new Map();
1494
+ for (let leftIndex = 0; leftIndex < results.length; leftIndex += 1) {
1495
+ for (let rightIndex = leftIndex + 1; rightIndex < results.length; rightIndex += 1) {
1496
+ const left = results[leftIndex];
1497
+ const right = results[rightIndex];
1498
+ if (!left || !right || pairConflictKeys(left, right).length === 0)
1499
+ continue;
1500
+ const leftConflicts = conflicts.get(left.jobId) ?? new Set();
1501
+ const rightConflicts = conflicts.get(right.jobId) ?? new Set();
1502
+ leftConflicts.add(right.jobId);
1503
+ rightConflicts.add(left.jobId);
1504
+ conflicts.set(left.jobId, leftConflicts);
1505
+ conflicts.set(right.jobId, rightConflicts);
625
1506
  }
626
1507
  }
627
- const conflicts = new Map();
628
- for (const jobIds of byPath.values()) {
629
- if (jobIds.length < 2)
630
- continue;
631
- for (const jobId of jobIds) {
632
- const set = conflicts.get(jobId) ?? new Set();
633
- for (const other of jobIds) {
634
- if (other !== jobId)
635
- set.add(other);
1508
+ return conflicts;
1509
+ }
1510
+ function queueOverlayStatusFromBundle(bundle) {
1511
+ if (bundle.staleAgainstHead || bundle.disposition === 'stale-against-head')
1512
+ return 'stale-against-head';
1513
+ if (bundle.disposition === 'rejected' || bundle.disposition === 'blocked' || bundle.status === 'failed' || bundle.commandsFailed.length > 0) {
1514
+ return 'failed-evidence';
1515
+ }
1516
+ if (bundle.disposition === 'auto-mergeable' && bundle.autoMergeable)
1517
+ return 'ready-to-apply';
1518
+ if (bundle.disposition === 'needs-port')
1519
+ return 'needs-human-port';
1520
+ if (bundle.disposition === 'discovery-only')
1521
+ return 'discovery-only';
1522
+ if (bundle.mergeReadiness === 'blocked')
1523
+ return 'blocked';
1524
+ if (bundle.mergeReadiness === 'rejected')
1525
+ return 'rejected';
1526
+ return 'unknown';
1527
+ }
1528
+ function queueOverlayStatusFromResult(result) {
1529
+ if (result.mergeDisposition === 'stale-against-head')
1530
+ return 'stale-against-head';
1531
+ if (result.status === 'failed' || result.exitCode !== undefined && result.exitCode !== 0 || result.ownershipViolations.length > 0)
1532
+ return 'failed-evidence';
1533
+ if (result.mergeDisposition === 'auto-mergeable')
1534
+ return 'ready-to-apply';
1535
+ if (result.mergeDisposition === 'needs-port')
1536
+ return 'needs-human-port';
1537
+ if (result.mergeDisposition === 'discovery-only')
1538
+ return 'discovery-only';
1539
+ if (result.status === 'blocked')
1540
+ return 'blocked';
1541
+ return 'unknown';
1542
+ }
1543
+ function queueJobStatusFromOverlay(entry) {
1544
+ if (entry.status === 'ready-to-apply' || entry.status === 'discovery-only')
1545
+ return 'completed';
1546
+ if (entry.status === 'needs-human-port')
1547
+ return 'blocked';
1548
+ if (entry.status === 'failed-evidence' || entry.status === 'rejected' || entry.status === 'stale-against-head')
1549
+ return 'failed';
1550
+ if (entry.status === 'blocked')
1551
+ return 'blocked';
1552
+ return 'completed';
1553
+ }
1554
+ function groupOverlayEntries(entries) {
1555
+ const out = {};
1556
+ for (const entry of entries)
1557
+ out[entry.queueItemId] = [...(out[entry.queueItemId] ?? []), entry];
1558
+ for (const key of Object.keys(out)) {
1559
+ out[key] = [...(out[key] ?? [])].sort((left, right) => right.generatedAt - left.generatedAt || left.jobId.localeCompare(right.jobId));
1560
+ }
1561
+ return out;
1562
+ }
1563
+ function mergeIndexConflictKeys(bundle) {
1564
+ return bundle.changedRegions.length
1565
+ ? bundle.changedRegions.map((region) => `region:${region}`).sort()
1566
+ : bundle.changedPaths.map((file) => `path:${file}`).sort();
1567
+ }
1568
+ function createMergeIndexConflicts(entries) {
1569
+ const conflicts = [];
1570
+ for (let leftIndex = 0; leftIndex < entries.length; leftIndex += 1) {
1571
+ for (let rightIndex = leftIndex + 1; rightIndex < entries.length; rightIndex += 1) {
1572
+ const left = entries[leftIndex];
1573
+ const right = entries[rightIndex];
1574
+ if (!left || !right)
1575
+ continue;
1576
+ const keys = pairConflictKeys(left, right);
1577
+ for (const key of keys) {
1578
+ const kind = key.startsWith('region:') ? 'region' : 'path';
1579
+ const value = key.slice(key.indexOf(':') + 1);
1580
+ conflicts.push({
1581
+ jobIds: [left.jobId, right.jobId].sort(),
1582
+ key,
1583
+ kind,
1584
+ ...(kind === 'region' ? { region: value } : { path: value })
1585
+ });
636
1586
  }
637
- conflicts.set(jobId, set);
638
1587
  }
639
1588
  }
640
- return conflicts;
1589
+ const deduped = new Map();
1590
+ for (const conflict of conflicts)
1591
+ deduped.set(`${conflict.key}:${conflict.jobIds.join(',')}`, conflict);
1592
+ return Array.from(deduped.values()).sort((left, right) => left.key.localeCompare(right.key) || left.jobIds.join(',').localeCompare(right.jobIds.join(',')));
1593
+ }
1594
+ function pairConflictKeys(left, right) {
1595
+ if (left.changedRegions.length > 0 && right.changedRegions.length > 0) {
1596
+ const rightRegions = new Set(right.changedRegions);
1597
+ return left.changedRegions.filter((region) => rightRegions.has(region)).map((region) => `region:${region}`).sort();
1598
+ }
1599
+ const rightPaths = new Set(right.changedPaths);
1600
+ return left.changedPaths.filter((file) => rightPaths.has(file)).map((file) => `path:${file}`).sort();
1601
+ }
1602
+ function groupJobIdsBy(items, key) {
1603
+ const out = {};
1604
+ for (const item of items)
1605
+ out[key(item)] = uniqueStrings([...(out[key(item)] ?? []), item.jobId]);
1606
+ return out;
1607
+ }
1608
+ function groupJobIdsByMany(items, key) {
1609
+ const out = {};
1610
+ for (const item of items) {
1611
+ for (const value of key(item))
1612
+ out[value] = uniqueStrings([...(out[value] ?? []), item.jobId]);
1613
+ }
1614
+ return out;
1615
+ }
1616
+ function suggestedModuleId(file) {
1617
+ const base = file.split('/').pop()?.replace(/\.[^.]+$/, '') ?? file;
1618
+ return slug(base).replace(/-/g, '.');
1619
+ }
1620
+ function reviewerLaneReasons(entry) {
1621
+ const reasons = [];
1622
+ if (entry.conflictingJobIds.length)
1623
+ reasons.push('conflicting-changes');
1624
+ if (entry.riskLevel === 'high')
1625
+ reasons.push('high-risk');
1626
+ if (entry.disposition !== 'auto-mergeable')
1627
+ reasons.push(entry.disposition);
1628
+ if (!entry.autoMergeable)
1629
+ reasons.push('not-auto-mergeable');
1630
+ if (entry.staleAgainstHead)
1631
+ reasons.push('stale-against-head');
1632
+ return uniqueStrings(reasons);
1633
+ }
1634
+ function hashBucket(value, buckets) {
1635
+ const hex = stableHash(value).split(':')[1] ?? '0';
1636
+ return parseInt(hex, 16) % Math.max(1, buckets);
1637
+ }
1638
+ function normalizeOracleArtifact(input) {
1639
+ return {
1640
+ id: normalizeId(input.id, 'oracle artifact id'),
1641
+ path: normalizeId(input.path, 'oracle artifact path'),
1642
+ kind: input.kind ?? 'oracle',
1643
+ ...(input.command ? { command: typeof input.command === 'string' ? normalizeCommands([input.command])[0] : normalizeCommands([input.command])[0] } : {}),
1644
+ ...(input.hash ? { hash: input.hash } : {}),
1645
+ ...(input.sourceRef ? { sourceRef: input.sourceRef } : {}),
1646
+ tags: uniqueStrings(input.tags ?? []),
1647
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
1648
+ };
1649
+ }
1650
+ function groupArtifactIdsBy(artifacts, key) {
1651
+ const out = {};
1652
+ for (const artifact of artifacts) {
1653
+ for (const value of key(artifact))
1654
+ out[value] = uniqueStrings([...(out[value] ?? []), artifact.id]);
1655
+ }
1656
+ return out;
1657
+ }
1658
+ function patchStackKey(entry) {
1659
+ const lane = entry.lane ?? 'unassigned';
1660
+ if (entry.changedRegions.length)
1661
+ return `${lane}:${entry.changedRegions[0]}`;
1662
+ const firstPath = entry.changedPaths[0] ?? 'evidence-only';
1663
+ return `${lane}:${firstPath.split('/').slice(0, 2).join('/') || firstPath}`;
1664
+ }
1665
+ function riskRank(risk) {
1666
+ if (risk === 'low')
1667
+ return 0;
1668
+ if (risk === 'medium')
1669
+ return 1;
1670
+ if (risk === 'unknown')
1671
+ return 2;
1672
+ if (risk === 'high')
1673
+ return 3;
1674
+ return 4;
641
1675
  }
642
1676
  function groupMergeReadyJobs(ready, results) {
643
1677
  const byJob = new Map(results.map((result) => [result.jobId, result]));
@@ -658,6 +1692,16 @@ function groupArtifacts(artifacts, key) {
658
1692
  }
659
1693
  return out;
660
1694
  }
1695
+ function groupIds(items, key) {
1696
+ const out = {};
1697
+ for (const item of items) {
1698
+ const group = key(item);
1699
+ out[group] = [...(out[group] ?? []), item.jobId];
1700
+ }
1701
+ for (const ids of Object.values(out))
1702
+ ids.sort();
1703
+ return out;
1704
+ }
661
1705
  function countBy(values) {
662
1706
  const out = {};
663
1707
  for (const value of values)
@@ -733,6 +1777,9 @@ function normalizeLane(input) {
733
1777
  allowedWrites,
734
1778
  sharedReadOnly: uniqueStrings(input.sharedReadOnly ?? []),
735
1779
  neverEdit: uniqueStrings(input.neverEdit ?? []),
1780
+ ownershipRegions: normalizeOwnershipRegions(input.ownershipRegions ?? []),
1781
+ capabilities: uniqueStrings(input.capabilities ?? []),
1782
+ ...(input.resourceRequirements ? { resourceRequirements: normalizeResourceRequirements(input.resourceRequirements) } : {}),
736
1783
  ...(input.worktreePath ? { worktreePath: input.worktreePath } : {}),
737
1784
  ...(input.evidencePrefix || input.evidenceOutDirPrefix ? { evidencePrefix: input.evidencePrefix ?? input.evidenceOutDirPrefix } : {}),
738
1785
  concurrencyKey: input.concurrencyKey ?? input.id,
@@ -742,6 +1789,68 @@ function normalizeLane(input) {
742
1789
  ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
743
1790
  };
744
1791
  }
1792
+ function normalizeOwnershipRegions(input = []) {
1793
+ return input.map((region) => {
1794
+ const globs = uniqueStrings([...(region.globs ?? []), ...(region.paths ?? [])]);
1795
+ return {
1796
+ id: normalizeId(region.id, 'ownership region id'),
1797
+ title: region.title ?? titleFromId(region.id),
1798
+ ...(region.description ? { description: region.description } : {}),
1799
+ globs,
1800
+ selectors: uniqueStrings(region.selectors ?? []),
1801
+ ...(region.owner ? { owner: region.owner } : {}),
1802
+ ...(toJsonObject(region.metadata) ? { metadata: toJsonObject(region.metadata) } : {})
1803
+ };
1804
+ });
1805
+ }
1806
+ function mergeOwnershipRegions(laneRegions, taskRegions) {
1807
+ const byId = new Map();
1808
+ for (const region of laneRegions)
1809
+ byId.set(region.id, cloneJsonValue(region));
1810
+ for (const region of taskRegions)
1811
+ byId.set(region.id, cloneJsonValue(region));
1812
+ return Array.from(byId.values()).sort((left, right) => left.id.localeCompare(right.id));
1813
+ }
1814
+ function normalizeResourceRequirements(input = {}) {
1815
+ const resources = {};
1816
+ for (const [key, value] of Object.entries(input.resources ?? {})) {
1817
+ if (Number.isFinite(value) && value > 0)
1818
+ resources[key] = value;
1819
+ }
1820
+ return {
1821
+ capabilities: uniqueStrings(input.capabilities ?? []),
1822
+ resources,
1823
+ ...(input.browser ? { browser: normalizeBrowserResource(input.browser) } : {}),
1824
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
1825
+ };
1826
+ }
1827
+ function normalizeBrowserResource(input) {
1828
+ return {
1829
+ required: input.required ?? true,
1830
+ portPool: uniqueStrings((input.portPool ?? []).map((port) => String(port))),
1831
+ ...(input.profileDir ? { profileDir: input.profileDir } : {}),
1832
+ ...(input.profileDirPrefix ? { profileDirPrefix: input.profileDirPrefix } : {}),
1833
+ ...(positiveNumber(input.maxConcurrency) ? { maxConcurrency: Math.floor(input.maxConcurrency) } : {}),
1834
+ ...(input.headless !== undefined ? { headless: input.headless } : {}),
1835
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
1836
+ };
1837
+ }
1838
+ function mergeResourceRequirements(lane, task, extraCapabilities = []) {
1839
+ if (!lane && !task && extraCapabilities.length === 0)
1840
+ return undefined;
1841
+ const capabilities = uniqueStrings([...(lane?.capabilities ?? []), ...(task?.capabilities ?? []), ...extraCapabilities]);
1842
+ const resources = { ...(lane?.resources ?? {}) };
1843
+ for (const [key, value] of Object.entries(task?.resources ?? {}))
1844
+ resources[key] = Math.max(resources[key] ?? 0, value);
1845
+ const browser = task?.browser ?? lane?.browser;
1846
+ const metadata = toJsonObject({ ...(lane?.metadata ?? {}), ...(task?.metadata ?? {}) });
1847
+ return {
1848
+ capabilities,
1849
+ resources,
1850
+ ...(browser ? { browser } : {}),
1851
+ ...(metadata && Object.keys(metadata).length ? { metadata } : {})
1852
+ };
1853
+ }
745
1854
  function normalizePolicy(input, defaultCompute) {
746
1855
  return {
747
1856
  mode: input?.mode ?? 'hard-file-ownership',
@@ -778,6 +1887,11 @@ function normalizeTask(input) {
778
1887
  sourceRefs: uniqueStrings(input.sourceRefs ?? []),
779
1888
  targetRefs,
780
1889
  allowedWrites: uniqueStrings([...(input.allowedWrites ?? []), ...targetRefs]),
1890
+ ownershipRegions: normalizeOwnershipRegions(input.ownershipRegions ?? []),
1891
+ ownedRegions: uniqueStrings(input.ownedRegions ?? []),
1892
+ changedRegions: uniqueStrings(input.changedRegions ?? []),
1893
+ capabilities: uniqueStrings(input.capabilities ?? []),
1894
+ ...(input.resourceRequirements ? { resourceRequirements: normalizeResourceRequirements(input.resourceRequirements) } : {}),
781
1895
  acceptance: normalizeAcceptance(input),
782
1896
  verification: normalizeCommands(input.verification ?? []),
783
1897
  ...(input.evidenceCommand ? { evidenceCommand: input.evidenceCommand } : {}),
@@ -827,6 +1941,55 @@ function selectSwarmTasks(manifest, tasks, options) {
827
1941
  .sort((left, right) => left.priority - right.priority || left.id.localeCompare(right.id))
828
1942
  .slice(0, limit);
829
1943
  }
1944
+ function createSelectionEntry(manifest, task, priority) {
1945
+ const lane = task.lane ? manifest.lanes.find((entry) => entry.id === task.lane) : undefined;
1946
+ return {
1947
+ task,
1948
+ ...(lane ? { lane } : {}),
1949
+ ownershipWarnings: selectionOwnershipWarnings(task, lane),
1950
+ selectionPriority: selectionPriority(task, priority)
1951
+ };
1952
+ }
1953
+ function selectionOwnershipWarnings(task, lane) {
1954
+ if (!lane || lane.allowedWrites.length === 0)
1955
+ return [];
1956
+ return task.targetRefs
1957
+ .filter((file) => !lane.allowedWrites.some((glob) => matchesGlob(file, glob)))
1958
+ .map((file) => `${file} is outside allowed write globs for ${lane.id}`);
1959
+ }
1960
+ function selectionPriority(task, input) {
1961
+ const statusRanks = input?.statuses ?? {};
1962
+ const workKindRanks = input?.workKinds ?? {};
1963
+ const statusRank = statusRanks[task.status] ?? input?.defaultStatusRank ?? 100;
1964
+ const workKindRank = workKindRanks[task.workKind] ?? input?.defaultWorkKindRank ?? 100;
1965
+ const statusWeight = input?.statusWeight ?? 1000;
1966
+ const workKindWeight = input?.workKindWeight ?? 1;
1967
+ return statusRank * statusWeight + workKindRank * workKindWeight;
1968
+ }
1969
+ function roundRobinSelectionByLane(entries) {
1970
+ const groups = new Map();
1971
+ for (const entry of entries)
1972
+ groups.set(entry.task.lane ?? 'unassigned', [...(groups.get(entry.task.lane ?? 'unassigned') ?? []), entry]);
1973
+ const selected = [];
1974
+ while (Array.from(groups.values()).some((group) => group.length > 0)) {
1975
+ for (const group of groups.values()) {
1976
+ const next = group.shift();
1977
+ if (next)
1978
+ selected.push(next);
1979
+ }
1980
+ }
1981
+ return selected;
1982
+ }
1983
+ function summarizeTaskSelection(entries) {
1984
+ return entries.reduce((summary, entry) => {
1985
+ const lane = entry.task.lane ?? 'unassigned';
1986
+ summary.total += 1;
1987
+ summary.byLane[lane] = (summary.byLane[lane] ?? 0) + 1;
1988
+ summary.byWorkKind[entry.task.workKind] = (summary.byWorkKind[entry.task.workKind] ?? 0) + 1;
1989
+ summary.ownershipWarningCount += entry.ownershipWarnings.length;
1990
+ return summary;
1991
+ }, { total: 0, byLane: {}, byWorkKind: {}, ownershipWarningCount: 0 });
1992
+ }
830
1993
  function createJob(compiled, task, options) {
831
1994
  const lane = task.lane ? compiled.lanesById.get(task.lane) : undefined;
832
1995
  const layer = task.layer ?? lane?.layer ?? compiled.manifest.policy.defaultLayer;
@@ -842,6 +2005,10 @@ function createJob(compiled, task, options) {
842
2005
  const ownershipWarnings = task.targetRefs
843
2006
  .filter((file) => allowedWrites.length > 0 && !allowedWrites.some((glob) => matchesGlob(file, glob)))
844
2007
  .map((file) => `${file} is outside allowed write globs for ${lane?.id ?? 'unassigned'}`);
2008
+ const capabilities = uniqueStrings([...(lane?.capabilities ?? []), ...task.capabilities]);
2009
+ const resourceRequirements = mergeResourceRequirements(lane?.resourceRequirements, task.resourceRequirements, capabilities);
2010
+ const ownershipRegions = mergeOwnershipRegions(lane?.ownershipRegions ?? [], task.ownershipRegions);
2011
+ const ownedRegions = uniqueStrings([...task.ownedRegions, ...ownershipRegions.map((region) => region.id)]);
845
2012
  return {
846
2013
  id: `${lane?.id ?? 'unassigned'}-${slug(task.id)}`,
847
2014
  taskId: task.id,
@@ -855,6 +2022,11 @@ function createJob(compiled, task, options) {
855
2022
  allowedWrites,
856
2023
  sharedReadOnly: uniqueStrings([...(compiled.manifest.policy.sharedReadOnly ?? []), ...(lane?.sharedReadOnly ?? [])]),
857
2024
  neverEdit: uniqueStrings([...(compiled.manifest.policy.neverEditWithoutParent ?? []), ...(lane?.neverEdit ?? [])]),
2025
+ ownershipRegions,
2026
+ ownedRegions,
2027
+ changedRegions: uniqueStrings(task.changedRegions),
2028
+ capabilities,
2029
+ ...(resourceRequirements ? { resourceRequirements } : {}),
858
2030
  ...(lane?.worktreePath ? { worktreePath: lane.worktreePath } : {}),
859
2031
  ...(evidencePrefix ? { evidencePrefix } : {}),
860
2032
  concurrencyKey: task.concurrencyKey ?? lane?.concurrencyKey ?? task.lane ?? compute.id,
@@ -949,26 +2121,121 @@ function normalizeEvent(input) {
949
2121
  ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
950
2122
  };
951
2123
  }
2124
+ function normalizeQueueJob(input) {
2125
+ return {
2126
+ jobId: input.jobId,
2127
+ ...(input.taskId ? { taskId: input.taskId } : {}),
2128
+ ...(input.runId ? { runId: input.runId } : {}),
2129
+ status: input.status ?? 'ready',
2130
+ ...(input.lane ? { lane: input.lane } : {}),
2131
+ ...(input.compute ? { compute: input.compute } : {}),
2132
+ ...(input.concurrencyKey ? { concurrencyKey: input.concurrencyKey } : {}),
2133
+ priority: input.priority ?? 100,
2134
+ attempts: Math.max(0, Math.floor(input.attempts ?? 0)),
2135
+ maxAttempts: Math.max(1, Math.floor(input.maxAttempts ?? 1)),
2136
+ ...(input.availableAt !== undefined ? { availableAt: input.availableAt } : {}),
2137
+ ...(input.lease ? { lease: cloneJsonValue(input.lease) } : {}),
2138
+ ...(input.lastError ? { lastError: input.lastError } : {}),
2139
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
2140
+ };
2141
+ }
2142
+ function queueJobsFromPlan(plan, run, leases) {
2143
+ const resultsByJob = new Map((run?.results ?? []).map((result) => [result.jobId, result]));
2144
+ const activeLeases = new Map(leases.filter((lease) => lease.status === 'active').map((lease) => [lease.jobId, lease]));
2145
+ return plan.jobs.map((job) => {
2146
+ const result = resultsByJob.get(job.id);
2147
+ const lease = activeLeases.get(job.id);
2148
+ const failed = result?.status === 'failed' || result?.exitCode !== undefined && result.exitCode !== 0;
2149
+ const completed = result?.status === 'completed' || result?.status === 'verified';
2150
+ const status = completed
2151
+ ? 'completed'
2152
+ : failed
2153
+ ? 'failed'
2154
+ : lease
2155
+ ? 'leased'
2156
+ : job.status === 'running'
2157
+ ? 'running'
2158
+ : 'ready';
2159
+ return normalizeQueueJob({
2160
+ jobId: job.id,
2161
+ taskId: job.taskId,
2162
+ runId: run?.id ?? plan.runId,
2163
+ status,
2164
+ lane: job.lane,
2165
+ compute: job.compute.id,
2166
+ concurrencyKey: job.concurrencyKey,
2167
+ priority: job.priority,
2168
+ attempts: result?.metadata && typeof result.metadata.attempts === 'number' ? result.metadata.attempts : undefined,
2169
+ maxAttempts: job.budget?.maxRetries !== undefined ? job.budget.maxRetries + 1 : 1,
2170
+ lease,
2171
+ lastError: result?.error
2172
+ });
2173
+ });
2174
+ }
952
2175
  function normalizeResult(input) {
953
2176
  const startedAt = input.startedAt;
954
2177
  const finishedAt = input.finishedAt;
2178
+ const status = input.status ?? (input.exitCode === 0 || input.exitCode === undefined ? 'completed' : 'failed');
955
2179
  return {
956
2180
  jobId: input.jobId,
957
- status: input.status ?? (input.exitCode === 0 || input.exitCode === undefined ? 'completed' : 'failed'),
2181
+ status,
2182
+ mergeReadiness: classifySwarmMergeReadiness({ ...input, status }),
958
2183
  ...(startedAt !== undefined ? { startedAt } : {}),
959
2184
  ...(finishedAt !== undefined ? { finishedAt } : {}),
960
2185
  ...(startedAt !== undefined && finishedAt !== undefined ? { durationMs: Math.max(0, finishedAt - startedAt) } : {}),
961
2186
  ...(input.exitCode !== undefined ? { exitCode: input.exitCode } : {}),
962
2187
  ...(input.signal ? { signal: input.signal } : {}),
963
2188
  changedPaths: uniqueStrings(input.changedPaths ?? []),
2189
+ changedRegions: uniqueStrings(input.changedRegions ?? []),
964
2190
  ownershipViolations: uniqueStrings(input.ownershipViolations ?? []),
965
2191
  evidencePaths: uniqueStrings(input.evidencePaths ?? []),
2192
+ ...(input.patchPath ? { patchPath: input.patchPath } : {}),
2193
+ queueItemIds: uniqueStrings(input.queueItemIds ?? []),
2194
+ riskLevel: input.riskLevel ?? 'unknown',
2195
+ mergeDisposition: input.mergeDisposition ?? classifySwarmMergeDisposition({ ...input, status }),
966
2196
  verification: (input.verification ?? []).map(normalizeVerificationResult),
967
2197
  ...(input.lastMessage ? { lastMessage: input.lastMessage } : {}),
968
2198
  ...(input.error !== undefined ? { error: stringifyError(input.error) } : {}),
969
2199
  ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
970
2200
  };
971
2201
  }
2202
+ function isSwarmJobResult(value) {
2203
+ return Array.isArray(value.changedPaths)
2204
+ && Array.isArray(value.changedRegions)
2205
+ && Array.isArray(value.verification)
2206
+ && Array.isArray(value.queueItemIds)
2207
+ && typeof value.riskLevel === 'string'
2208
+ && typeof value.mergeDisposition === 'string';
2209
+ }
2210
+ function mergeBundleReasons(result, disposition, staleAgainstHead) {
2211
+ const reasons = [];
2212
+ if (staleAgainstHead)
2213
+ reasons.push('stale-against-head');
2214
+ if (result.status === 'blocked')
2215
+ reasons.push('blocked');
2216
+ if (result.status === 'failed')
2217
+ reasons.push('failed');
2218
+ if (result.ownershipViolations.length)
2219
+ reasons.push('ownership-violations');
2220
+ if (result.verification.some((entry) => entry.required !== false && entry.status !== 0))
2221
+ reasons.push('failed-verification');
2222
+ if (disposition === 'needs-port')
2223
+ reasons.push('needs-human-port');
2224
+ if (disposition === 'rejected')
2225
+ reasons.push('rejected');
2226
+ return uniqueStrings(reasons);
2227
+ }
2228
+ function inferMergeRisk(result, disposition) {
2229
+ if (disposition === 'discovery-only')
2230
+ return 'low';
2231
+ if (disposition === 'rejected' || disposition === 'blocked' || disposition === 'stale-against-head')
2232
+ return 'high';
2233
+ if (result.changedPaths.length <= 2 && result.ownershipViolations.length === 0)
2234
+ return 'low';
2235
+ if (result.changedPaths.length <= 8)
2236
+ return 'medium';
2237
+ return 'high';
2238
+ }
972
2239
  function normalizeVerificationResult(input) {
973
2240
  return {
974
2241
  name: input.name ?? ((input.command ?? []).join(' ') || 'verification'),
@@ -1044,6 +2311,20 @@ function isSwarmManifest(value) {
1044
2311
  function isSwarmTask(value) {
1045
2312
  return !!value && typeof value === 'object' && value.kind === FRONTIER_SWARM_TASK_KIND;
1046
2313
  }
2314
+ function isSwarmEvent(value) {
2315
+ return !!value && typeof value === 'object' && value.kind === FRONTIER_SWARM_EVENT_KIND;
2316
+ }
2317
+ function readLaneId(value) {
2318
+ return typeof value === 'string' ? value : value.id;
2319
+ }
2320
+ function joinPathParts(...parts) {
2321
+ const first = parts[0] ? String(parts[0]) : '';
2322
+ const prefix = first.startsWith('/') ? '/' : '';
2323
+ return prefix + parts
2324
+ .map((part, index) => String(part).replace(index === 0 ? /\/+$/g : /^\/+|\/+$/g, ''))
2325
+ .filter(Boolean)
2326
+ .join('/');
2327
+ }
1047
2328
  function normalizeId(value, label) {
1048
2329
  const id = String(value || '').trim();
1049
2330
  if (!id)