sentinelayer-cli 0.8.11 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -5
- package/src/agents/devtestbot/config/definition.js +100 -0
- package/src/agents/devtestbot/config/system-prompt.js +92 -0
- package/src/agents/devtestbot/index.js +9 -0
- package/src/agents/devtestbot/runner.js +769 -0
- package/src/agents/devtestbot/tool.js +707 -0
- package/src/agents/jules/stream.js +2 -12
- package/src/audit/orchestrator.js +471 -114
- package/src/audit/persona-loop.js +1342 -0
- package/src/audit/registry.js +58 -2
- package/src/commands/audit.js +42 -1
- package/src/commands/legacy-args.js +32 -1
- package/src/commands/omargate.js +4 -0
- package/src/commands/session.js +417 -89
- package/src/commands/swarm.js +11 -2
- package/src/cost/history.js +41 -21
- package/src/events/schema.js +27 -1
- package/src/guide/generator.js +14 -0
- package/src/legacy-cli.js +110 -18
- package/src/prompt/generator.js +4 -16
- package/src/review/ai-review.js +95 -6
- package/src/review/dd-report-email-client.js +148 -0
- package/src/review/investor-dd-devtestbot.js +599 -0
- package/src/review/investor-dd-orchestrator.js +135 -3
- package/src/review/omargate-cache.js +285 -0
- package/src/review/omargate-orchestrator.js +605 -4
- package/src/review/persona-prompts.js +34 -1
- package/src/review/report.js +189 -4
- package/src/session/coordination-guidance.js +48 -0
- package/src/session/daemon.js +3 -2
- package/src/session/listener.js +236 -0
- package/src/session/senti-naming.js +36 -0
- package/src/session/setup-guides.js +3 -15
- package/src/session/store.js +54 -5
- package/src/session/sync.js +23 -0
- package/src/spec/generator.js +8 -10
- package/src/swarm/registry.js +20 -0
- package/src/swarm/runtime.js +139 -1
package/src/review/ai-review.js
CHANGED
|
@@ -58,6 +58,20 @@ function sanitizeExcerpt(text) {
|
|
|
58
58
|
.slice(0, 180);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function cloneJsonCompatible(value) {
|
|
62
|
+
if (value === undefined || value === null || value === "") {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
return normalizeString(value) || null;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(JSON.stringify(value));
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
function extractJsonPayload(rawText) {
|
|
62
76
|
const text = String(rawText || "").trim();
|
|
63
77
|
if (!text) {
|
|
@@ -80,7 +94,10 @@ function extractJsonPayload(rawText) {
|
|
|
80
94
|
for (const candidate of candidates) {
|
|
81
95
|
try {
|
|
82
96
|
const parsed = JSON.parse(candidate);
|
|
83
|
-
if (
|
|
97
|
+
if (Array.isArray(parsed)) {
|
|
98
|
+
return { findings: parsed };
|
|
99
|
+
}
|
|
100
|
+
if (parsed && typeof parsed === "object") {
|
|
84
101
|
return parsed;
|
|
85
102
|
}
|
|
86
103
|
} catch {
|
|
@@ -118,14 +135,31 @@ function normalizeConfidence(value) {
|
|
|
118
135
|
return Math.max(0, Math.min(1, normalized));
|
|
119
136
|
}
|
|
120
137
|
|
|
138
|
+
function normalizeTrafficLight(value) {
|
|
139
|
+
const normalized = normalizeString(value).toLowerCase();
|
|
140
|
+
if (["green", "yellow", "red"].includes(normalized)) {
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
|
|
121
146
|
function normalizeAiFinding(rawFinding, index) {
|
|
122
147
|
if (!rawFinding || typeof rawFinding !== "object" || Array.isArray(rawFinding)) {
|
|
123
148
|
return null;
|
|
124
149
|
}
|
|
125
150
|
|
|
126
151
|
const message = normalizeString(rawFinding.title || rawFinding.message);
|
|
127
|
-
const
|
|
128
|
-
const
|
|
152
|
+
const evidence = normalizeString(rawFinding.evidence || rawFinding.excerpt);
|
|
153
|
+
const rootCause = normalizeString(rawFinding.rootCause || rawFinding.root_cause || rawFinding.rationale);
|
|
154
|
+
const recommendedFix = normalizeString(
|
|
155
|
+
rawFinding.recommendedFix || rawFinding.recommended_fix || rawFinding.suggestedFix
|
|
156
|
+
);
|
|
157
|
+
const rationale = normalizeString(rawFinding.rationale || rootCause || evidence || rawFinding.excerpt);
|
|
158
|
+
const suggestedFix = normalizeString(rawFinding.suggestedFix || recommendedFix);
|
|
159
|
+
const lensEvidence = cloneJsonCompatible(rawFinding.lensEvidence || rawFinding.lens_evidence);
|
|
160
|
+
const reproduction = cloneJsonCompatible(rawFinding.reproduction);
|
|
161
|
+
const userImpact = normalizeString(rawFinding.userImpact || rawFinding.user_impact);
|
|
162
|
+
const trafficLight = normalizeTrafficLight(rawFinding.trafficLight || rawFinding.traffic_light);
|
|
129
163
|
|
|
130
164
|
return {
|
|
131
165
|
severity: normalizeSeverity(rawFinding.severity),
|
|
@@ -134,6 +168,13 @@ function normalizeAiFinding(rawFinding, index) {
|
|
|
134
168
|
message: message || `AI finding ${index + 1}`,
|
|
135
169
|
rationale: rationale || "AI reviewer flagged a potential issue requiring validation.",
|
|
136
170
|
suggestedFix: suggestedFix || "Review and remediate this finding.",
|
|
171
|
+
evidence,
|
|
172
|
+
lensEvidence,
|
|
173
|
+
reproduction,
|
|
174
|
+
userImpact,
|
|
175
|
+
trafficLight,
|
|
176
|
+
rootCause,
|
|
177
|
+
recommendedFix: recommendedFix || suggestedFix || "Review and remediate this finding.",
|
|
137
178
|
confidence: normalizeConfidence(rawFinding.confidence),
|
|
138
179
|
};
|
|
139
180
|
}
|
|
@@ -212,6 +253,7 @@ export function buildAiReviewPrompt({
|
|
|
212
253
|
deterministicFindings = [],
|
|
213
254
|
scopedFiles = [],
|
|
214
255
|
specContext = null,
|
|
256
|
+
systemPrompt = "",
|
|
215
257
|
maxFindings = DEFAULT_AI_MAX_FINDINGS,
|
|
216
258
|
} = {}) {
|
|
217
259
|
const normalizedSummary = deterministicSummary || { P0: 0, P1: 0, P2: 0, P3: 0 };
|
|
@@ -226,7 +268,7 @@ export function buildAiReviewPrompt({
|
|
|
226
268
|
const specAcceptanceCriteriaCount = Number(specContext?.acceptanceCriteriaCount || 0);
|
|
227
269
|
const specPreview = Array.isArray(specContext?.endpointsPreview) ? specContext.endpointsPreview : [];
|
|
228
270
|
|
|
229
|
-
|
|
271
|
+
const basePrompt = [
|
|
230
272
|
"You are Sentinelayer Omar reviewer layer 9.3.",
|
|
231
273
|
"Review the deterministic findings and scoped files. Add ONLY materially new findings.",
|
|
232
274
|
"Do not repeat deterministic findings unless you add new exploitability rationale.",
|
|
@@ -241,6 +283,13 @@ export function buildAiReviewPrompt({
|
|
|
241
283
|
' "file": "relative/path",',
|
|
242
284
|
' "line": 1,',
|
|
243
285
|
' "title": "finding title",',
|
|
286
|
+
' "evidence": "concrete code excerpt or static trace evidence",',
|
|
287
|
+
' "lensEvidence": {"A": "passed|failed|not_applicable: short evidence"},',
|
|
288
|
+
' "reproduction": {"type": "static_trace|manual_step|shell|runtime_probe", "steps": ["step 1"]},',
|
|
289
|
+
' "user_impact": "operator/user/system impact",',
|
|
290
|
+
' "trafficLight": "green|yellow|red",',
|
|
291
|
+
' "rootCause": "why this exists",',
|
|
292
|
+
' "recommendedFix": "specific remediation",',
|
|
244
293
|
' "rationale": "why this matters",',
|
|
245
294
|
' "suggestedFix": "specific remediation",',
|
|
246
295
|
' "confidence": 0.0',
|
|
@@ -265,6 +314,18 @@ export function buildAiReviewPrompt({
|
|
|
265
314
|
"Deterministic findings:",
|
|
266
315
|
findingLines || "- none",
|
|
267
316
|
].join("\n");
|
|
317
|
+
|
|
318
|
+
const promptPrelude = normalizeString(systemPrompt);
|
|
319
|
+
if (!promptPrelude) {
|
|
320
|
+
return basePrompt;
|
|
321
|
+
}
|
|
322
|
+
return [
|
|
323
|
+
promptPrelude,
|
|
324
|
+
"",
|
|
325
|
+
"---",
|
|
326
|
+
"",
|
|
327
|
+
basePrompt,
|
|
328
|
+
].join("\n");
|
|
268
329
|
}
|
|
269
330
|
|
|
270
331
|
function maybeEstimateModelCost({ modelId, inputTokens, outputTokens }) {
|
|
@@ -305,6 +366,9 @@ function composeAiReviewMarkdown({
|
|
|
305
366
|
(finding, index) =>
|
|
306
367
|
`${index + 1}. [${finding.severity}] ${finding.file}:${finding.line} ${finding.message}\n` +
|
|
307
368
|
` rationale: ${finding.rationale}\n` +
|
|
369
|
+
(finding.evidence ? ` evidence: ${finding.evidence}\n` : "") +
|
|
370
|
+
(finding.userImpact ? ` user_impact: ${finding.userImpact}\n` : "") +
|
|
371
|
+
(finding.trafficLight ? ` traffic_light: ${finding.trafficLight}\n` : "") +
|
|
308
372
|
` suggested_fix: ${finding.suggestedFix}` +
|
|
309
373
|
(finding.confidence === null ? "" : `\n confidence: ${finding.confidence.toFixed(2)}`)
|
|
310
374
|
)
|
|
@@ -335,16 +399,25 @@ function composeAiReviewMarkdown({
|
|
|
335
399
|
}
|
|
336
400
|
|
|
337
401
|
function toReviewFinding(aiFinding, index) {
|
|
402
|
+
const suggestedFix = aiFinding.suggestedFix || aiFinding.recommendedFix;
|
|
338
403
|
return {
|
|
339
404
|
severity: aiFinding.severity,
|
|
340
405
|
file: aiFinding.file,
|
|
341
406
|
line: aiFinding.line,
|
|
342
407
|
message: aiFinding.message,
|
|
343
|
-
excerpt: sanitizeExcerpt(aiFinding.rationale),
|
|
408
|
+
excerpt: sanitizeExcerpt(aiFinding.evidence || aiFinding.rationale),
|
|
344
409
|
ruleId: `SL-AI-${String(index + 1).padStart(3, "0")}`,
|
|
345
|
-
suggestedFix
|
|
410
|
+
suggestedFix,
|
|
346
411
|
layer: "ai_reasoning",
|
|
347
412
|
confidence: aiFinding.confidence,
|
|
413
|
+
evidence: aiFinding.evidence,
|
|
414
|
+
lensEvidence: aiFinding.lensEvidence,
|
|
415
|
+
reproduction: aiFinding.reproduction,
|
|
416
|
+
userImpact: aiFinding.userImpact,
|
|
417
|
+
trafficLight: aiFinding.trafficLight,
|
|
418
|
+
rootCause: aiFinding.rootCause,
|
|
419
|
+
recommendedFix: aiFinding.recommendedFix || suggestedFix,
|
|
420
|
+
rationale: aiFinding.rationale,
|
|
348
421
|
};
|
|
349
422
|
}
|
|
350
423
|
|
|
@@ -357,6 +430,20 @@ function buildDryRunResponse({ deterministicSummary, maxFindings } = {}) {
|
|
|
357
430
|
file: "src/example.js",
|
|
358
431
|
line: 1 + index,
|
|
359
432
|
title: `DRY_RUN finding ${index + 1}`,
|
|
433
|
+
evidence: "const unsafe = exampleInput;",
|
|
434
|
+
lensEvidence: {
|
|
435
|
+
A: "not_applicable: no route/runtime boundary in dry-run fixture",
|
|
436
|
+
J: "failed: synthetic path needs targeted verification before merge",
|
|
437
|
+
K: "passed: no AI tool permission escalation in dry-run fixture",
|
|
438
|
+
},
|
|
439
|
+
reproduction: {
|
|
440
|
+
type: "static_trace",
|
|
441
|
+
steps: ["Inspect src/example.js", "Trace exampleInput into the synthetic finding path"],
|
|
442
|
+
},
|
|
443
|
+
user_impact: "Operator sees a synthetic risk used to validate OmarGate evidence plumbing.",
|
|
444
|
+
trafficLight: index === 0 ? "yellow" : "green",
|
|
445
|
+
rootCause: "DRY_RUN synthetic root cause for evidence-contract validation.",
|
|
446
|
+
recommendedFix: "Validate this path with targeted remediation.",
|
|
360
447
|
rationale: `Synthetic AI rationale with deterministic context P1=${deterministicSummary.P1}.`,
|
|
361
448
|
suggestedFix: "Validate this path with targeted remediation.",
|
|
362
449
|
confidence: index === 0 ? 0.72 : 0.54,
|
|
@@ -393,6 +480,7 @@ export async function runAiReviewLayer({
|
|
|
393
480
|
maxToolCalls = 0,
|
|
394
481
|
maxNoProgress = 3,
|
|
395
482
|
warningThresholdPercent = 80,
|
|
483
|
+
systemPrompt = "",
|
|
396
484
|
dryRun = false,
|
|
397
485
|
env = process.env,
|
|
398
486
|
} = {}) {
|
|
@@ -436,6 +524,7 @@ export async function runAiReviewLayer({
|
|
|
436
524
|
deterministicFindings: deterministic?.findings || [],
|
|
437
525
|
scopedFiles: deterministic?.scope?.scannedRelativeFiles || [],
|
|
438
526
|
specContext: deterministic?.layers?.specBinding || null,
|
|
527
|
+
systemPrompt,
|
|
439
528
|
maxFindings: normalizedMaxFindings,
|
|
440
529
|
});
|
|
441
530
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
4
|
+
import { requestJson } from "../auth/http.js";
|
|
5
|
+
|
|
6
|
+
export const DD_REPORT_EMAIL_TIMEOUT_MS = 10_000;
|
|
7
|
+
|
|
8
|
+
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
9
|
+
|
|
10
|
+
function normalizeString(value) {
|
|
11
|
+
return String(value || "").trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function normalizeReportEmail(value) {
|
|
15
|
+
const normalized = normalizeString(value);
|
|
16
|
+
if (!EMAIL_RE.test(normalized)) {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildReportEmailIdempotencyKey({ runId, to }) {
|
|
23
|
+
const digest = crypto
|
|
24
|
+
.createHash("sha256")
|
|
25
|
+
.update(`${normalizeString(runId)}\0${normalizeString(to).toLowerCase()}`)
|
|
26
|
+
.digest("hex")
|
|
27
|
+
.slice(0, 32);
|
|
28
|
+
return `sl-cli-dd-email-${digest}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function redactDdEmailError(value) {
|
|
32
|
+
return normalizeString(value)
|
|
33
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]")
|
|
34
|
+
.replace(/[A-Za-z]:[\\/][^\s"'<>]+/g, "[LOCAL_PATH]")
|
|
35
|
+
.replace(/api[_-]?key\s*=\s*[^&\s]+/gi, "api_key=[REDACTED]")
|
|
36
|
+
.slice(0, 500);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeTimeoutMs(env = process.env) {
|
|
40
|
+
const parsed = Number(env.SENTINELAYER_DD_EMAIL_TIMEOUT_MS);
|
|
41
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
42
|
+
return Math.max(100, Math.floor(parsed));
|
|
43
|
+
}
|
|
44
|
+
return DD_REPORT_EMAIL_TIMEOUT_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function errorResult({ runId, to, code, message, status = 0, requestId = null }) {
|
|
48
|
+
return {
|
|
49
|
+
queued: false,
|
|
50
|
+
sent: false,
|
|
51
|
+
runId: normalizeString(runId),
|
|
52
|
+
to: normalizeString(to),
|
|
53
|
+
code: normalizeString(code) || "DD_EMAIL_FAILED",
|
|
54
|
+
error: redactDdEmailError(message) || "DD report email request failed.",
|
|
55
|
+
status: Number(status || 0),
|
|
56
|
+
requestId: requestId ? String(requestId) : null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Trigger the API-side investor-DD report email endpoint for a completed run.
|
|
62
|
+
*
|
|
63
|
+
* The caller owns run completion and event emission. This helper only handles
|
|
64
|
+
* auth resolution, bounded network behavior, idempotency, and redacted errors.
|
|
65
|
+
*/
|
|
66
|
+
export async function sendDdReportEmail({
|
|
67
|
+
runId,
|
|
68
|
+
to,
|
|
69
|
+
cwd = process.cwd(),
|
|
70
|
+
env = process.env,
|
|
71
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
72
|
+
requestJsonImpl = requestJson,
|
|
73
|
+
timeoutMs = normalizeTimeoutMs(env),
|
|
74
|
+
} = {}) {
|
|
75
|
+
const normalizedRunId = normalizeString(runId);
|
|
76
|
+
const normalizedTo = normalizeReportEmail(to);
|
|
77
|
+
if (!normalizedRunId) {
|
|
78
|
+
return errorResult({ runId, to, code: "DD_EMAIL_RUN_ID_REQUIRED", message: "runId is required." });
|
|
79
|
+
}
|
|
80
|
+
if (!normalizedTo) {
|
|
81
|
+
return errorResult({ runId, to, code: "DD_EMAIL_INVALID_RECIPIENT", message: "Invalid report email recipient." });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let session = null;
|
|
85
|
+
try {
|
|
86
|
+
session = await resolveAuthSession({
|
|
87
|
+
cwd,
|
|
88
|
+
env,
|
|
89
|
+
autoRotate: false,
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return errorResult({
|
|
93
|
+
runId: normalizedRunId,
|
|
94
|
+
to: normalizedTo,
|
|
95
|
+
code: "DD_EMAIL_AUTH_UNAVAILABLE",
|
|
96
|
+
message: err instanceof Error ? err.message : String(err),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!session || !session.token) {
|
|
101
|
+
return errorResult({
|
|
102
|
+
runId: normalizedRunId,
|
|
103
|
+
to: normalizedTo,
|
|
104
|
+
code: "DD_EMAIL_AUTH_REQUIRED",
|
|
105
|
+
message: "Authenticate before sending DD report email.",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const apiUrl = normalizeString(session.apiUrl) || "https://api.sentinelayer.com";
|
|
110
|
+
const endpoint = `${apiUrl.replace(/\/+$/, "")}/api/v1/runs/${encodeURIComponent(
|
|
111
|
+
normalizedRunId,
|
|
112
|
+
)}/send-report-email`;
|
|
113
|
+
const idempotencyKey = buildReportEmailIdempotencyKey({
|
|
114
|
+
runId: normalizedRunId,
|
|
115
|
+
to: normalizedTo,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const response = await requestJsonImpl(endpoint, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: {
|
|
122
|
+
Authorization: `Bearer ${session.token}`,
|
|
123
|
+
},
|
|
124
|
+
idempotencyKey,
|
|
125
|
+
body: { to: normalizedTo },
|
|
126
|
+
timeoutMs,
|
|
127
|
+
maxRetries: 1,
|
|
128
|
+
});
|
|
129
|
+
return {
|
|
130
|
+
queued: true,
|
|
131
|
+
sent: Boolean(response?.sent ?? true),
|
|
132
|
+
runId: normalizeString(response?.run_id) || normalizedRunId,
|
|
133
|
+
to: normalizeString(response?.to) || normalizedTo,
|
|
134
|
+
messageId: normalizeString(response?.message_id),
|
|
135
|
+
replay: Boolean(response?.replay),
|
|
136
|
+
idempotencyKey,
|
|
137
|
+
};
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return errorResult({
|
|
140
|
+
runId: normalizedRunId,
|
|
141
|
+
to: normalizedTo,
|
|
142
|
+
code: err?.code || "DD_EMAIL_REQUEST_FAILED",
|
|
143
|
+
message: err instanceof Error ? err.message : String(err),
|
|
144
|
+
status: err?.status || 0,
|
|
145
|
+
requestId: err?.requestId || null,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|