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
@@ -37,6 +37,8 @@ import {
37
37
  import { notifyRunCompleted } from "./investor-dd-notification.js";
38
38
  import { attachReproducibilityChain } from "./reproducibility-chain.js";
39
39
  import { renderInvestorDdHtml } from "./investor-dd-html-report.js";
40
+ import { runDevTestBotPhase } from "./investor-dd-devtestbot.js";
41
+ import { redactDdEmailError } from "./dd-report-email-client.js";
40
42
 
41
43
  const INVESTOR_DD_PERSONAS = Object.freeze([
42
44
  "security",
@@ -164,10 +166,95 @@ function buildSummaryMarkdown({ runId, summary, routing, byPersona }) {
164
166
  lines.push(`- **${sev}**: ${count}`);
165
167
  }
166
168
  lines.push("");
169
+ if (summary.devTestBot) {
170
+ lines.push("## devTestBot");
171
+ lines.push("");
172
+ lines.push(`- Skipped: ${summary.devTestBot.skipped ? "yes" : "no"}`);
173
+ lines.push(`- Subagents: ${summary.devTestBot.swarmCount || 0}`);
174
+ lines.push(`- Identities: ${summary.devTestBot.identityCount || 0}`);
175
+ lines.push(`- Findings: ${summary.devTestBot.findingCount || 0}`);
176
+ lines.push(`- Artifacts: ${summary.devTestBot.artifactRoot || "n/a"}`);
177
+ lines.push("");
178
+ }
167
179
  lines.push(`Total: ${allFindings.length}`);
168
180
  return lines.join("\n");
169
181
  }
170
182
 
183
+ async function triggerReportEmail({ reportEmail, runResult, dryRun, emit }) {
184
+ const to = String(reportEmail?.to || "").trim();
185
+ if (!to) return null;
186
+
187
+ if (dryRun && reportEmail.skipWhenDryRun !== false) {
188
+ const result = { queued: false, skipped: true, runId: runResult.runId, to, code: "DD_EMAIL_DRY_RUN" };
189
+ emit({
190
+ type: "dd_email_skipped",
191
+ event: "dd_email_skipped",
192
+ runId: runResult.runId,
193
+ to,
194
+ reason: "dry_run",
195
+ });
196
+ return result;
197
+ }
198
+
199
+ const client = reportEmail.client;
200
+ if (!client || typeof client.send !== "function") {
201
+ const result = { queued: false, runId: runResult.runId, to, code: "DD_EMAIL_CLIENT_MISSING" };
202
+ emit({
203
+ type: "dd_email_error",
204
+ event: "dd_email_error",
205
+ runId: runResult.runId,
206
+ to,
207
+ code: result.code,
208
+ error: "DD report email client is not configured.",
209
+ });
210
+ return result;
211
+ }
212
+
213
+ try {
214
+ const result = await client.send({ runId: runResult.runId, to, run: runResult });
215
+ if (result?.queued) {
216
+ emit({
217
+ type: "dd_email_queued",
218
+ event: "dd_email_queued",
219
+ runId: String(result.runId || runResult.runId),
220
+ to: String(result.to || to),
221
+ messageId: result.messageId || "",
222
+ replay: Boolean(result.replay),
223
+ sent: result.sent !== false,
224
+ });
225
+ return result;
226
+ }
227
+
228
+ emit({
229
+ type: "dd_email_error",
230
+ event: "dd_email_error",
231
+ runId: runResult.runId,
232
+ to,
233
+ code: String(result?.code || "DD_EMAIL_FAILED"),
234
+ status: Number(result?.status || 0),
235
+ error: redactDdEmailError(result?.error || "DD report email request failed."),
236
+ });
237
+ return result || { queued: false, runId: runResult.runId, to };
238
+ } catch (err) {
239
+ const result = {
240
+ queued: false,
241
+ runId: runResult.runId,
242
+ to,
243
+ code: "DD_EMAIL_EXCEPTION",
244
+ error: redactDdEmailError(err instanceof Error ? err.message : String(err)),
245
+ };
246
+ emit({
247
+ type: "dd_email_error",
248
+ event: "dd_email_error",
249
+ runId: runResult.runId,
250
+ to,
251
+ code: result.code,
252
+ error: result.error,
253
+ });
254
+ return result;
255
+ }
256
+ }
257
+
171
258
  /**
172
259
  * Run the investor-DD orchestration end to end.
173
260
  *
@@ -183,6 +270,10 @@ function buildSummaryMarkdown({ runId, summary, routing, byPersona }) {
183
270
  * @param {object} [params.liveValidator.devTestBot] - DevTestBot client.
184
271
  * @param {object} [params.liveValidator.aidenid] - AIdenID client.
185
272
  * @param {number} [params.liveValidator.maxInteractions]
273
+ * @param {object|false} [params.devTestBot] - Automated devTestBot phase config.
274
+ * @param {object|null} [params.reportEmail] - Optional API-side report email trigger.
275
+ * @param {string} [params.reportEmail.to]
276
+ * @param {object} [params.reportEmail.client] - { send({ runId, to, run }) }.
186
277
  * @param {object} [params.notification] - Optional notification config.
187
278
  * @param {string} [params.notification.notifyEmail]
188
279
  * @param {object} [params.notification.emailClient]
@@ -198,6 +289,8 @@ export async function runInvestorDd({
198
289
  dryRun = false,
199
290
  compliancePacks = COMPLIANCE_PACK_CATALOG,
200
291
  liveValidator = null,
292
+ devTestBot = {},
293
+ reportEmail = null,
201
294
  notification = null,
202
295
  } = {}) {
203
296
  if (!rootPath) throw new TypeError("runInvestorDd requires rootPath");
@@ -207,6 +300,10 @@ export async function runInvestorDd({
207
300
  const artifactBase = outputDir
208
301
  ? path.resolve(outputDir, runId, INVESTOR_DD_ARTIFACT_SUBDIR)
209
302
  : path.resolve(rootPath, ".sentinelayer", "runs", runId, INVESTOR_DD_ARTIFACT_SUBDIR);
303
+ const runRoot = path.dirname(artifactBase);
304
+ const outputRoot = outputDir
305
+ ? path.resolve(outputDir)
306
+ : path.resolve(rootPath, ".sentinelayer");
210
307
  await fsp.mkdir(artifactBase, { recursive: true });
211
308
 
212
309
  const streamPath = path.join(artifactBase, "stream.ndjson");
@@ -244,9 +341,11 @@ export async function runInvestorDd({
244
341
  let terminationReason = "ok";
245
342
  let reconciliationAvailable = false;
246
343
  let compliance = null;
344
+ let devTestBotPhase = null;
345
+ let budgetState = null;
247
346
 
248
347
  if (!dryRun) {
249
- const budgetState = createBudgetState({
348
+ budgetState = createBudgetState({
250
349
  maxUsd: resolvedBudget.maxCostUsd,
251
350
  maxRuntimeMs: resolvedBudget.maxRuntimeMinutes * 60_000,
252
351
  });
@@ -274,6 +373,20 @@ export async function runInvestorDd({
274
373
  totalGaps: compliance.totalGaps,
275
374
  });
276
375
 
376
+ devTestBotPhase = await runDevTestBotPhase({
377
+ runId,
378
+ rootPath,
379
+ outputRoot,
380
+ runRoot,
381
+ artifactDir: artifactBase,
382
+ files,
383
+ findings,
384
+ budget: budgetState,
385
+ options: devTestBot === false ? { enabled: false } : devTestBot || {},
386
+ onEvent: emit,
387
+ });
388
+ findings.push(...(devTestBotPhase.findings || []));
389
+
277
390
  // Live-web validation (Jules): optional; only runs when both
278
391
  // devTestBot + aidenid clients are supplied (pluggable contracts).
279
392
  if (
@@ -346,6 +459,16 @@ export async function runInvestorDd({
346
459
  ? { totalCovered: compliance.totalCovered, totalGaps: compliance.totalGaps }
347
460
  : null,
348
461
  reconciliation: reconciliationAvailable,
462
+ devTestBot: devTestBotPhase
463
+ ? {
464
+ skipped: Boolean(devTestBotPhase.skipped),
465
+ reason: devTestBotPhase.reason || "",
466
+ identityCount: devTestBotPhase.plan?.identityCount || devTestBotPhase.identities?.length || 0,
467
+ swarmCount: devTestBotPhase.plan?.swarmCount || devTestBotPhase.subagents?.length || 0,
468
+ findingCount: devTestBotPhase.findingCount || 0,
469
+ artifactRoot: devTestBotPhase.artifactRoot || "",
470
+ }
471
+ : null,
349
472
  };
350
473
  await writeJson(path.join(artifactBase, "summary.json"), summary);
351
474
 
@@ -369,6 +492,17 @@ export async function runInvestorDd({
369
492
  durationSeconds,
370
493
  terminationReason,
371
494
  });
495
+
496
+ const runResult = { runId, artifactDir: artifactBase, summary, findings, devTestBot: devTestBotPhase };
497
+ if (reportEmail) {
498
+ runResult.reportEmail = await triggerReportEmail({
499
+ reportEmail,
500
+ runResult,
501
+ dryRun,
502
+ emit,
503
+ });
504
+ }
505
+
372
506
  await streamHandle.close();
373
507
 
374
508
  const artifactFiles = await fsp.readdir(artifactBase);
@@ -385,8 +519,6 @@ export async function runInvestorDd({
385
519
  }
386
520
  await writeJson(path.join(artifactBase, "manifest.json"), manifest);
387
521
 
388
- const runResult = { runId, artifactDir: artifactBase, summary, findings };
389
-
390
522
  // Fire-and-forget notification dispatch (email + dashboard). Failures
391
523
  // are non-fatal — the report is already persisted to disk + manifest.
392
524
  if (notification && (notification.emailClient || notification.dashboardClient)) {
@@ -0,0 +1,285 @@
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { resolveOutputRoot } from "../config/service.js";
5
+
6
+ const CACHE_SCHEMA_VERSION = "1.0.0";
7
+ const CACHE_KIND = "omargate-deterministic-cache";
8
+ const LATEST_INDEX_NAME = "latest-omargate.json";
9
+
10
+ function normalizeString(value) {
11
+ return String(value || "").trim();
12
+ }
13
+
14
+ function normalizeTargetPath(value) {
15
+ return path.resolve(String(value || "."));
16
+ }
17
+
18
+ function normalizeSummary(value = {}) {
19
+ const summary = value && typeof value === "object" ? value : {};
20
+ const P0 = Math.max(0, Math.floor(Number(summary.P0 || 0)));
21
+ const P1 = Math.max(0, Math.floor(Number(summary.P1 || 0)));
22
+ const P2 = Math.max(0, Math.floor(Number(summary.P2 || 0)));
23
+ const P3 = Math.max(0, Math.floor(Number(summary.P3 || 0)));
24
+ return {
25
+ P0,
26
+ P1,
27
+ P2,
28
+ P3,
29
+ blocking: summary.blocking === undefined ? P0 > 0 || P1 > 0 : Boolean(summary.blocking),
30
+ };
31
+ }
32
+
33
+ function sanitizeRunId(value) {
34
+ const normalized = normalizeString(value);
35
+ if (!normalized) {
36
+ return "";
37
+ }
38
+ return normalized.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^-+|-+$/g, "");
39
+ }
40
+
41
+ function isSafeRequestedRunId(requested, sanitized) {
42
+ return Boolean(requested) && requested === sanitized && sanitized !== "." && sanitized !== "..";
43
+ }
44
+
45
+ function deterministicCachePath(outputRoot, runId) {
46
+ return path.join(outputRoot, "runs", runId, "deterministic.json");
47
+ }
48
+
49
+ async function readJsonFile(filePath) {
50
+ const content = await fsp.readFile(filePath, "utf-8");
51
+ return JSON.parse(content);
52
+ }
53
+
54
+ function isCacheForTarget(cache, normalizedTargetPath) {
55
+ const cacheTarget = normalizeString(cache?.targetPath);
56
+ if (!cacheTarget) {
57
+ return false;
58
+ }
59
+ return path.resolve(cacheTarget) === normalizedTargetPath;
60
+ }
61
+
62
+ function buildMissingResult({ outputRoot, requested = "latest", reason = "not_found" } = {}) {
63
+ return {
64
+ found: false,
65
+ requested,
66
+ reason,
67
+ outputRoot,
68
+ };
69
+ }
70
+
71
+ async function loadCacheFile({ filePath, outputRoot, requested, normalizedTargetPath }) {
72
+ let cache;
73
+ try {
74
+ cache = await readJsonFile(filePath);
75
+ } catch {
76
+ return buildMissingResult({
77
+ outputRoot,
78
+ requested,
79
+ reason: "malformed_or_missing_cache",
80
+ });
81
+ }
82
+
83
+ if (cache?.kind !== CACHE_KIND) {
84
+ return buildMissingResult({
85
+ outputRoot,
86
+ requested,
87
+ reason: "invalid_cache_kind",
88
+ });
89
+ }
90
+ if (!isCacheForTarget(cache, normalizedTargetPath)) {
91
+ return buildMissingResult({
92
+ outputRoot,
93
+ requested,
94
+ reason: "target_mismatch",
95
+ });
96
+ }
97
+
98
+ return {
99
+ found: true,
100
+ requested,
101
+ runId: normalizeString(cache.runId),
102
+ artifactPath: filePath,
103
+ outputRoot,
104
+ cache,
105
+ };
106
+ }
107
+
108
+ async function loadLatestFromIndex({ outputRoot, normalizedTargetPath }) {
109
+ const latestPath = path.join(outputRoot, "runs", LATEST_INDEX_NAME);
110
+ let index;
111
+ try {
112
+ index = await readJsonFile(latestPath);
113
+ } catch {
114
+ return null;
115
+ }
116
+
117
+ const runId = sanitizeRunId(index?.runId);
118
+ if (!runId || !isCacheForTarget(index, normalizedTargetPath)) {
119
+ return null;
120
+ }
121
+ const artifactPath = normalizeString(index.artifactPath) || deterministicCachePath(outputRoot, runId);
122
+ const loaded = await loadCacheFile({
123
+ filePath: artifactPath,
124
+ outputRoot,
125
+ requested: "latest",
126
+ normalizedTargetPath,
127
+ });
128
+ return loaded.found ? loaded : null;
129
+ }
130
+
131
+ async function loadLatestByScanning({ outputRoot, normalizedTargetPath }) {
132
+ const runsDir = path.join(outputRoot, "runs");
133
+ let entries = [];
134
+ try {
135
+ entries = await fsp.readdir(runsDir, { withFileTypes: true });
136
+ } catch {
137
+ return null;
138
+ }
139
+
140
+ const candidates = [];
141
+ for (const entry of entries) {
142
+ if (!entry.isDirectory()) {
143
+ continue;
144
+ }
145
+ const runId = sanitizeRunId(entry.name);
146
+ if (!isSafeRequestedRunId(entry.name, runId)) {
147
+ continue;
148
+ }
149
+ const artifactPath = deterministicCachePath(outputRoot, runId);
150
+ try {
151
+ const stat = await fsp.stat(artifactPath);
152
+ candidates.push({ runId, artifactPath, mtimeMs: Number(stat.mtimeMs || 0) });
153
+ } catch {
154
+ // Ignore incomplete run directories.
155
+ }
156
+ }
157
+
158
+ candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
159
+ for (const candidate of candidates) {
160
+ const loaded = await loadCacheFile({
161
+ filePath: candidate.artifactPath,
162
+ outputRoot,
163
+ requested: "latest",
164
+ normalizedTargetPath,
165
+ });
166
+ if (loaded.found) {
167
+ return loaded;
168
+ }
169
+ }
170
+ return null;
171
+ }
172
+
173
+ export async function writeOmarGateDeterministicCache({
174
+ targetPath,
175
+ outputDir = "",
176
+ runId,
177
+ deterministic = {},
178
+ reportPath = "",
179
+ } = {}) {
180
+ const normalizedTargetPath = normalizeTargetPath(targetPath);
181
+ const outputRoot = await resolveOutputRoot({
182
+ cwd: normalizedTargetPath,
183
+ outputDirOverride: outputDir,
184
+ env: process.env,
185
+ });
186
+ const normalizedRunId = sanitizeRunId(runId || deterministic?.runId);
187
+ if (!normalizedRunId) {
188
+ throw new Error("OmarGate deterministic cache requires a runId.");
189
+ }
190
+
191
+ const runDirectory = path.join(outputRoot, "runs", normalizedRunId);
192
+ const artifactPath = path.join(runDirectory, "deterministic.json");
193
+ const latestPath = path.join(outputRoot, "runs", LATEST_INDEX_NAME);
194
+ await fsp.mkdir(runDirectory, { recursive: true });
195
+
196
+ const cache = {
197
+ schemaVersion: CACHE_SCHEMA_VERSION,
198
+ kind: CACHE_KIND,
199
+ runId: normalizedRunId,
200
+ targetPath: normalizedTargetPath,
201
+ generatedAt: new Date().toISOString(),
202
+ deterministicRunId: normalizeString(deterministic?.runId),
203
+ mode: normalizeString(deterministic?.mode) || "full",
204
+ summary: normalizeSummary(deterministic?.summary),
205
+ findings: Array.isArray(deterministic?.findings) ? deterministic.findings : [],
206
+ scope: deterministic?.scope && typeof deterministic.scope === "object" ? deterministic.scope : {},
207
+ layers: deterministic?.layers && typeof deterministic.layers === "object" ? deterministic.layers : {},
208
+ metadata: deterministic?.metadata && typeof deterministic.metadata === "object" ? deterministic.metadata : {},
209
+ artifacts: deterministic?.artifacts && typeof deterministic.artifacts === "object" ? deterministic.artifacts : {},
210
+ source: {
211
+ command: "/omargate deep",
212
+ reportPath: normalizeString(reportPath),
213
+ },
214
+ };
215
+ await fsp.writeFile(artifactPath, `${JSON.stringify(cache, null, 2)}\n`, "utf-8");
216
+ await fsp.writeFile(
217
+ latestPath,
218
+ `${JSON.stringify(
219
+ {
220
+ schemaVersion: CACHE_SCHEMA_VERSION,
221
+ kind: "omargate-latest-index",
222
+ runId: normalizedRunId,
223
+ targetPath: normalizedTargetPath,
224
+ artifactPath,
225
+ updatedAt: cache.generatedAt,
226
+ },
227
+ null,
228
+ 2
229
+ )}\n`,
230
+ "utf-8"
231
+ );
232
+
233
+ return {
234
+ runId: normalizedRunId,
235
+ outputRoot,
236
+ runDirectory,
237
+ artifactPath,
238
+ latestPath,
239
+ cache,
240
+ };
241
+ }
242
+
243
+ export async function loadOmarGateDeterministicCache({
244
+ targetPath,
245
+ outputDir = "",
246
+ runIdOrLatest = "latest",
247
+ } = {}) {
248
+ const normalizedTargetPath = normalizeTargetPath(targetPath);
249
+ const outputRoot = await resolveOutputRoot({
250
+ cwd: normalizedTargetPath,
251
+ outputDirOverride: outputDir,
252
+ env: process.env,
253
+ });
254
+ const requested = normalizeString(runIdOrLatest) || "latest";
255
+
256
+ if (requested.toLowerCase() === "latest") {
257
+ const latestFromIndex = await loadLatestFromIndex({
258
+ outputRoot,
259
+ normalizedTargetPath,
260
+ });
261
+ if (latestFromIndex) {
262
+ return latestFromIndex;
263
+ }
264
+ const latestFromScan = await loadLatestByScanning({
265
+ outputRoot,
266
+ normalizedTargetPath,
267
+ });
268
+ return latestFromScan || buildMissingResult({ outputRoot, requested, reason: "not_found" });
269
+ }
270
+
271
+ const runId = sanitizeRunId(requested);
272
+ if (!isSafeRequestedRunId(requested, runId)) {
273
+ return buildMissingResult({
274
+ outputRoot,
275
+ requested,
276
+ reason: "invalid_run_id",
277
+ });
278
+ }
279
+ return loadCacheFile({
280
+ filePath: deterministicCachePath(outputRoot, runId),
281
+ outputRoot,
282
+ requested,
283
+ normalizedTargetPath,
284
+ });
285
+ }