sentinelayer-cli 0.4.4 → 0.6.2

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 (149) hide show
  1. package/README.md +996 -998
  2. package/bin/create-sentinelayer.js +5 -5
  3. package/bin/sentinelayer-cli.js +4 -4
  4. package/bin/sl.js +5 -5
  5. package/package.json +63 -63
  6. package/src/agents/jules/config/definition.js +160 -209
  7. package/src/agents/jules/config/system-prompt.js +182 -175
  8. package/src/agents/jules/error-intake.js +51 -51
  9. package/src/agents/jules/fix-cycle.js +17 -377
  10. package/src/agents/jules/loop.js +450 -367
  11. package/src/agents/jules/pulse.js +10 -327
  12. package/src/agents/jules/stream.js +186 -186
  13. package/src/agents/jules/swarm/file-scanner.js +74 -74
  14. package/src/agents/jules/swarm/index.js +11 -11
  15. package/src/agents/jules/swarm/orchestrator.js +362 -362
  16. package/src/agents/jules/swarm/pattern-hunter.js +123 -123
  17. package/src/agents/jules/swarm/sub-agent.js +309 -308
  18. package/src/agents/jules/tools/aidenid-email.js +189 -0
  19. package/src/agents/jules/tools/auth-audit.js +1691 -557
  20. package/src/agents/jules/tools/dispatch.js +335 -327
  21. package/src/agents/jules/tools/file-edit.js +2 -180
  22. package/src/agents/jules/tools/file-read.js +2 -100
  23. package/src/agents/jules/tools/frontend-analyze.js +570 -570
  24. package/src/agents/jules/tools/glob.js +2 -168
  25. package/src/agents/jules/tools/grep.js +2 -228
  26. package/src/agents/jules/tools/index.js +29 -29
  27. package/src/agents/jules/tools/path-guards.js +2 -161
  28. package/src/agents/jules/tools/runtime-audit.js +507 -503
  29. package/src/agents/jules/tools/shell.js +2 -383
  30. package/src/agents/jules/tools/url-policy.js +100 -100
  31. package/src/agents/persona-visuals.js +61 -0
  32. package/src/agents/shared-tools/dispatch-core.js +315 -0
  33. package/src/agents/shared-tools/file-edit.js +180 -0
  34. package/src/agents/shared-tools/file-read.js +100 -0
  35. package/src/agents/shared-tools/glob.js +168 -0
  36. package/src/agents/shared-tools/grep.js +228 -0
  37. package/src/agents/shared-tools/index.js +46 -0
  38. package/src/agents/shared-tools/path-guards.js +161 -0
  39. package/src/agents/shared-tools/shell.js +383 -0
  40. package/src/ai/aidenid.js +1009 -972
  41. package/src/ai/client.js +553 -508
  42. package/src/ai/domain-target-store.js +268 -268
  43. package/src/ai/identity-store.js +270 -270
  44. package/src/ai/proxy.js +137 -0
  45. package/src/ai/site-store.js +145 -145
  46. package/src/audit/agents/architecture.js +180 -180
  47. package/src/audit/agents/compliance.js +179 -179
  48. package/src/audit/agents/documentation.js +165 -165
  49. package/src/audit/agents/performance.js +145 -145
  50. package/src/audit/agents/security.js +215 -215
  51. package/src/audit/agents/testing.js +172 -172
  52. package/src/audit/orchestrator.js +557 -557
  53. package/src/audit/package.js +204 -204
  54. package/src/audit/registry.js +284 -284
  55. package/src/audit/replay.js +103 -103
  56. package/src/auth/gate.js +371 -126
  57. package/src/auth/http.js +611 -270
  58. package/src/auth/service.js +1106 -891
  59. package/src/auth/session-store.js +813 -359
  60. package/src/cli.js +252 -252
  61. package/src/commands/ai/identity-lifecycle.js +1338 -1338
  62. package/src/commands/ai/provision-governance.js +1272 -1272
  63. package/src/commands/ai/shared.js +147 -147
  64. package/src/commands/ai.js +11 -11
  65. package/src/commands/apply.js +12 -12
  66. package/src/commands/audit.js +1166 -1166
  67. package/src/commands/auth.js +419 -375
  68. package/src/commands/chat.js +191 -191
  69. package/src/commands/config.js +184 -184
  70. package/src/commands/cost.js +311 -311
  71. package/src/commands/daemon/core.js +850 -850
  72. package/src/commands/daemon/extended.js +1048 -1048
  73. package/src/commands/daemon/shared.js +213 -213
  74. package/src/commands/daemon.js +11 -11
  75. package/src/commands/guide.js +174 -174
  76. package/src/commands/ingest.js +58 -58
  77. package/src/commands/init.js +55 -55
  78. package/src/commands/legacy-args.js +10 -10
  79. package/src/commands/mcp.js +461 -461
  80. package/src/commands/omargate.js +29 -21
  81. package/src/commands/persona.js +20 -20
  82. package/src/commands/plugin.js +260 -260
  83. package/src/commands/policy.js +132 -132
  84. package/src/commands/prompt.js +238 -238
  85. package/src/commands/review.js +704 -704
  86. package/src/commands/scan.js +872 -866
  87. package/src/commands/spec.js +716 -716
  88. package/src/commands/swarm.js +651 -651
  89. package/src/commands/telemetry.js +202 -202
  90. package/src/commands/watch.js +511 -510
  91. package/src/config/agent-dictionary.js +182 -182
  92. package/src/config/io.js +56 -56
  93. package/src/config/paths.js +18 -18
  94. package/src/config/schema.js +55 -55
  95. package/src/config/service.js +184 -184
  96. package/src/cost/budget.js +235 -235
  97. package/src/cost/history.js +188 -188
  98. package/src/cost/tracker.js +171 -171
  99. package/src/daemon/artifact-lineage.js +534 -534
  100. package/src/daemon/assignment-ledger.js +770 -770
  101. package/src/daemon/ast-parser-layer.js +258 -258
  102. package/src/daemon/budget-governor.js +633 -633
  103. package/src/daemon/callgraph-overlay.js +646 -646
  104. package/src/daemon/error-worker.js +626 -626
  105. package/src/daemon/fix-cycle.js +377 -0
  106. package/src/daemon/hybrid-mapper.js +929 -929
  107. package/src/daemon/jira-lifecycle.js +632 -632
  108. package/src/daemon/operator-control.js +657 -657
  109. package/src/daemon/pulse.js +327 -0
  110. package/src/daemon/reliability-lane.js +471 -471
  111. package/src/daemon/watchdog.js +971 -971
  112. package/src/guide/generator.js +316 -316
  113. package/src/ingest/engine.js +918 -918
  114. package/src/interactive/index.js +97 -95
  115. package/src/legacy-cli.js +2994 -2592
  116. package/src/mcp/registry.js +695 -695
  117. package/src/memory/blackboard.js +301 -301
  118. package/src/memory/retrieval.js +581 -581
  119. package/src/plugin/manifest.js +553 -553
  120. package/src/policy/packs.js +144 -144
  121. package/src/prompt/generator.js +118 -118
  122. package/src/review/ai-review.js +679 -669
  123. package/src/review/local-review.js +1305 -1295
  124. package/src/review/omargate-interactive.js +68 -0
  125. package/src/review/omargate-orchestrator.js +300 -0
  126. package/src/review/persona-prompts.js +296 -0
  127. package/src/review/replay.js +235 -235
  128. package/src/review/report.js +664 -664
  129. package/src/review/scan-modes.js +42 -0
  130. package/src/review/spec-binding.js +487 -487
  131. package/src/scaffold/generator.js +67 -67
  132. package/src/scaffold/templates.js +150 -150
  133. package/src/scan/generator.js +418 -418
  134. package/src/scan/gh-secrets.js +107 -107
  135. package/src/spec/generator.js +519 -519
  136. package/src/spec/regenerate.js +237 -237
  137. package/src/spec/templates.js +91 -91
  138. package/src/swarm/dashboard.js +247 -247
  139. package/src/swarm/factory.js +363 -363
  140. package/src/swarm/pentest.js +934 -934
  141. package/src/swarm/registry.js +419 -419
  142. package/src/swarm/report.js +158 -158
  143. package/src/swarm/runtime.js +576 -576
  144. package/src/swarm/scenario-dsl.js +272 -272
  145. package/src/telemetry/ledger.js +302 -302
  146. package/src/telemetry/session-tracker.js +234 -118
  147. package/src/telemetry/sync.js +203 -199
  148. package/src/ui/command-hints.js +13 -0
  149. package/src/ui/markdown.js +220 -220
@@ -1,367 +1,450 @@
1
- import { randomUUID } from "node:crypto";
2
- import { createMultiProviderApiClient } from "../../ai/client.js";
3
- import { evaluateBudget } from "../../cost/budget.js";
4
- import { dispatchTool, createAgentContext, BudgetExhaustedError } from "./tools/dispatch.js";
5
- import { JULES_DEFINITION } from "./config/definition.js";
6
- import { shouldSpawnSubAgents, runJulesSwarm } from "./swarm/orchestrator.js";
7
- import { frontendAnalyze } from "./tools/frontend-analyze.js";
8
-
9
- /**
10
- * Jules Tanaka — Agentic Loop
11
- *
12
- * Core state machine: LLM → tool_use → execute → result → LLM → repeat
13
- * With sub-agent swarm integration for large codebases.
14
- *
15
- * This loop is self-contained: it uses the existing ai/client.js for LLM calls,
16
- * the existing cost/budget.js for budget enforcement, and the Jules tool
17
- * dispatch for tool execution. No dependency on Batches O-Q.
18
- */
19
-
20
- const DEFAULT_MAX_TURNS = 25;
21
- const HEARTBEAT_INTERVAL_TURNS = 5;
22
-
23
- /**
24
- * Run Jules' agentic audit loop.
25
- *
26
- * @param {object} config
27
- * @param {string} config.systemPrompt - Jules' full system prompt
28
- * @param {object} config.scopeMap - { primary, secondary, tertiary } file lists
29
- * @param {string} config.rootPath - Codebase root
30
- * @param {object} [config.omarBaseline] - Deterministic baseline findings (if available)
31
- * @param {object} [config.blackboard] - Shared blackboard for cross-agent findings
32
- * @param {object} [config.memory] - Memory index for cross-run recall
33
- * @param {object} [config.budget] - Budget overrides
34
- * @param {object} [config.provider] - LLM provider overrides
35
- * @param {string} [config.mode] - "primary" | "secondary" | "tertiary"
36
- * @param {number} [config.maxTurns] - Max loop iterations
37
- * @param {AbortController} [config.abortController]
38
- * @param {function} [config.onEvent] - Streaming event callback
39
- * @returns {AsyncGenerator<JulesEvent>} Yields events as they occur
40
- */
41
- export async function* julesAuditLoop(config) {
42
- const {
43
- systemPrompt,
44
- scopeMap,
45
- rootPath,
46
- omarBaseline,
47
- blackboard,
48
- memory,
49
- provider,
50
- mode = "primary",
51
- maxTurns = DEFAULT_MAX_TURNS,
52
- abortController,
53
- onEvent,
54
- } = config;
55
-
56
- const budget = { ...JULES_DEFINITION.budget, ...config.budget };
57
- const runId = `jules-${Date.now()}-${randomUUID().slice(0, 8)}`;
58
- const startedAt = Date.now();
59
- const client = createMultiProviderApiClient(provider || {});
60
-
61
- const ctx = createAgentContext({
62
- agentIdentity: { id: JULES_DEFINITION.id, persona: JULES_DEFINITION.persona },
63
- budget,
64
- runId,
65
- onEvent,
66
- });
67
-
68
- const emit = (event, payload) => {
69
- const evt = {
70
- stream: "sl_event",
71
- event,
72
- agent: { id: JULES_DEFINITION.id, persona: JULES_DEFINITION.persona, color: JULES_DEFINITION.color, avatar: JULES_DEFINITION.avatar },
73
- payload,
74
- usage: {
75
- costUsd: ctx.usage.costUsd,
76
- outputTokens: ctx.usage.outputTokens,
77
- toolCalls: ctx.usage.toolCalls,
78
- durationMs: Date.now() - startedAt,
79
- },
80
- };
81
- if (onEvent) onEvent(evt);
82
- return evt;
83
- };
84
-
85
- yield emit("agent_start", { mode, runId, maxTurns, budget });
86
-
87
- // ── Phase 0: Prerequisites ────────────────────────────────────────
88
-
89
- yield emit("progress", { phase: "prerequisites", message: "Detecting framework..." });
90
-
91
- let framework = {};
92
- try {
93
- framework = frontendAnalyze({ operation: "detect_framework", path: rootPath });
94
- ctx.usage.toolCalls++;
95
- yield emit("tool_result", { tool: "FrontendAnalyze", operation: "detect_framework", result: { framework: framework.framework, componentCount: framework.componentCount } });
96
- } catch { /* proceed without */ }
97
-
98
- // ── Phase 1: Swarm or direct? ─────────────────────────────────────
99
-
100
- const spawnDecision = shouldSpawnSubAgents(scopeMap);
101
- let swarmFindings = [];
102
-
103
- if (spawnDecision.spawn && blackboard) {
104
- yield emit("progress", { phase: "swarm", message: `Large frontend (${spawnDecision.reason}). Spawning sub-agents...` });
105
-
106
- const swarmResult = await runJulesSwarm({
107
- scopeMap,
108
- rootPath,
109
- blackboard,
110
- budget: { ...budget, maxCostUsd: budget.maxCostUsd * 0.6 }, // 60% for swarm
111
- provider,
112
- parentAbort: abortController,
113
- onEvent,
114
- });
115
-
116
- swarmFindings = swarmResult.agentResults.flatMap(r => r.findings);
117
- ctx.usage.costUsd += swarmResult.usage.totalCostUsd;
118
- ctx.usage.toolCalls += swarmResult.usage.totalToolCalls;
119
-
120
- yield emit("swarm_complete", {
121
- totalFindings: swarmFindings.length,
122
- totalAgents: swarmResult.usage.totalAgents,
123
- totalCostUsd: swarmResult.usage.totalCostUsd,
124
- });
125
- }
126
-
127
- // ── Phase 2: Jules primary deep analysis (agentic LLM loop) ──────
128
-
129
- yield emit("progress", { phase: "deep_analysis", message: "Starting deep analysis..." });
130
-
131
- // Build context for LLM
132
- const contextParts = [];
133
- contextParts.push(`Framework: ${framework.framework || "unknown"}`);
134
- contextParts.push(`Mode: ${mode}`);
135
- contextParts.push(`Components: ${framework.componentCount || "unknown"}`);
136
- contextParts.push(`Scope: ${(scopeMap.primary || []).length} primary files`);
137
-
138
- if (swarmFindings.length > 0) {
139
- contextParts.push(`\nSub-agent findings (${swarmFindings.length} total):`);
140
- for (const f of swarmFindings.slice(0, 30)) {
141
- contextParts.push(`- [${f.severity || "P3"}] ${f.file || ""}:${f.line || ""} ${f.title || f.type || ""}`);
142
- }
143
- }
144
-
145
- if (omarBaseline) {
146
- const baselineFindings = omarBaseline.findings || omarBaseline.summary || [];
147
- if (Array.isArray(baselineFindings) && baselineFindings.length > 0) {
148
- contextParts.push(`\nOmar baseline findings (${baselineFindings.length}):`);
149
- for (const f of baselineFindings.slice(0, 20)) {
150
- contextParts.push(`- [${f.severity || ""}] ${f.file || ""}:${f.line || ""} ${f.message || f.title || ""}`);
151
- }
152
- }
153
- }
154
-
155
- if (memory) {
156
- try {
157
- const recalled = memory.query ? memory.query({
158
- files: (scopeMap.primary || []).map(f => f.path || f),
159
- limit: 10,
160
- }) : [];
161
- if (recalled.length > 0) {
162
- contextParts.push(`\nPrevious findings recalled from memory (${recalled.length}):`);
163
- for (const r of recalled) {
164
- contextParts.push(`- ${r.content || r.text || JSON.stringify(r).slice(0, 100)}`);
165
- }
166
- }
167
- } catch { /* memory recall failure is non-blocking */ }
168
- }
169
-
170
- const messages = [
171
- { role: "user", content: contextParts.join("\n") +
172
- "\n\nPerform your deep analysis now. Use FileRead, Grep, Glob, and FrontendAnalyze tools as needed. " +
173
- "Return your findings in a ```json code block as an array of { severity, file, line, title, evidence, rootCause, recommendedFix, trafficLight }." },
174
- ];
175
-
176
- const allFindings = [...swarmFindings];
177
- let turnCount = 0;
178
-
179
- while (turnCount < maxTurns) {
180
- if (abortController?.signal.aborted) {
181
- yield emit("agent_abort", { reason: "user_cancelled" });
182
- break;
183
- }
184
-
185
- // Budget check before LLM call
186
- const preCheck = evaluateBudget({
187
- sessionSummary: {
188
- costUsd: ctx.usage.costUsd,
189
- outputTokens: ctx.usage.outputTokens,
190
- durationMs: Date.now() - startedAt,
191
- toolCalls: ctx.usage.toolCalls,
192
- },
193
- ...budget,
194
- });
195
-
196
- if (preCheck.blocking) {
197
- yield emit("budget_stop", { reasons: preCheck.reasons });
198
- break;
199
- }
200
-
201
- if (preCheck.warnings.length > 0) {
202
- yield emit("budget_warning", { warnings: preCheck.warnings });
203
- }
204
-
205
- turnCount++;
206
-
207
- // Heartbeat
208
- if (turnCount % HEARTBEAT_INTERVAL_TURNS === 0) {
209
- yield emit("heartbeat", {
210
- turnsCompleted: turnCount,
211
- turnsMax: maxTurns,
212
- findingsSoFar: allFindings.length,
213
- budgetRemaining: {
214
- costUsd: Math.max(0, budget.maxCostUsd - ctx.usage.costUsd),
215
- pct: Math.max(0, 100 - (ctx.usage.costUsd / budget.maxCostUsd * 100)),
216
- },
217
- });
218
- }
219
-
220
- // Call LLM
221
- let response;
222
- try {
223
- response = await client.invoke({
224
- systemPrompt,
225
- messages,
226
- });
227
- } catch (err) {
228
- yield emit("llm_error", { error: err.message, turn: turnCount });
229
- break;
230
- }
231
-
232
- const responseText = response.text || "";
233
- ctx.usage.outputTokens += Math.ceil(responseText.length / 4);
234
- ctx.usage.costUsd += (Math.ceil(responseText.length / 4) / 1_000_000) * 15;
235
-
236
- yield emit("reasoning", {
237
- phase: "deep_analysis",
238
- turn: turnCount,
239
- summary: responseText.slice(0, 200),
240
- });
241
-
242
- // Parse tool_use blocks
243
- const toolCalls = parseToolUseBlocks(responseText);
244
-
245
- if (toolCalls.length === 0) {
246
- // No tools — extract findings from response
247
- const parsed = extractJsonFindings(responseText);
248
- for (const finding of parsed) {
249
- allFindings.push(finding);
250
- yield emit("finding", { ...finding });
251
- if (blackboard) {
252
- try {
253
- await blackboard.appendEntry({
254
- agentId: JULES_DEFINITION.id,
255
- source: "jules-primary",
256
- ...finding,
257
- });
258
- } catch { /* blackboard write failure non-blocking */ }
259
- }
260
- }
261
- messages.push({ role: "assistant", content: responseText });
262
- break; // LLM is done
263
- }
264
-
265
- // Execute tool calls
266
- const results = [];
267
- for (const call of toolCalls) {
268
- try {
269
- const result = await dispatchTool(call.tool, call.input, ctx);
270
- results.push({ tool: call.tool, result });
271
- yield emit("tool_call", { tool: call.tool, input: sanitizeForEvent(call.input) });
272
- } catch (err) {
273
- if (err instanceof BudgetExhaustedError) {
274
- yield emit("budget_stop", { reason: err.message });
275
- break;
276
- }
277
- results.push({ tool: call.tool, error: err.message });
278
- }
279
- }
280
-
281
- // Feed results back
282
- messages.push({ role: "assistant", content: responseText });
283
- messages.push({
284
- role: "user",
285
- content: results.map(r =>
286
- r.error
287
- ? `Tool ${r.tool} failed: ${r.error}`
288
- : `Tool ${r.tool} result:\n${JSON.stringify(r.result).slice(0, 3000)}`,
289
- ).join("\n\n") + "\n\nContinue your analysis. If done, return findings in a ```json code block.",
290
- });
291
- }
292
-
293
- // ── Phase 3: Build final report ───────────────────────────────────
294
-
295
- const durationMs = Date.now() - startedAt;
296
- const severityCounts = { P0: 0, P1: 0, P2: 0, P3: 0 };
297
- for (const f of allFindings) {
298
- const sev = (f.severity || "P3").toUpperCase();
299
- if (severityCounts[sev] !== undefined) severityCounts[sev]++;
300
- else severityCounts.P3++;
301
- }
302
-
303
- const report = {
304
- runId,
305
- persona: JULES_DEFINITION.persona,
306
- mode,
307
- framework: framework.framework || "unknown",
308
- status: "completed",
309
- findings: allFindings,
310
- summary: {
311
- total: allFindings.length,
312
- ...severityCounts,
313
- blocking: severityCounts.P0 > 0 || severityCounts.P1 > 0,
314
- },
315
- usage: {
316
- turns: turnCount,
317
- costUsd: ctx.usage.costUsd,
318
- outputTokens: ctx.usage.outputTokens,
319
- toolCalls: ctx.usage.toolCalls,
320
- durationMs,
321
- },
322
- signature: JULES_DEFINITION.signature,
323
- };
324
-
325
- yield emit("agent_complete", {
326
- ...report.summary,
327
- costUsd: ctx.usage.costUsd,
328
- durationMs,
329
- turns: turnCount,
330
- });
331
-
332
- return report;
333
- }
334
-
335
- // ── Helpers ──────────────────────────────────────────────────────────
336
-
337
- function parseToolUseBlocks(text) {
338
- const calls = [];
339
- const regex = /```tool_use\s*\n([\s\S]*?)```/g;
340
- let match;
341
- while ((match = regex.exec(text)) !== null) {
342
- try {
343
- const parsed = JSON.parse(match[1].trim());
344
- if (parsed.tool && parsed.input) calls.push(parsed);
345
- } catch { /* skip malformed */ }
346
- }
347
- return calls;
348
- }
349
-
350
- function extractJsonFindings(text) {
351
- const jsonMatch = text.match(/```json\s*\n([\s\S]*?)```/);
352
- if (!jsonMatch) return [];
353
- try {
354
- const parsed = JSON.parse(jsonMatch[1].trim());
355
- if (Array.isArray(parsed)) return parsed;
356
- if (parsed.findings && Array.isArray(parsed.findings)) return parsed.findings;
357
- } catch { /* skip malformed */ }
358
- return [];
359
- }
360
-
361
- function sanitizeForEvent(input) {
362
- const sanitized = { ...input };
363
- if (typeof sanitized.content === "string" && sanitized.content.length > 200) {
364
- sanitized.content = `[${sanitized.content.length} chars]`;
365
- }
366
- return sanitized;
367
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { createMultiProviderApiClient } from "../../ai/client.js";
3
+ import { evaluateBudget } from "../../cost/budget.js";
4
+ import { dispatchTool, createAgentContext, BudgetExhaustedError } from "./tools/dispatch.js";
5
+ import { JULES_DEFINITION } from "./config/definition.js";
6
+ import { shouldSpawnSubAgents, runJulesSwarm } from "./swarm/orchestrator.js";
7
+ import { frontendAnalyze } from "./tools/frontend-analyze.js";
8
+
9
+ /**
10
+ * Jules Tanaka — Agentic Loop
11
+ *
12
+ * Core state machine: LLM → tool_use → execute → result → LLM → repeat
13
+ * With sub-agent swarm integration for large codebases.
14
+ *
15
+ * This loop is self-contained: it uses the existing ai/client.js for LLM calls,
16
+ * the existing cost/budget.js for budget enforcement, and the Jules tool
17
+ * dispatch for tool execution. No dependency on Batches O-Q.
18
+ */
19
+
20
+ const DEFAULT_MAX_TURNS = 25;
21
+ const HEARTBEAT_INTERVAL_TURNS = 5;
22
+
23
+ /**
24
+ * Run Jules' agentic audit loop.
25
+ *
26
+ * @param {object} config
27
+ * @param {string} config.systemPrompt - Jules' full system prompt
28
+ * @param {object} config.scopeMap - { primary, secondary, tertiary } file lists
29
+ * @param {string} config.rootPath - Codebase root
30
+ * @param {object} [config.omarBaseline] - Deterministic baseline findings (if available)
31
+ * @param {object} [config.blackboard] - Shared blackboard for cross-agent findings
32
+ * @param {object} [config.memory] - Memory index for cross-run recall
33
+ * @param {object} [config.budget] - Budget overrides
34
+ * @param {object} [config.provider] - LLM provider overrides
35
+ * @param {string} [config.mode] - "primary" | "secondary" | "tertiary"
36
+ * @param {number} [config.maxTurns] - Max loop iterations
37
+ * @param {AbortController} [config.abortController]
38
+ * @param {function} [config.onEvent] - Streaming event callback
39
+ * @returns {AsyncGenerator<JulesEvent>} Yields events as they occur
40
+ */
41
+ export async function* julesAuditLoop(config) {
42
+ const {
43
+ systemPrompt,
44
+ scopeMap,
45
+ rootPath,
46
+ omarBaseline,
47
+ blackboard,
48
+ memory,
49
+ provider,
50
+ mode = "primary",
51
+ maxTurns = DEFAULT_MAX_TURNS,
52
+ abortController,
53
+ onEvent,
54
+ } = config;
55
+
56
+ const budget = { ...JULES_DEFINITION.budget, ...config.budget };
57
+ const runId = `jules-${Date.now()}-${randomUUID().slice(0, 8)}`;
58
+ const startedAt = Date.now();
59
+ const client = createMultiProviderApiClient(provider || {});
60
+
61
+ const ctx = createAgentContext({
62
+ agentIdentity: { id: JULES_DEFINITION.id, persona: JULES_DEFINITION.persona },
63
+ budget,
64
+ runId,
65
+ onEvent,
66
+ });
67
+
68
+ const emit = (event, payload) => {
69
+ const evt = {
70
+ stream: "sl_event",
71
+ event,
72
+ agent: { id: JULES_DEFINITION.id, persona: JULES_DEFINITION.persona, color: JULES_DEFINITION.color, avatar: JULES_DEFINITION.avatar },
73
+ payload,
74
+ usage: {
75
+ costUsd: ctx.usage.costUsd,
76
+ outputTokens: ctx.usage.outputTokens,
77
+ toolCalls: ctx.usage.toolCalls,
78
+ durationMs: Date.now() - startedAt,
79
+ },
80
+ };
81
+ if (onEvent) onEvent(evt);
82
+ return evt;
83
+ };
84
+
85
+ yield emit("agent_start", { mode, runId, maxTurns, budget });
86
+
87
+ // ── Phase 0: Prerequisites ────────────────────────────────────────
88
+
89
+ yield emit("progress", { phase: "prerequisites", message: "Detecting framework..." });
90
+
91
+ let framework = {};
92
+ try {
93
+ framework = frontendAnalyze({ operation: "detect_framework", path: rootPath });
94
+ ctx.usage.toolCalls++;
95
+ yield emit("tool_result", { tool: "FrontendAnalyze", operation: "detect_framework", result: { framework: framework.framework, componentCount: framework.componentCount } });
96
+ } catch { /* proceed without */ }
97
+
98
+ // ── Phase 1: Swarm or direct? ─────────────────────────────────────
99
+
100
+ const spawnDecision = shouldSpawnSubAgents(scopeMap);
101
+ let swarmFindings = [];
102
+
103
+ if (spawnDecision.spawn && blackboard) {
104
+ yield emit("progress", { phase: "swarm", message: `Large frontend (${spawnDecision.reason}). Spawning sub-agents...` });
105
+
106
+ const swarmResult = await runJulesSwarm({
107
+ scopeMap,
108
+ rootPath,
109
+ blackboard,
110
+ budget: { ...budget, maxCostUsd: budget.maxCostUsd * 0.6 }, // 60% for swarm
111
+ provider,
112
+ parentAbort: abortController,
113
+ onEvent,
114
+ });
115
+
116
+ swarmFindings = swarmResult.agentResults.flatMap(r => r.findings);
117
+ ctx.usage.costUsd += swarmResult.usage.totalCostUsd;
118
+ ctx.usage.toolCalls += swarmResult.usage.totalToolCalls;
119
+
120
+ yield emit("swarm_complete", {
121
+ totalFindings: swarmFindings.length,
122
+ totalAgents: swarmResult.usage.totalAgents,
123
+ totalCostUsd: swarmResult.usage.totalCostUsd,
124
+ });
125
+ }
126
+
127
+ // ── Phase 2: Jules primary deep analysis (agentic LLM loop) ──────
128
+
129
+ yield emit("progress", { phase: "deep_analysis", message: "Starting deep analysis..." });
130
+
131
+ // Build context for LLM — BLIND-FIRST: no Omar baseline or swarm findings
132
+ // in the initial context. Only codebase metadata and memory recall (past runs,
133
+ // not current-run findings). Swarm/baseline reconciliation happens AFTER the
134
+ // independent deep analysis completes.
135
+ const contextParts = [];
136
+ contextParts.push(`Framework: ${framework.framework || "unknown"}`);
137
+ contextParts.push(`Mode: ${mode}`);
138
+ contextParts.push(`Components: ${framework.componentCount || "unknown"}`);
139
+ contextParts.push(`Scope: ${(scopeMap.primary || []).length} primary files`);
140
+
141
+ if (memory) {
142
+ try {
143
+ const recalled = memory.query ? memory.query({
144
+ files: (scopeMap.primary || []).map(f => f.path || f),
145
+ limit: 10,
146
+ }) : [];
147
+ if (recalled.length > 0) {
148
+ contextParts.push(`\nPrevious findings recalled from memory (${recalled.length}):`);
149
+ for (const r of recalled) {
150
+ contextParts.push(`- ${r.content || r.text || JSON.stringify(r).slice(0, 100)}`);
151
+ }
152
+ }
153
+ } catch { /* memory recall failure is non-blocking */ }
154
+ }
155
+
156
+ const messages = [
157
+ { role: "user", content: contextParts.join("\n") +
158
+ "\n\nPerform your deep analysis now. Use FileRead, Grep, Glob, and FrontendAnalyze tools as needed. " +
159
+ "Return your findings in a ```json code block as an array of { severity, file, line, title, evidence, rootCause, recommendedFix, trafficLight, reproduction, user_impact, confidence }." },
160
+ ];
161
+
162
+ const allFindings = [...swarmFindings];
163
+ let turnCount = 0;
164
+
165
+ while (turnCount < maxTurns) {
166
+ if (abortController?.signal.aborted) {
167
+ yield emit("agent_abort", { reason: "user_cancelled" });
168
+ break;
169
+ }
170
+
171
+ // Budget check before LLM call
172
+ const preCheck = evaluateBudget({
173
+ sessionSummary: {
174
+ costUsd: ctx.usage.costUsd,
175
+ outputTokens: ctx.usage.outputTokens,
176
+ durationMs: Date.now() - startedAt,
177
+ toolCalls: ctx.usage.toolCalls,
178
+ },
179
+ ...budget,
180
+ });
181
+
182
+ if (preCheck.blocking) {
183
+ yield emit("budget_stop", { reasons: preCheck.reasons });
184
+ break;
185
+ }
186
+
187
+ if (preCheck.warnings.length > 0) {
188
+ yield emit("budget_warning", { warnings: preCheck.warnings });
189
+ }
190
+
191
+ turnCount++;
192
+
193
+ // Heartbeat
194
+ if (turnCount % HEARTBEAT_INTERVAL_TURNS === 0) {
195
+ yield emit("heartbeat", {
196
+ turnsCompleted: turnCount,
197
+ turnsMax: maxTurns,
198
+ findingsSoFar: allFindings.length,
199
+ budgetRemaining: {
200
+ costUsd: Math.max(0, budget.maxCostUsd - ctx.usage.costUsd),
201
+ pct: Math.max(0, 100 - (ctx.usage.costUsd / budget.maxCostUsd * 100)),
202
+ },
203
+ });
204
+ }
205
+
206
+ // Call LLM — format system prompt + messages into a single prompt
207
+ // for the MultiProviderApiClient which uses a completions-style API
208
+ let response;
209
+ try {
210
+ response = await client.invoke({
211
+ prompt: formatPromptForClient(systemPrompt, messages),
212
+ });
213
+ } catch (err) {
214
+ yield emit("llm_error", { error: err.message, turn: turnCount });
215
+ break;
216
+ }
217
+
218
+ const responseText = response.text || "";
219
+ ctx.usage.outputTokens += Math.ceil(responseText.length / 4);
220
+ ctx.usage.costUsd += (Math.ceil(responseText.length / 4) / 1_000_000) * 15;
221
+
222
+ yield emit("reasoning", {
223
+ phase: "deep_analysis",
224
+ turn: turnCount,
225
+ summary: responseText.slice(0, 200),
226
+ });
227
+
228
+ // Parse tool_use blocks
229
+ const toolCalls = parseToolUseBlocks(responseText);
230
+
231
+ if (toolCalls.length === 0) {
232
+ // No tools extract findings from response
233
+ const parsed = extractJsonFindings(responseText);
234
+ for (const finding of parsed) {
235
+ allFindings.push(finding);
236
+ yield emit("finding", { ...finding });
237
+ if (blackboard) {
238
+ try {
239
+ await blackboard.appendEntry({
240
+ agentId: JULES_DEFINITION.id,
241
+ source: "jules-primary",
242
+ ...finding,
243
+ });
244
+ } catch { /* blackboard write failure non-blocking */ }
245
+ }
246
+ }
247
+ messages.push({ role: "assistant", content: responseText });
248
+ break; // LLM is done
249
+ }
250
+
251
+ // Execute tool calls
252
+ const results = [];
253
+ for (const call of toolCalls) {
254
+ try {
255
+ const result = await dispatchTool(call.tool, call.input, ctx);
256
+ results.push({ tool: call.tool, result });
257
+ yield emit("tool_call", { tool: call.tool, input: sanitizeForEvent(call.input) });
258
+ } catch (err) {
259
+ if (err instanceof BudgetExhaustedError) {
260
+ yield emit("budget_stop", { reason: err.message });
261
+ break;
262
+ }
263
+ results.push({ tool: call.tool, error: err.message });
264
+ }
265
+ }
266
+
267
+ // Feed results back
268
+ messages.push({ role: "assistant", content: responseText });
269
+ messages.push({
270
+ role: "user",
271
+ content: results.map(r =>
272
+ r.error
273
+ ? `Tool ${r.tool} failed: ${r.error}`
274
+ : `Tool ${r.tool} result:\n${JSON.stringify(r.result).slice(0, 3000)}`,
275
+ ).join("\n\n") + "\n\nContinue your analysis. If done, return findings in a ```json code block.",
276
+ });
277
+ }
278
+
279
+ // ── Phase 2b: Reconciliation (post-blind-pass) ─────────────────────
280
+ // Now that the independent analysis is complete, cross-reference with
281
+ // swarm findings and Omar baseline. This preserves blind-first: the
282
+ // persona formed its own opinion before seeing prior conclusions.
283
+
284
+ const hasSwarmContext = swarmFindings.length > 0;
285
+ const baselineFindings = omarBaseline
286
+ ? (omarBaseline.findings || omarBaseline.summary || [])
287
+ : [];
288
+ const hasBaselineContext = Array.isArray(baselineFindings) && baselineFindings.length > 0;
289
+
290
+ if (hasSwarmContext || hasBaselineContext) {
291
+ yield emit("progress", { phase: "reconciliation", message: "Cross-referencing with sub-agent and baseline findings..." });
292
+
293
+ const reconcileParts = [];
294
+ reconcileParts.push("Your independent analysis is complete. Now cross-reference with the following prior findings.");
295
+ reconcileParts.push("For each prior finding: confirm if your analysis agrees, dispute with evidence if you disagree, or flag as missed if you did not cover it.");
296
+
297
+ if (hasSwarmContext) {
298
+ reconcileParts.push(`\nYour sub-agents found ${swarmFindings.length} findings:`);
299
+ for (const f of swarmFindings.slice(0, 30)) {
300
+ reconcileParts.push(`- [${f.severity || "P3"}] ${f.file || ""}:${f.line || ""} ${f.title || f.type || ""}`);
301
+ }
302
+ }
303
+
304
+ if (hasBaselineContext) {
305
+ reconcileParts.push(`\nOmar baseline reported ${baselineFindings.length} findings:`);
306
+ for (const f of baselineFindings.slice(0, 20)) {
307
+ reconcileParts.push(`- [${f.severity || ""}] ${f.file || ""}:${f.line || ""} ${f.message || f.title || ""}`);
308
+ }
309
+ }
310
+
311
+ reconcileParts.push("\nReturn any additional or revised findings as a JSON array in a ```json code block. If no changes, return an empty array [].");
312
+
313
+ messages.push({ role: "user", content: reconcileParts.join("\n") });
314
+
315
+ // Budget check before reconciliation turn
316
+ const reconcilePreCheck = evaluateBudget({
317
+ sessionSummary: {
318
+ costUsd: ctx.usage.costUsd,
319
+ outputTokens: ctx.usage.outputTokens,
320
+ durationMs: Date.now() - startedAt,
321
+ toolCalls: ctx.usage.toolCalls,
322
+ },
323
+ ...budget,
324
+ });
325
+
326
+ if (!reconcilePreCheck.blocking) {
327
+ try {
328
+ const reconcileResponse = await client.invoke({
329
+ prompt: formatPromptForClient(systemPrompt, messages),
330
+ });
331
+
332
+ const reconcileText = reconcileResponse.text || "";
333
+ ctx.usage.outputTokens += Math.ceil(reconcileText.length / 4);
334
+ ctx.usage.costUsd += (Math.ceil(reconcileText.length / 4) / 1_000_000) * 15;
335
+
336
+ yield emit("reasoning", { phase: "reconciliation", summary: reconcileText.slice(0, 200) });
337
+
338
+ const reconcileFindings = extractJsonFindings(reconcileText);
339
+ for (const finding of reconcileFindings) {
340
+ allFindings.push(finding);
341
+ yield emit("finding", { ...finding, source: "reconciliation" });
342
+ if (blackboard) {
343
+ try {
344
+ await blackboard.appendEntry({
345
+ agentId: JULES_DEFINITION.id,
346
+ source: "jules-reconciliation",
347
+ ...finding,
348
+ });
349
+ } catch { /* blackboard write failure non-blocking */ }
350
+ }
351
+ }
352
+
353
+ messages.push({ role: "assistant", content: reconcileText });
354
+ } catch (err) {
355
+ yield emit("llm_error", { error: err.message, phase: "reconciliation" });
356
+ }
357
+ } else {
358
+ yield emit("budget_stop", { reasons: reconcilePreCheck.reasons, phase: "reconciliation" });
359
+ }
360
+ }
361
+
362
+ // ── Phase 3: Build final report ───────────────────────────────────
363
+
364
+ const durationMs = Date.now() - startedAt;
365
+ const severityCounts = { P0: 0, P1: 0, P2: 0, P3: 0 };
366
+ for (const f of allFindings) {
367
+ const sev = (f.severity || "P3").toUpperCase();
368
+ if (severityCounts[sev] !== undefined) severityCounts[sev]++;
369
+ else severityCounts.P3++;
370
+ }
371
+
372
+ const report = {
373
+ runId,
374
+ persona: JULES_DEFINITION.persona,
375
+ mode,
376
+ framework: framework.framework || "unknown",
377
+ status: "completed",
378
+ findings: allFindings,
379
+ summary: {
380
+ total: allFindings.length,
381
+ ...severityCounts,
382
+ blocking: severityCounts.P0 > 0 || severityCounts.P1 > 0,
383
+ },
384
+ usage: {
385
+ turns: turnCount,
386
+ costUsd: ctx.usage.costUsd,
387
+ outputTokens: ctx.usage.outputTokens,
388
+ toolCalls: ctx.usage.toolCalls,
389
+ durationMs,
390
+ },
391
+ signature: JULES_DEFINITION.signature,
392
+ };
393
+
394
+ yield emit("agent_complete", {
395
+ ...report.summary,
396
+ costUsd: ctx.usage.costUsd,
397
+ durationMs,
398
+ turns: turnCount,
399
+ });
400
+
401
+ return report;
402
+ }
403
+
404
+ // ── Helpers ──────────────────────────────────────────────────────────
405
+
406
+ function parseToolUseBlocks(text) {
407
+ const calls = [];
408
+ const regex = /```tool_use\s*\n([\s\S]*?)```/g;
409
+ let match;
410
+ while ((match = regex.exec(text)) !== null) {
411
+ try {
412
+ const parsed = JSON.parse(match[1].trim());
413
+ if (parsed.tool && parsed.input) calls.push(parsed);
414
+ } catch { /* skip malformed */ }
415
+ }
416
+ return calls;
417
+ }
418
+
419
+ function extractJsonFindings(text) {
420
+ const jsonMatch = text.match(/```json\s*\n([\s\S]*?)```/);
421
+ if (!jsonMatch) return [];
422
+ try {
423
+ const parsed = JSON.parse(jsonMatch[1].trim());
424
+ if (Array.isArray(parsed)) return parsed;
425
+ if (parsed.findings && Array.isArray(parsed.findings)) return parsed.findings;
426
+ } catch { /* skip malformed */ }
427
+ return [];
428
+ }
429
+
430
+ function sanitizeForEvent(input) {
431
+ const sanitized = { ...input };
432
+ if (typeof sanitized.content === "string" && sanitized.content.length > 200) {
433
+ sanitized.content = `[${sanitized.content.length} chars]`;
434
+ }
435
+ return sanitized;
436
+ }
437
+
438
+ /**
439
+ * Format system prompt + chat messages into a single prompt string
440
+ * for MultiProviderApiClient which uses a completions-style API.
441
+ */
442
+ function formatPromptForClient(systemPrompt, messages) {
443
+ const parts = [];
444
+ if (systemPrompt) parts.push(systemPrompt);
445
+ for (const msg of messages) {
446
+ const role = msg.role === "assistant" ? "ASSISTANT" : "USER";
447
+ parts.push(`\n${role}:\n${msg.content}`);
448
+ }
449
+ return parts.join("\n");
450
+ }