sentinelayer-cli 0.8.11 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/package.json +10 -5
  2. package/src/agents/devtestbot/config/definition.js +100 -0
  3. package/src/agents/devtestbot/config/system-prompt.js +92 -0
  4. package/src/agents/devtestbot/index.js +9 -0
  5. package/src/agents/devtestbot/runner.js +769 -0
  6. package/src/agents/devtestbot/tool.js +707 -0
  7. package/src/agents/jules/stream.js +2 -12
  8. package/src/audit/orchestrator.js +471 -114
  9. package/src/audit/persona-loop.js +1342 -0
  10. package/src/audit/registry.js +58 -2
  11. package/src/commands/audit.js +42 -1
  12. package/src/commands/legacy-args.js +32 -1
  13. package/src/commands/omargate.js +4 -0
  14. package/src/commands/session.js +417 -89
  15. package/src/commands/swarm.js +11 -2
  16. package/src/cost/history.js +41 -21
  17. package/src/events/schema.js +27 -1
  18. package/src/guide/generator.js +14 -0
  19. package/src/legacy-cli.js +110 -18
  20. package/src/prompt/generator.js +4 -16
  21. package/src/review/ai-review.js +95 -6
  22. package/src/review/dd-report-email-client.js +148 -0
  23. package/src/review/investor-dd-devtestbot.js +599 -0
  24. package/src/review/investor-dd-orchestrator.js +135 -3
  25. package/src/review/omargate-cache.js +285 -0
  26. package/src/review/omargate-orchestrator.js +605 -4
  27. package/src/review/persona-prompts.js +34 -1
  28. package/src/review/report.js +189 -4
  29. package/src/session/coordination-guidance.js +48 -0
  30. package/src/session/daemon.js +3 -2
  31. package/src/session/listener.js +236 -0
  32. package/src/session/senti-naming.js +36 -0
  33. package/src/session/setup-guides.js +3 -15
  34. package/src/session/store.js +54 -5
  35. package/src/session/sync.js +23 -0
  36. package/src/spec/generator.js +8 -10
  37. package/src/swarm/registry.js +20 -0
  38. package/src/swarm/runtime.js +139 -1
@@ -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,318 @@ 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 =
325
+ deterministic?.artifacts?.runDirectory || path.join(targetPath, ".sentinelayer", "reviews", runId);
326
+ const systemPrompt = buildPersonaReviewPrompt({
327
+ personaId,
328
+ targetPath,
329
+ deterministicSummary: deterministic?.summary || {},
330
+ });
331
+ const subagentResults = await runWithConcurrency(
332
+ partitions.map((files, index) => ({ files, subagentIndex: index + 1 })),
333
+ maxConcurrent,
334
+ async ({ files, subagentIndex }) => {
335
+ const subagentStart = Date.now();
336
+ const subagentIdentity = buildSubagentIdentity(identity, subagentIndex);
337
+ const scopedFiles = files.map((file) => file.path);
338
+ const subagentRunId = `${swarmRunId}-${subagentIndex}`;
339
+
340
+ if (onEvent) {
341
+ onEvent(createAgentEvent({
342
+ event: "agent_start",
343
+ agent: subagentIdentity,
344
+ payload: {
345
+ runId,
346
+ swarmRunId,
347
+ subagentRunId,
348
+ personaId,
349
+ identity,
350
+ subagentIndex,
351
+ partitionCount: partitions.length,
352
+ files: scopedFiles,
353
+ fileCount: scopedFiles.length,
354
+ maxCostUsd: budget.maxCostUsd,
355
+ },
356
+ runId,
357
+ }));
358
+ }
359
+
360
+ try {
361
+ const result = await runAiReviewLayer({
362
+ targetPath,
363
+ mode: "full",
364
+ runId: subagentRunId,
365
+ runDirectory: path.join(parentRunDirectory, "swarm", personaId, `subagent-${subagentIndex}`),
366
+ deterministic: {
367
+ ...deterministic,
368
+ scope: {
369
+ ...(deterministic?.scope || {}),
370
+ scannedFiles: scopedFiles.length,
371
+ scannedRelativeFiles: scopedFiles,
372
+ },
373
+ metadata: {
374
+ ...(deterministic?.metadata || {}),
375
+ omarSwarm: {
376
+ personaId,
377
+ subagentIndex,
378
+ partitionCount: partitions.length,
379
+ parentRunId: runId,
380
+ swarmRunId,
381
+ },
382
+ },
383
+ },
384
+ outputDir,
385
+ provider: provider || undefined,
386
+ model: model || undefined,
387
+ sessionId: `${subagentRunId}-ai`,
388
+ maxCostUsd: budget.maxCostUsd,
389
+ systemPrompt,
390
+ dryRun,
391
+ env: process.env,
392
+ });
393
+
394
+ const personaConfidenceFloor = omargateConfidenceFloorForPersona(personaId);
395
+ const findings = (result?.findings || []).map((finding) => {
396
+ const normalized = {
397
+ ...finding,
398
+ persona: personaId,
399
+ layer: personaId,
400
+ confidenceFloor: personaConfidenceFloor,
401
+ personaConfidenceFloor,
402
+ swarm: {
403
+ personaId,
404
+ subagentIndex,
405
+ partitionCount: partitions.length,
406
+ files: scopedFiles,
407
+ },
408
+ };
409
+ blackboard.push({
410
+ agentId: subagentIdentity.id,
411
+ source: personaId,
412
+ ...normalized,
413
+ });
414
+ return normalized;
415
+ });
416
+
417
+ if (onEvent) {
418
+ for (const finding of findings) {
419
+ onEvent(createAgentEvent({
420
+ event: "persona_finding",
421
+ agent: personaAgentFromIdentity(identity),
422
+ payload: { personaId, identity, subagentIndex, ...finding },
423
+ runId,
424
+ }));
425
+ }
426
+ onEvent(createAgentEvent({
427
+ event: "agent_complete",
428
+ agent: subagentIdentity,
429
+ payload: {
430
+ runId,
431
+ swarmRunId,
432
+ subagentRunId,
433
+ personaId,
434
+ subagentIndex,
435
+ partitionCount: partitions.length,
436
+ fileCount: scopedFiles.length,
437
+ findings: findings.length,
438
+ summary: result?.summary || summarizeFindings(findings),
439
+ costUsd: result?.usage?.costUsd || 0,
440
+ durationMs: Date.now() - subagentStart,
441
+ },
442
+ usage: {
443
+ costUsd: result?.usage?.costUsd || 0,
444
+ durationMs: Date.now() - subagentStart,
445
+ toolCalls: result?.usage?.toolCalls || 0,
446
+ },
447
+ runId,
448
+ }));
449
+ }
450
+
451
+ return {
452
+ status: "ok",
453
+ subagentIndex,
454
+ agentId: subagentIdentity.id,
455
+ files: scopedFiles,
456
+ findings,
457
+ summary: result?.summary || summarizeFindings(findings),
458
+ costUsd: result?.usage?.costUsd || 0,
459
+ model: result?.model || model || null,
460
+ artifacts: result?.artifacts || null,
461
+ durationMs: Date.now() - subagentStart,
462
+ };
463
+ } catch (err) {
464
+ if (onEvent) {
465
+ onEvent(createAgentEvent({
466
+ event: "agent_error",
467
+ agent: subagentIdentity,
468
+ payload: {
469
+ runId,
470
+ swarmRunId,
471
+ subagentRunId,
472
+ personaId,
473
+ subagentIndex,
474
+ partitionCount: partitions.length,
475
+ error: err.message,
476
+ durationMs: Date.now() - subagentStart,
477
+ },
478
+ usage: {
479
+ costUsd: 0,
480
+ durationMs: Date.now() - subagentStart,
481
+ toolCalls: 0,
482
+ },
483
+ runId,
484
+ }));
485
+ }
486
+ return {
487
+ status: "error",
488
+ subagentIndex,
489
+ agentId: subagentIdentity.id,
490
+ files: scopedFiles,
491
+ findings: [],
492
+ summary: { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
493
+ costUsd: 0,
494
+ error: err.message,
495
+ durationMs: Date.now() - subagentStart,
496
+ };
497
+ }
498
+ }
499
+ );
500
+
501
+ const settledSubagents = subagentResults.map((result) =>
502
+ result.status === "fulfilled"
503
+ ? result.value
504
+ : {
505
+ status: "error",
506
+ subagentIndex: 0,
507
+ agentId: "unknown",
508
+ files: [],
509
+ findings: [],
510
+ summary: { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
511
+ costUsd: 0,
512
+ error: result.reason?.message || "unknown",
513
+ durationMs: 0,
514
+ }
515
+ );
516
+ const findings = settledSubagents.flatMap((result) => result.findings || []);
517
+ const totalCostUsd = settledSubagents.reduce((sum, result) => sum + (result.costUsd || 0), 0);
518
+ const summary = summarizeFindings(findings);
519
+ const okCount = settledSubagents.filter((result) => result.status === "ok").length;
520
+ const errorCount = settledSubagents.filter((result) => result.status === "error").length;
521
+
522
+ if (onEvent) {
523
+ onEvent(createAgentEvent({
524
+ event: "swarm_complete",
525
+ agent: personaAgentFromIdentity(identity),
526
+ payload: {
527
+ runId,
528
+ swarmRunId,
529
+ personaId,
530
+ identity,
531
+ subagentCount: settledSubagents.length,
532
+ ok: okCount,
533
+ error: errorCount,
534
+ findings: findings.length,
535
+ summary,
536
+ totalCostUsd,
537
+ durationMs: Date.now() - startedAt,
538
+ blackboardEntries: blackboard.length,
539
+ },
540
+ usage: {
541
+ costUsd: totalCostUsd,
542
+ durationMs: Date.now() - startedAt,
543
+ toolCalls: settledSubagents.length,
544
+ },
545
+ runId,
546
+ }));
547
+ }
548
+
549
+ return {
550
+ personaId,
551
+ status: okCount > 0 ? "ok" : "error",
552
+ findings,
553
+ summary,
554
+ costUsd: totalCostUsd,
555
+ model: model || null,
556
+ durationMs: Date.now() - startedAt,
557
+ error: okCount > 0 ? null : settledSubagents.find((result) => result.error)?.error || null,
558
+ swarm: {
559
+ runId: swarmRunId,
560
+ decision,
561
+ subagentCount: settledSubagents.length,
562
+ ok: okCount,
563
+ error: errorCount,
564
+ partitionSizes: partitions.map((files) => files.length),
565
+ blackboardEntries: blackboard.length,
566
+ subagents: settledSubagents.map((result) => ({
567
+ id: result.agentId,
568
+ index: result.subagentIndex,
569
+ status: result.status,
570
+ files: result.files,
571
+ findings: (result.findings || []).length,
572
+ costUsd: result.costUsd || 0,
573
+ durationMs: result.durationMs || 0,
574
+ error: result.error || null,
575
+ artifacts: result.artifacts || null,
576
+ })),
577
+ },
578
+ };
579
+ }
580
+
70
581
  /**
71
582
  * Run the Omar Gate multi-persona orchestrator.
72
583
  *
@@ -218,6 +729,62 @@ export async function runOmarGateOrchestrator({
218
729
  }
219
730
 
220
731
  try {
732
+ const swarmResult = await runOmarPersonaSwarm({
733
+ personaId,
734
+ identity,
735
+ targetPath,
736
+ mode,
737
+ runId,
738
+ deterministic,
739
+ outputDir,
740
+ provider,
741
+ model,
742
+ perPersonaCost,
743
+ dryRun,
744
+ onEvent,
745
+ });
746
+
747
+ if (swarmResult) {
748
+ const personaCost = swarmResult.costUsd || 0;
749
+ runningCostUsd += personaCost;
750
+
751
+ if (onEvent) {
752
+ if (swarmResult.status === "error") {
753
+ onEvent(createAgentEvent({
754
+ event: "persona_error",
755
+ agent: personaAgentFromIdentity(identity),
756
+ payload: {
757
+ personaId,
758
+ identity,
759
+ error: swarmResult.error || "all subagents failed",
760
+ swarm: swarmResult.swarm,
761
+ },
762
+ runId,
763
+ }));
764
+ } else {
765
+ onEvent(createAgentEvent({
766
+ event: "persona_complete",
767
+ agent: personaAgentFromIdentity(identity),
768
+ payload: {
769
+ personaId,
770
+ identity,
771
+ findings: swarmResult.findings.length,
772
+ summary: swarmResult.summary,
773
+ costUsd: personaCost,
774
+ durationMs: Date.now() - personaStart,
775
+ swarm: swarmResult.swarm,
776
+ },
777
+ runId,
778
+ }));
779
+ }
780
+ }
781
+
782
+ return {
783
+ ...swarmResult,
784
+ durationMs: Date.now() - personaStart,
785
+ };
786
+ }
787
+
221
788
  const systemPrompt = buildPersonaReviewPrompt({
222
789
  personaId,
223
790
  targetPath,
@@ -228,24 +795,34 @@ export async function runOmarGateOrchestrator({
228
795
  targetPath,
229
796
  mode: "full",
230
797
  runId: `${runId}-${personaId}`,
231
- runDirectory: targetPath,
798
+ runDirectory: path.join(
799
+ deterministic?.artifacts?.runDirectory || path.join(targetPath, ".sentinelayer", "reviews", runId),
800
+ "personas",
801
+ personaId
802
+ ),
232
803
  deterministic: {
233
804
  summary: detSummary,
234
805
  findings: detFindings,
806
+ scope: deterministic?.scope || {},
807
+ layers: deterministic?.layers || {},
235
808
  metadata: deterministic?.metadata || {},
236
809
  },
237
810
  outputDir,
238
811
  provider: provider || undefined,
239
812
  model: model || undefined,
240
813
  maxCostUsd: perPersonaCost,
814
+ systemPrompt,
241
815
  dryRun,
242
816
  env: process.env,
243
817
  });
244
818
 
819
+ const personaConfidenceFloor = omargateConfidenceFloorForPersona(personaId);
245
820
  const findings = (result?.findings || []).map((f) => ({
246
821
  ...f,
247
822
  persona: personaId,
248
823
  layer: personaId,
824
+ confidenceFloor: personaConfidenceFloor,
825
+ personaConfidenceFloor,
249
826
  }));
250
827
 
251
828
  if (onEvent) {
@@ -296,6 +873,7 @@ export async function runOmarGateOrchestrator({
296
873
  summary: result?.summary || { P0: 0, P1: 0, P2: 0, P3: 0 },
297
874
  costUsd: personaCost,
298
875
  model: result?.model || model || null,
876
+ artifacts: result?.artifacts || null,
299
877
  durationMs: Date.now() - personaStart,
300
878
  };
301
879
  } catch (err) {
@@ -347,9 +925,17 @@ export async function runOmarGateOrchestrator({
347
925
  const reconciled = reconcileReviewFindings({
348
926
  deterministicFindings: detFindings,
349
927
  aiFindings: allAiFindings,
928
+ defaultConfidenceFloor: OMARGATE_DEFAULT_CONFIDENCE_FLOOR,
929
+ confidenceFloors: OMARGATE_CONFIDENCE_FLOORS,
350
930
  });
351
931
  const reconciledFindings = reconciled.findings;
352
932
  const reconciledSummary = reconciled.summary;
933
+ const droppedBelowConfidence = Number(reconciledSummary?.droppedBelowConfidence || 0);
934
+ const candidateFindingCount = detFindings.length + allAiFindings.length;
935
+ const dedupedCount = Math.max(
936
+ 0,
937
+ candidateFindingCount - reconciledFindings.length - droppedBelowConfidence
938
+ );
353
939
 
354
940
  const totalCost = settled.reduce((sum, r) => sum + (r.costUsd || 0), 0);
355
941
  const totalDuration = Date.now() - startTime;
@@ -407,6 +993,8 @@ export async function runOmarGateOrchestrator({
407
993
  durationMs: r.durationMs,
408
994
  model: r.model || null,
409
995
  error: r.error || null,
996
+ swarm: r.swarm || null,
997
+ artifacts: r.artifacts || null,
410
998
  })),
411
999
  personaHealth,
412
1000
  findings: reconciledFindings,
@@ -414,6 +1002,7 @@ export async function runOmarGateOrchestrator({
414
1002
  deterministic: detFindings.length,
415
1003
  ai: allAiFindings.length,
416
1004
  reconciled: reconciledFindings.length,
1005
+ droppedBelowConfidence,
417
1006
  },
418
1007
  summary: reconciledSummary,
419
1008
  totalCostUsd: totalCost,
@@ -422,7 +1011,12 @@ export async function runOmarGateOrchestrator({
422
1011
  deterministicFindings: detFindings.length,
423
1012
  aiFindings: allAiFindings.length,
424
1013
  reconciledFindings: reconciledFindings.length,
425
- dedupedCount: detFindings.length + allAiFindings.length - reconciledFindings.length,
1014
+ dedupedCount,
1015
+ droppedBelowConfidence,
1016
+ droppedLowConfidence: droppedBelowConfidence,
1017
+ droppedLowConfidenceSingleSource: Number(
1018
+ reconciledSummary?.droppedBelowConfidenceSingleSource || droppedBelowConfidence
1019
+ ),
426
1020
  multiSourceFindings: reconciledFindings.filter(
427
1021
  (f) => Array.isArray(f.sources) && f.sources.length > 1
428
1022
  ).length,
@@ -485,6 +1079,13 @@ export async function runOmarGateOrchestrator({
485
1079
  costUsd: r.costUsd || 0,
486
1080
  durationMs: r.durationMs || 0,
487
1081
  status: r.status,
1082
+ swarm: r.swarm
1083
+ ? {
1084
+ subagentCount: r.swarm.subagentCount || 0,
1085
+ ok: r.swarm.ok || 0,
1086
+ error: r.swarm.error || 0,
1087
+ }
1088
+ : null,
488
1089
  })),
489
1090
  }).catch(() => {});
490
1091