wolverine-ai 6.3.0 → 6.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "6.3.0",
3
+ "version": "6.4.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": {
@@ -0,0 +1,443 @@
1
+ /**
2
+ * Wolverine API — unified entry point for any OpenClaw process.
3
+ *
4
+ * Exposes every wolverine subsystem through a single, lazy-loaded API.
5
+ * Nothing is imported until first use, so requiring this module is free.
6
+ *
7
+ * Usage from any OpenClaw skill, plugin, or agent:
8
+ * const wolverine = require("wolverine-ai/src/claw/wolverine-api");
9
+ * const api = wolverine.init("/path/to/project");
10
+ *
11
+ * // Security
12
+ * api.security.detectInjection("some user input");
13
+ * api.security.redact("text with sk-abc123 key");
14
+ * api.security.sandbox.resolve("server/index.js");
15
+ *
16
+ * // Brain
17
+ * await api.brain.search("how to fix ECONNREFUSED");
18
+ * await api.brain.learn("Redis needs REDIS_URL env var", "fix");
19
+ *
20
+ * // AI
21
+ * const result = await api.ai.call({ model: "claude-sonnet-4-6", ... });
22
+ *
23
+ * // Errors
24
+ * api.errors.parse(stderrText);
25
+ * api.errors.classify(errorMessage);
26
+ * api.errors.routeTools("ECONNREFUSED");
27
+ *
28
+ * // Backup
29
+ * api.backup.create("before risky change");
30
+ * api.backup.rollbackLatest();
31
+ *
32
+ * // ... etc
33
+ */
34
+
35
+ const path = require("path");
36
+
37
+ let _projectRoot = null;
38
+ let _initialized = false;
39
+
40
+ // Lazy singletons — created on first access
41
+ const _cache = {};
42
+
43
+ function _require(relativePath) {
44
+ return require(path.join(_projectRoot, relativePath));
45
+ }
46
+
47
+ function _lazy(key, factory) {
48
+ if (!_cache[key]) _cache[key] = factory();
49
+ return _cache[key];
50
+ }
51
+
52
+ // ── Public API ──────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Initialize the Wolverine API for a project.
56
+ * Call once — returns the API object. Subsequent calls return the same instance.
57
+ */
58
+ function init(projectRoot) {
59
+ if (_initialized && _projectRoot === path.resolve(projectRoot)) return api;
60
+ _projectRoot = path.resolve(projectRoot);
61
+ _initialized = true;
62
+
63
+ // Clear cache if re-initializing with different root
64
+ for (const key of Object.keys(_cache)) delete _cache[key];
65
+
66
+ return api;
67
+ }
68
+
69
+ const api = {
70
+ // ── Security ────────────────────────────────────────────────
71
+
72
+ get security() {
73
+ return _lazy("security", () => {
74
+ const { detectInjection, localScan, INJECTION_PATTERNS } = _require("src/security/injection-detector");
75
+ const { initRedactor, redact, redactObj, hasSecrets } = _require("src/security/secret-redactor");
76
+ const { Sandbox } = _require("src/security/sandbox");
77
+ const { RateLimiter } = _require("src/security/rate-limiter");
78
+
79
+ // Initialize redactor for this project
80
+ const redactor = initRedactor(_projectRoot);
81
+
82
+ return {
83
+ // Prompt injection detection (~50 patterns)
84
+ detectInjection,
85
+ localScan,
86
+ INJECTION_PATTERNS,
87
+
88
+ // Secret redaction
89
+ redact,
90
+ redactObj,
91
+ hasSecrets,
92
+ redactor,
93
+
94
+ // File sandbox
95
+ sandbox: new Sandbox(_projectRoot),
96
+ Sandbox,
97
+
98
+ // Rate limiting
99
+ createRateLimiter: (opts) => new RateLimiter(opts),
100
+ RateLimiter,
101
+ };
102
+ });
103
+ },
104
+
105
+ // ── Brain (semantic memory) ─────────────────────────────────
106
+
107
+ get brain() {
108
+ return _lazy("brain", () => {
109
+ const { Brain } = _require("src/brain/brain");
110
+ const { VectorStore } = _require("src/brain/vector-store");
111
+ const { embed, embedBatch, compact, compactAndEmbed } = _require("src/brain/embedder");
112
+ const { scanProject, mapToChunks } = _require("src/brain/function-map");
113
+ const { route, getRoutePrompt, TOOL_ROUTES } = _require("src/brain/tool-router");
114
+
115
+ const brain = new Brain(_projectRoot);
116
+ let _initPromise = null;
117
+
118
+ return {
119
+ // High-level brain operations
120
+ async search(query, opts) {
121
+ if (!_initPromise) _initPromise = brain.init();
122
+ await _initPromise;
123
+ return brain.search ? brain.search(query, opts)
124
+ : brain.recall ? brain.recall(query, opts)
125
+ : [];
126
+ },
127
+ async learn(content, category) {
128
+ if (!_initPromise) _initPromise = brain.init();
129
+ await _initPromise;
130
+ if (brain.addDocument) {
131
+ return brain.addDocument({ content, namespace: category || "learnings", source: "wolverine-claw", timestamp: Date.now() });
132
+ }
133
+ if (brain.remember) return brain.remember(content, { namespace: category });
134
+ },
135
+ async getContext(query) {
136
+ if (!_initPromise) _initPromise = brain.init();
137
+ await _initPromise;
138
+ return brain.getContext ? brain.getContext(query) : "";
139
+ },
140
+ async init() {
141
+ if (!_initPromise) _initPromise = brain.init();
142
+ return _initPromise;
143
+ },
144
+
145
+ // Low-level access
146
+ instance: brain,
147
+ VectorStore,
148
+ embed,
149
+ embedBatch,
150
+ compact,
151
+ compactAndEmbed,
152
+ scanProject,
153
+ mapToChunks,
154
+
155
+ // Tool router — maps error types to tool chains
156
+ routeTools: route,
157
+ getRoutePrompt,
158
+ TOOL_ROUTES,
159
+ };
160
+ });
161
+ },
162
+
163
+ // ── AI Client ───────────────────────────────────────────────
164
+
165
+ get ai() {
166
+ return _lazy("ai", () => {
167
+ const client = _require("src/core/ai-client");
168
+ const { getModel, getEmbeddingModel, getModelConfig, detectProvider, MODEL_ROLES } = _require("src/core/models");
169
+
170
+ return {
171
+ // Make AI calls (auto-detects provider from model name)
172
+ call: client.aiCall,
173
+ callWithHistory: client.aiCallWithHistory,
174
+
175
+ // Embeddings
176
+ embed: async (text) => {
177
+ const { embed } = _require("src/brain/embedder");
178
+ return embed(text);
179
+ },
180
+
181
+ // Model utilities
182
+ getModel,
183
+ getEmbeddingModel,
184
+ getModelConfig,
185
+ detectProvider,
186
+ MODEL_ROLES,
187
+
188
+ // Token tracking
189
+ setTokenTracker: client.setTokenTracker,
190
+ getTrackerSnapshot: client.getTrackerSnapshot,
191
+ };
192
+ });
193
+ },
194
+
195
+ // ── Error Handling ──────────────────────────────────────────
196
+
197
+ get errors() {
198
+ return _lazy("errors", () => {
199
+ const { parseError, classifyError } = _require("src/core/error-parser");
200
+ const { route, getRoutePrompt } = _require("src/brain/tool-router");
201
+ const { Notifier, HUMAN_REQUIRED_PATTERNS } = _require("src/notifications/notifier");
202
+ const { LoopGuard } = _require("src/skills/loop-guard");
203
+
204
+ return {
205
+ // Parse error text → structured {file, line, message, errorType}
206
+ parse: parseError,
207
+ // Classify error → type (missing_module, syntax, runtime, etc.)
208
+ classify: classifyError,
209
+ // Map error type → recommended tool chain
210
+ routeTools: route,
211
+ getRoutePrompt,
212
+
213
+ // Detect if error needs human vs can be auto-fixed
214
+ HUMAN_REQUIRED_PATTERNS,
215
+ createNotifier: (opts) => new Notifier(opts),
216
+
217
+ // Loop detection — prevent infinite fix attempts
218
+ createLoopGuard: (opts) => new LoopGuard(_projectRoot, opts),
219
+ };
220
+ });
221
+ },
222
+
223
+ // ── Backup & Recovery ───────────────────────────────────────
224
+
225
+ get backup() {
226
+ return _lazy("backup", () => {
227
+ const { BackupManager } = _require("src/backup/backup-manager");
228
+ const manager = new BackupManager(_projectRoot);
229
+
230
+ return {
231
+ create: (reason) => manager.createBackup(reason),
232
+ rollback: (id) => manager.rollbackTo(id),
233
+ rollbackLatest: () => manager.rollbackLatest(),
234
+ undoRollback: () => manager.undoRollback(),
235
+ list: () => manager.list(),
236
+ getStats: () => manager.getStats(),
237
+ promote: (id) => manager.promote(id),
238
+ prune: () => manager.prune(),
239
+ instance: manager,
240
+ };
241
+ });
242
+ },
243
+
244
+ // ── Skills ──────────────────────────────────────────────────
245
+
246
+ get skills() {
247
+ return _lazy("skills", () => {
248
+ const { SkillRegistry } = _require("src/skills/skill-registry");
249
+ const registry = new SkillRegistry();
250
+ registry.load();
251
+
252
+ return {
253
+ // Skill registry
254
+ match: (query) => registry.match(query),
255
+ list: () => registry.getAll(),
256
+ registry,
257
+
258
+ // SQL injection protection
259
+ get sql() {
260
+ const { scanForInjection, deepScan, sqlGuard } = _require("src/skills/sql");
261
+ return { scanForInjection, deepScan, sqlGuard };
262
+ },
263
+
264
+ // Dependency analysis
265
+ get deps() {
266
+ const deps = _require("src/skills/deps");
267
+ return {
268
+ diagnose: deps.diagnose,
269
+ healthReport: deps.healthReport,
270
+ getMigration: deps.getMigration,
271
+ };
272
+ },
273
+
274
+ // Backup (alias)
275
+ get backup() {
276
+ const b = _require("src/skills/backup");
277
+ return { backup: b.backup, rollback: b.rollback, rollbackLatest: b.rollbackLatest, undoRollback: b.undoRollback, listBackups: b.listBackups };
278
+ },
279
+ };
280
+ });
281
+ },
282
+
283
+ // ── Monitoring ──────────────────────────────────────────────
284
+
285
+ get monitor() {
286
+ return _lazy("monitor", () => {
287
+ const { ErrorMonitor } = _require("src/monitor/error-monitor");
288
+ const { PerfMonitor } = _require("src/monitor/perf-monitor");
289
+ const { ProcessMonitor } = _require("src/monitor/process-monitor");
290
+ const { AdaptiveLimiter } = _require("src/monitor/adaptive-limiter");
291
+
292
+ return {
293
+ ErrorMonitor,
294
+ PerfMonitor,
295
+ ProcessMonitor,
296
+ AdaptiveLimiter,
297
+ };
298
+ });
299
+ },
300
+
301
+ // ── Logging & Metrics ───────────────────────────────────────
302
+
303
+ get logger() {
304
+ return _lazy("logger", () => {
305
+ const { EventLogger, EVENT_TYPES, SEVERITY } = _require("src/logger/event-logger");
306
+ const { TokenTracker } = _require("src/logger/token-tracker");
307
+ const { RepairHistory } = _require("src/logger/repair-history");
308
+
309
+ return {
310
+ createEventLogger: () => new EventLogger(_projectRoot),
311
+ createTokenTracker: () => new TokenTracker(_projectRoot),
312
+ createRepairHistory: () => new RepairHistory(_projectRoot),
313
+ EVENT_TYPES,
314
+ SEVERITY,
315
+ };
316
+ });
317
+ },
318
+
319
+ // ── Config ──────────────────────────────────────────────────
320
+
321
+ get config() {
322
+ return _lazy("config", () => {
323
+ const { loadConfig, getConfig } = _require("src/core/config");
324
+ return { load: loadConfig, get: getConfig };
325
+ });
326
+ },
327
+
328
+ // ── Agent Engine ────────────────────────────────────────────
329
+
330
+ get agent() {
331
+ return _lazy("agent", () => {
332
+ const { AgentEngine, TOOL_DEFINITIONS, BLOCKED_COMMANDS } = _require("src/agent/agent-engine");
333
+ const { Sandbox } = _require("src/security/sandbox");
334
+
335
+ return {
336
+ // Create a sandboxed agent engine
337
+ create: (opts = {}) => new AgentEngine({
338
+ cwd: opts.cwd || _projectRoot,
339
+ sandbox: opts.sandbox || new Sandbox(opts.cwd || _projectRoot),
340
+ maxTurns: opts.maxTurns || 25,
341
+ maxTokens: opts.maxTokens || 100000,
342
+ logger: opts.logger,
343
+ mcp: opts.mcp,
344
+ }),
345
+ TOOL_DEFINITIONS,
346
+ BLOCKED_COMMANDS,
347
+ AgentEngine,
348
+ };
349
+ });
350
+ },
351
+
352
+ // ── Server Context ──────────────────────────────────────────
353
+
354
+ get context() {
355
+ return _lazy("context", () => {
356
+ const { scan, load, getSummary } = _require("src/core/server-context");
357
+ return {
358
+ scan: () => scan(_projectRoot),
359
+ load: () => load(_projectRoot),
360
+ getSummary: () => getSummary(_projectRoot),
361
+ };
362
+ });
363
+ },
364
+
365
+ // ── Verification ────────────────────────────────────────────
366
+
367
+ get verify() {
368
+ return _lazy("verify", () => {
369
+ const { verifyFix, syntaxCheck, bootProbe } = _require("src/core/verifier");
370
+ return { verifyFix, syntaxCheck, bootProbe };
371
+ });
372
+ },
373
+
374
+ // ── MCP (Model Context Protocol) ───────────────────────────
375
+
376
+ get mcp() {
377
+ return _lazy("mcp", () => {
378
+ const { McpRegistry } = _require("src/mcp/mcp-registry");
379
+ return {
380
+ create: (opts = {}) => new McpRegistry({
381
+ projectRoot: _projectRoot,
382
+ ...opts,
383
+ }),
384
+ McpRegistry,
385
+ };
386
+ });
387
+ },
388
+
389
+ // ── Convenience: all-in-one scan ───────────────────────────
390
+
391
+ /**
392
+ * Run a fast security scan on text (regex injection + secrets). Sync, no AI call.
393
+ * For deep AI-powered injection scan, use api.security.detectInjection() (async).
394
+ * Returns { safe, injection, secrets, redacted }.
395
+ */
396
+ scanText(text) {
397
+ const sec = this.security;
398
+ const injectionResult = sec.localScan(text);
399
+ const hasSecretContent = sec.hasSecrets(text);
400
+ const redacted = sec.redact(text);
401
+
402
+ return {
403
+ safe: injectionResult.safe !== false && !hasSecretContent,
404
+ injection: injectionResult,
405
+ secrets: hasSecretContent,
406
+ redacted,
407
+ };
408
+ },
409
+
410
+ /**
411
+ * Diagnose an error: parse, classify, route tools, check if human-needed.
412
+ * Returns { parsed, type, tools, humanRequired }.
413
+ */
414
+ diagnoseError(errorText) {
415
+ const err = this.errors;
416
+ const parsed = err.parse(errorText);
417
+ const toolRoute = err.routeTools(parsed.errorType || "unknown");
418
+
419
+ // Check if human required
420
+ let humanRequired = false;
421
+ for (const pattern of err.HUMAN_REQUIRED_PATTERNS) {
422
+ if (pattern.test ? pattern.test(errorText) : errorText.includes(pattern)) {
423
+ humanRequired = true;
424
+ break;
425
+ }
426
+ }
427
+
428
+ return {
429
+ parsed,
430
+ type: parsed.errorType,
431
+ tools: toolRoute,
432
+ humanRequired,
433
+ };
434
+ },
435
+
436
+ /** Get the project root. */
437
+ get projectRoot() { return _projectRoot; },
438
+
439
+ /** Check if initialized. */
440
+ get initialized() { return _initialized; },
441
+ };
442
+
443
+ module.exports = { init, api };
@@ -1,14 +1,21 @@
1
1
  /**
2
2
  * Wolverine Integration Plugin for OpenClaw
3
3
  *
4
- * Gives the claw agent access to wolverine's self-healing capabilities:
5
- * - Brain: semantic memory, project context, and learning
6
- * - Backup: workspace snapshots and rollback
7
- * - Healing: error diagnosis and auto-fix pipeline
8
- * - Health: process monitoring and status reporting
4
+ * Two integration layers:
9
5
  *
10
- * This plugin registers as an OpenClaw skill/extension, adding wolverine-specific
11
- * tools to the agent's toolkit.
6
+ * 1. TOOLS 7 wolverine tools injected into the agent toolkit
7
+ * (backup, rollback, brain search/learn, health, self-heal)
8
+ *
9
+ * 2. HOOKS — taps into OpenClaw's 28-hook plugin system for deep
10
+ * error observability and self-healing triggers:
11
+ * - after_tool_call: catch skill/tool failures
12
+ * - agent_end: catch agent crashes and timeouts
13
+ * - before_tool_call: detect and report tool loops
14
+ * - message_sent: catch channel send failures
15
+ * - llm_output: catch provider errors, failovers, billing blocks
16
+ * - subagent_ended: catch subagent failures
17
+ * - gateway_start/gateway_stop: lifecycle tracking
18
+ * - session_end: session error tracking
12
19
  */
13
20
 
14
21
  const path = require("path");
@@ -16,51 +23,348 @@ const fs = require("fs");
16
23
 
17
24
  const PLUGIN_NAME = "wolverine-integration";
18
25
 
26
+ // ── Error tracking state ────────────────────────────────────────
27
+
28
+ const _errorCounts = {
29
+ tool: 0,
30
+ agent: 0,
31
+ channel: 0,
32
+ llm: 0,
33
+ subagent: 0,
34
+ total: 0,
35
+ };
36
+
37
+ const _recentErrors = []; // last 50 errors for context
38
+ const MAX_RECENT = 50;
39
+
40
+ function _trackError(category, summary, detail) {
41
+ _errorCounts[category] = (_errorCounts[category] || 0) + 1;
42
+ _errorCounts.total++;
43
+
44
+ const entry = {
45
+ category,
46
+ summary: summary?.slice(0, 200),
47
+ timestamp: Date.now(),
48
+ detail: detail?.slice?.(0, 500),
49
+ };
50
+ _recentErrors.push(entry);
51
+ if (_recentErrors.length > MAX_RECENT) _recentErrors.shift();
52
+
53
+ return entry;
54
+ }
55
+
56
+ // ── Main registration ───────────────────────────────────────────
57
+
19
58
  /**
20
59
  * Register the wolverine integration with the OpenClaw gateway.
60
+ * Supports both Plugin SDK (api.registerPluginHook) and basic EventEmitter.
21
61
  */
22
62
  async function register(gateway, config) {
23
63
  const projectRoot = path.resolve(__dirname, "../..");
24
64
 
25
- // Register wolverine tools as OpenClaw tools/skills
65
+ // Register wolverine tools
26
66
  const tools = buildWolverineTools(projectRoot, config);
67
+ _registerTools(gateway, tools);
27
68
 
28
- if (gateway.registerTools) {
29
- gateway.registerTools(PLUGIN_NAME, tools);
30
- } else if (gateway.addTools) {
31
- gateway.addTools(tools);
32
- } else if (gateway.skills?.register) {
33
- gateway.skills.register(PLUGIN_NAME, {
34
- description: "Wolverine self-healing integration — backup, brain, and health tools",
35
- tools,
36
- });
37
- }
38
-
39
- // Hook into gateway events for self-healing
40
- if (gateway.on) {
41
- gateway.on("error", (err) => handleGatewayError(err, projectRoot));
42
- gateway.on("agent:error", (err) => handleAgentError(err, projectRoot));
43
- gateway.on("skill:error", (err) => handleSkillError(err, projectRoot));
69
+ // Register into OpenClaw Plugin SDK hooks (preferred)
70
+ const api = _getPluginApi(gateway);
71
+ if (api) {
72
+ _registerPluginHooks(api, projectRoot, config);
73
+ console.log("[CLAW] Wolverine plugin hooks registered (Plugin SDK)");
44
74
  }
75
+ // Also register EventEmitter hooks (fallback / additional coverage)
76
+ _registerEventHooks(gateway, projectRoot);
45
77
 
46
- // Periodic health reporting
78
+ // Periodic health reporting with error stats
47
79
  const healthInterval = setInterval(() => {
48
80
  reportToWolverine("claw_heartbeat", {
49
81
  uptime: process.uptime(),
50
82
  memory: process.memoryUsage(),
83
+ errorCounts: { ..._errorCounts },
51
84
  timestamp: Date.now(),
52
85
  });
53
86
  }, 30000);
54
87
 
55
- // Cleanup on shutdown
56
88
  if (gateway.on) {
57
89
  gateway.on("shutdown", () => clearInterval(healthInterval));
58
90
  }
59
91
  }
60
92
 
93
+ // ── Plugin SDK Hooks ────────────────────────────────────────────
94
+
61
95
  /**
62
- * Build wolverine-specific tools for the OpenClaw agent.
96
+ * Get the Plugin SDK API from the gateway (if available).
63
97
  */
98
+ function _getPluginApi(gateway) {
99
+ // OpenClaw exposes plugin registration via multiple paths
100
+ if (gateway.pluginApi) return gateway.pluginApi;
101
+ if (gateway.api) return gateway.api;
102
+ if (gateway.plugins?.api) return gateway.plugins.api;
103
+ if (typeof gateway.registerPluginHook === "function") return gateway;
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * Register all wolverine hooks into the OpenClaw Plugin SDK.
109
+ */
110
+ function _registerPluginHooks(api, projectRoot, config) {
111
+ const registerHook = api.registerPluginHook || api.register;
112
+ if (typeof registerHook !== "function") return;
113
+
114
+ const reg = (name, handler, priority) => {
115
+ try {
116
+ registerHook.call(api, name, { handler, priority: priority || 100 });
117
+ } catch (e) {
118
+ // Hook not supported in this OpenClaw version — skip silently
119
+ try {
120
+ registerHook.call(api, name, handler);
121
+ } catch {}
122
+ }
123
+ };
124
+
125
+ // ── after_tool_call: catch skill and tool failures ──────────
126
+ reg("after_tool_call", (event, ctx) => {
127
+ if (!event) return;
128
+ const hasError = event.error || event.status === "error" || event.exitCode;
129
+
130
+ if (hasError) {
131
+ const toolName = event.toolName || event.name || "unknown";
132
+ const errorMsg = event.error?.message || event.error || event.stderr || "Tool failed";
133
+ const entry = _trackError("tool", `Tool ${toolName} failed: ${errorMsg}`);
134
+
135
+ console.error(`[CLAW] Tool error: ${toolName} — ${errorMsg.slice(0, 100)}`);
136
+
137
+ // Skill errors are wrapped as tool errors — detect them
138
+ const isSkill = toolName.includes("skill") || event.source === "skill";
139
+
140
+ reportToWolverine("route_error", {
141
+ path: `claw://tool/${toolName}`,
142
+ method: isSkill ? "SKILL" : "TOOL",
143
+ statusCode: 500,
144
+ message: `${isSkill ? "Skill" : "Tool"} ${toolName}: ${errorMsg}`.slice(0, 500),
145
+ stack: event.error?.stack || event.stderr || null,
146
+ file: event.file || null,
147
+ line: null,
148
+ timestamp: Date.now(),
149
+ });
150
+ }
151
+ });
152
+
153
+ // ── agent_end: catch agent failures and timeouts ────────────
154
+ reg("agent_end", (event, ctx) => {
155
+ if (!event) return;
156
+ const hasError = event.error || event.status === "error" || event.lifecycle === "error";
157
+
158
+ if (hasError) {
159
+ const reason = event.reason || event.error?.message || "Agent failed";
160
+ const isTimeout = reason.includes("timeout") || event.reason === "timeout";
161
+ const entry = _trackError("agent", `Agent ${isTimeout ? "timeout" : "error"}: ${reason}`);
162
+
163
+ console.error(`[CLAW] Agent ${isTimeout ? "timeout" : "error"}: ${reason.slice(0, 100)}`);
164
+
165
+ reportToWolverine("route_error", {
166
+ path: "claw://agent",
167
+ method: isTimeout ? "TIMEOUT" : "AGENT",
168
+ statusCode: isTimeout ? 408 : 500,
169
+ message: reason.slice(0, 500),
170
+ stack: event.error?.stack || null,
171
+ file: null,
172
+ line: null,
173
+ timestamp: Date.now(),
174
+ });
175
+ }
176
+ });
177
+
178
+ // ── before_tool_call: detect tool loops ─────────────────────
179
+ reg("before_tool_call", (event, ctx) => {
180
+ if (!event) return;
181
+
182
+ // OpenClaw's loop detection may have already flagged this
183
+ if (event.block || event.loopDetected) {
184
+ const toolName = event.toolName || event.name || "unknown";
185
+ const reason = event.blockReason || event.reason || "Loop detected";
186
+ _trackError("tool", `Tool loop blocked: ${toolName} — ${reason}`);
187
+
188
+ console.warn(`[CLAW] Tool loop blocked: ${toolName} — ${reason}`);
189
+
190
+ reportToWolverine("claw_warning", {
191
+ category: "tool_loop",
192
+ tool: toolName,
193
+ reason,
194
+ timestamp: Date.now(),
195
+ });
196
+ }
197
+ });
198
+
199
+ // ── message_sent: catch channel send failures ───────────────
200
+ reg("message_sent", (event, ctx) => {
201
+ if (!event) return;
202
+ const failed = event.success === false || event.error;
203
+
204
+ if (failed) {
205
+ const channel = event.channelId || event.channel || "unknown";
206
+ const errorMsg = event.error?.message || event.error || "Send failed";
207
+ _trackError("channel", `Channel ${channel}: ${errorMsg}`);
208
+
209
+ console.error(`[CLAW] Channel send failed: ${channel} — ${errorMsg.slice(0, 100)}`);
210
+
211
+ reportToWolverine("claw_warning", {
212
+ category: "channel_failure",
213
+ channel,
214
+ error: errorMsg.slice(0, 500),
215
+ timestamp: Date.now(),
216
+ });
217
+ }
218
+ });
219
+
220
+ // ── llm_output: catch provider errors and failovers ─────────
221
+ reg("llm_output", (event, ctx) => {
222
+ if (!event) return;
223
+ const hasError = event.error || event.failover;
224
+
225
+ if (hasError) {
226
+ const provider = event.provider || event.model || "unknown";
227
+ const reason = event.failoverReason || event.error?.message || event.error || "LLM error";
228
+ const status = event.httpCode || event.status;
229
+ _trackError("llm", `LLM ${provider}: ${reason} (${status || "?"})`);
230
+
231
+ // Billing errors (402) are critical — stop healing
232
+ const isBilling = status === 402 || reason.includes("billing");
233
+ // Rate limits (429) are transient
234
+ const isRateLimit = status === 429 || reason.includes("rate_limit");
235
+
236
+ if (isBilling) {
237
+ console.error(`[CLAW] BILLING ERROR: ${provider} — ${reason}`);
238
+ reportToWolverine("claw_critical", {
239
+ category: "billing",
240
+ provider,
241
+ error: reason.slice(0, 500),
242
+ timestamp: Date.now(),
243
+ });
244
+ } else if (!isRateLimit) {
245
+ // Don't spam wolverine with rate limit warnings
246
+ console.warn(`[CLAW] LLM error: ${provider} — ${reason.slice(0, 100)}`);
247
+ reportToWolverine("claw_warning", {
248
+ category: "llm_error",
249
+ provider,
250
+ status,
251
+ error: reason.slice(0, 500),
252
+ timestamp: Date.now(),
253
+ });
254
+ }
255
+ }
256
+ });
257
+
258
+ // ── subagent_ended: catch subagent failures ─────────────────
259
+ reg("subagent_ended", (event, ctx) => {
260
+ if (!event) return;
261
+ const failed = event.status === "error" || event.status === "timeout" || event.error;
262
+
263
+ if (failed) {
264
+ const agentId = event.subagentId || event.runId || "unknown";
265
+ const errorMsg = event.error?.message || event.error || `Subagent ${event.status}`;
266
+ _trackError("subagent", `Subagent ${agentId}: ${errorMsg}`);
267
+
268
+ console.error(`[CLAW] Subagent failed: ${agentId} — ${errorMsg.slice(0, 100)}`);
269
+
270
+ reportToWolverine("route_error", {
271
+ path: `claw://subagent/${agentId}`,
272
+ method: "SUBAGENT",
273
+ statusCode: 500,
274
+ message: errorMsg.slice(0, 500),
275
+ stack: event.error?.stack || null,
276
+ file: null,
277
+ line: null,
278
+ timestamp: Date.now(),
279
+ });
280
+ }
281
+ });
282
+
283
+ // ── gateway_start: lifecycle tracking ───────────────────────
284
+ reg("gateway_start", (event, ctx) => {
285
+ console.log("[CLAW] OpenClaw gateway started — wolverine watching");
286
+ reportToWolverine("claw_health", {
287
+ status: "running",
288
+ detail: "gateway_start",
289
+ timestamp: Date.now(),
290
+ });
291
+ });
292
+
293
+ // ── gateway_stop: lifecycle tracking ────────────────────────
294
+ reg("gateway_stop", (event, ctx) => {
295
+ console.log("[CLAW] OpenClaw gateway stopping");
296
+ reportToWolverine("claw_health", {
297
+ status: "stopping",
298
+ detail: "gateway_stop",
299
+ errorCounts: { ..._errorCounts },
300
+ timestamp: Date.now(),
301
+ });
302
+ });
303
+
304
+ // ── session_end: track session errors ───────────────────────
305
+ reg("session_end", (event, ctx) => {
306
+ if (event?.error) {
307
+ console.warn(`[CLAW] Session error: ${event.error?.message || event.error}`);
308
+ }
309
+ });
310
+ }
311
+
312
+ // ── EventEmitter fallback hooks ─────────────────────────────────
313
+
314
+ /**
315
+ * Register basic EventEmitter hooks (works with all versions).
316
+ */
317
+ function _registerEventHooks(gateway, projectRoot) {
318
+ if (!gateway.on) return;
319
+
320
+ gateway.on("error", (err) => {
321
+ const msg = err?.message || String(err);
322
+ _trackError("agent", `Gateway error: ${msg}`);
323
+ console.error(`[CLAW] Gateway error: ${msg}`);
324
+
325
+ reportToWolverine("route_error", {
326
+ path: "claw://gateway",
327
+ method: "INTERNAL",
328
+ statusCode: 500,
329
+ message: msg.slice(0, 500),
330
+ stack: err?.stack || null,
331
+ file: null,
332
+ line: null,
333
+ timestamp: Date.now(),
334
+ });
335
+ });
336
+
337
+ // These may or may not exist depending on OpenClaw version
338
+ const optionalEvents = ["agent:error", "skill:error", "channel:error", "channel:disconnect"];
339
+ for (const evt of optionalEvents) {
340
+ try {
341
+ gateway.on(evt, (err) => {
342
+ const msg = err?.message || String(err);
343
+ const category = evt.split(":")[0];
344
+ _trackError(category === "skill" ? "tool" : "agent", `${evt}: ${msg}`);
345
+ console.error(`[CLAW] ${evt}: ${msg.slice(0, 100)}`);
346
+ });
347
+ } catch {}
348
+ }
349
+ }
350
+
351
+ // ── Tool registration ───────────────────────────────────────────
352
+
353
+ function _registerTools(gateway, tools) {
354
+ if (gateway.registerTools) {
355
+ gateway.registerTools(PLUGIN_NAME, tools);
356
+ } else if (gateway.addTools) {
357
+ gateway.addTools(tools);
358
+ } else if (gateway.skills?.register) {
359
+ gateway.skills.register(PLUGIN_NAME, {
360
+ description: "Wolverine self-healing integration — backup, brain, health, and error pipeline",
361
+ tools,
362
+ });
363
+ }
364
+ }
365
+
366
+ // ── Wolverine tools for the agent ───────────────────────────────
367
+
64
368
  function buildWolverineTools(projectRoot, config) {
65
369
  return [
66
370
  {
@@ -163,7 +467,7 @@ function buildWolverineTools(projectRoot, config) {
163
467
  },
164
468
  {
165
469
  name: "wolverine_health",
166
- description: "Get wolverine system health status (process, memory, backups, heal history)",
470
+ description: "Get wolverine system health status including error tracking from all OpenClaw subsystems",
167
471
  parameters: { type: "object", properties: {} },
168
472
  execute: async () => {
169
473
  const status = {
@@ -171,6 +475,8 @@ function buildWolverineTools(projectRoot, config) {
171
475
  memory: `${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`,
172
476
  pid: process.pid,
173
477
  nodeVersion: process.version,
478
+ errors: { ..._errorCounts },
479
+ recentErrors: _recentErrors.slice(-5).map(e => `[${e.category}] ${e.summary}`),
174
480
  };
175
481
 
176
482
  try {
@@ -211,7 +517,6 @@ function buildWolverineTools(projectRoot, config) {
211
517
  required: ["error"],
212
518
  },
213
519
  execute: async ({ error, file }) => {
214
- // Report to parent wolverine process for healing
215
520
  reportToWolverine("route_error", {
216
521
  path: "claw://agent",
217
522
  method: "INTERNAL",
@@ -225,50 +530,20 @@ function buildWolverineTools(projectRoot, config) {
225
530
  return "Error reported to wolverine healing pipeline.";
226
531
  },
227
532
  },
533
+ {
534
+ name: "wolverine_error_stats",
535
+ description: "Get error statistics from all OpenClaw subsystems (tools, agent, channels, LLM, subagents)",
536
+ parameters: { type: "object", properties: {} },
537
+ execute: async () => {
538
+ return JSON.stringify({
539
+ counts: { ..._errorCounts },
540
+ recent: _recentErrors.slice(-10),
541
+ }, null, 2);
542
+ },
543
+ },
228
544
  ];
229
545
  }
230
546
 
231
- /**
232
- * Handle gateway errors — report to wolverine for healing.
233
- */
234
- function handleGatewayError(err, projectRoot) {
235
- console.error(`[CLAW] Gateway error: ${err.message}`);
236
- reportToWolverine("route_error", {
237
- path: "claw://gateway",
238
- method: "INTERNAL",
239
- statusCode: 500,
240
- message: err.message,
241
- stack: err.stack,
242
- file: null,
243
- line: null,
244
- timestamp: Date.now(),
245
- });
246
- }
247
-
248
- /**
249
- * Handle agent errors.
250
- */
251
- function handleAgentError(err, projectRoot) {
252
- console.error(`[CLAW] Agent error: ${err.message}`);
253
- reportToWolverine("route_error", {
254
- path: "claw://agent",
255
- method: "INTERNAL",
256
- statusCode: 500,
257
- message: err.message,
258
- stack: err.stack,
259
- file: null,
260
- line: null,
261
- timestamp: Date.now(),
262
- });
263
- }
264
-
265
- /**
266
- * Handle skill errors.
267
- */
268
- function handleSkillError(err, projectRoot) {
269
- console.warn(`[CLAW] Skill error (non-fatal): ${err.message}`);
270
- }
271
-
272
547
  /**
273
548
  * Report to wolverine parent process via IPC.
274
549
  */