Auto backup before evolver run - 2026年 03月 03日 星期二 23:12:56 CST
This commit is contained in:
@@ -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 };
|
||||
Reference in New Issue
Block a user