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
+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;