wolverine-ai 2.8.2 → 2.9.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 +16 -0
- package/package.json +1 -1
- package/server/config/settings.json +2 -2
- package/src/brain/brain.js +8 -0
- package/src/core/runner.js +46 -2
- package/src/core/wolverine.js +7 -0
- package/src/index.js +3 -0
- package/src/skills/loop-guard.js +267 -0
package/README.md
CHANGED
|
@@ -429,6 +429,22 @@ Wolverine (single process manager)
|
|
|
429
429
|
|
|
430
430
|
---
|
|
431
431
|
|
|
432
|
+
## Token Protection
|
|
433
|
+
|
|
434
|
+
Three layers prevent token waste:
|
|
435
|
+
|
|
436
|
+
| Layer | What it catches | Cost |
|
|
437
|
+
|-------|----------------|------|
|
|
438
|
+
| **Empty stderr guard** | Signal kills, clean shutdowns with no error | $0.00 |
|
|
439
|
+
| **Loop guard** | Same error failing 3+ times in 10min → files bug report, stops healing | $0.00 after detection |
|
|
440
|
+
| **Global rate limit** | Max 5 heals per 5 minutes regardless of error | Caps total spend |
|
|
441
|
+
|
|
442
|
+
**Process dedup:** PID file ensures only one wolverine instance runs. Kills old process on startup.
|
|
443
|
+
|
|
444
|
+
**Bug reports:** When loop guard triggers, generates a security-scanned report (no secrets/injection patterns) and sends to the platform backend for human review.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
432
448
|
## Cost Optimization
|
|
433
449
|
|
|
434
450
|
Wolverine minimizes AI spend through 7 techniques:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.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": {
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"hybrid_settings": {
|
|
35
35
|
"reasoning": "claude-haiku-4-5",
|
|
36
36
|
"coding": "claude-sonnet-4-6",
|
|
37
|
-
"chat": "
|
|
37
|
+
"chat": "gpt-5-nano",
|
|
38
38
|
"tool": "claude-sonnet-4-6",
|
|
39
39
|
"classifier": "gpt-4o-mini",
|
|
40
40
|
"audit": "gpt-4o-mini",
|
|
41
|
-
"compacting": "
|
|
41
|
+
"compacting": "gpt-4o-mini",
|
|
42
42
|
"research": "o4-mini-deep-research",
|
|
43
43
|
"embedding": "text-embedding-3-small"
|
|
44
44
|
},
|
package/src/brain/brain.js
CHANGED
|
@@ -249,6 +249,14 @@ const SEED_DOCS = [
|
|
|
249
249
|
text: "Auto-update: wolverine checks npm registry hourly for new versions. When found, runs npm install wolverine-ai@latest, backs up settings.json/.env.local before update and restores after. Config: autoUpdate.enabled (default true) in settings.json. Disable with WOLVERINE_AUTO_UPDATE=false env var. On successful update, signals runner to restart. Protected files never overwritten: settings.json, .env.local, .env, db.js. Update check runs 30s after startup then every hour (configurable via autoUpdate.intervalMs).",
|
|
250
250
|
metadata: { topic: "auto-update" },
|
|
251
251
|
},
|
|
252
|
+
{
|
|
253
|
+
text: "Loop guard: detects infinite heal loops and stops burning tokens. Tracks heal attempts by error signature — if 3+ heals fail on same error in 10 minutes, STOPS healing and generates a bug report. Bug report sent to platform backend for human review (security scanned for injection/secrets first). 30-minute cooldown after bug report filed. Process dedup via PID file (.wolverine/wolverine.pid) ensures only one wolverine instance runs — kills old process on startup. Config: WOLVERINE_LOOP_MAX_ATTEMPTS (default 3), WOLVERINE_LOOP_WINDOW_MS (default 600000).",
|
|
254
|
+
metadata: { topic: "loop-guard" },
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
text: "Token waste prevention: 3 layers. (1) Empty stderr guard — signal kills with no error output just restart, no AI ($0.00). (2) Loop guard — 3 failed heals on same error → stop and file bug report, no more AI calls. (3) Global rate limit — max 5 heals per 5 minutes regardless of error signature. Idle server burns exactly $0.00 in tokens.",
|
|
258
|
+
metadata: { topic: "token-protection" },
|
|
259
|
+
},
|
|
252
260
|
{
|
|
253
261
|
text: "Cost optimization: 7 techniques reduce heal cost from $0.31 to $0.02 for simple errors. (1) Verifier skips route probe for simple errors (TypeError/ReferenceError/SyntaxError) — trusts syntax+boot, ErrorMonitor is safety net. Prevents false-rejection cascades. (2) Sub-agents use Haiku (classifier model) for explore/plan/verify/research — only fixer uses Sonnet/Opus. 6 Haiku calls=$0.006 vs 6 Sonnet calls=$0.12. (3) Agent context compacted every 3 turns using compacting model — prevents 15K→95K token blowup. (4) Brain checked for cached fix patterns before AI — repeat errors cost $0. (5) Token budgets capped by error complexity: simple=20K agent budget, moderate=50K, complex=100K. Simple errors get 4 agent turns max. (6) Prior attempt summaries (not full context) passed between iterations — concise 'do NOT repeat' directives. (7) Fast path includes last known good backup code so AI can revert broken additions instead of patching around them.",
|
|
254
262
|
metadata: { topic: "cost-optimization" },
|
package/src/core/runner.js
CHANGED
|
@@ -23,6 +23,7 @@ const { Notifier } = require("../notifications/notifier");
|
|
|
23
23
|
const { loadConfig } = require("./config");
|
|
24
24
|
const { ErrorMonitor } = require("../monitor/error-monitor");
|
|
25
25
|
const { startAutoUpdate, stopAutoUpdate } = require("../platform/auto-update");
|
|
26
|
+
const { LoopGuard, ensureSingleProcess } = require("../skills/loop-guard");
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* The Wolverine process runner — v3.
|
|
@@ -104,6 +105,12 @@ class WolverineRunner {
|
|
|
104
105
|
onError: (routePath, errorDetails) => this._healFromError(routePath, errorDetails),
|
|
105
106
|
});
|
|
106
107
|
|
|
108
|
+
// Loop guard — detects infinite heal loops, generates bug reports
|
|
109
|
+
this.loopGuard = new LoopGuard(this.cwd, {
|
|
110
|
+
maxAttempts: parseInt(process.env.WOLVERINE_LOOP_MAX_ATTEMPTS, 10) || 3,
|
|
111
|
+
windowMs: parseInt(process.env.WOLVERINE_LOOP_WINDOW_MS, 10) || 600000,
|
|
112
|
+
});
|
|
113
|
+
|
|
107
114
|
// Brain — semantic memory + project context
|
|
108
115
|
this.brain = new Brain(this.cwd);
|
|
109
116
|
|
|
@@ -147,6 +154,9 @@ class WolverineRunner {
|
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
async start() {
|
|
157
|
+
// Ensure only one wolverine instance runs — kill any old process
|
|
158
|
+
ensureSingleProcess(this.cwd);
|
|
159
|
+
|
|
150
160
|
this.running = true;
|
|
151
161
|
this.retryCount = 0;
|
|
152
162
|
|
|
@@ -405,12 +415,21 @@ class WolverineRunner {
|
|
|
405
415
|
|
|
406
416
|
if (!this.running) return;
|
|
407
417
|
|
|
408
|
-
|
|
418
|
+
// Clean exit or graceful shutdown — don't heal
|
|
419
|
+
if (code === 0 || signal === "SIGTERM" || signal === "SIGINT") {
|
|
409
420
|
console.log(chalk.green("\n✅ Process exited cleanly."));
|
|
410
421
|
this.logger.info(EVENT_TYPES.PROCESS_HEALTHY, "Process exited cleanly");
|
|
411
422
|
return;
|
|
412
423
|
}
|
|
413
424
|
|
|
425
|
+
// Killed by signal with no stderr — just restart, don't waste tokens healing
|
|
426
|
+
if (!this._stderrBuffer.trim() || this._stderrBuffer.trim().length < 10) {
|
|
427
|
+
console.log(chalk.yellow(`\n⚠️ Process killed (code: ${code}, signal: ${signal}) — no error to heal, restarting`));
|
|
428
|
+
this.logger.warn(EVENT_TYPES.PROCESS_CRASH, `Killed with no stderr (code: ${code}, signal: ${signal})`, { exitCode: code, signal });
|
|
429
|
+
this._spawn();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
414
433
|
const uptime = Date.now() - this._lastStartTime;
|
|
415
434
|
console.log(chalk.red(`\n💥 Process crashed with exit code ${code} (uptime: ${Math.round(uptime / 1000)}s)`));
|
|
416
435
|
this.logger.error(EVENT_TYPES.PROCESS_CRASH, `Crashed with code ${code}`, {
|
|
@@ -466,6 +485,28 @@ class WolverineRunner {
|
|
|
466
485
|
this._healInProgress = true;
|
|
467
486
|
this._healStatus = { active: true, error: this._stderrBuffer.slice(0, 200), phase: "diagnosing", startedAt: Date.now() };
|
|
468
487
|
|
|
488
|
+
// Loop guard: check if we're stuck repeating failed heals
|
|
489
|
+
const errorSig = RateLimiter.signature(this._stderrBuffer.slice(0, 200), "");
|
|
490
|
+
const loopCheck = this.loopGuard.check(errorSig);
|
|
491
|
+
if (!loopCheck.allowed) {
|
|
492
|
+
console.log(chalk.red(`\n 🔄 ${loopCheck.reason}`));
|
|
493
|
+
if (loopCheck.shouldReport) {
|
|
494
|
+
const report = await this.loopGuard.generateBugReport({
|
|
495
|
+
errorMessage: this._stderrBuffer.slice(0, 500),
|
|
496
|
+
filePath: null,
|
|
497
|
+
attempts: loopCheck.attempts,
|
|
498
|
+
brain: this.brain,
|
|
499
|
+
logger: this.logger,
|
|
500
|
+
});
|
|
501
|
+
await this.loopGuard.sendToBackend(report);
|
|
502
|
+
}
|
|
503
|
+
this._healInProgress = false;
|
|
504
|
+
this._healStatus = null;
|
|
505
|
+
// Just restart without healing — the bug report is filed
|
|
506
|
+
this._spawn();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
469
510
|
try {
|
|
470
511
|
const result = await heal({
|
|
471
512
|
stderr: this._stderrBuffer,
|
|
@@ -482,9 +523,12 @@ class WolverineRunner {
|
|
|
482
523
|
repairHistory: this.repairHistory,
|
|
483
524
|
});
|
|
484
525
|
|
|
526
|
+
// Record attempt for loop guard
|
|
527
|
+
this.loopGuard.record(errorSig, result.healed, result.agentStats?.totalTokens || 0);
|
|
528
|
+
|
|
485
529
|
if (result.healed) {
|
|
486
530
|
this._lastBackupId = result.backupId;
|
|
487
|
-
this.retryCount = 0;
|
|
531
|
+
this.retryCount = 0;
|
|
488
532
|
const mode = result.mode === "agent" ? "multi-file agent" : result.mode || "fast path";
|
|
489
533
|
console.log(chalk.green(`\n🐺 Wolverine healed the error via ${mode}! Restarting...\n`));
|
|
490
534
|
|
package/src/core/wolverine.js
CHANGED
|
@@ -45,6 +45,13 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
45
45
|
const healStartTime = Date.now();
|
|
46
46
|
const { redact, hasSecrets } = require("../security/secret-redactor");
|
|
47
47
|
|
|
48
|
+
// Guard: don't burn tokens on empty stderr (signal kills, clean shutdowns, etc.)
|
|
49
|
+
if (!stderr || stderr.trim().length < 10) {
|
|
50
|
+
console.log(chalk.yellow("\n🐺 Empty stderr — nothing to heal (likely signal kill)"));
|
|
51
|
+
if (logger) logger.warn(EVENT_TYPES.HEAL_FAILED, "Empty stderr — skipping heal");
|
|
52
|
+
return { healed: false, explanation: "Empty stderr — nothing to diagnose" };
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
// Redact secrets BEFORE any processing, logging, or AI calls
|
|
49
56
|
const safeStderr = redact(stderr);
|
|
50
57
|
|
package/src/index.js
CHANGED
|
@@ -38,6 +38,7 @@ const { diagnose: diagnoseDeps, healthReport: depsHealthReport, getMigration } =
|
|
|
38
38
|
const { checkForUpdate, upgrade: upgradeWolverine, getCurrentVersion } = require("./platform/auto-update");
|
|
39
39
|
const { safeUpdate, createSafeBackup, listSafeBackups, restoreFromSafeBackup } = require("./skills/update");
|
|
40
40
|
const { backup, rollback, rollbackLatest, undoRollback, listBackups } = require("./skills/backup");
|
|
41
|
+
const { LoopGuard, ensureSingleProcess } = require("./skills/loop-guard");
|
|
41
42
|
|
|
42
43
|
module.exports = {
|
|
43
44
|
// Core
|
|
@@ -114,4 +115,6 @@ module.exports = {
|
|
|
114
115
|
rollbackLatest,
|
|
115
116
|
undoRollback,
|
|
116
117
|
listBackups,
|
|
118
|
+
LoopGuard,
|
|
119
|
+
ensureSingleProcess,
|
|
117
120
|
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop Guard — detects infinite heal loops and generates bug reports.
|
|
3
|
+
*
|
|
4
|
+
* Problem: wolverine can get stuck repeating the same fix attempt,
|
|
5
|
+
* burning tokens with nothing changing. This happens when:
|
|
6
|
+
* - The error is beyond the agent's capability to fix
|
|
7
|
+
* - The fix works but something else breaks, causing a new error
|
|
8
|
+
* - External deps are missing (not a code issue)
|
|
9
|
+
* - The verifier keeps rejecting a correct fix
|
|
10
|
+
*
|
|
11
|
+
* Solution: track heal attempts with signatures. If the same error
|
|
12
|
+
* (or same file) triggers N heals in T minutes with no success,
|
|
13
|
+
* STOP healing and generate a bug report instead.
|
|
14
|
+
*
|
|
15
|
+
* Bug reports are sent to the platform backend for human review.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const chalk = require("chalk");
|
|
21
|
+
|
|
22
|
+
const LOOP_FILE = ".wolverine/loop-guard.json";
|
|
23
|
+
|
|
24
|
+
class LoopGuard {
|
|
25
|
+
constructor(cwd, options = {}) {
|
|
26
|
+
this.cwd = cwd;
|
|
27
|
+
this.maxAttempts = options.maxAttempts || 3; // max heals on same error before giving up
|
|
28
|
+
this.windowMs = options.windowMs || 600000; // 10 minute window
|
|
29
|
+
this.cooldownMs = options.cooldownMs || 1800000; // 30 min cooldown after bug report
|
|
30
|
+
this._attempts = []; // { signature, timestamp, success, tokens }
|
|
31
|
+
this._bugReports = []; // { signature, report, timestamp }
|
|
32
|
+
this._load();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if healing should proceed for this error.
|
|
37
|
+
* Returns { allowed: boolean, reason?: string }
|
|
38
|
+
*/
|
|
39
|
+
check(errorSignature) {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
|
|
42
|
+
// Clean old attempts outside window
|
|
43
|
+
this._attempts = this._attempts.filter(a => now - a.timestamp < this.windowMs);
|
|
44
|
+
|
|
45
|
+
// Count recent attempts on this signature
|
|
46
|
+
const recentAttempts = this._attempts.filter(a => a.signature === errorSignature);
|
|
47
|
+
const recentFailures = recentAttempts.filter(a => !a.success);
|
|
48
|
+
|
|
49
|
+
// Check cooldown from previous bug report on this signature
|
|
50
|
+
const lastReport = this._bugReports.find(r => r.signature === errorSignature);
|
|
51
|
+
if (lastReport && now - lastReport.timestamp < this.cooldownMs) {
|
|
52
|
+
const remaining = Math.round((this.cooldownMs - (now - lastReport.timestamp)) / 60000);
|
|
53
|
+
return { allowed: false, reason: `Bug report filed ${remaining}min ago — cooldown active` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (recentFailures.length >= this.maxAttempts) {
|
|
57
|
+
return {
|
|
58
|
+
allowed: false,
|
|
59
|
+
reason: `Loop detected: ${recentFailures.length} failed heals on same error in ${Math.round(this.windowMs / 60000)}min`,
|
|
60
|
+
shouldReport: true,
|
|
61
|
+
attempts: recentAttempts,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { allowed: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Record a heal attempt.
|
|
70
|
+
*/
|
|
71
|
+
record(errorSignature, success, tokens = 0) {
|
|
72
|
+
this._attempts.push({
|
|
73
|
+
signature: errorSignature,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
success,
|
|
76
|
+
tokens,
|
|
77
|
+
});
|
|
78
|
+
this._save();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generate a bug report using AI reasoning.
|
|
83
|
+
* Compacts all context into a structured report for human review.
|
|
84
|
+
*/
|
|
85
|
+
async generateBugReport({ errorMessage, filePath, attempts, brain, logger }) {
|
|
86
|
+
const { redact } = require("../security/secret-redactor");
|
|
87
|
+
const safeError = redact(errorMessage || "");
|
|
88
|
+
|
|
89
|
+
// Compact attempt history
|
|
90
|
+
const attemptSummary = (attempts || this._attempts.slice(-5)).map(a =>
|
|
91
|
+
`[${new Date(a.timestamp).toISOString().slice(11, 19)}] ${a.success ? "OK" : "FAIL"} ${a.tokens} tokens`
|
|
92
|
+
).join("\n");
|
|
93
|
+
|
|
94
|
+
// Get brain context about this error
|
|
95
|
+
let brainContext = "";
|
|
96
|
+
if (brain && brain._initialized) {
|
|
97
|
+
try {
|
|
98
|
+
const results = brain.store.keywordSearch(safeError, { topK: 3 });
|
|
99
|
+
brainContext = results.map(r => r.text.slice(0, 100)).join("\n");
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build report without AI (save tokens — the whole point is to stop burning them)
|
|
104
|
+
const report = {
|
|
105
|
+
type: "bug_report",
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
iso: new Date().toISOString(),
|
|
108
|
+
error: safeError.slice(0, 500),
|
|
109
|
+
file: filePath || "unknown",
|
|
110
|
+
attempts: this._attempts.filter(a => a.signature === (filePath + "::" + safeError.slice(0, 50))).length,
|
|
111
|
+
totalTokensBurned: this._attempts.reduce((s, a) => s + (a.tokens || 0), 0),
|
|
112
|
+
history: attemptSummary,
|
|
113
|
+
brainContext: brainContext.slice(0, 300),
|
|
114
|
+
recommendation: "This error exceeded automatic healing capacity. Manual investigation required.",
|
|
115
|
+
severity: "high",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Check for injection/secrets before sending
|
|
119
|
+
const { hasSecrets } = require("../security/secret-redactor");
|
|
120
|
+
const reportStr = JSON.stringify(report);
|
|
121
|
+
if (hasSecrets(reportStr)) {
|
|
122
|
+
report.error = "[REDACTED — contained secrets]";
|
|
123
|
+
report.brainContext = "[REDACTED]";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Record the bug report
|
|
127
|
+
const sig = (filePath || "") + "::" + safeError.slice(0, 50);
|
|
128
|
+
this._bugReports.push({ signature: sig, report, timestamp: Date.now() });
|
|
129
|
+
this._save();
|
|
130
|
+
|
|
131
|
+
console.log(chalk.red(`\n 🐛 Bug Report Generated`));
|
|
132
|
+
console.log(chalk.red(` Error: ${safeError.slice(0, 80)}`));
|
|
133
|
+
console.log(chalk.red(` Attempts: ${report.attempts}, Tokens burned: ${report.totalTokensBurned}`));
|
|
134
|
+
console.log(chalk.red(` Filed for human review\n`));
|
|
135
|
+
|
|
136
|
+
if (logger) {
|
|
137
|
+
logger.critical("loop_guard.bug_report", `Bug report: ${safeError.slice(0, 100)}`, report);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return report;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Send bug report to platform backend.
|
|
145
|
+
*/
|
|
146
|
+
async sendToBackend(report) {
|
|
147
|
+
try {
|
|
148
|
+
const https = require("https");
|
|
149
|
+
const http = require("http");
|
|
150
|
+
const { URL } = require("url");
|
|
151
|
+
const { loadConfig } = require("../core/config");
|
|
152
|
+
const config = loadConfig();
|
|
153
|
+
const platformUrl = config.telemetry?.platformUrl || "https://api.wolverinenode.xyz";
|
|
154
|
+
|
|
155
|
+
const keyPath = path.join(this.cwd, ".wolverine", "platform-key");
|
|
156
|
+
let key = "";
|
|
157
|
+
try { key = fs.readFileSync(keyPath, "utf-8").trim(); } catch {}
|
|
158
|
+
if (!key) return;
|
|
159
|
+
|
|
160
|
+
const url = new URL(`${platformUrl}/api/v1/heartbeat`);
|
|
161
|
+
const body = JSON.stringify({
|
|
162
|
+
instanceId: require("../platform/telemetry").INSTANCE_ID,
|
|
163
|
+
timestamp: Date.now(),
|
|
164
|
+
alerts: [report],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const mod = url.protocol === "https:" ? https : http;
|
|
168
|
+
await new Promise((resolve) => {
|
|
169
|
+
const req = mod.request({
|
|
170
|
+
hostname: url.hostname, port: url.port, path: url.pathname,
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
|
|
173
|
+
timeout: 5000,
|
|
174
|
+
}, () => resolve());
|
|
175
|
+
req.on("error", () => resolve());
|
|
176
|
+
req.write(body);
|
|
177
|
+
req.end();
|
|
178
|
+
});
|
|
179
|
+
} catch {}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getStats() {
|
|
183
|
+
return {
|
|
184
|
+
recentAttempts: this._attempts.length,
|
|
185
|
+
bugReports: this._bugReports.length,
|
|
186
|
+
totalTokensBurned: this._attempts.reduce((s, a) => s + (a.tokens || 0), 0),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_load() {
|
|
191
|
+
try {
|
|
192
|
+
const fp = path.join(this.cwd, LOOP_FILE);
|
|
193
|
+
if (fs.existsSync(fp)) {
|
|
194
|
+
const data = JSON.parse(fs.readFileSync(fp, "utf-8"));
|
|
195
|
+
this._attempts = data.attempts || [];
|
|
196
|
+
this._bugReports = data.bugReports || [];
|
|
197
|
+
}
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_save() {
|
|
202
|
+
try {
|
|
203
|
+
const fp = path.join(this.cwd, LOOP_FILE);
|
|
204
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
205
|
+
fs.writeFileSync(fp, JSON.stringify({ attempts: this._attempts.slice(-50), bugReports: this._bugReports.slice(-20) }, null, 2), "utf-8");
|
|
206
|
+
} catch {}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Process Dedup ──
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Ensure only one wolverine process runs at a time.
|
|
214
|
+
* Kills any existing wolverine process before starting a new one.
|
|
215
|
+
*/
|
|
216
|
+
function ensureSingleProcess(cwd) {
|
|
217
|
+
const { execSync } = require("child_process");
|
|
218
|
+
const pidFile = path.join(cwd, ".wolverine", "wolverine.pid");
|
|
219
|
+
|
|
220
|
+
// Kill old process if PID file exists
|
|
221
|
+
try {
|
|
222
|
+
if (fs.existsSync(pidFile)) {
|
|
223
|
+
const oldPid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
224
|
+
if (oldPid && oldPid !== process.pid) {
|
|
225
|
+
try {
|
|
226
|
+
process.kill(oldPid, 0); // check if alive
|
|
227
|
+
console.log(chalk.yellow(` 🔒 Killing old wolverine process (PID ${oldPid})`));
|
|
228
|
+
if (process.platform === "win32") {
|
|
229
|
+
execSync(`taskkill /PID ${oldPid} /T /F`, { stdio: "ignore", timeout: 5000 });
|
|
230
|
+
} else {
|
|
231
|
+
process.kill(oldPid, "SIGTERM");
|
|
232
|
+
setTimeout(() => { try { process.kill(oldPid, "SIGKILL"); } catch {} }, 3000);
|
|
233
|
+
}
|
|
234
|
+
} catch {} // process already dead
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch {}
|
|
238
|
+
|
|
239
|
+
// Write our PID
|
|
240
|
+
try {
|
|
241
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
242
|
+
fs.writeFileSync(pidFile, String(process.pid), "utf-8");
|
|
243
|
+
} catch {}
|
|
244
|
+
|
|
245
|
+
// Clean up on exit
|
|
246
|
+
process.on("exit", () => { try { fs.unlinkSync(pidFile); } catch {} });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Skill Metadata ──
|
|
250
|
+
|
|
251
|
+
const SKILL_NAME = "loop-guard";
|
|
252
|
+
const SKILL_DESCRIPTION = "Detects infinite heal loops and generates bug reports. Prevents token waste by stopping repeated failed heals on the same error. Ensures only one wolverine process runs at a time.";
|
|
253
|
+
const SKILL_KEYWORDS = ["loop", "infinite", "stuck", "repeat", "guard", "bug", "report", "dedup", "single", "process", "pid"];
|
|
254
|
+
const SKILL_USAGE = `// Loop guard is automatic — integrated into the heal pipeline.
|
|
255
|
+
// Bug reports filed when 3+ heals fail on same error in 10 minutes.
|
|
256
|
+
// Process dedup runs on startup.
|
|
257
|
+
|
|
258
|
+
// Manual check:
|
|
259
|
+
const { LoopGuard } = require("wolverine-ai");
|
|
260
|
+
const guard = new LoopGuard(process.cwd());
|
|
261
|
+
const check = guard.check("server/api.js::TypeError");
|
|
262
|
+
// { allowed: false, reason: "Loop detected: 3 failed heals..." }`;
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
SKILL_NAME, SKILL_DESCRIPTION, SKILL_KEYWORDS, SKILL_USAGE,
|
|
266
|
+
LoopGuard, ensureSingleProcess,
|
|
267
|
+
};
|