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.
Files changed (38) hide show
  1. package/package.json +10 -5
  2. package/src/agents/devtestbot/config/definition.js +100 -0
  3. package/src/agents/devtestbot/config/system-prompt.js +92 -0
  4. package/src/agents/devtestbot/index.js +9 -0
  5. package/src/agents/devtestbot/runner.js +769 -0
  6. package/src/agents/devtestbot/tool.js +707 -0
  7. package/src/agents/jules/stream.js +2 -12
  8. package/src/audit/orchestrator.js +471 -114
  9. package/src/audit/persona-loop.js +1342 -0
  10. package/src/audit/registry.js +58 -2
  11. package/src/commands/audit.js +42 -1
  12. package/src/commands/legacy-args.js +32 -1
  13. package/src/commands/omargate.js +4 -0
  14. package/src/commands/session.js +417 -89
  15. package/src/commands/swarm.js +11 -2
  16. package/src/cost/history.js +41 -21
  17. package/src/events/schema.js +27 -1
  18. package/src/guide/generator.js +14 -0
  19. package/src/legacy-cli.js +110 -18
  20. package/src/prompt/generator.js +4 -16
  21. package/src/review/ai-review.js +95 -6
  22. package/src/review/dd-report-email-client.js +148 -0
  23. package/src/review/investor-dd-devtestbot.js +599 -0
  24. package/src/review/investor-dd-orchestrator.js +135 -3
  25. package/src/review/omargate-cache.js +285 -0
  26. package/src/review/omargate-orchestrator.js +605 -4
  27. package/src/review/persona-prompts.js +34 -1
  28. package/src/review/report.js +189 -4
  29. package/src/session/coordination-guidance.js +48 -0
  30. package/src/session/daemon.js +3 -2
  31. package/src/session/listener.js +236 -0
  32. package/src/session/senti-naming.js +36 -0
  33. package/src/session/setup-guides.js +3 -15
  34. package/src/session/store.js +54 -5
  35. package/src/session/sync.js +23 -0
  36. package/src/spec/generator.js +8 -10
  37. package/src/swarm/registry.js +20 -0
  38. package/src/swarm/runtime.js +139 -1
@@ -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 (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
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 rationale = normalizeString(rawFinding.rationale || rawFinding.excerpt);
128
- const suggestedFix = normalizeString(rawFinding.suggestedFix);
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
- return [
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: aiFinding.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
+ }