sentinelayer-cli 0.8.11 → 0.8.12

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.
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { randomUUID } from "node:crypto";
9
+ import path from "node:path";
9
10
 
10
11
  import { runAiReviewLayer } from "./ai-review.js";
11
12
  import { buildPersonaReviewPrompt, PERSONA_IDS } from "./persona-prompts.js";
@@ -20,6 +21,19 @@ const OMAR_ORCHESTRATOR_AGENT = Object.freeze({
20
21
  persona: "Omar Gate Orchestrator",
21
22
  });
22
23
 
24
+ const OMAR_SWARM_THRESHOLDS = Object.freeze({
25
+ minFilesForSwarm: 15,
26
+ minRouteGroupsForSwarm: 3,
27
+ minLocForSwarm: 5000,
28
+ maxFilesPerScanner: 12,
29
+ maxConcurrentAgents: 4,
30
+ });
31
+
32
+ const OMARGATE_DEFAULT_CONFIDENCE_FLOOR = 0.7;
33
+ const OMARGATE_CONFIDENCE_FLOORS = Object.freeze(
34
+ Object.fromEntries(PERSONA_IDS.map((personaId) => [personaId, OMARGATE_DEFAULT_CONFIDENCE_FLOOR]))
35
+ );
36
+
23
37
  /**
24
38
  * Run bounded-concurrency parallel execution.
25
39
  * @param {Array} items
@@ -32,9 +46,8 @@ async function runWithConcurrency(items, maxConcurrent, fn) {
32
46
  const executing = new Set();
33
47
 
34
48
  for (const item of items) {
35
- const p = fn(item).then((result) => {
49
+ const p = Promise.resolve().then(() => fn(item)).finally(() => {
36
50
  executing.delete(p);
37
- return result;
38
51
  });
39
52
  executing.add(p);
40
53
  results.push(p);
@@ -47,6 +60,192 @@ async function runWithConcurrency(items, maxConcurrent, fn) {
47
60
  return Promise.allSettled(results);
48
61
  }
49
62
 
63
+ function normalizeString(value) {
64
+ return String(value || "").trim();
65
+ }
66
+
67
+ function normalizeNumber(value, fallback = 0) {
68
+ const parsed = Number(value);
69
+ if (!Number.isFinite(parsed)) {
70
+ return fallback;
71
+ }
72
+ return parsed;
73
+ }
74
+
75
+ function toPosixPath(value) {
76
+ return normalizeString(value).replace(/\\/g, "/");
77
+ }
78
+
79
+ function uniqueScopeFiles(files = []) {
80
+ const seen = new Set();
81
+ const normalized = [];
82
+ for (const item of Array.isArray(files) ? files : []) {
83
+ const rawPath =
84
+ typeof item === "string"
85
+ ? item
86
+ : item?.path || item?.file || item?.relativePath || "";
87
+ const filePath = toPosixPath(rawPath);
88
+ if (!filePath || seen.has(filePath)) {
89
+ continue;
90
+ }
91
+ seen.add(filePath);
92
+ const loc =
93
+ typeof item === "object" && item
94
+ ? Math.max(0, Math.floor(normalizeNumber(item.loc ?? item.lines ?? item.lineCount, 0)))
95
+ : 0;
96
+ normalized.push({ path: filePath, loc });
97
+ }
98
+ return normalized;
99
+ }
100
+
101
+ function filesFromScope(scope = {}) {
102
+ if (Array.isArray(scope.files)) {
103
+ return uniqueScopeFiles(scope.files);
104
+ }
105
+ if (Array.isArray(scope.primary)) {
106
+ return uniqueScopeFiles(scope.primary);
107
+ }
108
+ if (Array.isArray(scope.scannedRelativeFiles)) {
109
+ return uniqueScopeFiles(scope.scannedRelativeFiles);
110
+ }
111
+ return [];
112
+ }
113
+
114
+ function estimateScopeLoc(scope = {}, files = []) {
115
+ const explicit =
116
+ normalizeNumber(scope.totalLoc, 0) ||
117
+ normalizeNumber(scope.estimatedLoc, 0) ||
118
+ normalizeNumber(scope.summary?.totalLoc, 0);
119
+ if (explicit > 0) {
120
+ return Math.floor(explicit);
121
+ }
122
+ return files.reduce((sum, file) => sum + (file.loc > 0 ? file.loc : 80), 0);
123
+ }
124
+
125
+ function detectRouteGroups(files = []) {
126
+ const routeGroups = new Set();
127
+ for (const file of files) {
128
+ const filePath = toPosixPath(file.path || file);
129
+ const match = filePath.match(/(?:^|\/)(?:app|pages|routes)\/([^/]+)/);
130
+ if (match?.[1]) {
131
+ routeGroups.add(match[1]);
132
+ }
133
+ }
134
+ return [...routeGroups].sort();
135
+ }
136
+
137
+ /**
138
+ * Build the OmarGate persona file scope from deterministic review output.
139
+ */
140
+ export function buildPersonaFileScope({ deterministic = {} } = {}) {
141
+ const rawScope = deterministic?.scope || {};
142
+ const ingestSummary = deterministic?.layers?.ingest?.summary || {};
143
+ const files = filesFromScope(rawScope);
144
+ const totalLoc =
145
+ normalizeNumber(rawScope.totalLoc, 0) ||
146
+ normalizeNumber(rawScope.estimatedLoc, 0) ||
147
+ normalizeNumber(ingestSummary.totalLoc, 0) ||
148
+ estimateScopeLoc(rawScope, files);
149
+
150
+ return {
151
+ files,
152
+ scannedFiles: files.length,
153
+ scannedRelativeFiles: files.map((file) => file.path),
154
+ totalLoc: Math.floor(totalLoc),
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Decide whether an OmarGate persona should fan out into scoped subagents.
160
+ */
161
+ export function decideSwarm({ scope = {} } = {}) {
162
+ const files = filesFromScope(scope);
163
+ const routeGroups = detectRouteGroups(files);
164
+ const estimatedLoc = estimateScopeLoc(scope, files);
165
+ const spawn =
166
+ files.length > OMAR_SWARM_THRESHOLDS.minFilesForSwarm ||
167
+ routeGroups.length >= OMAR_SWARM_THRESHOLDS.minRouteGroupsForSwarm ||
168
+ estimatedLoc > OMAR_SWARM_THRESHOLDS.minLocForSwarm;
169
+
170
+ let reason = "below all thresholds";
171
+ if (files.length > OMAR_SWARM_THRESHOLDS.minFilesForSwarm) {
172
+ reason = `${files.length} files exceeds threshold (${OMAR_SWARM_THRESHOLDS.minFilesForSwarm})`;
173
+ } else if (routeGroups.length >= OMAR_SWARM_THRESHOLDS.minRouteGroupsForSwarm) {
174
+ reason = `${routeGroups.length} route groups exceeds threshold (${OMAR_SWARM_THRESHOLDS.minRouteGroupsForSwarm})`;
175
+ } else if (estimatedLoc > OMAR_SWARM_THRESHOLDS.minLocForSwarm) {
176
+ reason = `${estimatedLoc} LOC exceeds threshold (${OMAR_SWARM_THRESHOLDS.minLocForSwarm})`;
177
+ }
178
+
179
+ return {
180
+ spawn,
181
+ fileCount: files.length,
182
+ routeGroups: routeGroups.length,
183
+ routeGroupNames: routeGroups,
184
+ estimatedLoc,
185
+ reason,
186
+ thresholds: { ...OMAR_SWARM_THRESHOLDS },
187
+ maxConcurrent: OMAR_SWARM_THRESHOLDS.maxConcurrentAgents,
188
+ };
189
+ }
190
+
191
+ export function partitionFiles(files = [], maxPerPartition = OMAR_SWARM_THRESHOLDS.maxFilesPerScanner) {
192
+ const normalizedFiles = uniqueScopeFiles(files);
193
+ const partitionSize = Math.max(1, Math.floor(normalizeNumber(maxPerPartition, OMAR_SWARM_THRESHOLDS.maxFilesPerScanner)));
194
+ const partitions = [];
195
+ for (let index = 0; index < normalizedFiles.length; index += partitionSize) {
196
+ partitions.push(normalizedFiles.slice(index, index + partitionSize));
197
+ }
198
+ return partitions;
199
+ }
200
+
201
+ export function divideSwarmBudget(perPersonaCost, subagentCount) {
202
+ const count = Math.max(1, Math.floor(normalizeNumber(subagentCount, 1)));
203
+ const maxCostUsd = Math.max(0, normalizeNumber(perPersonaCost, 0)) / count;
204
+ return {
205
+ maxCostUsd,
206
+ subagentCount: count,
207
+ };
208
+ }
209
+
210
+ function summarizeFindings(findings = []) {
211
+ const summary = { P0: 0, P1: 0, P2: 0, P3: 0 };
212
+ for (const finding of findings) {
213
+ const severity = normalizeString(finding.severity).toUpperCase();
214
+ if (summary[severity] !== undefined) {
215
+ summary[severity] += 1;
216
+ } else {
217
+ summary.P3 += 1;
218
+ }
219
+ }
220
+ return {
221
+ ...summary,
222
+ blocking: summary.P0 > 0 || summary.P1 > 0,
223
+ };
224
+ }
225
+
226
+ function personaAgentFromIdentity(identity = {}) {
227
+ return {
228
+ id: identity.id,
229
+ persona: identity.fullName,
230
+ shortName: identity.shortName,
231
+ color: identity.color,
232
+ avatar: identity.avatar,
233
+ domain: identity.domain,
234
+ };
235
+ }
236
+
237
+ function buildSubagentIdentity(identity, subagentIndex) {
238
+ return {
239
+ id: `${identity.id}-subagent-${subagentIndex}`,
240
+ persona: `${identity.fullName} Subagent ${subagentIndex}`,
241
+ shortName: `${identity.shortName || identity.id} #${subagentIndex}`,
242
+ color: identity.color,
243
+ avatar: identity.avatar,
244
+ domain: identity.domain,
245
+ parentId: identity.id,
246
+ };
247
+ }
248
+
50
249
  /**
51
250
  * Annotate persona result with visual identity so stream consumers
52
251
  * and downstream reports never see faceless persona IDs.
@@ -67,6 +266,309 @@ function decoratePersonaResult(personaId, baseResult) {
67
266
  };
68
267
  }
69
268
 
269
+ function omargateConfidenceFloorForPersona(personaId) {
270
+ return OMARGATE_CONFIDENCE_FLOORS[personaId] || OMARGATE_DEFAULT_CONFIDENCE_FLOOR;
271
+ }
272
+
273
+ async function runOmarPersonaSwarm({
274
+ personaId,
275
+ identity,
276
+ targetPath,
277
+ mode,
278
+ runId,
279
+ deterministic,
280
+ outputDir,
281
+ provider,
282
+ model,
283
+ perPersonaCost,
284
+ dryRun,
285
+ onEvent,
286
+ } = {}) {
287
+ const scope = buildPersonaFileScope({ deterministic });
288
+ const decision = decideSwarm({ scope });
289
+ if (!decision.spawn || scope.files.length === 0) {
290
+ return null;
291
+ }
292
+
293
+ const partitions = partitionFiles(scope.files, OMAR_SWARM_THRESHOLDS.maxFilesPerScanner);
294
+ const budget = divideSwarmBudget(perPersonaCost, partitions.length);
295
+ const maxConcurrent = Math.min(OMAR_SWARM_THRESHOLDS.maxConcurrentAgents, partitions.length);
296
+ const swarmRunId = `${runId}-${personaId}-swarm`;
297
+ const startedAt = Date.now();
298
+ const blackboard = [];
299
+
300
+ if (onEvent) {
301
+ onEvent(createAgentEvent({
302
+ event: "swarm_start",
303
+ agent: personaAgentFromIdentity(identity),
304
+ payload: {
305
+ runId,
306
+ swarmRunId,
307
+ personaId,
308
+ identity,
309
+ mode,
310
+ reason: decision.reason,
311
+ fileCount: decision.fileCount,
312
+ estimatedLoc: decision.estimatedLoc,
313
+ routeGroups: decision.routeGroups,
314
+ partitionCount: partitions.length,
315
+ maxFilesPerSubagent: OMAR_SWARM_THRESHOLDS.maxFilesPerScanner,
316
+ maxConcurrent,
317
+ perPersonaCost,
318
+ subagentMaxCostUsd: budget.maxCostUsd,
319
+ },
320
+ runId,
321
+ }));
322
+ }
323
+
324
+ const parentRunDirectory = deterministic?.artifacts?.runDirectory || targetPath;
325
+ const subagentResults = await runWithConcurrency(
326
+ partitions.map((files, index) => ({ files, subagentIndex: index + 1 })),
327
+ maxConcurrent,
328
+ async ({ files, subagentIndex }) => {
329
+ const subagentStart = Date.now();
330
+ const subagentIdentity = buildSubagentIdentity(identity, subagentIndex);
331
+ const scopedFiles = files.map((file) => file.path);
332
+ const subagentRunId = `${swarmRunId}-${subagentIndex}`;
333
+
334
+ if (onEvent) {
335
+ onEvent(createAgentEvent({
336
+ event: "agent_start",
337
+ agent: subagentIdentity,
338
+ payload: {
339
+ runId,
340
+ swarmRunId,
341
+ subagentRunId,
342
+ personaId,
343
+ identity,
344
+ subagentIndex,
345
+ partitionCount: partitions.length,
346
+ files: scopedFiles,
347
+ fileCount: scopedFiles.length,
348
+ maxCostUsd: budget.maxCostUsd,
349
+ },
350
+ runId,
351
+ }));
352
+ }
353
+
354
+ try {
355
+ const result = await runAiReviewLayer({
356
+ targetPath,
357
+ mode: "full",
358
+ runId: subagentRunId,
359
+ runDirectory: path.join(parentRunDirectory, "swarm", personaId, `subagent-${subagentIndex}`),
360
+ deterministic: {
361
+ ...deterministic,
362
+ scope: {
363
+ ...(deterministic?.scope || {}),
364
+ scannedFiles: scopedFiles.length,
365
+ scannedRelativeFiles: scopedFiles,
366
+ },
367
+ metadata: {
368
+ ...(deterministic?.metadata || {}),
369
+ omarSwarm: {
370
+ personaId,
371
+ subagentIndex,
372
+ partitionCount: partitions.length,
373
+ parentRunId: runId,
374
+ swarmRunId,
375
+ },
376
+ },
377
+ },
378
+ outputDir,
379
+ provider: provider || undefined,
380
+ model: model || undefined,
381
+ sessionId: `${subagentRunId}-ai`,
382
+ maxCostUsd: budget.maxCostUsd,
383
+ dryRun,
384
+ env: process.env,
385
+ });
386
+
387
+ const personaConfidenceFloor = omargateConfidenceFloorForPersona(personaId);
388
+ const findings = (result?.findings || []).map((finding) => {
389
+ const normalized = {
390
+ ...finding,
391
+ persona: personaId,
392
+ layer: personaId,
393
+ confidenceFloor: personaConfidenceFloor,
394
+ personaConfidenceFloor,
395
+ swarm: {
396
+ personaId,
397
+ subagentIndex,
398
+ partitionCount: partitions.length,
399
+ files: scopedFiles,
400
+ },
401
+ };
402
+ blackboard.push({
403
+ agentId: subagentIdentity.id,
404
+ source: personaId,
405
+ ...normalized,
406
+ });
407
+ return normalized;
408
+ });
409
+
410
+ if (onEvent) {
411
+ for (const finding of findings) {
412
+ onEvent(createAgentEvent({
413
+ event: "persona_finding",
414
+ agent: personaAgentFromIdentity(identity),
415
+ payload: { personaId, identity, subagentIndex, ...finding },
416
+ runId,
417
+ }));
418
+ }
419
+ onEvent(createAgentEvent({
420
+ event: "agent_complete",
421
+ agent: subagentIdentity,
422
+ payload: {
423
+ runId,
424
+ swarmRunId,
425
+ subagentRunId,
426
+ personaId,
427
+ subagentIndex,
428
+ partitionCount: partitions.length,
429
+ fileCount: scopedFiles.length,
430
+ findings: findings.length,
431
+ summary: result?.summary || summarizeFindings(findings),
432
+ costUsd: result?.usage?.costUsd || 0,
433
+ durationMs: Date.now() - subagentStart,
434
+ },
435
+ usage: {
436
+ costUsd: result?.usage?.costUsd || 0,
437
+ durationMs: Date.now() - subagentStart,
438
+ toolCalls: result?.usage?.toolCalls || 0,
439
+ },
440
+ runId,
441
+ }));
442
+ }
443
+
444
+ return {
445
+ status: "ok",
446
+ subagentIndex,
447
+ agentId: subagentIdentity.id,
448
+ files: scopedFiles,
449
+ findings,
450
+ summary: result?.summary || summarizeFindings(findings),
451
+ costUsd: result?.usage?.costUsd || 0,
452
+ model: result?.model || model || null,
453
+ durationMs: Date.now() - subagentStart,
454
+ };
455
+ } catch (err) {
456
+ if (onEvent) {
457
+ onEvent(createAgentEvent({
458
+ event: "agent_error",
459
+ agent: subagentIdentity,
460
+ payload: {
461
+ runId,
462
+ swarmRunId,
463
+ subagentRunId,
464
+ personaId,
465
+ subagentIndex,
466
+ partitionCount: partitions.length,
467
+ error: err.message,
468
+ durationMs: Date.now() - subagentStart,
469
+ },
470
+ usage: {
471
+ costUsd: 0,
472
+ durationMs: Date.now() - subagentStart,
473
+ toolCalls: 0,
474
+ },
475
+ runId,
476
+ }));
477
+ }
478
+ return {
479
+ status: "error",
480
+ subagentIndex,
481
+ agentId: subagentIdentity.id,
482
+ files: scopedFiles,
483
+ findings: [],
484
+ summary: { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
485
+ costUsd: 0,
486
+ error: err.message,
487
+ durationMs: Date.now() - subagentStart,
488
+ };
489
+ }
490
+ }
491
+ );
492
+
493
+ const settledSubagents = subagentResults.map((result) =>
494
+ result.status === "fulfilled"
495
+ ? result.value
496
+ : {
497
+ status: "error",
498
+ subagentIndex: 0,
499
+ agentId: "unknown",
500
+ files: [],
501
+ findings: [],
502
+ summary: { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
503
+ costUsd: 0,
504
+ error: result.reason?.message || "unknown",
505
+ durationMs: 0,
506
+ }
507
+ );
508
+ const findings = settledSubagents.flatMap((result) => result.findings || []);
509
+ const totalCostUsd = settledSubagents.reduce((sum, result) => sum + (result.costUsd || 0), 0);
510
+ const summary = summarizeFindings(findings);
511
+ const okCount = settledSubagents.filter((result) => result.status === "ok").length;
512
+ const errorCount = settledSubagents.filter((result) => result.status === "error").length;
513
+
514
+ if (onEvent) {
515
+ onEvent(createAgentEvent({
516
+ event: "swarm_complete",
517
+ agent: personaAgentFromIdentity(identity),
518
+ payload: {
519
+ runId,
520
+ swarmRunId,
521
+ personaId,
522
+ identity,
523
+ subagentCount: settledSubagents.length,
524
+ ok: okCount,
525
+ error: errorCount,
526
+ findings: findings.length,
527
+ summary,
528
+ totalCostUsd,
529
+ durationMs: Date.now() - startedAt,
530
+ blackboardEntries: blackboard.length,
531
+ },
532
+ usage: {
533
+ costUsd: totalCostUsd,
534
+ durationMs: Date.now() - startedAt,
535
+ toolCalls: settledSubagents.length,
536
+ },
537
+ runId,
538
+ }));
539
+ }
540
+
541
+ return {
542
+ personaId,
543
+ status: okCount > 0 ? "ok" : "error",
544
+ findings,
545
+ summary,
546
+ costUsd: totalCostUsd,
547
+ model: model || null,
548
+ durationMs: Date.now() - startedAt,
549
+ error: okCount > 0 ? null : settledSubagents.find((result) => result.error)?.error || null,
550
+ swarm: {
551
+ runId: swarmRunId,
552
+ decision,
553
+ subagentCount: settledSubagents.length,
554
+ ok: okCount,
555
+ error: errorCount,
556
+ partitionSizes: partitions.map((files) => files.length),
557
+ blackboardEntries: blackboard.length,
558
+ subagents: settledSubagents.map((result) => ({
559
+ id: result.agentId,
560
+ index: result.subagentIndex,
561
+ status: result.status,
562
+ files: result.files,
563
+ findings: (result.findings || []).length,
564
+ costUsd: result.costUsd || 0,
565
+ durationMs: result.durationMs || 0,
566
+ error: result.error || null,
567
+ })),
568
+ },
569
+ };
570
+ }
571
+
70
572
  /**
71
573
  * Run the Omar Gate multi-persona orchestrator.
72
574
  *
@@ -218,6 +720,62 @@ export async function runOmarGateOrchestrator({
218
720
  }
219
721
 
220
722
  try {
723
+ const swarmResult = await runOmarPersonaSwarm({
724
+ personaId,
725
+ identity,
726
+ targetPath,
727
+ mode,
728
+ runId,
729
+ deterministic,
730
+ outputDir,
731
+ provider,
732
+ model,
733
+ perPersonaCost,
734
+ dryRun,
735
+ onEvent,
736
+ });
737
+
738
+ if (swarmResult) {
739
+ const personaCost = swarmResult.costUsd || 0;
740
+ runningCostUsd += personaCost;
741
+
742
+ if (onEvent) {
743
+ if (swarmResult.status === "error") {
744
+ onEvent(createAgentEvent({
745
+ event: "persona_error",
746
+ agent: personaAgentFromIdentity(identity),
747
+ payload: {
748
+ personaId,
749
+ identity,
750
+ error: swarmResult.error || "all subagents failed",
751
+ swarm: swarmResult.swarm,
752
+ },
753
+ runId,
754
+ }));
755
+ } else {
756
+ onEvent(createAgentEvent({
757
+ event: "persona_complete",
758
+ agent: personaAgentFromIdentity(identity),
759
+ payload: {
760
+ personaId,
761
+ identity,
762
+ findings: swarmResult.findings.length,
763
+ summary: swarmResult.summary,
764
+ costUsd: personaCost,
765
+ durationMs: Date.now() - personaStart,
766
+ swarm: swarmResult.swarm,
767
+ },
768
+ runId,
769
+ }));
770
+ }
771
+ }
772
+
773
+ return {
774
+ ...swarmResult,
775
+ durationMs: Date.now() - personaStart,
776
+ };
777
+ }
778
+
221
779
  const systemPrompt = buildPersonaReviewPrompt({
222
780
  personaId,
223
781
  targetPath,
@@ -242,10 +800,13 @@ export async function runOmarGateOrchestrator({
242
800
  env: process.env,
243
801
  });
244
802
 
803
+ const personaConfidenceFloor = omargateConfidenceFloorForPersona(personaId);
245
804
  const findings = (result?.findings || []).map((f) => ({
246
805
  ...f,
247
806
  persona: personaId,
248
807
  layer: personaId,
808
+ confidenceFloor: personaConfidenceFloor,
809
+ personaConfidenceFloor,
249
810
  }));
250
811
 
251
812
  if (onEvent) {
@@ -347,9 +908,17 @@ export async function runOmarGateOrchestrator({
347
908
  const reconciled = reconcileReviewFindings({
348
909
  deterministicFindings: detFindings,
349
910
  aiFindings: allAiFindings,
911
+ defaultConfidenceFloor: OMARGATE_DEFAULT_CONFIDENCE_FLOOR,
912
+ confidenceFloors: OMARGATE_CONFIDENCE_FLOORS,
350
913
  });
351
914
  const reconciledFindings = reconciled.findings;
352
915
  const reconciledSummary = reconciled.summary;
916
+ const droppedBelowConfidence = Number(reconciledSummary?.droppedBelowConfidence || 0);
917
+ const candidateFindingCount = detFindings.length + allAiFindings.length;
918
+ const dedupedCount = Math.max(
919
+ 0,
920
+ candidateFindingCount - reconciledFindings.length - droppedBelowConfidence
921
+ );
353
922
 
354
923
  const totalCost = settled.reduce((sum, r) => sum + (r.costUsd || 0), 0);
355
924
  const totalDuration = Date.now() - startTime;
@@ -407,6 +976,7 @@ export async function runOmarGateOrchestrator({
407
976
  durationMs: r.durationMs,
408
977
  model: r.model || null,
409
978
  error: r.error || null,
979
+ swarm: r.swarm || null,
410
980
  })),
411
981
  personaHealth,
412
982
  findings: reconciledFindings,
@@ -414,6 +984,7 @@ export async function runOmarGateOrchestrator({
414
984
  deterministic: detFindings.length,
415
985
  ai: allAiFindings.length,
416
986
  reconciled: reconciledFindings.length,
987
+ droppedBelowConfidence,
417
988
  },
418
989
  summary: reconciledSummary,
419
990
  totalCostUsd: totalCost,
@@ -422,7 +993,12 @@ export async function runOmarGateOrchestrator({
422
993
  deterministicFindings: detFindings.length,
423
994
  aiFindings: allAiFindings.length,
424
995
  reconciledFindings: reconciledFindings.length,
425
- dedupedCount: detFindings.length + allAiFindings.length - reconciledFindings.length,
996
+ dedupedCount,
997
+ droppedBelowConfidence,
998
+ droppedLowConfidence: droppedBelowConfidence,
999
+ droppedLowConfidenceSingleSource: Number(
1000
+ reconciledSummary?.droppedBelowConfidenceSingleSource || droppedBelowConfidence
1001
+ ),
426
1002
  multiSourceFindings: reconciledFindings.filter(
427
1003
  (f) => Array.isArray(f.sources) && f.sources.length > 1
428
1004
  ).length,
@@ -485,6 +1061,13 @@ export async function runOmarGateOrchestrator({
485
1061
  costUsd: r.costUsd || 0,
486
1062
  durationMs: r.durationMs || 0,
487
1063
  status: r.status,
1064
+ swarm: r.swarm
1065
+ ? {
1066
+ subagentCount: r.swarm.subagentCount || 0,
1067
+ ok: r.swarm.ok || 0,
1068
+ error: r.swarm.error || 0,
1069
+ }
1070
+ : null,
488
1071
  })),
489
1072
  }).catch(() => {});
490
1073