wolverine-ai 1.3.0 → 1.5.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/README.md +46 -18
- package/package.json +1 -1
- package/src/agent/agent-engine.js +288 -46
- package/src/agent/sub-agents.js +6 -6
- package/src/brain/brain.js +16 -0
- package/src/core/error-hook.js +127 -0
- package/src/core/runner.js +89 -2
- package/src/core/wolverine.js +44 -34
- package/src/dashboard/server.js +2 -0
- package/src/index.js +2 -0
- package/src/monitor/error-monitor.js +121 -0
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ wolverine/
|
|
|
74
74
|
│ │ ├── models.js ← 10-model configuration system
|
|
75
75
|
│ │ ├── verifier.js ← Fix verification (syntax + boot probe)
|
|
76
76
|
│ │ ├── error-parser.js ← Stack trace parsing + error classification
|
|
77
|
+
│ │ ├── error-hook.js ← Auto-injected into child (IPC error reporting)
|
|
77
78
|
│ │ ├── patcher.js ← File patching with sandbox
|
|
78
79
|
│ │ ├── health-monitor.js← PM2-style health checks
|
|
79
80
|
│ │ ├── config.js ← Config loader (settings.json + env)
|
|
@@ -105,7 +106,8 @@ wolverine/
|
|
|
105
106
|
│ ├── monitor/ ← Performance + process management
|
|
106
107
|
│ │ ├── perf-monitor.js ← Endpoint response times + spam detection
|
|
107
108
|
│ │ ├── process-monitor.js← Memory/CPU/heartbeat + leak detection
|
|
108
|
-
│ │
|
|
109
|
+
│ │ ├── route-prober.js ← Auto-discovers and tests all routes
|
|
110
|
+
│ │ └── error-monitor.js ← Caught 500 error detection (no-crash healing)
|
|
109
111
|
│ ├── dashboard/ ← Web UI
|
|
110
112
|
│ │ └── server.js ← Real-time dashboard + command interface
|
|
111
113
|
│ ├── notifications/ ← Alerts
|
|
@@ -176,24 +178,50 @@ After fix:
|
|
|
176
178
|
→ Promote backup to stable after 30min uptime
|
|
177
179
|
```
|
|
178
180
|
|
|
181
|
+
### Caught Error Healing (No-Crash)
|
|
182
|
+
|
|
183
|
+
Most production bugs don't crash the process — Fastify/Express catch them and return 500. Wolverine now detects these too:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
Route returns 500 (process still alive)
|
|
187
|
+
→ Error hook reports to parent via IPC (auto-injected, zero user code changes)
|
|
188
|
+
→ ErrorMonitor tracks consecutive 500s per route
|
|
189
|
+
→ 3 failures in 30s → triggers heal pipeline (same as crash healing)
|
|
190
|
+
→ Fix applied → server restarted → route prober verifies fix
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
| Setting | Default | Env Variable |
|
|
194
|
+
|---------|---------|-------------|
|
|
195
|
+
| Failure threshold | 3 | `WOLVERINE_ERROR_THRESHOLD` |
|
|
196
|
+
| Time window | 30s | `WOLVERINE_ERROR_WINDOW_MS` |
|
|
197
|
+
| Cooldown per route | 60s | `WOLVERINE_ERROR_COOLDOWN_MS` |
|
|
198
|
+
|
|
199
|
+
The error hook auto-patches Fastify and Express via `--require` preload. No middleware, no code changes to your server.
|
|
200
|
+
|
|
179
201
|
---
|
|
180
202
|
|
|
181
203
|
## Agent Tool Harness
|
|
182
204
|
|
|
183
|
-
The AI agent has
|
|
184
|
-
|
|
185
|
-
| Tool |
|
|
186
|
-
|
|
187
|
-
| `read_file` |
|
|
188
|
-
| `write_file` |
|
|
189
|
-
| `edit_file` |
|
|
190
|
-
| `glob_files` |
|
|
191
|
-
| `grep_code` |
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
205
|
+
The AI agent has 16 built-in tools (inspired by [claw-code](https://github.com/ultraworkers/claw-code)):
|
|
206
|
+
|
|
207
|
+
| Tool | Category | Description |
|
|
208
|
+
|------|----------|-------------|
|
|
209
|
+
| `read_file` | File | Read any file with optional offset/limit for large files |
|
|
210
|
+
| `write_file` | File | Write complete file content, creates parent dirs |
|
|
211
|
+
| `edit_file` | File | Surgical find-and-replace without rewriting entire file |
|
|
212
|
+
| `glob_files` | File | Pattern-based file discovery (`**/*.js`, `src/**/*.json`) |
|
|
213
|
+
| `grep_code` | File | Regex search across codebase with context lines |
|
|
214
|
+
| `list_dir` | File | List directory contents with sizes (find misplaced files) |
|
|
215
|
+
| `move_file` | File | Move or rename files (fix structure problems) |
|
|
216
|
+
| `bash_exec` | Shell | Sandboxed shell execution (npm install, chmod, kill, etc.) |
|
|
217
|
+
| `git_log` | Shell | View recent commit history |
|
|
218
|
+
| `git_diff` | Shell | View uncommitted changes |
|
|
219
|
+
| `inspect_db` | Database | List tables, show schema, run SELECT on SQLite databases |
|
|
220
|
+
| `run_db_fix` | Database | UPDATE/DELETE/INSERT/ALTER on SQLite (auto-backup before write) |
|
|
221
|
+
| `check_port` | Diagnostic | Check if a port is in use and by what process |
|
|
222
|
+
| `check_env` | Diagnostic | Check environment variables (values auto-redacted) |
|
|
223
|
+
| `web_fetch` | Research | Fetch URL content for documentation/research |
|
|
224
|
+
| `done` | Control | Signal task completion with summary |
|
|
197
225
|
|
|
198
226
|
**Blocked commands** (from claw-code's `destructiveCommandWarning`):
|
|
199
227
|
`rm -rf /`, `git push --force`, `git reset --hard`, `npm publish`, `curl | bash`, `eval()`
|
|
@@ -209,15 +237,15 @@ For complex repairs, wolverine spawns specialized sub-agents that run in sequenc
|
|
|
209
237
|
|
|
210
238
|
| Agent | Access | Model | Role |
|
|
211
239
|
|-------|--------|-------|------|
|
|
212
|
-
| `explore` | Read
|
|
240
|
+
| `explore` | Read+diagnostics | REASONING | Investigate codebase, check env/ports/databases |
|
|
213
241
|
| `plan` | Read-only | REASONING | Analyze problem, propose fix strategy |
|
|
214
242
|
| `fix` | Read+write+shell | CODING | Execute targeted fix — code edits AND npm install/chmod |
|
|
215
243
|
| `verify` | Read-only | REASONING | Check if fix actually works |
|
|
216
244
|
| `research` | Read-only | RESEARCH | Search brain + web for solutions |
|
|
217
245
|
| `security` | Read-only | AUDIT | Audit code for vulnerabilities |
|
|
218
|
-
| `database` | Read+write | CODING | Database
|
|
246
|
+
| `database` | Read+write+SQL | CODING | Database fixes: inspect_db + run_db_fix + SQL skill |
|
|
219
247
|
|
|
220
|
-
Each sub-agent gets **restricted tools** — the explorer can't write files, the fixer can't search the web. This prevents agents from overstepping their role.
|
|
248
|
+
Each sub-agent gets **restricted tools** — the explorer can't write files, the fixer can't search the web. This prevents agents from overstepping their role. Diagnostic tools (check_port, check_env, inspect_db, list_dir) are available to explorers and planners for investigation.
|
|
221
249
|
|
|
222
250
|
**Workflows:**
|
|
223
251
|
- `exploreAndFix()` — explore → plan → fix (sequential, 3 agents)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -172,6 +172,95 @@ const TOOL_DEFINITIONS = [
|
|
|
172
172
|
},
|
|
173
173
|
},
|
|
174
174
|
},
|
|
175
|
+
// ── DIAGNOSTICS (investigate non-code problems) ──
|
|
176
|
+
{
|
|
177
|
+
type: "function",
|
|
178
|
+
function: {
|
|
179
|
+
name: "list_dir",
|
|
180
|
+
description: "List directory contents with file sizes. Use to check if files exist, find misplaced files, or verify directory structure.",
|
|
181
|
+
parameters: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
path: { type: "string", description: "Relative directory path (default: project root)" },
|
|
185
|
+
},
|
|
186
|
+
required: [],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: "function",
|
|
192
|
+
function: {
|
|
193
|
+
name: "move_file",
|
|
194
|
+
description: "Move or rename a file. Use to fix misplaced files, reorganize structure, or rename incorrectly named files.",
|
|
195
|
+
parameters: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
from: { type: "string", description: "Source relative path" },
|
|
199
|
+
to: { type: "string", description: "Destination relative path" },
|
|
200
|
+
},
|
|
201
|
+
required: ["from", "to"],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: "function",
|
|
207
|
+
function: {
|
|
208
|
+
name: "check_port",
|
|
209
|
+
description: "Check if a port is in use and what process is using it. Use for EADDRINUSE errors.",
|
|
210
|
+
parameters: {
|
|
211
|
+
type: "object",
|
|
212
|
+
properties: {
|
|
213
|
+
port: { type: "number", description: "Port number to check" },
|
|
214
|
+
},
|
|
215
|
+
required: ["port"],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
type: "function",
|
|
221
|
+
function: {
|
|
222
|
+
name: "check_env",
|
|
223
|
+
description: "Check environment variables. Lists all env vars (values redacted) or checks if a specific var is set. Use to diagnose missing config.",
|
|
224
|
+
parameters: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {
|
|
227
|
+
variable: { type: "string", description: "Specific env var to check (optional — omit to list all)" },
|
|
228
|
+
},
|
|
229
|
+
required: [],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
type: "function",
|
|
235
|
+
function: {
|
|
236
|
+
name: "inspect_db",
|
|
237
|
+
description: "Inspect a SQLite database: list tables, describe schema, or run a read-only query. Use for database errors, invalid entries, schema mismatches.",
|
|
238
|
+
parameters: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
db_path: { type: "string", description: "Relative path to .db or .sqlite file" },
|
|
242
|
+
action: { type: "string", description: "Action: 'tables' (list tables), 'schema' (show CREATE statements), 'query' (run read-only SQL)" },
|
|
243
|
+
sql: { type: "string", description: "SQL query (required if action is 'query', must be SELECT/PRAGMA only)" },
|
|
244
|
+
},
|
|
245
|
+
required: ["db_path", "action"],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
type: "function",
|
|
251
|
+
function: {
|
|
252
|
+
name: "run_db_fix",
|
|
253
|
+
description: "Run a write query on a SQLite database to fix data issues: UPDATE invalid entries, DELETE corrupt rows, ALTER schema. Creates a backup first.",
|
|
254
|
+
parameters: {
|
|
255
|
+
type: "object",
|
|
256
|
+
properties: {
|
|
257
|
+
db_path: { type: "string", description: "Relative path to .db or .sqlite file" },
|
|
258
|
+
sql: { type: "string", description: "SQL statement (UPDATE, DELETE, INSERT, ALTER, CREATE)" },
|
|
259
|
+
},
|
|
260
|
+
required: ["db_path", "sql"],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
175
264
|
// ── COMPLETION ──
|
|
176
265
|
{
|
|
177
266
|
type: "function",
|
|
@@ -235,68 +324,92 @@ class AgentEngine {
|
|
|
235
324
|
async run({ errorMessage, stackTrace, primaryFile, sourceCode, brainContext }) {
|
|
236
325
|
const model = getModel("reasoning");
|
|
237
326
|
|
|
238
|
-
const systemPrompt = `You are Wolverine, an autonomous Node.js server repair agent. A server
|
|
327
|
+
const systemPrompt = `You are Wolverine, an autonomous Node.js server repair agent. A server has an error and you must diagnose and fix it.
|
|
328
|
+
|
|
329
|
+
You are NOT just a code editor — you are a full server doctor. Errors can be code bugs, missing dependencies, database problems, misplaced files, configuration issues, port conflicts, permission errors, corrupted state, or environment problems. Use your tools to investigate the ACTUAL root cause before attempting a fix.
|
|
239
330
|
|
|
240
|
-
|
|
331
|
+
## YOUR TOOLS
|
|
241
332
|
|
|
242
333
|
FILE TOOLS:
|
|
243
334
|
- read_file: Read any file (with optional offset/limit for large files)
|
|
244
|
-
- write_file: Write a complete file
|
|
335
|
+
- write_file: Write a complete file (creates parent dirs)
|
|
245
336
|
- edit_file: Surgical find-and-replace (preferred for small fixes)
|
|
246
|
-
- glob_files: Find files by pattern (e.g. "**/*.js", "
|
|
337
|
+
- glob_files: Find files by pattern (e.g. "**/*.js", "server/**/*.json")
|
|
247
338
|
- grep_code: Search code with regex across the project
|
|
339
|
+
- list_dir: List directory contents (check structure, find misplaced files)
|
|
340
|
+
- move_file: Move or rename files (fix misplaced files)
|
|
248
341
|
|
|
249
342
|
SHELL TOOLS:
|
|
250
|
-
- bash_exec: Run any shell command (
|
|
251
|
-
- git_log: View recent commits
|
|
343
|
+
- bash_exec: Run any shell command (npm install, chmod, kill, etc.)
|
|
344
|
+
- git_log: View recent commits (what changed recently?)
|
|
252
345
|
- git_diff: View uncommitted changes
|
|
253
346
|
|
|
347
|
+
DATABASE TOOLS:
|
|
348
|
+
- inspect_db: List tables, show schema, or run SELECT on SQLite databases
|
|
349
|
+
- run_db_fix: Run UPDATE/DELETE/INSERT/ALTER on SQLite databases (backs up first)
|
|
350
|
+
|
|
351
|
+
DIAGNOSTICS:
|
|
352
|
+
- check_port: Check if a port is in use and by what process
|
|
353
|
+
- check_env: Check environment variables (values auto-redacted for security)
|
|
354
|
+
|
|
254
355
|
RESEARCH:
|
|
255
|
-
- web_fetch: Fetch a URL (docs, npm packages,
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
|
270
|
-
|
|
271
|
-
|
|
|
272
|
-
|
|
|
273
|
-
|
|
|
274
|
-
|
|
|
275
|
-
|
|
|
276
|
-
|
|
|
277
|
-
|
|
|
278
|
-
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
356
|
+
- web_fetch: Fetch a URL (docs, npm packages, error solutions)
|
|
357
|
+
|
|
358
|
+
## DIAGNOSIS FLOWCHART — follow this order:
|
|
359
|
+
|
|
360
|
+
1. READ THE ERROR CAREFULLY — what type of problem is this?
|
|
361
|
+
2. If no file path: use glob_files, grep_code, list_dir to investigate
|
|
362
|
+
3. If file path: read_file to see the code, then investigate related files
|
|
363
|
+
|
|
364
|
+
## ERROR → FIX STRATEGY TABLE
|
|
365
|
+
|
|
366
|
+
| Error Pattern | Category | Diagnostic Steps | Fix |
|
|
367
|
+
|---|---|---|---|
|
|
368
|
+
| Cannot find module 'X' | DEPENDENCY | check package.json | bash_exec: npm install X |
|
|
369
|
+
| Cannot find module './X' | IMPORT | glob_files to find real path | edit_file: fix require path |
|
|
370
|
+
| ENOENT: no such file | FILE MISSING | list_dir to check structure | write_file or move_file |
|
|
371
|
+
| EACCES/EPERM | PERMISSION | bash_exec: ls -la | bash_exec: chmod 755 |
|
|
372
|
+
| EADDRINUSE | PORT | check_port to find blocker | bash_exec: kill PID, or edit config |
|
|
373
|
+
| ECONNREFUSED | SERVICE DOWN | check if DB/service is running | bash_exec: start service |
|
|
374
|
+
| SyntaxError | CODE | read_file to see context | edit_file: fix syntax |
|
|
375
|
+
| TypeError/ReferenceError | CODE | read_file + grep_code | edit_file: fix logic |
|
|
376
|
+
| ER_NO_SUCH_TABLE | DATABASE | inspect_db: tables | run_db_fix: CREATE TABLE or bash_exec migration |
|
|
377
|
+
| SQLITE_ERROR/CONSTRAINT | DATABASE | inspect_db: schema + query | run_db_fix: UPDATE/ALTER |
|
|
378
|
+
| Invalid JSON | CONFIG | read_file the JSON | edit_file: fix JSON syntax |
|
|
379
|
+
| ENOMEM / heap out of memory | RESOURCE | check_env for NODE_OPTIONS | edit config or bash_exec: increase limit |
|
|
380
|
+
| Missing env variable | CONFIG | check_env | write_file .env or edit config |
|
|
381
|
+
| Wrong file location | STRUCTURE | list_dir + glob_files | move_file to correct location |
|
|
382
|
+
| Corrupted node_modules | DEPENDENCY | bash_exec: ls node_modules | bash_exec: rm -rf node_modules && npm install |
|
|
383
|
+
| Git conflict markers | CODE | grep_code: <<<<<<< | edit_file: resolve conflicts |
|
|
384
|
+
|
|
385
|
+
## RULES
|
|
386
|
+
|
|
387
|
+
1. INVESTIGATE FIRST — never guess. Read files, check directories, inspect databases before fixing.
|
|
388
|
+
2. Read files before modifying them. Check package.json before editing imports.
|
|
389
|
+
3. Make minimal, targeted changes — fix the root cause, not symptoms.
|
|
390
|
+
4. Use the right tool: bash_exec for operational fixes, edit_file for code, run_db_fix for data.
|
|
391
|
+
5. You can edit ANY file type: .js, .json, .sql, .yaml, .env, .toml, .sh, .dockerfile, etc.
|
|
392
|
+
6. If the error has no file path, USE YOUR TOOLS to find the problem (glob, grep, list_dir, inspect_db).
|
|
393
|
+
7. When done, call the "done" tool with a summary of what you found and fixed.
|
|
394
|
+
|
|
395
|
+
Project root: ${this.cwd}${primaryFile ? `\nPrimary crash file: ${primaryFile}` : ""}`;
|
|
396
|
+
|
|
397
|
+
// Build user message — handle cases with and without a specific file
|
|
398
|
+
let userContent = `The server has an error:\n\n**Error:** ${errorMessage}\n\n**Stack Trace:**\n\`\`\`\n${stackTrace}\n\`\`\``;
|
|
399
|
+
if (primaryFile && sourceCode) {
|
|
400
|
+
userContent += `\n\n**Primary file (${primaryFile}):**\n\`\`\`\n${sourceCode}\n\`\`\``;
|
|
401
|
+
} else if (!primaryFile) {
|
|
402
|
+
userContent += `\n\n**No specific file identified.** Use your investigation tools (glob_files, grep_code, list_dir, inspect_db, check_env, check_port) to find the root cause.`;
|
|
403
|
+
}
|
|
404
|
+
if (brainContext) userContent += `\n\n**Context from Wolverine Brain:**\n${brainContext}`;
|
|
405
|
+
userContent += `\n\nDiagnose the root cause, investigate with your tools, and fix the issue.`;
|
|
290
406
|
|
|
291
407
|
this.messages = [
|
|
292
408
|
{ role: "system", content: systemPrompt },
|
|
293
|
-
{
|
|
294
|
-
role: "user",
|
|
295
|
-
content: `The server crashed with this error:\n\n**Error:** ${errorMessage}\n\n**Stack Trace:**\n\`\`\`\n${stackTrace}\n\`\`\`\n\n**Primary file (${primaryFile}):**\n\`\`\`\n${sourceCode}\n\`\`\`${brainContext ? `\n\n**Context from Wolverine Brain:**\n${brainContext}` : ""}\n\nAnalyze the error, explore any related files you need, and fix the issue. Use your tools.`,
|
|
296
|
-
},
|
|
409
|
+
{ role: "user", content: userContent },
|
|
297
410
|
];
|
|
298
411
|
|
|
299
|
-
this.filesRead.add(primaryFile);
|
|
412
|
+
if (primaryFile) this.filesRead.add(primaryFile);
|
|
300
413
|
|
|
301
414
|
// Merge MCP tools with built-in tools
|
|
302
415
|
const allTools = [...TOOL_DEFINITIONS];
|
|
@@ -411,6 +524,12 @@ Primary crash file: ${primaryFile}`;
|
|
|
411
524
|
case "git_log": return this._gitLog(args);
|
|
412
525
|
case "git_diff": return this._gitDiff(args);
|
|
413
526
|
case "web_fetch": return this._webFetch(args);
|
|
527
|
+
case "list_dir": return this._listDir(args);
|
|
528
|
+
case "move_file": return this._moveFile(args);
|
|
529
|
+
case "check_port": return this._checkPort(args);
|
|
530
|
+
case "check_env": return this._checkEnv(args);
|
|
531
|
+
case "inspect_db": return this._inspectDb(args);
|
|
532
|
+
case "run_db_fix": return this._runDbFix(args);
|
|
414
533
|
case "done": return this._done(args);
|
|
415
534
|
// Legacy aliases
|
|
416
535
|
case "list_files": return this._globFiles({ pattern: (args.dir || ".") + "/*" + (args.pattern || "") });
|
|
@@ -690,6 +809,129 @@ Primary crash file: ${primaryFile}`;
|
|
|
690
809
|
|
|
691
810
|
// ── COMPLETION ──
|
|
692
811
|
|
|
812
|
+
// ── DIAGNOSTIC TOOLS ──
|
|
813
|
+
|
|
814
|
+
_listDir(args) {
|
|
815
|
+
const dirPath = path.resolve(this.cwd, args.path || ".");
|
|
816
|
+
try {
|
|
817
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
818
|
+
const lines = entries.map(e => {
|
|
819
|
+
try {
|
|
820
|
+
const stat = fs.statSync(path.join(dirPath, e.name));
|
|
821
|
+
const size = e.isDirectory() ? "DIR" : `${Math.round(stat.size / 1024)}KB`;
|
|
822
|
+
return `${e.isDirectory() ? "📁" : "📄"} ${e.name} (${size})`;
|
|
823
|
+
} catch { return `${e.name} (?)` ; }
|
|
824
|
+
});
|
|
825
|
+
console.log(chalk.gray(` 📁 Listed ${lines.length} entries in ${args.path || "."}`));
|
|
826
|
+
return { content: lines.join("\n") || "(empty directory)" };
|
|
827
|
+
} catch (e) { return { content: `Error: ${e.message}` }; }
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
_moveFile(args) {
|
|
831
|
+
if (this._isProtectedPath(args.from) || this._isProtectedPath(args.to)) {
|
|
832
|
+
return { content: "BLOCKED: Cannot move protected files" };
|
|
833
|
+
}
|
|
834
|
+
const from = path.resolve(this.cwd, args.from);
|
|
835
|
+
const to = path.resolve(this.cwd, args.to);
|
|
836
|
+
try {
|
|
837
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
838
|
+
fs.renameSync(from, to);
|
|
839
|
+
this.filesModified.push(args.to);
|
|
840
|
+
console.log(chalk.green(` 📦 Moved: ${args.from} → ${args.to}`));
|
|
841
|
+
return { content: `Moved ${args.from} → ${args.to}` };
|
|
842
|
+
} catch (e) { return { content: `Error moving: ${e.message}` }; }
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
_checkPort(args) {
|
|
846
|
+
const port = args.port;
|
|
847
|
+
try {
|
|
848
|
+
const platform = process.platform;
|
|
849
|
+
let cmd;
|
|
850
|
+
if (platform === "win32") {
|
|
851
|
+
cmd = `netstat -ano | findstr :${port}`;
|
|
852
|
+
} else {
|
|
853
|
+
cmd = `lsof -i :${port} 2>/dev/null || ss -tlnp 2>/dev/null | grep :${port}`;
|
|
854
|
+
}
|
|
855
|
+
const result = execSync(cmd, { timeout: 5000, stdio: "pipe" }).toString().trim();
|
|
856
|
+
console.log(chalk.gray(` 🔌 Port ${port}: ${result ? "IN USE" : "free"}`));
|
|
857
|
+
return { content: result || `Port ${port} is free` };
|
|
858
|
+
} catch { return { content: `Port ${port} appears free (no listeners found)` }; }
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
_checkEnv(args) {
|
|
862
|
+
const { redact } = require("../security/secret-redactor");
|
|
863
|
+
if (args.variable) {
|
|
864
|
+
const val = process.env[args.variable];
|
|
865
|
+
const display = val ? redact(val) : "(not set)";
|
|
866
|
+
return { content: `${args.variable}=${display}` };
|
|
867
|
+
}
|
|
868
|
+
// List all env vars with redacted values
|
|
869
|
+
const keys = Object.keys(process.env).sort();
|
|
870
|
+
const lines = keys.map(k => {
|
|
871
|
+
const val = process.env[k];
|
|
872
|
+
return `${k}=${val && val.length > 50 ? "(set, " + val.length + " chars)" : redact(val || "")}`;
|
|
873
|
+
});
|
|
874
|
+
return { content: lines.join("\n") };
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
_inspectDb(args) {
|
|
878
|
+
const dbPath = path.resolve(this.cwd, args.db_path);
|
|
879
|
+
try {
|
|
880
|
+
let Database;
|
|
881
|
+
try { Database = require("better-sqlite3"); } catch {
|
|
882
|
+
return { content: "better-sqlite3 not installed. Run: npm install better-sqlite3" };
|
|
883
|
+
}
|
|
884
|
+
const db = new Database(dbPath, { readonly: true });
|
|
885
|
+
let result;
|
|
886
|
+
if (args.action === "tables") {
|
|
887
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all();
|
|
888
|
+
result = tables.map(t => t.name).join("\n") || "(no tables)";
|
|
889
|
+
} else if (args.action === "schema") {
|
|
890
|
+
const schemas = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND sql IS NOT NULL").all();
|
|
891
|
+
result = schemas.map(s => s.sql).join("\n\n") || "(no tables)";
|
|
892
|
+
} else if (args.action === "query") {
|
|
893
|
+
if (!args.sql) return { content: "Error: sql required for query action" };
|
|
894
|
+
const upper = args.sql.trim().toUpperCase();
|
|
895
|
+
if (!upper.startsWith("SELECT") && !upper.startsWith("PRAGMA")) {
|
|
896
|
+
return { content: "BLOCKED: inspect_db only allows SELECT/PRAGMA. Use run_db_fix for writes." };
|
|
897
|
+
}
|
|
898
|
+
const rows = db.prepare(args.sql).all();
|
|
899
|
+
result = JSON.stringify(rows.slice(0, 50), null, 2);
|
|
900
|
+
if (rows.length > 50) result += `\n... (${rows.length} total rows, showing first 50)`;
|
|
901
|
+
} else {
|
|
902
|
+
result = "Unknown action. Use: tables, schema, or query";
|
|
903
|
+
}
|
|
904
|
+
db.close();
|
|
905
|
+
const { redact } = require("../security/secret-redactor");
|
|
906
|
+
console.log(chalk.gray(` 🗃️ DB ${args.action}: ${args.db_path}`));
|
|
907
|
+
return { content: redact(result) };
|
|
908
|
+
} catch (e) { return { content: `DB error: ${e.message}` }; }
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
_runDbFix(args) {
|
|
912
|
+
const dbPath = path.resolve(this.cwd, args.db_path);
|
|
913
|
+
try {
|
|
914
|
+
let Database;
|
|
915
|
+
try { Database = require("better-sqlite3"); } catch {
|
|
916
|
+
return { content: "better-sqlite3 not installed. Run: npm install better-sqlite3" };
|
|
917
|
+
}
|
|
918
|
+
// Block dangerous operations
|
|
919
|
+
const upper = args.sql.trim().toUpperCase();
|
|
920
|
+
if (upper.startsWith("DROP DATABASE") || upper.includes("DROP TABLE sqlite_")) {
|
|
921
|
+
return { content: "BLOCKED: Cannot drop system tables" };
|
|
922
|
+
}
|
|
923
|
+
// Backup the DB file first
|
|
924
|
+
const backupPath = dbPath + ".wolverine-backup";
|
|
925
|
+
fs.copyFileSync(dbPath, backupPath);
|
|
926
|
+
const db = new Database(dbPath);
|
|
927
|
+
const result = db.prepare(args.sql).run();
|
|
928
|
+
db.close();
|
|
929
|
+
this.filesModified.push(args.db_path);
|
|
930
|
+
console.log(chalk.green(` 🗃️ DB fix applied: ${args.sql.slice(0, 60)} (changes: ${result.changes})`));
|
|
931
|
+
return { content: `SQL executed. Changes: ${result.changes}. Backup at: ${backupPath}` };
|
|
932
|
+
} catch (e) { return { content: `DB error: ${e.message}` }; }
|
|
933
|
+
}
|
|
934
|
+
|
|
693
935
|
_done(args) {
|
|
694
936
|
console.log(chalk.green(` ✅ Agent done: ${args.summary}`));
|
|
695
937
|
if (this.logger) {
|
package/src/agent/sub-agents.js
CHANGED
|
@@ -23,13 +23,13 @@ const { getModel } = require("../core/models");
|
|
|
23
23
|
|
|
24
24
|
// Tool restrictions per agent type (claw-code: allowed_tools_for_subagent)
|
|
25
25
|
const AGENT_TOOL_SETS = {
|
|
26
|
-
explore: ["read_file", "glob_files", "grep_code", "git_log", "git_diff", "done"],
|
|
27
|
-
plan: ["read_file", "glob_files", "grep_code", "search_brain", "done"],
|
|
28
|
-
fix: ["read_file", "write_file", "edit_file", "glob_files", "grep_code", "bash_exec", "done"],
|
|
29
|
-
verify: ["read_file", "glob_files", "grep_code", "bash_exec", "done"],
|
|
26
|
+
explore: ["read_file", "glob_files", "grep_code", "git_log", "git_diff", "list_dir", "check_env", "check_port", "inspect_db", "done"],
|
|
27
|
+
plan: ["read_file", "glob_files", "grep_code", "list_dir", "inspect_db", "check_env", "search_brain", "done"],
|
|
28
|
+
fix: ["read_file", "write_file", "edit_file", "glob_files", "grep_code", "bash_exec", "move_file", "run_db_fix", "done"],
|
|
29
|
+
verify: ["read_file", "glob_files", "grep_code", "bash_exec", "inspect_db", "check_port", "done"],
|
|
30
30
|
research: ["read_file", "grep_code", "web_fetch", "search_brain", "done"],
|
|
31
|
-
security: ["read_file", "glob_files", "grep_code", "done"],
|
|
32
|
-
database: ["read_file", "write_file", "edit_file", "glob_files", "grep_code", "bash_exec", "done"],
|
|
31
|
+
security: ["read_file", "glob_files", "grep_code", "inspect_db", "done"],
|
|
32
|
+
database: ["read_file", "write_file", "edit_file", "glob_files", "grep_code", "bash_exec", "inspect_db", "run_db_fix", "done"],
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
// Default model + budget per agent type
|
package/src/brain/brain.js
CHANGED
|
@@ -211,6 +211,22 @@ const SEED_DOCS = [
|
|
|
211
211
|
text: "Agent fix strategy table: the agent system prompt includes a decision table mapping error patterns to correct fix actions. Cannot find module 'X' (package) → bash_exec: npm install X. Cannot find module './X' (local) → edit_file: fix require path. ENOENT → write_file: create missing file. EACCES → bash_exec: chmod. EADDRINUSE → bash_exec: kill process. SyntaxError → edit_file: fix code. TypeError → edit_file: fix logic. MODULE_NOT_FOUND + node_modules → bash_exec: rm -rf node_modules && npm install. The fast path AI response format now supports both 'changes' (code edits) and 'commands' (shell commands like npm install). Dangerous commands blocked: rm -rf /, format, mkfs.",
|
|
212
212
|
metadata: { topic: "agent-fix-strategy" },
|
|
213
213
|
},
|
|
214
|
+
{
|
|
215
|
+
text: "Error Monitor: detects caught 500 errors that don't crash the process. Most production bugs are caught by Fastify/Express error handlers — the server stays alive but routes return 500. Wolverine's crash-based heal pipeline never triggers for these. ErrorMonitor tracks 5xx errors per route via IPC from child process. After N consecutive 500s within a time window (default: 3 failures in 30s), triggers the heal pipeline without killing the server. Error hook auto-injected via --require preload (no user code changes). Cooldown prevents heal spam (default: 60s per route). Stats available in dashboard and telemetry. Config: WOLVERINE_ERROR_THRESHOLD, WOLVERINE_ERROR_WINDOW_MS, WOLVERINE_ERROR_COOLDOWN_MS.",
|
|
216
|
+
metadata: { topic: "error-monitor" },
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
text: "Agent tool harness v2: 16 built-in tools. FILE: read_file, write_file, edit_file, glob_files, grep_code, list_dir, move_file. SHELL: bash_exec, git_log, git_diff. DATABASE: inspect_db (list tables, show schema, run SELECT), run_db_fix (UPDATE/DELETE/INSERT/ALTER with auto-backup). DIAGNOSTICS: check_port (find what's using a port), check_env (list/check env vars, values redacted). RESEARCH: web_fetch. COMPLETION: done. Sub-agents get restricted sets: explorer gets diagnostics (list_dir, check_env, check_port, inspect_db), fixer gets action tools (bash_exec, move_file, run_db_fix), verifier gets inspection tools.",
|
|
220
|
+
metadata: { topic: "agent-tools-v2" },
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
text: "Server problem categories the agent can fix: CODE BUGS (SyntaxError, TypeError, ReferenceError → edit_file), DEPENDENCIES (Cannot find module → npm install, corrupted node_modules → rm + reinstall), DATABASE (invalid entries → run_db_fix UPDATE, missing table → CREATE TABLE, schema mismatch → ALTER TABLE, constraint violation → fix data or schema), CONFIG (invalid JSON → edit_file, missing env vars → write .env, wrong port → edit config), FILESYSTEM (misplaced files → move_file, missing directories → bash_exec mkdir, wrong permissions → chmod), NETWORK (port conflict → check_port + kill, service down → restart, connection refused → check config), STATE (corrupted cache → delete + restart, stale locks → remove lock file, git conflicts → resolve markers). The agent investigates before fixing — reads files, checks directories, inspects databases, never guesses.",
|
|
224
|
+
metadata: { topic: "server-problems" },
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
text: "Heal pipeline no longer requires a file path. When no file is identified from the error (database errors, config problems, port conflicts), the pipeline skips fast path and goes straight to the agent, which uses investigation tools (glob_files, grep_code, list_dir, inspect_db, check_env, check_port) to find the root cause. Agent verification for no-file errors: if agent made changes or ran commands, trust the agent's assessment. For file-based errors, verification uses syntax check + boot probe as before.",
|
|
228
|
+
metadata: { topic: "fileless-heal" },
|
|
229
|
+
},
|
|
214
230
|
];
|
|
215
231
|
|
|
216
232
|
class Brain {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Hook — preloaded into the child server process via --require.
|
|
3
|
+
*
|
|
4
|
+
* Patches Fastify and Express error handlers to report caught errors
|
|
5
|
+
* back to the Wolverine parent process via IPC. This enables healing
|
|
6
|
+
* of 500 errors that don't crash the process.
|
|
7
|
+
*
|
|
8
|
+
* How it works:
|
|
9
|
+
* 1. Runner spawns child with: node --require ./src/core/error-hook.js server/index.js
|
|
10
|
+
* 2. This file hooks into Module._load to intercept fastify/express creation
|
|
11
|
+
* 3. When a framework instance is created, we add an error handler that sends IPC messages
|
|
12
|
+
* 4. Parent's ErrorMonitor receives the messages and triggers heal after threshold
|
|
13
|
+
*
|
|
14
|
+
* Zero changes to user's server code.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const Module = require("module");
|
|
18
|
+
const originalLoad = Module._load;
|
|
19
|
+
|
|
20
|
+
let _hooked = false;
|
|
21
|
+
|
|
22
|
+
Module._load = function (request, parent, isMain) {
|
|
23
|
+
const result = originalLoad.apply(this, arguments);
|
|
24
|
+
|
|
25
|
+
// Hook Fastify
|
|
26
|
+
if (request === "fastify" && typeof result === "function" && !_hooked) {
|
|
27
|
+
const originalFastify = result;
|
|
28
|
+
const wrapped = function (...args) {
|
|
29
|
+
const instance = originalFastify(...args);
|
|
30
|
+
_hookFastify(instance);
|
|
31
|
+
return instance;
|
|
32
|
+
};
|
|
33
|
+
// Preserve all properties (fastify.default, etc.)
|
|
34
|
+
Object.keys(originalFastify).forEach((key) => {
|
|
35
|
+
wrapped[key] = originalFastify[key];
|
|
36
|
+
});
|
|
37
|
+
_hooked = true;
|
|
38
|
+
return wrapped;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Hook Express
|
|
42
|
+
if (request === "express" && typeof result === "function" && !_hooked) {
|
|
43
|
+
const originalExpress = result;
|
|
44
|
+
const wrapped = function (...args) {
|
|
45
|
+
const app = originalExpress(...args);
|
|
46
|
+
_hookExpress(app);
|
|
47
|
+
return app;
|
|
48
|
+
};
|
|
49
|
+
Object.keys(originalExpress).forEach((key) => {
|
|
50
|
+
wrapped[key] = originalExpress[key];
|
|
51
|
+
});
|
|
52
|
+
_hooked = true;
|
|
53
|
+
return wrapped;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function _hookFastify(fastify) {
|
|
60
|
+
// Use onReady to add hooks after all plugins are loaded
|
|
61
|
+
fastify.addHook("onReady", function (done) {
|
|
62
|
+
// Add a global error handler that reports to parent
|
|
63
|
+
fastify.addHook("onError", function (request, reply, error, done) {
|
|
64
|
+
_reportError(request.url, request.method, error);
|
|
65
|
+
done();
|
|
66
|
+
});
|
|
67
|
+
done();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Also intercept the setErrorHandler if user sets one
|
|
71
|
+
const originalSetError = fastify.setErrorHandler.bind(fastify);
|
|
72
|
+
fastify.setErrorHandler = function (handler) {
|
|
73
|
+
return originalSetError(function (error, request, reply) {
|
|
74
|
+
_reportError(request.url, request.method, error);
|
|
75
|
+
return handler(error, request, reply);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _hookExpress(app) {
|
|
81
|
+
// For Express, we monkey-patch app.use to detect error middleware
|
|
82
|
+
// and also add our own at the end via a delayed hook
|
|
83
|
+
const originalListen = app.listen.bind(app);
|
|
84
|
+
app.listen = function (...args) {
|
|
85
|
+
// Add our error handler AFTER all user middleware
|
|
86
|
+
app.use(function wolverineErrorHandler(err, req, res, next) {
|
|
87
|
+
_reportError(req.originalUrl || req.url, req.method, err);
|
|
88
|
+
next(err);
|
|
89
|
+
});
|
|
90
|
+
return originalListen(...args);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _reportError(url, method, error) {
|
|
95
|
+
if (!process.send) return; // No IPC channel — not spawned by wolverine
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Extract file/line from stack trace
|
|
99
|
+
let file = null;
|
|
100
|
+
let line = null;
|
|
101
|
+
if (error && error.stack) {
|
|
102
|
+
const stackLines = error.stack.split("\n");
|
|
103
|
+
for (const sl of stackLines) {
|
|
104
|
+
const match = sl.match(/\(([^)]+):(\d+):(\d+)\)/) || sl.match(/at\s+([^\s(]+):(\d+):(\d+)/);
|
|
105
|
+
if (match && !match[1].includes("node_modules") && !match[1].includes("node:")) {
|
|
106
|
+
file = match[1];
|
|
107
|
+
line = parseInt(match[2], 10);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.send({
|
|
114
|
+
type: "route_error",
|
|
115
|
+
path: url,
|
|
116
|
+
method: method || "GET",
|
|
117
|
+
statusCode: 500,
|
|
118
|
+
message: error?.message || "Unknown error",
|
|
119
|
+
stack: error?.stack?.slice(0, 2000) || "",
|
|
120
|
+
file,
|
|
121
|
+
line,
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
// Silently fail — don't break the server for IPC issues
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/core/runner.js
CHANGED
|
@@ -20,6 +20,7 @@ const { ProcessMonitor } = require("../monitor/process-monitor");
|
|
|
20
20
|
const { RouteProber } = require("../monitor/route-prober");
|
|
21
21
|
const { startHeartbeat, stopHeartbeat } = require("../platform/heartbeat");
|
|
22
22
|
const { Notifier } = require("../notifications/notifier");
|
|
23
|
+
const { ErrorMonitor } = require("../monitor/error-monitor");
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* The Wolverine process runner — v3.
|
|
@@ -90,6 +91,15 @@ class WolverineRunner {
|
|
|
90
91
|
brain: this.brain,
|
|
91
92
|
});
|
|
92
93
|
|
|
94
|
+
// Error monitor — detects caught 500 errors without process crash
|
|
95
|
+
this.errorMonitor = new ErrorMonitor({
|
|
96
|
+
threshold: parseInt(process.env.WOLVERINE_ERROR_THRESHOLD, 10) || 3,
|
|
97
|
+
windowMs: parseInt(process.env.WOLVERINE_ERROR_WINDOW_MS, 10) || 30000,
|
|
98
|
+
cooldownMs: parseInt(process.env.WOLVERINE_ERROR_COOLDOWN_MS, 10) || 60000,
|
|
99
|
+
logger: this.logger,
|
|
100
|
+
onError: (routePath, errorDetails) => this._healFromError(routePath, errorDetails),
|
|
101
|
+
});
|
|
102
|
+
|
|
93
103
|
// Brain — semantic memory + project context
|
|
94
104
|
this.brain = new Brain(this.cwd);
|
|
95
105
|
|
|
@@ -120,6 +130,7 @@ class WolverineRunner {
|
|
|
120
130
|
repairHistory: this.repairHistory,
|
|
121
131
|
processMonitor: this.processMonitor,
|
|
122
132
|
routeProber: this.routeProber,
|
|
133
|
+
errorMonitor: this.errorMonitor,
|
|
123
134
|
});
|
|
124
135
|
|
|
125
136
|
// Stability tracking
|
|
@@ -287,10 +298,13 @@ class WolverineRunner {
|
|
|
287
298
|
this._stderrBuffer = "";
|
|
288
299
|
this._lastStartTime = Date.now();
|
|
289
300
|
|
|
290
|
-
|
|
301
|
+
// Spawn with --require error-hook.js for IPC error reporting
|
|
302
|
+
// The error hook auto-patches Fastify/Express to report caught 500s
|
|
303
|
+
const errorHookPath = path.join(__dirname, "error-hook.js");
|
|
304
|
+
this.child = spawn("node", ["--require", errorHookPath, this.scriptPath], {
|
|
291
305
|
cwd: this.cwd,
|
|
292
306
|
env: { ...process.env },
|
|
293
|
-
stdio: ["inherit", "inherit", "pipe"],
|
|
307
|
+
stdio: ["inherit", "inherit", "pipe", "ipc"],
|
|
294
308
|
});
|
|
295
309
|
|
|
296
310
|
this.child.stderr.on("data", (data) => {
|
|
@@ -367,6 +381,30 @@ class WolverineRunner {
|
|
|
367
381
|
this.logger.error(EVENT_TYPES.PROCESS_CRASH, `Failed to start: ${err.message}`);
|
|
368
382
|
this.running = false;
|
|
369
383
|
});
|
|
384
|
+
|
|
385
|
+
// IPC channel: child reports caught 500 errors (Fastify/Express)
|
|
386
|
+
this.child.on("message", (msg) => {
|
|
387
|
+
if (msg && msg.type === "route_error") {
|
|
388
|
+
const { redact } = require("../security/secret-redactor");
|
|
389
|
+
const safeMsg = redact(msg.message || "");
|
|
390
|
+
const safeStack = redact(msg.stack || "");
|
|
391
|
+
console.log(chalk.yellow(` 🔍 Caught error on ${msg.method} ${msg.path}: ${safeMsg.slice(0, 100)}`));
|
|
392
|
+
this.logger.warn("error_monitor.caught", `${msg.method} ${msg.path} → 500: ${safeMsg.slice(0, 200)}`, {
|
|
393
|
+
route: msg.path, method: msg.method, file: msg.file, line: msg.line,
|
|
394
|
+
});
|
|
395
|
+
this.errorMonitor.record(msg.path, msg.statusCode || 500, {
|
|
396
|
+
message: safeMsg,
|
|
397
|
+
stack: safeStack,
|
|
398
|
+
file: msg.file,
|
|
399
|
+
line: msg.line,
|
|
400
|
+
path: msg.path,
|
|
401
|
+
method: msg.method,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Reset error monitor on new spawn
|
|
407
|
+
this.errorMonitor.reset();
|
|
370
408
|
}
|
|
371
409
|
|
|
372
410
|
async _healAndRestart() {
|
|
@@ -432,6 +470,55 @@ class WolverineRunner {
|
|
|
432
470
|
}
|
|
433
471
|
}
|
|
434
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Heal from a caught 500 error (ErrorMonitor threshold reached).
|
|
475
|
+
* Unlike crash healing, the server is still running — we heal and restart.
|
|
476
|
+
*/
|
|
477
|
+
async _healFromError(routePath, errorDetails) {
|
|
478
|
+
if (this._healInProgress || this._shuttingDown) return;
|
|
479
|
+
this._healInProgress = true;
|
|
480
|
+
|
|
481
|
+
console.log(chalk.yellow(`\n🐺 Wolverine healing caught error on ${routePath}...`));
|
|
482
|
+
this.logger.info("heal.error_monitor", `Healing caught 500 on ${routePath}`, { route: routePath });
|
|
483
|
+
|
|
484
|
+
// Build a synthetic stderr from the error details
|
|
485
|
+
const stderr = [
|
|
486
|
+
errorDetails.message || "Unknown error",
|
|
487
|
+
errorDetails.stack || "",
|
|
488
|
+
errorDetails.file ? ` at ${errorDetails.file}:${errorDetails.line || 0}` : "",
|
|
489
|
+
].filter(Boolean).join("\n");
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
const result = await heal({
|
|
493
|
+
stderr,
|
|
494
|
+
cwd: this.cwd,
|
|
495
|
+
sandbox: this.sandbox,
|
|
496
|
+
redactor: this.redactor,
|
|
497
|
+
notifier: this.notifier,
|
|
498
|
+
rateLimiter: this.rateLimiter,
|
|
499
|
+
backupManager: this.backupManager,
|
|
500
|
+
logger: this.logger,
|
|
501
|
+
brain: this.brain,
|
|
502
|
+
mcp: this.mcp,
|
|
503
|
+
skills: this.skills,
|
|
504
|
+
repairHistory: this.repairHistory,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
if (result.healed) {
|
|
508
|
+
console.log(chalk.green(`\n🐺 Wolverine healed ${routePath} via ${result.mode}! Restarting...\n`));
|
|
509
|
+
this.errorMonitor.clearRoute(routePath);
|
|
510
|
+
this._healInProgress = false;
|
|
511
|
+
this.restart();
|
|
512
|
+
} else {
|
|
513
|
+
console.log(chalk.red(`\n🐺 Could not heal ${routePath}: ${result.explanation}`));
|
|
514
|
+
this._healInProgress = false;
|
|
515
|
+
}
|
|
516
|
+
} catch (err) {
|
|
517
|
+
console.log(chalk.red(`\n🐺 Error during heal: ${err.message}`));
|
|
518
|
+
this._healInProgress = false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
435
522
|
_startStabilityTimer() {
|
|
436
523
|
this._clearStabilityTimer();
|
|
437
524
|
this._stabilityTimer = setTimeout(() => {
|
package/src/core/wolverine.js
CHANGED
|
@@ -47,30 +47,25 @@ async function heal({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager
|
|
|
47
47
|
|
|
48
48
|
if (logger) logger.debug(EVENT_TYPES.HEAL_PARSE, `Parsed: ${parsed.errorMessage}`, { file: parsed.filePath, line: parsed.line });
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
// File path is optional — some errors (database, config, port) don't trace to a file.
|
|
51
|
+
// When no file is found, skip fast path and go straight to agent investigation.
|
|
52
|
+
let hasFile = false;
|
|
53
|
+
if (parsed.filePath) {
|
|
54
|
+
// 2. Sandbox check
|
|
55
|
+
try {
|
|
56
|
+
sandbox.resolve(parsed.filePath);
|
|
57
|
+
hasFile = sandbox.exists(parsed.filePath);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (e instanceof SandboxViolationError) {
|
|
60
|
+
console.log(chalk.red(` 🔒 SANDBOX: ${e.message}`));
|
|
61
|
+
if (logger) logger.error(EVENT_TYPES.SECURITY_SANDBOX_VIOLATION, e.message, { file: parsed.filePath });
|
|
62
|
+
return { healed: false, explanation: "File outside sandbox — access denied" };
|
|
63
|
+
}
|
|
64
|
+
throw e;
|
|
64
65
|
}
|
|
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
66
|
}
|
|
72
67
|
|
|
73
|
-
console.log(chalk.cyan(` File: ${parsed.filePath}`));
|
|
68
|
+
console.log(chalk.cyan(` File: ${parsed.filePath || "(no file — agent will investigate)"}`));
|
|
74
69
|
console.log(chalk.cyan(` Line: ${parsed.line || "unknown"}`));
|
|
75
70
|
console.log(chalk.cyan(` Error: ${parsed.errorMessage}`));
|
|
76
71
|
console.log(chalk.cyan(` Type: ${parsed.errorType || "unknown"}`));
|
|
@@ -130,8 +125,8 @@ async function heal({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager
|
|
|
130
125
|
return { healed: true, explanation: opsFix.action, mode: "operational" };
|
|
131
126
|
}
|
|
132
127
|
|
|
133
|
-
// 5. Read the source file + get brain context
|
|
134
|
-
const sourceCode = sandbox.readFile(parsed.filePath);
|
|
128
|
+
// 5. Read the source file (if available) + get brain context
|
|
129
|
+
const sourceCode = hasFile ? sandbox.readFile(parsed.filePath) : "";
|
|
135
130
|
|
|
136
131
|
let brainContext = "";
|
|
137
132
|
// Inject relevant skill context (claw-code: pre-enrich prompt with matched tools)
|
|
@@ -175,15 +170,16 @@ async function heal({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager
|
|
|
175
170
|
onAttempt: async (iteration, researchCtx) => {
|
|
176
171
|
// Create backup for this attempt
|
|
177
172
|
// Full server/ backup — includes all files, configs, databases
|
|
178
|
-
const bid = backupManager.createBackup(
|
|
173
|
+
const bid = backupManager.createBackup(`heal attempt ${iteration}: ${parsed.errorMessage.slice(0, 60)}`);
|
|
179
174
|
backupManager.setErrorSignature(bid, errorSignature);
|
|
180
175
|
if (logger) logger.info(EVENT_TYPES.BACKUP_CREATED, `Backup ${bid} (iteration ${iteration})`, { backupId: bid });
|
|
181
176
|
|
|
182
177
|
const fullContext = [brainContext, researchContext, researchCtx].filter(Boolean).join("\n");
|
|
183
178
|
|
|
184
179
|
let result;
|
|
185
|
-
if (iteration === 1) {
|
|
180
|
+
if (iteration === 1 && hasFile) {
|
|
186
181
|
// Fast path — CODING_MODEL, single file + optional commands
|
|
182
|
+
// Only available when we have a specific file to fix
|
|
187
183
|
console.log(chalk.yellow(` 🧠 Fast path (${getModel("coding")})...`));
|
|
188
184
|
try {
|
|
189
185
|
const repair = await requestRepair({
|
|
@@ -235,8 +231,8 @@ async function heal({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager
|
|
|
235
231
|
backupManager.rollbackTo(bid);
|
|
236
232
|
return { healed: false, explanation: `Fast path error: ${err.message}` };
|
|
237
233
|
}
|
|
238
|
-
} else if (iteration
|
|
239
|
-
//
|
|
234
|
+
} else if (iteration <= 2) {
|
|
235
|
+
// Agent path — REASONING_MODEL (also handles iteration 1 when no file)
|
|
240
236
|
console.log(chalk.magenta(` 🤖 Agent path (${getModel("reasoning")})...`));
|
|
241
237
|
const agent = new AgentEngine({
|
|
242
238
|
sandbox, logger, cwd, mcp,
|
|
@@ -251,9 +247,17 @@ async function heal({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager
|
|
|
251
247
|
});
|
|
252
248
|
rateLimiter.record(errorSignature, agentResult.totalTokens);
|
|
253
249
|
|
|
254
|
-
if (agentResult.success
|
|
255
|
-
|
|
256
|
-
if (
|
|
250
|
+
if (agentResult.success) {
|
|
251
|
+
// Verify: if we have a file, do syntax + boot check. Otherwise just boot probe.
|
|
252
|
+
if (hasFile) {
|
|
253
|
+
const verification = await verifyFix(parsed.filePath, cwd, errorSignature);
|
|
254
|
+
if (verification.verified) {
|
|
255
|
+
backupManager.markVerified(bid);
|
|
256
|
+
rateLimiter.clearSignature(errorSignature);
|
|
257
|
+
return { healed: true, explanation: agentResult.summary, backupId: bid, mode: "agent", agentStats: agentResult };
|
|
258
|
+
}
|
|
259
|
+
} else if (agentResult.filesModified.length > 0 || agentResult.toolCalls?.some(t => t.name === "bash_exec")) {
|
|
260
|
+
// No specific file but agent made changes or ran commands — trust it
|
|
257
261
|
backupManager.markVerified(bid);
|
|
258
262
|
rateLimiter.clearSignature(errorSignature);
|
|
259
263
|
return { healed: true, explanation: agentResult.summary, backupId: bid, mode: "agent", agentStats: agentResult };
|
|
@@ -267,14 +271,20 @@ async function heal({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager
|
|
|
267
271
|
console.log(chalk.magenta(` 🤖 Sub-agent path (explore → plan → fix)...`));
|
|
268
272
|
|
|
269
273
|
const subResult = await exploreAndFix(
|
|
270
|
-
`Error: ${parsed.errorMessage}\
|
|
274
|
+
`Error: ${parsed.errorMessage}\n${parsed.filePath ? "File: " + parsed.filePath + "\n" : ""}Stack: ${parsed.stackTrace?.slice(0, 300)}`,
|
|
271
275
|
{ sandbox, logger, cwd, mcp, brainContext: fullContext }
|
|
272
276
|
);
|
|
273
277
|
rateLimiter.record(errorSignature, subResult.totalTokens);
|
|
274
278
|
|
|
275
|
-
if (subResult.success
|
|
276
|
-
|
|
277
|
-
|
|
279
|
+
if (subResult.success) {
|
|
280
|
+
if (hasFile) {
|
|
281
|
+
const verification = await verifyFix(parsed.filePath, cwd, errorSignature);
|
|
282
|
+
if (verification.verified) {
|
|
283
|
+
backupManager.markVerified(bid);
|
|
284
|
+
rateLimiter.clearSignature(errorSignature);
|
|
285
|
+
return { healed: true, explanation: subResult.summary, backupId: bid, mode: "sub-agents", agentStats: subResult };
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
278
288
|
backupManager.markVerified(bid);
|
|
279
289
|
rateLimiter.clearSignature(errorSignature);
|
|
280
290
|
return { healed: true, explanation: subResult.summary, backupId: bid, mode: "sub-agents", agentStats: subResult };
|
package/src/dashboard/server.js
CHANGED
|
@@ -29,6 +29,7 @@ class DashboardServer {
|
|
|
29
29
|
this.repairHistory = options.repairHistory;
|
|
30
30
|
this.processMonitor = options.processMonitor;
|
|
31
31
|
this.routeProber = options.routeProber;
|
|
32
|
+
this.errorMonitor = options.errorMonitor;
|
|
32
33
|
|
|
33
34
|
this.auth = new AdminAuth();
|
|
34
35
|
this._sseClients = new Set();
|
|
@@ -869,6 +870,7 @@ ${context ? "\nBrain:\n" + context : ""}`,
|
|
|
869
870
|
session: this.logger ? this.logger.getSessionStats() : {},
|
|
870
871
|
backups: this.backupManager ? this.backupManager.getStats() : {},
|
|
871
872
|
health: this.healthMonitor ? this.healthMonitor.getStats() : {},
|
|
873
|
+
errorMonitor: this.errorMonitor ? this.errorMonitor.getStats() : {},
|
|
872
874
|
}));
|
|
873
875
|
}
|
|
874
876
|
|
package/src/index.js
CHANGED
|
@@ -23,6 +23,7 @@ const { spawnAgent, spawnParallel, exploreAndFix } = require("./agent/sub-agents
|
|
|
23
23
|
const { McpRegistry } = require("./mcp/mcp-registry");
|
|
24
24
|
const { McpSecurity } = require("./mcp/mcp-security");
|
|
25
25
|
const { PerfMonitor } = require("./monitor/perf-monitor");
|
|
26
|
+
const { ErrorMonitor } = require("./monitor/error-monitor");
|
|
26
27
|
const { DashboardServer } = require("./dashboard/server");
|
|
27
28
|
const { Notifier } = require("./notifications/notifier");
|
|
28
29
|
const { Brain } = require("./brain/brain");
|
|
@@ -72,6 +73,7 @@ module.exports = {
|
|
|
72
73
|
McpSecurity,
|
|
73
74
|
// Monitor
|
|
74
75
|
PerfMonitor,
|
|
76
|
+
ErrorMonitor,
|
|
75
77
|
// Dashboard
|
|
76
78
|
DashboardServer,
|
|
77
79
|
// Notifications
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error Monitor — detects caught 500 errors that don't crash the process.
|
|
5
|
+
*
|
|
6
|
+
* Most production bugs are caught by Fastify/Express error handlers.
|
|
7
|
+
* The server stays alive but routes return 500. Wolverine's crash-based
|
|
8
|
+
* heal pipeline never triggers. This module bridges that gap.
|
|
9
|
+
*
|
|
10
|
+
* Tracks 5xx errors per route. After N consecutive failures within
|
|
11
|
+
* a time window, triggers the heal pipeline — without killing the server.
|
|
12
|
+
*
|
|
13
|
+
* Error flow: server error handler → IPC message → ErrorMonitor.record()
|
|
14
|
+
* → threshold reached → onError callback → heal()
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
class ErrorMonitor {
|
|
18
|
+
constructor({ threshold = 3, windowMs = 30000, cooldownMs = 60000, onError, logger } = {}) {
|
|
19
|
+
this.threshold = threshold; // consecutive 5xx before triggering heal
|
|
20
|
+
this.windowMs = windowMs; // time window for counting errors
|
|
21
|
+
this.cooldownMs = cooldownMs; // cooldown after triggering (prevent heal spam)
|
|
22
|
+
this.onError = onError; // callback: (routePath, errorDetails) => heal()
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
this.routes = new Map(); // path → { count, firstSeen, lastError }
|
|
25
|
+
this._cooldowns = new Map(); // path → timestamp of last heal trigger
|
|
26
|
+
this._totalErrors = 0;
|
|
27
|
+
this._totalHeals = 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Record a route response. Call on every response from the child.
|
|
32
|
+
* @param {string} routePath — e.g. "/api/users"
|
|
33
|
+
* @param {number} statusCode — HTTP status
|
|
34
|
+
* @param {object} errorDetails — { message, stack, file, line }
|
|
35
|
+
*/
|
|
36
|
+
record(routePath, statusCode, errorDetails) {
|
|
37
|
+
if (statusCode < 500) {
|
|
38
|
+
// Success — reset the error counter for this route
|
|
39
|
+
if (this.routes.has(routePath)) {
|
|
40
|
+
this.routes.delete(routePath);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this._totalErrors++;
|
|
46
|
+
|
|
47
|
+
// Check cooldown — don't trigger heal for same route too quickly
|
|
48
|
+
const lastHeal = this._cooldowns.get(routePath);
|
|
49
|
+
if (lastHeal && Date.now() - lastHeal < this.cooldownMs) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const entry = this.routes.get(routePath) || { count: 0, firstSeen: Date.now(), lastError: null };
|
|
54
|
+
entry.count++;
|
|
55
|
+
entry.lastError = errorDetails;
|
|
56
|
+
|
|
57
|
+
// Reset if outside time window
|
|
58
|
+
if (Date.now() - entry.firstSeen > this.windowMs) {
|
|
59
|
+
entry.count = 1;
|
|
60
|
+
entry.firstSeen = Date.now();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.routes.set(routePath, entry);
|
|
64
|
+
|
|
65
|
+
if (entry.count >= this.threshold) {
|
|
66
|
+
this._totalHeals++;
|
|
67
|
+
console.log(chalk.yellow(`\n🔍 ErrorMonitor: ${routePath} failed ${entry.count}x in ${Math.round((Date.now() - entry.firstSeen) / 1000)}s — triggering heal`));
|
|
68
|
+
|
|
69
|
+
if (this.logger) {
|
|
70
|
+
this.logger.warn("error_monitor.threshold", `Route ${routePath} hit ${this.threshold} consecutive 500s`, {
|
|
71
|
+
route: routePath,
|
|
72
|
+
count: entry.count,
|
|
73
|
+
error: errorDetails?.message?.slice(0, 200),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Set cooldown and reset counter
|
|
78
|
+
this._cooldowns.set(routePath, Date.now());
|
|
79
|
+
this.routes.delete(routePath);
|
|
80
|
+
|
|
81
|
+
// Trigger the heal callback
|
|
82
|
+
if (this.onError) {
|
|
83
|
+
this.onError(routePath, errorDetails);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Clear a route's error state (e.g., after a successful heal).
|
|
90
|
+
*/
|
|
91
|
+
clearRoute(routePath) {
|
|
92
|
+
this.routes.delete(routePath);
|
|
93
|
+
this._cooldowns.delete(routePath);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get stats for dashboard/telemetry.
|
|
98
|
+
*/
|
|
99
|
+
getStats() {
|
|
100
|
+
const activeRoutes = {};
|
|
101
|
+
for (const [path, entry] of this.routes) {
|
|
102
|
+
activeRoutes[path] = { count: entry.count, lastError: entry.lastError?.message?.slice(0, 100) };
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
totalErrors: this._totalErrors,
|
|
106
|
+
totalHeals: this._totalHeals,
|
|
107
|
+
activeRoutes,
|
|
108
|
+
trackedRoutes: this.routes.size,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Reset all state (e.g., after server restart).
|
|
114
|
+
*/
|
|
115
|
+
reset() {
|
|
116
|
+
this.routes.clear();
|
|
117
|
+
// Keep cooldowns — don't re-trigger immediately after restart
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { ErrorMonitor };
|