sentinelayer-cli 0.1.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 (124) hide show
  1. package/README.md +996 -0
  2. package/bin/create-sentinelayer.js +5 -0
  3. package/bin/sentinelayer-cli.js +5 -0
  4. package/bin/sl.js +5 -0
  5. package/package.json +54 -0
  6. package/src/agents/jules/config/definition.js +209 -0
  7. package/src/agents/jules/config/system-prompt.js +175 -0
  8. package/src/agents/jules/error-intake.js +51 -0
  9. package/src/agents/jules/fix-cycle.js +377 -0
  10. package/src/agents/jules/loop.js +367 -0
  11. package/src/agents/jules/pulse.js +319 -0
  12. package/src/agents/jules/stream.js +186 -0
  13. package/src/agents/jules/swarm/file-scanner.js +74 -0
  14. package/src/agents/jules/swarm/index.js +11 -0
  15. package/src/agents/jules/swarm/orchestrator.js +362 -0
  16. package/src/agents/jules/swarm/pattern-hunter.js +123 -0
  17. package/src/agents/jules/swarm/sub-agent.js +308 -0
  18. package/src/agents/jules/tools/auth-audit.js +222 -0
  19. package/src/agents/jules/tools/dispatch.js +327 -0
  20. package/src/agents/jules/tools/file-edit.js +180 -0
  21. package/src/agents/jules/tools/file-read.js +100 -0
  22. package/src/agents/jules/tools/frontend-analyze.js +570 -0
  23. package/src/agents/jules/tools/glob.js +168 -0
  24. package/src/agents/jules/tools/grep.js +228 -0
  25. package/src/agents/jules/tools/index.js +29 -0
  26. package/src/agents/jules/tools/path-guards.js +161 -0
  27. package/src/agents/jules/tools/runtime-audit.js +409 -0
  28. package/src/agents/jules/tools/shell.js +383 -0
  29. package/src/ai/aidenid.js +945 -0
  30. package/src/ai/client.js +508 -0
  31. package/src/ai/domain-target-store.js +268 -0
  32. package/src/ai/identity-store.js +270 -0
  33. package/src/ai/site-store.js +145 -0
  34. package/src/audit/agents/architecture.js +180 -0
  35. package/src/audit/agents/compliance.js +179 -0
  36. package/src/audit/agents/documentation.js +165 -0
  37. package/src/audit/agents/performance.js +145 -0
  38. package/src/audit/agents/security.js +215 -0
  39. package/src/audit/agents/testing.js +172 -0
  40. package/src/audit/orchestrator.js +557 -0
  41. package/src/audit/package.js +204 -0
  42. package/src/audit/registry.js +284 -0
  43. package/src/audit/replay.js +103 -0
  44. package/src/auth/http.js +113 -0
  45. package/src/auth/service.js +848 -0
  46. package/src/auth/session-store.js +345 -0
  47. package/src/cli.js +244 -0
  48. package/src/commands/ai/identity-lifecycle.js +1337 -0
  49. package/src/commands/ai/provision-governance.js +1246 -0
  50. package/src/commands/ai/shared.js +147 -0
  51. package/src/commands/ai.js +11 -0
  52. package/src/commands/apply.js +19 -0
  53. package/src/commands/audit.js +1147 -0
  54. package/src/commands/auth.js +366 -0
  55. package/src/commands/chat.js +191 -0
  56. package/src/commands/config.js +184 -0
  57. package/src/commands/cost.js +311 -0
  58. package/src/commands/daemon/core.js +850 -0
  59. package/src/commands/daemon/extended.js +1048 -0
  60. package/src/commands/daemon/shared.js +213 -0
  61. package/src/commands/daemon.js +11 -0
  62. package/src/commands/guide.js +174 -0
  63. package/src/commands/ingest.js +58 -0
  64. package/src/commands/init.js +55 -0
  65. package/src/commands/legacy-args.js +30 -0
  66. package/src/commands/mcp.js +404 -0
  67. package/src/commands/omargate.js +21 -0
  68. package/src/commands/persona.js +27 -0
  69. package/src/commands/plugin.js +260 -0
  70. package/src/commands/policy.js +132 -0
  71. package/src/commands/prompt.js +238 -0
  72. package/src/commands/review.js +704 -0
  73. package/src/commands/scan.js +788 -0
  74. package/src/commands/spec.js +716 -0
  75. package/src/commands/swarm.js +651 -0
  76. package/src/commands/telemetry.js +202 -0
  77. package/src/commands/watch.js +510 -0
  78. package/src/config/agent-dictionary.js +182 -0
  79. package/src/config/io.js +56 -0
  80. package/src/config/paths.js +18 -0
  81. package/src/config/schema.js +55 -0
  82. package/src/config/service.js +184 -0
  83. package/src/cost/budget.js +235 -0
  84. package/src/cost/history.js +188 -0
  85. package/src/cost/tracker.js +171 -0
  86. package/src/daemon/artifact-lineage.js +534 -0
  87. package/src/daemon/assignment-ledger.js +770 -0
  88. package/src/daemon/ast-parser-layer.js +258 -0
  89. package/src/daemon/budget-governor.js +633 -0
  90. package/src/daemon/callgraph-overlay.js +646 -0
  91. package/src/daemon/error-worker.js +626 -0
  92. package/src/daemon/hybrid-mapper.js +929 -0
  93. package/src/daemon/jira-lifecycle.js +632 -0
  94. package/src/daemon/operator-control.js +657 -0
  95. package/src/daemon/reliability-lane.js +471 -0
  96. package/src/daemon/watchdog.js +971 -0
  97. package/src/guide/generator.js +316 -0
  98. package/src/ingest/engine.js +918 -0
  99. package/src/legacy-cli.js +2435 -0
  100. package/src/mcp/registry.js +695 -0
  101. package/src/memory/blackboard.js +301 -0
  102. package/src/memory/retrieval.js +581 -0
  103. package/src/plugin/manifest.js +553 -0
  104. package/src/policy/packs.js +144 -0
  105. package/src/prompt/generator.js +106 -0
  106. package/src/review/ai-review.js +669 -0
  107. package/src/review/local-review.js +1284 -0
  108. package/src/review/replay.js +235 -0
  109. package/src/review/report.js +664 -0
  110. package/src/review/spec-binding.js +487 -0
  111. package/src/scan/generator.js +351 -0
  112. package/src/spec/generator.js +519 -0
  113. package/src/spec/regenerate.js +237 -0
  114. package/src/spec/templates.js +91 -0
  115. package/src/swarm/dashboard.js +247 -0
  116. package/src/swarm/factory.js +363 -0
  117. package/src/swarm/pentest.js +934 -0
  118. package/src/swarm/registry.js +419 -0
  119. package/src/swarm/report.js +158 -0
  120. package/src/swarm/runtime.js +576 -0
  121. package/src/swarm/scenario-dsl.js +272 -0
  122. package/src/telemetry/ledger.js +302 -0
  123. package/src/ui/markdown.js +220 -0
  124. package/src/ui/progress.js +100 -0
@@ -0,0 +1,204 @@
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ function normalizeString(value) {
5
+ return String(value || "").trim();
6
+ }
7
+
8
+ function toPosixPath(value) {
9
+ return String(value || "").replace(/\\/g, "/");
10
+ }
11
+
12
+ function normalizeRunDirectory(runDirectory) {
13
+ const normalized = normalizeString(runDirectory);
14
+ if (!normalized) {
15
+ throw new Error("runDirectory is required for DD package generation.");
16
+ }
17
+ return path.resolve(normalized);
18
+ }
19
+
20
+ function buildFindingsIndex(report = {}) {
21
+ const seen = new Set();
22
+ const indexed = [];
23
+ for (const agent of report.agentResults || []) {
24
+ for (const finding of agent.findings || []) {
25
+ const key = `${toPosixPath(finding.file)}:${finding.line}:${normalizeString(
26
+ finding.message
27
+ ).toLowerCase()}`;
28
+ if (seen.has(key)) {
29
+ continue;
30
+ }
31
+ seen.add(key);
32
+ indexed.push({
33
+ findingId: `DD-${String(indexed.length + 1).padStart(4, "0")}`,
34
+ severity: finding.severity,
35
+ file: toPosixPath(finding.file),
36
+ line: finding.line,
37
+ message: finding.message,
38
+ ruleId: finding.ruleId || "",
39
+ suggestedFix: finding.suggestedFix || "",
40
+ ownerAgentId: agent.agentId,
41
+ });
42
+ }
43
+ }
44
+ return indexed.slice(0, 500);
45
+ }
46
+
47
+ function buildExecutiveSummaryMarkdown({ report, manifest, findingsIndex }) {
48
+ const topFindings = findingsIndex
49
+ .slice(0, 20)
50
+ .map((item) => `- [${item.severity}] ${item.file}:${item.line} ${item.message}`)
51
+ .join("\n");
52
+
53
+ return `# DD_EXEC_SUMMARY
54
+
55
+ Generated: ${manifest.generatedAt}
56
+ Run ID: ${manifest.runId}
57
+ Target: ${manifest.targetPath}
58
+
59
+ Overall findings:
60
+ - P0=${manifest.summary.P0}
61
+ - P1=${manifest.summary.P1}
62
+ - P2=${manifest.summary.P2}
63
+ - P3=${manifest.summary.P3}
64
+ - Blocking: ${manifest.summary.blocking ? "yes" : "no"}
65
+
66
+ Agent coverage:
67
+ ${(manifest.agents || [])
68
+ .map(
69
+ (agent) =>
70
+ `- ${agent.agentId} (${agent.persona}, ${agent.domain}) findings=${agent.findingCount} status=${agent.status}`
71
+ )
72
+ .join("\n")}
73
+
74
+ Deterministic baseline:
75
+ - Run ID: ${manifest.deterministicBaseline.runId || "n/a"}
76
+ - Summary: P0=${manifest.deterministicBaseline.summary.P0} P1=${manifest.deterministicBaseline.summary.P1} P2=${manifest.deterministicBaseline.summary.P2} P3=${manifest.deterministicBaseline.summary.P3}
77
+ - Report: ${manifest.deterministicBaseline.reportPath || "n/a"}
78
+
79
+ Top findings index:
80
+ ${topFindings || "- none"}
81
+
82
+ Package artifacts:
83
+ - manifest: ${path.join(report.runDirectory, "DD_PACKAGE_MANIFEST.json")}
84
+ - findings index: ${path.join(report.runDirectory, "DD_FINDINGS_INDEX.json")}
85
+ - executive summary: ${path.join(report.runDirectory, "DD_EXEC_SUMMARY.md")}
86
+ `;
87
+ }
88
+
89
+ export function buildDdPackageManifest(report = {}) {
90
+ const findingsIndex = buildFindingsIndex(report);
91
+ return {
92
+ schemaVersion: "1.0.0",
93
+ generatedAt: new Date().toISOString(),
94
+ runId: report.runId || "",
95
+ targetPath: report.targetPath || "",
96
+ runDirectory: report.runDirectory || "",
97
+ summary: report.summary || { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
98
+ deterministicBaseline: {
99
+ runId: report.deterministicBaseline?.runId || "",
100
+ reportPath: report.deterministicBaseline?.reportPath || "",
101
+ reportJsonPath: report.deterministicBaseline?.reportJsonPath || "",
102
+ summary: report.deterministicBaseline?.summary || { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
103
+ },
104
+ ingest: report.ingest || {},
105
+ agents: (report.agentResults || []).map((agent) => ({
106
+ agentId: agent.agentId,
107
+ persona: agent.persona,
108
+ domain: agent.domain,
109
+ status: agent.status,
110
+ findingCount: agent.findingCount,
111
+ confidence: agent.confidence,
112
+ artifactPath: agent.artifactPath || "",
113
+ specialistReportPath: agent.specialistReportPath || "",
114
+ })),
115
+ findingsIndexCount: findingsIndex.length,
116
+ };
117
+ }
118
+
119
+ export async function writeDdPackage({ report = {}, runDirectory } = {}) {
120
+ const resolvedRunDirectory = normalizeRunDirectory(runDirectory || report.runDirectory);
121
+ const manifest = buildDdPackageManifest({
122
+ ...report,
123
+ runDirectory: resolvedRunDirectory,
124
+ });
125
+ const findingsIndex = buildFindingsIndex({
126
+ ...report,
127
+ runDirectory: resolvedRunDirectory,
128
+ });
129
+ const manifestPath = path.join(resolvedRunDirectory, "DD_PACKAGE_MANIFEST.json");
130
+ const findingsIndexPath = path.join(resolvedRunDirectory, "DD_FINDINGS_INDEX.json");
131
+ const executiveSummaryPath = path.join(resolvedRunDirectory, "DD_EXEC_SUMMARY.md");
132
+ const executiveSummary = buildExecutiveSummaryMarkdown({
133
+ report: {
134
+ ...report,
135
+ runDirectory: resolvedRunDirectory,
136
+ },
137
+ manifest,
138
+ findingsIndex,
139
+ });
140
+
141
+ await fsp.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
142
+ await fsp.writeFile(findingsIndexPath, `${JSON.stringify(findingsIndex, null, 2)}\n`, "utf-8");
143
+ await fsp.writeFile(executiveSummaryPath, `${executiveSummary.trim()}\n`, "utf-8");
144
+
145
+ return {
146
+ manifest,
147
+ findingsIndexCount: findingsIndex.length,
148
+ manifestPath,
149
+ findingsIndexPath,
150
+ executiveSummaryPath,
151
+ };
152
+ }
153
+
154
+ export async function loadAuditRunReport(runDirectory) {
155
+ const resolvedRunDirectory = normalizeRunDirectory(runDirectory);
156
+ const reportPath = path.join(resolvedRunDirectory, "AUDIT_REPORT.json");
157
+ const raw = await fsp.readFile(reportPath, "utf-8");
158
+ const report = JSON.parse(raw);
159
+ return {
160
+ report,
161
+ reportPath,
162
+ runDirectory: resolvedRunDirectory,
163
+ };
164
+ }
165
+
166
+ export async function resolveAuditRunDirectory({ outputRoot, runId = "" } = {}) {
167
+ const normalizedOutputRoot = path.resolve(String(outputRoot || "."));
168
+ const auditsDirectory = path.join(normalizedOutputRoot, "audits");
169
+ const requestedRunId = normalizeString(runId);
170
+
171
+ if (requestedRunId) {
172
+ const requestedDirectory = path.join(auditsDirectory, requestedRunId);
173
+ await fsp.access(path.join(requestedDirectory, "AUDIT_REPORT.json"));
174
+ return requestedDirectory;
175
+ }
176
+
177
+ const entries = await fsp.readdir(auditsDirectory, { withFileTypes: true });
178
+ const candidates = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
179
+ if (candidates.length === 0) {
180
+ throw new Error("No audit runs found to package.");
181
+ }
182
+
183
+ const withMtime = [];
184
+ for (const candidate of candidates) {
185
+ const candidateDirectory = path.join(auditsDirectory, candidate);
186
+ const reportPath = path.join(candidateDirectory, "AUDIT_REPORT.json");
187
+ try {
188
+ const stat = await fsp.stat(reportPath);
189
+ withMtime.push({
190
+ candidateDirectory,
191
+ mtimeMs: stat.mtimeMs,
192
+ });
193
+ } catch {
194
+ // Ignore directories without a report artifact.
195
+ }
196
+ }
197
+
198
+ if (withMtime.length === 0) {
199
+ throw new Error("No valid audit run reports found under output root.");
200
+ }
201
+
202
+ withMtime.sort((left, right) => right.mtimeMs - left.mtimeMs);
203
+ return withMtime[0].candidateDirectory;
204
+ }
@@ -0,0 +1,284 @@
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const BUILTIN_AUDIT_AGENTS = Object.freeze([
5
+ {
6
+ id: "security",
7
+ persona: "Nina Patel",
8
+ domain: "Security",
9
+ tools: ["read", "grep", "dependency-audit"],
10
+ permissionMode: "plan",
11
+ maxTurns: 8,
12
+ confidenceFloor: 0.85,
13
+ evidenceRequirements: ["file_line_reference", "repro_steps"],
14
+ escalationTargets: ["release", "architecture"],
15
+ },
16
+ {
17
+ id: "architecture",
18
+ persona: "Maya Volkov",
19
+ domain: "Architecture",
20
+ tools: ["read", "grep", "structure-map"],
21
+ permissionMode: "plan",
22
+ maxTurns: 8,
23
+ confidenceFloor: 0.8,
24
+ evidenceRequirements: ["component_map", "impact_summary"],
25
+ escalationTargets: ["security", "performance"],
26
+ },
27
+ {
28
+ id: "performance",
29
+ persona: "Arjun Mehta",
30
+ domain: "Performance",
31
+ tools: ["read", "grep", "profiler-hints"],
32
+ permissionMode: "plan",
33
+ maxTurns: 6,
34
+ confidenceFloor: 0.8,
35
+ evidenceRequirements: ["latency_paths", "runtime_assumptions"],
36
+ escalationTargets: ["architecture", "reliability"],
37
+ },
38
+ {
39
+ id: "compliance",
40
+ persona: "Leila Farouk",
41
+ domain: "Compliance",
42
+ tools: ["read", "grep", "control-map"],
43
+ permissionMode: "plan",
44
+ maxTurns: 6,
45
+ confidenceFloor: 0.82,
46
+ evidenceRequirements: ["control_refs", "evidence_paths"],
47
+ escalationTargets: ["security", "release"],
48
+ },
49
+ {
50
+ id: "frontend",
51
+ persona: "Jules Tanaka",
52
+ domain: "Frontend",
53
+ tools: ["read", "grep", "ui-lint"],
54
+ permissionMode: "plan",
55
+ maxTurns: 6,
56
+ confidenceFloor: 0.75,
57
+ evidenceRequirements: ["component_paths", "repro_steps"],
58
+ escalationTargets: ["testing", "security"],
59
+ },
60
+ {
61
+ id: "data-layer",
62
+ persona: "Linh Tran",
63
+ domain: "Data Layer",
64
+ tools: ["read", "grep", "query-review"],
65
+ permissionMode: "plan",
66
+ maxTurns: 6,
67
+ confidenceFloor: 0.8,
68
+ evidenceRequirements: ["query_paths", "risk_evidence"],
69
+ escalationTargets: ["performance", "security"],
70
+ },
71
+ {
72
+ id: "release",
73
+ persona: "Omar Singh",
74
+ domain: "Release Engineering",
75
+ tools: ["read", "grep", "workflow-review"],
76
+ permissionMode: "plan",
77
+ maxTurns: 6,
78
+ confidenceFloor: 0.8,
79
+ evidenceRequirements: ["workflow_refs", "gate_matrix"],
80
+ escalationTargets: ["security", "reliability"],
81
+ },
82
+ {
83
+ id: "infrastructure",
84
+ persona: "Kat Hughes",
85
+ domain: "Infrastructure",
86
+ tools: ["read", "grep", "infra-lint"],
87
+ permissionMode: "plan",
88
+ maxTurns: 6,
89
+ confidenceFloor: 0.78,
90
+ evidenceRequirements: ["infra_paths", "blast_radius"],
91
+ escalationTargets: ["security", "reliability"],
92
+ },
93
+ {
94
+ id: "reliability",
95
+ persona: "Noah Ben-David",
96
+ domain: "Reliability",
97
+ tools: ["read", "grep", "test-runner"],
98
+ permissionMode: "plan",
99
+ maxTurns: 6,
100
+ confidenceFloor: 0.78,
101
+ evidenceRequirements: ["failure_modes", "rollback_plan"],
102
+ escalationTargets: ["release", "observability"],
103
+ },
104
+ {
105
+ id: "observability",
106
+ persona: "Sofia Alvarez",
107
+ domain: "Observability",
108
+ tools: ["read", "grep", "telemetry-review"],
109
+ permissionMode: "plan",
110
+ maxTurns: 6,
111
+ confidenceFloor: 0.75,
112
+ evidenceRequirements: ["signal_inventory", "gaps"],
113
+ escalationTargets: ["reliability", "release"],
114
+ },
115
+ {
116
+ id: "testing",
117
+ persona: "Priya Raman",
118
+ domain: "Testing",
119
+ tools: ["read", "grep", "test-runner"],
120
+ permissionMode: "plan",
121
+ maxTurns: 6,
122
+ confidenceFloor: 0.8,
123
+ evidenceRequirements: ["failing_paths", "coverage_gaps"],
124
+ escalationTargets: ["frontend", "architecture"],
125
+ },
126
+ {
127
+ id: "supply-chain",
128
+ persona: "Nora Kline",
129
+ domain: "Supply Chain",
130
+ tools: ["read", "grep", "dependency-audit"],
131
+ permissionMode: "plan",
132
+ maxTurns: 6,
133
+ confidenceFloor: 0.82,
134
+ evidenceRequirements: ["dependency_refs", "version_risks"],
135
+ escalationTargets: ["security", "release"],
136
+ },
137
+ {
138
+ id: "code-quality",
139
+ persona: "Ethan Park",
140
+ domain: "Code Quality",
141
+ tools: ["read", "grep", "lint-review"],
142
+ permissionMode: "plan",
143
+ maxTurns: 6,
144
+ confidenceFloor: 0.75,
145
+ evidenceRequirements: ["rule_hits", "refactor_candidates"],
146
+ escalationTargets: ["architecture", "testing"],
147
+ },
148
+ {
149
+ id: "documentation",
150
+ persona: "Samir Okafor",
151
+ domain: "Documentation",
152
+ tools: ["read", "grep", "spec-check"],
153
+ permissionMode: "plan",
154
+ maxTurns: 4,
155
+ confidenceFloor: 0.7,
156
+ evidenceRequirements: ["doc_paths", "spec_mismatches"],
157
+ escalationTargets: ["architecture", "release"],
158
+ },
159
+ {
160
+ id: "ai-governance",
161
+ persona: "Amina Chen",
162
+ domain: "AI Governance",
163
+ tools: ["read", "grep", "policy-check"],
164
+ permissionMode: "plan",
165
+ maxTurns: 6,
166
+ confidenceFloor: 0.82,
167
+ evidenceRequirements: ["budget_controls", "eval_refs"],
168
+ escalationTargets: ["security", "reliability"],
169
+ },
170
+ ]);
171
+
172
+ function normalizeString(value) {
173
+ return String(value || "").trim();
174
+ }
175
+
176
+ function normalizeAgentId(value) {
177
+ return normalizeString(value).toLowerCase();
178
+ }
179
+
180
+ function normalizeAgentRecord(record = {}) {
181
+ return {
182
+ id: normalizeAgentId(record.id),
183
+ persona: normalizeString(record.persona),
184
+ domain: normalizeString(record.domain),
185
+ tools: Array.isArray(record.tools) ? record.tools.map((item) => normalizeString(item)).filter(Boolean) : [],
186
+ permissionMode: normalizeString(record.permissionMode || "plan") || "plan",
187
+ maxTurns: Math.max(1, Math.floor(Number(record.maxTurns || 1))),
188
+ confidenceFloor: Math.max(0, Math.min(1, Number(record.confidenceFloor || 0))),
189
+ evidenceRequirements: Array.isArray(record.evidenceRequirements)
190
+ ? record.evidenceRequirements.map((item) => normalizeString(item)).filter(Boolean)
191
+ : [],
192
+ escalationTargets: Array.isArray(record.escalationTargets)
193
+ ? record.escalationTargets.map((item) => normalizeAgentId(item)).filter(Boolean)
194
+ : [],
195
+ };
196
+ }
197
+
198
+ function mergeRegistry(builtinAgents = [], overrideAgents = []) {
199
+ const byId = new Map();
200
+ for (const builtin of builtinAgents) {
201
+ byId.set(builtin.id, { ...builtin });
202
+ }
203
+ for (const override of overrideAgents) {
204
+ const normalized = normalizeAgentRecord(override);
205
+ if (!normalized.id) {
206
+ continue;
207
+ }
208
+ const existing = byId.get(normalized.id) || {};
209
+ byId.set(normalized.id, {
210
+ ...existing,
211
+ ...normalized,
212
+ });
213
+ }
214
+ return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id));
215
+ }
216
+
217
+ function parseAgentFilter(rawValue) {
218
+ const normalized = normalizeString(rawValue);
219
+ if (!normalized) {
220
+ return [];
221
+ }
222
+ return [...new Set(normalized.split(",").map((item) => normalizeAgentId(item)).filter(Boolean))];
223
+ }
224
+
225
+ export function listBuiltinAuditAgents() {
226
+ return BUILTIN_AUDIT_AGENTS.map((agent) => ({ ...agent }));
227
+ }
228
+
229
+ export async function loadAuditRegistry({ registryFile = "" } = {}) {
230
+ const builtin = listBuiltinAuditAgents();
231
+ const resolvedRegistryFile = normalizeString(registryFile)
232
+ ? path.resolve(process.cwd(), registryFile)
233
+ : "";
234
+ if (!resolvedRegistryFile) {
235
+ return {
236
+ registrySource: "builtin",
237
+ registryFile: "",
238
+ agents: builtin,
239
+ };
240
+ }
241
+
242
+ const raw = await fsp.readFile(resolvedRegistryFile, "utf-8");
243
+ const parsed = JSON.parse(raw);
244
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.agents)) {
245
+ throw new Error("Invalid audit registry file: expected { agents: [...] }.");
246
+ }
247
+
248
+ const merged = mergeRegistry(builtin, parsed.agents);
249
+ return {
250
+ registrySource: "custom",
251
+ registryFile: resolvedRegistryFile,
252
+ agents: merged,
253
+ };
254
+ }
255
+
256
+ export function selectAuditAgents(agents = [], requested = "") {
257
+ const requestedIds = parseAgentFilter(requested);
258
+ if (requestedIds.length === 0) {
259
+ return {
260
+ selected: [...agents],
261
+ requestedIds,
262
+ missing: [],
263
+ };
264
+ }
265
+
266
+ const byId = new Map(agents.map((agent) => [agent.id, agent]));
267
+ const selected = [];
268
+ const missing = [];
269
+ for (const id of requestedIds) {
270
+ const agent = byId.get(id);
271
+ if (!agent) {
272
+ missing.push(id);
273
+ continue;
274
+ }
275
+ selected.push(agent);
276
+ }
277
+
278
+ return {
279
+ selected,
280
+ requestedIds,
281
+ missing,
282
+ };
283
+ }
284
+
@@ -0,0 +1,103 @@
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ function normalizeString(value) {
5
+ return String(value || "").trim();
6
+ }
7
+
8
+ function toPosixPath(value) {
9
+ return String(value || "").replace(/\\/g, "/");
10
+ }
11
+
12
+ function fingerprintFinding(finding = {}) {
13
+ return [
14
+ normalizeString(finding.severity).toUpperCase(),
15
+ toPosixPath(finding.file),
16
+ String(Number(finding.line || 0)),
17
+ normalizeString(finding.message).toLowerCase(),
18
+ ].join("|");
19
+ }
20
+
21
+ function collectFindings(report = {}) {
22
+ const flattened = [];
23
+ for (const agent of report.agentResults || []) {
24
+ for (const finding of agent.findings || []) {
25
+ flattened.push({
26
+ severity: normalizeString(finding.severity).toUpperCase(),
27
+ file: toPosixPath(finding.file),
28
+ line: Number(finding.line || 0),
29
+ message: normalizeString(finding.message),
30
+ ruleId: normalizeString(finding.ruleId),
31
+ ownerAgentId: agent.agentId,
32
+ });
33
+ }
34
+ }
35
+ return flattened;
36
+ }
37
+
38
+ function summaryDelta(base = {}, candidate = {}) {
39
+ const keys = ["P0", "P1", "P2", "P3"];
40
+ const delta = {};
41
+ for (const key of keys) {
42
+ delta[key] = Number(candidate[key] || 0) - Number(base[key] || 0);
43
+ }
44
+ delta.blockingChanged = Boolean(base.blocking) !== Boolean(candidate.blocking);
45
+ return delta;
46
+ }
47
+
48
+ export function compareAuditReports(baseReport = {}, candidateReport = {}) {
49
+ const baseFindings = collectFindings(baseReport);
50
+ const candidateFindings = collectFindings(candidateReport);
51
+ const baseByFingerprint = new Map(baseFindings.map((finding) => [fingerprintFinding(finding), finding]));
52
+ const candidateByFingerprint = new Map(
53
+ candidateFindings.map((finding) => [fingerprintFinding(finding), finding])
54
+ );
55
+
56
+ const added = [];
57
+ const removed = [];
58
+
59
+ for (const [fingerprint, finding] of candidateByFingerprint.entries()) {
60
+ if (!baseByFingerprint.has(fingerprint)) {
61
+ added.push(finding);
62
+ }
63
+ }
64
+ for (const [fingerprint, finding] of baseByFingerprint.entries()) {
65
+ if (!candidateByFingerprint.has(fingerprint)) {
66
+ removed.push(finding);
67
+ }
68
+ }
69
+
70
+ const deterministicEquivalent = added.length === 0 && removed.length === 0;
71
+ return {
72
+ schemaVersion: "1.0.0",
73
+ generatedAt: new Date().toISOString(),
74
+ baseRunId: normalizeString(baseReport.runId),
75
+ candidateRunId: normalizeString(candidateReport.runId),
76
+ baseSummary: baseReport.summary || { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
77
+ candidateSummary: candidateReport.summary || { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
78
+ summaryDelta: summaryDelta(baseReport.summary || {}, candidateReport.summary || {}),
79
+ baseFindingCount: baseFindings.length,
80
+ candidateFindingCount: candidateFindings.length,
81
+ addedCount: added.length,
82
+ removedCount: removed.length,
83
+ deterministicEquivalent,
84
+ added: added.slice(0, 500),
85
+ removed: removed.slice(0, 500),
86
+ };
87
+ }
88
+
89
+ export async function writeAuditComparisonArtifact({
90
+ baseReport = {},
91
+ candidateReport = {},
92
+ outputDirectory = "",
93
+ } = {}) {
94
+ const resolvedOutputDirectory = path.resolve(String(outputDirectory || "."));
95
+ const comparison = compareAuditReports(baseReport, candidateReport);
96
+ const fileName = `AUDIT_COMPARISON_${comparison.baseRunId}_vs_${comparison.candidateRunId}.json`;
97
+ const outputPath = path.join(resolvedOutputDirectory, fileName);
98
+ await fsp.writeFile(outputPath, `${JSON.stringify(comparison, null, 2)}\n`, "utf-8");
99
+ return {
100
+ comparison,
101
+ outputPath,
102
+ };
103
+ }
@@ -0,0 +1,113 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+
3
+ /**
4
+ * Default timeout applied to Sentinelayer API requests when no override is provided.
5
+ * @type {number}
6
+ */
7
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 20_000;
8
+
9
+ function normalizeApiError(errorPayload = {}) {
10
+ if (!errorPayload || typeof errorPayload !== "object" || Array.isArray(errorPayload)) {
11
+ return {
12
+ code: "UNKNOWN",
13
+ message: "Unknown API error",
14
+ requestId: null,
15
+ };
16
+ }
17
+ return {
18
+ code: String(errorPayload.code || "UNKNOWN"),
19
+ message: String(errorPayload.message || "Unknown API error"),
20
+ requestId: errorPayload.request_id ? String(errorPayload.request_id) : null,
21
+ };
22
+ }
23
+
24
+ export class SentinelayerApiError extends Error {
25
+ /**
26
+ * @param {string} message
27
+ * @param {{ status?: number, code?: string, requestId?: string | null }} [options]
28
+ */
29
+ constructor(message, { status = 500, code = "UNKNOWN", requestId = null } = {}) {
30
+ super(String(message || "Sentinelayer API error"));
31
+ this.name = "SentinelayerApiError";
32
+ this.status = Number(status || 500);
33
+ this.code = String(code || "UNKNOWN");
34
+ this.requestId = requestId ? String(requestId) : null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Execute an HTTP request against the Sentinelayer API and parse a JSON response.
40
+ * Throws `SentinelayerApiError` for transport errors, timeouts, API failures, and invalid JSON.
41
+ *
42
+ * @param {string} url
43
+ * @param {{
44
+ * method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
45
+ * headers?: Record<string, string>,
46
+ * body?: unknown,
47
+ * timeoutMs?: number
48
+ * }} [options]
49
+ * @returns {Promise<any>}
50
+ */
51
+ export async function requestJson(
52
+ url,
53
+ { method = "GET", headers = {}, body, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = {}
54
+ ) {
55
+ const controller = new AbortController();
56
+ const timeout = setTimeout(() => controller.abort(), Number(timeoutMs || DEFAULT_REQUEST_TIMEOUT_MS));
57
+
58
+ try {
59
+ const response = await fetch(String(url), {
60
+ method,
61
+ headers: {
62
+ "Content-Type": "application/json",
63
+ ...headers,
64
+ },
65
+ body: body === undefined ? undefined : JSON.stringify(body),
66
+ signal: controller.signal,
67
+ });
68
+
69
+ const rawBody = await response.text();
70
+ let json = {};
71
+ if (rawBody.trim()) {
72
+ try {
73
+ json = JSON.parse(rawBody);
74
+ } catch {
75
+ throw new SentinelayerApiError("Invalid JSON returned by API.", {
76
+ status: response.status,
77
+ code: "INVALID_JSON",
78
+ });
79
+ }
80
+ }
81
+
82
+ if (!response.ok) {
83
+ const apiError = normalizeApiError(json && typeof json === "object" ? json.error : {});
84
+ throw new SentinelayerApiError(apiError.message, {
85
+ status: response.status,
86
+ code: apiError.code,
87
+ requestId: apiError.requestId,
88
+ });
89
+ }
90
+
91
+ return json;
92
+ } catch (error) {
93
+ if (error instanceof SentinelayerApiError) {
94
+ throw error;
95
+ }
96
+ if (error && typeof error === "object" && error.name === "AbortError") {
97
+ throw new SentinelayerApiError("Request timed out.", {
98
+ status: 408,
99
+ code: "TIMEOUT",
100
+ });
101
+ }
102
+ throw new SentinelayerApiError(
103
+ error instanceof Error ? error.message : String(error || "Request failed"),
104
+ {
105
+ status: 503,
106
+ code: "NETWORK_ERROR",
107
+ }
108
+ );
109
+ } finally {
110
+ clearTimeout(timeout);
111
+ await sleep(0);
112
+ }
113
+ }