sentinelayer-cli 0.37.1 → 0.38.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 +3 -1
- package/src/cost/tokenizer.js +104 -33
- package/src/legacy-cli.js +6 -6
- package/src/review/omargate-orchestrator.js +62 -4
- package/src/scan/generator.js +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sentinelayer-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"url": "https://github.com/mrrCarter/create-sentinelayer/issues"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
+
"@anthropic-ai/tokenizer": "0.0.4",
|
|
51
52
|
"@axe-core/playwright": "4.11.2",
|
|
52
53
|
"@babel/parser": "7.29.2",
|
|
53
54
|
"axe-core": "4.11.3",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"picocolors": "1.1.1",
|
|
61
62
|
"playwright": "1.59.1",
|
|
62
63
|
"prompts": "2.4.2",
|
|
64
|
+
"tiktoken": "1.0.22",
|
|
63
65
|
"yaml": "2.8.3",
|
|
64
66
|
"zod": "4.1.12"
|
|
65
67
|
},
|
package/src/cost/tokenizer.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
+
import { countTokens as countAnthropicTokens } from "@anthropic-ai/tokenizer";
|
|
2
|
+
import { encoding_for_model, get_encoding } from "tiktoken";
|
|
3
|
+
|
|
1
4
|
// Provider-aware token estimator (#A12, spec §5.2).
|
|
2
5
|
//
|
|
3
6
|
// The rest of the CLI has been guessing token counts with `text.length / 4`
|
|
4
7
|
// since v0.1. That's off by 20-40% vs. the real tokenizer on prose, and
|
|
5
8
|
// wildly off on code (identifiers are much more tokens per char than prose).
|
|
6
|
-
// This module
|
|
7
|
-
// and
|
|
8
|
-
//
|
|
9
|
+
// This module uses the real Anthropic and OpenAI tokenizers where available
|
|
10
|
+
// and keeps the calibrated provider-aware heuristic as the fail-safe for
|
|
11
|
+
// unsupported providers or tokenizer runtime failures.
|
|
9
12
|
//
|
|
10
13
|
// Design goals:
|
|
11
|
-
// -
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// backend takes precedence over the heuristic.
|
|
14
|
+
// - Real tokenizer first for supported providers.
|
|
15
|
+
// - API stable enough for tests and callers that need custom counting:
|
|
16
|
+
// pass `{ backend: fn }` to `estimateTokens` and the backend takes
|
|
17
|
+
// precedence over the built-in tokenizers.
|
|
16
18
|
// - Calibrated ratios per provider family. Numbers below are measured
|
|
17
19
|
// against published BPE stats for cl100k_base (OpenAI), claude (Anthropic),
|
|
18
|
-
// and gemini (Google) across a mix of English prose + JS/TS source
|
|
20
|
+
// and gemini (Google) across a mix of English prose + JS/TS source, and
|
|
21
|
+
// are only used when a real tokenizer cannot be applied.
|
|
19
22
|
|
|
20
23
|
const PROVIDER_FAMILIES = Object.freeze(["anthropic", "openai", "google", "unknown"]);
|
|
21
24
|
|
|
@@ -51,6 +54,8 @@ const MODEL_PROVIDER_RULES = [
|
|
|
51
54
|
{ pattern: /^google[/:]/i, family: "google" },
|
|
52
55
|
];
|
|
53
56
|
|
|
57
|
+
const OPENAI_DEFAULT_ENCODING = "cl100k_base";
|
|
58
|
+
|
|
54
59
|
// Detect provider family from a loose model id: Anthropic conventions like
|
|
55
60
|
// "claude-opus-4-7", OpenAI "gpt-5.3-codex" / "o4-mini" / "codex-mini-2026",
|
|
56
61
|
// Google "gemini-2.5-pro". Unknown ids fall back to the generic tokenizer.
|
|
@@ -85,50 +90,111 @@ function countWords(text) {
|
|
|
85
90
|
return parts.length;
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
function safePositiveCeil(value) {
|
|
94
|
+
const normalized = Number(value);
|
|
95
|
+
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return Math.max(1, Math.ceil(normalized));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function heuristicEstimateTokens(str, family) {
|
|
102
|
+
const charsPerToken = CHARS_PER_TOKEN[family] || CHARS_PER_TOKEN.unknown;
|
|
103
|
+
const tokensPerWord = TOKENS_PER_WORD[family] || TOKENS_PER_WORD.unknown;
|
|
104
|
+
|
|
105
|
+
const normalized = str.replace(/\s+/g, " ").trim();
|
|
106
|
+
if (!normalized) {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
const charEstimate = Math.ceil(normalized.length / charsPerToken);
|
|
110
|
+
const wordEstimate = Math.ceil(countWords(normalized) * tokensPerWord);
|
|
111
|
+
// Blend: the higher-accuracy answer depends on whether the input is
|
|
112
|
+
// whitespace-sparse (code/json/base64 — char estimate wins) or
|
|
113
|
+
// whitespace-dense prose (word estimate is more accurate). Take the max
|
|
114
|
+
// of the two, because underestimating token counts blows budgets; this
|
|
115
|
+
// biases cost estimates slightly on the safe side.
|
|
116
|
+
return Math.max(1, charEstimate, wordEstimate);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function countOpenAiTokens(str, model) {
|
|
120
|
+
let encoder = null;
|
|
121
|
+
try {
|
|
122
|
+
encoder = model
|
|
123
|
+
? encoding_for_model(String(model).trim())
|
|
124
|
+
: get_encoding(OPENAI_DEFAULT_ENCODING);
|
|
125
|
+
} catch {
|
|
126
|
+
encoder = get_encoding(OPENAI_DEFAULT_ENCODING);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
return encoder.encode(str).length;
|
|
131
|
+
} finally {
|
|
132
|
+
if (encoder && typeof encoder.free === "function") {
|
|
133
|
+
encoder.free();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function countWithProviderTokenizer(
|
|
139
|
+
text,
|
|
140
|
+
{ provider = "", model = "" } = {}
|
|
141
|
+
) {
|
|
142
|
+
const str = typeof text === "string" ? text : text == null ? "" : String(text);
|
|
143
|
+
if (!str || !str.trim()) {
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let family = normalizeProviderFamily(provider);
|
|
148
|
+
if (family === "unknown" && model) {
|
|
149
|
+
family = detectProviderFamily(model);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (family === "anthropic") {
|
|
153
|
+
return safePositiveCeil(countAnthropicTokens(str));
|
|
154
|
+
}
|
|
155
|
+
if (family === "openai") {
|
|
156
|
+
return safePositiveCeil(countOpenAiTokens(str, model));
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
88
161
|
// Estimate token count for a text against a provider family. Uses a blend
|
|
89
|
-
// of
|
|
90
|
-
// function of token-per-word behavior) and long runs of no-break chars
|
|
91
|
-
// (where the char ratio dominates) both get sensible answers.
|
|
162
|
+
// of real provider tokenizers and the provider-aware fallback heuristic.
|
|
92
163
|
//
|
|
93
164
|
// Options:
|
|
94
165
|
// - provider: "anthropic" | "openai" | "google" | "unknown" (explicit)
|
|
95
166
|
// - model: model id, used to infer provider when provider is omitted
|
|
96
|
-
// - backend: fn(text) -> number. Overrides the
|
|
97
|
-
// hook for swapping in @anthropic-ai/tokenizer / tiktoken
|
|
98
|
-
// without rewriting callers.
|
|
167
|
+
// - backend: fn(text) -> number. Overrides the built-in tokenizer path.
|
|
99
168
|
export function estimateTokens(
|
|
100
169
|
text,
|
|
101
170
|
{ provider = "", model = "", backend = null } = {}
|
|
102
171
|
) {
|
|
103
172
|
const str = typeof text === "string" ? text : text == null ? "" : String(text);
|
|
104
|
-
if (!str) {
|
|
173
|
+
if (!str || !str.trim()) {
|
|
105
174
|
return 0;
|
|
106
175
|
}
|
|
107
176
|
if (typeof backend === "function") {
|
|
108
|
-
const custom =
|
|
109
|
-
if (
|
|
110
|
-
return
|
|
177
|
+
const custom = safePositiveCeil(backend(str));
|
|
178
|
+
if (custom !== null) {
|
|
179
|
+
return custom;
|
|
111
180
|
}
|
|
112
181
|
}
|
|
182
|
+
|
|
113
183
|
let family = normalizeProviderFamily(provider);
|
|
114
184
|
if (family === "unknown" && model) {
|
|
115
185
|
family = detectProviderFamily(model);
|
|
116
186
|
}
|
|
117
|
-
const charsPerToken = CHARS_PER_TOKEN[family] || CHARS_PER_TOKEN.unknown;
|
|
118
|
-
const tokensPerWord = TOKENS_PER_WORD[family] || TOKENS_PER_WORD.unknown;
|
|
119
187
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
188
|
+
try {
|
|
189
|
+
const tokenizerCount = countWithProviderTokenizer(str, { provider: family, model });
|
|
190
|
+
if (tokenizerCount !== null) {
|
|
191
|
+
return tokenizerCount;
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// Keep budget calculations available even if a native/WASM tokenizer
|
|
195
|
+
// cannot initialize in a constrained runtime.
|
|
123
196
|
}
|
|
124
|
-
|
|
125
|
-
const wordEstimate = Math.ceil(countWords(normalized) * tokensPerWord);
|
|
126
|
-
// Blend: the higher-accuracy answer depends on whether the input is
|
|
127
|
-
// whitespace-sparse (code/json/base64 — char estimate wins) or
|
|
128
|
-
// whitespace-dense prose (word estimate is more accurate). Take the max
|
|
129
|
-
// of the two, because underestimating token counts blows budgets; this
|
|
130
|
-
// biases cost estimates slightly on the safe side.
|
|
131
|
-
return Math.max(1, charEstimate, wordEstimate);
|
|
197
|
+
return heuristicEstimateTokens(str, family);
|
|
132
198
|
}
|
|
133
199
|
|
|
134
200
|
// Combined token count + cost calculation for a single request. Consumers
|
|
@@ -157,4 +223,9 @@ export function estimateTokensForMessages(
|
|
|
157
223
|
return total;
|
|
158
224
|
}
|
|
159
225
|
|
|
160
|
-
export {
|
|
226
|
+
export {
|
|
227
|
+
CHARS_PER_TOKEN,
|
|
228
|
+
OPENAI_DEFAULT_ENCODING,
|
|
229
|
+
PROVIDER_FAMILIES,
|
|
230
|
+
TOKENS_PER_WORD,
|
|
231
|
+
};
|
package/src/legacy-cli.js
CHANGED
|
@@ -2517,7 +2517,7 @@ jobs:
|
|
|
2517
2517
|
fi
|
|
2518
2518
|
- name: Run Omar Gate
|
|
2519
2519
|
id: omar
|
|
2520
|
-
uses: mrrCarter/sentinelayer-v1-action@
|
|
2520
|
+
uses: mrrCarter/sentinelayer-v1-action@a496be33a466c0cc3f8616d66bbd7d78f7d3c31d
|
|
2521
2521
|
with:
|
|
2522
2522
|
github_token: \${{ github.token }}
|
|
2523
2523
|
openai_api_key: \${{ secrets.OPENAI_API_KEY }}
|
|
@@ -2527,9 +2527,9 @@ jobs:
|
|
|
2527
2527
|
sentinelayer_managed_llm: \${{ secrets.${normalizedSecret} != '' }}
|
|
2528
2528
|
scan_mode: \${{ github.event_name == 'workflow_dispatch' && inputs.scan_mode || 'deep' }}
|
|
2529
2529
|
severity_gate: \${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}
|
|
2530
|
-
model: \${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-
|
|
2530
|
+
model: \${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-5.3-codex') }}
|
|
2531
2531
|
codex_model: gpt-5.3-codex
|
|
2532
|
-
model_fallback: \${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-
|
|
2532
|
+
model_fallback: \${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}
|
|
2533
2533
|
use_codex: \${{ secrets.OPENAI_API_KEY != '' || (secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '') }}
|
|
2534
2534
|
codex_only: "false"
|
|
2535
2535
|
llm_failure_policy: block
|
|
@@ -2585,8 +2585,8 @@ jobs:
|
|
|
2585
2585
|
OMAR_GOOGLE_KEY_PRESENT: \${{ secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '' }}
|
|
2586
2586
|
OMAR_MANAGED_LLM: \${{ secrets.${normalizedSecret} != '' }}
|
|
2587
2587
|
OMAR_LLM_PROVIDER: \${{ secrets.OPENAI_API_KEY != '' && 'openai' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'google' || 'openai') }}
|
|
2588
|
-
OMAR_MODEL: \${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-
|
|
2589
|
-
OMAR_MODEL_FALLBACK: \${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-
|
|
2588
|
+
OMAR_MODEL: \${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-5.3-codex') }}
|
|
2589
|
+
OMAR_MODEL_FALLBACK: \${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}
|
|
2590
2590
|
run: |
|
|
2591
2591
|
set -euo pipefail
|
|
2592
2592
|
mkdir -p omar-artifacts
|
|
@@ -2626,7 +2626,7 @@ jobs:
|
|
|
2626
2626
|
},
|
|
2627
2627
|
"scan": {
|
|
2628
2628
|
"mode": env("OMAR_SCAN_MODE", "deep"),
|
|
2629
|
-
"action_ref": "mrrCarter/sentinelayer-v1-action@
|
|
2629
|
+
"action_ref": "mrrCarter/sentinelayer-v1-action@a496be33a466c0cc3f8616d66bbd7d78f7d3c31d",
|
|
2630
2630
|
"llm_provider": env("OMAR_LLM_PROVIDER", "openai"),
|
|
2631
2631
|
"model": env("OMAR_MODEL", "gpt-5.3-codex"),
|
|
2632
2632
|
"model_fallback": env("OMAR_MODEL_FALLBACK", "gpt-4.1-mini"),
|
|
@@ -376,6 +376,52 @@ function summarizeFindings(findings = []) {
|
|
|
376
376
|
};
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
function buildPersonaHealthFindings(personaHealth = {}) {
|
|
380
|
+
if (personaHealth.healthy || personaHealth.total <= 0) {
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const warnings = Array.isArray(personaHealth.warnings) ? personaHealth.warnings : [];
|
|
385
|
+
const errorCount = Number(personaHealth.error || 0);
|
|
386
|
+
const total = Number(personaHealth.total || 0);
|
|
387
|
+
const errorRatio = Number(personaHealth.errorRatio || 0);
|
|
388
|
+
const totalCostUsd = Number(personaHealth.totalCostUsd || 0);
|
|
389
|
+
const allPersonasFailed = total > 0 && errorCount === total;
|
|
390
|
+
const severity = allPersonasFailed ? "P0" : "P1";
|
|
391
|
+
let message = `Omar AI coverage degraded: ${errorCount}/${total} dispatched personas errored (${Math.round(errorRatio * 100)}%).`;
|
|
392
|
+
if (allPersonasFailed) {
|
|
393
|
+
message = `Omar AI coverage failed closed: all ${total} dispatched personas errored before producing findings.`;
|
|
394
|
+
} else if (errorCount === 0 && totalCostUsd <= 0) {
|
|
395
|
+
message = "Omar AI coverage failed closed: personas reported ok but total AI cost was $0.00, indicating no LLM calls were made.";
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return [
|
|
399
|
+
{
|
|
400
|
+
severity,
|
|
401
|
+
file: "<omargate>",
|
|
402
|
+
line: 1,
|
|
403
|
+
message,
|
|
404
|
+
category: "Omar AI Coverage",
|
|
405
|
+
source: "orchestrator",
|
|
406
|
+
layer: "orchestrator",
|
|
407
|
+
confidence: 1,
|
|
408
|
+
confidenceFloor: 0,
|
|
409
|
+
sources: ["orchestrator"],
|
|
410
|
+
remediation:
|
|
411
|
+
"Fix SentinelLayer managed LLM proxy/provider credentials or run with --no-ai when deterministic-only coverage is intentional.",
|
|
412
|
+
evidence: warnings,
|
|
413
|
+
personaHealth: {
|
|
414
|
+
ok: Number(personaHealth.ok || 0),
|
|
415
|
+
error: errorCount,
|
|
416
|
+
skipped: Number(personaHealth.skipped || 0),
|
|
417
|
+
total,
|
|
418
|
+
errorRatio,
|
|
419
|
+
totalCostUsd,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
];
|
|
423
|
+
}
|
|
424
|
+
|
|
379
425
|
function personaAgentFromIdentity(identity = {}) {
|
|
380
426
|
return {
|
|
381
427
|
id: identity.id,
|
|
@@ -443,6 +489,7 @@ async function runOmarPersonaSwarm({
|
|
|
443
489
|
usageSessionId = "",
|
|
444
490
|
requireUsageLedger = false,
|
|
445
491
|
usageRecorder = undefined,
|
|
492
|
+
aiReviewRunner = runAiReviewLayer,
|
|
446
493
|
} = {}) {
|
|
447
494
|
const scope = buildPersonaFileScope({ deterministic });
|
|
448
495
|
const decision = decideSwarm({ scope });
|
|
@@ -518,7 +565,7 @@ async function runOmarPersonaSwarm({
|
|
|
518
565
|
}
|
|
519
566
|
|
|
520
567
|
try {
|
|
521
|
-
const result = await
|
|
568
|
+
const result = await aiReviewRunner({
|
|
522
569
|
targetPath,
|
|
523
570
|
mode: "full",
|
|
524
571
|
runId: subagentRunId,
|
|
@@ -827,6 +874,7 @@ export async function runOmarGateOrchestrator({
|
|
|
827
874
|
requireUsageLedger = false,
|
|
828
875
|
usageSessionId = "",
|
|
829
876
|
usageRecorder = undefined,
|
|
877
|
+
aiReviewRunner = runAiReviewLayer,
|
|
830
878
|
} = {}) {
|
|
831
879
|
const runId = `omargate-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
832
880
|
const startTime = Date.now();
|
|
@@ -999,6 +1047,7 @@ export async function runOmarGateOrchestrator({
|
|
|
999
1047
|
requireUsageLedger,
|
|
1000
1048
|
usageSessionId,
|
|
1001
1049
|
usageRecorder,
|
|
1050
|
+
aiReviewRunner,
|
|
1002
1051
|
});
|
|
1003
1052
|
|
|
1004
1053
|
if (swarmResult) {
|
|
@@ -1048,7 +1097,7 @@ export async function runOmarGateOrchestrator({
|
|
|
1048
1097
|
deterministicSummary: personaDeterministic?.summary || detSummary,
|
|
1049
1098
|
});
|
|
1050
1099
|
|
|
1051
|
-
const result = await
|
|
1100
|
+
const result = await aiReviewRunner({
|
|
1052
1101
|
targetPath,
|
|
1053
1102
|
mode: "full",
|
|
1054
1103
|
runId: `${runId}-${personaId}`,
|
|
@@ -1210,8 +1259,8 @@ export async function runOmarGateOrchestrator({
|
|
|
1210
1259
|
defaultConfidenceFloor: OMARGATE_DEFAULT_CONFIDENCE_FLOOR,
|
|
1211
1260
|
confidenceFloors: OMARGATE_CONFIDENCE_FLOORS,
|
|
1212
1261
|
});
|
|
1213
|
-
|
|
1214
|
-
|
|
1262
|
+
let reconciledFindings = reconciled.findings;
|
|
1263
|
+
let reconciledSummary = reconciled.summary;
|
|
1215
1264
|
const droppedBelowConfidence = Number(reconciledSummary?.droppedBelowConfidence || 0);
|
|
1216
1265
|
const candidateFindingCount = detFindings.length + allAiFindings.length;
|
|
1217
1266
|
const dedupedCount = Math.max(
|
|
@@ -1241,6 +1290,7 @@ export async function runOmarGateOrchestrator({
|
|
|
1241
1290
|
skipped: personaSkippedCount,
|
|
1242
1291
|
total: totalPersonas,
|
|
1243
1292
|
errorRatio,
|
|
1293
|
+
totalCostUsd: totalCost,
|
|
1244
1294
|
healthy: aiCoverageHealthy || dryRun,
|
|
1245
1295
|
warnings: [],
|
|
1246
1296
|
};
|
|
@@ -1261,6 +1311,12 @@ export async function runOmarGateOrchestrator({
|
|
|
1261
1311
|
}
|
|
1262
1312
|
}
|
|
1263
1313
|
|
|
1314
|
+
const personaHealthFindings = buildPersonaHealthFindings(personaHealth);
|
|
1315
|
+
if (personaHealthFindings.length > 0) {
|
|
1316
|
+
reconciledFindings = [...reconciledFindings, ...personaHealthFindings];
|
|
1317
|
+
reconciledSummary = summarizeFindings(reconciledFindings);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1264
1320
|
const result = {
|
|
1265
1321
|
runId,
|
|
1266
1322
|
mode,
|
|
@@ -1286,6 +1342,7 @@ export async function runOmarGateOrchestrator({
|
|
|
1286
1342
|
findingsBySource: {
|
|
1287
1343
|
deterministic: detFindings.length,
|
|
1288
1344
|
ai: allAiFindings.length,
|
|
1345
|
+
orchestrator: personaHealthFindings.length,
|
|
1289
1346
|
reconciled: reconciledFindings.length,
|
|
1290
1347
|
droppedBelowConfidence,
|
|
1291
1348
|
},
|
|
@@ -1295,6 +1352,7 @@ export async function runOmarGateOrchestrator({
|
|
|
1295
1352
|
reconciliation: {
|
|
1296
1353
|
deterministicFindings: detFindings.length,
|
|
1297
1354
|
aiFindings: allAiFindings.length,
|
|
1355
|
+
orchestratorFindings: personaHealthFindings.length,
|
|
1298
1356
|
reconciledFindings: reconciledFindings.length,
|
|
1299
1357
|
dedupedCount,
|
|
1300
1358
|
droppedBelowConfidence,
|
package/src/scan/generator.js
CHANGED
|
@@ -2,7 +2,7 @@ import YAML from "yaml";
|
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_SCAN_WORKFLOW_PATH = ".github/workflows/omar-gate.yml";
|
|
4
4
|
export const DEFAULT_SCAN_SECRET_NAME = "SENTINELAYER_TOKEN";
|
|
5
|
-
export const SENTINELAYER_ACTION_REF = "mrrCarter/sentinelayer-v1-action@
|
|
5
|
+
export const SENTINELAYER_ACTION_REF = "mrrCarter/sentinelayer-v1-action@a496be33a466c0cc3f8616d66bbd7d78f7d3c31d";
|
|
6
6
|
export const SUPPORTED_E2E_HINTS = Object.freeze(["auto", "yes", "no"]);
|
|
7
7
|
export const SUPPORTED_PLAYWRIGHT_MODES = Object.freeze(["auto", "off", "baseline", "audit"]);
|
|
8
8
|
|
|
@@ -236,10 +236,10 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
|
|
|
236
236
|
scan_mode: profile.scanMode || "deep",
|
|
237
237
|
severity_gate: profile.severityGate || "P1",
|
|
238
238
|
model:
|
|
239
|
-
"${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-
|
|
239
|
+
"${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-5.3-codex') }}",
|
|
240
240
|
codex_model: "gpt-5.3-codex",
|
|
241
241
|
model_fallback:
|
|
242
|
-
"${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-
|
|
242
|
+
"${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}",
|
|
243
243
|
use_codex:
|
|
244
244
|
"${{ secrets.OPENAI_API_KEY != '' || (secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '') }}",
|
|
245
245
|
codex_only: "false",
|
|
@@ -306,9 +306,9 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
|
|
|
306
306
|
OMAR_LLM_PROVIDER:
|
|
307
307
|
"${{ secrets.OPENAI_API_KEY != '' && 'openai' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'google' || 'openai') }}",
|
|
308
308
|
OMAR_MODEL:
|
|
309
|
-
"${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-
|
|
309
|
+
"${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-5.3-codex') }}",
|
|
310
310
|
OMAR_MODEL_FALLBACK:
|
|
311
|
-
"${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-
|
|
311
|
+
"${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}",
|
|
312
312
|
},
|
|
313
313
|
run: [
|
|
314
314
|
"set -euo pipefail",
|