sentinelayer-cli 0.25.2 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.25.2",
3
+ "version": "0.27.0",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -58,6 +58,9 @@ export function buildLegacyArgs(baseArgs, { commandOptions = {}, command } = {})
58
58
  appendPassthroughFlag(args, "--model", commandOptions.model);
59
59
  appendPassthroughFlag(args, "--provider", commandOptions.provider);
60
60
  appendPassthroughFlag(args, "--reuse-omargate", commandOptions.reuseOmargate);
61
+ appendPassthroughFlag(args, "--scope-mode", commandOptions.scopeMode);
62
+ appendBooleanFlag(args, "--diff", commandOptions.diff);
63
+ appendBooleanFlag(args, "--staged", commandOptions.staged);
61
64
  appendPassthroughFlag(args, "--notify-email", commandOptions.notifyEmail);
62
65
  appendPassthroughFlag(args, "--email-on-complete", commandOptions.emailOnComplete);
63
66
  appendPassthroughFlag(args, "--notify-session", commandOptions.notifySession);
@@ -12,6 +12,9 @@ export function registerOmarGateCommand(program, invokeLegacy) {
12
12
  .option("--output-dir <path>", "Artifact root for report output")
13
13
  .option("--no-ai", "Skip AI review layer (deterministic only)")
14
14
  .option("--ai-dry-run", "Run AI layer in dry-run mode (no LLM call)")
15
+ .option("--scope-mode <mode>", "Deterministic file scope: full, diff, or staged")
16
+ .option("--diff", "Alias for --scope-mode diff")
17
+ .option("--staged", "Alias for --scope-mode staged")
15
18
  .option("--scan-mode <mode>", "Scan depth: baseline (1 persona), deep (13), full-depth (13)")
16
19
  .option("--max-parallel <n>", "Max concurrent persona calls (default: 4)")
17
20
  .option("--model <id>", "LLM model override (default: gpt-5.3-codex)")
package/src/legacy-cli.js CHANGED
@@ -847,6 +847,33 @@ function hasCommandOption(args, optionName) {
847
847
  return args.some((arg) => String(arg || "").trim() === optionName);
848
848
  }
849
849
 
850
+ function resolveScopeModeFromOptions(args, { defaultMode = "full" } = {}) {
851
+ const explicitMode = String(getCommandOptionValue(args, "--scope-mode") || "").trim().toLowerCase();
852
+ const diffFlag = hasCommandOption(args, "--diff");
853
+ const stagedFlag = hasCommandOption(args, "--staged");
854
+
855
+ if (diffFlag && stagedFlag) {
856
+ throw new Error("Use only one of --diff or --staged.");
857
+ }
858
+
859
+ let modeFromFlags = "";
860
+ if (diffFlag) {
861
+ modeFromFlags = "diff";
862
+ } else if (stagedFlag) {
863
+ modeFromFlags = "staged";
864
+ }
865
+
866
+ if (explicitMode && modeFromFlags && explicitMode !== modeFromFlags) {
867
+ throw new Error(`Conflicting scope selection: --scope-mode ${explicitMode} with --${modeFromFlags}.`);
868
+ }
869
+
870
+ const resolved = explicitMode || modeFromFlags || defaultMode;
871
+ if (!["full", "diff", "staged"].includes(resolved)) {
872
+ throw new Error("scope mode must be one of: full, diff, staged.");
873
+ }
874
+ return resolved;
875
+ }
876
+
850
877
  async function collectScanFiles(rootPath) {
851
878
  const files = [];
852
879
  const stack = [rootPath];
@@ -1237,6 +1264,7 @@ async function runLocalOmarGateCommand(args) {
1237
1264
  const asJson = hasCommandOption(args, "--json");
1238
1265
  const pathArg = getCommandOptionValue(args, "--path") || ".";
1239
1266
  const outputDirArg = getCommandOptionValue(args, "--output-dir") || "";
1267
+ const scopeMode = resolveScopeModeFromOptions(args, { defaultMode: "full" });
1240
1268
  const aiEnabled = !hasCommandOption(args, "--no-ai");
1241
1269
  const aiDryRun = hasCommandOption(args, "--ai-dry-run");
1242
1270
  const maxCostUsd = parseFloat(getCommandOptionValue(args, "--max-cost") || "5.0") || 5.0;
@@ -1261,6 +1289,7 @@ async function runLocalOmarGateCommand(args) {
1261
1289
  if (!asJson) {
1262
1290
  printSection("Local Omar Gate Deep");
1263
1291
  printInfo(`Target: ${targetPath}`);
1292
+ printInfo(`Scope mode: ${scopeMode}`);
1264
1293
  printInfo(`Scan mode: ${scanMode} | AI: ${aiEnabled ? "enabled" : "disabled"}`);
1265
1294
  console.error("");
1266
1295
  console.error(pc.gray(` [${formatElapsed(Date.now() - commandStartedAt)}] Phase 1: Deterministic analysis (22 rules)...`));
@@ -1270,13 +1299,17 @@ async function runLocalOmarGateCommand(args) {
1270
1299
  const { runDeterministicReviewPipeline } = await import("./review/local-review.js");
1271
1300
  const deterministic = await runDeterministicReviewPipeline({
1272
1301
  targetPath,
1273
- mode: "full",
1302
+ mode: scopeMode,
1274
1303
  outputDir: outputDirArg,
1275
1304
  });
1276
1305
 
1277
1306
  const detFindings = deterministic.findings || [];
1278
1307
  const detSummary = deterministic.summary || { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false };
1279
- const scannedFiles = deterministic.metadata?.ingest?.filesScanned || deterministic.metadata?.scannedFiles || detFindings.length;
1308
+ const scannedFiles =
1309
+ deterministic.scope?.scannedFiles ??
1310
+ deterministic.metadata?.ingest?.filesScanned ??
1311
+ deterministic.metadata?.scannedFiles ??
1312
+ detFindings.length;
1280
1313
 
1281
1314
  if (!asJson) {
1282
1315
  console.error(` ${pc.green("✓")} [${formatElapsed(Date.now() - commandStartedAt)}] Deterministic: ${scannedFiles} files → P1=${detSummary.P1} P2=${detSummary.P2} findings`);
@@ -1312,6 +1345,7 @@ async function runLocalOmarGateCommand(args) {
1312
1345
  deterministic: {
1313
1346
  summary: detSummary,
1314
1347
  findings: detFindings,
1348
+ mode: deterministic.mode || scopeMode,
1315
1349
  scope: deterministic.scope || {},
1316
1350
  layers: deterministic.layers || {},
1317
1351
  metadata: deterministic.metadata || {},
@@ -1430,6 +1464,8 @@ async function runLocalOmarGateCommand(args) {
1430
1464
  runId: orchestratorResult.runId,
1431
1465
  mode: orchestratorResult.mode,
1432
1466
  roster: orchestratorResult.roster,
1467
+ basePersonas: orchestratorResult.basePersonas || [],
1468
+ personaRouting: orchestratorResult.personaRouting || null,
1433
1469
  findings: (orchestratorResult.personas || []).flatMap((p) => []),
1434
1470
  aiFindings: aiFindings,
1435
1471
  personaCount: (orchestratorResult.personas || []).length,
@@ -1445,6 +1481,7 @@ async function runLocalOmarGateCommand(args) {
1445
1481
  findings: orchestratorResult.findings || [],
1446
1482
  summary: orchestratorResult.summary,
1447
1483
  reconciliation: orchestratorResult.reconciliation,
1484
+ personaRouting: orchestratorResult.personaRouting || null,
1448
1485
  findingsBySource: orchestratorResult.findingsBySource,
1449
1486
  });
1450
1487
  }
@@ -1461,6 +1498,8 @@ Elapsed: ${totalElapsed}
1461
1498
 
1462
1499
  Summary:
1463
1500
  - Files scanned: ${scannedFiles}
1501
+ - Scope mode: ${scopeMode}
1502
+ - Persona routing: ${orchestratorResult?.personaRouting?.enabled ? `${orchestratorResult.personaRouting.effectivePersonas.length}/${orchestratorResult.personaRouting.basePersonas.length} personas for ${orchestratorResult.personaRouting.changedFileCount} changed files` : "full roster"}
1464
1503
  - Deterministic findings: P0=${detSummary.P0} P1=${detSummary.P1} P2=${detSummary.P2} P3=${detSummary.P3 || 0}
1465
1504
  - AI findings (raw, pre-reconciliation): ${aiResult ? `P0=${aiResult.summary?.P0 || 0} P1=${aiResult.summary?.P1 || 0} P2=${aiResult.summary?.P2 || 0} P3=${aiResult.summary?.P3 || 0}` : "skipped"}
1466
1505
  - Reconciled (deduped + confidence-boosted): P0=${combinedP0} P1=${combinedP1} P2=${combinedP2} P3=${combinedP3}
@@ -1494,6 +1533,8 @@ ${formatFindingsMarkdown(allFindings)}
1494
1533
  targetPath,
1495
1534
  runId: omargateRunId,
1496
1535
  reportPath,
1536
+ scopeMode,
1537
+ scopedFiles: deterministic.scope?.scannedRelativeFiles || [],
1497
1538
  scannedFiles,
1498
1539
  p0: combinedP0,
1499
1540
  p1: combinedP1,
@@ -1518,6 +1559,8 @@ ${formatFindingsMarkdown(allFindings)}
1518
1559
  }
1519
1560
  : null,
1520
1561
  reconciliation: orchestratorResult?.reconciliation || null,
1562
+ personaRouting: orchestratorResult?.personaRouting || null,
1563
+ basePersonas: orchestratorResult?.basePersonas || [],
1521
1564
  roster: orchestratorResult?.roster || [],
1522
1565
  },
1523
1566
  null,
@@ -2435,6 +2478,80 @@ jobs:
2435
2478
  echo "- run_id: \\\`\${{ steps.omar.outputs.run_id }}\\\`" >> "\$GITHUB_STEP_SUMMARY"
2436
2479
  echo "- gate_status: \\\`\${{ steps.omar.outputs.gate_status }}\\\`" >> "\$GITHUB_STEP_SUMMARY"
2437
2480
  echo "- findings: P0=\${{ steps.omar.outputs.p0_count }} P1=\${{ steps.omar.outputs.p1_count }} P2=\${{ steps.omar.outputs.p2_count }} P3=\${{ steps.omar.outputs.p3_count }}" >> "\$GITHUB_STEP_SUMMARY"
2481
+ - name: Stage Omar summary artifact
2482
+ shell: bash
2483
+ env:
2484
+ OMAR_RUN_ID: \${{ steps.omar.outputs.run_id || '' }}
2485
+ OMAR_GATE_STATUS: \${{ steps.omar.outputs.gate_status || '' }}
2486
+ OMAR_P0: \${{ steps.omar.outputs.p0_count || '0' }}
2487
+ OMAR_P1: \${{ steps.omar.outputs.p1_count || '0' }}
2488
+ OMAR_P2: \${{ steps.omar.outputs.p2_count || '0' }}
2489
+ OMAR_P3: \${{ steps.omar.outputs.p3_count || '0' }}
2490
+ OMAR_SCAN_MODE: \${{ github.event_name == 'workflow_dispatch' && inputs.scan_mode || 'deep' }}
2491
+ OMAR_SEVERITY_GATE: \${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}
2492
+ OMAR_P2_MAX_ALLOWED: \${{ github.event_name == 'workflow_dispatch' && inputs.p2_max_allowed || '5' }}
2493
+ run: |
2494
+ set -euo pipefail
2495
+ mkdir -p omar-artifacts
2496
+ python3 - <<'PY'
2497
+ import json
2498
+ import os
2499
+ from pathlib import Path
2500
+
2501
+ def env(name, default=""):
2502
+ return os.environ.get(name, default)
2503
+
2504
+ def int_env(name):
2505
+ value = env(name, "0").strip()
2506
+ return int(value) if value.isdigit() else 0
2507
+
2508
+ github_run_id = env("GITHUB_RUN_ID")
2509
+ server_url = env("GITHUB_SERVER_URL", "https://github.com").rstrip("/")
2510
+ repository = env("GITHUB_REPOSITORY")
2511
+ run_url = f"{server_url}/{repository}/actions/runs/{github_run_id}" if repository and github_run_id else ""
2512
+ summary = {
2513
+ "schema_version": 1,
2514
+ "kind": "omar_gate_summary",
2515
+ "run_id": env("OMAR_RUN_ID"),
2516
+ "gate_status": env("OMAR_GATE_STATUS"),
2517
+ "findings": {
2518
+ "P0": int_env("OMAR_P0"),
2519
+ "P1": int_env("OMAR_P1"),
2520
+ "P2": int_env("OMAR_P2"),
2521
+ "P3": int_env("OMAR_P3"),
2522
+ },
2523
+ "threshold": {
2524
+ "severity_gate": env("OMAR_SEVERITY_GATE", "P1"),
2525
+ "p2_max_allowed": int_env("OMAR_P2_MAX_ALLOWED"),
2526
+ },
2527
+ "scan": {
2528
+ "mode": env("OMAR_SCAN_MODE", "deep"),
2529
+ "action_ref": "mrrCarter/sentinelayer-v1-action@03d7369cba7de2e9f15b959275c982111f0ee493",
2530
+ "managed_llm": False,
2531
+ "llm_failure_policy": "block",
2532
+ },
2533
+ "github": {
2534
+ "repository": repository,
2535
+ "sha": env("GITHUB_SHA"),
2536
+ "ref": env("GITHUB_REF"),
2537
+ "event_name": env("GITHUB_EVENT_NAME"),
2538
+ "workflow": env("GITHUB_WORKFLOW"),
2539
+ "run_id": github_run_id,
2540
+ "run_attempt": env("GITHUB_RUN_ATTEMPT"),
2541
+ "run_url": run_url,
2542
+ },
2543
+ }
2544
+ Path("omar-artifacts/summary.json").write_text(
2545
+ json.dumps(summary, indent=2, sort_keys=True) + "\n",
2546
+ encoding="utf-8",
2547
+ )
2548
+ PY
2549
+ - name: Upload Omar summary artifact
2550
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874
2551
+ with:
2552
+ name: omar-gate-artifacts
2553
+ path: omar-artifacts/summary.json
2554
+ if-no-files-found: error
2438
2555
  `;
2439
2556
  }
2440
2557
 
@@ -15,6 +15,7 @@ import { reconcileReviewFindings } from "./report.js";
15
15
  import { resolvePersonaVisual } from "../agents/persona-visuals.js";
16
16
  import { syncRunToDashboard } from "../telemetry/sync.js";
17
17
  import { createAgentEvent } from "../events/schema.js";
18
+ import { buildOwnershipMap, computeRoutingStats, loadScaffoldConfig } from "../ingest/ownership.js";
18
19
 
19
20
  const OMAR_ORCHESTRATOR_AGENT = Object.freeze({
20
21
  id: "omar-orchestrator",
@@ -98,6 +99,158 @@ function uniqueScopeFiles(files = []) {
98
99
  return normalized;
99
100
  }
100
101
 
102
+ function normalizeScopeMode(value) {
103
+ const normalized = normalizeString(value).toLowerCase();
104
+ if (normalized === "diff" || normalized === "staged") {
105
+ return normalized;
106
+ }
107
+ return "full";
108
+ }
109
+
110
+ function findingRelativePath(finding = {}) {
111
+ return toPosixPath(finding.file || finding.path || finding.location || "");
112
+ }
113
+
114
+ function summarizeDeterministicFindings(findings = []) {
115
+ return summarizeFindings(findings);
116
+ }
117
+
118
+ function buildScopeForFiles(scope = {}, files = []) {
119
+ const wanted = new Set(files.map((file) => toPosixPath(file)));
120
+ const scopedObjects = filesFromScope(scope).filter((file) => wanted.has(file.path));
121
+ const scopedFiles =
122
+ scopedObjects.length > 0
123
+ ? scopedObjects
124
+ : files.map((filePath) => ({ path: toPosixPath(filePath), loc: 0 }));
125
+ const totalLoc = scopedFiles.reduce((sum, file) => sum + (file.loc > 0 ? file.loc : 80), 0);
126
+ return {
127
+ ...scope,
128
+ files: scopedFiles,
129
+ primary: scopedFiles,
130
+ scannedFiles: files.length,
131
+ scannedRelativeFiles: files,
132
+ totalLoc,
133
+ estimatedLoc: totalLoc,
134
+ };
135
+ }
136
+
137
+ function buildPersonaScopedDeterministic(deterministic = {}, personaId, personaRouting = {}) {
138
+ if (!personaRouting?.enabled) {
139
+ return deterministic;
140
+ }
141
+ const files = Array.isArray(personaRouting.filesByPersona?.[personaId])
142
+ ? personaRouting.filesByPersona[personaId]
143
+ : [];
144
+ const wanted = new Set(files);
145
+ const findings = (deterministic?.findings || []).filter((finding) =>
146
+ wanted.has(findingRelativePath(finding))
147
+ );
148
+ const summary = summarizeDeterministicFindings(findings);
149
+ return {
150
+ ...deterministic,
151
+ summary,
152
+ findings,
153
+ scope: buildScopeForFiles(deterministic?.scope || {}, files),
154
+ metadata: {
155
+ ...(deterministic?.metadata || {}),
156
+ personaRouting: {
157
+ enabled: true,
158
+ scopeMode: personaRouting.scopeMode,
159
+ personaId,
160
+ scopedFiles: files.length,
161
+ },
162
+ },
163
+ };
164
+ }
165
+
166
+ async function resolveChangedFilePersonaRouting({
167
+ targetPath,
168
+ personas = [],
169
+ deterministic = {},
170
+ manualInclude = false,
171
+ } = {}) {
172
+ const scopeMode = normalizeScopeMode(deterministic?.mode || deterministic?.metadata?.scopeMode);
173
+ const basePersonas = [...personas];
174
+ const base = {
175
+ enabled: false,
176
+ scopeMode,
177
+ reason: "full_scope",
178
+ basePersonas,
179
+ effectivePersonas: basePersonas,
180
+ droppedPersonas: [],
181
+ changedFileCount: 0,
182
+ personaCoverage: {},
183
+ tokenReductionEstimatePct: 0,
184
+ filesByPersona: {},
185
+ scaffold: { found: false },
186
+ };
187
+
188
+ if (scopeMode === "full") {
189
+ return base;
190
+ }
191
+ if (manualInclude) {
192
+ return {
193
+ ...base,
194
+ reason: "manual_persona_filter",
195
+ };
196
+ }
197
+
198
+ const scopedFiles = filesFromScope(deterministic?.scope || {})
199
+ .map((file) => file.path)
200
+ .filter(Boolean);
201
+ if (scopedFiles.length === 0) {
202
+ return {
203
+ ...base,
204
+ enabled: true,
205
+ reason: "no_changed_files",
206
+ effectivePersonas: [],
207
+ droppedPersonas: basePersonas,
208
+ changedFileCount: 0,
209
+ };
210
+ }
211
+
212
+ let scaffold = { found: false, ownershipRules: [] };
213
+ try {
214
+ scaffold = await loadScaffoldConfig({ targetPath });
215
+ } catch {
216
+ scaffold = { found: false, ownershipRules: [] };
217
+ }
218
+ const ownershipMap = buildOwnershipMap(scopedFiles, scaffold);
219
+ const stats = computeRoutingStats(ownershipMap);
220
+ const allowed = new Set(basePersonas);
221
+ const filesByPersona = {};
222
+ for (const [file, persona] of ownershipMap.entries()) {
223
+ if (!allowed.has(persona)) {
224
+ continue;
225
+ }
226
+ if (!filesByPersona[persona]) {
227
+ filesByPersona[persona] = [];
228
+ }
229
+ filesByPersona[persona].push(file);
230
+ }
231
+
232
+ for (const persona of Object.keys(filesByPersona)) {
233
+ filesByPersona[persona].sort((left, right) => left.localeCompare(right));
234
+ }
235
+
236
+ const effectivePersonas = basePersonas.filter((persona) => filesByPersona[persona]?.length > 0);
237
+ return {
238
+ ...base,
239
+ enabled: true,
240
+ reason: "changed_file_scope",
241
+ effectivePersonas,
242
+ droppedPersonas: basePersonas.filter((persona) => !effectivePersonas.includes(persona)),
243
+ changedFileCount: scopedFiles.length,
244
+ personaCoverage: stats.personaCoverage,
245
+ tokenReductionEstimatePct: stats.tokenReductionEstimatePct,
246
+ filesByPersona,
247
+ scaffold: {
248
+ found: Boolean(scaffold?.found),
249
+ ruleCount: Array.isArray(scaffold?.ownershipRules) ? scaffold.ownershipRules.length : 0,
250
+ },
251
+ };
252
+ }
253
+
101
254
  function filesFromScope(scope = {}) {
102
255
  if (Array.isArray(scope.files)) {
103
256
  return uniqueScopeFiles(scope.files);
@@ -624,7 +777,7 @@ export async function runOmarGateOrchestrator({
624
777
  })
625
778
  : { ...resolveScanMode(scanMode), dropped: [], unknown: [] };
626
779
 
627
- const { mode, personas } = resolved;
780
+ const { mode, personas: basePersonas } = resolved;
628
781
  const droppedPersonas = resolved.dropped || [];
629
782
  const unknownPersonas = resolved.unknown || [];
630
783
 
@@ -637,7 +790,28 @@ export async function runOmarGateOrchestrator({
637
790
  mode,
638
791
  dropped: droppedPersonas,
639
792
  unknown: unknownPersonas,
640
- effective: personas,
793
+ effective: basePersonas,
794
+ },
795
+ runId,
796
+ }));
797
+ }
798
+
799
+ const personaRouting = await resolveChangedFilePersonaRouting({
800
+ targetPath,
801
+ personas: basePersonas,
802
+ deterministic,
803
+ manualInclude: Array.isArray(includeOnly) && includeOnly.length > 0,
804
+ });
805
+ const personas = personaRouting.effectivePersonas;
806
+
807
+ if (onEvent && personaRouting.enabled) {
808
+ onEvent(createAgentEvent({
809
+ event: "omargate_persona_routing",
810
+ agent: OMAR_ORCHESTRATOR_AGENT,
811
+ payload: {
812
+ runId,
813
+ mode,
814
+ routing: personaRouting,
641
815
  },
642
816
  runId,
643
817
  }));
@@ -659,7 +833,17 @@ export async function runOmarGateOrchestrator({
659
833
  onEvent(createAgentEvent({
660
834
  event: "omargate_start",
661
835
  agent: OMAR_ORCHESTRATOR_AGENT,
662
- payload: { runId, mode, personas, roster, maxParallel, maxCostUsd, dryRun },
836
+ payload: {
837
+ runId,
838
+ mode,
839
+ personas,
840
+ basePersonas,
841
+ roster,
842
+ maxParallel,
843
+ maxCostUsd,
844
+ dryRun,
845
+ personaRouting,
846
+ },
663
847
  runId,
664
848
  }));
665
849
  }
@@ -667,8 +851,8 @@ export async function runOmarGateOrchestrator({
667
851
  const detSummary = deterministic?.summary || { P0: 0, P1: 0, P2: 0, P3: 0 };
668
852
  const detFindings = deterministic?.findings || [];
669
853
 
670
- // Per-persona cost budget = global / persona count (with minimum floor)
671
- const perPersonaCost = Math.max(0.25, maxCostUsd / personas.length);
854
+ // Per-persona cost budget = global / effective persona count (with minimum floor).
855
+ const perPersonaCost = personas.length > 0 ? Math.max(0.25, maxCostUsd / personas.length) : 0;
672
856
  let runningCostUsd = 0;
673
857
 
674
858
  const personaResults = await runWithConcurrency(personas, maxParallel, async (personaId) => {
@@ -711,6 +895,11 @@ export async function runOmarGateOrchestrator({
711
895
  }
712
896
 
713
897
  const personaStart = Date.now();
898
+ const personaDeterministic = buildPersonaScopedDeterministic(
899
+ deterministic,
900
+ personaId,
901
+ personaRouting
902
+ );
714
903
 
715
904
  if (onEvent) {
716
905
  onEvent(createAgentEvent({
@@ -735,7 +924,7 @@ export async function runOmarGateOrchestrator({
735
924
  targetPath,
736
925
  mode,
737
926
  runId,
738
- deterministic,
927
+ deterministic: personaDeterministic,
739
928
  outputDir,
740
929
  provider,
741
930
  model,
@@ -788,7 +977,7 @@ export async function runOmarGateOrchestrator({
788
977
  const systemPrompt = buildPersonaReviewPrompt({
789
978
  personaId,
790
979
  targetPath,
791
- deterministicSummary: detSummary,
980
+ deterministicSummary: personaDeterministic?.summary || detSummary,
792
981
  });
793
982
 
794
983
  const result = await runAiReviewLayer({
@@ -801,11 +990,11 @@ export async function runOmarGateOrchestrator({
801
990
  personaId
802
991
  ),
803
992
  deterministic: {
804
- summary: detSummary,
805
- findings: detFindings,
806
- scope: deterministic?.scope || {},
807
- layers: deterministic?.layers || {},
808
- metadata: deterministic?.metadata || {},
993
+ summary: personaDeterministic?.summary || detSummary,
994
+ findings: personaDeterministic?.findings || detFindings,
995
+ scope: personaDeterministic?.scope || deterministic?.scope || {},
996
+ layers: personaDeterministic?.layers || deterministic?.layers || {},
997
+ metadata: personaDeterministic?.metadata || deterministic?.metadata || {},
809
998
  },
810
999
  outputDir,
811
1000
  provider: provider || undefined,
@@ -982,7 +1171,9 @@ export async function runOmarGateOrchestrator({
982
1171
  const result = {
983
1172
  runId,
984
1173
  mode,
1174
+ basePersonas,
985
1175
  roster,
1176
+ personaRouting,
986
1177
  personas: settled.map((r) => ({
987
1178
  id: r.personaId,
988
1179
  identity: r.persona,
@@ -1051,6 +1242,7 @@ export async function runOmarGateOrchestrator({
1051
1242
  findings: reconciledFindings.length,
1052
1243
  summary: result.summary,
1053
1244
  reconciliation: result.reconciliation,
1245
+ personaRouting,
1054
1246
  totalCostUsd: totalCost,
1055
1247
  totalDurationMs: totalDuration,
1056
1248
  },
@@ -8,6 +8,7 @@ export const SUPPORTED_PLAYWRIGHT_MODES = Object.freeze(["auto", "off", "baselin
8
8
 
9
9
  const CHECKOUT_ACTION_REF = "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683";
10
10
  const SETUP_NODE_ACTION_REF = "actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020";
11
+ const UPLOAD_ARTIFACT_ACTION_REF = "actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874";
11
12
  const SECRET_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
12
13
  const REPO_SLUG_REGEX = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
13
14
 
@@ -274,6 +275,84 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
274
275
  'echo "- findings: P0=${{ steps.omar.outputs.p0_count }} P1=${{ steps.omar.outputs.p1_count }} P2=${{ steps.omar.outputs.p2_count }} P3=${{ steps.omar.outputs.p3_count }}" >> "$GITHUB_STEP_SUMMARY"',
275
276
  ].join("\n"),
276
277
  },
278
+ {
279
+ name: "Stage Omar summary artifact",
280
+ shell: "bash",
281
+ env: {
282
+ OMAR_RUN_ID: "${{ steps.omar.outputs.run_id || '' }}",
283
+ OMAR_GATE_STATUS: "${{ steps.omar.outputs.gate_status || '' }}",
284
+ OMAR_P0: "${{ steps.omar.outputs.p0_count || '0' }}",
285
+ OMAR_P1: "${{ steps.omar.outputs.p1_count || '0' }}",
286
+ OMAR_P2: "${{ steps.omar.outputs.p2_count || '0' }}",
287
+ OMAR_P3: "${{ steps.omar.outputs.p3_count || '0' }}",
288
+ OMAR_SCAN_MODE: "${{ github.event_name == 'workflow_dispatch' && inputs.scan_mode || 'deep' }}",
289
+ OMAR_SEVERITY_GATE: "${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}",
290
+ OMAR_P2_MAX_ALLOWED: "${{ github.event_name == 'workflow_dispatch' && inputs.p2_max_allowed || '5' }}",
291
+ },
292
+ run: [
293
+ "set -euo pipefail",
294
+ "mkdir -p omar-artifacts",
295
+ "python3 - <<'PY'",
296
+ "import json",
297
+ "import os",
298
+ "from pathlib import Path",
299
+ "",
300
+ "def env(name, default=''):",
301
+ " return os.environ.get(name, default)",
302
+ "",
303
+ "def int_env(name):",
304
+ " value = env(name, '0').strip()",
305
+ " return int(value) if value.isdigit() else 0",
306
+ "",
307
+ "github_run_id = env('GITHUB_RUN_ID')",
308
+ "server_url = env('GITHUB_SERVER_URL', 'https://github.com').rstrip('/')",
309
+ "repository = env('GITHUB_REPOSITORY')",
310
+ "run_url = f'{server_url}/{repository}/actions/runs/{github_run_id}' if repository and github_run_id else ''",
311
+ "summary = {",
312
+ " 'schema_version': 1,",
313
+ " 'kind': 'omar_gate_summary',",
314
+ " 'run_id': env('OMAR_RUN_ID'),",
315
+ " 'gate_status': env('OMAR_GATE_STATUS'),",
316
+ " 'findings': {",
317
+ " 'P0': int_env('OMAR_P0'),",
318
+ " 'P1': int_env('OMAR_P1'),",
319
+ " 'P2': int_env('OMAR_P2'),",
320
+ " 'P3': int_env('OMAR_P3'),",
321
+ " },",
322
+ " 'threshold': {",
323
+ " 'severity_gate': env('OMAR_SEVERITY_GATE', 'P1'),",
324
+ " 'p2_max_allowed': int_env('OMAR_P2_MAX_ALLOWED'),",
325
+ " },",
326
+ " 'scan': {",
327
+ " 'mode': env('OMAR_SCAN_MODE', 'deep'),",
328
+ ` 'action_ref': '${SENTINELAYER_ACTION_REF}',`,
329
+ " 'managed_llm': False,",
330
+ " 'llm_failure_policy': 'block',",
331
+ " },",
332
+ " 'github': {",
333
+ " 'repository': repository,",
334
+ " 'sha': env('GITHUB_SHA'),",
335
+ " 'ref': env('GITHUB_REF'),",
336
+ " 'event_name': env('GITHUB_EVENT_NAME'),",
337
+ " 'workflow': env('GITHUB_WORKFLOW'),",
338
+ " 'run_id': github_run_id,",
339
+ " 'run_attempt': env('GITHUB_RUN_ATTEMPT'),",
340
+ " 'run_url': run_url,",
341
+ " },",
342
+ "}",
343
+ "Path('omar-artifacts/summary.json').write_text(json.dumps(summary, indent=2, sort_keys=True) + '\\n', encoding='utf-8')",
344
+ "PY",
345
+ ].join("\n"),
346
+ },
347
+ {
348
+ name: "Upload Omar summary artifact",
349
+ uses: UPLOAD_ARTIFACT_ACTION_REF,
350
+ with: {
351
+ name: "omar-gate-artifacts",
352
+ path: "omar-artifacts/summary.json",
353
+ "if-no-files-found": "error",
354
+ },
355
+ },
277
356
  ],
278
357
  },
279
358
  },