@shapeshift-labs/frontier-swarm 0.5.21 → 0.5.22

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
@@ -1,3 +1,4 @@
1
+ import { createRunDashboardSnapshot, createRunEdgeEvent, createRunEvent, createRunNodeEvent, defineRunArtifact, defineRunAttempt, defineRunDecision, defineRunEvidence, defineRunLane, defineRunPatch, defineRunTask, defineRunVerification, linkRunNodes, replayRunEvents } from '@shapeshift-labs/frontier-run';
1
2
  export const FRONTIER_SWARM_MANIFEST_KIND = 'frontier.swarm.manifest';
2
3
  export const FRONTIER_SWARM_MANIFEST_VERSION = 1;
3
4
  export const FRONTIER_SWARM_TASK_KIND = 'frontier.swarm.task';
@@ -10,6 +11,22 @@ export const FRONTIER_SWARM_EVENT_KIND = 'frontier.swarm.event';
10
11
  export const FRONTIER_SWARM_EVENT_VERSION = 1;
11
12
  export const FRONTIER_SWARM_EVENT_STREAM_KIND = 'frontier.swarm.event-stream';
12
13
  export const FRONTIER_SWARM_EVENT_STREAM_VERSION = 1;
14
+ export const FRONTIER_SWARM_RUN_GRAPH_KIND = 'frontier.swarm.run-graph';
15
+ export const FRONTIER_SWARM_RUN_GRAPH_VERSION = 1;
16
+ export const FRONTIER_SWARM_GRAPH_SNAPSHOT_KIND = 'frontier.swarm.graph-snapshot';
17
+ export const FRONTIER_SWARM_GRAPH_SNAPSHOT_VERSION = 1;
18
+ export const FRONTIER_SWARM_RUN_GRAPH_CHUNK_KIND = 'frontier.swarm.run-graph-chunk';
19
+ export const FRONTIER_SWARM_RUN_GRAPH_CHUNK_VERSION = 1;
20
+ export const FRONTIER_SWARM_GATE_RECORD_KIND = 'frontier.swarm.gate-record';
21
+ export const FRONTIER_SWARM_GATE_RECORD_VERSION = 1;
22
+ export const FRONTIER_SWARM_EVIDENCE_RECORD_KIND = 'frontier.swarm.evidence-record';
23
+ export const FRONTIER_SWARM_EVIDENCE_RECORD_VERSION = 1;
24
+ export const FRONTIER_SWARM_PATCH_EVENT_KIND = 'frontier.swarm.patch-event';
25
+ export const FRONTIER_SWARM_PATCH_EVENT_VERSION = 1;
26
+ export const FRONTIER_SWARM_REPLAY_RECORD_KIND = 'frontier.swarm.replay-record';
27
+ export const FRONTIER_SWARM_REPLAY_RECORD_VERSION = 1;
28
+ export const FRONTIER_SWARM_IMPROVEMENT_LOOP_KIND = 'frontier.swarm.improvement-loop';
29
+ export const FRONTIER_SWARM_IMPROVEMENT_LOOP_VERSION = 1;
13
30
  export const FRONTIER_SWARM_MAILBOX_KIND = 'frontier.swarm.mailbox';
14
31
  export const FRONTIER_SWARM_MAILBOX_VERSION = 1;
15
32
  export const FRONTIER_SWARM_PROOF_KIND = 'frontier.swarm.proof';
@@ -30,6 +47,10 @@ export const FRONTIER_SWARM_MERGE_PLAN_KIND = 'frontier.swarm.merge-plan';
30
47
  export const FRONTIER_SWARM_MERGE_PLAN_VERSION = 1;
31
48
  export const FRONTIER_SWARM_MERGE_BUNDLE_KIND = 'frontier.swarm.merge-bundle';
32
49
  export const FRONTIER_SWARM_MERGE_BUNDLE_VERSION = 1;
50
+ export const FRONTIER_SWARM_SEMANTIC_CHANGE_KIND = 'frontier.swarm.semantic-change';
51
+ export const FRONTIER_SWARM_SEMANTIC_CHANGE_VERSION = 1;
52
+ export const FRONTIER_SWARM_MERGE_CANDIDATE_KIND = 'frontier.swarm.merge-candidate';
53
+ export const FRONTIER_SWARM_MERGE_CANDIDATE_VERSION = 1;
33
54
  export const FRONTIER_SWARM_QUEUE_OVERLAY_KIND = 'frontier.swarm.queue-overlay';
34
55
  export const FRONTIER_SWARM_QUEUE_OVERLAY_VERSION = 1;
35
56
  export const FRONTIER_SWARM_MERGE_INDEX_KIND = 'frontier.swarm.merge-index';
@@ -96,6 +117,8 @@ export const FRONTIER_SWARM_QUEUE_OUTCOME_MODEL_KIND = 'frontier.swarm.queue-out
96
117
  export const FRONTIER_SWARM_QUEUE_OUTCOME_MODEL_VERSION = 1;
97
118
  export const FRONTIER_SWARM_TERMINAL_STATE_RECONCILIATION_KIND = 'frontier.swarm.terminal-state-reconciliation';
98
119
  export const FRONTIER_SWARM_TERMINAL_STATE_RECONCILIATION_VERSION = 1;
120
+ export const FRONTIER_SWARM_TERMINAL_OUTCOME_RECORD_KIND = 'frontier.swarm.terminal-outcome-record';
121
+ export const FRONTIER_SWARM_TERMINAL_OUTCOME_RECORD_VERSION = 1;
99
122
  export const FRONTIER_SWARM_PRIORITY_POLICY_KIND = 'frontier.swarm.priority-policy';
100
123
  export const FRONTIER_SWARM_PRIORITY_POLICY_VERSION = 1;
101
124
  export const FRONTIER_SWARM_MODEL_ROUTE_KIND = 'frontier.swarm.model-route';
@@ -118,6 +141,42 @@ export const FRONTIER_SWARM_VERIFICATION_CATEGORY_HINTS = [
118
141
  'browser',
119
142
  'oracle'
120
143
  ];
144
+ export const FRONTIER_SWARM_GRAPH_NODE_KINDS = [
145
+ 'intent',
146
+ 'task',
147
+ 'worker',
148
+ 'candidate',
149
+ 'evidence',
150
+ 'gate',
151
+ 'decision',
152
+ 'merge',
153
+ 'replay',
154
+ 'rsi'
155
+ ];
156
+ export const FRONTIER_SWARM_GRAPH_EDGE_KINDS = [
157
+ 'dependsOn',
158
+ 'blocks',
159
+ 'produces',
160
+ 'verifies',
161
+ 'conflictsWith',
162
+ 'supersedes',
163
+ 'mergesInto'
164
+ ];
165
+ export const FRONTIER_SWARM_MERGE_CANDIDATE_ADMISSION_STATUSES = [
166
+ 'safe',
167
+ 'safe-with-losses',
168
+ 'review-required',
169
+ 'blocked'
170
+ ];
171
+ export const FRONTIER_SWARM_MERGE_CANDIDATE_REASON_CODES = [
172
+ 'missing-sidecar',
173
+ 'empty-sidecar',
174
+ 'stale-source-hash',
175
+ 'symbol-conflict',
176
+ 'effect-conflict',
177
+ 'lossy-import',
178
+ 'tests-missing'
179
+ ];
121
180
  export const FRONTIER_SWARM_SEMANTIC_OWNERSHIP_EXPORT_STABLE_KEY_KIND = 'exported-declaration';
122
181
  export const FRONTIER_SWARM_SEMANTIC_OWNERSHIP_NAMESPACE_EXPORT_STABLE_KEY_KIND = 'namespace-export';
123
182
  export const FRONTIER_SWARM_SEMANTIC_OWNERSHIP_TYPE_STABLE_KEY_KIND = 'type-declaration';
@@ -176,6 +235,7 @@ export const FRONTIER_SWARM_TERMINAL_OUTCOME_LABELS = [
176
235
  'superseded',
177
236
  'evidence-only',
178
237
  'no-change',
238
+ 'research-complete',
179
239
  'generated-by-collector',
180
240
  'patch-missing',
181
241
  'bundle-missing',
@@ -186,6 +246,26 @@ export const FRONTIER_SWARM_TERMINAL_OUTCOME_LABELS = [
186
246
  'human-blocked',
187
247
  'coordinator-review'
188
248
  ];
249
+ export const FRONTIER_SWARM_TERMINAL_OUTCOME_STATUSES = [
250
+ 'applied',
251
+ 'rejected',
252
+ 'superseded',
253
+ 'no-change',
254
+ 'conflict',
255
+ 'human-needed',
256
+ 'research-complete',
257
+ 'rerun'
258
+ ];
259
+ export const FRONTIER_SWARM_TERMINAL_OUTCOME_REASON_CODES = [
260
+ 'accepted-by-admission',
261
+ 'failed-verification',
262
+ 'superseded-by-newer-output',
263
+ 'no-effective-change',
264
+ 'conflict-detected',
265
+ 'human-decision-required',
266
+ 'research-complete',
267
+ 'stale-rerun-required'
268
+ ];
189
269
  const DEFAULT_COMPLETED_STATUSES = ['completed', 'verified', 'done', 'verified-local-harness'];
190
270
  const DEFAULT_SWARM_EVENT_TYPES = [
191
271
  'swarm.started',
@@ -378,6 +458,1016 @@ export function createSwarmTaskSelection(manifestInput, taskInput, options = {})
378
458
  summary: summarizeTaskSelection(entries)
379
459
  };
380
460
  }
461
+ export function createSwarmRunGraph(input = {}) {
462
+ const generatedAt = input.generatedAt ?? Date.now();
463
+ const nodes = dedupeGraphNodes((input.nodes ?? []).map((node) => normalizeGraphNode(node, generatedAt)));
464
+ const knownNodeIds = new Set(nodes.map((node) => node.id));
465
+ const edges = dedupeGraphEdges((input.edges ?? [])
466
+ .map((edge) => normalizeGraphEdge(edge, generatedAt))
467
+ .filter((edge) => knownNodeIds.has(edge.from) && knownNodeIds.has(edge.to)));
468
+ const knownEdgeIds = new Set(edges.map((edge) => edge.id));
469
+ const events = (input.events ?? [])
470
+ .map((event) => normalizeGraphEvent(event, generatedAt))
471
+ .filter((event) => event.nodeId === undefined || knownNodeIds.has(event.nodeId))
472
+ .filter((event) => event.edgeId === undefined || knownEdgeIds.has(event.edgeId))
473
+ .sort((left, right) => left.timestamp - right.timestamp || left.id.localeCompare(right.id));
474
+ const graph = {
475
+ kind: FRONTIER_SWARM_RUN_GRAPH_KIND,
476
+ version: FRONTIER_SWARM_RUN_GRAPH_VERSION,
477
+ id: input.id ?? 'swarm-run-graph:' + stableHash([input.runId, nodes.map((node) => node.id), edges.map((edge) => edge.id)]),
478
+ ...(input.runId ? { runId: input.runId } : {}),
479
+ ...(input.title ? { title: input.title } : {}),
480
+ generatedAt,
481
+ nodes,
482
+ edges,
483
+ events,
484
+ summary: summarizeSwarmRunGraph(nodes, edges, events),
485
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
486
+ };
487
+ return graph;
488
+ }
489
+ export function normalizeSwarmRunGraph(input) {
490
+ if (input.kind === FRONTIER_SWARM_RUN_GRAPH_KIND) {
491
+ const graph = input;
492
+ return createSwarmRunGraph({
493
+ id: graph.id,
494
+ runId: graph.runId,
495
+ title: graph.title,
496
+ generatedAt: graph.generatedAt,
497
+ nodes: graph.nodes,
498
+ edges: graph.edges,
499
+ events: graph.events,
500
+ metadata: graph.metadata
501
+ });
502
+ }
503
+ return createSwarmRunGraph(input);
504
+ }
505
+ export function createSwarmGraphSnapshot(input) {
506
+ const graph = normalizeSwarmRunGraph(input.graph);
507
+ const generatedAt = input.generatedAt ?? Date.now();
508
+ return {
509
+ kind: FRONTIER_SWARM_GRAPH_SNAPSHOT_KIND,
510
+ version: FRONTIER_SWARM_GRAPH_SNAPSHOT_VERSION,
511
+ id: input.id ?? 'swarm-graph-snapshot:' + stableHash([graph.id, generatedAt, graph.summary]),
512
+ graphId: graph.id,
513
+ generatedAt,
514
+ summary: graph.summary,
515
+ graph,
516
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
517
+ };
518
+ }
519
+ export function createSwarmGateRecord(input) {
520
+ const metrics = toJsonObject(input.metrics);
521
+ const metadata = toJsonObject(input.metadata);
522
+ return {
523
+ kind: FRONTIER_SWARM_GATE_RECORD_KIND,
524
+ version: FRONTIER_SWARM_GATE_RECORD_VERSION,
525
+ id: input.id ?? 'swarm-gate:' + stableHash([input.type, input.command, input.path, input.jobId, input.taskId, input.startedAt, input.finishedAt]),
526
+ type: input.type,
527
+ status: input.status ?? 'unknown',
528
+ required: input.required ?? false,
529
+ ...(input.command ? { command: input.command } : {}),
530
+ ...(input.path ? { path: input.path } : {}),
531
+ ...(input.jobId ? { jobId: input.jobId } : {}),
532
+ ...(input.taskId ? { taskId: input.taskId } : {}),
533
+ ...(input.startedAt !== undefined ? { startedAt: input.startedAt } : {}),
534
+ ...(input.finishedAt !== undefined ? { finishedAt: input.finishedAt } : {}),
535
+ ...(input.failureReason ? { failureReason: input.failureReason } : {}),
536
+ ...(input.confidence ? { confidence: input.confidence } : {}),
537
+ ...(metrics ? { metrics } : {}),
538
+ ...(metadata ? { metadata } : {})
539
+ };
540
+ }
541
+ export function createSwarmEvidenceRecord(input) {
542
+ const metrics = toJsonObject(input.metrics);
543
+ const metadata = toJsonObject(input.metadata);
544
+ const producedAt = input.producedAt ?? Date.now();
545
+ return {
546
+ kind: FRONTIER_SWARM_EVIDENCE_RECORD_KIND,
547
+ version: FRONTIER_SWARM_EVIDENCE_RECORD_VERSION,
548
+ id: input.id ?? 'swarm-evidence:' + stableHash([input.type, input.path, input.ref, input.jobId, input.taskId, input.gateId]),
549
+ type: input.type,
550
+ ...(input.path ? { path: input.path } : {}),
551
+ ...(input.ref ? { ref: input.ref } : {}),
552
+ ...(input.jobId ? { jobId: input.jobId } : {}),
553
+ ...(input.taskId ? { taskId: input.taskId } : {}),
554
+ ...(input.gateId ? { gateId: input.gateId } : {}),
555
+ producedAt,
556
+ ...(input.confidence ? { confidence: input.confidence } : {}),
557
+ ...(metrics ? { metrics } : {}),
558
+ ...(metadata ? { metadata } : {})
559
+ };
560
+ }
561
+ export function createSwarmPatchEvent(input) {
562
+ const generatedAt = input.generatedAt ?? Date.now();
563
+ const graphRefs = normalizeSwarmGraphRefs(input.graphRefs ?? []);
564
+ const sequence = input.sequence !== undefined ? Math.max(0, Math.floor(input.sequence)) : undefined;
565
+ return {
566
+ kind: FRONTIER_SWARM_PATCH_EVENT_KIND,
567
+ version: FRONTIER_SWARM_PATCH_EVENT_VERSION,
568
+ id: input.id ?? 'swarm-patch-event:' + stableHash([input.replayRecordId, sequence, input.operation, input.path, input.from, input.hash]),
569
+ operation: input.operation,
570
+ path: input.path,
571
+ ...(input.from ? { from: input.from } : {}),
572
+ ...(input.hash ? { hash: input.hash } : {}),
573
+ ...(sequence !== undefined ? { sequence } : {}),
574
+ ...(input.value !== undefined ? { value: toJsonValue(input.value) } : {}),
575
+ ...(input.before !== undefined ? { before: toJsonValue(input.before) } : {}),
576
+ ...(input.after !== undefined ? { after: toJsonValue(input.after) } : {}),
577
+ ...(input.jobId ? { jobId: input.jobId } : {}),
578
+ ...(input.taskId ? { taskId: input.taskId } : {}),
579
+ ...(input.replayRecordId ? { replayRecordId: input.replayRecordId } : {}),
580
+ graphRefs,
581
+ generatedAt,
582
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
583
+ };
584
+ }
585
+ export function createSwarmReplayRecord(input = {}) {
586
+ const generatedAt = input.generatedAt ?? Date.now();
587
+ const title = input.title ?? titleFromId(input.id ?? input.subject ?? input.replayBundleId ?? 'replay record');
588
+ const evidenceRefs = normalizeNamedRefs(input.evidenceRefs ?? [], 'evidence');
589
+ const graphRefs = normalizeSwarmGraphRefs(input.graphRefs ?? []);
590
+ const replayId = input.id ?? 'swarm-replay-record:' + stableHash([
591
+ title,
592
+ input.status,
593
+ input.subject,
594
+ input.replayBundleId,
595
+ input.patchEvents ?? [],
596
+ evidenceRefs,
597
+ graphRefs,
598
+ input.jobId,
599
+ input.taskId
600
+ ]);
601
+ const patchEvents = (input.patchEvents ?? [])
602
+ .map((event, index) => normalizeSwarmPatchEvent(event, replayId, index))
603
+ .sort((left, right) => (left.sequence ?? Number.MAX_SAFE_INTEGER) - (right.sequence ?? Number.MAX_SAFE_INTEGER) || left.id.localeCompare(right.id));
604
+ return {
605
+ kind: FRONTIER_SWARM_REPLAY_RECORD_KIND,
606
+ version: FRONTIER_SWARM_REPLAY_RECORD_VERSION,
607
+ id: replayId,
608
+ title,
609
+ status: input.status ?? 'unknown',
610
+ ...(input.subject ? { subject: input.subject } : {}),
611
+ ...(input.replayBundleId ? { replayBundleId: input.replayBundleId } : {}),
612
+ patchEvents,
613
+ evidenceRefs,
614
+ graphRefs,
615
+ ...(input.jobId ? { jobId: input.jobId } : {}),
616
+ ...(input.taskId ? { taskId: input.taskId } : {}),
617
+ ...(input.startedAt !== undefined ? { startedAt: input.startedAt } : {}),
618
+ ...(input.finishedAt !== undefined ? { finishedAt: input.finishedAt } : {}),
619
+ generatedAt,
620
+ summary: {
621
+ patchEventCount: patchEvents.length,
622
+ evidenceRefCount: evidenceRefs.length,
623
+ graphRefCount: graphRefs.length
624
+ },
625
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
626
+ };
627
+ }
628
+ export function createSwarmImprovementLoop(input = {}) {
629
+ const generatedAt = input.generatedAt ?? Date.now();
630
+ const title = input.title ?? titleFromId(input.id ?? input.subject ?? 'improvement loop');
631
+ const replayRecordIds = uniqueStrings(input.replayRecordIds ?? []);
632
+ const patchEventIds = uniqueStrings(input.patchEventIds ?? []);
633
+ const evidenceRefs = normalizeNamedRefs(input.evidenceRefs ?? [], 'evidence');
634
+ const graphRefs = normalizeSwarmGraphRefs(input.graphRefs ?? []);
635
+ return {
636
+ kind: FRONTIER_SWARM_IMPROVEMENT_LOOP_KIND,
637
+ version: FRONTIER_SWARM_IMPROVEMENT_LOOP_VERSION,
638
+ id: input.id ?? 'swarm-improvement-loop:' + stableHash([
639
+ title,
640
+ input.status,
641
+ input.subject,
642
+ input.observation,
643
+ input.action,
644
+ input.result,
645
+ replayRecordIds,
646
+ patchEventIds,
647
+ evidenceRefs,
648
+ graphRefs,
649
+ input.jobId,
650
+ input.taskId
651
+ ]),
652
+ title,
653
+ status: input.status ?? inferImprovementLoopStatus(input),
654
+ ...(input.subject ? { subject: input.subject } : {}),
655
+ ...(input.observation !== undefined ? { observation: toJsonValue(input.observation) } : {}),
656
+ ...(input.action !== undefined ? { action: toJsonValue(input.action) } : {}),
657
+ ...(input.result !== undefined ? { result: toJsonValue(input.result) } : {}),
658
+ replayRecordIds,
659
+ patchEventIds,
660
+ evidenceRefs,
661
+ graphRefs,
662
+ ...(input.jobId ? { jobId: input.jobId } : {}),
663
+ ...(input.taskId ? { taskId: input.taskId } : {}),
664
+ generatedAt,
665
+ summary: {
666
+ replayRecordCount: replayRecordIds.length,
667
+ patchEventCount: patchEventIds.length,
668
+ evidenceRefCount: evidenceRefs.length,
669
+ graphRefCount: graphRefs.length
670
+ },
671
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
672
+ };
673
+ }
674
+ export function mapSwarmGateEvidenceToGraph(input) {
675
+ const generatedAt = input.generatedAt ?? Date.now();
676
+ const gateRecords = (input.gates ?? []).map((gate) => isSwarmGateRecord(gate) ? gate : createSwarmGateRecord(gate));
677
+ const evidenceRecords = (input.evidence ?? []).map((evidence) => isSwarmEvidenceRecord(evidence) ? evidence : createSwarmEvidenceRecord(evidence));
678
+ const nodes = [
679
+ ...gateRecords.map((gate) => ({
680
+ id: graphNodeId('gate', gate.id),
681
+ kind: 'gate',
682
+ title: gate.command ?? gate.path ?? gate.type,
683
+ status: gate.status,
684
+ jobId: gate.jobId,
685
+ taskId: gate.taskId,
686
+ path: gate.path,
687
+ generatedAt,
688
+ metadata: gate
689
+ })),
690
+ ...evidenceRecords.map((evidence) => ({
691
+ id: graphNodeId('evidence', evidence.id),
692
+ kind: 'evidence',
693
+ title: evidence.path ?? evidence.ref ?? evidence.type,
694
+ status: evidence.confidence,
695
+ jobId: evidence.jobId,
696
+ taskId: evidence.taskId,
697
+ path: evidence.path,
698
+ generatedAt,
699
+ metadata: evidence
700
+ }))
701
+ ];
702
+ const edges = [];
703
+ for (const evidence of evidenceRecords) {
704
+ if (evidence.gateId) {
705
+ edges.push({
706
+ kind: 'produces',
707
+ from: graphNodeId('gate', evidence.gateId),
708
+ to: graphNodeId('evidence', evidence.id),
709
+ generatedAt
710
+ });
711
+ edges.push({
712
+ kind: 'verifies',
713
+ from: graphNodeId('evidence', evidence.id),
714
+ to: graphNodeId('gate', evidence.gateId),
715
+ generatedAt
716
+ });
717
+ }
718
+ }
719
+ return createSwarmRunGraph({
720
+ runId: input.runId,
721
+ title: 'Gate and evidence graph',
722
+ generatedAt,
723
+ nodes,
724
+ edges
725
+ });
726
+ }
727
+ export function createSwarmGateEvidenceGraph(input) {
728
+ return mapSwarmGateEvidenceToGraph(input);
729
+ }
730
+ export function classifySwarmMergeCandidateAdmission(input = {}) {
731
+ const arrayInput = Array.isArray(input);
732
+ const objectInput = arrayInput
733
+ ? undefined
734
+ : input;
735
+ const status = objectInput ? normalizeOptionalString(objectInput.status) : undefined;
736
+ if (status)
737
+ return status;
738
+ const reasonCodes = arrayInput
739
+ ? normalizeSwarmMergeCandidateReasonCodes(input)
740
+ : normalizeSwarmMergeCandidateReasonCodes(objectInput?.reasonCodes ?? []);
741
+ if (reasonCodes.some((reason) => FRONTIER_SWARM_BLOCKING_MERGE_CANDIDATE_REASON_CODES.has(reason)))
742
+ return 'blocked';
743
+ if (reasonCodes.some((reason) => FRONTIER_SWARM_REVIEW_MERGE_CANDIDATE_REASON_CODES.has(reason)))
744
+ return 'review-required';
745
+ if (reasonCodes.includes('lossy-import'))
746
+ return 'safe-with-losses';
747
+ return 'safe';
748
+ }
749
+ export function createSwarmSemanticChange(input) {
750
+ if (isSwarmSemanticChange(input))
751
+ return cloneJsonValue(input);
752
+ const generatedAt = input.generatedAt ?? Date.now();
753
+ const symbolId = normalizeId(input.symbolId, 'semantic change symbol id');
754
+ const declarationKind = normalizeId(input.declarationKind, 'semantic change declaration kind');
755
+ const sourceSpan = normalizeSwarmSourceSpan(input.sourceSpan, {
756
+ sourcePath: input.sourcePath,
757
+ sourceHash: input.sourceHash,
758
+ expectedSourceHash: input.expectedSourceHash
759
+ });
760
+ const operation = normalizeOptionalString(input.operation) ?? 'unknown';
761
+ const derivedReasonCodes = deriveSwarmSemanticChangeReasonCodes(input, sourceSpan);
762
+ const reasonCodes = normalizeSwarmMergeCandidateReasonCodes([...(input.reasonCodes ?? []), ...derivedReasonCodes]);
763
+ const conflictReason = normalizeSwarmConflictReason(input.conflictReason, reasonCodes);
764
+ return {
765
+ kind: FRONTIER_SWARM_SEMANTIC_CHANGE_KIND,
766
+ version: FRONTIER_SWARM_SEMANTIC_CHANGE_VERSION,
767
+ id: input.id ?? 'swarm-semantic-change:' + stableHash([symbolId, declarationKind, sourceSpan, operation, reasonCodes]),
768
+ symbolId,
769
+ declarationKind,
770
+ sourceSpan,
771
+ operation,
772
+ confidence: normalizeSwarmSemanticConfidence(input.confidence),
773
+ ...(conflictReason ? { conflictReason } : {}),
774
+ status: classifySwarmMergeCandidateAdmission({ status: input.status, reasonCodes }),
775
+ reasonCodes,
776
+ generatedAt,
777
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
778
+ };
779
+ }
780
+ export function createSwarmMergeCandidate(input) {
781
+ if (isSwarmMergeCandidate(input))
782
+ return cloneJsonValue(input);
783
+ const generatedAt = input.generatedAt ?? Date.now();
784
+ const shorthandChange = input.symbolId || input.declarationKind || input.operation || input.sourceSpan || input.sourcePath
785
+ ? [{
786
+ symbolId: input.symbolId ?? 'unknown-symbol',
787
+ declarationKind: input.declarationKind ?? 'unknown',
788
+ operation: input.operation ?? 'unknown',
789
+ sourceSpan: input.sourceSpan,
790
+ sourcePath: input.sourcePath,
791
+ sourceHash: input.sourceHash,
792
+ expectedSourceHash: input.expectedSourceHash,
793
+ confidence: input.confidence,
794
+ conflictReason: input.conflictReason,
795
+ status: input.status,
796
+ reasonCodes: input.reasonCodes,
797
+ generatedAt
798
+ }]
799
+ : [];
800
+ const semanticChanges = [...(input.semanticChanges ?? []), ...(input.changes ?? []), ...shorthandChange]
801
+ .map((change) => createSwarmSemanticChange({ ...change, generatedAt: change.generatedAt ?? generatedAt }));
802
+ const primaryChange = semanticChanges[0];
803
+ const candidateSourceSpan = primaryChange?.sourceSpan ?? normalizeSwarmSourceSpan(input.sourceSpan, {
804
+ sourcePath: input.sourcePath,
805
+ sourceHash: input.sourceHash,
806
+ expectedSourceHash: input.expectedSourceHash
807
+ });
808
+ const derivedReasonCodes = deriveSwarmMergeCandidateReasonCodes(input, semanticChanges, candidateSourceSpan);
809
+ const reasonCodes = normalizeSwarmMergeCandidateReasonCodes([
810
+ ...(input.reasonCodes ?? []),
811
+ ...semanticChanges.flatMap((change) => change.reasonCodes),
812
+ ...derivedReasonCodes
813
+ ]);
814
+ const symbolIds = uniqueStrings([
815
+ input.symbolId,
816
+ ...semanticChanges.map((change) => change.symbolId)
817
+ ]);
818
+ const declarationKinds = uniqueStrings([
819
+ input.declarationKind,
820
+ ...semanticChanges.map((change) => change.declarationKind)
821
+ ]);
822
+ const changedPaths = uniqueStrings(semanticChanges.map((change) => change.sourceSpan.path).concat(candidateSourceSpan.path));
823
+ const conflictReason = normalizeSwarmConflictReason(input.conflictReason, reasonCodes);
824
+ const metadata = toJsonObject(input.metadata);
825
+ return {
826
+ kind: FRONTIER_SWARM_MERGE_CANDIDATE_KIND,
827
+ version: FRONTIER_SWARM_MERGE_CANDIDATE_VERSION,
828
+ id: input.id ?? 'swarm-merge-candidate:' + stableHash([input.jobId, input.taskId, symbolIds, declarationKinds, semanticChanges, reasonCodes]),
829
+ ...(input.jobId ? { jobId: input.jobId } : {}),
830
+ ...(input.taskId ? { taskId: input.taskId } : {}),
831
+ ...(input.lane ? { lane: input.lane } : {}),
832
+ ...(input.title ? { title: input.title } : {}),
833
+ ...(input.sidecarPath ? { sidecarPath: input.sidecarPath } : {}),
834
+ symbolId: symbolIds[0] ?? primaryChange?.symbolId ?? 'unknown-symbol',
835
+ symbolIds,
836
+ declarationKind: declarationKinds[0] ?? primaryChange?.declarationKind ?? 'unknown',
837
+ declarationKinds,
838
+ sourceSpan: candidateSourceSpan,
839
+ operation: input.operation ?? primaryChange?.operation ?? 'unknown',
840
+ confidence: normalizeSwarmSemanticConfidence(input.confidence ?? primaryChange?.confidence),
841
+ ...(conflictReason ? { conflictReason } : {}),
842
+ status: classifySwarmMergeCandidateAdmission({ status: input.status, reasonCodes }),
843
+ reasonCodes,
844
+ semanticChanges,
845
+ changedPaths,
846
+ evidencePaths: uniqueStrings(input.evidencePaths ?? []),
847
+ generatedAt,
848
+ summary: {
849
+ changeCount: semanticChanges.length,
850
+ symbolCount: symbolIds.length,
851
+ reasonCount: reasonCodes.length
852
+ },
853
+ ...(metadata ? { metadata } : {})
854
+ };
855
+ }
856
+ export function mapSwarmMergeCandidatesToGraph(input) {
857
+ const generatedAt = input.generatedAt ?? Date.now();
858
+ const candidates = input.candidates.map((candidate) => createSwarmMergeCandidate({ ...candidate, generatedAt: candidate.generatedAt ?? generatedAt }));
859
+ const includeChangeNodes = input.includeChangeNodes !== false;
860
+ const nodes = [];
861
+ const edges = [];
862
+ for (const candidate of candidates) {
863
+ const candidateNodeId = graphNodeId('candidate', candidate.id);
864
+ nodes.push({
865
+ id: candidateNodeId,
866
+ kind: 'candidate',
867
+ title: candidate.title ?? candidate.symbolId,
868
+ status: candidate.status,
869
+ jobId: candidate.jobId,
870
+ taskId: candidate.taskId,
871
+ path: candidate.changedPaths[0],
872
+ generatedAt,
873
+ metadata: candidate
874
+ });
875
+ if (!includeChangeNodes)
876
+ continue;
877
+ for (const change of candidate.semanticChanges) {
878
+ const changeNodeId = graphNodeId('semantic-change', change.id);
879
+ nodes.push({
880
+ id: changeNodeId,
881
+ kind: 'semantic-change',
882
+ title: `${change.operation} ${change.symbolId}`,
883
+ status: change.status,
884
+ jobId: candidate.jobId,
885
+ taskId: candidate.taskId,
886
+ path: change.sourceSpan.path,
887
+ generatedAt,
888
+ metadata: change
889
+ });
890
+ edges.push({
891
+ kind: 'produces',
892
+ from: candidateNodeId,
893
+ to: changeNodeId,
894
+ label: 'semantic-change',
895
+ generatedAt,
896
+ metadata: {
897
+ symbolId: change.symbolId,
898
+ declarationKind: change.declarationKind,
899
+ operation: change.operation,
900
+ reasonCodes: change.reasonCodes
901
+ }
902
+ });
903
+ if (change.conflictReason) {
904
+ edges.push({
905
+ kind: 'conflictsWith',
906
+ from: candidateNodeId,
907
+ to: changeNodeId,
908
+ label: change.conflictReason,
909
+ generatedAt
910
+ });
911
+ }
912
+ }
913
+ }
914
+ return { candidates, nodes, edges };
915
+ }
916
+ export function createSwarmMergeCandidateGraph(input) {
917
+ const generatedAt = input.generatedAt ?? Date.now();
918
+ const projection = mapSwarmMergeCandidatesToGraph({ ...input, generatedAt });
919
+ return createSwarmRunGraph({
920
+ id: input.id,
921
+ runId: input.runId,
922
+ title: input.title ?? 'Semantic merge candidate graph',
923
+ generatedAt,
924
+ nodes: projection.nodes,
925
+ edges: projection.edges,
926
+ metadata: input.metadata
927
+ });
928
+ }
929
+ export function mapSwarmReplayRecordsToGraph(input) {
930
+ const generatedAt = input.generatedAt ?? Date.now();
931
+ const replayRecords = (input.replayRecords ?? []).map((record) => isSwarmReplayRecord(record) ? record : createSwarmReplayRecord(record));
932
+ const nodes = [];
933
+ const edges = [];
934
+ for (const replay of replayRecords) {
935
+ const replayNodeId = graphNodeId('replay', replay.id);
936
+ nodes.push({
937
+ id: replayNodeId,
938
+ kind: 'replay',
939
+ title: replay.title,
940
+ status: replay.status,
941
+ jobId: replay.jobId,
942
+ taskId: replay.taskId,
943
+ generatedAt,
944
+ metadata: replay
945
+ });
946
+ for (const event of replay.patchEvents) {
947
+ const eventNodeId = graphNodeId('replay', event.id);
948
+ nodes.push({
949
+ id: eventNodeId,
950
+ kind: 'replay',
951
+ title: `${event.operation} ${event.path}`,
952
+ jobId: event.jobId ?? replay.jobId,
953
+ taskId: event.taskId ?? replay.taskId,
954
+ path: event.path,
955
+ generatedAt,
956
+ metadata: event
957
+ });
958
+ edges.push({
959
+ kind: 'produces',
960
+ from: replayNodeId,
961
+ to: eventNodeId,
962
+ label: event.operation,
963
+ generatedAt
964
+ });
965
+ }
966
+ for (const evidence of replay.evidenceRefs) {
967
+ const evidenceNodeId = graphNodeId('evidence', evidence.id);
968
+ nodes.push({
969
+ id: evidenceNodeId,
970
+ kind: 'evidence',
971
+ title: evidence.path ?? evidence.uri ?? evidence.id,
972
+ path: evidence.path,
973
+ generatedAt,
974
+ metadata: evidence
975
+ });
976
+ edges.push({
977
+ kind: 'verifies',
978
+ from: evidenceNodeId,
979
+ to: replayNodeId,
980
+ label: evidence.role ?? evidence.kind,
981
+ generatedAt
982
+ });
983
+ }
984
+ }
985
+ return createSwarmRunGraph({
986
+ runId: input.runId,
987
+ title: 'Replay records graph',
988
+ generatedAt,
989
+ nodes,
990
+ edges
991
+ });
992
+ }
993
+ export function createSwarmReplayRecordGraph(input) {
994
+ return mapSwarmReplayRecordsToGraph(input);
995
+ }
996
+ export function mapSwarmImprovementLoopsToGraph(input) {
997
+ const generatedAt = input.generatedAt ?? Date.now();
998
+ const loops = (input.improvementLoops ?? []).map((loop) => isSwarmImprovementLoop(loop) ? loop : createSwarmImprovementLoop(loop));
999
+ const nodes = [];
1000
+ const edges = [];
1001
+ for (const loop of loops) {
1002
+ const phaseNodes = [];
1003
+ if (loop.observation !== undefined) {
1004
+ phaseNodes.push(createImprovementLoopPhaseNode(loop, 'observation', loop.observation, generatedAt));
1005
+ }
1006
+ if (loop.action !== undefined) {
1007
+ phaseNodes.push(createImprovementLoopPhaseNode(loop, 'action', loop.action, generatedAt));
1008
+ }
1009
+ if (loop.result !== undefined) {
1010
+ phaseNodes.push(createImprovementLoopPhaseNode(loop, 'result', loop.result, generatedAt));
1011
+ }
1012
+ if (phaseNodes.length === 0) {
1013
+ phaseNodes.push({
1014
+ id: graphNodeId('rsi', loop.id),
1015
+ kind: 'rsi',
1016
+ title: loop.title,
1017
+ status: loop.status,
1018
+ jobId: loop.jobId,
1019
+ taskId: loop.taskId,
1020
+ generatedAt,
1021
+ metadata: loop
1022
+ });
1023
+ }
1024
+ nodes.push(...phaseNodes);
1025
+ for (let index = 1; index < phaseNodes.length; index += 1) {
1026
+ edges.push({
1027
+ kind: 'produces',
1028
+ from: phaseNodes[index - 1].id,
1029
+ to: phaseNodes[index].id,
1030
+ label: index === 1 ? 'action' : 'result',
1031
+ generatedAt
1032
+ });
1033
+ }
1034
+ const entryNodeId = phaseNodes[0]?.id;
1035
+ const exitNodeId = phaseNodes.at(-1)?.id;
1036
+ for (const replayRecordId of loop.replayRecordIds) {
1037
+ const replayNodeId = graphNodeId('replay', replayRecordId);
1038
+ nodes.push({
1039
+ id: replayNodeId,
1040
+ kind: 'replay',
1041
+ title: titleFromId(replayRecordId),
1042
+ generatedAt,
1043
+ metadata: { replayRecordId }
1044
+ });
1045
+ if (entryNodeId)
1046
+ edges.push({ kind: 'produces', from: replayNodeId, to: entryNodeId, label: 'feeds-rsi', generatedAt });
1047
+ }
1048
+ for (const patchEventId of loop.patchEventIds) {
1049
+ const patchNodeId = graphNodeId('replay', patchEventId);
1050
+ nodes.push({
1051
+ id: patchNodeId,
1052
+ kind: 'replay',
1053
+ title: titleFromId(patchEventId),
1054
+ generatedAt,
1055
+ metadata: { patchEventId }
1056
+ });
1057
+ if (entryNodeId)
1058
+ edges.push({ kind: 'produces', from: patchNodeId, to: entryNodeId, label: 'feeds-rsi', generatedAt });
1059
+ }
1060
+ for (const evidence of loop.evidenceRefs) {
1061
+ const evidenceNodeId = graphNodeId('evidence', evidence.id);
1062
+ nodes.push({
1063
+ id: evidenceNodeId,
1064
+ kind: 'evidence',
1065
+ title: evidence.path ?? evidence.uri ?? evidence.id,
1066
+ path: evidence.path,
1067
+ generatedAt,
1068
+ metadata: evidence
1069
+ });
1070
+ if (exitNodeId)
1071
+ edges.push({ kind: 'verifies', from: evidenceNodeId, to: exitNodeId, label: evidence.role ?? evidence.kind, generatedAt });
1072
+ }
1073
+ }
1074
+ return createSwarmRunGraph({
1075
+ runId: input.runId,
1076
+ title: 'Improvement loop graph',
1077
+ generatedAt,
1078
+ nodes,
1079
+ edges
1080
+ });
1081
+ }
1082
+ export function createSwarmImprovementLoopGraph(input) {
1083
+ return mapSwarmImprovementLoopsToGraph(input);
1084
+ }
1085
+ export function createSwarmRunGraphChunk(input = {}) {
1086
+ const generatedAt = Date.now();
1087
+ const nodes = dedupeGraphNodes((input.nodes ?? []).map((node) => normalizeGraphNode(node, generatedAt)));
1088
+ const known = new Set(nodes.map((node) => node.id));
1089
+ const edges = dedupeGraphEdges((input.edges ?? [])
1090
+ .map((edge) => normalizeGraphEdge(edge, generatedAt))
1091
+ .filter((edge) => known.has(edge.from) && known.has(edge.to)));
1092
+ const entryNodeIds = uniqueStrings((input.entryNodeIds ?? []).filter((id) => known.has(id)));
1093
+ const exitNodeIds = uniqueStrings((input.exitNodeIds ?? []).filter((id) => known.has(id)));
1094
+ return {
1095
+ kind: FRONTIER_SWARM_RUN_GRAPH_CHUNK_KIND,
1096
+ version: FRONTIER_SWARM_RUN_GRAPH_CHUNK_VERSION,
1097
+ id: input.id ?? 'swarm-run-graph-chunk:' + stableHash([nodes.map((node) => node.id), edges.map((edge) => edge.id), entryNodeIds, exitNodeIds]),
1098
+ nodes,
1099
+ edges,
1100
+ entryNodeIds,
1101
+ exitNodeIds,
1102
+ summary: {
1103
+ nodeCount: nodes.length,
1104
+ edgeCount: edges.length,
1105
+ entryCount: entryNodeIds.length,
1106
+ exitCount: exitNodeIds.length
1107
+ },
1108
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
1109
+ };
1110
+ }
1111
+ export function createSwarmRunGraphChain(input) {
1112
+ const nodes = input.nodes.map((node) => normalizeGraphNode(node));
1113
+ const edges = [];
1114
+ for (let index = 1; index < nodes.length; index += 1) {
1115
+ edges.push({ kind: input.edgeKind ?? 'dependsOn', from: nodes[index - 1].id, to: nodes[index].id });
1116
+ }
1117
+ return createSwarmRunGraphChunk({
1118
+ id: input.id,
1119
+ nodes,
1120
+ edges,
1121
+ entryNodeIds: nodes[0] ? [nodes[0].id] : [],
1122
+ exitNodeIds: nodes.at(-1) ? [nodes.at(-1).id] : [],
1123
+ metadata: input.metadata
1124
+ });
1125
+ }
1126
+ export function createSwarmRunGraphFork(input) {
1127
+ const source = normalizeGraphNode(input.source);
1128
+ const branches = input.branches.map((node) => normalizeGraphNode(node));
1129
+ return createSwarmRunGraphChunk({
1130
+ id: input.id,
1131
+ nodes: [source, ...branches],
1132
+ edges: branches.map((branch) => ({ kind: 'dependsOn', from: source.id, to: branch.id, label: 'fork' })),
1133
+ entryNodeIds: [source.id],
1134
+ exitNodeIds: branches.map((branch) => branch.id),
1135
+ metadata: input.metadata
1136
+ });
1137
+ }
1138
+ export function createSwarmRunGraphJoin(input) {
1139
+ const branches = input.branches.map((node) => normalizeGraphNode(node));
1140
+ const join = normalizeGraphNode(input.join);
1141
+ return createSwarmRunGraphChunk({
1142
+ id: input.id,
1143
+ nodes: [...branches, join],
1144
+ edges: branches.map((branch) => ({ kind: 'dependsOn', from: branch.id, to: join.id, label: 'join' })),
1145
+ entryNodeIds: branches.map((branch) => branch.id),
1146
+ exitNodeIds: [join.id],
1147
+ metadata: input.metadata
1148
+ });
1149
+ }
1150
+ export function createSwarmRunGraphBarrier(input) {
1151
+ const prerequisites = input.prerequisites.map((node) => normalizeGraphNode(node));
1152
+ const prerequisiteIds = prerequisites.map((node) => node.id);
1153
+ const barrierMetadata = toJsonObject(input.barrier?.metadata);
1154
+ const barrier = normalizeGraphNode({
1155
+ ...(input.barrier ?? {}),
1156
+ id: input.barrier?.id ?? (input.id ? `${input.id}:barrier` : graphNodeId('gate', 'barrier:' + stableHash(prerequisiteIds))),
1157
+ kind: input.barrier?.kind ?? 'gate',
1158
+ title: input.barrier?.title ?? 'Barrier',
1159
+ metadata: {
1160
+ ...(barrierMetadata ?? {}),
1161
+ role: 'barrier',
1162
+ graphChunkKind: 'barrier',
1163
+ prerequisiteIds,
1164
+ prerequisiteCount: prerequisiteIds.length
1165
+ }
1166
+ });
1167
+ return createSwarmRunGraphChunk({
1168
+ id: input.id,
1169
+ nodes: [...prerequisites, barrier],
1170
+ edges: prerequisites.map((node, index) => ({
1171
+ kind: 'dependsOn',
1172
+ from: node.id,
1173
+ to: barrier.id,
1174
+ label: 'barrier',
1175
+ metadata: {
1176
+ role: 'barrier-prerequisite',
1177
+ barrierId: barrier.id,
1178
+ prerequisiteId: node.id,
1179
+ prerequisiteIndex: index,
1180
+ prerequisiteCount: prerequisites.length
1181
+ }
1182
+ })),
1183
+ entryNodeIds: prerequisiteIds,
1184
+ exitNodeIds: [barrier.id],
1185
+ metadata: input.metadata
1186
+ });
1187
+ }
1188
+ export function createSwarmRunGraphRaceSelect(input) {
1189
+ const baseBranches = input.branches.map((node) => normalizeGraphNode(node));
1190
+ const branchIds = baseBranches.map((node) => node.id);
1191
+ const selectedBranchId = input.selectedBranchId && branchIds.includes(input.selectedBranchId) ? input.selectedBranchId : undefined;
1192
+ const rejectedBranchIds = new Set(selectedBranchId
1193
+ ? branchIds.filter((id) => id !== selectedBranchId)
1194
+ : uniqueStrings(input.rejectedBranchIds ?? []).filter((id) => branchIds.includes(id)));
1195
+ if (selectedBranchId)
1196
+ rejectedBranchIds.delete(selectedBranchId);
1197
+ const selectorMetadata = toJsonObject(input.selector?.metadata);
1198
+ const selector = normalizeGraphNode({
1199
+ ...(input.selector ?? {}),
1200
+ id: input.selector?.id ?? (input.id ? `${input.id}:selector` : graphNodeId('decision', 'race-select:' + stableHash(branchIds))),
1201
+ kind: input.selector?.kind ?? 'decision',
1202
+ title: input.selector?.title ?? 'Race/select',
1203
+ metadata: {
1204
+ ...(selectorMetadata ?? {}),
1205
+ role: 'race-select',
1206
+ graphChunkKind: 'race-select',
1207
+ branchIds,
1208
+ branchCount: branchIds.length,
1209
+ ...(selectedBranchId ? { selectedBranchId } : {}),
1210
+ rejectedBranchIds: Array.from(rejectedBranchIds).sort()
1211
+ }
1212
+ });
1213
+ const branches = baseBranches.map((node, index) => {
1214
+ const disposition = node.id === selectedBranchId ? 'selected' : rejectedBranchIds.has(node.id) ? 'rejected' : 'candidate';
1215
+ return normalizeGraphNode({
1216
+ ...node,
1217
+ status: node.status ?? (disposition === 'selected' ? 'accepted' : disposition === 'rejected' ? 'rejected' : undefined),
1218
+ metadata: {
1219
+ ...(node.metadata ?? {}),
1220
+ raceSelect: {
1221
+ selectorId: selector.id,
1222
+ branchId: node.id,
1223
+ branchIndex: index,
1224
+ branchCount: baseBranches.length,
1225
+ disposition,
1226
+ selected: disposition === 'selected',
1227
+ rejected: disposition === 'rejected'
1228
+ }
1229
+ }
1230
+ }, node.generatedAt);
1231
+ });
1232
+ return createSwarmRunGraphChunk({
1233
+ id: input.id,
1234
+ nodes: [selector, ...branches],
1235
+ edges: branches.map((branch, index) => {
1236
+ const disposition = branch.id === selectedBranchId ? 'selected' : rejectedBranchIds.has(branch.id) ? 'rejected' : 'candidate';
1237
+ return {
1238
+ kind: disposition === 'selected' ? 'mergesInto' : disposition === 'rejected' ? 'supersedes' : 'dependsOn',
1239
+ from: branch.id,
1240
+ to: selector.id,
1241
+ label: disposition,
1242
+ metadata: {
1243
+ role: 'race-select-branch',
1244
+ selectorId: selector.id,
1245
+ branchId: branch.id,
1246
+ branchIndex: index,
1247
+ branchCount: branches.length,
1248
+ disposition,
1249
+ selected: disposition === 'selected',
1250
+ rejected: disposition === 'rejected'
1251
+ }
1252
+ };
1253
+ }),
1254
+ entryNodeIds: branchIds,
1255
+ exitNodeIds: [selector.id],
1256
+ metadata: input.metadata
1257
+ });
1258
+ }
1259
+ export function createSwarmRunGraphTournament(input) {
1260
+ const tournament = normalizeGraphNode({ id: input.id ? `${input.id}:tournament` : undefined, kind: 'decision', title: 'Tournament', metadata: input.metadata });
1261
+ const candidates = input.candidates.map((node) => normalizeGraphNode({ ...node, kind: node.kind ?? 'candidate' }));
1262
+ const rejected = new Set(input.rejectedCandidates ?? []);
1263
+ const edges = candidates.map((candidate) => ({
1264
+ kind: candidate.id === input.winner ? 'mergesInto' : rejected.has(candidate.id) ? 'supersedes' : 'dependsOn',
1265
+ from: candidate.id,
1266
+ to: tournament.id,
1267
+ label: candidate.id === input.winner ? 'winner' : rejected.has(candidate.id) ? 'rejected' : 'candidate'
1268
+ }));
1269
+ return createSwarmRunGraphChunk({
1270
+ id: input.id,
1271
+ nodes: [tournament, ...candidates],
1272
+ edges,
1273
+ entryNodeIds: candidates.map((candidate) => candidate.id),
1274
+ exitNodeIds: [tournament.id],
1275
+ metadata: input.metadata
1276
+ });
1277
+ }
1278
+ export function createSwarmRunGraphRetryLoop(input) {
1279
+ const action = normalizeGraphNode(input.action);
1280
+ const gate = normalizeGraphNode({ ...input.gate, kind: input.gate.kind ?? 'gate' });
1281
+ const retry = normalizeGraphNode(input.retry);
1282
+ const optional = [input.success, input.failure].filter((node) => Boolean(node)).map((node) => normalizeGraphNode(node));
1283
+ return createSwarmRunGraphChunk({
1284
+ id: input.id,
1285
+ nodes: [action, gate, retry, ...optional],
1286
+ edges: [
1287
+ { kind: 'verifies', from: action.id, to: gate.id },
1288
+ { kind: 'blocks', from: gate.id, to: retry.id, label: 'retry' },
1289
+ { kind: 'dependsOn', from: retry.id, to: action.id, label: 'loop' },
1290
+ ...optional.map((node) => ({ kind: node.status === 'failed' ? 'blocks' : 'mergesInto', from: gate.id, to: node.id }))
1291
+ ],
1292
+ entryNodeIds: [action.id],
1293
+ exitNodeIds: optional.length > 0 ? optional.map((node) => node.id) : [gate.id],
1294
+ metadata: input.metadata
1295
+ });
1296
+ }
1297
+ export function createSwarmRunGraphRsiLoop(input) {
1298
+ const observe = normalizeGraphNode({ ...input.observe, kind: input.observe.kind ?? 'rsi' });
1299
+ const improve = normalizeGraphNode({ ...input.improve, kind: input.improve.kind ?? 'rsi' });
1300
+ const apply = input.apply ? normalizeGraphNode(input.apply) : undefined;
1301
+ return createSwarmRunGraphChunk({
1302
+ id: input.id,
1303
+ nodes: apply ? [observe, improve, apply] : [observe, improve],
1304
+ edges: [
1305
+ { kind: 'produces', from: observe.id, to: improve.id, label: 'feedback' },
1306
+ ...(apply ? [{ kind: 'mergesInto', from: improve.id, to: apply.id, label: 'applies' }] : [])
1307
+ ],
1308
+ entryNodeIds: [observe.id],
1309
+ exitNodeIds: [apply?.id ?? improve.id],
1310
+ metadata: input.metadata
1311
+ });
1312
+ }
1313
+ export function createSwarmRunGraphSynthesisChunk(input = {}) {
1314
+ const source = input.source ? normalizeGraphNode(input.source) : undefined;
1315
+ const panel = normalizeGraphNode({
1316
+ ...input.panel,
1317
+ id: input.panel?.id ?? (input.id ? `${input.id}:panel` : undefined),
1318
+ kind: input.panel?.kind ?? 'decision',
1319
+ title: input.panel?.title ?? 'Synthesis panel'
1320
+ });
1321
+ const candidates = (input.candidates ?? []).map((candidate) => normalizeGraphNode({
1322
+ ...candidate,
1323
+ kind: candidate.kind ?? 'candidate'
1324
+ }));
1325
+ const rejectedCandidateIds = new Set(input.rejectedCandidateIds ?? candidates
1326
+ .filter((candidate) => candidate.status === 'rejected' || candidate.status === 'failed' || candidate.status === 'superseded')
1327
+ .map((candidate) => candidate.id));
1328
+ const selectedCandidateId = input.selectedCandidateId ?? candidates.find((candidate) => !rejectedCandidateIds.has(candidate.id))?.id;
1329
+ const selectedCandidate = selectedCandidateId ? candidates.find((candidate) => candidate.id === selectedCandidateId) : undefined;
1330
+ const decision = input.decision ? normalizeGraphNode({
1331
+ ...input.decision,
1332
+ id: input.decision.id ?? (input.id ? `${input.id}:decision` : undefined),
1333
+ kind: input.decision.kind ?? 'decision',
1334
+ title: input.decision.title ?? 'Coordinator synthesis decision'
1335
+ }) : undefined;
1336
+ const output = input.output ? normalizeGraphNode({
1337
+ ...input.output,
1338
+ kind: input.output.kind ?? 'candidate'
1339
+ }) : undefined;
1340
+ const edges = [];
1341
+ if (source) {
1342
+ edges.push({ kind: 'produces', from: source.id, to: panel.id, label: 'synthesis-request' });
1343
+ }
1344
+ for (const candidate of candidates) {
1345
+ edges.push({ kind: 'produces', from: panel.id, to: candidate.id, label: 'candidate' });
1346
+ }
1347
+ if (decision && selectedCandidate) {
1348
+ edges.push({ kind: 'mergesInto', from: selectedCandidate.id, to: decision.id, label: 'selected' });
1349
+ }
1350
+ for (const candidate of candidates) {
1351
+ if (!rejectedCandidateIds.has(candidate.id))
1352
+ continue;
1353
+ edges.push(selectedCandidate && selectedCandidate.id !== candidate.id
1354
+ ? { kind: 'supersedes', from: selectedCandidate.id, to: candidate.id, label: 'rejected' }
1355
+ : { kind: 'supersedes', from: candidate.id, to: decision?.id ?? panel.id, label: 'rejected' });
1356
+ }
1357
+ if (decision && output) {
1358
+ edges.push({
1359
+ kind: isBlockingGraphStatus(decision.status) ? 'blocks' : 'produces',
1360
+ from: decision.id,
1361
+ to: output.id,
1362
+ label: isBlockingGraphStatus(decision.status) ? 'blocked-output' : 'synthesized-output'
1363
+ });
1364
+ }
1365
+ else if (selectedCandidate && output) {
1366
+ edges.push({ kind: 'mergesInto', from: selectedCandidate.id, to: output.id, label: 'synthesized-output' });
1367
+ }
1368
+ const nodes = [source, panel, ...candidates, decision, output].filter((node) => Boolean(node));
1369
+ return createSwarmRunGraphChunk({
1370
+ id: input.id,
1371
+ nodes,
1372
+ edges,
1373
+ entryNodeIds: [source?.id ?? panel.id],
1374
+ exitNodeIds: [output?.id ?? decision?.id ?? selectedCandidate?.id ?? panel.id],
1375
+ metadata: input.metadata
1376
+ });
1377
+ }
1378
+ export function createSwarmRunGraphVerificationGateChunk(input) {
1379
+ const subject = normalizeGraphNode(input.subject);
1380
+ const gate = normalizeGraphNode({
1381
+ ...input.gate,
1382
+ kind: input.gate.kind ?? 'gate',
1383
+ title: input.gate.title ?? 'Verification gate'
1384
+ });
1385
+ const evidence = (input.evidence ?? []).map((node) => normalizeGraphNode({
1386
+ ...node,
1387
+ kind: node.kind ?? 'evidence'
1388
+ }));
1389
+ const pass = input.pass ? normalizeGraphNode(input.pass) : undefined;
1390
+ const block = input.block ? normalizeGraphNode(input.block) : undefined;
1391
+ const passing = isPassingGraphStatus(gate.status);
1392
+ const blocking = isBlockingGraphStatus(gate.status);
1393
+ const edges = [
1394
+ { kind: 'verifies', from: gate.id, to: subject.id, label: 'gate' },
1395
+ ...evidence.map((node) => ({ kind: 'produces', from: gate.id, to: node.id, label: 'evidence' })),
1396
+ ...evidence.map((node) => ({ kind: 'verifies', from: node.id, to: gate.id, label: 'evidence-verifies-gate' }))
1397
+ ];
1398
+ if (passing && pass) {
1399
+ edges.push({ kind: 'mergesInto', from: gate.id, to: pass.id, label: 'passed' });
1400
+ }
1401
+ if (blocking) {
1402
+ edges.push({ kind: 'blocks', from: gate.id, to: block?.id ?? subject.id, label: gate.status === 'failed' ? 'failed' : 'blocked' });
1403
+ }
1404
+ return createSwarmRunGraphChunk({
1405
+ id: input.id,
1406
+ nodes: [subject, gate, ...evidence, pass, block].filter((node) => Boolean(node)),
1407
+ edges,
1408
+ entryNodeIds: [subject.id],
1409
+ exitNodeIds: [passing && pass ? pass.id : blocking && block ? block.id : gate.id],
1410
+ metadata: input.metadata
1411
+ });
1412
+ }
1413
+ export function createSwarmRunGraphMergeGateChunk(input) {
1414
+ const candidate = normalizeGraphNode({
1415
+ ...input.candidate,
1416
+ kind: input.candidate.kind ?? 'candidate'
1417
+ });
1418
+ const target = normalizeGraphNode(input.target);
1419
+ const gate = input.gate ? normalizeGraphNode({
1420
+ ...input.gate,
1421
+ kind: input.gate.kind ?? 'gate',
1422
+ title: input.gate.title ?? 'Merge gate'
1423
+ }) : undefined;
1424
+ const decision = input.decision ? normalizeGraphNode({
1425
+ ...input.decision,
1426
+ kind: input.decision.kind ?? 'decision',
1427
+ title: input.decision.title ?? 'Coordinator merge decision'
1428
+ }) : undefined;
1429
+ const blockers = (input.blockers ?? []).map((node) => normalizeGraphNode({
1430
+ ...node,
1431
+ kind: node.kind ?? 'gate'
1432
+ }));
1433
+ const superseded = (input.superseded ?? []).map((node) => normalizeGraphNode({
1434
+ ...node,
1435
+ kind: node.kind ?? 'candidate'
1436
+ }));
1437
+ const status = input.status ?? decision?.status ?? gate?.status ?? candidate.status;
1438
+ const passing = isPassingGraphStatus(status);
1439
+ const blocking = isBlockingGraphStatus(status);
1440
+ const edges = [];
1441
+ if (gate) {
1442
+ edges.push({ kind: 'verifies', from: gate.id, to: candidate.id, label: 'merge-gate' });
1443
+ }
1444
+ if (gate && decision) {
1445
+ edges.push({ kind: blocking ? 'blocks' : 'produces', from: gate.id, to: decision.id, label: 'gate-decision' });
1446
+ }
1447
+ if (decision) {
1448
+ edges.push({ kind: 'verifies', from: decision.id, to: candidate.id, label: 'coordinator-decision' });
1449
+ }
1450
+ for (const blocker of blockers) {
1451
+ edges.push({ kind: 'blocks', from: blocker.id, to: candidate.id, label: 'merge-blocker' });
1452
+ }
1453
+ for (const node of superseded) {
1454
+ edges.push({ kind: 'supersedes', from: candidate.id, to: node.id, label: 'superseded' });
1455
+ }
1456
+ if (passing) {
1457
+ edges.push({ kind: 'mergesInto', from: candidate.id, to: target.id, label: 'accepted' });
1458
+ }
1459
+ else if (blocking) {
1460
+ edges.push({ kind: 'blocks', from: decision?.id ?? gate?.id ?? candidate.id, to: target.id, label: status === 'failed' ? 'failed' : 'blocked' });
1461
+ }
1462
+ return createSwarmRunGraphChunk({
1463
+ id: input.id,
1464
+ nodes: [candidate, target, gate, decision, ...blockers, ...superseded].filter((node) => Boolean(node)),
1465
+ edges,
1466
+ entryNodeIds: [candidate.id],
1467
+ exitNodeIds: [passing ? target.id : blocking ? decision?.id ?? gate?.id ?? candidate.id : target.id],
1468
+ metadata: input.metadata
1469
+ });
1470
+ }
381
1471
  export const FRONTIER_SWARM_TASK_MODEL_PROFILES = [
382
1472
  {
383
1473
  workKind: 'agent-task',
@@ -750,6 +1840,497 @@ export function createSwarmRun(input) {
750
1840
  };
751
1841
  return run;
752
1842
  }
1843
+ export function createRunEventsFromSwarmPlan(plan, options = {}) {
1844
+ const actorId = options.actorId ?? 'frontier-swarm';
1845
+ const runId = options.runId ?? plan.runId;
1846
+ const time = swarmRunEventTime(options, plan.createdAt);
1847
+ let actorSeq = options.startActorSeq ?? 1;
1848
+ const events = [];
1849
+ const created = createRunEvent({
1850
+ runId,
1851
+ actorId,
1852
+ actorSeq: actorSeq++,
1853
+ parents: [...(options.parents ?? [])],
1854
+ time,
1855
+ type: 'run.created',
1856
+ payload: {
1857
+ goal: plan.metadata?.objective ?? plan.metadata?.goal ?? `Frontier swarm ${plan.id}`,
1858
+ metadata: pruneUndefinedJsonObject({
1859
+ source: 'frontier-swarm.plan',
1860
+ planId: plan.id,
1861
+ manifestId: plan.manifestId,
1862
+ summary: plan.summary,
1863
+ filters: plan.filters,
1864
+ limits: plan.limits,
1865
+ metadata: plan.metadata,
1866
+ adapterMetadata: toJsonObject(options.metadata)
1867
+ })
1868
+ }
1869
+ });
1870
+ events.push(created);
1871
+ const laneIds = uniqueStrings(plan.jobs.map((job) => job.lane));
1872
+ const laneEventByLane = new Map();
1873
+ for (const lane of laneIds) {
1874
+ const laneJobs = plan.jobs.filter((job) => job.lane === lane);
1875
+ const laneEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunLane({
1876
+ id: swarmRunLaneNodeId(lane),
1877
+ title: lane,
1878
+ packageId: inferSwarmLanePackageId(laneJobs),
1879
+ allowedWrites: uniqueStrings(laneJobs.flatMap((job) => job.allowedWrites)),
1880
+ requiredChecks: uniqueStrings(laneJobs.flatMap((job) => job.verification.map(formatSwarmCommandLine))),
1881
+ maxConcurrency: plan.limits.maxLaneConcurrency[lane],
1882
+ concurrencyKey: lane,
1883
+ semanticRegions: uniqueStrings(laneJobs.flatMap((job) => [...job.ownedRegions, ...job.changedRegions])),
1884
+ status: 'ready',
1885
+ createdAt: time,
1886
+ updatedAt: time,
1887
+ metadata: pruneUndefinedJsonObject({
1888
+ source: 'frontier-swarm.plan.lane',
1889
+ jobIds: laneJobs.map((job) => job.id),
1890
+ taskIds: uniqueStrings(laneJobs.map((job) => job.taskId))
1891
+ })
1892
+ }), {
1893
+ parents: [created.id],
1894
+ time
1895
+ });
1896
+ events.push(laneEvent);
1897
+ laneEventByLane.set(lane, laneEvent);
1898
+ }
1899
+ const taskById = new Map();
1900
+ for (const job of plan.jobs)
1901
+ if (!taskById.has(job.taskId))
1902
+ taskById.set(job.taskId, job);
1903
+ for (const job of taskById.values()) {
1904
+ const task = job.task;
1905
+ const taskEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunTask({
1906
+ id: swarmRunTaskNodeId(job.taskId),
1907
+ title: task.title || job.title,
1908
+ laneId: swarmRunLaneNodeId(job.lane),
1909
+ status: swarmStatusToRunStatus(job.status),
1910
+ targetRefs: uniqueStrings([...(task.targetRefs ?? []), ...job.allowedWrites]),
1911
+ sourceRefs: task.sourceRefs,
1912
+ allowedWrites: job.allowedWrites,
1913
+ semanticRegions: uniqueStrings([...job.ownedRegions, ...job.changedRegions]),
1914
+ acceptance: job.acceptance,
1915
+ verification: job.verification.map((command) => ({
1916
+ id: command.name,
1917
+ command: command.command,
1918
+ args: command.args,
1919
+ cwd: command.cwd,
1920
+ required: command.required,
1921
+ metadata: command.metadata
1922
+ })),
1923
+ priority: job.priority,
1924
+ concurrencyKey: job.concurrencyKey,
1925
+ createdAt: time,
1926
+ updatedAt: time,
1927
+ metadata: pruneUndefinedJsonObject({
1928
+ source: 'frontier-swarm.plan.task',
1929
+ jobIds: plan.jobs.filter((entry) => entry.taskId === job.taskId).map((entry) => entry.id),
1930
+ workKind: task.workKind,
1931
+ layer: job.layer,
1932
+ compute: job.compute.id,
1933
+ tags: job.tags,
1934
+ task: task
1935
+ })
1936
+ }), {
1937
+ parents: [laneEventByLane.get(job.lane)?.id ?? created.id],
1938
+ time
1939
+ });
1940
+ events.push(taskEvent);
1941
+ }
1942
+ return events;
1943
+ }
1944
+ export function createRunEventsFromSwarmLease(lease, options = {}) {
1945
+ const actorId = options.actorId ?? lease.workerId ?? 'frontier-swarm';
1946
+ const runId = options.runId ?? options.job?.metadata?.runId ?? 'frontier-swarm';
1947
+ const eventType = options.eventType ?? (lease.status === 'released' ? 'lease.released' : 'lease.granted');
1948
+ const time = swarmRunEventTime(options, eventType === 'lease.released' ? options.now : lease.leasedAt);
1949
+ const leaseNode = {
1950
+ kind: 'lease',
1951
+ id: swarmRunLeaseNodeId(lease.id),
1952
+ title: `Lease ${lease.jobId}`,
1953
+ scopeId: options.job ? swarmRunTaskNodeId(options.job.taskId) : swarmRunTaskNodeId(lease.jobId),
1954
+ leaseKey: options.job?.concurrencyKey ?? lease.jobId,
1955
+ holderId: lease.workerId,
1956
+ status: eventType === 'lease.released' ? 'released' : 'granted',
1957
+ requestedAt: new Date(lease.leasedAt).toISOString(),
1958
+ grantedAt: new Date(lease.leasedAt).toISOString(),
1959
+ releasedAt: eventType === 'lease.released' ? time : undefined,
1960
+ metadata: pruneUndefinedJsonObject({
1961
+ source: 'frontier-swarm.lease',
1962
+ jobId: lease.jobId,
1963
+ token: lease.token,
1964
+ fencingToken: lease.fencingToken,
1965
+ expiresAt: lease.expiresAt,
1966
+ status: lease.status
1967
+ })
1968
+ };
1969
+ return [createRunEvent({
1970
+ runId,
1971
+ actorId,
1972
+ actorSeq: options.startActorSeq ?? 1,
1973
+ parents: [...(options.parents ?? [])],
1974
+ time,
1975
+ type: eventType,
1976
+ payload: pruneUndefinedJsonObject({
1977
+ subjectId: options.job ? swarmRunTaskNodeId(options.job.taskId) : undefined,
1978
+ lease: leaseNode
1979
+ })
1980
+ })];
1981
+ }
1982
+ export function createRunEventsFromSwarmResult(resultInput, options = {}) {
1983
+ const result = isSwarmJobResult(resultInput) ? cloneJsonValue(resultInput) : normalizeResult(resultInput);
1984
+ const job = options.job;
1985
+ const runId = options.runId ?? 'frontier-swarm';
1986
+ const actorId = options.actorId ?? 'frontier-swarm-worker';
1987
+ const time = swarmRunEventTime(options, result.finishedAt ?? result.startedAt);
1988
+ let actorSeq = options.startActorSeq ?? 1;
1989
+ const parents = [...(options.parents ?? [])];
1990
+ const events = [];
1991
+ const attemptId = swarmRunAttemptNodeId(result.jobId);
1992
+ const taskId = swarmRunTaskNodeId(job?.taskId ?? result.jobId);
1993
+ const attemptEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunAttempt({
1994
+ id: attemptId,
1995
+ title: job?.title ?? result.jobId,
1996
+ taskId,
1997
+ actorId,
1998
+ runnerId: job?.compute.kind ?? job?.compute.id,
1999
+ workspaceId: job?.worktreePath,
2000
+ status: swarmResultStatusToAttemptStatus(result.status),
2001
+ startedAt: result.startedAt ? new Date(result.startedAt).toISOString() : undefined,
2002
+ endedAt: result.finishedAt ? new Date(result.finishedAt).toISOString() : undefined,
2003
+ model: job?.compute.model,
2004
+ reason: result.error,
2005
+ metadata: pruneUndefinedJsonObject({
2006
+ source: 'frontier-swarm.result',
2007
+ jobId: result.jobId,
2008
+ mergeReadiness: result.mergeReadiness,
2009
+ mergeDisposition: result.mergeDisposition,
2010
+ riskLevel: result.riskLevel,
2011
+ exitCode: result.exitCode,
2012
+ signal: result.signal,
2013
+ queueItemIds: result.queueItemIds,
2014
+ semanticImport: result.semanticImport,
2015
+ metadata: result.metadata
2016
+ })
2017
+ }), { parents, time });
2018
+ events.push(attemptEvent);
2019
+ let latestParents = [attemptEvent.id];
2020
+ if (result.patchPath || result.changedPaths.length) {
2021
+ const patchId = swarmRunPatchNodeId(result.jobId);
2022
+ const patchEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunPatch({
2023
+ id: patchId,
2024
+ title: `Patch ${result.jobId}`,
2025
+ changedPaths: result.changedPaths,
2026
+ artifactId: result.patchPath ? swarmRunArtifactNodeId(result.patchPath) : undefined,
2027
+ summary: result.lastMessage,
2028
+ risk: swarmRiskToRunRisk(result.riskLevel),
2029
+ metadata: pruneUndefinedJsonObject({
2030
+ source: 'frontier-swarm.result.patch',
2031
+ patchPath: result.patchPath,
2032
+ changedRegions: result.changedRegions,
2033
+ ownershipViolations: result.ownershipViolations
2034
+ })
2035
+ }), { parents: latestParents, time });
2036
+ events.push(patchEvent);
2037
+ events.push(createRunEdgeEvent(runId, actorId, actorSeq++, linkRunNodes(attemptId, patchId, 'produces-patch', { createdAt: time }), {
2038
+ parents: [patchEvent.id],
2039
+ time
2040
+ }));
2041
+ if (result.patchPath) {
2042
+ events.push(createRunEvent({
2043
+ runId,
2044
+ actorId,
2045
+ actorSeq: actorSeq++,
2046
+ parents: [patchEvent.id],
2047
+ time,
2048
+ type: 'artifact.attached',
2049
+ payload: {
2050
+ subjectId: patchId,
2051
+ artifact: toJsonValue(defineRunArtifact({
2052
+ id: swarmRunArtifactNodeId(result.patchPath),
2053
+ title: result.patchPath,
2054
+ artifactType: 'patch',
2055
+ path: result.patchPath,
2056
+ summary: `Patch artifact for ${result.jobId}`,
2057
+ metadata: { source: 'frontier-swarm.result.patch-artifact', jobId: result.jobId }
2058
+ }))
2059
+ }
2060
+ }));
2061
+ }
2062
+ latestParents = [patchEvent.id];
2063
+ }
2064
+ for (const evidencePath of result.evidencePaths) {
2065
+ const artifactId = swarmRunArtifactNodeId(evidencePath);
2066
+ const evidenceId = swarmRunEvidenceNodeId(result.jobId, evidencePath);
2067
+ const artifactEvent = createRunEvent({
2068
+ runId,
2069
+ actorId,
2070
+ actorSeq: actorSeq++,
2071
+ parents: latestParents,
2072
+ time,
2073
+ type: 'artifact.attached',
2074
+ payload: {
2075
+ subjectId: attemptId,
2076
+ artifact: toJsonValue(defineRunArtifact({
2077
+ id: artifactId,
2078
+ title: evidencePath,
2079
+ artifactType: inferSwarmArtifactType(evidencePath),
2080
+ path: evidencePath,
2081
+ summary: `Evidence artifact for ${result.jobId}`,
2082
+ metadata: { source: 'frontier-swarm.result.evidence-artifact', jobId: result.jobId }
2083
+ }))
2084
+ }
2085
+ });
2086
+ events.push(artifactEvent);
2087
+ const evidenceEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunEvidence({
2088
+ id: evidenceId,
2089
+ title: evidencePath,
2090
+ evidenceType: inferSwarmArtifactType(evidencePath),
2091
+ result: result.status === 'completed' || result.status === 'verified' ? 'pass' : result.status === 'failed' ? 'fail' : 'unknown',
2092
+ artifactIds: [artifactId],
2093
+ summary: `Evidence from ${result.jobId}`,
2094
+ metadata: { source: 'frontier-swarm.result.evidence', jobId: result.jobId }
2095
+ }), { parents: [artifactEvent.id], time });
2096
+ events.push(evidenceEvent);
2097
+ events.push(createRunEdgeEvent(runId, actorId, actorSeq++, linkRunNodes(attemptId, evidenceId, 'produces-evidence', { createdAt: time }), {
2098
+ parents: [evidenceEvent.id],
2099
+ time
2100
+ }));
2101
+ }
2102
+ for (let index = 0; index < result.verification.length; index += 1) {
2103
+ const verification = result.verification[index];
2104
+ const verificationId = swarmRunVerificationNodeId(result.jobId, index, verification.name);
2105
+ const verificationEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunVerification({
2106
+ id: verificationId,
2107
+ title: verification.name,
2108
+ status: verification.status === 0 ? 'passed' : verification.required === false && verification.status !== 0 ? 'skipped' : 'failed',
2109
+ command: verification.command[0],
2110
+ args: verification.command.slice(1),
2111
+ cwd: verification.cwd,
2112
+ exitCode: verification.status,
2113
+ artifactIds: [],
2114
+ required: verification.required,
2115
+ summary: verification.commandLine,
2116
+ metadata: pruneUndefinedJsonObject({
2117
+ source: 'frontier-swarm.result.verification',
2118
+ jobId: result.jobId,
2119
+ durationMs: verification.durationMs,
2120
+ stdoutTail: verification.stdoutTail,
2121
+ stderrTail: verification.stderrTail,
2122
+ category: verification.category,
2123
+ metadata: verification.metadata
2124
+ })
2125
+ }), { parents: latestParents, time });
2126
+ events.push(verificationEvent);
2127
+ events.push(createRunEdgeEvent(runId, actorId, actorSeq++, linkRunNodes(attemptId, verificationId, 'verified-by', { createdAt: time }), {
2128
+ parents: [verificationEvent.id],
2129
+ time
2130
+ }));
2131
+ }
2132
+ return events;
2133
+ }
2134
+ export function createRunEventsFromMergeBundle(bundle, options = {}) {
2135
+ const runId = options.runId ?? bundle.runId ?? 'frontier-swarm';
2136
+ const actorId = options.actorId ?? 'frontier-swarm-collector';
2137
+ const time = swarmRunEventTime(options, bundle.generatedAt);
2138
+ let actorSeq = options.startActorSeq ?? 1;
2139
+ const parents = [...(options.parents ?? [])];
2140
+ const events = [];
2141
+ const patchId = swarmRunPatchNodeId(bundle.jobId);
2142
+ const patchEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunPatch({
2143
+ id: patchId,
2144
+ title: bundle.title ?? `Merge bundle ${bundle.jobId}`,
2145
+ changedPaths: bundle.changedPaths,
2146
+ artifactId: bundle.patchPath ? swarmRunArtifactNodeId(bundle.patchPath) : undefined,
2147
+ summary: bundle.reasons.join('; '),
2148
+ risk: swarmRiskToRunRisk(bundle.riskLevel),
2149
+ metadata: pruneUndefinedJsonObject({
2150
+ source: 'frontier-swarm.merge-bundle',
2151
+ bundleId: bundle.id,
2152
+ jobId: bundle.jobId,
2153
+ taskId: bundle.taskId,
2154
+ lane: bundle.lane,
2155
+ mergeReadiness: bundle.mergeReadiness,
2156
+ disposition: bundle.disposition,
2157
+ autoMergeable: bundle.autoMergeable,
2158
+ staleAgainstHead: bundle.staleAgainstHead,
2159
+ changedRegions: bundle.changedRegions,
2160
+ ownedFilesTouched: bundle.ownedFilesTouched,
2161
+ ownershipViolations: bundle.ownershipViolations,
2162
+ patchHash: bundle.patchHash,
2163
+ branchName: bundle.branchName,
2164
+ commit: bundle.commit,
2165
+ semanticImport: bundle.semanticImport
2166
+ })
2167
+ }), { parents, time });
2168
+ events.push(patchEvent);
2169
+ if (bundle.patchPath) {
2170
+ events.push(createRunEvent({
2171
+ runId,
2172
+ actorId,
2173
+ actorSeq: actorSeq++,
2174
+ parents: [patchEvent.id],
2175
+ time,
2176
+ type: 'artifact.attached',
2177
+ payload: {
2178
+ subjectId: patchId,
2179
+ artifact: toJsonValue(defineRunArtifact({
2180
+ id: swarmRunArtifactNodeId(bundle.patchPath),
2181
+ title: bundle.patchPath,
2182
+ artifactType: 'patch',
2183
+ path: bundle.patchPath,
2184
+ hash: bundle.patchHash,
2185
+ summary: `Merge patch for ${bundle.jobId}`,
2186
+ metadata: { source: 'frontier-swarm.merge-bundle.patch', bundleId: bundle.id }
2187
+ }))
2188
+ }
2189
+ }));
2190
+ }
2191
+ for (const evidencePath of bundle.evidencePaths) {
2192
+ events.push(createRunEvent({
2193
+ runId,
2194
+ actorId,
2195
+ actorSeq: actorSeq++,
2196
+ parents: [patchEvent.id],
2197
+ time,
2198
+ type: 'artifact.attached',
2199
+ payload: {
2200
+ subjectId: patchId,
2201
+ artifact: toJsonValue(defineRunArtifact({
2202
+ id: swarmRunArtifactNodeId(evidencePath),
2203
+ title: evidencePath,
2204
+ artifactType: inferSwarmArtifactType(evidencePath),
2205
+ path: evidencePath,
2206
+ summary: `Merge evidence for ${bundle.jobId}`,
2207
+ metadata: { source: 'frontier-swarm.merge-bundle.evidence', bundleId: bundle.id }
2208
+ }))
2209
+ }
2210
+ }));
2211
+ }
2212
+ const decisionKind = swarmDispositionToRunDecision(bundle.disposition, bundle.mergeReadiness);
2213
+ const decisionEvent = createRunEvent({
2214
+ runId,
2215
+ actorId,
2216
+ actorSeq: actorSeq++,
2217
+ parents: [patchEvent.id],
2218
+ time,
2219
+ type: 'decision.recorded',
2220
+ payload: {
2221
+ decision: toJsonValue(defineRunDecision({
2222
+ id: swarmRunDecisionNodeId(bundle.id || bundle.jobId),
2223
+ title: `Merge decision ${bundle.jobId}`,
2224
+ decision: decisionKind,
2225
+ subjectIds: uniqueStrings([patchId, bundle.taskId ? swarmRunTaskNodeId(bundle.taskId) : undefined]),
2226
+ actorId,
2227
+ reason: bundle.reasons.join('; ') || bundle.disposition,
2228
+ requiredActions: bundle.disposition === 'needs-port' ? ['human-port'] : bundle.staleAgainstHead ? ['rerun-against-head'] : [],
2229
+ metadata: pruneUndefinedJsonObject({
2230
+ source: 'frontier-swarm.merge-bundle.decision',
2231
+ bundleId: bundle.id,
2232
+ disposition: bundle.disposition,
2233
+ mergeReadiness: bundle.mergeReadiness,
2234
+ riskLevel: bundle.riskLevel,
2235
+ autoMergeable: bundle.autoMergeable
2236
+ })
2237
+ }))
2238
+ }
2239
+ });
2240
+ events.push(decisionEvent);
2241
+ return events;
2242
+ }
2243
+ export function createRunEventsFromCoordinatorDecision(decision, options = {}) {
2244
+ const normalized = createSwarmQueueOutcomeDecision(decision);
2245
+ const runId = options.runId ?? 'frontier-swarm';
2246
+ const actorId = options.actorId ?? 'frontier-swarm-coordinator';
2247
+ const time = swarmRunEventTime(options, normalized.generatedAt);
2248
+ const subjectIds = uniqueStrings([
2249
+ normalized.jobId ? swarmRunAttemptNodeId(normalized.jobId) : undefined,
2250
+ normalized.taskId ? swarmRunTaskNodeId(normalized.taskId) : undefined,
2251
+ normalized.subjectId
2252
+ ]);
2253
+ return [createRunEvent({
2254
+ runId,
2255
+ actorId,
2256
+ actorSeq: options.startActorSeq ?? 1,
2257
+ parents: [...(options.parents ?? [])],
2258
+ time,
2259
+ type: 'decision.recorded',
2260
+ payload: {
2261
+ decision: toJsonValue(defineRunDecision({
2262
+ id: swarmRunDecisionNodeId(normalized.id),
2263
+ title: `Coordinator decision ${normalized.subjectId}`,
2264
+ decision: swarmQueueOutcomeToRunDecision(normalized),
2265
+ subjectIds,
2266
+ actorId,
2267
+ reason: normalized.reasons.join('; ') || normalized.outcome,
2268
+ requiredActions: swarmQueueOutcomeRequiredActions(normalized),
2269
+ metadata: pruneUndefinedJsonObject({
2270
+ source: 'frontier-swarm.queue-outcome-decision',
2271
+ decision: normalized.decision,
2272
+ action: normalized.action,
2273
+ assignedAction: normalized.assignedAction,
2274
+ category: normalized.category,
2275
+ outcome: normalized.outcome,
2276
+ terminal: normalized.terminal,
2277
+ queueItemIds: normalized.queueItemIds,
2278
+ queueId: normalized.queueId,
2279
+ lane: normalized.lane,
2280
+ disposition: normalized.disposition,
2281
+ mergeReadiness: normalized.mergeReadiness,
2282
+ status: normalized.status,
2283
+ conflictingJobIds: normalized.conflictingJobIds,
2284
+ metadata: normalized.metadata
2285
+ })
2286
+ }))
2287
+ }
2288
+ })];
2289
+ }
2290
+ export function createRunProjectionFromSwarmRunEvents(events, options = {}) {
2291
+ return replayRunEvents(events, {
2292
+ id: options.runId,
2293
+ goal: options.goal,
2294
+ metadata: toJsonObject(options.metadata)
2295
+ });
2296
+ }
2297
+ export function createRunDashboardFromSwarmRun(input, options = {}) {
2298
+ const events = isFrontierRunEventList(input) ? input : createRunEventsFromSwarmRun(input, options);
2299
+ return createRunDashboardSnapshot(createRunProjectionFromSwarmRunEvents(events, options));
2300
+ }
2301
+ export function createRunEventsFromSwarmRun(run, options = {}) {
2302
+ const runId = options.runId ?? run.id;
2303
+ const actorId = options.actorId ?? 'frontier-swarm';
2304
+ const created = createRunEvent({
2305
+ runId,
2306
+ actorId,
2307
+ actorSeq: options.startActorSeq ?? 1,
2308
+ parents: [...(options.parents ?? [])],
2309
+ time: swarmRunEventTime(options, run.startedAt),
2310
+ type: 'run.created',
2311
+ payload: {
2312
+ goal: run.metadata?.objective ?? run.metadata?.goal ?? `Frontier swarm ${run.id}`,
2313
+ metadata: pruneUndefinedJsonObject({
2314
+ source: 'frontier-swarm.run',
2315
+ planId: run.planId,
2316
+ manifestId: run.manifestId,
2317
+ status: run.status,
2318
+ summary: run.summary,
2319
+ metadata: run.metadata
2320
+ })
2321
+ }
2322
+ });
2323
+ return [
2324
+ created,
2325
+ ...run.results.flatMap((result, index) => createRunEventsFromSwarmResult(result, {
2326
+ runId,
2327
+ actorId: 'frontier-swarm-worker',
2328
+ startActorSeq: 1000 + index * 100,
2329
+ parents: [created.id],
2330
+ job: run.jobs.find((job) => job.id === result.jobId)
2331
+ }))
2332
+ ];
2333
+ }
753
2334
  export function recordSwarmEvent(runInput, eventInput) {
754
2335
  const run = cloneJsonValue(runInput);
755
2336
  run.events = run.events.concat(normalizeEvent({ ...eventInput, runId: eventInput.runId ?? run.id }));
@@ -1030,6 +2611,14 @@ const SEMANTIC_IMPORT_CONFLICT_HINT_KEYS = new Set([
1030
2611
  'symbols',
1031
2612
  'touchedSymbols'
1032
2613
  ]);
2614
+ const SEMANTIC_IMPORT_PUBLIC_CONTRACT_HINT_KEYS = new Set([
2615
+ 'apiContract',
2616
+ 'apiContracts',
2617
+ 'publicContract',
2618
+ 'publicContractId',
2619
+ 'publicContractIds',
2620
+ 'publicContracts'
2621
+ ]);
1033
2622
  function semanticImportExportMetadata(value, key, file) {
1034
2623
  const explicitKind = semanticImportStringValue(value.kind);
1035
2624
  const declarationKind = semanticImportStringValue(value.declarationKind)
@@ -1119,9 +2708,28 @@ function semanticImportStringValue(value) {
1119
2708
  function semanticMergeConflictKeys(semanticImport) {
1120
2709
  const records = semanticImportRecords(semanticImport);
1121
2710
  const sources = records.length > 0 ? records : [semanticImport];
1122
- return uniqueStrings(sources.flatMap((source) => semanticImportConflictHintStringsForValue('mergeCandidate' in source && source.mergeCandidate !== undefined
2711
+ return uniqueStrings(sources.flatMap((source) => semanticImportConflictKeyStringsForValue('mergeCandidate' in source && source.mergeCandidate !== undefined
1123
2712
  ? source.mergeCandidate
1124
- : source).map((symbol) => `symbol:${symbol}`)));
2713
+ : source)));
2714
+ }
2715
+ function semanticImportConflictKeyStringsForValue(value, key, depth = 0) {
2716
+ if (value === undefined || value === null)
2717
+ return [];
2718
+ if (typeof value === 'string') {
2719
+ const normalized = value.trim();
2720
+ if (!normalized)
2721
+ return [];
2722
+ if (key && SEMANTIC_IMPORT_PUBLIC_CONTRACT_HINT_KEYS.has(key))
2723
+ return [`public-contract:${publicContractConflictValue(normalized)}`];
2724
+ if (key && SEMANTIC_IMPORT_CONFLICT_HINT_KEYS.has(key))
2725
+ return [`symbol:${normalized}`];
2726
+ return [];
2727
+ }
2728
+ if (depth >= 4 || typeof value !== 'object')
2729
+ return [];
2730
+ if (Array.isArray(value))
2731
+ return value.flatMap((item) => semanticImportConflictKeyStringsForValue(item, key, depth + 1));
2732
+ return Object.entries(value).flatMap(([nextKey, nextValue]) => (semanticImportConflictKeyStringsForValue(nextValue, nextKey, depth + 1)));
1125
2733
  }
1126
2734
  function semanticImportConflictHintStringsForValue(value, key, depth = 0) {
1127
2735
  if (value === undefined || value === null)
@@ -2879,7 +4487,8 @@ export function classifySwarmQueueOutcome(input) {
2879
4487
  || action === 'reject'
2880
4488
  || action === 'record-only'
2881
4489
  || input.disposition === 'rejected'
2882
- || input.mergeReadiness === 'discovery-only') {
4490
+ || input.mergeReadiness === 'discovery-only'
4491
+ || queueOutcomeHas(search, 'research-complete', 'discovery-complete', 'synthesized')) {
2883
4492
  category = 'terminal';
2884
4493
  }
2885
4494
  else if (action === 'rerun'
@@ -3137,6 +4746,8 @@ function terminalOutcomeLabelFromText(value) {
3137
4746
  return 'evidence-only';
3138
4747
  if (token === 'no-change' || token === 'nochange' || token === 'no-op' || token === 'noop' || token === 'unchanged')
3139
4748
  return 'no-change';
4749
+ if (token === 'research-complete' || token === 'discovery-complete' || token === 'synthesized')
4750
+ return 'research-complete';
3140
4751
  if (token === 'generated-by-collector' || token === 'collector-generated' || token === 'generated-collector')
3141
4752
  return 'generated-by-collector';
3142
4753
  if (token === 'patch-missing' || token === 'missing-patch' || token === 'patchmissing')
@@ -3149,7 +4760,7 @@ function terminalOutcomeLabelFromText(value) {
3149
4760
  return 'rejected';
3150
4761
  if (token === 'conflict-blocked' || token === 'merge-conflict' || token === 'textual-conflict' || token === 'conflict')
3151
4762
  return 'conflict-blocked';
3152
- if (token === 'human-question' || token === 'human-blocked' || token === 'blocked')
4763
+ if (token === 'human-needed' || token === 'human-question' || token === 'human-blocked' || token === 'blocked')
3153
4764
  return 'human-question';
3154
4765
  if (token === 'coordinator-review' || token === 'needs-port' || token === 'escalated' || token === 'review')
3155
4766
  return 'coordinator-review';
@@ -3178,6 +4789,12 @@ export function normalizeSwarmTerminalOutcome(input = {}) {
3178
4789
  || terminalOutcomeTextMatches(input.outcome, 'no-change', 'nochange', 'no-op', 'noop', 'unchanged')
3179
4790
  || terminalOutcomeTextMatches(input.status, 'no-change', 'nochange', 'no-op', 'noop', 'unchanged')
3180
4791
  || terminalOutcomeTextMatches(input.decision, 'no-change', 'nochange', 'no-op', 'noop', 'unchanged');
4792
+ const researchComplete = typeof input === 'string'
4793
+ ? terminalOutcomeTextMatches(input, 'research-complete', 'discovery-complete', 'synthesized')
4794
+ : terminalOutcomeTextMatches(input.label, 'research-complete', 'discovery-complete', 'synthesized')
4795
+ || terminalOutcomeTextMatches(input.outcome, 'research-complete', 'discovery-complete', 'synthesized')
4796
+ || terminalOutcomeTextMatches(input.status, 'research-complete', 'discovery-complete', 'synthesized')
4797
+ || terminalOutcomeTextMatches(input.decision, 'research-complete', 'discovery-complete', 'synthesized');
3181
4798
  const patchMissing = typeof input === 'string'
3182
4799
  ? terminalOutcomeTextMatches(input, 'patch-missing', 'missing-patch', 'patchmissing')
3183
4800
  : input.patchMissing === true
@@ -3200,8 +4817,12 @@ export function normalizeSwarmTerminalOutcome(input = {}) {
3200
4817
  || terminalOutcomeTextMatches(input.status, 'conflict-blocked', 'merge-conflict', 'textual-conflict', 'conflict')
3201
4818
  || terminalOutcomeTextMatches(input.decision, 'conflict-blocked', 'merge-conflict', 'textual-conflict', 'conflict');
3202
4819
  const humanQuestion = typeof input === 'string'
3203
- ? terminalOutcomeTextMatches(input, 'human-question')
4820
+ ? terminalOutcomeTextMatches(input, 'human-needed', 'human-question')
3204
4821
  : input.humanQuestion === true
4822
+ || terminalOutcomeTextMatches(input.label, 'human-needed')
4823
+ || terminalOutcomeTextMatches(input.outcome, 'human-needed')
4824
+ || terminalOutcomeTextMatches(input.status, 'human-needed')
4825
+ || terminalOutcomeTextMatches(input.decision, 'human-needed')
3205
4826
  || terminalOutcomeTextMatches(input.label, 'human-question')
3206
4827
  || terminalOutcomeTextMatches(input.outcome, 'human-question')
3207
4828
  || terminalOutcomeTextMatches(input.status, 'human-question')
@@ -3227,18 +4848,20 @@ export function normalizeSwarmTerminalOutcome(input = {}) {
3227
4848
  ? 'patch-missing'
3228
4849
  : evidenceOnly
3229
4850
  ? 'evidence-only'
3230
- : noChange
3231
- ? 'no-change'
3232
- : generatedByCollector
3233
- ? 'generated-by-collector'
3234
- : conflictBlocked
3235
- ? 'conflict-blocked'
3236
- : humanQuestion || humanBlocked
3237
- ? 'human-question'
3238
- : coordinatorReview
3239
- ? 'coordinator-review'
3240
- : explicitLabel ?? 'unknown';
3241
- const category = label === 'applied' || label === 'committed' || label === 'checked' || label === 'superseded' || label === 'evidence-only' || label === 'no-change' || label === 'generated-by-collector'
4851
+ : researchComplete
4852
+ ? 'research-complete'
4853
+ : noChange
4854
+ ? 'no-change'
4855
+ : generatedByCollector
4856
+ ? 'generated-by-collector'
4857
+ : conflictBlocked
4858
+ ? 'conflict-blocked'
4859
+ : humanQuestion || humanBlocked
4860
+ ? 'human-question'
4861
+ : coordinatorReview
4862
+ ? 'coordinator-review'
4863
+ : explicitLabel ?? 'unknown';
4864
+ const category = label === 'applied' || label === 'committed' || label === 'checked' || label === 'superseded' || label === 'evidence-only' || label === 'no-change' || label === 'research-complete' || label === 'generated-by-collector'
3242
4865
  ? 'success'
3243
4866
  : label === 'patch-missing' || label === 'bundle-missing'
3244
4867
  ? 'incomplete'
@@ -3264,6 +4887,65 @@ export function normalizeSwarmTerminalOutcome(input = {}) {
3264
4887
  ...(typeof input !== 'string' && toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
3265
4888
  };
3266
4889
  }
4890
+ export function createSwarmTerminalOutcomeRecord(input = {}) {
4891
+ const generatedAt = input.generatedAt ?? Date.now();
4892
+ const queueItemIds = uniqueStrings([input.queueItemId, ...(input.queueItemIds ?? [])]);
4893
+ const subjectId = input.subjectId?.trim()
4894
+ || queueItemIds[0]
4895
+ || input.taskId?.trim()
4896
+ || input.jobId?.trim()
4897
+ || input.id?.trim()
4898
+ || 'unknown-subject';
4899
+ const subjectAliases = uniqueStrings([
4900
+ input.subjectId,
4901
+ ...(input.subjectAliases ?? []),
4902
+ ...queueItemIds,
4903
+ input.taskId,
4904
+ input.jobId
4905
+ ]);
4906
+ const outcome = normalizeSwarmTerminalOutcome(terminalOutcomeInputForRecord(input));
4907
+ const status = terminalOutcomeRecordStatusFromText(input.status)
4908
+ ?? terminalOutcomeRecordStatusFromOutcome(outcome);
4909
+ const reasonCodes = normalizeSwarmTerminalOutcomeReasonCodes([
4910
+ ...(input.reasonCodes ?? []),
4911
+ ...defaultTerminalOutcomeRecordReasonCodes(status)
4912
+ ]);
4913
+ const admitted = input.admitted ?? terminalOutcomeRecordStatusIsAdmitted(status);
4914
+ const evidenceRefs = normalizeNamedRefs(input.evidenceRefs ?? [], 'evidence');
4915
+ return {
4916
+ kind: FRONTIER_SWARM_TERMINAL_OUTCOME_RECORD_KIND,
4917
+ version: FRONTIER_SWARM_TERMINAL_OUTCOME_RECORD_VERSION,
4918
+ id: input.id ?? 'swarm-terminal-outcome-record:' + stableHash([subjectId, subjectAliases, status, reasonCodes, generatedAt]),
4919
+ generatedAt,
4920
+ subjectId,
4921
+ subjectAliases: subjectAliases.length ? subjectAliases : [subjectId],
4922
+ ...(input.jobId ? { jobId: input.jobId } : {}),
4923
+ ...(input.taskId ? { taskId: input.taskId } : {}),
4924
+ ...(input.queueId ? { queueId: input.queueId } : {}),
4925
+ queueItemIds,
4926
+ ...(input.lane ? { lane: input.lane } : {}),
4927
+ ...(input.source ? { source: input.source } : {}),
4928
+ status,
4929
+ outcome,
4930
+ terminal: true,
4931
+ admitted,
4932
+ success: outcome.success,
4933
+ rejected: status === 'rejected',
4934
+ conflict: status === 'conflict',
4935
+ humanNeeded: status === 'human-needed',
4936
+ rerun: status === 'rerun',
4937
+ noChange: status === 'no-change',
4938
+ researchComplete: status === 'research-complete',
4939
+ reasonCodes,
4940
+ reasons: uniqueStrings(input.reasons ?? []),
4941
+ conflictingJobIds: uniqueStrings(input.conflictingJobIds ?? []),
4942
+ supersedes: uniqueStrings(input.supersedes ?? []),
4943
+ ...(input.supersededBy ? { supersededBy: input.supersededBy } : {}),
4944
+ ...(input.rerunOf ? { rerunOf: input.rerunOf } : {}),
4945
+ evidenceRefs,
4946
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
4947
+ };
4948
+ }
3267
4949
  export function reconcileSwarmTerminalState(input = {}) {
3268
4950
  const generatedAt = input.generatedAt ?? Date.now();
3269
4951
  const doneBucket = input.doneBucket ?? 'done';
@@ -4054,13 +5736,25 @@ function createMergeIndexConflicts(entries) {
4054
5736
  continue;
4055
5737
  const keys = pairConflictKeys(left, right);
4056
5738
  for (const key of keys) {
4057
- const kind = key.startsWith('symbol:') ? 'symbol' : key.startsWith('region:') ? 'region' : 'path';
5739
+ const kind = key.startsWith('symbol:')
5740
+ ? 'symbol'
5741
+ : key.startsWith('public-contract:')
5742
+ ? 'public-contract'
5743
+ : key.startsWith('region:')
5744
+ ? 'region'
5745
+ : 'path';
4058
5746
  const value = key.slice(key.indexOf(':') + 1);
4059
5747
  conflicts.push({
4060
5748
  jobIds: [left.jobId, right.jobId].sort(),
4061
5749
  key,
4062
5750
  kind,
4063
- ...(kind === 'region' ? { region: value } : kind === 'symbol' ? { symbol: value } : { path: value })
5751
+ ...(kind === 'region'
5752
+ ? { region: value }
5753
+ : kind === 'symbol'
5754
+ ? { symbol: value }
5755
+ : kind === 'public-contract'
5756
+ ? { publicContract: value }
5757
+ : { path: value })
4064
5758
  });
4065
5759
  }
4066
5760
  }
@@ -4077,6 +5771,9 @@ function pairConflictKeys(left, right) {
4077
5771
  const sharedSymbols = leftKeys.filter((key) => key.startsWith('symbol:') && rightKeySet.has(key));
4078
5772
  if (sharedSymbols.length > 0)
4079
5773
  return sharedSymbols.sort();
5774
+ const sharedPublicContracts = leftKeys.filter((key) => key.startsWith('public-contract:') && rightKeySet.has(key));
5775
+ if (sharedPublicContracts.length > 0)
5776
+ return sharedPublicContracts.sort();
4080
5777
  const leftRegions = leftKeys.filter((key) => key.startsWith('region:'));
4081
5778
  const rightRegions = rightKeys.filter((key) => key.startsWith('region:'));
4082
5779
  if (leftRegions.length > 0 && rightRegions.length > 0) {
@@ -4137,6 +5834,11 @@ function stableIdPart(value) {
4137
5834
  return 'star';
4138
5835
  return normalized.length > 0 ? normalized : 'anonymous';
4139
5836
  }
5837
+ function publicContractConflictValue(value) {
5838
+ const trimmed = value.trim();
5839
+ const withoutPrefix = trimmed.startsWith('public-contract:') ? trimmed.slice('public-contract:'.length) : trimmed;
5840
+ return stableIdPart(withoutPrefix);
5841
+ }
4140
5842
  function reviewerLaneReasons(entry) {
4141
5843
  const reasons = [];
4142
5844
  if (entry.conflictingJobIds.length)
@@ -5186,6 +6888,8 @@ function defaultQueueOutcomeForCategory(category, input, search) {
5186
6888
  return 'committed';
5187
6889
  if (action === 'apply-local' || input.decision === 'applied' || queueOutcomeHas(search, 'applied', 'apply-local', 'apply'))
5188
6890
  return 'applied';
6891
+ if (input.decision === 'research-complete' || queueOutcomeHas(search, 'research-complete', 'discovery-complete', 'synthesized'))
6892
+ return 'research-complete';
5189
6893
  if (input.decision === 'checked' || queueOutcomeHas(search, 'checked', 'check'))
5190
6894
  return 'checked';
5191
6895
  if (input.decision === 'superseded' || queueOutcomeHas(search, 'superseded'))
@@ -5249,6 +6953,8 @@ function canonicalizeSwarmQueueOutcome(value) {
5249
6953
  return 'superseded';
5250
6954
  if (token === 'no-change' || token === 'nochange' || token === 'no-op' || token === 'noop' || token === 'unchanged')
5251
6955
  return 'no-change';
6956
+ if (token === 'research-complete' || token === 'discovery-complete' || token === 'synthesized')
6957
+ return 'research-complete';
5252
6958
  if (token === 'rejected' || token === 'reject' || token === 'failed' || token === 'failure')
5253
6959
  return 'rejected';
5254
6960
  if (token === 'rerun' || token === 're-run' || token === 'retry' || token === 'needs-rerun' || token === 'stale-rerun')
@@ -5263,6 +6969,96 @@ function canonicalizeSwarmQueueOutcome(value) {
5263
6969
  return 'closed';
5264
6970
  return undefined;
5265
6971
  }
6972
+ function terminalOutcomeInputForRecord(input) {
6973
+ if (input.outcome && typeof input.outcome === 'object')
6974
+ return input.outcome;
6975
+ return {
6976
+ label: input.label,
6977
+ outcome: typeof input.outcome === 'string' ? input.outcome : undefined,
6978
+ status: input.status,
6979
+ decision: input.decision,
6980
+ reasons: input.reasons,
6981
+ metadata: input.metadata
6982
+ };
6983
+ }
6984
+ function terminalOutcomeRecordStatusFromText(value) {
6985
+ const normalized = normalizeOptionalString(value);
6986
+ if (!normalized)
6987
+ return undefined;
6988
+ const token = normalizeSwarmTerminalOutcomeText(normalized);
6989
+ if (token === 'applied' || token === 'apply-local' || token === 'apply' || token === 'committed' || token === 'commit' || token === 'checked' || token === 'check')
6990
+ return 'applied';
6991
+ if (token === 'rejected' || token === 'reject' || token === 'failed' || token === 'failure')
6992
+ return 'rejected';
6993
+ if (token === 'superseded')
6994
+ return 'superseded';
6995
+ if (token === 'no-change' || token === 'nochange' || token === 'no-op' || token === 'noop' || token === 'unchanged' || token === 'recorded' || token === 'closed' || token === 'evidence-only' || token === 'evidenceonly' || token === 'generated-by-collector')
6996
+ return 'no-change';
6997
+ if (token === 'conflict' || token === 'conflict-blocked' || token === 'merge-conflict' || token === 'textual-conflict')
6998
+ return 'conflict';
6999
+ if (token === 'human-needed' || token === 'human-question' || token === 'human-blocked' || token === 'blocked' || token === 'coordinator-review' || token === 'needs-port' || token === 'escalated' || token === 'review')
7000
+ return 'human-needed';
7001
+ if (token === 'research-complete' || token === 'discovery-complete' || token === 'synthesized')
7002
+ return 'research-complete';
7003
+ if (token === 'rerun' || token === 're-run' || token === 'retry' || token === 'needs-rerun' || token === 'stale-rerun')
7004
+ return 'rerun';
7005
+ return token;
7006
+ }
7007
+ function terminalOutcomeRecordStatusFromOutcome(outcome) {
7008
+ return terminalOutcomeRecordStatusFromText(outcome.label)
7009
+ ?? terminalOutcomeRecordStatusFromText(outcome.category)
7010
+ ?? 'no-change';
7011
+ }
7012
+ function terminalOutcomeRecordStatusIsAdmitted(status) {
7013
+ return status === 'applied'
7014
+ || status === 'superseded'
7015
+ || status === 'no-change'
7016
+ || status === 'research-complete';
7017
+ }
7018
+ function defaultTerminalOutcomeRecordReasonCodes(status) {
7019
+ if (status === 'applied')
7020
+ return ['accepted-by-admission'];
7021
+ if (status === 'rejected')
7022
+ return ['failed-verification'];
7023
+ if (status === 'superseded')
7024
+ return ['superseded-by-newer-output'];
7025
+ if (status === 'no-change')
7026
+ return ['no-effective-change'];
7027
+ if (status === 'conflict')
7028
+ return ['conflict-detected'];
7029
+ if (status === 'human-needed')
7030
+ return ['human-decision-required'];
7031
+ if (status === 'research-complete')
7032
+ return ['research-complete'];
7033
+ if (status === 'rerun')
7034
+ return ['stale-rerun-required'];
7035
+ return [];
7036
+ }
7037
+ function normalizeSwarmTerminalOutcomeReasonCodes(reasonCodes) {
7038
+ return uniqueStrings(uniqueStrings(reasonCodes).map(canonicalizeSwarmTerminalOutcomeReasonCode));
7039
+ }
7040
+ function canonicalizeSwarmTerminalOutcomeReasonCode(value) {
7041
+ const token = normalizeSwarmTerminalOutcomeText(value);
7042
+ if (!token)
7043
+ return value;
7044
+ if (token === 'accepted' || token === 'admitted' || token === 'accepted-by-admission')
7045
+ return 'accepted-by-admission';
7046
+ if (token === 'failed' || token === 'failed-verification' || token === 'verification-failed')
7047
+ return 'failed-verification';
7048
+ if (token === 'superseded' || token === 'superseded-by-newer-output')
7049
+ return 'superseded-by-newer-output';
7050
+ if (token === 'no-change' || token === 'nochange' || token === 'no-effective-change')
7051
+ return 'no-effective-change';
7052
+ if (token === 'conflict' || token === 'conflict-detected' || token === 'merge-conflict')
7053
+ return 'conflict-detected';
7054
+ if (token === 'human-needed' || token === 'human-decision-required' || token === 'human-question')
7055
+ return 'human-decision-required';
7056
+ if (token === 'research-complete' || token === 'discovery-complete')
7057
+ return 'research-complete';
7058
+ if (token === 'rerun' || token === 'stale-rerun-required' || token === 'needs-rerun')
7059
+ return 'stale-rerun-required';
7060
+ return token;
7061
+ }
5266
7062
  function queueOutcomeInputsFromMergeQueue(queue) {
5267
7063
  return queue.assignments.map((assignment) => ({
5268
7064
  subjectAliases: uniqueStrings([assignment.taskId, assignment.jobId, ...assignment.queueItemIds]),
@@ -5420,6 +7216,7 @@ function queueOutcomeDecisionIsResolvedOutput(decision) {
5420
7216
  || decision.outcome === 'checked'
5421
7217
  || decision.outcome === 'superseded'
5422
7218
  || decision.outcome === 'no-change'
7219
+ || decision.outcome === 'research-complete'
5423
7220
  || decision.outcome === 'recorded'
5424
7221
  || decision.outcome === 'closed';
5425
7222
  }
@@ -5867,6 +7664,253 @@ function countBy(values) {
5867
7664
  out[value] = (out[value] ?? 0) + 1;
5868
7665
  return out;
5869
7666
  }
7667
+ const FRONTIER_SWARM_BLOCKING_MERGE_CANDIDATE_REASON_CODES = new Set([
7668
+ 'missing-sidecar',
7669
+ 'empty-sidecar',
7670
+ 'stale-source-hash',
7671
+ 'symbol-conflict',
7672
+ 'effect-conflict'
7673
+ ]);
7674
+ const FRONTIER_SWARM_REVIEW_MERGE_CANDIDATE_REASON_CODES = new Set([
7675
+ 'tests-missing'
7676
+ ]);
7677
+ const FRONTIER_SWARM_CONFLICT_MERGE_CANDIDATE_REASON_CODES = new Set([
7678
+ 'symbol-conflict',
7679
+ 'effect-conflict'
7680
+ ]);
7681
+ function isSwarmSemanticChange(input) {
7682
+ return input.kind === FRONTIER_SWARM_SEMANTIC_CHANGE_KIND;
7683
+ }
7684
+ function isSwarmMergeCandidate(input) {
7685
+ return input.kind === FRONTIER_SWARM_MERGE_CANDIDATE_KIND;
7686
+ }
7687
+ function normalizeOptionalString(value) {
7688
+ if (typeof value !== 'string')
7689
+ return undefined;
7690
+ const normalized = value.trim();
7691
+ return normalized.length > 0 ? normalized : undefined;
7692
+ }
7693
+ function normalizeSwarmMergeCandidateReasonCodes(reasonCodes) {
7694
+ return uniqueStrings(reasonCodes);
7695
+ }
7696
+ function normalizeSwarmSourceSpan(input, fallback = {}) {
7697
+ const metadata = toJsonObject(input?.metadata);
7698
+ const startLine = readNonNegativeNumber(input?.startLine);
7699
+ const startColumn = readNonNegativeNumber(input?.startColumn);
7700
+ const endLine = readNonNegativeNumber(input?.endLine);
7701
+ const endColumn = readNonNegativeNumber(input?.endColumn);
7702
+ const startOffset = readNonNegativeNumber(input?.startOffset);
7703
+ const endOffset = readNonNegativeNumber(input?.endOffset);
7704
+ const path = normalizeOptionalString(input?.path) ?? normalizeOptionalString(fallback.sourcePath);
7705
+ const sourceHash = normalizeOptionalString(input?.sourceHash) ?? normalizeOptionalString(fallback.sourceHash);
7706
+ const expectedSourceHash = normalizeOptionalString(input?.expectedSourceHash) ?? normalizeOptionalString(fallback.expectedSourceHash);
7707
+ return {
7708
+ ...(path ? { path } : {}),
7709
+ ...(startLine !== undefined ? { startLine: Math.floor(startLine) } : {}),
7710
+ ...(startColumn !== undefined ? { startColumn: Math.floor(startColumn) } : {}),
7711
+ ...(endLine !== undefined ? { endLine: Math.floor(endLine) } : {}),
7712
+ ...(endColumn !== undefined ? { endColumn: Math.floor(endColumn) } : {}),
7713
+ ...(startOffset !== undefined ? { startOffset: Math.floor(startOffset) } : {}),
7714
+ ...(endOffset !== undefined ? { endOffset: Math.floor(endOffset) } : {}),
7715
+ ...(sourceHash ? { sourceHash } : {}),
7716
+ ...(expectedSourceHash ? { expectedSourceHash } : {}),
7717
+ ...(metadata ? { metadata } : {})
7718
+ };
7719
+ }
7720
+ function deriveSwarmSemanticChangeReasonCodes(input, sourceSpan) {
7721
+ const reasons = [];
7722
+ if (input.conflictReason)
7723
+ reasons.push(input.conflictReason);
7724
+ if (sourceSpan.sourceHash && sourceSpan.expectedSourceHash && sourceSpan.sourceHash !== sourceSpan.expectedSourceHash) {
7725
+ reasons.push('stale-source-hash');
7726
+ }
7727
+ return normalizeSwarmMergeCandidateReasonCodes(reasons);
7728
+ }
7729
+ function deriveSwarmMergeCandidateReasonCodes(input, semanticChanges, sourceSpan) {
7730
+ const reasons = [];
7731
+ if (input.sidecarRequired && !input.sidecarPath)
7732
+ reasons.push('missing-sidecar');
7733
+ if (input.sidecarEmpty)
7734
+ reasons.push('empty-sidecar');
7735
+ if (input.hasLosses)
7736
+ reasons.push('lossy-import');
7737
+ if (input.testsRequired && input.testsPassed !== true)
7738
+ reasons.push('tests-missing');
7739
+ if (input.conflictReason)
7740
+ reasons.push(input.conflictReason);
7741
+ if (sourceSpan.sourceHash && sourceSpan.expectedSourceHash && sourceSpan.sourceHash !== sourceSpan.expectedSourceHash) {
7742
+ reasons.push('stale-source-hash');
7743
+ }
7744
+ if (semanticChanges.length === 0 && input.sidecarPath)
7745
+ reasons.push('empty-sidecar');
7746
+ return normalizeSwarmMergeCandidateReasonCodes(reasons);
7747
+ }
7748
+ function normalizeSwarmSemanticConfidence(value) {
7749
+ if (typeof value === 'number' && Number.isFinite(value))
7750
+ return clamp01(value);
7751
+ const normalized = normalizeOptionalString(value);
7752
+ return normalized ?? 'medium';
7753
+ }
7754
+ function normalizeSwarmConflictReason(conflictReason, reasonCodes) {
7755
+ const explicit = normalizeOptionalString(conflictReason);
7756
+ if (explicit)
7757
+ return explicit;
7758
+ return reasonCodes.find((reason) => FRONTIER_SWARM_CONFLICT_MERGE_CANDIDATE_REASON_CODES.has(reason));
7759
+ }
7760
+ function normalizeGraphNode(input, generatedAt = Date.now()) {
7761
+ const metadata = toJsonObject(input.metadata);
7762
+ const id = input.id ?? graphNodeId(input.kind, input.title ?? input.jobId ?? input.taskId ?? String(generatedAt));
7763
+ return {
7764
+ id,
7765
+ kind: input.kind,
7766
+ ...(input.title ? { title: input.title } : {}),
7767
+ ...(input.status ? { status: input.status } : {}),
7768
+ ...(input.jobId ? { jobId: input.jobId } : {}),
7769
+ ...(input.taskId ? { taskId: input.taskId } : {}),
7770
+ ...(input.path ? { path: input.path } : {}),
7771
+ generatedAt: input.generatedAt ?? generatedAt,
7772
+ ...(metadata ? { metadata } : {})
7773
+ };
7774
+ }
7775
+ function normalizeGraphEdge(input, generatedAt = Date.now()) {
7776
+ const metadata = toJsonObject(input.metadata);
7777
+ return {
7778
+ id: input.id ?? `${input.kind}:${input.from}->${input.to}`,
7779
+ from: input.from,
7780
+ to: input.to,
7781
+ kind: input.kind,
7782
+ ...(input.label ? { label: input.label } : {}),
7783
+ generatedAt: input.generatedAt ?? generatedAt,
7784
+ ...(metadata ? { metadata } : {})
7785
+ };
7786
+ }
7787
+ function normalizeGraphEvent(input, generatedAt = Date.now()) {
7788
+ const timestamp = input.timestamp ?? generatedAt;
7789
+ const metadata = toJsonObject(input.metadata);
7790
+ return {
7791
+ id: input.id ?? 'swarm-graph-event:' + stableHash([input.type, input.nodeId, input.edgeId, timestamp]),
7792
+ type: input.type,
7793
+ ...(input.nodeId ? { nodeId: input.nodeId } : {}),
7794
+ ...(input.edgeId ? { edgeId: input.edgeId } : {}),
7795
+ timestamp,
7796
+ ...(metadata ? { metadata } : {})
7797
+ };
7798
+ }
7799
+ function isPassingGraphStatus(status) {
7800
+ return status === 'passed' || status === 'accepted' || status === 'verified' || status === 'completed';
7801
+ }
7802
+ function isBlockingGraphStatus(status) {
7803
+ return status === 'failed' || status === 'rejected' || status === 'blocked';
7804
+ }
7805
+ function dedupeGraphNodes(nodes) {
7806
+ const byId = new Map();
7807
+ for (const node of nodes)
7808
+ byId.set(node.id, mergeGraphNode(byId.get(node.id), node));
7809
+ return Array.from(byId.values()).sort((left, right) => left.id.localeCompare(right.id));
7810
+ }
7811
+ function mergeGraphNode(existing, node) {
7812
+ if (!existing)
7813
+ return node;
7814
+ return {
7815
+ ...existing,
7816
+ ...node,
7817
+ metadata: mergeGraphMetadata(existing.metadata, node.metadata)
7818
+ };
7819
+ }
7820
+ function dedupeGraphEdges(edges) {
7821
+ const byId = new Map();
7822
+ for (const edge of edges)
7823
+ byId.set(edge.id, edge);
7824
+ return Array.from(byId.values()).sort((left, right) => left.id.localeCompare(right.id));
7825
+ }
7826
+ function summarizeSwarmRunGraph(nodes, edges, events) {
7827
+ return {
7828
+ nodeCount: nodes.length,
7829
+ edgeCount: edges.length,
7830
+ eventCount: events.length,
7831
+ nodeKinds: countBy(nodes.map((node) => node.kind)),
7832
+ edgeKinds: countBy(edges.map((edge) => edge.kind)),
7833
+ openBlockerCount: nodes.filter((node) => node.status === 'blocked').length + edges.filter((edge) => edge.kind === 'blocks').length,
7834
+ humanQuestionCount: nodes.filter((node) => node.kind === 'decision' && /human|question/i.test(node.title ?? '')).length
7835
+ };
7836
+ }
7837
+ function graphNodeId(kind, value) {
7838
+ return `${kind}:${normalizeGraphIdPart(value)}`;
7839
+ }
7840
+ function normalizeGraphIdPart(value) {
7841
+ return value.trim().toLowerCase().replace(/[^a-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '') || stableHash(value);
7842
+ }
7843
+ function normalizeSwarmGraphRef(input) {
7844
+ return {
7845
+ id: input.id ?? 'swarm-graph-ref:' + stableHash([input.kind, input.graphId, input.nodeId, input.edgeId, input.role]),
7846
+ kind: input.kind ?? 'graph',
7847
+ ...(input.graphId ? { graphId: input.graphId } : {}),
7848
+ ...(input.nodeId ? { nodeId: input.nodeId } : {}),
7849
+ ...(input.edgeId ? { edgeId: input.edgeId } : {}),
7850
+ ...(input.role ? { role: input.role } : {}),
7851
+ ...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
7852
+ };
7853
+ }
7854
+ function normalizeSwarmGraphRefs(input) {
7855
+ return input.map(normalizeSwarmGraphRef).sort((left, right) => left.id.localeCompare(right.id));
7856
+ }
7857
+ function normalizeSwarmPatchEvent(input, replayRecordId, index) {
7858
+ if (isSwarmPatchEvent(input) && (!replayRecordId || input.replayRecordId) && (index === undefined || input.sequence !== undefined)) {
7859
+ return input;
7860
+ }
7861
+ return createSwarmPatchEvent({
7862
+ ...input,
7863
+ replayRecordId: input.replayRecordId ?? replayRecordId,
7864
+ sequence: input.sequence ?? index
7865
+ });
7866
+ }
7867
+ function inferImprovementLoopStatus(input) {
7868
+ if (input.result !== undefined)
7869
+ return 'applied';
7870
+ if (input.action !== undefined)
7871
+ return 'planned';
7872
+ if (input.observation !== undefined)
7873
+ return 'observed';
7874
+ return 'unknown';
7875
+ }
7876
+ function createImprovementLoopPhaseNode(loop, phase, value, generatedAt) {
7877
+ return {
7878
+ id: graphNodeId('rsi', `${loop.id}:${phase}`),
7879
+ kind: 'rsi',
7880
+ title: `${loop.title} ${phase}`,
7881
+ status: loop.status,
7882
+ jobId: loop.jobId,
7883
+ taskId: loop.taskId,
7884
+ generatedAt,
7885
+ metadata: {
7886
+ phase,
7887
+ value,
7888
+ loop
7889
+ }
7890
+ };
7891
+ }
7892
+ function isSwarmGateRecord(input) {
7893
+ return input.kind === FRONTIER_SWARM_GATE_RECORD_KIND;
7894
+ }
7895
+ function isSwarmEvidenceRecord(input) {
7896
+ return input.kind === FRONTIER_SWARM_EVIDENCE_RECORD_KIND;
7897
+ }
7898
+ function isSwarmPatchEvent(input) {
7899
+ return input.kind === FRONTIER_SWARM_PATCH_EVENT_KIND;
7900
+ }
7901
+ function isSwarmReplayRecord(input) {
7902
+ return input.kind === FRONTIER_SWARM_REPLAY_RECORD_KIND;
7903
+ }
7904
+ function isSwarmImprovementLoop(input) {
7905
+ return input.kind === FRONTIER_SWARM_IMPROVEMENT_LOOP_KIND;
7906
+ }
7907
+ function mergeGraphMetadata(left, right) {
7908
+ if (!left)
7909
+ return right;
7910
+ if (!right)
7911
+ return left;
7912
+ return { ...left, ...right };
7913
+ }
5870
7914
  function hasJobDependencyCycle(start, dependenciesByJobId) {
5871
7915
  const visiting = new Set();
5872
7916
  const visited = new Set();
@@ -8123,6 +10167,141 @@ function joinPathParts(...parts) {
8123
10167
  .filter(Boolean)
8124
10168
  .join('/');
8125
10169
  }
10170
+ function swarmRunEventTime(options, fallback) {
10171
+ if (options.time)
10172
+ return options.time;
10173
+ const value = options.now ?? fallback;
10174
+ return new Date(Number.isFinite(value) ? value : Date.now()).toISOString();
10175
+ }
10176
+ function isFrontierRunEventList(input) {
10177
+ return Array.isArray(input);
10178
+ }
10179
+ function swarmRunLaneNodeId(lane) {
10180
+ return String(lane).startsWith('lane:') ? String(lane) : `lane:${lane}`;
10181
+ }
10182
+ function swarmRunTaskNodeId(taskId) {
10183
+ return String(taskId).startsWith('task:') ? String(taskId) : `task:${taskId}`;
10184
+ }
10185
+ function swarmRunAttemptNodeId(jobId) {
10186
+ return String(jobId).startsWith('attempt:') ? String(jobId) : `attempt:${jobId}`;
10187
+ }
10188
+ function swarmRunLeaseNodeId(leaseId) {
10189
+ return String(leaseId).startsWith('lease:') ? String(leaseId) : `lease:${leaseId}`;
10190
+ }
10191
+ function swarmRunPatchNodeId(jobId) {
10192
+ return String(jobId).startsWith('patch:') ? String(jobId) : `patch:${jobId}`;
10193
+ }
10194
+ function swarmRunArtifactNodeId(artifactPath) {
10195
+ return `artifact:${slug(artifactPath)}:${stableHash(artifactPath).slice('fnv1a32:'.length)}`;
10196
+ }
10197
+ function swarmRunEvidenceNodeId(jobId, evidencePath) {
10198
+ return `evidence:${slug(jobId)}:${stableHash(evidencePath).slice('fnv1a32:'.length)}`;
10199
+ }
10200
+ function swarmRunVerificationNodeId(jobId, index, name) {
10201
+ return `verification:${slug(jobId)}:${index}:${slug(name || 'command')}`;
10202
+ }
10203
+ function swarmRunDecisionNodeId(id) {
10204
+ return String(id).startsWith('decision:') ? String(id) : `decision:${id}`;
10205
+ }
10206
+ function inferSwarmLanePackageId(jobs) {
10207
+ for (const job of jobs) {
10208
+ const candidate = job.task.metadata?.packageId ?? job.metadata?.packageId ?? job.task.metadata?.package ?? job.metadata?.package;
10209
+ if (typeof candidate === 'string' && candidate)
10210
+ return candidate;
10211
+ }
10212
+ return undefined;
10213
+ }
10214
+ function formatSwarmCommandLine(command) {
10215
+ return [command.command, ...command.args].join(' ');
10216
+ }
10217
+ function swarmStatusToRunStatus(status) {
10218
+ if (status === 'completed' || status === 'verified')
10219
+ return 'done';
10220
+ if (status === 'failed' || status === 'rejected')
10221
+ return 'rejected';
10222
+ if (status === 'blocked')
10223
+ return 'blocked';
10224
+ if (status === 'running')
10225
+ return 'running';
10226
+ if (status === 'planned')
10227
+ return 'planned';
10228
+ return 'ready';
10229
+ }
10230
+ function swarmResultStatusToAttemptStatus(status) {
10231
+ if (status === 'completed' || status === 'verified')
10232
+ return 'completed';
10233
+ if (status === 'running')
10234
+ return 'running';
10235
+ if (status === 'cancelled')
10236
+ return 'cancelled';
10237
+ if (status === 'timed-out' || status === 'timeout')
10238
+ return 'timed-out';
10239
+ if (status === 'queued' || status === 'ready' || status === 'planned')
10240
+ return 'queued';
10241
+ return 'failed';
10242
+ }
10243
+ function swarmRiskToRunRisk(risk) {
10244
+ if (risk === 'low' || risk === 'medium' || risk === 'high')
10245
+ return risk;
10246
+ return 'medium';
10247
+ }
10248
+ function inferSwarmArtifactType(file) {
10249
+ const lower = file.toLowerCase();
10250
+ if (lower.endsWith('.patch') || lower.endsWith('.diff'))
10251
+ return 'patch';
10252
+ if (lower.endsWith('.json') || lower.endsWith('.jsonl'))
10253
+ return 'json';
10254
+ if (lower.endsWith('.log') || lower.endsWith('.txt') || lower.includes('stdout') || lower.includes('stderr'))
10255
+ return 'log';
10256
+ if (lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.jpeg') || lower.endsWith('.webp'))
10257
+ return 'image';
10258
+ return 'artifact';
10259
+ }
10260
+ function swarmDispositionToRunDecision(disposition, readiness) {
10261
+ if (disposition === 'auto-mergeable' || readiness === 'verified-patch')
10262
+ return 'apply';
10263
+ if (disposition === 'stale-against-head')
10264
+ return 'rerun';
10265
+ if (disposition === 'rejected' || readiness === 'rejected')
10266
+ return 'reject';
10267
+ if (disposition === 'needs-port' || disposition === 'blocked' || readiness === 'blocked')
10268
+ return 'human-question';
10269
+ return 'record-only';
10270
+ }
10271
+ function swarmQueueOutcomeToRunDecision(decision) {
10272
+ if (decision.outcome === 'applied' || decision.decision === 'applied' || decision.action === 'apply-local')
10273
+ return 'apply';
10274
+ if (decision.outcome === 'rerun' || decision.decision === 'rerun' || decision.staleOrRerun)
10275
+ return 'rerun';
10276
+ if (decision.outcome === 'rejected' || decision.decision === 'rejected')
10277
+ return 'reject';
10278
+ if (decision.action === 'promote')
10279
+ return 'promote';
10280
+ if (decision.humanBlocked || decision.outcome === 'human-blocked')
10281
+ return 'human-question';
10282
+ return 'record-only';
10283
+ }
10284
+ function swarmQueueOutcomeRequiredActions(decision) {
10285
+ const actions = [];
10286
+ if (decision.coordinatorReview || decision.reviewDebt)
10287
+ actions.push('coordinator-review');
10288
+ if (decision.humanBlocked)
10289
+ actions.push('human-answer');
10290
+ if (decision.staleOrRerun)
10291
+ actions.push('rerun');
10292
+ if (decision.conflict)
10293
+ actions.push('resolve-conflict');
10294
+ return uniqueStrings(actions);
10295
+ }
10296
+ function pruneUndefinedJsonObject(value) {
10297
+ const out = {};
10298
+ for (const [key, entry] of Object.entries(value)) {
10299
+ if (entry === undefined)
10300
+ continue;
10301
+ out[key] = toJsonValue(entry);
10302
+ }
10303
+ return out;
10304
+ }
8126
10305
  function normalizeId(value, label) {
8127
10306
  const id = String(value || '').trim();
8128
10307
  if (!id)