sentinelayer-cli 0.8.0 → 0.8.1
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/README.md +13 -0
- package/package.json +4 -4
- package/src/agents/ai-governance/index.js +12 -0
- package/src/agents/ai-governance/tools/base.js +171 -0
- package/src/agents/ai-governance/tools/eval-regression.js +47 -0
- package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
- package/src/agents/ai-governance/tools/index.js +52 -0
- package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
- package/src/agents/ai-governance/tools/provenance-check.js +69 -0
- package/src/agents/backend/index.js +12 -0
- package/src/agents/backend/tools/base.js +189 -0
- package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
- package/src/agents/backend/tools/idempotency-audit.js +105 -0
- package/src/agents/backend/tools/index.js +87 -0
- package/src/agents/backend/tools/retry-audit.js +132 -0
- package/src/agents/backend/tools/timeout-audit.js +144 -0
- package/src/agents/code-quality/index.js +12 -0
- package/src/agents/code-quality/tools/base.js +159 -0
- package/src/agents/code-quality/tools/complexity-measure.js +197 -0
- package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
- package/src/agents/code-quality/tools/cycle-detect.js +49 -0
- package/src/agents/code-quality/tools/dep-graph.js +196 -0
- package/src/agents/code-quality/tools/index.js +89 -0
- package/src/agents/data-layer/index.js +12 -0
- package/src/agents/data-layer/tools/base.js +181 -0
- package/src/agents/data-layer/tools/index-audit.js +165 -0
- package/src/agents/data-layer/tools/index.js +83 -0
- package/src/agents/data-layer/tools/migration-scan.js +135 -0
- package/src/agents/data-layer/tools/query-explain.js +120 -0
- package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
- package/src/agents/documentation/index.js +12 -0
- package/src/agents/documentation/tools/api-diff.js +91 -0
- package/src/agents/documentation/tools/base.js +151 -0
- package/src/agents/documentation/tools/dead-link-check.js +58 -0
- package/src/agents/documentation/tools/docstring-coverage.js +78 -0
- package/src/agents/documentation/tools/index.js +52 -0
- package/src/agents/documentation/tools/readme-freshness.js +61 -0
- package/src/agents/envelope/fix-cycle.js +45 -0
- package/src/agents/envelope/index.js +31 -0
- package/src/agents/envelope/loop.js +150 -0
- package/src/agents/envelope/pulse.js +18 -0
- package/src/agents/envelope/stream.js +40 -0
- package/src/agents/infrastructure/index.js +12 -0
- package/src/agents/infrastructure/tools/base.js +171 -0
- package/src/agents/infrastructure/tools/checkov-run.js +32 -0
- package/src/agents/infrastructure/tools/drift-detect.js +59 -0
- package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
- package/src/agents/infrastructure/tools/index.js +52 -0
- package/src/agents/infrastructure/tools/tflint-run.js +31 -0
- package/src/agents/jules/loop.js +7 -4
- package/src/agents/jules/swarm/sub-agent.js +5 -1
- package/src/agents/jules/tools/auth-audit.js +10 -1
- package/src/agents/mode.js +113 -0
- package/src/agents/observability/index.js +12 -0
- package/src/agents/observability/tools/alert-audit.js +39 -0
- package/src/agents/observability/tools/base.js +181 -0
- package/src/agents/observability/tools/dashboard-gap.js +42 -0
- package/src/agents/observability/tools/index.js +54 -0
- package/src/agents/observability/tools/log-schema-check.js +74 -0
- package/src/agents/observability/tools/span-coverage.js +74 -0
- package/src/agents/persona-visuals.js +38 -0
- package/src/agents/release/index.js +12 -0
- package/src/agents/release/tools/base.js +181 -0
- package/src/agents/release/tools/changelog-diff.js +86 -0
- package/src/agents/release/tools/feature-flag-audit.js +126 -0
- package/src/agents/release/tools/index.js +61 -0
- package/src/agents/release/tools/rollback-verify.js +129 -0
- package/src/agents/release/tools/semver-check.js +109 -0
- package/src/agents/reliability/index.js +12 -0
- package/src/agents/reliability/tools/backpressure-check.js +129 -0
- package/src/agents/reliability/tools/base.js +181 -0
- package/src/agents/reliability/tools/chaos-probe.js +109 -0
- package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
- package/src/agents/reliability/tools/health-check-audit.js +111 -0
- package/src/agents/reliability/tools/index.js +87 -0
- package/src/agents/run-persona.js +109 -0
- package/src/agents/security/index.js +12 -0
- package/src/agents/security/tools/authz-audit.js +134 -0
- package/src/agents/security/tools/base.js +190 -0
- package/src/agents/security/tools/crypto-review.js +175 -0
- package/src/agents/security/tools/index.js +97 -0
- package/src/agents/security/tools/sast-scan.js +175 -0
- package/src/agents/security/tools/secrets-scan.js +216 -0
- package/src/agents/supply-chain/index.js +12 -0
- package/src/agents/supply-chain/tools/attestation-check.js +42 -0
- package/src/agents/supply-chain/tools/base.js +151 -0
- package/src/agents/supply-chain/tools/index.js +52 -0
- package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
- package/src/agents/supply-chain/tools/package-verify.js +56 -0
- package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
- package/src/agents/testing/index.js +12 -0
- package/src/agents/testing/tools/base.js +202 -0
- package/src/agents/testing/tools/coverage-gap.js +144 -0
- package/src/agents/testing/tools/flake-detect.js +125 -0
- package/src/agents/testing/tools/index.js +85 -0
- package/src/agents/testing/tools/mutation-test.js +143 -0
- package/src/agents/testing/tools/snapshot-diff.js +103 -0
- package/src/auth/gate.js +65 -37
- package/src/cli.js +1 -1
- package/src/commands/chat.js +3 -10
- package/src/commands/legacy-args.js +10 -0
- package/src/commands/omargate.js +36 -2
- package/src/commands/persona.js +46 -1
- package/src/commands/scan.js +3 -10
- package/src/commands/session.js +654 -6
- package/src/commands/spec.js +3 -10
- package/src/coord/events-log.js +141 -0
- package/src/coord/handshake.js +719 -0
- package/src/coord/index.js +35 -0
- package/src/coord/paths.js +84 -0
- package/src/coord/priority.js +62 -0
- package/src/coord/tarjan.js +157 -0
- package/src/cost/tokenizer.js +160 -0
- package/src/cost/tracker.js +61 -0
- package/src/daemon/artifact-lineage.js +362 -0
- package/src/daemon/assignment-ledger.js +117 -0
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ingest-refresh.js +69 -2
- package/src/ingest/engine.js +15 -0
- package/src/ingest/ownership.js +380 -0
- package/src/legacy-cli.js +68 -1
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/review/ai-review.js +3 -10
- package/src/review/compliance-pack.js +389 -0
- package/src/review/investor-dd-config.js +54 -0
- package/src/review/investor-dd-file-loop.js +303 -0
- package/src/review/investor-dd-file-router.js +406 -0
- package/src/review/investor-dd-html-report.js +233 -0
- package/src/review/investor-dd-notification.js +120 -0
- package/src/review/investor-dd-orchestrator.js +405 -0
- package/src/review/investor-dd-persona-runner.js +275 -0
- package/src/review/live-validator.js +253 -0
- package/src/review/omargate-orchestrator.js +90 -2
- package/src/review/persona-prompts.js +244 -56
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +102 -3
- package/src/session/agent-registry.js +7 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +609 -14
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +4 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +24 -1
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +318 -2
- package/src/session/stream.js +9 -1
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- package/src/swarm/runtime.js +1 -8
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live-web validator for investor-DD (#investor-dd-25..28).
|
|
3
|
+
*
|
|
4
|
+
* Jules owns this lane. For each interactive element discovered by
|
|
5
|
+
* scanning the frontend source (buttons, forms, links), the validator
|
|
6
|
+
* provisions an ephemeral AIdenID identity, drives devTestBot to
|
|
7
|
+
* perform the interaction against the running site, and captures:
|
|
8
|
+
*
|
|
9
|
+
* - the observed HTTP status
|
|
10
|
+
* - console errors
|
|
11
|
+
* - network errors
|
|
12
|
+
* - navigation outcome
|
|
13
|
+
* - a short free-form observed-behavior summary
|
|
14
|
+
* - trace + video URIs (supplied by devTestBot)
|
|
15
|
+
*
|
|
16
|
+
* The module is driven through a pluggable client surface so the main
|
|
17
|
+
* flow can be unit-tested without spinning up a real browser and a
|
|
18
|
+
* real AIdenID tenant. Production wiring is a separate PR that swaps
|
|
19
|
+
* the stub client for the real devTestBot + AIdenID SDKs.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fsp from "node:fs/promises";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
|
|
25
|
+
const INTERACTIVE_TAGS = Object.freeze([
|
|
26
|
+
"button",
|
|
27
|
+
"a",
|
|
28
|
+
"input",
|
|
29
|
+
"form",
|
|
30
|
+
"select",
|
|
31
|
+
"textarea",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const SOURCE_EXTENSIONS = Object.freeze([".tsx", ".jsx", ".html", ".vue", ".svelte"]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Walk the frontend directory and extract candidate interactive
|
|
38
|
+
* elements from JSX/HTML-like files. Deliberately simple regex-based
|
|
39
|
+
* extraction; misses dynamic elements. Caller can fall back to a live
|
|
40
|
+
* DOM crawl when static extraction returns < 80% of expected element
|
|
41
|
+
* counts.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} rootPath
|
|
44
|
+
* @param {string[]} [globLike] Optional include roots (default common frontend folders).
|
|
45
|
+
* @returns {Promise<Array<{elementLabel: string, sourceFile: string, lineIndex: number}>>}
|
|
46
|
+
*/
|
|
47
|
+
export async function discoverInteractiveElements(rootPath, globLike = null) {
|
|
48
|
+
const candidateRoots = globLike || [
|
|
49
|
+
"src",
|
|
50
|
+
"app",
|
|
51
|
+
"pages",
|
|
52
|
+
"components",
|
|
53
|
+
"web",
|
|
54
|
+
"frontend",
|
|
55
|
+
"client",
|
|
56
|
+
];
|
|
57
|
+
const elements = [];
|
|
58
|
+
for (const candidate of candidateRoots) {
|
|
59
|
+
const abs = path.join(rootPath, candidate);
|
|
60
|
+
try {
|
|
61
|
+
await fsp.access(abs);
|
|
62
|
+
} catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
await walk(abs, candidate);
|
|
66
|
+
}
|
|
67
|
+
return elements;
|
|
68
|
+
|
|
69
|
+
async function walk(abs, rel) {
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await fsp.readdir(abs, { withFileTypes: true });
|
|
73
|
+
} catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.name.startsWith(".")) continue;
|
|
78
|
+
const absPath = path.join(abs, entry.name);
|
|
79
|
+
const relPath = `${rel}/${entry.name}`;
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
if (entry.name === "node_modules" || entry.name === "dist") continue;
|
|
82
|
+
await walk(absPath, relPath);
|
|
83
|
+
} else if (entry.isFile()) {
|
|
84
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
85
|
+
if (!SOURCE_EXTENSIONS.includes(ext)) continue;
|
|
86
|
+
try {
|
|
87
|
+
const stat = await fsp.stat(absPath);
|
|
88
|
+
if (stat.size > 512 * 1024) continue;
|
|
89
|
+
const text = await fsp.readFile(absPath, "utf-8");
|
|
90
|
+
extractFromText(text, relPath, elements);
|
|
91
|
+
} catch {
|
|
92
|
+
// skip unreadable
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function extractFromText(text, sourceFile, elements) {
|
|
100
|
+
const lines = text.split(/\r?\n/);
|
|
101
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
102
|
+
const line = lines[i];
|
|
103
|
+
for (const tag of INTERACTIVE_TAGS) {
|
|
104
|
+
// Match both lowercase and Capitalized component forms; avoid
|
|
105
|
+
// overmatching by requiring a tag-like opener.
|
|
106
|
+
const re = new RegExp(`<${tag}[\\s>]|<${tag[0].toUpperCase()}${tag.slice(1)}[\\s>]`, "i");
|
|
107
|
+
if (!re.test(line)) continue;
|
|
108
|
+
const labelMatch =
|
|
109
|
+
/(?:aria-label|title|data-testid|id)="([^"]+)"/i.exec(line) ||
|
|
110
|
+
/>([^<]{1,40})</.exec(line);
|
|
111
|
+
const elementLabel = labelMatch ? labelMatch[1].trim() : `${tag}-anon-${i}`;
|
|
112
|
+
elements.push({
|
|
113
|
+
elementLabel,
|
|
114
|
+
sourceFile,
|
|
115
|
+
lineIndex: i + 1,
|
|
116
|
+
});
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @typedef {object} DevTestBotClient
|
|
124
|
+
* @property {(element: {elementLabel: string, sourceFile: string}, identity: object) => Promise<LiveObservation>} interact
|
|
125
|
+
* @property {(runId: string) => Promise<{videoUri: string, traceUri: string}>} [artifact]
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @typedef {object} AidenidClient
|
|
130
|
+
* @property {(runId: string) => Promise<{identityId: string, email: string}>} provisionEphemeralIdentity
|
|
131
|
+
* @property {(identityId: string) => Promise<void>} [release]
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Run the live validator across the discovered element plan.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} params
|
|
138
|
+
* @param {string} params.runId
|
|
139
|
+
* @param {Array<{elementLabel: string, sourceFile: string}>} params.elements
|
|
140
|
+
* @param {DevTestBotClient} params.devTestBot
|
|
141
|
+
* @param {AidenidClient} params.aidenid
|
|
142
|
+
* @param {Function} [params.onEvent]
|
|
143
|
+
* @param {number} [params.maxInteractions] - Cap; defaults to elements.length.
|
|
144
|
+
* @returns {Promise<{identity: object, observations: Array<object>, skipped: number}>}
|
|
145
|
+
*/
|
|
146
|
+
export async function runLiveValidator({
|
|
147
|
+
runId,
|
|
148
|
+
elements,
|
|
149
|
+
devTestBot,
|
|
150
|
+
aidenid,
|
|
151
|
+
onEvent = () => {},
|
|
152
|
+
maxInteractions = Infinity,
|
|
153
|
+
} = {}) {
|
|
154
|
+
if (!runId) throw new TypeError("runLiveValidator requires runId");
|
|
155
|
+
if (!Array.isArray(elements)) throw new TypeError("runLiveValidator requires elements array");
|
|
156
|
+
if (!devTestBot || typeof devTestBot.interact !== "function") {
|
|
157
|
+
throw new TypeError("runLiveValidator requires a devTestBot client with interact()");
|
|
158
|
+
}
|
|
159
|
+
if (!aidenid || typeof aidenid.provisionEphemeralIdentity !== "function") {
|
|
160
|
+
throw new TypeError(
|
|
161
|
+
"runLiveValidator requires an AIdenID client with provisionEphemeralIdentity()",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
onEvent({ type: "live_validator_start", runId, elementCount: elements.length });
|
|
166
|
+
const identity = await aidenid.provisionEphemeralIdentity(runId);
|
|
167
|
+
onEvent({ type: "live_validator_identity_ready", runId, identityId: identity.identityId });
|
|
168
|
+
|
|
169
|
+
const observations = [];
|
|
170
|
+
let skipped = 0;
|
|
171
|
+
const budget = Number.isFinite(maxInteractions) ? maxInteractions : elements.length;
|
|
172
|
+
for (let i = 0; i < Math.min(elements.length, budget); i += 1) {
|
|
173
|
+
const element = elements[i];
|
|
174
|
+
onEvent({ type: "live_validator_interaction_start", runId, element });
|
|
175
|
+
try {
|
|
176
|
+
const obs = await devTestBot.interact(element, identity);
|
|
177
|
+
const enriched = {
|
|
178
|
+
...obs,
|
|
179
|
+
sourceFile: element.sourceFile,
|
|
180
|
+
elementLabel: element.elementLabel,
|
|
181
|
+
interactionId: obs.interactionId || `${element.sourceFile}#${i}`,
|
|
182
|
+
};
|
|
183
|
+
observations.push(enriched);
|
|
184
|
+
onEvent({
|
|
185
|
+
type: "live_validator_interaction_complete",
|
|
186
|
+
runId,
|
|
187
|
+
interactionId: enriched.interactionId,
|
|
188
|
+
});
|
|
189
|
+
} catch (err) {
|
|
190
|
+
skipped += 1;
|
|
191
|
+
onEvent({
|
|
192
|
+
type: "live_validator_interaction_error",
|
|
193
|
+
runId,
|
|
194
|
+
element,
|
|
195
|
+
error: err instanceof Error ? err.message : String(err),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (typeof aidenid.release === "function") {
|
|
201
|
+
try {
|
|
202
|
+
await aidenid.release(identity.identityId);
|
|
203
|
+
} catch {
|
|
204
|
+
// release errors never block the report
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
onEvent({
|
|
209
|
+
type: "live_validator_complete",
|
|
210
|
+
runId,
|
|
211
|
+
observationCount: observations.length,
|
|
212
|
+
skipped,
|
|
213
|
+
});
|
|
214
|
+
return { identity, observations, skipped };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Build a lookup map keyed by `sourceFile:lineIndex` so the
|
|
219
|
+
* reconciliation engine can pair each source finding with 0 or 1
|
|
220
|
+
* matching live observation.
|
|
221
|
+
*
|
|
222
|
+
* @param {Array<{sourceFile: string, lineIndex?: number, interactionId: string}>} observations
|
|
223
|
+
* @returns {Map<string, object>}
|
|
224
|
+
*/
|
|
225
|
+
export function buildObservationIndex(observations) {
|
|
226
|
+
const map = new Map();
|
|
227
|
+
for (const obs of observations || []) {
|
|
228
|
+
if (!obs.sourceFile) continue;
|
|
229
|
+
const fileKey = obs.sourceFile;
|
|
230
|
+
if (obs.lineIndex) {
|
|
231
|
+
map.set(`${fileKey}:${obs.lineIndex}`, obs);
|
|
232
|
+
}
|
|
233
|
+
if (!map.has(fileKey)) {
|
|
234
|
+
map.set(fileKey, obs);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return map;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Pair function factory for reconcileFindings(). Looks up an
|
|
242
|
+
* observation for each finding by (file, line) or (file) fallback.
|
|
243
|
+
*
|
|
244
|
+
* @param {Map<string, object>} index
|
|
245
|
+
* @returns {(finding: object) => object | null}
|
|
246
|
+
*/
|
|
247
|
+
export function createFindingObservationPair(index) {
|
|
248
|
+
return (finding) => {
|
|
249
|
+
if (!finding || !finding.file) return null;
|
|
250
|
+
const key = finding.line ? `${finding.file}:${finding.line}` : finding.file;
|
|
251
|
+
return index.get(key) || index.get(finding.file) || null;
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -9,7 +9,7 @@ import { randomUUID } from "node:crypto";
|
|
|
9
9
|
|
|
10
10
|
import { runAiReviewLayer } from "./ai-review.js";
|
|
11
11
|
import { buildPersonaReviewPrompt, PERSONA_IDS } from "./persona-prompts.js";
|
|
12
|
-
import { resolveScanMode } from "./scan-modes.js";
|
|
12
|
+
import { resolveFilteredPersonas, resolveScanMode } from "./scan-modes.js";
|
|
13
13
|
import { reconcileReviewFindings } from "./report.js";
|
|
14
14
|
import { resolvePersonaVisual } from "../agents/persona-visuals.js";
|
|
15
15
|
import { syncRunToDashboard } from "../telemetry/sync.js";
|
|
@@ -81,6 +81,8 @@ function decoratePersonaResult(personaId, baseResult) {
|
|
|
81
81
|
* @param {string} [options.outputDir] - Output directory override
|
|
82
82
|
* @param {object} [options.deterministic] - Deterministic scan results
|
|
83
83
|
* @param {Function} [options.onEvent] - Event callback for streaming
|
|
84
|
+
* @param {string[] | null} [options.includeOnly] - Only run these persona IDs (filters scan-mode roster).
|
|
85
|
+
* @param {string[] | null} [options.skipPersonas] - Skip these persona IDs (filters scan-mode roster).
|
|
84
86
|
* @returns {Promise<object>} Orchestrated results
|
|
85
87
|
*/
|
|
86
88
|
export async function runOmarGateOrchestrator({
|
|
@@ -94,11 +96,41 @@ export async function runOmarGateOrchestrator({
|
|
|
94
96
|
outputDir = "",
|
|
95
97
|
deterministic = null,
|
|
96
98
|
onEvent = null,
|
|
99
|
+
includeOnly = null,
|
|
100
|
+
skipPersonas = null,
|
|
97
101
|
} = {}) {
|
|
98
102
|
const runId = `omargate-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
99
103
|
const startTime = Date.now();
|
|
100
104
|
|
|
101
|
-
const
|
|
105
|
+
const filterRequested =
|
|
106
|
+
(Array.isArray(includeOnly) && includeOnly.length > 0)
|
|
107
|
+
|| (Array.isArray(skipPersonas) && skipPersonas.length > 0);
|
|
108
|
+
|
|
109
|
+
const resolved = filterRequested
|
|
110
|
+
? resolveFilteredPersonas(scanMode, {
|
|
111
|
+
includeOnly: Array.isArray(includeOnly) ? includeOnly : undefined,
|
|
112
|
+
skipPersonas: Array.isArray(skipPersonas) ? skipPersonas : undefined,
|
|
113
|
+
})
|
|
114
|
+
: { ...resolveScanMode(scanMode), dropped: [], unknown: [] };
|
|
115
|
+
|
|
116
|
+
const { mode, personas } = resolved;
|
|
117
|
+
const droppedPersonas = resolved.dropped || [];
|
|
118
|
+
const unknownPersonas = resolved.unknown || [];
|
|
119
|
+
|
|
120
|
+
if (onEvent && (droppedPersonas.length > 0 || unknownPersonas.length > 0)) {
|
|
121
|
+
onEvent(createAgentEvent({
|
|
122
|
+
event: "omargate_persona_filter",
|
|
123
|
+
agent: OMAR_ORCHESTRATOR_AGENT,
|
|
124
|
+
payload: {
|
|
125
|
+
runId,
|
|
126
|
+
mode,
|
|
127
|
+
dropped: droppedPersonas,
|
|
128
|
+
unknown: unknownPersonas,
|
|
129
|
+
effective: personas,
|
|
130
|
+
},
|
|
131
|
+
runId,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
102
134
|
|
|
103
135
|
const roster = personas.map((personaId) => {
|
|
104
136
|
const visual = resolvePersonaVisual(personaId) || {};
|
|
@@ -322,6 +354,45 @@ export async function runOmarGateOrchestrator({
|
|
|
322
354
|
const totalCost = settled.reduce((sum, r) => sum + (r.costUsd || 0), 0);
|
|
323
355
|
const totalDuration = Date.now() - startTime;
|
|
324
356
|
|
|
357
|
+
// Silent-failure detection: if >=50% of personas errored OR total cost is
|
|
358
|
+
// zero with non-zero personas dispatched, treat as a LOUD orchestrator
|
|
359
|
+
// warning. Prior behavior silently returned zero AI findings, masking
|
|
360
|
+
// auth failures or LLM proxy outages as "clean scan".
|
|
361
|
+
const personaErrorCount = settled.filter((r) => r.status === "error").length;
|
|
362
|
+
const personaSkippedCount = settled.filter((r) => r.status === "skipped").length;
|
|
363
|
+
const personaOkCount = settled.filter((r) => r.status === "ok").length;
|
|
364
|
+
const totalPersonas = settled.length;
|
|
365
|
+
const errorRatio = totalPersonas > 0 ? personaErrorCount / totalPersonas : 0;
|
|
366
|
+
const aiCoverageHealthy =
|
|
367
|
+
totalPersonas === 0 ||
|
|
368
|
+
(personaOkCount > 0 && totalCost > 0 && errorRatio < 0.5 && !dryRun);
|
|
369
|
+
|
|
370
|
+
const personaHealth = {
|
|
371
|
+
ok: personaOkCount,
|
|
372
|
+
error: personaErrorCount,
|
|
373
|
+
skipped: personaSkippedCount,
|
|
374
|
+
total: totalPersonas,
|
|
375
|
+
errorRatio,
|
|
376
|
+
healthy: aiCoverageHealthy || dryRun,
|
|
377
|
+
warnings: [],
|
|
378
|
+
};
|
|
379
|
+
if (!dryRun && totalPersonas > 0) {
|
|
380
|
+
if (personaOkCount === 0 && personaErrorCount > 0) {
|
|
381
|
+
personaHealth.warnings.push(
|
|
382
|
+
`ALL ${totalPersonas} personas errored. AI coverage is ZERO. Re-check auth (sl auth login) or LLM proxy config.`
|
|
383
|
+
);
|
|
384
|
+
} else if (errorRatio >= 0.5) {
|
|
385
|
+
personaHealth.warnings.push(
|
|
386
|
+
`${personaErrorCount}/${totalPersonas} personas errored (${Math.round(errorRatio * 100)}%). AI coverage is degraded.`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
if (personaOkCount > 0 && totalCost <= 0) {
|
|
390
|
+
personaHealth.warnings.push(
|
|
391
|
+
`Personas reported ok status but totalCost=$0.00 — likely silently returned empty findings without making LLM calls.`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
325
396
|
const result = {
|
|
326
397
|
runId,
|
|
327
398
|
mode,
|
|
@@ -337,6 +408,7 @@ export async function runOmarGateOrchestrator({
|
|
|
337
408
|
model: r.model || null,
|
|
338
409
|
error: r.error || null,
|
|
339
410
|
})),
|
|
411
|
+
personaHealth,
|
|
340
412
|
findings: reconciledFindings,
|
|
341
413
|
findingsBySource: {
|
|
342
414
|
deterministic: detFindings.length,
|
|
@@ -358,6 +430,22 @@ export async function runOmarGateOrchestrator({
|
|
|
358
430
|
dryRun,
|
|
359
431
|
};
|
|
360
432
|
|
|
433
|
+
// Emit warnings to the event stream so terminal handler can render them.
|
|
434
|
+
if (onEvent && personaHealth.warnings.length > 0) {
|
|
435
|
+
onEvent(createAgentEvent({
|
|
436
|
+
event: "persona_health_warning",
|
|
437
|
+
agent: { id: "orchestrator", persona: "Omar Orchestrator" },
|
|
438
|
+
payload: {
|
|
439
|
+
ok: personaOkCount,
|
|
440
|
+
error: personaErrorCount,
|
|
441
|
+
total: totalPersonas,
|
|
442
|
+
errorRatio,
|
|
443
|
+
warnings: personaHealth.warnings,
|
|
444
|
+
},
|
|
445
|
+
runId,
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
|
|
361
449
|
if (onEvent) {
|
|
362
450
|
onEvent(createAgentEvent({
|
|
363
451
|
event: "omargate_complete",
|