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:
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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.',
|
||||
'',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user