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.
- package/PLATFORM.md +442 -0
- package/README.md +475 -0
- package/SERVER_BEST_PRACTICES.md +62 -0
- package/TELEMETRY.md +108 -0
- package/bin/wolverine.js +95 -0
- package/examples/01-basic-typo.js +31 -0
- package/examples/02-multi-file/routes/users.js +15 -0
- package/examples/02-multi-file/server.js +25 -0
- package/examples/03-syntax-error.js +23 -0
- package/examples/04-secret-leak.js +14 -0
- package/examples/05-expired-key.js +27 -0
- package/examples/06-json-config/config.json +13 -0
- package/examples/06-json-config/server.js +28 -0
- package/examples/07-rate-limit-loop.js +11 -0
- package/examples/08-sandbox-escape.js +20 -0
- package/examples/buggy-server.js +39 -0
- package/examples/demos/01-basic-typo/index.js +20 -0
- package/examples/demos/01-basic-typo/routes/api.js +13 -0
- package/examples/demos/01-basic-typo/routes/health.js +4 -0
- package/examples/demos/02-multi-file/index.js +24 -0
- package/examples/demos/02-multi-file/routes/api.js +13 -0
- package/examples/demos/02-multi-file/routes/health.js +4 -0
- package/examples/demos/03-syntax-error/index.js +18 -0
- package/examples/demos/04-secret-leak/index.js +16 -0
- package/examples/demos/05-expired-key/index.js +21 -0
- package/examples/demos/06-json-config/config.json +9 -0
- package/examples/demos/06-json-config/index.js +20 -0
- package/examples/demos/07-null-crash/index.js +16 -0
- package/examples/run-demo.js +110 -0
- package/package.json +67 -0
- package/server/config/settings.json +62 -0
- package/server/index.js +33 -0
- package/server/routes/api.js +12 -0
- package/server/routes/health.js +16 -0
- package/server/routes/time.js +12 -0
- package/src/agent/agent-engine.js +727 -0
- package/src/agent/goal-loop.js +140 -0
- package/src/agent/research-agent.js +120 -0
- package/src/agent/sub-agents.js +176 -0
- package/src/backup/backup-manager.js +321 -0
- package/src/brain/brain.js +315 -0
- package/src/brain/embedder.js +131 -0
- package/src/brain/function-map.js +263 -0
- package/src/brain/vector-store.js +267 -0
- package/src/core/ai-client.js +387 -0
- package/src/core/cluster-manager.js +144 -0
- package/src/core/config.js +89 -0
- package/src/core/error-parser.js +87 -0
- package/src/core/health-monitor.js +129 -0
- package/src/core/models.js +132 -0
- package/src/core/patcher.js +55 -0
- package/src/core/runner.js +464 -0
- package/src/core/system-info.js +141 -0
- package/src/core/verifier.js +146 -0
- package/src/core/wolverine.js +290 -0
- package/src/dashboard/server.js +1332 -0
- package/src/index.js +94 -0
- package/src/logger/event-logger.js +237 -0
- package/src/logger/pricing.js +96 -0
- package/src/logger/repair-history.js +109 -0
- package/src/logger/token-tracker.js +277 -0
- package/src/mcp/mcp-client.js +224 -0
- package/src/mcp/mcp-registry.js +228 -0
- package/src/mcp/mcp-security.js +152 -0
- package/src/monitor/perf-monitor.js +300 -0
- package/src/monitor/process-monitor.js +231 -0
- package/src/monitor/route-prober.js +191 -0
- package/src/notifications/notifier.js +227 -0
- package/src/platform/heartbeat.js +93 -0
- package/src/platform/queue.js +53 -0
- package/src/platform/register.js +64 -0
- package/src/platform/telemetry.js +76 -0
- package/src/security/admin-auth.js +150 -0
- package/src/security/injection-detector.js +174 -0
- package/src/security/rate-limiter.js +152 -0
- package/src/security/sandbox.js +128 -0
- package/src/security/secret-redactor.js +217 -0
- package/src/skills/skill-registry.js +129 -0
- 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 };
|