sentinelayer-cli 0.4.5 → 0.8.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/README.md +16 -18
- package/package.json +7 -6
- package/src/agents/jules/config/definition.js +13 -62
- package/src/agents/jules/config/system-prompt.js +8 -1
- package/src/agents/jules/fix-cycle.js +12 -372
- package/src/agents/jules/loop.js +116 -26
- package/src/agents/jules/pulse.js +10 -327
- package/src/agents/jules/stream.js +13 -12
- package/src/agents/jules/swarm/orchestrator.js +3 -3
- package/src/agents/jules/swarm/sub-agent.js +6 -3
- package/src/agents/jules/tools/aidenid-email.js +189 -0
- package/src/agents/jules/tools/auth-audit.js +1187 -45
- package/src/agents/jules/tools/dispatch.js +25 -12
- package/src/agents/jules/tools/file-edit.js +2 -180
- package/src/agents/jules/tools/file-read.js +2 -100
- package/src/agents/jules/tools/glob.js +2 -168
- package/src/agents/jules/tools/grep.js +2 -228
- package/src/agents/jules/tools/path-guards.js +2 -161
- package/src/agents/jules/tools/runtime-audit.js +6 -2
- package/src/agents/jules/tools/shell.js +2 -383
- package/src/agents/persona-visuals.js +64 -0
- package/src/agents/shared-tools/dispatch-core.js +320 -0
- package/src/agents/shared-tools/file-edit.js +180 -0
- package/src/agents/shared-tools/file-read.js +100 -0
- package/src/agents/shared-tools/glob.js +168 -0
- package/src/agents/shared-tools/grep.js +228 -0
- package/src/agents/shared-tools/index.js +46 -0
- package/src/agents/shared-tools/path-guards.js +161 -0
- package/src/agents/shared-tools/shell.js +383 -0
- package/src/ai/aidenid.js +56 -7
- package/src/ai/client.js +45 -0
- package/src/ai/proxy.js +137 -0
- package/src/auth/gate.js +290 -16
- package/src/auth/http.js +450 -39
- package/src/auth/service.js +262 -47
- package/src/auth/session-store.js +475 -21
- package/src/cli.js +5 -0
- package/src/commands/audit.js +13 -8
- package/src/commands/auth.js +53 -9
- package/src/commands/omargate.js +10 -2
- package/src/commands/scan.js +10 -4
- package/src/commands/session.js +590 -0
- package/src/commands/spec.js +62 -0
- package/src/commands/watch.js +3 -2
- package/src/daemon/assignment-ledger.js +196 -0
- package/src/daemon/error-worker.js +599 -16
- package/src/daemon/fix-cycle.js +384 -0
- package/src/daemon/ingest-refresh.js +10 -9
- package/src/daemon/jira-lifecycle.js +135 -0
- package/src/daemon/pulse.js +327 -0
- package/src/daemon/scope-engine.js +1068 -0
- package/src/events/schema.js +190 -0
- package/src/interactive/index.js +18 -16
- package/src/legacy-cli.js +606 -37
- package/src/prompt/generator.js +19 -1
- package/src/review/ai-review.js +11 -1
- package/src/review/local-review.js +75 -19
- package/src/review/omargate-interactive.js +68 -0
- package/src/review/omargate-orchestrator.js +404 -0
- package/src/review/persona-prompts.js +296 -0
- package/src/review/scan-modes.js +48 -0
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +352 -0
- package/src/session/daemon.js +801 -0
- package/src/session/paths.js +33 -0
- package/src/session/runtime-bridge.js +739 -0
- package/src/session/store.js +388 -0
- package/src/session/stream.js +325 -0
- package/src/spec/generator.js +100 -0
- package/src/telemetry/session-tracker.js +148 -32
- package/src/telemetry/sync.js +6 -2
- package/src/ui/command-hints.js +13 -0
package/src/prompt/generator.js
CHANGED
|
@@ -34,6 +34,12 @@ const TARGET_GUIDANCE = Object.freeze({
|
|
|
34
34
|
],
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
const SESSION_COORDINATION_GUIDANCE = Object.freeze([
|
|
38
|
+
"Multi-agent coordination: use `sl session` commands to communicate with other agents.",
|
|
39
|
+
"Always update the session chat room with your current activity so joining agents have context.",
|
|
40
|
+
"Never break your autonomous loop on unexpected file changes; ask in the session first.",
|
|
41
|
+
]);
|
|
42
|
+
|
|
37
43
|
function normalizeTarget(target) {
|
|
38
44
|
const normalized = String(target || "generic").trim().toLowerCase();
|
|
39
45
|
if (!SUPPORTED_PROMPT_TARGETS.includes(normalized)) {
|
|
@@ -55,6 +61,14 @@ function buildAgentHeader(target) {
|
|
|
55
61
|
return headers[target] || headers.generic;
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
function shouldAppendSessionGuidance(specMarkdown) {
|
|
65
|
+
const normalized = String(specMarkdown || "").toLowerCase();
|
|
66
|
+
if (!normalized) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return normalized.includes("coordination protocol") || normalized.includes("session");
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
export function resolvePromptTarget(target) {
|
|
59
73
|
return normalizeTarget(target);
|
|
60
74
|
}
|
|
@@ -81,7 +95,11 @@ export function generateExecutionPrompt({
|
|
|
81
95
|
throw new Error("Spec content is empty. Generate or provide a spec before creating a prompt.");
|
|
82
96
|
}
|
|
83
97
|
|
|
84
|
-
const
|
|
98
|
+
const operatingRules = [...guidance];
|
|
99
|
+
if (shouldAppendSessionGuidance(specText)) {
|
|
100
|
+
operatingRules.push(...SESSION_COORDINATION_GUIDANCE);
|
|
101
|
+
}
|
|
102
|
+
const guidanceMarkdown = operatingRules.map((item, index) => `${index + 1}. ${item}`).join("\n");
|
|
85
103
|
|
|
86
104
|
const hasAidenId = specText.toLowerCase().includes("aidenid");
|
|
87
105
|
const aidenidGuidance = hasAidenId
|
package/src/review/ai-review.js
CHANGED
|
@@ -413,11 +413,21 @@ export async function runAiReviewLayer({
|
|
|
413
413
|
const normalizedRunId = normalizeString(runId) || "review-ai";
|
|
414
414
|
|
|
415
415
|
const config = await loadConfig({ cwd: normalizedTargetPath, env });
|
|
416
|
-
|
|
416
|
+
let resolvedProvider = resolveProvider({
|
|
417
417
|
provider,
|
|
418
418
|
configProvider: config.resolved.defaultModelProvider,
|
|
419
419
|
env,
|
|
420
420
|
});
|
|
421
|
+
// If no explicit provider and default fell through to openai,
|
|
422
|
+
// check for stored sentinelayer session (async fallback)
|
|
423
|
+
if (resolvedProvider === "openai" && !provider && !config.resolved.defaultModelProvider) {
|
|
424
|
+
try {
|
|
425
|
+
const { resolveProviderAsync } = await import("../ai/client.js");
|
|
426
|
+
resolvedProvider = await resolveProviderAsync({ env });
|
|
427
|
+
} catch {
|
|
428
|
+
// keep sync result
|
|
429
|
+
}
|
|
430
|
+
}
|
|
421
431
|
const resolvedModel = resolveModel({
|
|
422
432
|
provider: resolvedProvider,
|
|
423
433
|
model,
|
|
@@ -14,8 +14,34 @@ const IGNORED_DIRS = new Set([
|
|
|
14
14
|
".next",
|
|
15
15
|
"dist",
|
|
16
16
|
"build",
|
|
17
|
+
"out",
|
|
18
|
+
"coverage",
|
|
19
|
+
"__pycache__",
|
|
20
|
+
".turbo",
|
|
21
|
+
".cache",
|
|
22
|
+
".parcel-cache",
|
|
23
|
+
".svelte-kit",
|
|
24
|
+
".nuxt",
|
|
25
|
+
".output",
|
|
26
|
+
".vercel",
|
|
17
27
|
".sentinelayer",
|
|
28
|
+
// v0.7 (2026-04-16): exclude generated demo/e2e folders that create massive
|
|
29
|
+
// self-scan noise. These are fixtures produced by the CLI itself and
|
|
30
|
+
// contain intentionally-naive demo code that pollutes reviewer signal.
|
|
31
|
+
"demo-e2e-cli-todo",
|
|
32
|
+
"demo-playground",
|
|
33
|
+
"e2e-demo-2026-04-10",
|
|
34
|
+
".worktree-runtime",
|
|
18
35
|
]);
|
|
36
|
+
|
|
37
|
+
// Paths that are always "test-like" — rules with high false-positive rates
|
|
38
|
+
// in tests (hardcoded localhost, HTTP literals, example credentials) should
|
|
39
|
+
// suppress or demote findings here. Keep narrow; do not suppress P0/P1.
|
|
40
|
+
const TEST_LIKE_PATH_PATTERN = /(?:^|[\\/])(?:tests?|__tests__|fixtures?|e2e|demo|demo-[^\\/]*|examples?|samples?|docs?[\\/]examples?)(?:[\\/]|$)/i;
|
|
41
|
+
|
|
42
|
+
function isTestLikePath(relPath) {
|
|
43
|
+
return TEST_LIKE_PATH_PATTERN.test(String(relPath || ""));
|
|
44
|
+
}
|
|
19
45
|
const MAX_FILE_SIZE_BYTES = 512 * 1024;
|
|
20
46
|
const MAX_FINDINGS = 250;
|
|
21
47
|
const STATIC_CHECK_TIMEOUT_MS = 120_000;
|
|
@@ -184,8 +210,12 @@ const DETERMINISTIC_REVIEW_RULES = Object.freeze([
|
|
|
184
210
|
severity: "P2",
|
|
185
211
|
message: "Plain HTTP endpoint literal found.",
|
|
186
212
|
suggestedFix: "Prefer HTTPS endpoints in production paths.",
|
|
187
|
-
|
|
213
|
+
// Skip localhost/127.0.0.1/0.0.0.0 and common local-dev host patterns —
|
|
214
|
+
// those are dev-time fixtures and do not warrant P2. Plain http://example.com,
|
|
215
|
+
// cdn URLs, and external hosts still fire.
|
|
216
|
+
regex: /\bhttp:\/\/(?!(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1|\[::1\]))[^\s'"]+/i,
|
|
188
217
|
sourceOnly: true,
|
|
218
|
+
excludePathPattern: TEST_LIKE_PATH_PATTERN,
|
|
189
219
|
},
|
|
190
220
|
{
|
|
191
221
|
id: "SL-SEC-014",
|
|
@@ -203,6 +233,7 @@ const DETERMINISTIC_REVIEW_RULES = Object.freeze([
|
|
|
203
233
|
suggestedFix: "Replace eval with explicit parser or safe handlers.",
|
|
204
234
|
regex: /\beval\s*\(/,
|
|
205
235
|
sourceOnly: true,
|
|
236
|
+
excludePathPattern: TEST_LIKE_PATH_PATTERN,
|
|
206
237
|
},
|
|
207
238
|
{
|
|
208
239
|
id: "SL-SEC-016",
|
|
@@ -219,6 +250,9 @@ const DETERMINISTIC_REVIEW_RULES = Object.freeze([
|
|
|
219
250
|
suggestedFix: "Use parameterized queries and prepared statements.",
|
|
220
251
|
regex: /\b(?:SELECT|INSERT|UPDATE|DELETE)\b[^;\n]{0,140}\+/i,
|
|
221
252
|
sourceOnly: true,
|
|
253
|
+
// Self-scan dampener: the local-review source itself contains SQL-like
|
|
254
|
+
// regex literals and must not flag itself.
|
|
255
|
+
excludePathPattern: LOCAL_REVIEW_SOURCE_PATH_PATTERN,
|
|
222
256
|
},
|
|
223
257
|
{
|
|
224
258
|
id: "SL-SEC-018",
|
|
@@ -241,8 +275,12 @@ const DETERMINISTIC_REVIEW_RULES = Object.freeze([
|
|
|
241
275
|
severity: "P2",
|
|
242
276
|
message: "Potentially sensitive value logged directly.",
|
|
243
277
|
suggestedFix: "Redact secrets/tokens before logging.",
|
|
244
|
-
regex:
|
|
278
|
+
// Tighter regex: require the secret-like identifier to be a variable
|
|
279
|
+
// reference, not just appear inside any string/template. Excludes
|
|
280
|
+
// error-message text like "invalid token" and doc examples.
|
|
281
|
+
regex: /console\.(?:log|debug|info)\(\s*(?:[`"'][^`"']*\$\{)?\s*[A-Za-z_$][A-Za-z0-9_$]*\.?(token|secret|password|api[_-]?key)/i,
|
|
245
282
|
sourceOnly: true,
|
|
283
|
+
excludePathPattern: TEST_LIKE_PATH_PATTERN,
|
|
246
284
|
},
|
|
247
285
|
{
|
|
248
286
|
id: "SL-SEC-021",
|
|
@@ -260,6 +298,7 @@ const DETERMINISTIC_REVIEW_RULES = Object.freeze([
|
|
|
260
298
|
suggestedFix: "Externalize callback URLs to environment config.",
|
|
261
299
|
regex: /https?:\/\/localhost:\d{2,5}\//i,
|
|
262
300
|
sourceOnly: true,
|
|
301
|
+
excludePathPattern: TEST_LIKE_PATH_PATTERN,
|
|
263
302
|
},
|
|
264
303
|
]);
|
|
265
304
|
|
|
@@ -651,21 +690,33 @@ async function runPatternChecks({ rootPath, filePaths, maxFindings = MAX_FINDING
|
|
|
651
690
|
continue;
|
|
652
691
|
}
|
|
653
692
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
693
|
+
// Require actual JSX attribute usage OR DOM property assignment, not
|
|
694
|
+
// bare mentions in strings/docstrings (common in prompt templates and
|
|
695
|
+
// detector files that search for the pattern as a label).
|
|
696
|
+
const realJsxUsage = /dangerouslySetInnerHTML\s*=\s*\{\s*\{/.test(line);
|
|
697
|
+
const realDomAssign = /(?:^|[.\s])innerHTML\s*=\s*(?!=)/.test(line);
|
|
698
|
+
if (realJsxUsage || realDomAssign) {
|
|
699
|
+
const isPromptOrConfig =
|
|
700
|
+
/(?:^|[\\/])(?:config|prompts?|templates?|system-prompt|swarm|agents)[\\/]/i.test(relativePath) ||
|
|
701
|
+
/-prompts?\.(?:m?js|tsx?)$/i.test(relativePath) ||
|
|
702
|
+
/system-prompt\.(?:m?js|tsx?)$/i.test(relativePath) ||
|
|
703
|
+
/(?:file-scanner|pattern-hunter|-scanner|-hunter)\.(?:m?js|tsx?)$/i.test(relativePath);
|
|
704
|
+
if (!isTestLikePath(relativePath) && !isPromptOrConfig) {
|
|
705
|
+
tryPushFinding(
|
|
706
|
+
findings,
|
|
707
|
+
createFinding({
|
|
708
|
+
severity: "P1",
|
|
709
|
+
file: relativePath,
|
|
710
|
+
line: index + 1,
|
|
711
|
+
message: "Direct HTML sink detected; validate/sanitize untrusted content.",
|
|
712
|
+
excerpt: sanitizeLineForExcerpt(line),
|
|
713
|
+
ruleId: "SL-PAT-002",
|
|
714
|
+
suggestedFix: "Apply strict sanitization and avoid raw HTML sinks.",
|
|
715
|
+
layer: "pattern",
|
|
716
|
+
}),
|
|
717
|
+
maxFindings
|
|
718
|
+
);
|
|
719
|
+
}
|
|
669
720
|
}
|
|
670
721
|
|
|
671
722
|
if (/useEffect\s*\(/.test(line)) {
|
|
@@ -690,7 +741,7 @@ async function runPatternChecks({ rootPath, filePaths, maxFindings = MAX_FINDING
|
|
|
690
741
|
|
|
691
742
|
if (/(for|while)\s*\([^)]*\)/.test(line)) {
|
|
692
743
|
const window = lines.slice(index, Math.min(lines.length, index + 10)).join("\n");
|
|
693
|
-
if (/findMany\(|query\(|SELECT\b|fetch\(/i.test(window)) {
|
|
744
|
+
if (/findMany\(|query\(|SELECT\b|fetch\(/i.test(window) && !isTestLikePath(relativePath)) {
|
|
694
745
|
tryPushFinding(
|
|
695
746
|
findings,
|
|
696
747
|
createFinding({
|
|
@@ -710,7 +761,12 @@ async function runPatternChecks({ rootPath, filePaths, maxFindings = MAX_FINDING
|
|
|
710
761
|
}
|
|
711
762
|
|
|
712
763
|
const sqlConcat = /\b(?:SELECT|INSERT|UPDATE|DELETE)\b[^\n]{0,160}\+/i.exec(text);
|
|
713
|
-
if (
|
|
764
|
+
if (
|
|
765
|
+
sqlConcat &&
|
|
766
|
+
findings.length < maxFindings &&
|
|
767
|
+
!isTestLikePath(relativePath) &&
|
|
768
|
+
!LOCAL_REVIEW_SOURCE_PATH_PATTERN.test(relativePath)
|
|
769
|
+
) {
|
|
714
770
|
const lineNumber = resolveLineNumberFromIndex(text, sqlConcat.index);
|
|
715
771
|
tryPushFinding(
|
|
716
772
|
findings,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive post-scan menu for Omar Gate deep-dive.
|
|
3
|
+
*
|
|
4
|
+
* After the scan completes, prompts the user to select a domain agent
|
|
5
|
+
* for full agentic loop analysis (multi-turn, tool-using).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
|
|
12
|
+
import { PERSONA_IDS } from "./persona-prompts.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Show interactive persona selection after Omar Gate scan.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} options
|
|
18
|
+
* @param {object} options.scanResult - Result from runOmarGateOrchestrator
|
|
19
|
+
* @returns {Promise<string|null>} Selected persona ID, "all", or null (skip)
|
|
20
|
+
*/
|
|
21
|
+
export async function promptPersonaDeepDive({ scanResult } = {}) {
|
|
22
|
+
const summary = scanResult?.summary || {};
|
|
23
|
+
const personas = scanResult?.personas || [];
|
|
24
|
+
|
|
25
|
+
console.log("");
|
|
26
|
+
console.log(pc.bold("Omar Gate scan complete."));
|
|
27
|
+
console.log(
|
|
28
|
+
`Findings: P0=${summary.P0 || 0} P1=${summary.P1 || 0} P2=${summary.P2 || 0} P3=${summary.P3 || 0} | ` +
|
|
29
|
+
`Cost: $${(scanResult?.totalCostUsd || 0).toFixed(4)} | ` +
|
|
30
|
+
`Duration: ${((scanResult?.totalDurationMs || 0) / 1000).toFixed(1)}s`
|
|
31
|
+
);
|
|
32
|
+
console.log("");
|
|
33
|
+
|
|
34
|
+
// Show persona results
|
|
35
|
+
for (const p of personas) {
|
|
36
|
+
const icon = p.status === "ok" ? pc.green("✓") : p.status === "skipped" ? pc.gray("○") : pc.red("✗");
|
|
37
|
+
const count = p.findings || 0;
|
|
38
|
+
console.log(` ${icon} ${p.id} — ${count} finding${count === 1 ? "" : "s"}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log(pc.gray("Deep-dive runs a full agentic loop (multi-turn, tool-using) for deeper analysis."));
|
|
43
|
+
console.log(pc.gray(`Available: ${PERSONA_IDS.join(", ")}, all, none`));
|
|
44
|
+
console.log("");
|
|
45
|
+
|
|
46
|
+
const rl = createInterface({ input, output });
|
|
47
|
+
try {
|
|
48
|
+
const answer = await rl.question(pc.cyan("Deep-dive into which agent? [none] "));
|
|
49
|
+
const normalized = String(answer || "").trim().toLowerCase();
|
|
50
|
+
|
|
51
|
+
if (!normalized || normalized === "none" || normalized === "n" || normalized === "skip") {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (normalized === "all") {
|
|
56
|
+
return "all";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (PERSONA_IDS.includes(normalized)) {
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(pc.yellow(`Unknown agent '${normalized}'. Skipping deep-dive.`));
|
|
64
|
+
return null;
|
|
65
|
+
} finally {
|
|
66
|
+
rl.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Omar Gate multi-persona orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Runs N persona-scoped AI review calls in parallel (bounded concurrency),
|
|
5
|
+
* merges findings into a blackboard, deduplicates, and produces a unified report.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
|
|
10
|
+
import { runAiReviewLayer } from "./ai-review.js";
|
|
11
|
+
import { buildPersonaReviewPrompt, PERSONA_IDS } from "./persona-prompts.js";
|
|
12
|
+
import { resolveScanMode } from "./scan-modes.js";
|
|
13
|
+
import { reconcileReviewFindings } from "./report.js";
|
|
14
|
+
import { resolvePersonaVisual } from "../agents/persona-visuals.js";
|
|
15
|
+
import { syncRunToDashboard } from "../telemetry/sync.js";
|
|
16
|
+
import { createAgentEvent } from "../events/schema.js";
|
|
17
|
+
|
|
18
|
+
const OMAR_ORCHESTRATOR_AGENT = Object.freeze({
|
|
19
|
+
id: "omar-orchestrator",
|
|
20
|
+
persona: "Omar Gate Orchestrator",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run bounded-concurrency parallel execution.
|
|
25
|
+
* @param {Array} items
|
|
26
|
+
* @param {number} maxConcurrent
|
|
27
|
+
* @param {Function} fn
|
|
28
|
+
* @returns {Promise<Array>}
|
|
29
|
+
*/
|
|
30
|
+
async function runWithConcurrency(items, maxConcurrent, fn) {
|
|
31
|
+
const results = [];
|
|
32
|
+
const executing = new Set();
|
|
33
|
+
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
const p = fn(item).then((result) => {
|
|
36
|
+
executing.delete(p);
|
|
37
|
+
return result;
|
|
38
|
+
});
|
|
39
|
+
executing.add(p);
|
|
40
|
+
results.push(p);
|
|
41
|
+
|
|
42
|
+
if (executing.size >= maxConcurrent) {
|
|
43
|
+
await Promise.race(executing);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return Promise.allSettled(results);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Annotate persona result with visual identity so stream consumers
|
|
52
|
+
* and downstream reports never see faceless persona IDs.
|
|
53
|
+
*/
|
|
54
|
+
function decoratePersonaResult(personaId, baseResult) {
|
|
55
|
+
const visual = resolvePersonaVisual(personaId) || {};
|
|
56
|
+
return {
|
|
57
|
+
...baseResult,
|
|
58
|
+
personaId,
|
|
59
|
+
persona: {
|
|
60
|
+
id: personaId,
|
|
61
|
+
shortName: visual.shortName || personaId,
|
|
62
|
+
fullName: visual.fullName || personaId,
|
|
63
|
+
avatar: visual.avatar || "",
|
|
64
|
+
color: visual.color || "gray",
|
|
65
|
+
domain: visual.domain || personaId,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Run the Omar Gate multi-persona orchestrator.
|
|
72
|
+
*
|
|
73
|
+
* @param {object} options
|
|
74
|
+
* @param {string} options.targetPath - Repository path
|
|
75
|
+
* @param {string} [options.scanMode] - "baseline", "deep", or "full-depth"
|
|
76
|
+
* @param {number} [options.maxParallel] - Max concurrent persona calls (default 4)
|
|
77
|
+
* @param {string} [options.provider] - LLM provider override
|
|
78
|
+
* @param {string} [options.model] - LLM model override
|
|
79
|
+
* @param {number} [options.maxCostUsd] - Global cost ceiling (default 5.0)
|
|
80
|
+
* @param {boolean} [options.dryRun] - Dry-run mode (no LLM calls)
|
|
81
|
+
* @param {string} [options.outputDir] - Output directory override
|
|
82
|
+
* @param {object} [options.deterministic] - Deterministic scan results
|
|
83
|
+
* @param {Function} [options.onEvent] - Event callback for streaming
|
|
84
|
+
* @returns {Promise<object>} Orchestrated results
|
|
85
|
+
*/
|
|
86
|
+
export async function runOmarGateOrchestrator({
|
|
87
|
+
targetPath,
|
|
88
|
+
scanMode = "deep",
|
|
89
|
+
maxParallel = 4,
|
|
90
|
+
provider = "",
|
|
91
|
+
model = "",
|
|
92
|
+
maxCostUsd = 5.0,
|
|
93
|
+
dryRun = false,
|
|
94
|
+
outputDir = "",
|
|
95
|
+
deterministic = null,
|
|
96
|
+
onEvent = null,
|
|
97
|
+
} = {}) {
|
|
98
|
+
const runId = `omargate-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
99
|
+
const startTime = Date.now();
|
|
100
|
+
|
|
101
|
+
const { mode, personas } = resolveScanMode(scanMode);
|
|
102
|
+
|
|
103
|
+
const roster = personas.map((personaId) => {
|
|
104
|
+
const visual = resolvePersonaVisual(personaId) || {};
|
|
105
|
+
return {
|
|
106
|
+
id: personaId,
|
|
107
|
+
shortName: visual.shortName || personaId,
|
|
108
|
+
fullName: visual.fullName || personaId,
|
|
109
|
+
avatar: visual.avatar || "",
|
|
110
|
+
color: visual.color || "gray",
|
|
111
|
+
domain: visual.domain || personaId,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (onEvent) {
|
|
116
|
+
onEvent(createAgentEvent({
|
|
117
|
+
event: "omargate_start",
|
|
118
|
+
agent: OMAR_ORCHESTRATOR_AGENT,
|
|
119
|
+
payload: { runId, mode, personas, roster, maxParallel, maxCostUsd, dryRun },
|
|
120
|
+
runId,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const detSummary = deterministic?.summary || { P0: 0, P1: 0, P2: 0, P3: 0 };
|
|
125
|
+
const detFindings = deterministic?.findings || [];
|
|
126
|
+
|
|
127
|
+
// Per-persona cost budget = global / persona count (with minimum floor)
|
|
128
|
+
const perPersonaCost = Math.max(0.25, maxCostUsd / personas.length);
|
|
129
|
+
let runningCostUsd = 0;
|
|
130
|
+
|
|
131
|
+
const personaResults = await runWithConcurrency(personas, maxParallel, async (personaId) => {
|
|
132
|
+
const visual = resolvePersonaVisual(personaId) || {};
|
|
133
|
+
const identity = {
|
|
134
|
+
id: personaId,
|
|
135
|
+
shortName: visual.shortName || personaId,
|
|
136
|
+
fullName: visual.fullName || personaId,
|
|
137
|
+
avatar: visual.avatar || "",
|
|
138
|
+
color: visual.color || "gray",
|
|
139
|
+
domain: visual.domain || personaId,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Global budget check — skip remaining personas if exhausted
|
|
143
|
+
if (runningCostUsd >= maxCostUsd) {
|
|
144
|
+
if (onEvent) {
|
|
145
|
+
onEvent(createAgentEvent({
|
|
146
|
+
event: "persona_skipped",
|
|
147
|
+
agent: {
|
|
148
|
+
id: identity.id,
|
|
149
|
+
persona: identity.fullName,
|
|
150
|
+
shortName: identity.shortName,
|
|
151
|
+
color: identity.color,
|
|
152
|
+
avatar: identity.avatar,
|
|
153
|
+
domain: identity.domain,
|
|
154
|
+
},
|
|
155
|
+
payload: { personaId, identity, reason: "global_budget_exhausted", runningCostUsd, maxCostUsd },
|
|
156
|
+
runId,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
personaId,
|
|
161
|
+
status: "skipped",
|
|
162
|
+
findings: [],
|
|
163
|
+
summary: { P0: 0, P1: 0, P2: 0, P3: 0 },
|
|
164
|
+
costUsd: 0,
|
|
165
|
+
durationMs: 0,
|
|
166
|
+
reason: "global_budget_exhausted",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const personaStart = Date.now();
|
|
171
|
+
|
|
172
|
+
if (onEvent) {
|
|
173
|
+
onEvent(createAgentEvent({
|
|
174
|
+
event: "persona_start",
|
|
175
|
+
agent: {
|
|
176
|
+
id: identity.id,
|
|
177
|
+
persona: identity.fullName,
|
|
178
|
+
shortName: identity.shortName,
|
|
179
|
+
color: identity.color,
|
|
180
|
+
avatar: identity.avatar,
|
|
181
|
+
domain: identity.domain,
|
|
182
|
+
},
|
|
183
|
+
payload: { personaId, identity, mode, runId },
|
|
184
|
+
runId,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const systemPrompt = buildPersonaReviewPrompt({
|
|
190
|
+
personaId,
|
|
191
|
+
targetPath,
|
|
192
|
+
deterministicSummary: detSummary,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = await runAiReviewLayer({
|
|
196
|
+
targetPath,
|
|
197
|
+
mode: "full",
|
|
198
|
+
runId: `${runId}-${personaId}`,
|
|
199
|
+
runDirectory: targetPath,
|
|
200
|
+
deterministic: {
|
|
201
|
+
summary: detSummary,
|
|
202
|
+
findings: detFindings,
|
|
203
|
+
metadata: deterministic?.metadata || {},
|
|
204
|
+
},
|
|
205
|
+
outputDir,
|
|
206
|
+
provider: provider || undefined,
|
|
207
|
+
model: model || undefined,
|
|
208
|
+
maxCostUsd: perPersonaCost,
|
|
209
|
+
dryRun,
|
|
210
|
+
env: process.env,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const findings = (result?.findings || []).map((f) => ({
|
|
214
|
+
...f,
|
|
215
|
+
persona: personaId,
|
|
216
|
+
layer: personaId,
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
if (onEvent) {
|
|
220
|
+
for (const finding of findings) {
|
|
221
|
+
onEvent(createAgentEvent({
|
|
222
|
+
event: "persona_finding",
|
|
223
|
+
agent: {
|
|
224
|
+
id: identity.id,
|
|
225
|
+
persona: identity.fullName,
|
|
226
|
+
shortName: identity.shortName,
|
|
227
|
+
color: identity.color,
|
|
228
|
+
avatar: identity.avatar,
|
|
229
|
+
domain: identity.domain,
|
|
230
|
+
},
|
|
231
|
+
payload: { personaId, identity, ...finding },
|
|
232
|
+
runId,
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
onEvent(createAgentEvent({
|
|
236
|
+
event: "persona_complete",
|
|
237
|
+
agent: {
|
|
238
|
+
id: identity.id,
|
|
239
|
+
persona: identity.fullName,
|
|
240
|
+
shortName: identity.shortName,
|
|
241
|
+
color: identity.color,
|
|
242
|
+
avatar: identity.avatar,
|
|
243
|
+
domain: identity.domain,
|
|
244
|
+
},
|
|
245
|
+
payload: {
|
|
246
|
+
personaId,
|
|
247
|
+
identity,
|
|
248
|
+
findings: findings.length,
|
|
249
|
+
summary: result?.summary || {},
|
|
250
|
+
costUsd: result?.costUsd || 0,
|
|
251
|
+
durationMs: Date.now() - personaStart,
|
|
252
|
+
},
|
|
253
|
+
runId,
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const personaCost = result?.costUsd || 0;
|
|
258
|
+
runningCostUsd += personaCost;
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
personaId,
|
|
262
|
+
status: "ok",
|
|
263
|
+
findings,
|
|
264
|
+
summary: result?.summary || { P0: 0, P1: 0, P2: 0, P3: 0 },
|
|
265
|
+
costUsd: personaCost,
|
|
266
|
+
model: result?.model || model || null,
|
|
267
|
+
durationMs: Date.now() - personaStart,
|
|
268
|
+
};
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if (onEvent) {
|
|
271
|
+
onEvent(createAgentEvent({
|
|
272
|
+
event: "persona_error",
|
|
273
|
+
agent: {
|
|
274
|
+
id: identity.id,
|
|
275
|
+
persona: identity.fullName,
|
|
276
|
+
shortName: identity.shortName,
|
|
277
|
+
color: identity.color,
|
|
278
|
+
avatar: identity.avatar,
|
|
279
|
+
domain: identity.domain,
|
|
280
|
+
},
|
|
281
|
+
payload: { personaId, identity, error: err.message },
|
|
282
|
+
runId,
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
personaId,
|
|
287
|
+
status: "error",
|
|
288
|
+
findings: [],
|
|
289
|
+
summary: { P0: 0, P1: 0, P2: 0, P3: 0 },
|
|
290
|
+
costUsd: 0,
|
|
291
|
+
error: err.message,
|
|
292
|
+
durationMs: Date.now() - personaStart,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Collect results (handle settled promises)
|
|
298
|
+
const settled = personaResults.map((r) =>
|
|
299
|
+
r.status === "fulfilled"
|
|
300
|
+
? decoratePersonaResult(r.value.personaId, r.value)
|
|
301
|
+
: decoratePersonaResult("unknown", {
|
|
302
|
+
status: "error",
|
|
303
|
+
findings: [],
|
|
304
|
+
summary: { P0: 0, P1: 0, P2: 0, P3: 0 },
|
|
305
|
+
costUsd: 0,
|
|
306
|
+
error: r.reason?.message || "unknown",
|
|
307
|
+
durationMs: 0,
|
|
308
|
+
})
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Reconcile AI findings with deterministic findings — canonical single list.
|
|
312
|
+
// Confidence boost when multiple layers agree; deterministic findings get
|
|
313
|
+
// confidence 1.0; AI findings keep their self-reported confidence.
|
|
314
|
+
const allAiFindings = settled.flatMap((r) => r.findings);
|
|
315
|
+
const reconciled = reconcileReviewFindings({
|
|
316
|
+
deterministicFindings: detFindings,
|
|
317
|
+
aiFindings: allAiFindings,
|
|
318
|
+
});
|
|
319
|
+
const reconciledFindings = reconciled.findings;
|
|
320
|
+
const reconciledSummary = reconciled.summary;
|
|
321
|
+
|
|
322
|
+
const totalCost = settled.reduce((sum, r) => sum + (r.costUsd || 0), 0);
|
|
323
|
+
const totalDuration = Date.now() - startTime;
|
|
324
|
+
|
|
325
|
+
const result = {
|
|
326
|
+
runId,
|
|
327
|
+
mode,
|
|
328
|
+
roster,
|
|
329
|
+
personas: settled.map((r) => ({
|
|
330
|
+
id: r.personaId,
|
|
331
|
+
identity: r.persona,
|
|
332
|
+
status: r.status,
|
|
333
|
+
findings: (r.findings || []).length,
|
|
334
|
+
summary: r.summary || { P0: 0, P1: 0, P2: 0, P3: 0 },
|
|
335
|
+
costUsd: r.costUsd,
|
|
336
|
+
durationMs: r.durationMs,
|
|
337
|
+
model: r.model || null,
|
|
338
|
+
error: r.error || null,
|
|
339
|
+
})),
|
|
340
|
+
findings: reconciledFindings,
|
|
341
|
+
findingsBySource: {
|
|
342
|
+
deterministic: detFindings.length,
|
|
343
|
+
ai: allAiFindings.length,
|
|
344
|
+
reconciled: reconciledFindings.length,
|
|
345
|
+
},
|
|
346
|
+
summary: reconciledSummary,
|
|
347
|
+
totalCostUsd: totalCost,
|
|
348
|
+
totalDurationMs: totalDuration,
|
|
349
|
+
reconciliation: {
|
|
350
|
+
deterministicFindings: detFindings.length,
|
|
351
|
+
aiFindings: allAiFindings.length,
|
|
352
|
+
reconciledFindings: reconciledFindings.length,
|
|
353
|
+
dedupedCount: detFindings.length + allAiFindings.length - reconciledFindings.length,
|
|
354
|
+
multiSourceFindings: reconciledFindings.filter(
|
|
355
|
+
(f) => Array.isArray(f.sources) && f.sources.length > 1
|
|
356
|
+
).length,
|
|
357
|
+
},
|
|
358
|
+
dryRun,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
if (onEvent) {
|
|
362
|
+
onEvent(createAgentEvent({
|
|
363
|
+
event: "omargate_complete",
|
|
364
|
+
agent: OMAR_ORCHESTRATOR_AGENT,
|
|
365
|
+
payload: {
|
|
366
|
+
runId,
|
|
367
|
+
mode,
|
|
368
|
+
personaCount: settled.length,
|
|
369
|
+
findings: reconciledFindings.length,
|
|
370
|
+
summary: result.summary,
|
|
371
|
+
reconciliation: result.reconciliation,
|
|
372
|
+
totalCostUsd: totalCost,
|
|
373
|
+
totalDurationMs: totalDuration,
|
|
374
|
+
},
|
|
375
|
+
runId,
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Fire-and-forget telemetry sync to dashboard
|
|
380
|
+
syncRunToDashboard({
|
|
381
|
+
command: `omargate deep --scan-mode ${mode}`,
|
|
382
|
+
persona: "omar-orchestrator",
|
|
383
|
+
usage: {
|
|
384
|
+
inputTokens: 0,
|
|
385
|
+
outputTokens: 0,
|
|
386
|
+
costUsd: totalCost,
|
|
387
|
+
durationMs: totalDuration,
|
|
388
|
+
toolCalls: personas.length,
|
|
389
|
+
},
|
|
390
|
+
summary: result.summary,
|
|
391
|
+
reconciliation: result.reconciliation,
|
|
392
|
+
stopReason: result.summary.blocking ? "blocked" : "passed",
|
|
393
|
+
personaBreakdown: settled.map((r) => ({
|
|
394
|
+
personaId: r.personaId,
|
|
395
|
+
fullName: r.persona?.fullName || r.personaId,
|
|
396
|
+
findings: r.findings?.length || 0,
|
|
397
|
+
costUsd: r.costUsd || 0,
|
|
398
|
+
durationMs: r.durationMs || 0,
|
|
399
|
+
status: r.status,
|
|
400
|
+
})),
|
|
401
|
+
}).catch(() => {});
|
|
402
|
+
|
|
403
|
+
return result;
|
|
404
|
+
}
|