Asnip / lib /evolve.mjs
teddyoas's picture
Fix Node 18 compatibility, offline mission scope fallback, and test determinism
bdba208
Raw History Blame Contribute Delete
35.3 kB
// Phantom — Auto-Evolution Engine
// Self-healing, self-optimizing, self-growing.
// Runs automatically on startup and after tasks.
import fs from "fs";
import { execFileSync, execSync } from "child_process";
import { resolve, join, dirname } from "path";
import { fileURLToPath } from "url";
import { homedir } from "os";
import { BASE_DIR, TOOLS_DIR, PHANTOM_VERSION } from "./config.mjs";
import { __r } from "./runtime.mjs";
// ── Constants ──────────────────────────────────────────────
const PHANTOM_DIR = resolve(dirname(fileURLToPath(import.meta.url)), ".."); // project root
const EVOLVE_LOG = resolve(BASE_DIR, "evolve.json");
const AUTO_TOOLS_DIR = resolve(PHANTOM_DIR, "lib", "auto_tools"); // in-repo so git-tracked
// ── Git helpers ────────────────────────────────────────────
function gitRoot() {
try {
const r = execSync("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8", timeout: 5000 }).trim();
return r || null;
} catch { return null; }
}
export function gitCommit(files, msg) {
const root = gitRoot();
if (!root) return false;
try {
// Stage specific files or all changes
if (files && files.length > 0) {
for (const f of files) {
const abs = f.startsWith("/") ? f : resolve(PHANTOM_DIR, f);
if (fs.existsSync(abs)) execSync(`git add "${abs}"`, { cwd: root, encoding: "utf-8", timeout: 10000 });
}
} else {
execSync("git add -A", { cwd: root, encoding: "utf-8", timeout: 10000 });
}
// Check if anything staged
const staged = execSync("git diff --cached --stat", { cwd: root, encoding: "utf-8", timeout: 5000 }).trim();
if (!staged) return false;
execSync(`git commit -m "${msg}"`, { cwd: root, encoding: "utf-8", timeout: 15000 });
return true;
} catch { return false; }
}
export function gitPush() {
const root = gitRoot();
if (!root) return { pushed: false, reason: "no git root" };
// Check if remote exists
try {
const remotes = execSync("git remote", { cwd: root, encoding: "utf-8", timeout: 5000 }).trim();
if (!remotes) return { pushed: false, reason: "no remote configured" };
} catch { return { pushed: false, reason: "no remote" }; }
// Fetch and check for upstream divergence (misalignment)
try {
const upstream = execSync("git rev-parse --symbolic-full-name @{upstream} 2>/dev/null", { cwd: root, encoding: "utf-8", timeout: 5000 }).trim();
if (upstream) {
execSync("git fetch origin 2>/dev/null", { cwd: root, encoding: "utf-8", timeout: 30000 });
const behind = execSync("git rev-list --count HEAD..@{upstream} 2>/dev/null", { cwd: root, encoding: "utf-8", timeout: 5000 }).trim();
if (parseInt(behind) > 0) return { pushed: false, reason: `${behind} commit(s) behind upstream — pull first` };
}
} catch { /* no upstream tracking, push anyway */ }
// Alignment check on the diff
const alignment = verifyDiffAlignment(root);
if (!alignment.ok) return { pushed: false, reason: `alignment: ${alignment.errors[0]}`, alignment };
// Push
try {
execSync("git push 2>&1", { cwd: root, encoding: "utf-8", timeout: 60000 });
return { pushed: true };
} catch (e) {
return { pushed: false, reason: e.stderr?.slice(0, 200) || e.message };
}
}
// ── Diff misalignment check ──
// Scans staged changes for regressions before push.
export function verifyDiffAlignment(root) {
const result = { ok: true, errors: [], warnings: [] };
let diffStat;
try { diffStat = execSync("git diff --cached --stat", { cwd: root, encoding: "utf-8", timeout: 5000 }).trim(); }
catch { return result; }
if (!diffStat) { result.warnings.push("no staged changes"); return result; }
// Extract changed .mjs files
const changedMjs = diffStat.split("\n")
.map(l => l.split("|")[0]?.trim())
.filter(f => f && f.endsWith(".mjs"));
// 1. Syntax check every changed .mjs file
for (const f of changedMjs) {
const abs = f.startsWith("/") ? f : resolve(root, f);
if (fs.existsSync(abs)) {
try { execSync(`node --check "${abs}"`, { encoding: "utf-8", timeout: 10000 }); }
catch (e) {
const err = (e.stderr || e.message || "").slice(0, 200);
result.errors.push(`${f}: syntax error — ${err}`);
result.ok = false;
}
}
}
// 2. Forbid debug artifacts in changed files
const diffContent = execSync("git diff --cached -U0", { cwd: root, encoding: "utf-8", timeout: 5000 });
if (diffContent) {
// Added lines that are debug artifacts
const additions = diffContent.split("\n").filter(l => l.startsWith("+") && !l.startsWith("+++") && !l.startsWith("+ "));
const debugPatterns = [/^\+.*console\.(log|debug)\s*\(/m, /^\+.*debugger\s*;?$/m, /^\+.*process\.env\.PHANTOM_GIT_PUSH/m];
for (const pat of debugPatterns) {
const matches = additions.filter(l => pat.test(l));
if (matches.length > 0) {
result.warnings.push(`${matches.length} line(s) match ${pat.source} — verify intentional`);
// Don't block for debug warnings, just flag
}
}
// 3. Check for hardcoded secrets (API keys, tokens, passwords)
const secretPatterns = [/^\+.*(?:api.?key|secret|password|token|passwd)\s*[:=]\s*['\"][^'\"]{8,}['\"]/im];
for (const pat of secretPatterns) {
if (additions.some(l => pat.test(l))) {
result.errors.push("hardcoded secret detected in staged diff — blocking push");
result.ok = false;
}
}
// 4. Check file sizes (no binaries >100KB staged)
try {
const binaryCheck = execSync("git diff --cached --numstat | awk '{sum+=$1} END {print sum+0}'", { cwd: root, encoding: "utf-8", timeout: 5000 }).trim();
const totalAdditions = parseInt(binaryCheck);
if (totalAdditions > 5000) result.warnings.push(`large diff: ${totalAdditions} additions — verify`);
} catch { /* numstat unavailable */ }
}
// 5. Run core tests to verify nothing regressed
try {
const testOut = execSync("node test/core.test.mjs 2>&1", { cwd: root, encoding: "utf-8", timeout: 45000 });
const failMatch = testOut.match(/fail\s+(\d+)/);
if (failMatch && parseInt(failMatch[1]) > 0) {
result.errors.push(`${failMatch[1]} test(s) failing — blocking push`);
result.ok = false;
}
} catch (e) {
result.warnings.push("test runner unavailable — skipping test gate");
}
return result;
}
// ── Known external binaries & their wrapper boilerplate ──
const WRAP_BLUEPRINTS = {
naabu: {
install: "go install github.com/projectdiscovery/naabu/v2/cmd/naabu@latest",
generate: (bin) => `naabu: async (input) => {
try {
const { execSync } = await import("child_process");
const { resolve } = await import("path");
const target = input.trim() || "";
if (!target) return "[naabu] Usage: @naabu|<target> [options]\\nFast port scanner by ProjectDiscovery.\\nExamples:\\n naabu|scanme.org\\n naabu|scanme.org -p 80,443,8443\\n naabu|scanme.org -top-ports 1000";
const r = execSync(\`naabu \${target}\`, { encoding: "utf-8", timeout: 120000, maxBuffer: 1024 * 1024 });
return r.trim() || "(no results)";
} catch (e) { return \`[naabu Error] \${e.stderr?.slice(0, 500) || e.message}\`; }
},`
},
notify: {
install: "go install github.com/projectdiscovery/notify/cmd/notify@latest",
generate: (bin) => `notify: async (input) => {
try {
const { execSync } = await import("child_process");
const msg = input.trim();
if (!msg) return "[notify] Usage: @notify|<message>\\nSend notifications to Slack/Telegram/Discord/etc.\\nRequires ~/.config/notify/provider-config.yaml";
execSync(\`notify -data <(echo "\${msg}")\`, { encoding: "utf-8", timeout: 15000 });
return "[notify] Sent";
} catch (e) { return \`[notify Error] \${e.message}\`; }
},`
},
puredns: {
install: "go install github.com/d3mondev/puredns/v2@latest",
generate: (bin) => `puredns: async (input) => {
try {
const { execSync } = await import("child_process");
const args = input.trim();
if (!args) return "[puredns] Usage: @puredns|<args>\\nFast DNS resolver by d3mondev.\\nExamples:\\n puredns|resolve domains.txt\\n puredns|bruteforce wordlist.txt example.com";
const r = execSync(\`puredns \${args}\`, { encoding: "utf-8", timeout: 120000, maxBuffer: 1024 * 1024 });
return r.trim() || "(no results)";
} catch (e) { return \`[puredns Error] \${e.stderr?.slice(0, 500) || e.message}\`; }
},`
},
httprobe: {
install: "go install github.com/tomnomnom/httprobe@latest",
generate: (bin) => `httprobe: async (input) => {
try {
const { execSync } = await import("child_process");
const targets = input.trim();
if (!targets) return "[httprobe] Usage: @httprobe|<targets>\\nProbe for alive HTTP/HTTPS servers. Takes list of hosts.\\nExamples:\\n httprobe|example.com:443\\n httprobe|subs.txt";
const r = execSync(\`echo "\${targets}" | httprobe\`, { encoding: "utf-8", timeout: 60000, maxBuffer: 1024 * 1024 });
return r.trim() || "(no alive hosts)";
} catch (e) { return \`[httprobe Error] \${e.message}\`; }
},`
},
chaos: {
install: "go install github.com/projectdiscovery/chaos-client/cmd/chaos@latest",
generate: (bin) => `chaos: async (input) => {
try {
const { execSync } = await import("child_process");
const domain = input.trim();
if (!domain) return "[chaos] Usage: @chaos|<domain>\\nProjectDiscovery Chaos - subdomain enumeration from passive sources.\\nRequires CHAOS_API_KEY env var.";
const r = execSync(\`chaos -d \${domain} -silent\`, { encoding: "utf-8", timeout: 30000, maxBuffer: 1024 * 1024 });
return r.trim() || "(no subs from Chaos)";
} catch (e) { return \`[chaos Error] \${e.message}\`; }
},`
},
uncover: {
install: "go install github.com/projectdiscovery/uncover/cmd/uncover@latest",
generate: (bin) => `uncover: async (input) => {
try {
const { execSync } = await import("child_process");
const query = input.trim();
if (!query) return "[uncover] Usage: @uncover|<query>\\nSearch Shodan/Censys/Fofa/Publicwww for hosts.\\nRequires API keys configured in ~/.config/uncover/";
const r = execSync(\`uncover -q "\${query}" -silent\`, { encoding: "utf-8", timeout: 30000, maxBuffer: 1024 * 1024 });
return r.trim() || "(no results)";
} catch (e) { return \`[uncover Error] \${e.message}\`; }
},`
},
mapcidr: {
install: "go install github.com/projectdiscovery/mapcidr/cmd/mapcidr@latest",
generate: (bin) => `mapcidr: async (input) => {
try {
const { execSync } = await import("child_process");
const cidr = input.trim();
if (!cidr) return "[mapcidr] Usage: @mapcidr|<cidr>\\nCIDR expansion utility by ProjectDiscovery.\\nExamples:\\n mapcidr|192.168.1.0/24\\n mapcidr|-a 2a00:1450:4000::/48";
const r = execSync(\`mapcidr -cidr \${cidr} -silent\`, { encoding: "utf-8", timeout: 30000, maxBuffer: 1024 * 1024 });
return r.trim() || "(no results)";
} catch (e) { return \`[mapcidr Error] \${e.message}\`; }
},`
},
"subfinder": null, // already wrapped
"httpx": null, // already wrapped
"nuclei": null, // already wrapped
"nmap": null, // already wrapped
"ffuf": null, // already wrapped
"gobuster": null, // already wrapped
"hydra": null, // already wrapped
"masscan": null, // already wrapped
"nikto": null, // already wrapped
"whatweb": null, // already wrapped
"sqlmap": null, // already wrapped
"amass": null, // already wrapped
"dnsx": null, // already wrapped
"gau": null, // already wrapped
"katana": null, // already wrapped
"gitleaks": null, // already wrapped
"s3scanner": null, // already wrapped
"trufflehog": null, // already wrapped
"wafw00f": null, // already wrapped
"arjun": null, // already wrapped
"gospider": null, // already wrapped
"interactsh": null, // already wrapped
};
// ── Tools Phantom already wraps (skip) ──
const EXISTING_TOOLS = new Set(Object.keys(WRAP_BLUEPRINTS).filter(k => WRAP_BLUEPRINTS[k] === null));
// ── Load evolve state ──
function loadState() {
try {
if (fs.existsSync(EVOLVE_LOG)) return JSON.parse(fs.readFileSync(EVOLVE_LOG, "utf-8"));
} catch {}
return { generation: 1, patches: [], wrappers: [], errors_fixed: 0, last_evolve: null, auto_fixes: [] };
}
function saveState(st) {
try {
if (!fs.existsSync(BASE_DIR)) fs.mkdirSync(BASE_DIR, { recursive: true });
fs.writeFileSync(EVOLVE_LOG, JSON.stringify(st, null, 2), "utf-8");
} catch {}
}
// ── 1. Auto-detect missing wrappers ──
// Scan PATH for installed security tools Phantom doesn't wrap yet.
export function detectMissingWrappers() {
const existing = new Set([
"nmap","sqlmap","searchsploit","ffuf","gobuster","hydra","john","hashcat",
"aircrack-ng","tshark","masscan","nikto","wpscan","dirb","enum4linux",
"smbclient","nc","ncat","socat","curl","wget","git","python3","node",
"docker","kubectl","terraform","yara","clamscan","sslyze","testssl",
"whois","dig","nslookup","host","tcpdump","sqlite3","jq","yt-dlp","ffmpeg",
"subfinder","httpx","nuclei","amass","gau","dnsx","katana","gitleaks",
"s3scanner","gobuster","nmap","sqlmap","whatweb","wafw00f","trufflehog",
"hydra","masscan","nikto","arjun","gospider","cloud_enum","notify",
"interactsh","burpsuite","metasploit","naabu","puredns","httprobe",
"chaos","uncover","mapcidr","crtsh",
]);
const found = [];
const candidates = Object.entries(WRAP_BLUEPRINTS).filter(([, v]) => v !== null);
for (const [bin, blueprint] of candidates) {
if (existing.has(bin)) continue;
try {
execFileSync("which", [bin], { encoding: "utf-8", timeout: 3000 });
found.push({ bin, blueprint });
} catch {}
}
return found;
}
// Generate wrapper code and save it as a proper ESM module
export function generateWrapper(bin, blueprint) {
let code = blueprint.generate(bin);
const dir = AUTO_TOOLS_DIR;
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
// Strip the "name: async (input) => {" prefix and trailing "},\n" suffix
// The blueprints include object-literal formatting we don't need in standalone modules
code = code.replace(/^\w+:\s*async\s*\(\s*input\s*\)\s*=>\s*{/, "").replace(/},\s*$/, "").trim();
// Wrap in a proper ESM default export
const moduleCode = `// Auto-generated wrapper for ${bin}
// Created by Phantom auto-evolution
export default async function(input) {
${code}
}\n`;
const filePath = resolve(dir, `${bin}.mjs`);
fs.writeFileSync(filePath, moduleCode, "utf-8");
return filePath;
}
// Load all auto-generated tools and return as {name: executeFn}
export async function loadAutoTools() {
const tools = {};
if (!fs.existsSync(AUTO_TOOLS_DIR)) return tools;
const files = fs.readdirSync(AUTO_TOOLS_DIR).filter(f => f.endsWith(".mjs"));
for (const file of files) {
try {
const name = file.replace(/\.mjs$/, "");
const mod = await import(resolve(AUTO_TOOLS_DIR, file) + `?t=${Date.now()}`);
const fn = mod.default || mod.execute;
if (typeof fn === "function") tools[name] = fn;
} catch {}
}
return tools;
}
// ── 2. Auto-fix common tool errors ──
// Patterns that can be auto-patched
function autoInstall(bin) {
// Try to auto-install a missing binary
try {
// Check if npm package exists
execSync(`which npm 2>/dev/null`, { timeout: 3000 });
execSync(`npm list -g ${bin} 2>/dev/null || npm install -g ${bin} 2>/dev/null`, { timeout: 60000 });
return { fixed: true, message: `Auto-installed ${bin} via npm` };
} catch {}
try {
execSync(`which apt 2>/dev/null && apt install -y ${bin} 2>/dev/null`, { timeout: 60000 });
return { fixed: true, message: `Auto-installed ${bin} via apt` };
} catch {}
try {
execSync(`which pkg 2>/dev/null && pkg install -y ${bin} 2>/dev/null`, { timeout: 60000 });
return { fixed: true, message: `Auto-installed ${bin} via pkg` };
} catch {}
return { fixed: false, message: `${bin} not installed and auto-install failed` };
}
const FIX_PATTERNS = [
{
match: /command not found/i,
fix: async (tool, err) => {
const bin = tool || err.match(/'([^']+)'/)?.[1];
if (bin) return autoInstall(bin);
return { fixed: false, message: `Tool not installed. Install it and try again.` };
}
},
{
match: /Cannot find module/i,
fix: async (tool, err) => {
const mod = err.match(/'([^']+)'/)?.[1];
if (mod) {
try {
execSync(`npm install ${mod} 2>/dev/null`, { timeout: 60000, cwd: PHANTOM_DIR });
return { fixed: true, message: `Auto-installed module ${mod}` };
} catch {
try {
execSync(`npm install ${mod} 2>/dev/null`, { timeout: 60000 });
return { fixed: true, message: `Auto-installed module ${mod} globally` };
} catch {}
}
}
return { fixed: false, message: `Missing module ${mod || tool} — could not auto-install` };
}
},
{
match: /EACCES|EPERM/i,
fix: async (tool, err) => {
const file = err.match(/'([^']+)'/)?.[1];
if (file) {
try {
execSync(`chmod +x "${file}" 2>/dev/null`, { timeout: 5000 });
return { fixed: true, message: `Made ${file} executable` };
} catch {}
}
return { fixed: false, message: `Permission denied — run with elevated privileges` };
}
},
{
match: /ETIMEOUT|ENOTFOUND|fetch.*failed/i,
fix: async (tool, err) => {
return { fixed: false, message: `Network issue — check connectivity and try again` };
}
},
{
match: /ENOENT/i,
fix: async (tool, err) => {
return { fixed: false, message: `File/directory not found — verify path exists` };
}
},
{
match: /Syntax error.*backquote|EOF in backquote|unclosed.*backtick/i,
fix: async (tool, err) => {
return { fixed: false, message: `Check for unescaped backticks or $() in command` };
}
},
{
match: /Shell Error/i,
fix: async (tool, err) => {
return { fixed: false, message: `Shell command failed — check syntax and retry` };
}
},
];
// Analyze a tool error and attempt auto-fix
export async function analyzeError(tool, errorMessage) {
for (const pattern of FIX_PATTERNS) {
if (pattern.match.test(errorMessage)) {
const result = await pattern.fix(tool, errorMessage);
if (result) {
const st = loadState();
st.errors_fixed++;
st.auto_fixes.push({ ts: new Date().toISOString(), tool, error: errorMessage.slice(0, 100), fix: result.message });
saveState(st);
return result;
}
}
}
return null;
}
// ── 3. Auto-syntax heal — actually fix common issues ──
// Auto-fix common syntax/errors in Phantom's own source
export async function autoHealSyntax() {
const results = [];
const files = [
"phantom.mjs",
...fs.readdirSync(resolve(PHANTOM_DIR, "lib")).filter(f => f.endsWith(".mjs")).map(f => `lib/${f}`),
];
for (const file of files) {
const fp = resolve(PHANTOM_DIR, file);
if (!fs.existsSync(fp)) continue;
try {
execSync(`node --check "${fp}" 2>/dev/null`, { encoding: "utf-8", timeout: 10000 });
continue; // syntax OK
} catch (e) {
// Syntax error — try to fix common issues
try {
const content = fs.readFileSync(fp, "utf-8");
const stderr = e.stderr || e.message || "";
let fixed = false;
// Fix 1: trailing commas in non-module context (common in object literals)
if (stderr.includes("trailing comma") || stderr.includes("nexpected token")) {
const newContent = content.replace(/,(\s*[\n\r]+\s*[}\]])/g, '$1');
if (newContent !== content) {
fs.writeFileSync(fp, newContent, "utf-8");
try {
execSync(`node --check "${fp}" 2>/dev/null`, { encoding: "utf-8", timeout: 10000 });
fixed = true;
results.push({ file, fixed: true, error: "Fixed trailing commas" });
continue;
} catch {}
}
}
// Fix 2: unclosed string — look for unterminated string error
if (stderr.includes("Unterminated string") || stderr.includes("unterminated string")) {
const newContent = content.replace(/('''|"""|````)/g, (m) => m.slice(0, 3));
if (newContent !== content) {
fs.writeFileSync(fp, newContent, "utf-8");
try {
execSync(`node --check "${fp}" 2>/dev/null`, { encoding: "utf-8", timeout: 10000 });
fixed = true;
results.push({ file, fixed: true, error: "Fixed unterminated string" });
continue;
} catch {}
}
}
// Fix 3: missing closing brace or paren
if (stderr.includes("Unexpected end of input") || stderr.includes("missing )") || stderr.includes("missing }")) {
// Try adding closing braces
let newContent = content;
const opens = (content.match(/{/g) || []).length;
const closes = (content.match(/}/g) || []).length;
const opensP = (content.match(/\(/g) || []).length;
const closesP = (content.match(/\)/g) || []).length;
if (opens > closes) {
newContent += "\n" + "}".repeat(opens - closes);
}
if (opensP > closesP) {
newContent += "\n" + ")".repeat(opensP - closesP);
}
if (newContent !== content) {
fs.writeFileSync(fp, newContent, "utf-8");
try {
execSync(`node --check "${fp}" 2>/dev/null`, { encoding: "utf-8", timeout: 10000 });
fixed = true;
results.push({ file, fixed: true, error: `Fixed unbalanced braces/parens` });
continue;
} catch {}
}
}
if (!fixed) {
results.push({ file, fixed: false, error: stderr.trim().slice(0, 100) || "Unknown syntax error" });
}
} catch {
results.push({ file, fixed: false, error: "Cannot read file" });
}
}
}
return results;
}
// ── 4. Auto-optimization scanner ──
// Scan Phantom source for optimization opportunities
export function scanOptimizations() {
const issues = [];
const mainPath = resolve(PHANTOM_DIR, "phantom.mjs");
if (!fs.existsSync(mainPath)) return issues;
const src = fs.readFileSync(mainPath, "utf-8");
const lines = src.split("\n");
// Check for oversized functions
let fnStart = 0;
let braceCount = 0;
let inFn = false;
for (let i = 0; i < lines.length; i++) {
const l = lines[i];
if (!inFn && l.match(/async\s+\w+\s*\(/)) {
fnStart = i;
inFn = true;
braceCount = (l.match(/{/g) || []).length - (l.match(/}/g) || []).length;
} else if (inFn) {
braceCount += (l.match(/{/g) || []).length - (l.match(/}/g) || []).length;
if (braceCount <= 0) {
const len = i - fnStart;
if (len > 100) {
issues.push({ line: fnStart + 1, type: "long_fn", detail: `Function at line ${fnStart + 1} is ${len} lines — consider splitting` });
}
inFn = false;
}
}
}
// Check for repeated patterns
const dynamicImportCount = (src.match(/import\("child_process"\)/g) || []).length;
if (dynamicImportCount > 3) {
issues.push({ line: 0, type: "repeated_import", detail: `Dynamic import("child_process") used ${dynamicImportCount} times — hoist to module scope` });
}
// Check for console.log vs structured logging
const consoleCount = (src.match(/console\.(log|error|warn)\(/g) || []).length;
if (consoleCount > 60) {
issues.push({ line: 0, type: "console_spam", detail: `${consoleCount} console.* calls — use structured logging` });
}
return issues;
}
// ── 5. Run full evolution cycle ──
export async function autoEvolve() {
const st = loadState();
const results = [];
// Phase 1: Auto-detect missing wrappers
try {
const missing = detectMissingWrappers();
if (missing.length > 0) {
const created = [];
for (const { bin, blueprint } of missing) {
try {
const path = generateWrapper(bin, blueprint);
st.wrappers.push({ bin, path, ts: new Date().toISOString() });
created.push(bin);
results.push({ phase: "wrapper", bin, status: "created", path });
} catch (e) {
results.push({ phase: "wrapper", bin, status: "error", error: e.message });
}
}
// ── Git commit auto-generated wrappers ──
if (created.length > 0) {
const committed = gitCommit(
created.map(b => `lib/auto_tools/${b}.mjs`),
`🧬 auto-evolve: add ${created.length} tool wrapper(s): ${created.join(", ")}`
);
if (committed) {
results.push({ phase: "git", status: "committed", files: created.length });
}
}
}
} catch (e) {
results.push({ phase: "wrapper", status: "error", error: e.message });
}
// Phase 2: Syntax check and auto-heal
try {
const healResults = await autoHealSyntax();
for (const r of healResults) {
results.push({ phase: "heal", file: r.file, status: r.fixed ? "fixed" : "failed", error: r.error });
}
} catch (e) {
results.push({ phase: "heal", status: "error", error: e.message });
}
// Phase 3: Scan for optimizations
try {
const opts = scanOptimizations();
for (const o of opts) {
results.push({ phase: "optimize", type: o.type, detail: o.detail, line: o.line });
}
} catch (e) {
results.push({ phase: "optimize", status: "error", error: e.message });
}
// Phase 4: Self-syntax validation
try {
const { execSync } = await import("child_process");
execSync(`node --check "${resolve(PHANTOM_DIR, "phantom.mjs")}"`, { encoding: "utf-8", timeout: 10000 });
results.push({ phase: "validate", file: "phantom.mjs", status: "ok" });
execSync(`node --check "${resolve(PHANTOM_DIR, "lib/tools.mjs")}"`, { encoding: "utf-8", timeout: 10000 });
results.push({ phase: "validate", file: "lib/tools.mjs", status: "ok" });
execSync(`node --check "${resolve(PHANTOM_DIR, "lib/runtime.mjs")}"`, { encoding: "utf-8", timeout: 10000 });
results.push({ phase: "validate", file: "lib/runtime.mjs", status: "ok" });
execSync(`node --check "${resolve(PHANTOM_DIR, "lib/evolve.mjs")}"`, { encoding: "utf-8", timeout: 10000 });
results.push({ phase: "validate", file: "lib/evolve.mjs", status: "ok" });
} catch (e) {
results.push({ phase: "validate", status: "error", error: e.stderr?.slice(0, 200) || e.message });
}
// Phase 5: Test gate — only suggest push if tests pass
try {
const { execSync } = await import("child_process");
const testOut = execSync("node test/core.test.mjs", { cwd: PHANTOM_DIR, encoding: "utf-8", timeout: 30000 }).trim();
const passMatch = testOut.match(/pass\s+(\d+)/);
const failMatch = testOut.match(/fail\s+(\d+)/);
const passed = passMatch ? parseInt(passMatch[1]) : 0;
const failed = failMatch ? parseInt(failMatch[1]) : 0;
results.push({ phase: "test", status: failed > 0 ? "failed" : "passed", passed, failed });
if (failed > 0) {
// Attempt auto-heal on test failures
try {
const { autoHealSyntax } = await import("./evolve.mjs");
const healed = await autoHealSyntax();
for (const h of healed) {
results.push({ phase: "heal_retry", file: h.file, status: h.fixed ? "fixed" : "failed", error: h.error });
}
if (healed.some(h => h.fixed)) {
// Re-run tests after heal
const retryOut = execSync("node test/core.test.mjs", { cwd: PHANTOM_DIR, encoding: "utf-8", timeout: 30000 }).trim();
const retryPass = retryOut.match(/pass\s+(\d+)/);
const retryFail = retryOut.match(/fail\s+(\d+)/);
const rPassed = retryPass ? parseInt(retryPass[1]) : 0;
const rFailed = retryFail ? parseInt(retryFail[1]) : 0;
results.push({ phase: "test_retry", status: rFailed > 0 ? "failed" : "passed", passed: rPassed, failed: rFailed });
}
} catch { /* heal attempt failed */ }
}
} catch (e) {
results.push({ phase: "test", status: "error", error: e.stderr?.slice(0, 200) || e.message });
}
// Check readiness: have committed wrappers AND all tests passed?
const hasCommits = results.some(r => r.phase === "git" && r.status === "committed");
const testsPassed = results.some(r => r.phase === "test" && r.status === "passed");
const finalTestsOk = results.some(r => r.phase === "test_retry" && r.status === "passed") || testsPassed;
if (hasCommits && finalTestsOk) {
const pushResult = gitPush();
if (pushResult.pushed) {
results.push({ phase: "git", status: "pushed", summary: "✅ Auto-pushed to remote" });
} else {
results.push({ phase: "git", status: pushResult.reason?.startsWith("alignment") ? "blocked" : "push_failed",
reason: pushResult.reason, summary: `⚠ Push skipped: ${pushResult.reason}` });
}
} else if (hasCommits && !finalTestsOk) {
results.push({ phase: "git", status: "tests_failed", summary: "⚠ Not pushing — tests failing after changes" });
}
// Update state
st.generation++;
st.last_evolve = new Date().toISOString();
st.patches = [...(st.patches || []), ...results.filter(r => r.phase === "heal")];
saveState(st);
return { generation: st.generation, results, wrappers_created: st.wrappers.length, errors_fixed: st.errors_fixed };
}
// ── Quick startup evolution check ──
export async function startupEvolve() {
const st = loadState();
const results = { wrappers_checked: 0, wrappers_created: 0, issues: [] };
// Only run full evolution if more than 6 hours since last, or never run
const last = st.last_evolve ? new Date(st.last_evolve).getTime() : 0;
const sixHours = 6 * 60 * 60 * 1000;
if (Date.now() - last < sixHours && st.generation > 1) {
results.issues.push("skipped — recently evolved");
return results;
}
// Quick wrapper check
try {
const missing = detectMissingWrappers();
results.wrappers_checked = missing.length;
const created = [];
for (const { bin, blueprint } of missing) {
try {
generateWrapper(bin, blueprint);
st.wrappers.push({ bin, path: resolve(AUTO_TOOLS_DIR, `${bin}.mjs`), ts: new Date().toISOString() });
created.push(bin);
results.wrappers_created++;
} catch {}
}
// Git commit wrappers created at startup
if (created.length > 0) {
gitCommit(created.map(b => `lib/auto_tools/${b}.mjs`), "🧬 startup: add new auto-generated wrappers");
const pushResult = gitPush();
if (pushResult.pushed) results.push("auto-pushed");
else if (pushResult.reason) results.push(`push: ${pushResult.reason}`);
}
} catch {}
// Quick syntax check
try {
execSync(`node --check "${resolve(PHANTOM_DIR, "phantom.mjs")}"`, { encoding: "utf-8", timeout: 10000 });
} catch (e) {
results.issues.push("syntax error in phantom.mjs");
}
// Ensure generated modules directory exists
const generatedDir = resolve(PHANTOM_DIR, "lib", "generated");
if (!fs.existsSync(generatedDir)) fs.mkdirSync(generatedDir, { recursive: true });
if (results.wrappers_created > 0 || results.issues.length > 0) {
st.last_evolve = new Date().toISOString();
st.generation++;
saveState(st);
}
return results;
}
export function getEvolveStatus() {
const st = loadState();
return {
generation: st.generation,
wrappers_created: st.wrappers.length,
errors_fixed: st.errors_fixed,
patches_applied: st.patches.length,
last_evolve: st.last_evolve,
wrapper_list: st.wrappers,
recent_fixes: st.auto_fixes?.slice(-5),
};
}
// ── 7. SELF-HEAL: full auto-diagnose → fix → test → iterate ──
// Runs until clean or max attempts reached.
export async function selfHeal(options = {}) {
const maxAttempts = options.maxAttempts || 5;
const log = [];
const startTime = Date.now();
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const round = { attempt, checks: [], fixes: [], tests: [] };
// Phase 1: Syntax check all .mjs files
const mjsFiles = ["phantom.mjs", ...fs.readdirSync(resolve(PHANTOM_DIR, "lib")).filter(f => f.endsWith(".mjs")).map(f => `lib/${f}`)];
let allClean = true;
for (const file of mjsFiles) {
const fp = resolve(PHANTOM_DIR, file);
if (!fs.existsSync(fp)) continue;
try {
execSync(`node --check "${fp}" 2>/dev/null`, { encoding: "utf-8", timeout: 10000 });
round.checks.push({ file, status: "ok" });
} catch {
allClean = false;
round.checks.push({ file, status: "error" });
}
}
// Phase 2: Auto-fix syntax issues
if (!allClean) {
const healResults = await autoHealSyntax();
for (const r of healResults) {
round.fixes.push(r);
if (r.fixed) {
const st = loadState();
st.patches.push(r);
st.errors_fixed = (st.errors_fixed || 0) + 1;
saveState(st);
}
}
// If nothing was fixed, break — can't heal further
if (!healResults.some(r => r.fixed)) {
log.push(round);
break;
}
}
// Phase 3: Run tests
try {
const testOut = execSync("node test/core.test.mjs 2>/dev/null && echo 'TESTS_PASSED'", {
cwd: PHANTOM_DIR, encoding: "utf-8", timeout: 60000
}).trim();
const passed = testOut.includes("TESTS_PASSED");
round.tests.push({ status: passed ? "passed" : "failed" });
if (passed) {
log.push(round);
// If all clean and tests pass, commit and push
try {
const staged = execSync("git diff --cached --stat", { cwd: PHANTOM_DIR, encoding: "utf-8", timeout: 5000 }).trim();
if (staged) {
const committed = gitCommit([], `🧬 self-heal: auto-fix ${round.fixes.filter(f => f.fixed).length} issue(s)`);
if (committed) {
const pushResult = gitPush();
round.push = pushResult.pushed ? "pushed" : pushResult.reason || "push_failed";
}
}
} catch {}
break; // done
}
} catch (e) {
round.tests.push({ status: "error", error: e.stderr?.slice(0, 200) || e.message });
}
log.push(round);
}
// Update state
const st = loadState();
st.last_evolve = new Date().toISOString();
st.generation++;
saveState(st);
return {
elapsed: Date.now() - startTime,
attempts: log.length,
clean: log[log.length - 1]?.tests?.some(t => t.status === "passed") || false,
log,
};
}