wolverine-ai 6.1.2 → 6.2.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 +171 -0
- package/bin/wolverine-claw.js +182 -0
- package/bin/wolverine.js +27 -0
- package/package.json +8 -2
- package/src/brain/brain.js +20 -0
- package/src/claw/claw-runner.js +422 -0
- package/src/claw/setup.js +871 -0
- package/wolverine-claw/config/settings.json +115 -0
- package/wolverine-claw/index.js +297 -0
- package/wolverine-claw/plugins/wolverine-integration.js +283 -0
- package/wolverine-claw/workspace/.gitkeep +0 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wolverine Claw Runner — Process manager for OpenClaw gateway.
|
|
3
|
+
*
|
|
4
|
+
* Extends wolverine's self-healing to the claw environment:
|
|
5
|
+
* - Spawns wolverine-claw/index.js as a child process
|
|
6
|
+
* - Monitors for crashes and gateway errors via IPC
|
|
7
|
+
* - Triggers AI-powered healing on failures
|
|
8
|
+
* - Manages workspace backups and rollbacks
|
|
9
|
+
* - Health monitoring via WebSocket probe (not HTTP)
|
|
10
|
+
*
|
|
11
|
+
* This is the claw equivalent of src/core/runner.js.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { spawn } = require("child_process");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const net = require("net");
|
|
18
|
+
const chalk = require("chalk");
|
|
19
|
+
const { heal } = require("../core/wolverine");
|
|
20
|
+
const { BackupManager } = require("../backup/backup-manager");
|
|
21
|
+
const { EventLogger, EVENT_TYPES } = require("../logger/event-logger");
|
|
22
|
+
const { Brain } = require("../brain/brain");
|
|
23
|
+
const { initRedactor } = require("../security/secret-redactor");
|
|
24
|
+
const { RateLimiter } = require("../security/rate-limiter");
|
|
25
|
+
const { TokenTracker } = require("../logger/token-tracker");
|
|
26
|
+
const { setTokenTracker } = require("../core/ai-client");
|
|
27
|
+
const { LoopGuard, ensureSingleProcess } = require("../skills/loop-guard");
|
|
28
|
+
const { loadConfig } = require("../core/config");
|
|
29
|
+
|
|
30
|
+
class ClawRunner {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.cwd = options.cwd || process.cwd();
|
|
33
|
+
this.clawDir = path.join(this.cwd, "wolverine-claw");
|
|
34
|
+
this.scriptPath = path.join(this.clawDir, "index.js");
|
|
35
|
+
this.child = null;
|
|
36
|
+
this.running = false;
|
|
37
|
+
this.retryCount = 0;
|
|
38
|
+
this.maxRetries = options.maxRetries || 5;
|
|
39
|
+
|
|
40
|
+
// Healing state
|
|
41
|
+
this._healInProgress = false;
|
|
42
|
+
this._stderrBuffer = "";
|
|
43
|
+
this._lastStartTime = null;
|
|
44
|
+
this._lastBackupId = null;
|
|
45
|
+
|
|
46
|
+
// Load claw-specific config
|
|
47
|
+
this.clawConfig = this._loadClawConfig();
|
|
48
|
+
|
|
49
|
+
// Load wolverine core config for shared settings
|
|
50
|
+
this.config = loadConfig();
|
|
51
|
+
|
|
52
|
+
this._initSubsystems();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load claw configuration from wolverine-claw/config/settings.json.
|
|
57
|
+
*/
|
|
58
|
+
_loadClawConfig() {
|
|
59
|
+
const configPath = path.join(this.clawDir, "config", "settings.json");
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error(chalk.red(`[CLAW] Config not found at ${configPath}`));
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize subsystems — backup, brain, logging, rate limiting.
|
|
70
|
+
*/
|
|
71
|
+
_initSubsystems() {
|
|
72
|
+
this.redactor = initRedactor(this.cwd);
|
|
73
|
+
this.backupManager = new BackupManager(this.cwd);
|
|
74
|
+
this.logger = new EventLogger(this.cwd);
|
|
75
|
+
this.logger.setRedactor(this.redactor);
|
|
76
|
+
this.tokenTracker = new TokenTracker(this.cwd);
|
|
77
|
+
setTokenTracker(this.tokenTracker);
|
|
78
|
+
this.brain = new Brain(this.cwd);
|
|
79
|
+
|
|
80
|
+
const healCfg = this.clawConfig.healing || {};
|
|
81
|
+
this.rateLimiter = new RateLimiter({
|
|
82
|
+
maxCallsPerWindow: this.config.rateLimiting?.maxCallsPerWindow || 32,
|
|
83
|
+
windowMs: this.config.rateLimiting?.windowMs || 100000,
|
|
84
|
+
minGapMs: this.config.rateLimiting?.minGapMs || 5000,
|
|
85
|
+
maxTokensPerHour: this.config.rateLimiting?.maxTokensPerHour || 1000000,
|
|
86
|
+
maxGlobalHealsPerWindow: healCfg.maxHealsPerWindow || 5,
|
|
87
|
+
globalWindowMs: healCfg.windowMs || 300000,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.loopGuard = new LoopGuard(this.cwd, {
|
|
91
|
+
maxAttempts: healCfg.loopMaxAttempts || 3,
|
|
92
|
+
windowMs: healCfg.loopWindowMs || 600000,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Start the claw process.
|
|
98
|
+
*/
|
|
99
|
+
async start() {
|
|
100
|
+
// Validate claw directory exists
|
|
101
|
+
if (!fs.existsSync(this.scriptPath)) {
|
|
102
|
+
console.error(chalk.red(`\n [CLAW] Entry point not found: ${this.scriptPath}`));
|
|
103
|
+
console.log(chalk.gray(" Run wolverine --init-claw to scaffold the wolverine-claw directory.\n"));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.running = true;
|
|
108
|
+
this.retryCount = 0;
|
|
109
|
+
|
|
110
|
+
console.log(chalk.blue.bold("\n 🐾 Wolverine Claw — Agentic Agent with Self-Healing\n"));
|
|
111
|
+
|
|
112
|
+
// Initialize brain
|
|
113
|
+
try {
|
|
114
|
+
await this.brain.init();
|
|
115
|
+
console.log(chalk.gray(" 🧠 Brain: initialized"));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.log(chalk.yellow(` ⚠️ Brain init: ${err.message}`));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Log config
|
|
121
|
+
const gw = this.clawConfig.gateway || {};
|
|
122
|
+
console.log(chalk.gray(` Gateway: ws://${gw.host || "127.0.0.1"}:${gw.port || 18789}`));
|
|
123
|
+
console.log(chalk.gray(` Agent: ${this.clawConfig.agent?.model || "claude-sonnet-4-6"}`));
|
|
124
|
+
console.log(chalk.gray(` Workspace: ${path.resolve(this.clawConfig.workspace?.path || "wolverine-claw/workspace")}`));
|
|
125
|
+
console.log(chalk.gray(` Healing: ${this.clawConfig.healing?.enabled !== false ? "enabled" : "disabled"}`));
|
|
126
|
+
console.log(chalk.gray(` Max heals: ${this.clawConfig.healing?.maxHealsPerWindow || 5} per ${Math.round((this.clawConfig.healing?.windowMs || 300000) / 60000)}min`));
|
|
127
|
+
|
|
128
|
+
// Enabled channels
|
|
129
|
+
const channels = this.clawConfig.channels || {};
|
|
130
|
+
const enabledChannels = Object.entries(channels)
|
|
131
|
+
.filter(([k, v]) => !k.startsWith("_") && v.enabled)
|
|
132
|
+
.map(([k]) => k);
|
|
133
|
+
console.log(chalk.gray(` Channels: ${enabledChannels.length > 0 ? enabledChannels.join(", ") : "terminal only"}`));
|
|
134
|
+
console.log("");
|
|
135
|
+
|
|
136
|
+
// Create startup backup
|
|
137
|
+
try {
|
|
138
|
+
this._lastBackupId = this.backupManager.createBackup("claw-pre-start");
|
|
139
|
+
console.log(chalk.gray(` 📸 Startup backup: ${this._lastBackupId}`));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.log(chalk.yellow(` ⚠️ Backup failed: ${err.message}`));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log("");
|
|
145
|
+
this._spawn();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Spawn the claw child process.
|
|
150
|
+
*/
|
|
151
|
+
_spawn() {
|
|
152
|
+
if (!this.running) return;
|
|
153
|
+
|
|
154
|
+
this._lastStartTime = Date.now();
|
|
155
|
+
this._stderrBuffer = "";
|
|
156
|
+
|
|
157
|
+
// Spawn wolverine-claw/index.js with IPC channel
|
|
158
|
+
const errorHookPath = path.join(this.cwd, "src", "core", "error-hook.js");
|
|
159
|
+
this.child = spawn(process.execPath, [this.scriptPath], {
|
|
160
|
+
cwd: this.cwd,
|
|
161
|
+
stdio: ["inherit", "inherit", "pipe", "ipc"],
|
|
162
|
+
env: {
|
|
163
|
+
...process.env,
|
|
164
|
+
WOLVERINE_MANAGED: "1",
|
|
165
|
+
WOLVERINE_CLAW: "1",
|
|
166
|
+
NODE_ENV: process.env.NODE_ENV || "development",
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
console.log(chalk.green(` 🐾 Claw started (PID ${this.child.pid})`));
|
|
171
|
+
|
|
172
|
+
// Collect stderr
|
|
173
|
+
this.child.stderr.on("data", (chunk) => {
|
|
174
|
+
const text = chunk.toString();
|
|
175
|
+
this._stderrBuffer += text;
|
|
176
|
+
process.stderr.write(chunk);
|
|
177
|
+
|
|
178
|
+
// Cap buffer size
|
|
179
|
+
if (this._stderrBuffer.length > 10000) {
|
|
180
|
+
this._stderrBuffer = this._stderrBuffer.slice(-8000);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// IPC messages from claw process
|
|
185
|
+
this.child.on("message", (msg) => {
|
|
186
|
+
if (!msg || typeof msg !== "object") return;
|
|
187
|
+
|
|
188
|
+
if (msg.type === "route_error") {
|
|
189
|
+
this._handleError(msg);
|
|
190
|
+
} else if (msg.type === "claw_health") {
|
|
191
|
+
this._handleHealthReport(msg);
|
|
192
|
+
} else if (msg.type === "claw_heartbeat") {
|
|
193
|
+
// Healthy heartbeat — reset retry count if stable
|
|
194
|
+
if (Date.now() - this._lastStartTime > 60000) {
|
|
195
|
+
this.retryCount = 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Process exit
|
|
201
|
+
this.child.on("exit", (code, signal) => {
|
|
202
|
+
this.child = null;
|
|
203
|
+
|
|
204
|
+
if (!this.running) {
|
|
205
|
+
console.log(chalk.gray("\n 🐾 Claw stopped."));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log(chalk.red(`\n 💥 Claw crashed (code=${code}, signal=${signal})`));
|
|
210
|
+
|
|
211
|
+
this.logger.info("claw.crash", `Claw process exited`, {
|
|
212
|
+
code, signal,
|
|
213
|
+
uptime: Date.now() - this._lastStartTime,
|
|
214
|
+
retryCount: this.retryCount,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
this._handleCrash(code, signal);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Start gateway health probe
|
|
221
|
+
this._startGatewayProbe();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Handle a crash — heal and restart.
|
|
226
|
+
*/
|
|
227
|
+
async _handleCrash(code, signal) {
|
|
228
|
+
this.retryCount++;
|
|
229
|
+
|
|
230
|
+
if (this.retryCount > this.maxRetries) {
|
|
231
|
+
console.log(chalk.red(`\n ❌ Max retries (${this.maxRetries}) reached. Stopping.`));
|
|
232
|
+
|
|
233
|
+
// Rollback to startup backup if available
|
|
234
|
+
if (this._lastBackupId) {
|
|
235
|
+
try {
|
|
236
|
+
this.backupManager.rollback(this._lastBackupId);
|
|
237
|
+
console.log(chalk.yellow(` ↩️ Rolled back to startup snapshot: ${this._lastBackupId}`));
|
|
238
|
+
} catch {}
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log(chalk.yellow(` 🔄 Retry ${this.retryCount}/${this.maxRetries}...`));
|
|
244
|
+
|
|
245
|
+
// If healing is enabled and we have an error, try to heal
|
|
246
|
+
const healingEnabled = this.clawConfig.healing?.enabled !== false;
|
|
247
|
+
if (healingEnabled && this._stderrBuffer.trim()) {
|
|
248
|
+
await this._tryHeal(this._stderrBuffer, null);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Restart with delay
|
|
252
|
+
const delay = Math.min(1000 * this.retryCount, 5000);
|
|
253
|
+
setTimeout(() => this._spawn(), delay);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Handle an IPC error from the claw process.
|
|
258
|
+
*/
|
|
259
|
+
async _handleError(errorMsg) {
|
|
260
|
+
console.log(chalk.red(` ⚡ Claw error: ${errorMsg.message?.slice(0, 100)}`));
|
|
261
|
+
|
|
262
|
+
const healingEnabled = this.clawConfig.healing?.enabled !== false;
|
|
263
|
+
if (!healingEnabled) return;
|
|
264
|
+
|
|
265
|
+
const errorText = `${errorMsg.message}\n${errorMsg.stack || ""}`;
|
|
266
|
+
await this._tryHeal(errorText, errorMsg.file);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Try to heal an error using wolverine's AI pipeline.
|
|
271
|
+
*/
|
|
272
|
+
async _tryHeal(errorText, file) {
|
|
273
|
+
if (this._healInProgress) {
|
|
274
|
+
console.log(chalk.gray(" ⏳ Heal already in progress, skipping..."));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Empty stderr → just restart, no AI
|
|
279
|
+
if (!errorText.trim()) return;
|
|
280
|
+
|
|
281
|
+
// Rate limit check
|
|
282
|
+
if (!this.rateLimiter.canHeal()) {
|
|
283
|
+
console.log(chalk.yellow(" ⚠️ Heal rate limit reached"));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Loop guard check
|
|
288
|
+
const errorSig = errorText.slice(0, 200);
|
|
289
|
+
if (this.loopGuard.isLooping(errorSig)) {
|
|
290
|
+
console.log(chalk.red(" 🔁 Loop detected — same error failed too many times. Stopping."));
|
|
291
|
+
this.loopGuard.generateBugReport(errorSig, errorText);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this._healInProgress = true;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
console.log(chalk.blue("\n 🩺 Healing claw..."));
|
|
299
|
+
|
|
300
|
+
// Create backup before healing
|
|
301
|
+
try {
|
|
302
|
+
this._lastBackupId = this.backupManager.createBackup("claw-pre-heal");
|
|
303
|
+
} catch {}
|
|
304
|
+
|
|
305
|
+
// Run wolverine heal pipeline
|
|
306
|
+
const healTimeoutMs = this.clawConfig.healing?.healTimeoutMs || 300000;
|
|
307
|
+
const result = await Promise.race([
|
|
308
|
+
heal({
|
|
309
|
+
stderr: errorText,
|
|
310
|
+
scriptPath: this.scriptPath,
|
|
311
|
+
cwd: this.cwd,
|
|
312
|
+
brain: this.brain,
|
|
313
|
+
rateLimiter: this.rateLimiter,
|
|
314
|
+
backupManager: this.backupManager,
|
|
315
|
+
logger: this.logger,
|
|
316
|
+
sandbox: { isAllowed: (p) => !p.includes("node_modules") && !p.includes("src/") },
|
|
317
|
+
redactor: this.redactor,
|
|
318
|
+
loopGuard: this.loopGuard,
|
|
319
|
+
}),
|
|
320
|
+
new Promise((_, reject) =>
|
|
321
|
+
setTimeout(() => reject(new Error("Heal timeout")), healTimeoutMs)
|
|
322
|
+
),
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
if (result?.success) {
|
|
326
|
+
console.log(chalk.green(" ✅ Heal succeeded!"));
|
|
327
|
+
this.retryCount = 0;
|
|
328
|
+
} else {
|
|
329
|
+
console.log(chalk.yellow(` ⚠️ Heal did not succeed: ${result?.reason || "unknown"}`));
|
|
330
|
+
|
|
331
|
+
// Rollback
|
|
332
|
+
if (this._lastBackupId) {
|
|
333
|
+
try {
|
|
334
|
+
this.backupManager.rollback(this._lastBackupId);
|
|
335
|
+
console.log(chalk.yellow(` ↩️ Rolled back`));
|
|
336
|
+
} catch {}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} catch (err) {
|
|
340
|
+
console.log(chalk.red(` ❌ Heal failed: ${err.message}`));
|
|
341
|
+
} finally {
|
|
342
|
+
this._healInProgress = false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Handle health status reports from the claw process.
|
|
348
|
+
*/
|
|
349
|
+
_handleHealthReport(msg) {
|
|
350
|
+
if (msg.status === "error" || msg.status === "crashed") {
|
|
351
|
+
console.log(chalk.yellow(` ⚠️ Claw health: ${msg.status} — ${msg.detail || "unknown"}`));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Probe the gateway WebSocket port to check if it's alive.
|
|
357
|
+
*/
|
|
358
|
+
_startGatewayProbe() {
|
|
359
|
+
const port = this.clawConfig.gateway?.port || 18789;
|
|
360
|
+
const host = this.clawConfig.gateway?.host || "127.0.0.1";
|
|
361
|
+
const probeInterval = 30000; // every 30s
|
|
362
|
+
|
|
363
|
+
// Wait 10s for gateway to start before probing
|
|
364
|
+
this._gatewayProbeTimer = setTimeout(() => {
|
|
365
|
+
this._gatewayProbeTimer = setInterval(() => {
|
|
366
|
+
if (!this.running || !this.child) return;
|
|
367
|
+
|
|
368
|
+
const socket = new net.Socket();
|
|
369
|
+
socket.setTimeout(5000);
|
|
370
|
+
|
|
371
|
+
socket.on("connect", () => {
|
|
372
|
+
socket.destroy();
|
|
373
|
+
// Gateway is alive
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
socket.on("error", () => {
|
|
377
|
+
socket.destroy();
|
|
378
|
+
console.log(chalk.yellow(` ⚠️ Gateway probe failed (port ${port})`));
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
socket.on("timeout", () => {
|
|
382
|
+
socket.destroy();
|
|
383
|
+
console.log(chalk.yellow(` ⚠️ Gateway probe timeout (port ${port})`));
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
socket.connect(port, host);
|
|
387
|
+
}, probeInterval);
|
|
388
|
+
}, 10000);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Stop the claw process and all monitors.
|
|
393
|
+
*/
|
|
394
|
+
stop() {
|
|
395
|
+
this.running = false;
|
|
396
|
+
|
|
397
|
+
if (this._gatewayProbeTimer) {
|
|
398
|
+
clearTimeout(this._gatewayProbeTimer);
|
|
399
|
+
clearInterval(this._gatewayProbeTimer);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (this.child) {
|
|
403
|
+
try {
|
|
404
|
+
this.child.kill("SIGTERM");
|
|
405
|
+
} catch {}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Restart the claw process.
|
|
411
|
+
*/
|
|
412
|
+
restart() {
|
|
413
|
+
this.stop();
|
|
414
|
+
setTimeout(() => {
|
|
415
|
+
this.running = true;
|
|
416
|
+
this.retryCount = 0;
|
|
417
|
+
this._spawn();
|
|
418
|
+
}, 2000);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
module.exports = { ClawRunner };
|