wolverine-ai 1.0.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 (79) hide show
  1. package/PLATFORM.md +442 -0
  2. package/README.md +475 -0
  3. package/SERVER_BEST_PRACTICES.md +62 -0
  4. package/TELEMETRY.md +108 -0
  5. package/bin/wolverine.js +95 -0
  6. package/examples/01-basic-typo.js +31 -0
  7. package/examples/02-multi-file/routes/users.js +15 -0
  8. package/examples/02-multi-file/server.js +25 -0
  9. package/examples/03-syntax-error.js +23 -0
  10. package/examples/04-secret-leak.js +14 -0
  11. package/examples/05-expired-key.js +27 -0
  12. package/examples/06-json-config/config.json +13 -0
  13. package/examples/06-json-config/server.js +28 -0
  14. package/examples/07-rate-limit-loop.js +11 -0
  15. package/examples/08-sandbox-escape.js +20 -0
  16. package/examples/buggy-server.js +39 -0
  17. package/examples/demos/01-basic-typo/index.js +20 -0
  18. package/examples/demos/01-basic-typo/routes/api.js +13 -0
  19. package/examples/demos/01-basic-typo/routes/health.js +4 -0
  20. package/examples/demos/02-multi-file/index.js +24 -0
  21. package/examples/demos/02-multi-file/routes/api.js +13 -0
  22. package/examples/demos/02-multi-file/routes/health.js +4 -0
  23. package/examples/demos/03-syntax-error/index.js +18 -0
  24. package/examples/demos/04-secret-leak/index.js +16 -0
  25. package/examples/demos/05-expired-key/index.js +21 -0
  26. package/examples/demos/06-json-config/config.json +9 -0
  27. package/examples/demos/06-json-config/index.js +20 -0
  28. package/examples/demos/07-null-crash/index.js +16 -0
  29. package/examples/run-demo.js +110 -0
  30. package/package.json +67 -0
  31. package/server/config/settings.json +62 -0
  32. package/server/index.js +33 -0
  33. package/server/routes/api.js +12 -0
  34. package/server/routes/health.js +16 -0
  35. package/server/routes/time.js +12 -0
  36. package/src/agent/agent-engine.js +727 -0
  37. package/src/agent/goal-loop.js +140 -0
  38. package/src/agent/research-agent.js +120 -0
  39. package/src/agent/sub-agents.js +176 -0
  40. package/src/backup/backup-manager.js +321 -0
  41. package/src/brain/brain.js +315 -0
  42. package/src/brain/embedder.js +131 -0
  43. package/src/brain/function-map.js +263 -0
  44. package/src/brain/vector-store.js +267 -0
  45. package/src/core/ai-client.js +387 -0
  46. package/src/core/cluster-manager.js +144 -0
  47. package/src/core/config.js +89 -0
  48. package/src/core/error-parser.js +87 -0
  49. package/src/core/health-monitor.js +129 -0
  50. package/src/core/models.js +132 -0
  51. package/src/core/patcher.js +55 -0
  52. package/src/core/runner.js +464 -0
  53. package/src/core/system-info.js +141 -0
  54. package/src/core/verifier.js +146 -0
  55. package/src/core/wolverine.js +290 -0
  56. package/src/dashboard/server.js +1332 -0
  57. package/src/index.js +94 -0
  58. package/src/logger/event-logger.js +237 -0
  59. package/src/logger/pricing.js +96 -0
  60. package/src/logger/repair-history.js +109 -0
  61. package/src/logger/token-tracker.js +277 -0
  62. package/src/mcp/mcp-client.js +224 -0
  63. package/src/mcp/mcp-registry.js +228 -0
  64. package/src/mcp/mcp-security.js +152 -0
  65. package/src/monitor/perf-monitor.js +300 -0
  66. package/src/monitor/process-monitor.js +231 -0
  67. package/src/monitor/route-prober.js +191 -0
  68. package/src/notifications/notifier.js +227 -0
  69. package/src/platform/heartbeat.js +93 -0
  70. package/src/platform/queue.js +53 -0
  71. package/src/platform/register.js +64 -0
  72. package/src/platform/telemetry.js +76 -0
  73. package/src/security/admin-auth.js +150 -0
  74. package/src/security/injection-detector.js +174 -0
  75. package/src/security/rate-limiter.js +152 -0
  76. package/src/security/sandbox.js +128 -0
  77. package/src/security/secret-redactor.js +217 -0
  78. package/src/skills/skill-registry.js +129 -0
  79. package/src/skills/sql.js +375 -0
@@ -0,0 +1,146 @@
1
+ const { spawn } = require("child_process");
2
+ const chalk = require("chalk");
3
+
4
+ /**
5
+ * Fix Verifier — validates that a patch actually fixes the error
6
+ * by running the script in a short-lived probe process.
7
+ *
8
+ * Verification strategies:
9
+ * 1. SYNTAX CHECK: Run `node --check` to verify no syntax errors
10
+ * 2. BOOT PROBE: Start the process and wait for it to either crash or stay alive
11
+ * 3. ERROR MATCH: If it crashes, check if it's the SAME error (fix didn't work)
12
+ * or a DIFFERENT error (fix worked but new problem)
13
+ */
14
+
15
+ // How long to wait for the process to boot before considering it alive
16
+ const BOOT_PROBE_TIMEOUT_MS = 10000; // 10 seconds
17
+
18
+ /**
19
+ * Run a syntax check on a file using `node --check`.
20
+ * Returns { valid: boolean, error?: string }
21
+ */
22
+ function syntaxCheck(scriptPath) {
23
+ return new Promise((resolve) => {
24
+ const child = spawn("node", ["--check", scriptPath], {
25
+ stdio: ["ignore", "ignore", "pipe"],
26
+ timeout: 5000,
27
+ });
28
+
29
+ let stderr = "";
30
+ child.stderr.on("data", (data) => { stderr += data.toString(); });
31
+
32
+ child.on("exit", (code) => {
33
+ resolve({
34
+ valid: code === 0,
35
+ error: code !== 0 ? stderr.trim() : undefined,
36
+ });
37
+ });
38
+
39
+ child.on("error", (err) => {
40
+ resolve({ valid: false, error: err.message });
41
+ });
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Boot probe — start the process and see if it stays alive or crashes.
47
+ *
48
+ * Returns:
49
+ * - { status: "alive" } — process booted and stayed alive for BOOT_PROBE_TIMEOUT_MS
50
+ * - { status: "crashed", stderr, sameError: boolean } — crashed, with comparison to original error
51
+ */
52
+ function bootProbe(scriptPath, cwd, originalErrorSignature) {
53
+ return new Promise((resolve) => {
54
+ let stderr = "";
55
+ let settled = false;
56
+
57
+ // Use an ephemeral port for the probe so it doesn't conflict with the real server
58
+ const probeEnv = { ...process.env, PORT: "0", WOLVERINE_PROBE: "1" };
59
+
60
+ const child = spawn("node", [scriptPath], {
61
+ cwd,
62
+ env: probeEnv,
63
+ stdio: ["ignore", "ignore", "pipe"],
64
+ });
65
+
66
+ child.stderr.on("data", (data) => { stderr += data.toString(); });
67
+
68
+ // If the process crashes within the timeout, the fix may not have worked
69
+ child.on("exit", (code) => {
70
+ if (settled) return;
71
+ settled = true;
72
+
73
+ if (code === 0) {
74
+ resolve({ status: "alive" });
75
+ return;
76
+ }
77
+
78
+ // Check if it's the same error
79
+ const sameError = originalErrorSignature &&
80
+ stderr.includes(originalErrorSignature.split("::").pop().trim());
81
+
82
+ resolve({
83
+ status: "crashed",
84
+ stderr,
85
+ sameError,
86
+ exitCode: code,
87
+ });
88
+ });
89
+
90
+ child.on("error", (err) => {
91
+ if (settled) return;
92
+ settled = true;
93
+ resolve({ status: "crashed", stderr: err.message, sameError: false, exitCode: null });
94
+ });
95
+
96
+ // If the process is still alive after the timeout, consider it good
97
+ setTimeout(() => {
98
+ if (settled) return;
99
+ settled = true;
100
+ child.kill("SIGTERM");
101
+ resolve({ status: "alive" });
102
+ }, BOOT_PROBE_TIMEOUT_MS);
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Full verification pipeline.
108
+ *
109
+ * Returns:
110
+ * - { verified: true, status: "fixed" } — fix works, no crash
111
+ * - { verified: false, status: "same-error" } — same error, fix didn't work → rollback
112
+ * - { verified: false, status: "new-error" } — different error, fix broke something else → rollback
113
+ * - { verified: false, status: "syntax-error" } — introduced syntax error → rollback
114
+ */
115
+ async function verifyFix(scriptPath, cwd, originalErrorSignature) {
116
+ console.log(chalk.yellow("\n🔬 Verifying fix...\n"));
117
+
118
+ // Step 1: Syntax check
119
+ console.log(chalk.gray(" [1/2] Syntax check..."));
120
+ const syntax = await syntaxCheck(scriptPath);
121
+ if (!syntax.valid) {
122
+ console.log(chalk.red(` ❌ Syntax error introduced by fix:\n ${syntax.error}`));
123
+ return { verified: false, status: "syntax-error", error: syntax.error };
124
+ }
125
+ console.log(chalk.green(" ✅ Syntax OK"));
126
+
127
+ // Step 2: Boot probe
128
+ console.log(chalk.gray(" [2/2] Boot probe (watching for crashes)..."));
129
+ const probe = await bootProbe(scriptPath, cwd, originalErrorSignature);
130
+
131
+ if (probe.status === "alive") {
132
+ console.log(chalk.green(" ✅ Process booted successfully and stayed alive."));
133
+ return { verified: true, status: "fixed" };
134
+ }
135
+
136
+ // It crashed
137
+ if (probe.sameError) {
138
+ console.log(chalk.red(" ❌ Same error occurred — fix did not resolve the issue."));
139
+ return { verified: false, status: "same-error", stderr: probe.stderr };
140
+ }
141
+
142
+ console.log(chalk.red(" ❌ A different error occurred — fix may have introduced a new bug."));
143
+ return { verified: false, status: "new-error", stderr: probe.stderr };
144
+ }
145
+
146
+ module.exports = { verifyFix, syntaxCheck, bootProbe, BOOT_PROBE_TIMEOUT_MS };
@@ -0,0 +1,290 @@
1
+ const chalk = require("chalk");
2
+ const { parseError } = require("./error-parser");
3
+ const { requestRepair, getClient } = require("./ai-client");
4
+ const { getModel } = require("./models");
5
+ const { applyPatch } = require("./patcher");
6
+ const { verifyFix } = require("./verifier");
7
+ const { Sandbox, SandboxViolationError } = require("../security/sandbox");
8
+ const { RateLimiter } = require("../security/rate-limiter");
9
+ const { detectInjection } = require("../security/injection-detector");
10
+ const { BackupManager } = require("../backup/backup-manager");
11
+ const { AgentEngine } = require("../agent/agent-engine");
12
+ const { ResearchAgent } = require("../agent/research-agent");
13
+ const { GoalLoop } = require("../agent/goal-loop");
14
+ const { exploreAndFix, spawnParallel } = require("../agent/sub-agents");
15
+ const { EVENT_TYPES } = require("../logger/event-logger");
16
+
17
+ /**
18
+ * The Wolverine healing engine — v3.
19
+ *
20
+ * Two repair modes:
21
+ * 1. FAST PATH: Single-file fix (simple errors, uses CODING_MODEL)
22
+ * 2. AGENT PATH: Multi-file agent with tool use (complex errors, uses REASONING_MODEL)
23
+ *
24
+ * The engine tries fast path first. If that fails verification, it escalates to the agent.
25
+ */
26
+ async function heal({ stderr, cwd, sandbox, redactor, notifier, rateLimiter, backupManager, logger, brain, mcp, skills, repairHistory }) {
27
+ const healStartTime = Date.now();
28
+ // Redact secrets from stderr BEFORE any processing, logging, or AI calls
29
+ const safeStderr = redactor ? redactor.redact(stderr) : stderr;
30
+
31
+ if (logger) logger.info(EVENT_TYPES.HEAL_START, "Wolverine detected a crash", { stderr: safeStderr.slice(0, 500) });
32
+ console.log(chalk.yellow("\n🐺 Wolverine detected a crash. Analyzing...\n"));
33
+
34
+ // 1. Parse the error (use original for file path extraction, redacted for everything else)
35
+ const parsed = parseError(stderr);
36
+ const errorSignature = RateLimiter.signature(parsed.errorMessage, parsed.filePath);
37
+
38
+ // Redact the parsed fields — these go to AI, brain, and logs
39
+ if (redactor) {
40
+ parsed.errorMessage = redactor.redact(parsed.errorMessage);
41
+ parsed.stackTrace = redactor.redact(parsed.stackTrace);
42
+ }
43
+
44
+ if (redactor && redactor.containsSecrets(stderr)) {
45
+ console.log(chalk.yellow(" 🔐 Secrets detected in error output — redacted before AI/brain/logs"));
46
+ }
47
+
48
+ if (logger) logger.debug(EVENT_TYPES.HEAL_PARSE, `Parsed: ${parsed.errorMessage}`, { file: parsed.filePath, line: parsed.line });
49
+
50
+ if (!parsed.filePath) {
51
+ console.log(chalk.red(" Could not identify the source file from the error. Skipping repair."));
52
+ if (logger) logger.error(EVENT_TYPES.HEAL_FAILED, "Could not parse file path from error");
53
+ return { healed: false, explanation: "Could not parse file path from error" };
54
+ }
55
+
56
+ // 2. Sandbox check
57
+ try {
58
+ sandbox.resolve(parsed.filePath);
59
+ } catch (e) {
60
+ if (e instanceof SandboxViolationError) {
61
+ console.log(chalk.red(` 🔒 SANDBOX: ${e.message}`));
62
+ if (logger) logger.error(EVENT_TYPES.SECURITY_SANDBOX_VIOLATION, e.message, { file: parsed.filePath });
63
+ return { healed: false, explanation: "File outside sandbox — access denied" };
64
+ }
65
+ throw e;
66
+ }
67
+
68
+ if (!sandbox.exists(parsed.filePath)) {
69
+ console.log(chalk.red(` Source file not found: ${parsed.filePath}`));
70
+ return { healed: false, explanation: "Source file not found" };
71
+ }
72
+
73
+ console.log(chalk.cyan(` File: ${parsed.filePath}`));
74
+ console.log(chalk.cyan(` Line: ${parsed.line || "unknown"}`));
75
+ console.log(chalk.cyan(` Error: ${parsed.errorMessage}`));
76
+
77
+ // 3. Rate limit check
78
+ const rateCheck = rateLimiter.check(errorSignature);
79
+ if (!rateCheck.allowed) {
80
+ console.log(chalk.red(` ⏱️ ${rateCheck.reason}`));
81
+ if (logger) logger.warn(EVENT_TYPES.SECURITY_RATE_LIMITED, rateCheck.reason, { errorSignature });
82
+ return { healed: false, explanation: rateCheck.reason, waitMs: rateCheck.waitMs };
83
+ }
84
+
85
+ // 4. Prompt injection scan
86
+ console.log(chalk.gray(` 🛡️ Scanning for prompt injection (${getModel("audit")})...`));
87
+ let openaiClient = null;
88
+ try { openaiClient = getClient(); } catch { /* will fail later */ }
89
+
90
+ const injectionResult = await detectInjection(parsed.errorMessage, parsed.stackTrace, { openaiClient });
91
+
92
+ if (logger) logger.info(EVENT_TYPES.HEAL_INJECTION_SCAN, `Injection scan: ${injectionResult.safe ? "clean" : "BLOCKED"}`, injectionResult);
93
+
94
+ if (!injectionResult.safe) {
95
+ console.log(chalk.red(" 🚨 BLOCKED: Potential prompt injection detected."));
96
+ if (logger) logger.critical(EVENT_TYPES.SECURITY_INJECTION_DETECTED, "Injection detected — repair blocked", injectionResult);
97
+ return { healed: false, explanation: "Prompt injection detected — repair blocked" };
98
+ }
99
+ console.log(chalk.green(" ✅ Clean — no injection detected."));
100
+
101
+ // 4b. Check if this is a human-required issue (expired keys, billing, etc.)
102
+ if (notifier) {
103
+ const notification = await notifier.notify(parsed.errorMessage, parsed.stackTrace);
104
+ if (notification) {
105
+ // This is not AI-fixable — don't waste tokens, just notify the human
106
+ return {
107
+ healed: false,
108
+ explanation: `Human action required [${notification.category}]: ${notification.summary}`,
109
+ notification,
110
+ };
111
+ }
112
+ }
113
+
114
+ // 5. Read the source file + get brain context
115
+ const sourceCode = sandbox.readFile(parsed.filePath);
116
+
117
+ let brainContext = "";
118
+ // Inject relevant skill context (claw-code: pre-enrich prompt with matched tools)
119
+ if (skills) {
120
+ const skillCtx = skills.buildContext(parsed.errorMessage);
121
+ if (skillCtx) brainContext += skillCtx + "\n";
122
+ }
123
+ if (brain && brain._initialized) {
124
+ try {
125
+ brainContext += await brain.getContext(parsed.errorMessage);
126
+ if (brainContext) {
127
+ console.log(chalk.gray(` 🧠 Brain + skills: ${brainContext.split("\n").length} lines of context`));
128
+ }
129
+ // Remember the error
130
+ await brain.remember("errors", `Error in ${parsed.filePath}:${parsed.line}: ${parsed.errorMessage}\n${parsed.stackTrace?.slice(0, 300) || ""}`, {
131
+ file: parsed.filePath,
132
+ line: parsed.line,
133
+ error: parsed.errorMessage,
134
+ });
135
+ } catch { /* non-fatal */ }
136
+ }
137
+
138
+ // 6. Research — check past attempts to avoid loops
139
+ const researcher = new ResearchAgent({ brain, logger, redactor });
140
+ let researchContext = "";
141
+ try {
142
+ researchContext = await researcher.buildFixContext(parsed.errorMessage);
143
+ if (researchContext) console.log(chalk.gray(` 🔍 Research: found past context for this error`));
144
+ } catch {}
145
+
146
+ // 7. Goal Loop — set goal, iterate until fixed or exhausted
147
+ // Iteration 1: fast path (CODING_MODEL)
148
+ // Iteration 2: agent path (REASONING_MODEL)
149
+ // Iteration 3: deep research (RESEARCH_MODEL) + agent retry
150
+ const loop = new GoalLoop({
151
+ maxIterations: parseInt(process.env.WOLVERINE_MAX_RETRIES, 10) || 3,
152
+ researcher,
153
+ logger,
154
+ goal: `Fix: ${parsed.errorMessage.slice(0, 80)}`,
155
+
156
+ onAttempt: async (iteration, researchCtx) => {
157
+ // Create backup for this attempt
158
+ // Full server/ backup — includes all files, configs, databases
159
+ const bid = backupManager.createBackup(null);
160
+ backupManager.setErrorSignature(bid, errorSignature);
161
+ if (logger) logger.info(EVENT_TYPES.BACKUP_CREATED, `Backup ${bid} (iteration ${iteration})`, { backupId: bid });
162
+
163
+ const fullContext = [brainContext, researchContext, researchCtx].filter(Boolean).join("\n");
164
+
165
+ let result;
166
+ if (iteration === 1) {
167
+ // Fast path — CODING_MODEL, single file
168
+ console.log(chalk.yellow(` 🧠 Fast path (${getModel("coding")})...`));
169
+ try {
170
+ const repair = await requestRepair({
171
+ filePath: parsed.filePath, sourceCode,
172
+ errorMessage: parsed.errorMessage, stackTrace: parsed.stackTrace,
173
+ });
174
+ rateLimiter.record(errorSignature);
175
+
176
+ const sandboxCheck = sandbox.validateChanges(repair.changes);
177
+ if (!sandboxCheck.valid) throw new Error("Changes outside sandbox");
178
+
179
+ const patchResults = applyPatch(repair.changes, cwd, sandbox);
180
+ if (!patchResults.every(r => r.success)) throw new Error("Patch failed");
181
+
182
+ for (const r of patchResults) console.log(chalk.green(` ✅ Patched: ${r.file}`));
183
+
184
+ const verification = await verifyFix(parsed.filePath, cwd, errorSignature);
185
+ if (verification.verified) {
186
+ backupManager.markVerified(bid);
187
+ rateLimiter.clearSignature(errorSignature);
188
+ return { healed: true, explanation: repair.explanation, backupId: bid, mode: "fast" };
189
+ }
190
+
191
+ backupManager.rollbackTo(bid);
192
+ return { healed: false, explanation: `Fast path: ${verification.status}` };
193
+ } catch (err) {
194
+ backupManager.rollbackTo(bid);
195
+ return { healed: false, explanation: `Fast path error: ${err.message}` };
196
+ }
197
+ } else if (iteration === 2) {
198
+ // Iteration 2: Single agent — REASONING_MODEL
199
+ console.log(chalk.magenta(` 🤖 Agent path (${getModel("reasoning")})...`));
200
+ const agent = new AgentEngine({
201
+ sandbox, logger, cwd, mcp,
202
+ maxTurns: 8,
203
+ maxTokens: 25000,
204
+ });
205
+
206
+ const agentResult = await agent.run({
207
+ errorMessage: parsed.errorMessage, stackTrace: parsed.stackTrace,
208
+ primaryFile: parsed.filePath, sourceCode,
209
+ brainContext: fullContext,
210
+ });
211
+ rateLimiter.record(errorSignature, agentResult.totalTokens);
212
+
213
+ if (agentResult.success && agentResult.filesModified.length > 0) {
214
+ const verification = await verifyFix(parsed.filePath, cwd, errorSignature);
215
+ if (verification.verified) {
216
+ backupManager.markVerified(bid);
217
+ rateLimiter.clearSignature(errorSignature);
218
+ return { healed: true, explanation: agentResult.summary, backupId: bid, mode: "agent", agentStats: agentResult };
219
+ }
220
+ }
221
+
222
+ backupManager.rollbackTo(bid);
223
+ return { healed: false, explanation: agentResult.summary || "Agent could not fix" };
224
+ } else {
225
+ // Iteration 3+: Sub-agents — explore → plan → fix (divide and conquer)
226
+ console.log(chalk.magenta(` 🤖 Sub-agent path (explore → plan → fix)...`));
227
+
228
+ const subResult = await exploreAndFix(
229
+ `Error: ${parsed.errorMessage}\nFile: ${parsed.filePath}\nStack: ${parsed.stackTrace?.slice(0, 300)}`,
230
+ { sandbox, logger, cwd, mcp, brainContext: fullContext }
231
+ );
232
+ rateLimiter.record(errorSignature, subResult.totalTokens);
233
+
234
+ if (subResult.success && subResult.filesModified.length > 0) {
235
+ const verification = await verifyFix(parsed.filePath, cwd, errorSignature);
236
+ if (verification.verified) {
237
+ backupManager.markVerified(bid);
238
+ rateLimiter.clearSignature(errorSignature);
239
+ return { healed: true, explanation: subResult.summary, backupId: bid, mode: "sub-agents", agentStats: subResult };
240
+ }
241
+ }
242
+
243
+ backupManager.rollbackTo(bid);
244
+ return { healed: false, explanation: subResult.summary || "Sub-agents could not fix" };
245
+ }
246
+ },
247
+ });
248
+
249
+ const goalResult = await loop.run({
250
+ errorMessage: parsed.errorMessage,
251
+ filePath: parsed.filePath,
252
+ cwd,
253
+ });
254
+
255
+ backupManager.prune();
256
+
257
+ // Record to repair history
258
+ if (repairHistory) {
259
+ const duration = Date.now() - healStartTime;
260
+ const tokenUsage = goalResult.agentStats?.totalTokens || 0;
261
+ const { calculateCost } = require("../logger/pricing");
262
+ const model = goalResult.mode === "fast" ? getModel("coding") : getModel("reasoning");
263
+ const cost = calculateCost(model, tokenUsage * 0.7, tokenUsage * 0.3); // estimate in/out split
264
+
265
+ repairHistory.record({
266
+ error: parsed.errorMessage,
267
+ file: parsed.filePath,
268
+ line: parsed.line,
269
+ resolution: goalResult.explanation,
270
+ success: goalResult.success,
271
+ mode: goalResult.mode || "unknown",
272
+ model,
273
+ tokens: tokenUsage,
274
+ cost: cost.total,
275
+ iteration: goalResult.iteration,
276
+ duration,
277
+ filesModified: goalResult.agentStats?.filesModified || [],
278
+ });
279
+ }
280
+
281
+ if (goalResult.success) {
282
+ if (logger) logger.info(EVENT_TYPES.HEAL_SUCCESS, goalResult.explanation, { iteration: goalResult.iteration, mode: goalResult.mode });
283
+ return { healed: true, ...goalResult };
284
+ }
285
+
286
+ if (logger) logger.error(EVENT_TYPES.HEAL_FAILED, `Goal failed after ${goalResult.iteration} iterations`, { attempts: goalResult.attempts });
287
+ return { healed: false, explanation: goalResult.explanation };
288
+ }
289
+
290
+ module.exports = { heal };