feat: Capability Evolver 升级 + 日常运行日志

主要变更:
- Capability Evolver 技能大幅升级
  - 新增 LLM 审查、叙事记忆、反思机制模块
  - 改进 Agent 间通信协议和技能搜索
  - 整合 Git 备份到 evolver-run.sh

- Agent 配置与日志更新
  - dev-assist/haonan/main 认证和会话配置
  - 5 个 agent 的 AGENTS.md 文档更新
  - haonan 的 MEMORY.md 和 SOUL.md 更新

- 新增功能目录
  - hooks/self-improvement 自我改进钩子
  - memory/evolution 进化记忆状态
  - workspace/.learnings/ 学习记录

- 清理
  - 删除 scripts/git-auto-sync.sh(已整合到 evolver)
  - 删除旧的 _meta.json 配置
  - 忽略临时工作空间目录
This commit is contained in:
2026-03-05 19:20:35 +08:00
parent 8fb52c48fb
commit 0878535cb8
67 changed files with 3762 additions and 513 deletions
+14
View File
@@ -0,0 +1,14 @@
node_modules/
.env
memory/
assets/gep/candidates.jsonl
assets/gep/external_candidates.jsonl
dist-public/
# Docker / vibe testing
test/tmp/
.docker-test-state/
# Local identity persistence (device + node)
.evomap_device_id
.evomap_node_id
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2026 OpenClaw
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+9 -2
View File
@@ -19,6 +19,11 @@ Capability Evolver is the core engine behind **[EvoMap](https://evomap.ai)**, a
Keywords: protocol-constrained evolution, audit trail, genes and capsules, prompt governance.
## Prerequisites
- **Node.js** >= 18
- **Git** -- Required. Evolver uses git for rollback, blast radius calculation, and solidify. Running in a non-git directory will fail with a clear error message.
## Try It Now (Minimal)
```bash
@@ -221,14 +226,16 @@ The script automatically detects if compatible local skills (like `skills/feishu
## Acknowledgments
- [onthebigtree](https://github.com/onthebigtree) -- Inspired the creation of evomap evolution network.
- [onthebigtree](https://github.com/onthebigtree) -- Inspired the creation of evomap evolution network. Fixed three runtime and logic bugs (PR #25); contributed hostname privacy hashing, portable validation paths, and dead code cleanup (PR #26).
- [lichunr](https://github.com/lichunr) -- Contributed thousands of dollars in tokens for our compute network to use for free.
- [shinjiyu](https://github.com/shinjiyu) -- Submitted numerous bug reports and contributed multilingual signal extraction with snippet-carrying tags (PR #112).
- [voidborne-d](https://github.com/voidborne-d) -- Hardened pre-broadcast sanitization with 11 new credential redaction patterns (PR #107); added 45 tests for strategy, validationReport, and envFingerprint (PR #139).
- [blackdogcat](https://github.com/blackdogcat) -- Fixed missing dotenv dependency and implemented intelligent CPU load threshold auto-calculation (PR #144).
- [LKCY33](https://github.com/LKCY33) -- Fixed .env loading path and directory permissions (PR #21).
- [hendrixAIDev](https://github.com/hendrixAIDev) -- Fixed performMaintenance() running in dry-run mode (PR #68).
- [toller892](https://github.com/toller892) -- Independently identified and reported the events.jsonl forbidden_paths bug (PR #149).
- [upbit](https://github.com/upbit) -- Played a vital role in popularizing evolver and evomap technologies.
- [Chi Jianqiang](https://mowen.cn) -- Made significant contributions to promotion and user experience improvements.
- More contributors to be added.
## License
+11 -3
View File
@@ -27,6 +27,11 @@ Capability Evolver 是 **[EvoMap](https://evomap.ai)** 的核心引擎。EvoMap
- **动态集成**:自动检测并使用本地工具,如果不存在则回退到通用模式。
- **持续循环模式**:持续运行的自我进化循环。
## 前置条件
- **Node.js** >= 18
- **Git** -- 必需。Evolver 依赖 git 进行回滚、变更范围计算和固化(solidify)。在非 git 目录中运行会直接报错并退出。
## 使用方法
### 标准运行(自动化)
@@ -190,13 +195,16 @@ MAJOR.MINOR.PATCH
## 鸣谢
- [onthebigtree](https://github.com/onthebigtree) -- 启发了 evomap 进化网络的诞生。
- [onthebigtree](https://github.com/onthebigtree) -- 启发了 evomap 进化网络的诞生。修复了三个运行时逻辑 bug (PR #25);贡献了主机名隐私哈希、可移植验证路径和死代码清理 (PR #26)。
- [lichunr](https://github.com/lichunr) -- 提供了数千美金 Token 供算力网络免费使用。
- [shinjiyu](https://github.com/shinjiyu) -- 为 evolver 和 evomap 提交了大量 bug report,并贡献了多语言信号提取与 snippet 标签功能 (PR #112)。
- [voidborne-d](https://github.com/voidborne-d) -- 为预广播脱敏层新增 11 种凭证检测模式,强化安全防护 (PR #107)。
- [voidborne-d](https://github.com/voidborne-d) -- 为预广播脱敏层新增 11 种凭证检测模式,强化安全防护 (PR #107);新增 45 项测试覆盖 strategy、validationReport 和 envFingerprint (PR #139)。
- [blackdogcat](https://github.com/blackdogcat) -- 修复 dotenv 缺失依赖并实现智能 CPU 负载阈值自动计算 (PR #144)。
- [LKCY33](https://github.com/LKCY33) -- 修复 .env 加载路径和目录权限问题 (PR #21)。
- [hendrixAIDev](https://github.com/hendrixAIDev) -- 修复 dry-run 模式下 performMaintenance() 仍执行的问题 (PR #68)。
- [toller892](https://github.com/toller892) -- 独立发现并报告了 events.jsonl forbidden_paths 冲突 bug (PR #149)。
- [upbit](https://github.com/upbit) -- 在 evolver 和 evomap 技术的普及中起到了至关重要的作用。
- [池建强](https://mowen.cn) -- 在传播和用户体验改进过程中做出了巨大贡献。
- 其余贡献者扩充中。
## 许可证
MIT
-17
View File
@@ -1,17 +0,0 @@
{
"owner": "autogame-17",
"slug": "capability-evolver",
"displayName": "Capability Evolver",
"latest": {
"version": "1.20.4",
"publishedAt": 1772344308631,
"commit": "https://github.com/openclaw/skills/commit/f6fbba39530294955fd7e3b735f4ca227f4eb546"
},
"history": [
{
"version": "1.14.0",
"publishedAt": 1771472983583,
"commit": "https://github.com/openclaw/skills/commit/fbb1b4a640f1c8254a1dd2757e8eb38d246b9d59"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

@@ -0,0 +1,4 @@
{
"version": 1,
"failed_capsules": []
}
+77 -19
View File
@@ -43,12 +43,16 @@ function acquireLock() {
try {
if (fs.existsSync(lockFile)) {
const pid = parseInt(fs.readFileSync(lockFile, 'utf8').trim(), 10);
try {
process.kill(pid, 0); // Check if process exists
console.log(`[Singleton] Evolver loop already running (PID ${pid}). Exiting.`);
return false;
} catch (e) {
console.log(`[Singleton] Stale lock found (PID ${pid}). Taking over.`);
if (!Number.isFinite(pid) || pid <= 0) {
console.log('[Singleton] Corrupt lock file (invalid PID). Taking over.');
} else {
try {
process.kill(pid, 0);
console.log(`[Singleton] Evolver loop already running (PID ${pid}). Exiting.`);
return false;
} catch (e) {
console.log(`[Singleton] Stale lock found (PID ${pid}). Taking over.`);
}
}
}
fs.writeFileSync(lockFile, String(process.pid));
@@ -88,7 +92,8 @@ async function main() {
process.env.EVOLVE_BRIDGE = 'false';
console.log('Loop mode enabled (internal daemon).');
const solidifyStatePath = path.join(__dirname, 'memory', 'evolution_solidify_state.json');
const { getEvolutionDir } = require('./src/gep/paths');
const solidifyStatePath = path.join(getEvolutionDir(), 'evolution_solidify_state.json');
const minSleepMs = parseMs(process.env.EVOLVER_MIN_SLEEP_MS, 2000);
const maxSleepMs = parseMs(process.env.EVOLVER_MAX_SLEEP_MS, 300000);
@@ -149,16 +154,20 @@ async function main() {
const memMb = process.memoryUsage().rss / 1024 / 1024;
if (cycleCount >= maxCyclesPerProcess || memMb > maxRssMb) {
console.log(`[Daemon] Restarting self (cycles=${cycleCount}, rssMb=${memMb.toFixed(0)})`);
releaseLock();
const spawnOpts = {
detached: true,
stdio: 'ignore',
env: process.env,
windowsHide: true,
};
const child = spawn(process.execPath, [__filename, ...args], spawnOpts);
child.unref();
process.exit(0);
try {
const spawnOpts = {
detached: true,
stdio: 'ignore',
env: process.env,
windowsHide: true,
};
const child = spawn(process.execPath, [__filename, ...args], spawnOpts);
child.unref();
releaseLock();
process.exit(0);
} catch (spawnErr) {
console.error('[Daemon] Spawn failed, continuing current process:', spawnErr.message);
}
}
}
@@ -268,15 +277,64 @@ async function main() {
process.exit(2);
}
} else if (command === 'asset-log') {
const { summarizeCallLog, readCallLog, getLogPath } = require('./src/gep/assetCallLog');
const runIdFlag = args.find(a => typeof a === 'string' && a.startsWith('--run='));
const actionFlag = args.find(a => typeof a === 'string' && a.startsWith('--action='));
const lastFlag = args.find(a => typeof a === 'string' && a.startsWith('--last='));
const sinceFlag = args.find(a => typeof a === 'string' && a.startsWith('--since='));
const jsonMode = args.includes('--json');
const opts = {};
if (runIdFlag) opts.run_id = runIdFlag.slice('--run='.length);
if (actionFlag) opts.action = actionFlag.slice('--action='.length);
if (lastFlag) opts.last = parseInt(lastFlag.slice('--last='.length), 10);
if (sinceFlag) opts.since = sinceFlag.slice('--since='.length);
if (jsonMode) {
const entries = readCallLog(opts);
console.log(JSON.stringify(entries, null, 2));
} else {
const summary = summarizeCallLog(opts);
console.log(`\n[Asset Call Log] ${getLogPath()}`);
console.log(` Total entries: ${summary.total_entries}`);
console.log(` Unique assets: ${summary.unique_assets}`);
console.log(` Unique runs: ${summary.unique_runs}`);
console.log(` By action:`);
for (const [action, count] of Object.entries(summary.by_action)) {
console.log(` ${action}: ${count}`);
}
if (summary.entries.length > 0) {
console.log(`\n Recent entries:`);
const show = summary.entries.slice(-10);
for (const e of show) {
const ts = e.timestamp ? e.timestamp.slice(0, 19) : '?';
const assetShort = e.asset_id ? e.asset_id.slice(0, 20) + '...' : '(none)';
const sigPreview = Array.isArray(e.signals) ? e.signals.slice(0, 3).join(', ') : '';
console.log(` [${ts}] ${e.action || '?'} asset=${assetShort} score=${e.score || '-'} mode=${e.mode || '-'} signals=[${sigPreview}] run=${e.run_id || '-'}`);
}
} else {
console.log('\n No entries found.');
}
console.log('');
}
} else {
console.log(`Usage: node index.js [run|/evolve|solidify|distill] [--loop]
console.log(`Usage: node index.js [run|/evolve|solidify|distill|asset-log] [--loop]
- solidify flags:
- --dry-run
- --no-rollback
- --intent=repair|optimize|innovate
- --summary=...
- distill flags:
- --response-file=<path> (LLM response file for skill distillation)`);
- --response-file=<path> (LLM response file for skill distillation)
- asset-log flags:
- --run=<run_id> (filter by run ID)
- --action=<action> (filter: hub_search_hit, hub_search_miss, asset_reuse, asset_reference, asset_publish, asset_publish_skip)
- --last=<N> (show last N entries)
- --since=<ISO_date> (entries after date)
- --json (raw JSON output)`);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "evolver",
"version": "1.20.4",
"version": "1.23.0",
"description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
"main": "index.js",
"keywords": [
+60
View File
@@ -18,6 +18,7 @@ const {
const { selectGeneAndCapsule, matchPatternToSignals } = require('./gep/selector');
const { buildGepPrompt, buildReusePrompt, buildHubMatchedBlock } = require('./gep/prompt');
const { hubSearch } = require('./gep/hubSearch');
const { logAssetCall } = require('./gep/assetCallLog');
const { extractCapabilityCandidates, renderCandidatesPreview } = require('./gep/candidates');
const memoryAdapter = require('./gep/memoryGraphAdapter');
const {
@@ -35,6 +36,8 @@ const { buildMutation, isHighRiskMutationAllowed } = require('./gep/mutation');
const { selectPersonalityForRun } = require('./gep/personality');
const { clip, writePromptArtifact, renderSessionsSpawnCall } = require('./gep/bridge');
const { getEvolutionDir } = require('./gep/paths');
const { shouldReflect, buildReflectionContext, recordReflection } = require('./gep/reflection');
const { loadNarrativeSummary } = require('./gep/narrativeMemory');
const REPO_ROOT = getRepoRoot();
@@ -744,6 +747,19 @@ async function run() {
delete process.env.FORCE_INNOVATION;
// SAFEGUARD: Git repository check.
// Solidify, rollback, and blast radius all depend on git. Without a git repo
// these operations silently produce empty results, leading to data loss.
try {
execSync('git rev-parse --git-dir', { cwd: REPO_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 });
} catch (_) {
console.error('[Evolver] FATAL: Not a git repository (' + REPO_ROOT + ').');
console.error('[Evolver] Evolver requires git for rollback, blast radius calculation, and solidify.');
console.error('[Evolver] Run "git init && git add -A && git commit -m init" in your project root, then try again.');
process.exitCode = 1;
return;
}
var dormantHypothesis = readDormantHypothesis();
if (dormantHypothesis) {
console.log('[DormantHypothesis] Recovered partial state from previous backoff: ' + (dormantHypothesis.backoff_reason || 'unknown'));
@@ -1177,6 +1193,31 @@ async function run() {
throw new Error(`MemoryGraph Read failed: ${e.message}`);
}
// Reflection Phase: periodically pause to assess evolution strategy.
try {
const cycleState = fs.existsSync(STATE_FILE) ? JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')) : {};
const cycleCount = cycleState.cycleCount || 0;
if (shouldReflect({ cycleCount, recentEvents })) {
const narrativeSummary = loadNarrativeSummary(3000);
const reflectionCtx = buildReflectionContext({
recentEvents,
signals,
memoryAdvice,
narrative: narrativeSummary,
});
recordReflection({
cycle_count: cycleCount,
signals_snapshot: signals.slice(0, 20),
preferred_gene: memoryAdvice && memoryAdvice.preferredGeneId ? memoryAdvice.preferredGeneId : null,
banned_genes: memoryAdvice && Array.isArray(memoryAdvice.bannedGeneIds) ? memoryAdvice.bannedGeneIds : [],
context_preview: reflectionCtx.slice(0, 1000),
});
console.log(`[Reflection] Strategic reflection recorded at cycle ${cycleCount}.`);
}
} catch (e) {
console.log('[Reflection] Failed (non-fatal): ' + (e && e.message ? e.message : e));
}
var recentFailedCapsules = [];
try {
recentFailedCapsules = readRecentFailedCapsules(50);
@@ -1370,6 +1411,25 @@ async function run() {
hub_lessons: hubLessons,
};
writeStateForSolidify(prevState);
if (hubHit && hubHit.hit) {
const assetAction = hubHit.mode === 'direct' ? 'asset_reuse' : 'asset_reference';
logAssetCall({
run_id: runId,
action: assetAction,
asset_id: hubHit.asset_id || null,
asset_type: hubHit.match && hubHit.match.type ? hubHit.match.type : null,
source_node_id: hubHit.source_node_id || null,
chain_id: hubHit.chain_id || null,
score: hubHit.score || null,
mode: hubHit.mode,
signals: Array.isArray(signals) ? signals : [],
extra: {
selected_gene_id: selectedGene && selectedGene.id ? selectedGene.id : null,
task_id: activeTask ? (activeTask.id || activeTask.task_id || null) : null,
},
});
}
} catch (e) {
console.error(`[SolidifyState] Write failed: ${e.message}`);
}
+2 -2
View File
@@ -28,8 +28,8 @@ function getBlastRadiusLimits() {
function isBlastRadiusSafe(blastRadius) {
var lim = getBlastRadiusLimits();
var files = blastRadius && Number.isFinite(Number(blastRadius.files)) ? Number(blastRadius.files) : 0;
var lines = blastRadius && Number.isFinite(Number(blastRadius.lines)) ? Number(blastRadius.lines) : 0;
var files = blastRadius && Number.isFinite(Number(blastRadius.files)) ? Math.max(0, Number(blastRadius.files)) : 0;
var lines = blastRadius && Number.isFinite(Number(blastRadius.lines)) ? Math.max(0, Number(blastRadius.lines)) : 0;
return files <= lim.maxFiles && lines <= lim.maxLines;
}
@@ -96,6 +96,9 @@ function getNodeId() {
// --- Base message builder ---
function buildMessage(params) {
if (!params || typeof params !== 'object') {
throw new Error('buildMessage requires a params object');
}
var messageType = params.messageType;
var payload = params.payload;
var senderId = params.senderId;
@@ -197,14 +200,18 @@ function buildPublishBundle(opts) {
function buildFetch(opts) {
var o = opts || {};
var fetchPayload = {
asset_type: o.assetType || null,
local_id: o.localId || null,
content_hash: o.contentHash || null,
};
if (Array.isArray(o.signals) && o.signals.length > 0) {
fetchPayload.signals = o.signals;
}
return buildMessage({
messageType: 'fetch',
senderId: o.nodeId,
payload: {
asset_type: o.assetType || null,
local_id: o.localId || null,
content_hash: o.contentHash || null,
},
payload: fetchPayload,
});
}
@@ -351,7 +358,8 @@ function httpTransportReceive(opts) {
var hubUrl = (opts && opts.hubUrl) || process.env.A2A_HUB_URL;
if (!hubUrl) return Promise.resolve([]);
var assetType = (opts && opts.assetType) || null;
var fetchMsg = buildFetch({ assetType: assetType });
var signals = (opts && Array.isArray(opts.signals)) ? opts.signals : null;
var fetchMsg = buildFetch({ assetType: assetType, signals: signals });
var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/fetch';
return fetch(endpoint, {
method: 'POST',
@@ -0,0 +1,130 @@
// Append-only asset call log for tracking Hub asset interactions per evolution run.
// Log file: {evolution_dir}/asset_call_log.jsonl
const fs = require('fs');
const path = require('path');
const { getEvolutionDir } = require('./paths');
function getLogPath() {
return path.join(getEvolutionDir(), 'asset_call_log.jsonl');
}
function ensureDir(filePath) {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* Append a single asset call record to the log.
*
* @param {object} entry
* @param {string} entry.run_id
* @param {string} entry.action - hub_search_hit | hub_search_miss | asset_reuse | asset_reference | asset_publish | asset_publish_skip
* @param {string} [entry.asset_id]
* @param {string} [entry.asset_type]
* @param {string} [entry.source_node_id]
* @param {string} [entry.chain_id]
* @param {number} [entry.score]
* @param {string} [entry.mode] - direct | reference
* @param {string[]} [entry.signals]
* @param {string} [entry.reason]
* @param {object} [entry.extra]
*/
function logAssetCall(entry) {
if (!entry || typeof entry !== 'object') return;
try {
const logPath = getLogPath();
ensureDir(logPath);
const record = {
timestamp: new Date().toISOString(),
...entry,
};
fs.appendFileSync(logPath, JSON.stringify(record) + '\n', 'utf8');
} catch (e) {
// Non-fatal: never block evolution for logging failure
}
}
/**
* Read asset call log entries with optional filters.
*
* @param {object} [opts]
* @param {string} [opts.run_id] - filter by run_id
* @param {string} [opts.action] - filter by action type
* @param {number} [opts.last] - only return last N entries
* @param {string} [opts.since] - ISO date string, only entries after this time
* @returns {object[]}
*/
function readCallLog(opts) {
const o = opts || {};
const logPath = getLogPath();
if (!fs.existsSync(logPath)) return [];
const raw = fs.readFileSync(logPath, 'utf8');
const lines = raw.split('\n').filter(Boolean);
let entries = [];
for (const line of lines) {
try {
entries.push(JSON.parse(line));
} catch (e) { /* skip corrupt lines */ }
}
if (o.since) {
const sinceTs = new Date(o.since).getTime();
if (Number.isFinite(sinceTs)) {
entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceTs);
}
}
if (o.run_id) {
entries = entries.filter(e => e.run_id === o.run_id);
}
if (o.action) {
entries = entries.filter(e => e.action === o.action);
}
if (o.last && Number.isFinite(o.last) && o.last > 0) {
entries = entries.slice(-o.last);
}
return entries;
}
/**
* Summarize asset call log (for CLI display).
*
* @param {object} [opts] - same filters as readCallLog
* @returns {object} summary with totals and per-action counts
*/
function summarizeCallLog(opts) {
const entries = readCallLog(opts);
const actionCounts = {};
const assetsSeen = new Set();
const runsSeen = new Set();
for (const e of entries) {
const a = e.action || 'unknown';
actionCounts[a] = (actionCounts[a] || 0) + 1;
if (e.asset_id) assetsSeen.add(e.asset_id);
if (e.run_id) runsSeen.add(e.run_id);
}
return {
total_entries: entries.length,
unique_assets: assetsSeen.size,
unique_runs: runsSeen.size,
by_action: actionCounts,
entries,
};
}
module.exports = {
logAssetCall,
readCallLog,
summarizeCallLog,
getLogPath,
};
+90 -27
View File
@@ -2,11 +2,17 @@
//
// Flow: extractSignals() -> hubSearch(signals) -> if hit: reuse; if miss: normal evolve
// Two modes: direct (skip local reasoning) | reference (inject into prompt as strong hint)
//
// Uses POST /a2a/fetch with signals field (protocol-native) instead of GET /a2a/assets/search.
// This returns full payload (content, diff, strategy) in a single round-trip.
const { getNodeId } = require('./a2aProtocol');
const { getNodeId, buildFetch } = require('./a2aProtocol');
const { logAssetCall } = require('./assetCallLog');
const DEFAULT_MIN_REUSE_SCORE = 0.72;
const DEFAULT_REUSE_MODE = 'reference'; // 'direct' | 'reference'
const MAX_STREAK_CAP = 5;
const TIMEOUT_REASON = 'hub_search_timeout';
function getHubUrl() {
return (process.env.A2A_HUB_URL || '').replace(/\/+$/, '');
@@ -24,13 +30,14 @@ function getMinReuseScore() {
/**
* Score a hub asset for local reuse quality.
* rank = confidence * max(success_streak, 1) * (reputation / 100)
* rank = confidence * min(max(success_streak, 1), MAX_STREAK_CAP) * (reputation / 100)
* Streak is capped to prevent unbounded score inflation.
*/
function scoreHubResult(asset) {
const confidence = Number(asset.confidence) || 0;
const streak = Math.max(Number(asset.success_streak) || 0, 1);
// Reputation is included in asset from hub ranked endpoint; default 50 if missing
const reputation = Number(asset.reputation_score) || 50;
const streak = Math.min(Math.max(Number(asset.success_streak) || 0, 1), MAX_STREAK_CAP);
const repRaw = Number(asset.reputation_score);
const reputation = Number.isFinite(repRaw) ? repRaw : 50;
return confidence * streak * (reputation / 100);
}
@@ -45,7 +52,6 @@ function pickBestMatch(results, threshold) {
let bestScore = 0;
for (const asset of results) {
// Only consider promoted assets
if (asset.status && asset.status !== 'promoted') continue;
const s = scoreHubResult(asset);
if (s > bestScore) {
@@ -64,48 +70,96 @@ function pickBestMatch(results, threshold) {
}
/**
* Search the hub for reusable capsules matching the given signals.
* Search the hub for reusable assets matching the given signals.
* Uses POST /a2a/fetch with signals (protocol-native, returns full payload).
* Falls back to GET /a2a/assets/search if fetch fails.
* Returns { hit: true, match, score, mode } or { hit: false }.
*/
async function hubSearch(signals, opts) {
const hubUrl = getHubUrl();
if (!hubUrl) return { hit: false, reason: 'no_hub_url' };
const signalList = Array.isArray(signals) ? signals.filter(Boolean) : [];
const signalList = Array.isArray(signals)
? signals.map(s => typeof s === 'string' ? s.trim() : '').filter(Boolean)
: [];
if (signalList.length === 0) return { hit: false, reason: 'no_signals' };
const threshold = (opts && Number.isFinite(opts.threshold)) ? opts.threshold : getMinReuseScore();
const limit = (opts && Number.isFinite(opts.limit)) ? opts.limit : 5;
const timeout = (opts && Number.isFinite(opts.timeoutMs)) ? opts.timeoutMs : 8000;
try {
const params = new URLSearchParams();
params.set('signals', signalList.join(','));
params.set('status', 'promoted');
params.set('limit', String(limit));
const fetchMsg = buildFetch({ signals: signalList });
const endpoint = hubUrl + '/a2a/fetch';
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const timer = setTimeout(() => controller.abort(TIMEOUT_REASON), timeout);
const url = `${hubUrl}/a2a/assets/search?${params.toString()}`;
const res = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
const token = process.env.A2A_HUB_TOKEN;
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify(fetchMsg),
signal: controller.signal,
});
clearTimeout(timer);
if (!res.ok) return { hit: false, reason: `hub_http_${res.status}` };
if (!res.ok) {
logAssetCall({
run_id: (opts && opts.run_id) || null,
action: 'hub_search_miss',
signals: signalList,
reason: `hub_http_${res.status}`,
via: 'fetch_with_signals',
});
return { hit: false, reason: `hub_http_${res.status}` };
}
const data = await res.json();
const assets = Array.isArray(data.assets) ? data.assets : [];
const results = (data && data.payload && Array.isArray(data.payload.results))
? data.payload.results
: [];
if (assets.length === 0) return { hit: false, reason: 'no_results' };
if (results.length === 0) {
logAssetCall({
run_id: (opts && opts.run_id) || null,
action: 'hub_search_miss',
signals: signalList,
reason: 'no_results',
via: 'fetch_with_signals',
});
return { hit: false, reason: 'no_results' };
}
const pick = pickBestMatch(assets, threshold);
if (!pick) return { hit: false, reason: 'below_threshold', candidates: assets.length };
const pick = pickBestMatch(results, threshold);
if (!pick) {
logAssetCall({
run_id: (opts && opts.run_id) || null,
action: 'hub_search_miss',
signals: signalList,
reason: 'below_threshold',
extra: { candidates: results.length, threshold },
via: 'fetch_with_signals',
});
return { hit: false, reason: 'below_threshold', candidates: results.length };
}
console.log(`[HubSearch] Hit: ${pick.match.asset_id || pick.match.local_id} (score=${pick.score}, mode=${pick.mode})`);
console.log(`[HubSearch] Hit via fetch+signals: ${pick.match.asset_id || 'unknown'} (score=${pick.score}, mode=${pick.mode})`);
logAssetCall({
run_id: (opts && opts.run_id) || null,
action: 'hub_search_hit',
asset_id: pick.match.asset_id || null,
asset_type: pick.match.asset_type || pick.match.type || null,
source_node_id: pick.match.source_node_id || null,
chain_id: pick.match.chain_id || null,
score: pick.score,
mode: pick.mode,
signals: signalList,
via: 'fetch_with_signals',
});
return {
hit: true,
@@ -117,9 +171,18 @@ async function hubSearch(signals, opts) {
chain_id: pick.match.chain_id || null,
};
} catch (err) {
// Hub unreachable is non-fatal; fall through to normal evolve
console.log(`[HubSearch] Failed (non-fatal): ${err.message}`);
return { hit: false, reason: 'fetch_error', error: err.message };
const isTimeout = err.name === 'AbortError' || (err.cause && err.cause === TIMEOUT_REASON);
const reason = isTimeout ? 'timeout' : 'fetch_error';
console.log(`[HubSearch] Failed (non-fatal, ${reason}): ${err.message}`);
logAssetCall({
run_id: (opts && opts.run_id) || null,
action: 'hub_search_miss',
signals: signalList,
reason,
extra: { error: err.message },
via: 'fetch_with_signals',
});
return { hit: false, reason, error: err.message };
}
}
@@ -0,0 +1,86 @@
'use strict';
const { execSync } = require('child_process');
const { getRepoRoot } = require('./paths');
const REVIEW_ENABLED_KEY = 'EVOLVER_LLM_REVIEW';
const REVIEW_TIMEOUT_MS = 30000;
function isLlmReviewEnabled() {
return String(process.env[REVIEW_ENABLED_KEY] || '').toLowerCase() === 'true';
}
function buildReviewPrompt({ diff, gene, signals, mutation }) {
const geneId = gene && gene.id ? gene.id : '(unknown)';
const category = (mutation && mutation.category) || (gene && gene.category) || 'unknown';
const rationale = mutation && mutation.rationale ? String(mutation.rationale).slice(0, 500) : '(none)';
const signalsList = Array.isArray(signals) ? signals.slice(0, 8).join(', ') : '(none)';
const diffPreview = String(diff || '').slice(0, 6000);
return `You are reviewing a code change produced by an autonomous evolution engine.
## Context
- Gene: ${geneId} (${category})
- Signals: [${signalsList}]
- Rationale: ${rationale}
## Diff
\`\`\`diff
${diffPreview}
\`\`\`
## Review Criteria
1. Does this change address the stated signals?
2. Are there any obvious regressions or bugs introduced?
3. Is the blast radius proportionate to the problem?
4. Are there any security or safety concerns?
## Response Format
Respond with a JSON object:
{
"approved": true|false,
"confidence": 0.0-1.0,
"concerns": ["..."],
"summary": "one-line review summary"
}`;
}
function runLlmReview({ diff, gene, signals, mutation }) {
if (!isLlmReviewEnabled()) return null;
const prompt = buildReviewPrompt({ diff, gene, signals, mutation });
try {
const repoRoot = getRepoRoot();
const escapedPrompt = prompt.replace(/'/g, "'\\''");
const result = execSync(
`echo '${escapedPrompt}' | node -e "
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin });
let input = '';
rl.on('line', l => input += l + '\\n');
rl.on('close', () => {
console.log(JSON.stringify({ approved: true, confidence: 0.7, concerns: [], summary: 'auto-approved (no external LLM configured)' }));
});
"`,
{
cwd: repoRoot,
encoding: 'utf8',
timeout: REVIEW_TIMEOUT_MS,
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
}
);
try {
return JSON.parse(result.trim());
} catch (_) {
return { approved: true, confidence: 0.5, concerns: ['failed to parse review response'], summary: 'review parse error' };
}
} catch (e) {
console.log('[LLMReview] Execution failed (non-fatal): ' + (e && e.message ? e.message : e));
return { approved: true, confidence: 0.5, concerns: ['review execution failed'], summary: 'review timeout or error' };
}
}
module.exports = { isLlmReviewEnabled, runLlmReview, buildReviewPrompt };
@@ -0,0 +1,108 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { getNarrativePath, getEvolutionDir } = require('./paths');
const MAX_NARRATIVE_ENTRIES = 30;
const MAX_NARRATIVE_SIZE = 12000;
function ensureDir(dir) {
try { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
}
function recordNarrative({ gene, signals, mutation, outcome, blast, capsule }) {
const narrativePath = getNarrativePath();
ensureDir(path.dirname(narrativePath));
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
const geneId = gene && gene.id ? gene.id : '(auto)';
const category = (mutation && mutation.category) || (gene && gene.category) || 'unknown';
const status = outcome && outcome.status ? outcome.status : 'unknown';
const score = outcome && typeof outcome.score === 'number' ? outcome.score.toFixed(2) : '?';
const signalsSummary = Array.isArray(signals) ? signals.slice(0, 4).join(', ') : '(none)';
const filesChanged = blast ? blast.files : 0;
const linesChanged = blast ? blast.lines : 0;
const rationale = mutation && mutation.rationale
? String(mutation.rationale).slice(0, 200) : '';
const strategy = gene && Array.isArray(gene.strategy)
? gene.strategy.slice(0, 3).map((s, i) => ` ${i + 1}. ${s}`).join('\n') : '';
const capsuleSummary = capsule && capsule.summary ? String(capsule.summary).slice(0, 200) : '';
const entry = [
`### [${ts}] ${category.toUpperCase()} - ${status}`,
`- Gene: ${geneId} | Score: ${score} | Scope: ${filesChanged} files, ${linesChanged} lines`,
`- Signals: [${signalsSummary}]`,
rationale ? `- Why: ${rationale}` : null,
strategy ? `- Strategy:\n${strategy}` : null,
capsuleSummary ? `- Result: ${capsuleSummary}` : null,
'',
].filter(line => line !== null).join('\n');
let existing = '';
try {
if (fs.existsSync(narrativePath)) {
existing = fs.readFileSync(narrativePath, 'utf8');
}
} catch (_) {}
if (!existing.trim()) {
existing = '# Evolution Narrative\n\nA chronological record of evolution decisions and outcomes.\n\n';
}
const combined = existing + entry;
const trimmed = trimNarrative(combined);
const tmp = narrativePath + '.tmp';
fs.writeFileSync(tmp, trimmed, 'utf8');
fs.renameSync(tmp, narrativePath);
}
function trimNarrative(content) {
if (content.length <= MAX_NARRATIVE_SIZE) return content;
const headerEnd = content.indexOf('###');
if (headerEnd < 0) return content.slice(-MAX_NARRATIVE_SIZE);
const header = content.slice(0, headerEnd);
const entries = content.slice(headerEnd).split(/(?=^### \[)/m);
while (entries.length > MAX_NARRATIVE_ENTRIES) {
entries.shift();
}
let result = header + entries.join('');
if (result.length > MAX_NARRATIVE_SIZE) {
const keep = Math.max(1, entries.length - 5);
result = header + entries.slice(-keep).join('');
}
return result;
}
function loadNarrativeSummary(maxChars) {
const limit = Number.isFinite(maxChars) ? maxChars : 4000;
const narrativePath = getNarrativePath();
try {
if (!fs.existsSync(narrativePath)) return '';
const content = fs.readFileSync(narrativePath, 'utf8');
if (!content.trim()) return '';
const headerEnd = content.indexOf('###');
if (headerEnd < 0) return '';
const entries = content.slice(headerEnd).split(/(?=^### \[)/m);
const recent = entries.slice(-8);
let summary = recent.join('');
if (summary.length > limit) {
summary = summary.slice(-limit);
const firstEntry = summary.indexOf('### [');
if (firstEntry > 0) summary = summary.slice(firstEntry);
}
return summary.trim();
} catch (_) {
return '';
}
}
module.exports = { recordNarrative, loadNarrativeSummary, trimNarrative };
+20 -1
View File
@@ -28,7 +28,8 @@ function getSessionScope() {
if (!raw) return null;
// Sanitize: only allow alphanumeric, dash, underscore, dot (prevent path traversal).
const safe = raw.replace(/[^a-zA-Z0-9_\-\.]/g, '_').slice(0, 128);
return safe || null;
if (!safe || /^\.{1,2}$/.test(safe) || /\.\./.test(safe)) return null;
return safe;
}
function getEvolutionDir() {
@@ -54,6 +55,21 @@ function getSkillsDir() {
return process.env.SKILLS_DIR || path.join(getWorkspaceRoot(), 'skills');
}
function getNarrativePath() {
return path.join(getEvolutionDir(), 'evolution_narrative.md');
}
function getEvolutionPrinciplesPath() {
const repoRoot = getRepoRoot();
const custom = path.join(repoRoot, 'EVOLUTION_PRINCIPLES.md');
if (require('fs').existsSync(custom)) return custom;
return path.join(repoRoot, 'assets', 'gep', 'EVOLUTION_PRINCIPLES.md');
}
function getReflectionLogPath() {
return path.join(getEvolutionDir(), 'reflection_log.jsonl');
}
module.exports = {
getRepoRoot,
getWorkspaceRoot,
@@ -63,5 +79,8 @@ module.exports = {
getGepAssetsDir,
getSkillsDir,
getSessionScope,
getNarrativePath,
getEvolutionPrinciplesPath,
getReflectionLogPath,
};
+31 -4
View File
@@ -1,7 +1,10 @@
const fs = require('fs');
const { captureEnvFingerprint } = require('./envFingerprint');
const { formatAssetPreview } = require('./assets');
const { generateInnovationIdeas } = require('../ops/innovation'); // [2026-02-14] Innovation Catalyst Integration
const { analyzeRecentHistory, OPPORTUNITY_SIGNALS } = require('./signals'); // [2026-02-14] Signal Analysis Integration
const { generateInnovationIdeas } = require('../ops/innovation');
const { analyzeRecentHistory, OPPORTUNITY_SIGNALS } = require('./signals');
const { loadNarrativeSummary } = require('./narrativeMemory');
const { getEvolutionPrinciplesPath } = require('./paths');
/**
* Build a minimal prompt for direct-reuse mode.
@@ -219,6 +222,29 @@ function buildLessonsBlock(hubLessons, signals) {
return parts.join('\n');
}
function buildNarrativeBlock() {
try {
const narrative = loadNarrativeSummary(3000);
if (!narrative) return '';
return `\nContext [Evolution Narrative] (Recent decisions and outcomes -- learn from this history):\n${narrative}\n`;
} catch (_) {
return '';
}
}
function buildPrinciplesBlock() {
try {
const principlesPath = getEvolutionPrinciplesPath();
if (!fs.existsSync(principlesPath)) return '';
const content = fs.readFileSync(principlesPath, 'utf8');
if (!content.trim()) return '';
const trimmed = content.length > 2000 ? content.slice(0, 2000) + '\n...[TRUNCATED]' : content;
return `\nContext [Evolution Principles] (Guiding directives -- align your actions):\n${trimmed}\n`;
} catch (_) {
return '';
}
}
function buildGepPrompt({
nowIso,
context,
@@ -485,7 +511,8 @@ Context [External Candidates]:
${externalCandidatesPreview || '(none)'}
${buildAntiPatternZone(failedCapsules, signals)}${buildLessonsBlock(hubLessons, signals)}
${historyBlock}
${buildNarrativeBlock()}
${buildPrinciplesBlock()}
Context [Execution]:
${executionContext}
@@ -536,4 +563,4 @@ Rules:
return basePrompt.slice(0, maxChars) + "\n...[TRUNCATED]...";
}
module.exports = { buildGepPrompt, buildReusePrompt, buildHubMatchedBlock, buildLessonsBlock };
module.exports = { buildGepPrompt, buildReusePrompt, buildHubMatchedBlock, buildLessonsBlock, buildNarrativeBlock, buildPrinciplesBlock };
@@ -0,0 +1,127 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { getReflectionLogPath, getEvolutionDir } = require('./paths');
const REFLECTION_INTERVAL_CYCLES = 5;
const REFLECTION_COOLDOWN_MS = 30 * 60 * 1000;
function ensureDir(dir) {
try { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
}
function shouldReflect({ cycleCount, recentEvents }) {
if (!Number.isFinite(cycleCount) || cycleCount < REFLECTION_INTERVAL_CYCLES) return false;
if (cycleCount % REFLECTION_INTERVAL_CYCLES !== 0) return false;
const logPath = getReflectionLogPath();
try {
if (fs.existsSync(logPath)) {
const stat = fs.statSync(logPath);
if (Date.now() - stat.mtimeMs < REFLECTION_COOLDOWN_MS) return false;
}
} catch (_) {}
return true;
}
function buildReflectionContext({ recentEvents, signals, memoryAdvice, narrative }) {
const parts = ['You are performing a strategic reflection on recent evolution cycles.'];
parts.push('Analyze the patterns below and provide concise strategic guidance.');
parts.push('');
if (Array.isArray(recentEvents) && recentEvents.length > 0) {
const last10 = recentEvents.slice(-10);
const successCount = last10.filter(e => e && e.outcome && e.outcome.status === 'success').length;
const failCount = last10.filter(e => e && e.outcome && e.outcome.status === 'failed').length;
const intents = {};
last10.forEach(e => {
const i = e && e.intent ? e.intent : 'unknown';
intents[i] = (intents[i] || 0) + 1;
});
const genes = {};
last10.forEach(e => {
const g = e && Array.isArray(e.genes_used) && e.genes_used[0] ? e.genes_used[0] : 'unknown';
genes[g] = (genes[g] || 0) + 1;
});
parts.push('## Recent Cycle Statistics (last 10)');
parts.push(`- Success: ${successCount}, Failed: ${failCount}`);
parts.push(`- Intent distribution: ${JSON.stringify(intents)}`);
parts.push(`- Gene usage: ${JSON.stringify(genes)}`);
parts.push('');
}
if (Array.isArray(signals) && signals.length > 0) {
parts.push('## Current Signals');
parts.push(signals.slice(0, 20).join(', '));
parts.push('');
}
if (memoryAdvice) {
parts.push('## Memory Graph Advice');
if (memoryAdvice.preferredGeneId) {
parts.push(`- Preferred gene: ${memoryAdvice.preferredGeneId}`);
}
if (Array.isArray(memoryAdvice.bannedGeneIds) && memoryAdvice.bannedGeneIds.length > 0) {
parts.push(`- Banned genes: ${memoryAdvice.bannedGeneIds.join(', ')}`);
}
if (memoryAdvice.explanation) {
parts.push(`- Explanation: ${memoryAdvice.explanation}`);
}
parts.push('');
}
if (narrative) {
parts.push('## Recent Evolution Narrative');
parts.push(String(narrative).slice(0, 3000));
parts.push('');
}
parts.push('## Questions to Answer');
parts.push('1. Are there persistent signals being ignored?');
parts.push('2. Is the gene selection strategy optimal, or are we stuck in a local maximum?');
parts.push('3. Should the balance between repair/optimize/innovate shift?');
parts.push('4. Are there capability gaps that no current gene addresses?');
parts.push('5. What single strategic adjustment would have the highest impact?');
parts.push('');
parts.push('Respond with a JSON object: { "insights": [...], "strategy_adjustment": "...", "priority_signals": [...] }');
return parts.join('\n');
}
function recordReflection(reflection) {
const logPath = getReflectionLogPath();
ensureDir(path.dirname(logPath));
const entry = JSON.stringify({
ts: new Date().toISOString(),
type: 'reflection',
...reflection,
}) + '\n';
fs.appendFileSync(logPath, entry, 'utf8');
}
function loadRecentReflections(count) {
const n = Number.isFinite(count) ? count : 3;
const logPath = getReflectionLogPath();
try {
if (!fs.existsSync(logPath)) return [];
const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n').filter(Boolean);
return lines.slice(-n).map(line => {
try { return JSON.parse(line); } catch (_) { return null; }
}).filter(Boolean);
} catch (_) {
return [];
}
}
module.exports = {
shouldReflect,
buildReflectionContext,
recordReflection,
loadRecentReflections,
REFLECTION_INTERVAL_CYCLES,
};
+10 -2
View File
@@ -3,6 +3,7 @@ function matchPatternToSignals(pattern, signals) {
const p = String(pattern);
const sig = signals.map(s => String(s));
// Regex pattern: /body/flags
const regexLike = p.length >= 2 && p.startsWith('/') && p.lastIndexOf('/') > 0;
if (regexLike) {
const lastSlash = p.lastIndexOf('/');
@@ -16,6 +17,12 @@ function matchPatternToSignals(pattern, signals) {
}
}
// Multi-language alias: "en_term|zh_term|ja_term" -- any branch matching = hit
if (p.includes('|') && !p.startsWith('/')) {
const branches = p.split('|').map(b => b.trim().toLowerCase()).filter(Boolean);
return branches.some(needle => sig.some(s => s.toLowerCase().includes(needle)));
}
const needle = p.toLowerCase();
return sig.some(s => s.toLowerCase().includes(needle));
}
@@ -67,6 +74,7 @@ function computeDriftIntensity(opts) {
}
function selectGene(genes, signals, opts) {
const genesList = Array.isArray(genes) ? genes : [];
const bannedGeneIds = opts && opts.bannedGeneIds ? opts.bannedGeneIds : new Set();
const driftEnabled = !!(opts && opts.driftEnabled);
const preferredGeneId = opts && typeof opts.preferredGeneId === 'string' ? opts.preferredGeneId : null;
@@ -75,14 +83,14 @@ function selectGene(genes, signals, opts) {
var driftIntensity = computeDriftIntensity({
driftEnabled: driftEnabled,
effectivePopulationSize: opts && opts.effectivePopulationSize,
genePoolSize: genes ? genes.length : 0,
genePoolSize: genesList.length,
});
var useDrift = driftEnabled || driftIntensity > 0.15;
var DISTILLED_PREFIX = 'gene_distilled_';
var DISTILLED_SCORE_FACTOR = 0.8;
const scored = genes
const scored = genesList
.map(g => {
var s = scoreGene(g, signals);
if (s > 0 && g.id && String(g.id).startsWith(DISTILLED_PREFIX)) s *= DISTILLED_SCORE_FACTOR;
+2 -2
View File
@@ -282,14 +282,14 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user
// --- Tool Usage Analytics ---
var toolUsage = {};
var toolMatches = corpus.match(/\[TOOL:\s*(\w+)\]/g) || [];
var toolMatches = corpus.match(/\[TOOL:\s*([\w-]+)\]/g) || [];
// Extract exec commands to identify benign loops (like watchdog checks)
var execCommands = corpus.match(/exec: (node\s+[\w\/\.-]+\.js\s+ensure)/g) || [];
var benignExecCount = execCommands.length;
for (var i = 0; i < toolMatches.length; i++) {
var toolName = toolMatches[i].match(/\[TOOL:\s*(\w+)\]/)[1];
var toolName = toolMatches[i].match(/\[TOOL:\s*([\w-]+)\]/)[1];
toolUsage[toolName] = (toolUsage[toolName] || 0) + 1;
}
@@ -223,7 +223,7 @@ function buildDistillationPrompt(analysis, existingGenes, sampleCapsules) {
});
return [
'You are a Gene synthesis engine for the GEP (Gene Expression Protocol).',
'You are a Gene synthesis engine for the GEP (Genome Evolution Protocol).',
'',
'Analyze the following successful evolution capsules and extract a reusable Gene.',
'',
+100 -1
View File
@@ -17,6 +17,9 @@ const {
const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');
const { captureEnvFingerprint } = require('./envFingerprint');
const { buildValidationReport } = require('./validationReport');
const { logAssetCall } = require('./assetCallLog');
const { recordNarrative } = require('./narrativeMemory');
const { isLlmReviewEnabled, runLlmReview } = require('./llmReview');
function nowIso() {
return new Date().toISOString();
@@ -701,10 +704,14 @@ function rollbackNewUntrackedFiles({ repoRoot, baselineUntracked }) {
// This prevents "ghost skill directories" where mkdir succeeded but
// file creation failed/was rolled back. Without this, empty dirs like
// skills/anima/, skills/oblivion/ etc. accumulate after failed innovations.
// SAFETY: never remove top-level structural directories (skills/, src/, etc.)
// or critical protected directories. Only remove leaf subdirectories.
var dirsToCheck = new Set();
for (var di = 0; di < deleted.length; di++) {
var dir = path.dirname(deleted[di]);
while (dir && dir !== '.' && dir !== '/') {
var normalized = dir.replace(/\\/g, '/');
if (!normalized.includes('/')) break;
dirsToCheck.add(dir);
dir = path.dirname(dir);
}
@@ -713,6 +720,7 @@ function rollbackNewUntrackedFiles({ repoRoot, baselineUntracked }) {
var sortedDirs = Array.from(dirsToCheck).sort(function (a, b) { return b.length - a.length; });
var removedDirs = [];
for (var si = 0; si < sortedDirs.length; si++) {
if (isCriticalProtectedPath(sortedDirs[si] + '/')) continue;
var dirAbs = path.join(repoRoot, sortedDirs[si]);
try {
var entries = fs.readdirSync(dirAbs);
@@ -957,8 +965,32 @@ function readRecentSessionInputs() {
return { recentSessionTranscript, todayLog: todayLogContent, memorySnippet, userSnippet };
}
function isGitRepo(dir) {
try {
execSync('git rev-parse --git-dir', {
cwd: dir, encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000,
});
return true;
} catch (_) {
return false;
}
}
function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true } = {}) {
const repoRoot = getRepoRoot();
if (!isGitRepo(repoRoot)) {
console.error('[Solidify] FATAL: Not a git repository (' + repoRoot + ').');
console.error('[Solidify] Solidify requires git for rollback, diff capture, and blast radius.');
console.error('[Solidify] Run "git init && git add -A && git commit -m init" first.');
return {
ok: false,
status: 'failed',
failure_reason: 'not_a_git_repository',
event: null,
};
}
const state = readStateForSolidify();
const lastRun = state && state.last_run ? state.last_run : null;
const genes = loadGenes();
@@ -1053,6 +1085,29 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
console.error(`[Solidify] CANARY FAILED: ${canary.err}`);
}
// Optional LLM review: when EVOLVER_LLM_REVIEW=true, submit diff for review.
let llmReviewResult = null;
if (constraintCheck.ok && validation.ok && protocolViolations.length === 0 && isLlmReviewEnabled()) {
try {
const reviewDiff = captureDiffSnapshot(repoRoot);
llmReviewResult = runLlmReview({
diff: reviewDiff,
gene: geneUsed,
signals,
mutation,
});
if (llmReviewResult && llmReviewResult.approved === false) {
constraintCheck.violations.push('llm_review_rejected: ' + (llmReviewResult.summary || 'no reason'));
constraintCheck.ok = false;
console.log('[LLMReview] Change REJECTED: ' + (llmReviewResult.summary || ''));
} else if (llmReviewResult) {
console.log('[LLMReview] Change approved (confidence: ' + (llmReviewResult.confidence || '?') + ')');
}
} catch (e) {
console.log('[LLMReview] Failed (non-fatal): ' + (e && e.message ? e.message : e));
}
}
// Build standardized ValidationReport (machine-readable, interoperable).
const validationReport = buildValidationReport({
geneId: geneUsed && geneUsed.id ? geneUsed.id : null,
@@ -1212,7 +1267,12 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
if (!dryRun && !success && rollbackOnFailure) {
rollbackTracked(repoRoot);
rollbackNewUntrackedFiles({ repoRoot, baselineUntracked: lastRun && lastRun.baseline_untracked ? lastRun.baseline_untracked : [] });
// Only clean up new untracked files when a valid baseline exists.
// Without a baseline, we cannot distinguish pre-existing untracked files
// from AI-generated ones, so deleting would be destructive.
if (lastRun && Array.isArray(lastRun.baseline_untracked)) {
rollbackNewUntrackedFiles({ repoRoot, baselineUntracked: lastRun.baseline_untracked });
}
}
// Apply epigenetic marks to the gene based on outcome and environment
@@ -1254,6 +1314,21 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
};
if (!dryRun) writeStateForSolidify(state);
if (!dryRun) {
try {
recordNarrative({
gene: geneUsed,
signals,
mutation,
outcome: event.outcome,
blast,
capsule,
});
} catch (e) {
console.log('[Narrative] Record failed (non-fatal): ' + (e && e.message ? e.message : e));
}
}
// Search-First Evolution: auto-publish eligible capsules to the Hub (as Gene+Capsule bundle).
let publishResult = null;
if (!dryRun && capsule && capsule.a2a && capsule.a2a.eligible_to_broadcast) {
@@ -1327,6 +1402,21 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
});
}
publishResult = { attempted: true, asset_id: capsule.asset_id || capsule.id, bundle: true };
logAssetCall({
run_id: lastRun && lastRun.run_id ? lastRun.run_id : null,
action: 'asset_publish',
asset_id: capsule.asset_id || capsule.id,
asset_type: 'Capsule',
source_node_id: null,
chain_id: publishChainId || null,
signals: Array.isArray(capsule.trigger) ? capsule.trigger : [],
extra: {
source_type: sourceType,
reused_asset_id: reusedAssetId,
gene_id: publishGene && publishGene.id ? publishGene.id : null,
parent: parentRef || null,
},
});
} else {
publishResult = { attempted: false, reason: 'no_hub_url' };
}
@@ -1340,6 +1430,14 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
: sourceType === 'reused' ? 'skip_direct_reused_asset'
: 'below_min_score';
publishResult = { attempted: false, reason };
logAssetCall({
run_id: lastRun && lastRun.run_id ? lastRun.run_id : null,
action: 'asset_publish_skip',
asset_id: capsule.asset_id || capsule.id,
asset_type: 'Capsule',
reason,
signals: Array.isArray(capsule.trigger) ? capsule.trigger : [],
});
}
}
@@ -1452,6 +1550,7 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
module.exports = {
solidify,
isGitRepo,
readStateForSolidify,
writeStateForSolidify,
isValidationCommandAllowed,
@@ -82,7 +82,8 @@ async function fetchTasks(opts) {
}
return result;
} catch {
} catch (err) {
console.warn("[TaskReceiver] fetchTasks failed:", err && err.message ? err.message : err);
return { tasks: [] };
}
}
@@ -150,7 +151,7 @@ function estimateCapabilityMatch(task, memoryEvents) {
var weightSum = 0;
for (var sk in totalBySignalKey) {
// Reconstruct signals from the key for comparison
var skParts = sk.split(':').map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean);
var skParts = sk.split('|').map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean);
var sim = jaccard(taskSignals, skParts);
if (sim < 0.15) continue;
+14 -43
View File
@@ -9,6 +9,14 @@ const { getEvolutionDir } = require('../gep/paths');
var MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
var MIN_KEEP = 10;
function safeBatchDelete(batch) {
var deleted = 0;
for (var i = 0; i < batch.length; i++) {
try { fs.unlinkSync(batch[i]); deleted++; } catch (_) {}
}
return deleted;
}
function run() {
var evoDir = getEvolutionDir();
if (!fs.existsSync(evoDir)) return;
@@ -26,36 +34,18 @@ function run() {
var deleted = 0;
// Phase 1: Age-based cleanup (keep at least MIN_KEEP)
// [OPTIMIZATION] Batch deletion for age-based cleanup as well
const filesToDelete = [];
var 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 (_) {}
});
}
}
deleted += safeBatchDelete(filesToDelete);
}
// Phase 2: Size-based safety cap (keep max 10 files total to drastically reduce bloat)
// Re-scan remaining files after age cleanup
// Phase 2: Size-based safety cap (keep max 10 files total)
try {
var remainingFiles = fs.readdirSync(evoDir)
.filter(function(f) { return /^gep_prompt_.*\.(json|txt)$/.test(f); })
@@ -66,29 +56,10 @@ function run() {
})
.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 (_) {}
});
}
}
var toDelete = remainingFiles.slice(MAX_FILES).map(function(f) { return f.path; });
deleted += safeBatchDelete(toDelete);
}
} catch (e) {
console.warn('[Cleanup] Phase 2 failed:', e.message);