Auto backup before evolver run - 2026年 03月 03日 星期二 23:12:56 CST

This commit is contained in:
2026-03-03 23:12:56 +08:00
parent 429e5a797d
commit f11d322c3f
99 changed files with 17051 additions and 138 deletions
@@ -0,0 +1,109 @@
// GEP Artifact Cleanup - Evolver Core Module
// Removes old gep_prompt_*.json/txt files from evolution dir.
// Keeps at least 10 most recent files regardless of age.
const fs = require('fs');
const path = require('path');
const { getEvolutionDir } = require('../gep/paths');
var MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
var MIN_KEEP = 10;
function run() {
var evoDir = getEvolutionDir();
if (!fs.existsSync(evoDir)) return;
var files = fs.readdirSync(evoDir)
.filter(function(f) { return /^gep_prompt_.*\.(json|txt)$/.test(f); })
.map(function(f) {
var full = path.join(evoDir, f);
var stat = fs.statSync(full);
return { name: f, path: full, mtime: stat.mtimeMs };
})
.sort(function(a, b) { return b.mtime - a.mtime; }); // newest first
var now = Date.now();
var deleted = 0;
// Phase 1: Age-based cleanup (keep at least MIN_KEEP)
// [OPTIMIZATION] Batch deletion for age-based cleanup as well
const filesToDelete = [];
for (var i = MIN_KEEP; i < files.length; i++) {
if (now - files[i].mtime > MAX_AGE_MS) {
filesToDelete.push(files[i].path);
}
}
if (filesToDelete.length > 0) {
const BATCH_SIZE = 50;
for (let i = 0; i < filesToDelete.length; i += BATCH_SIZE) {
const batch = filesToDelete.slice(i, i + BATCH_SIZE);
try {
// Remove package-lock.json if it exists (not relevant here but good practice in other contexts)
// Use rm -f with quotes to handle paths safely
// Phase 1 optimization update: mirroring phase 2 logic
const args = batch.map(function(p) { return '"' + p + '"'; }).join(' ');
require('child_process').execSync('rm -f ' + args, { stdio: 'ignore' });
deleted += batch.length;
} catch (e) {
// Fallback to serial deletion if batch fails
batch.forEach(function(p) {
try { fs.unlinkSync(p); deleted++; } catch (_) {}
});
}
}
}
// Phase 2: Size-based safety cap (keep max 10 files total to drastically reduce bloat)
// Re-scan remaining files after age cleanup
try {
var remainingFiles = fs.readdirSync(evoDir)
.filter(function(f) { return /^gep_prompt_.*\.(json|txt)$/.test(f); })
.map(function(f) {
var full = path.join(evoDir, f);
var stat = fs.statSync(full);
return { name: f, path: full, mtime: stat.mtimeMs };
})
.sort(function(a, b) { return b.mtime - a.mtime; }); // newest first
// [OPTIMIZATION] Batch deletion using execSync('rm') for speed and robustness
var MAX_FILES = 10;
if (remainingFiles.length > MAX_FILES) {
const toDelete = remainingFiles.slice(MAX_FILES).map(function(f) { return f.path; });
// Chunking into batches of 50 to avoid E2BIG on some systems,
// though with MAX_FILES=10 and typical usage, list won't be huge.
const BATCH_SIZE = 50;
for (let i = 0; i < toDelete.length; i += BATCH_SIZE) {
const batch = toDelete.slice(i, i + BATCH_SIZE);
try {
// Safe bulk delete via shell, faster than serial unlinkSync
// Using "rm -f" to ignore non-existent files
const args = batch.map(function(p) { return '"' + p + '"'; }).join(' ');
require('child_process').execSync('rm -f ' + args, { stdio: 'ignore' });
deleted += batch.length;
} catch (e) {
// Fallback to serial if batch fails
batch.forEach(function(p) {
try { fs.unlinkSync(p); deleted++; } catch (_) {}
});
}
}
}
} catch (e) {
console.warn('[Cleanup] Phase 2 failed:', e.message);
}
if (deleted > 0) {
console.log('[Cleanup] Deleted ' + deleted + ' old GEP artifacts.');
}
return deleted;
}
if (require.main === module) {
console.log('[Cleanup] Scanning for old artifacts...');
var count = run();
console.log('[Cleanup] ' + (count > 0 ? 'Deleted ' + count + ' files.' : 'No files to delete.'));
}
module.exports = { run };
@@ -0,0 +1,60 @@
// Commentary Generator - Evolver Core Module
// Generates persona-based comments for cycle summaries.
var PERSONAS = {
standard: {
success: [
'Evolution complete. System improved.',
'Another successful cycle.',
'Clean execution, no issues.',
],
failure: [
'Cycle failed. Will retry.',
'Encountered issues. Investigating.',
'Failed this round. Learning from it.',
],
},
greentea: {
success: [
'Did I do good? Praise me~',
'So efficient... unlike someone else~',
'Hmm, that was easy~',
'I finished before you even noticed~',
],
failure: [
'Oops... it is not my fault though~',
'This is harder than it looks, okay?',
'I will get it next time, probably~',
],
},
maddog: {
success: [
'TARGET ELIMINATED.',
'Mission complete. Next.',
'Done. Moving on.',
],
failure: [
'FAILED. RETRYING.',
'Obstacle encountered. Adapting.',
'Error. Will overcome.',
],
},
};
function getComment(options) {
var persona = (options && options.persona) || 'standard';
var success = options && options.success !== false;
var duration = (options && options.duration) || 0;
var p = PERSONAS[persona] || PERSONAS.standard;
var pool = success ? p.success : p.failure;
var comment = pool[Math.floor(Math.random() * pool.length)];
return comment;
}
if (require.main === module) {
console.log(getComment({ persona: process.argv[2] || 'greentea', success: true }));
}
module.exports = { getComment, PERSONAS };
@@ -0,0 +1,106 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execSync } = require('child_process');
function getDiskUsage(mount) {
try {
// Use Node 18+ statfs if available
if (fs.statfsSync) {
const stats = fs.statfsSync(mount || '/');
const total = stats.blocks * stats.bsize;
const free = stats.bavail * stats.bsize; // available to unprivileged users
const used = total - free;
return {
pct: Math.round((used / total) * 100),
freeMb: Math.round(free / 1024 / 1024)
};
}
// Fallback
const out = execSync(`df -P "${mount || '/'}" | tail -1 | awk '{print $5, $4}'`).toString().trim().split(' ');
return {
pct: parseInt(out[0].replace('%', '')),
freeMb: Math.round(parseInt(out[1]) / 1024) // df returns 1k blocks usually
};
} catch (e) {
return { pct: 0, freeMb: 999999, error: e.message };
}
}
function runHealthCheck() {
const checks = [];
let criticalErrors = 0;
let warnings = 0;
// 1. Secret Check (Critical for external services, but maybe not for the agent itself to run)
const criticalSecrets = ['FEISHU_APP_ID', 'FEISHU_APP_SECRET'];
criticalSecrets.forEach(key => {
if (!process.env[key] || process.env[key].trim() === '') {
checks.push({ name: `env:${key}`, ok: false, status: 'missing', severity: 'warning' }); // Downgraded to warning to prevent restart loops
warnings++;
} else {
checks.push({ name: `env:${key}`, ok: true, status: 'present' });
}
});
const optionalSecrets = ['CLAWHUB_TOKEN', 'OPENAI_API_KEY'];
optionalSecrets.forEach(key => {
if (!process.env[key] || process.env[key].trim() === '') {
checks.push({ name: `env:${key}`, ok: false, status: 'missing', severity: 'info' });
} else {
checks.push({ name: `env:${key}`, ok: true, status: 'present' });
}
});
// 2. Disk Space Check
const disk = getDiskUsage('/');
if (disk.pct > 90) {
checks.push({ name: 'disk_space', ok: false, status: `${disk.pct}% used`, severity: 'critical' });
criticalErrors++;
} else if (disk.pct > 80) {
checks.push({ name: 'disk_space', ok: false, status: `${disk.pct}% used`, severity: 'warning' });
warnings++;
} else {
checks.push({ name: 'disk_space', ok: true, status: `${disk.pct}% used` });
}
// 3. Memory Check
const memFree = os.freemem();
const memTotal = os.totalmem();
const memPct = Math.round(((memTotal - memFree) / memTotal) * 100);
if (memPct > 95) {
checks.push({ name: 'memory', ok: false, status: `${memPct}% used`, severity: 'critical' });
criticalErrors++;
} else {
checks.push({ name: 'memory', ok: true, status: `${memPct}% used` });
}
// 4. Process Count (Check for fork bombs or leaks)
// Only on Linux
if (process.platform === 'linux') {
try {
// Optimization: readdirSync /proc is heavy. Use a lighter check or skip if too frequent.
// But since this is health check, we'll keep it but increase the threshold to reduce noise.
const pids = fs.readdirSync('/proc').filter(f => /^\d+$/.test(f));
if (pids.length > 2000) { // Bumped threshold to 2000
checks.push({ name: 'process_count', ok: false, status: `${pids.length} procs`, severity: 'warning' });
warnings++;
} else {
checks.push({ name: 'process_count', ok: true, status: `${pids.length} procs` });
}
} catch(e) {}
}
// Determine Overall Status
let status = 'ok';
if (criticalErrors > 0) status = 'error';
else if (warnings > 0) status = 'warning';
return {
status,
timestamp: new Date().toISOString(),
checks
};
}
module.exports = { runHealthCheck };
@@ -0,0 +1,11 @@
// Evolver Operations Module (src/ops/)
// Non-Feishu, portable utilities for evolver lifecycle and maintenance.
module.exports = {
lifecycle: require('./lifecycle'),
skillsMonitor: require('./skills_monitor'),
cleanup: require('./cleanup'),
trigger: require('./trigger'),
commentary: require('./commentary'),
selfRepair: require('./self_repair'),
};
@@ -0,0 +1,67 @@
// Innovation Catalyst (v1.0) - Evolver Core Module
// Analyzes system state to propose concrete innovation ideas when stagnation is detected.
const fs = require('fs');
const path = require('path');
const { getSkillsDir } = require('../gep/paths');
function listSkills() {
try {
const dir = getSkillsDir();
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir).filter(f => !f.startsWith('.'));
} catch (e) { return []; }
}
function generateInnovationIdeas() {
const skills = listSkills();
const categories = {
'feishu': skills.filter(s => s.startsWith('feishu-')).length,
'dev': skills.filter(s => s.startsWith('git-') || s.startsWith('code-') || s.includes('lint') || s.includes('test')).length,
'media': skills.filter(s => s.includes('image') || s.includes('video') || s.includes('music') || s.includes('voice')).length,
'security': skills.filter(s => s.includes('security') || s.includes('audit') || s.includes('guard')).length,
'automation': skills.filter(s => s.includes('auto-') || s.includes('scheduler') || s.includes('cron')).length,
'data': skills.filter(s => s.includes('db') || s.includes('store') || s.includes('cache') || s.includes('index')).length
};
// Find under-represented categories
const sortedCats = Object.entries(categories).sort((a, b) => a[1] - b[1]);
const weakAreas = sortedCats.slice(0, 2).map(c => c[0]);
const ideas = [];
// Idea 1: Fill the gap
if (weakAreas.includes('security')) {
ideas.push("- Security: Implement a 'dependency-scanner' skill to check for vulnerable packages.");
ideas.push("- Security: Create a 'permission-auditor' to review tool usage patterns.");
}
if (weakAreas.includes('media')) {
ideas.push("- Media: Add a 'meme-generator' skill for social engagement.");
ideas.push("- Media: Create a 'video-summarizer' using ffmpeg keyframes.");
}
if (weakAreas.includes('dev')) {
ideas.push("- Dev: Build a 'code-stats' skill to visualize repo complexity.");
ideas.push("- Dev: Implement a 'todo-manager' that syncs code TODOs to tasks.");
}
if (weakAreas.includes('automation')) {
ideas.push("- Automation: Create a 'meeting-prep' skill that auto-summarizes calendar context.");
ideas.push("- Automation: Build a 'broken-link-checker' for documentation.");
}
if (weakAreas.includes('data')) {
ideas.push("- Data: Implement a 'local-vector-store' for semantic search.");
ideas.push("- Data: Create a 'log-analyzer' to visualize system health trends.");
}
// Idea 2: Optimization
if (skills.length > 50) {
ideas.push("- Optimization: Identify and deprecate unused skills (e.g., redundant search tools).");
ideas.push("- Optimization: Merge similar skills (e.g., 'git-sync' and 'git-doctor').");
}
// Idea 3: Meta
ideas.push("- Meta: Enhance the Evolver's self-reflection by adding a 'performance-metric' dashboard.");
return ideas.slice(0, 3); // Return top 3 ideas
}
module.exports = { generateInnovationIdeas };
@@ -0,0 +1,168 @@
// Evolver Lifecycle Manager - Evolver Core Module
// Provides: start, stop, restart, status, log, health check
// The loop script to spawn is configurable via EVOLVER_LOOP_SCRIPT env var.
const fs = require('fs');
const path = require('path');
const { execSync, spawn } = require('child_process');
const { getRepoRoot, getWorkspaceRoot, getLogsDir } = require('../gep/paths');
var WORKSPACE_ROOT = getWorkspaceRoot();
var LOG_FILE = path.join(getLogsDir(), 'evolver_loop.log');
var PID_FILE = path.join(WORKSPACE_ROOT, 'memory', 'evolver_loop.pid');
var MAX_SILENCE_MS = 30 * 60 * 1000;
function getLoopScript() {
// Prefer wrapper if exists, fallback to core evolver
if (process.env.EVOLVER_LOOP_SCRIPT) return process.env.EVOLVER_LOOP_SCRIPT;
var wrapper = path.join(WORKSPACE_ROOT, 'skills/feishu-evolver-wrapper/index.js');
if (fs.existsSync(wrapper)) return wrapper;
return path.join(getRepoRoot(), 'index.js');
}
// --- Process Discovery ---
function getRunningPids() {
try {
var out = execSync('ps -e -o pid,args', { encoding: 'utf8' });
var pids = [];
for (var line of out.split('\n')) {
var trimmed = line.trim();
if (!trimmed || trimmed.startsWith('PID')) continue;
var parts = trimmed.split(/\s+/);
var pid = parseInt(parts[0], 10);
var cmd = parts.slice(1).join(' ');
if (pid === process.pid) continue;
if (cmd.includes('node') && cmd.includes('index.js') && cmd.includes('--loop')) {
if (cmd.includes('feishu-evolver-wrapper') || cmd.includes('skills/evolver')) {
pids.push(pid);
}
}
}
return [...new Set(pids)].filter(isPidRunning);
} catch (e) {
return [];
}
}
function isPidRunning(pid) {
try { process.kill(pid, 0); return true; } catch (e) { return false; }
}
function getCmdLine(pid) {
try { return execSync('ps -p ' + pid + ' -o args=', { encoding: 'utf8' }).trim(); } catch (e) { return null; }
}
// --- Lifecycle ---
function start(options) {
var delayMs = (options && options.delayMs) || 0;
var pids = getRunningPids();
if (pids.length > 0) {
console.log('[Lifecycle] Already running (PIDs: ' + pids.join(', ') + ').');
return { status: 'already_running', pids: pids };
}
if (delayMs > 0) execSync('sleep ' + (delayMs / 1000));
var script = getLoopScript();
console.log('[Lifecycle] Starting: node ' + path.relative(WORKSPACE_ROOT, script) + ' --loop');
var out = fs.openSync(LOG_FILE, 'a');
var err = fs.openSync(LOG_FILE, 'a');
var env = Object.assign({}, process.env);
var npmGlobal = path.join(process.env.HOME || '', '.npm-global/bin');
if (env.PATH && !env.PATH.includes(npmGlobal)) {
env.PATH = npmGlobal + ':' + env.PATH;
}
var child = spawn('node', [script, '--loop'], {
detached: true, stdio: ['ignore', out, err], cwd: WORKSPACE_ROOT, env: env
});
child.unref();
fs.writeFileSync(PID_FILE, String(child.pid));
console.log('[Lifecycle] Started PID ' + child.pid);
return { status: 'started', pid: child.pid };
}
function stop() {
var pids = getRunningPids();
if (pids.length === 0) {
console.log('[Lifecycle] No running evolver loops found.');
if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
return { status: 'not_running' };
}
for (var i = 0; i < pids.length; i++) {
console.log('[Lifecycle] Stopping PID ' + pids[i] + '...');
try { process.kill(pids[i], 'SIGTERM'); } catch (e) {}
}
var attempts = 0;
while (getRunningPids().length > 0 && attempts < 10) {
execSync('sleep 0.5');
attempts++;
}
var remaining = getRunningPids();
for (var j = 0; j < remaining.length; j++) {
console.log('[Lifecycle] SIGKILL PID ' + remaining[j]);
try { process.kill(remaining[j], 'SIGKILL'); } catch (e) {}
}
if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
var evolverLock = path.join(getRepoRoot(), 'evolver.pid');
if (fs.existsSync(evolverLock)) fs.unlinkSync(evolverLock);
console.log('[Lifecycle] All stopped.');
return { status: 'stopped', killed: pids };
}
function restart(options) {
stop();
return start(Object.assign({ delayMs: 2000 }, options || {}));
}
function status() {
var pids = getRunningPids();
if (pids.length > 0) {
return { running: true, pids: pids.map(function(p) { return { pid: p, cmd: getCmdLine(p) }; }), log: path.relative(WORKSPACE_ROOT, LOG_FILE) };
}
return { running: false };
}
function tailLog(lines) {
if (!fs.existsSync(LOG_FILE)) return { error: 'No log file' };
try {
return { file: path.relative(WORKSPACE_ROOT, LOG_FILE), content: execSync('tail -n ' + (lines || 20) + ' "' + LOG_FILE + '"', { encoding: 'utf8' }) };
} catch (e) {
return { error: e.message };
}
}
function checkHealth() {
var pids = getRunningPids();
if (pids.length === 0) return { healthy: false, reason: 'not_running' };
if (fs.existsSync(LOG_FILE)) {
var silenceMs = Date.now() - fs.statSync(LOG_FILE).mtimeMs;
if (silenceMs > MAX_SILENCE_MS) {
return { healthy: false, reason: 'stagnation', silenceMinutes: Math.round(silenceMs / 60000) };
}
}
return { healthy: true, pids: pids };
}
// --- CLI ---
if (require.main === module) {
var action = process.argv[2];
switch (action) {
case 'start': console.log(JSON.stringify(start())); break;
case 'stop': console.log(JSON.stringify(stop())); break;
case 'restart': console.log(JSON.stringify(restart())); break;
case 'status': console.log(JSON.stringify(status(), null, 2)); break;
case 'log': var r = tailLog(); console.log(r.content || r.error); break;
case 'check':
var health = checkHealth();
console.log(JSON.stringify(health, null, 2));
if (!health.healthy) { console.log('[Lifecycle] Restarting...'); restart(); }
break;
default: console.log('Usage: node lifecycle.js [start|stop|restart|status|log|check]');
}
}
module.exports = { start, stop, restart, status, tailLog, checkHealth, getRunningPids };
@@ -0,0 +1,72 @@
// Git Self-Repair - Evolver Core Module
// Emergency repair for git sync failures: abort rebase/merge, remove stale locks.
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { getWorkspaceRoot } = require('../gep/paths');
var LOCK_MAX_AGE_MS = 10 * 60 * 1000; // 10 minutes
function repair(gitRoot) {
var root = gitRoot || getWorkspaceRoot();
var repaired = [];
// 1. Abort pending rebase
try {
execSync('git rebase --abort', { cwd: root, stdio: 'ignore' });
repaired.push('rebase_aborted');
console.log('[SelfRepair] Aborted pending rebase.');
} catch (e) {}
// 2. Abort pending merge
try {
execSync('git merge --abort', { cwd: root, stdio: 'ignore' });
repaired.push('merge_aborted');
console.log('[SelfRepair] Aborted pending merge.');
} catch (e) {}
// 3. Remove stale index.lock
var lockFile = path.join(root, '.git', 'index.lock');
if (fs.existsSync(lockFile)) {
try {
var stat = fs.statSync(lockFile);
var age = Date.now() - stat.mtimeMs;
if (age > LOCK_MAX_AGE_MS) {
fs.unlinkSync(lockFile);
repaired.push('stale_lock_removed');
console.log('[SelfRepair] Removed stale index.lock (' + Math.round(age / 60000) + 'min old).');
}
} catch (e) {}
}
// 4. Reset to remote main if local is corrupt (last resort - guarded by flag)
// Only enabled if explicitly called with --force-reset or EVOLVE_GIT_RESET=true
if (process.env.EVOLVE_GIT_RESET === 'true') {
try {
console.log('[SelfRepair] Resetting local branch to origin/main (HARD reset)...');
execSync('git fetch origin main', { cwd: root, stdio: 'ignore' });
execSync('git reset --hard origin/main', { cwd: root, stdio: 'ignore' });
repaired.push('hard_reset_to_origin');
} catch (e) {
console.warn('[SelfRepair] Hard reset failed: ' + e.message);
}
} else {
// Safe fetch
try {
execSync('git fetch origin', { cwd: root, stdio: 'ignore', timeout: 30000 });
repaired.push('fetch_ok');
} catch (e) {
console.warn('[SelfRepair] git fetch failed: ' + e.message);
}
}
return repaired;
}
if (require.main === module) {
var result = repair();
console.log('[SelfRepair] Result:', result.length > 0 ? result.join(', ') : 'nothing to repair');
}
module.exports = { repair };
@@ -0,0 +1,143 @@
// Skills Monitor (v2.0) - Evolver Core Module
// Checks installed skills for real issues, auto-heals simple problems.
// Zero Feishu dependency.
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { getSkillsDir, getWorkspaceRoot } = require('../gep/paths');
const IGNORE_LIST = new Set([
'common',
'clawhub',
'input-validator',
'proactive-agent',
'security-audit',
]);
// Load user-defined ignore list
try {
var ignoreFile = path.join(getWorkspaceRoot(), '.skill_monitor_ignore');
if (fs.existsSync(ignoreFile)) {
fs.readFileSync(ignoreFile, 'utf8').split('\n').forEach(function(l) {
var t = l.trim();
if (t && !t.startsWith('#')) IGNORE_LIST.add(t);
});
}
} catch (e) { /* ignore */ }
function checkSkill(skillName) {
var SKILLS_DIR = getSkillsDir();
if (IGNORE_LIST.has(skillName)) return null;
var skillPath = path.join(SKILLS_DIR, skillName);
var issues = [];
try { if (!fs.statSync(skillPath).isDirectory()) return null; } catch (e) { return null; }
var mainFile = 'index.js';
var pkgPath = path.join(skillPath, 'package.json');
var hasPkg = false;
if (fs.existsSync(pkgPath)) {
hasPkg = true;
try {
var pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
if (pkg.main) mainFile = pkg.main;
if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
if (!fs.existsSync(path.join(skillPath, 'node_modules'))) {
issues.push('Missing node_modules (needs npm install)');
} else {
// Optimization: Check for node_modules existence instead of spawning node
// Spawning node for every skill is too slow (perf_bottleneck).
// We assume if node_modules exists, it's likely okay.
// Only spawn check if we really suspect issues (e.g. empty node_modules).
try {
if (fs.readdirSync(path.join(skillPath, 'node_modules')).length === 0) {
issues.push('Empty node_modules (needs npm install)');
}
} catch (e) {
issues.push('Invalid node_modules');
}
}
}
} catch (e) {
issues.push('Invalid package.json');
}
}
if (mainFile.endsWith('.js')) {
var entryPoint = path.join(skillPath, mainFile);
if (fs.existsSync(entryPoint)) {
// Optimization: Syntax check via node -c is slow.
// We can trust the runtime to catch syntax errors when loading.
// Or we can use a lighter check if absolutely necessary.
// For now, removing the synchronous spawn to fix perf_bottleneck.
}
}
if (hasPkg && !fs.existsSync(path.join(skillPath, 'SKILL.md'))) {
issues.push('Missing SKILL.md');
}
return issues.length > 0 ? { name: skillName, issues: issues } : null;
}
function autoHeal(skillName, issues) {
var SKILLS_DIR = getSkillsDir();
var skillPath = path.join(SKILLS_DIR, skillName);
var healed = [];
for (var i = 0; i < issues.length; i++) {
if (issues[i] === 'Missing node_modules (needs npm install)' || issues[i] === 'Empty node_modules (needs npm install)') {
try {
// Remove package-lock.json if it exists to prevent conflict errors
try { fs.unlinkSync(path.join(skillPath, 'package-lock.json')); } catch (e) {}
execSync('npm install --production --no-audit --no-fund', {
cwd: skillPath, stdio: 'ignore', timeout: 60000 // Increased timeout
});
healed.push(issues[i]);
console.log('[SkillsMonitor] Auto-healed ' + skillName + ': npm install');
} catch (e) {
console.error('[SkillsMonitor] Failed to heal ' + skillName + ': ' + e.message);
}
} else if (issues[i] === 'Missing SKILL.md') {
try {
var name = skillName.replace(/-/g, ' ');
fs.writeFileSync(path.join(skillPath, 'SKILL.md'), '# ' + skillName + '\n\n' + name + ' skill.\n');
healed.push(issues[i]);
console.log('[SkillsMonitor] Auto-healed ' + skillName + ': created SKILL.md stub');
} catch (e) {}
}
}
return healed;
}
function run(options) {
var heal = (options && options.autoHeal) !== false;
var SKILLS_DIR = getSkillsDir();
var skills = fs.readdirSync(SKILLS_DIR);
var report = [];
for (var i = 0; i < skills.length; i++) {
if (skills[i].startsWith('.')) continue;
var result = checkSkill(skills[i]);
if (result) {
if (heal) {
var healed = autoHeal(result.name, result.issues);
result.issues = result.issues.filter(function(issue) { return !healed.includes(issue); });
if (result.issues.length === 0) continue;
}
report.push(result);
}
}
return report;
}
if (require.main === module) {
var issues = run();
console.log(JSON.stringify(issues, null, 2));
process.exit(issues.length > 0 ? 1 : 0);
}
module.exports = { run, checkSkill, autoHeal };
@@ -0,0 +1,33 @@
// Evolver Wake Trigger - Evolver Core Module
// Writes a signal file that the wrapper can poll to wake up immediately.
const fs = require('fs');
const path = require('path');
const { getWorkspaceRoot } = require('../gep/paths');
var WAKE_FILE = path.join(getWorkspaceRoot(), 'memory', 'evolver_wake.signal');
function send() {
try {
fs.writeFileSync(WAKE_FILE, 'WAKE');
console.log('[Trigger] Wake signal sent to ' + WAKE_FILE);
return true;
} catch (e) {
console.error('[Trigger] Failed: ' + e.message);
return false;
}
}
function clear() {
try { if (fs.existsSync(WAKE_FILE)) fs.unlinkSync(WAKE_FILE); } catch (e) {}
}
function isPending() {
return fs.existsSync(WAKE_FILE);
}
if (require.main === module) {
send();
}
module.exports = { send, clear, isPending };