@evomap/evolver 1.29.8 → 1.30.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +19 -15
- package/package.json +1 -1
- package/src/evolve.js +159 -47
- package/src/gep/a2aProtocol.js +33 -0
- package/src/gep/candidates.js +5 -1
- package/src/gep/executionTrace.js +201 -0
- package/src/gep/hubSearch.js +152 -72
- package/src/gep/selector.js +55 -8
- package/src/gep/skillDistiller.js +128 -22
- package/src/gep/skillPublisher.js +142 -34
- package/src/gep/solidify.js +21 -1
package/index.js
CHANGED
|
@@ -25,24 +25,13 @@ function readJsonSafe(p) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function rejectPendingRun(statePath) {
|
|
28
|
-
try {
|
|
29
|
-
const { getRepoRoot } = require('./src/gep/paths');
|
|
30
|
-
const { execSync } = require('child_process');
|
|
31
|
-
const repoRoot = getRepoRoot();
|
|
32
|
-
|
|
33
|
-
execSync('git checkout -- .', { cwd: repoRoot, encoding: 'utf8', timeout: 30000 });
|
|
34
|
-
execSync('git clean -fd', { cwd: repoRoot, encoding: 'utf8', timeout: 30000 });
|
|
35
|
-
} catch (e) {
|
|
36
|
-
console.warn('[Loop] Pending run rollback failed: ' + (e.message || e));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
28
|
try {
|
|
40
29
|
const state = readJsonSafe(statePath);
|
|
41
30
|
if (state && state.last_run && state.last_run.run_id) {
|
|
42
31
|
state.last_solidify = {
|
|
43
32
|
run_id: state.last_run.run_id,
|
|
44
33
|
rejected: true,
|
|
45
|
-
reason: '
|
|
34
|
+
reason: 'loop_bridge_disabled_autoreject_no_rollback',
|
|
46
35
|
timestamp: new Date().toISOString(),
|
|
47
36
|
};
|
|
48
37
|
fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
@@ -184,7 +173,7 @@ async function main() {
|
|
|
184
173
|
if (isPendingSolidify(stAfterRun)) {
|
|
185
174
|
const cleared = rejectPendingRun(solidifyStatePath);
|
|
186
175
|
if (cleared) {
|
|
187
|
-
console.warn('[Loop] Auto-rejected pending run because bridge is disabled in loop mode.');
|
|
176
|
+
console.warn('[Loop] Auto-rejected pending run because bridge is disabled in loop mode (state only, no rollback).');
|
|
188
177
|
}
|
|
189
178
|
}
|
|
190
179
|
}
|
|
@@ -285,11 +274,19 @@ async function main() {
|
|
|
285
274
|
if (res && res.ok && !dryRun) {
|
|
286
275
|
try {
|
|
287
276
|
const { shouldDistill, prepareDistillation } = require('./src/gep/skillDistiller');
|
|
288
|
-
|
|
277
|
+
const { readStateForSolidify } = require('./src/gep/solidify');
|
|
278
|
+
const solidifyState = readStateForSolidify();
|
|
279
|
+
const count = solidifyState.solidify_count || 0;
|
|
280
|
+
const autoDistillInterval = 5;
|
|
281
|
+
const autoTrigger = count > 0 && count % autoDistillInterval === 0;
|
|
282
|
+
|
|
283
|
+
if (autoTrigger || shouldDistill()) {
|
|
289
284
|
const dr = prepareDistillation();
|
|
290
285
|
if (dr && dr.ok && dr.promptPath) {
|
|
286
|
+
const trigger = autoTrigger ? `auto (every ${autoDistillInterval} solidifies, count=${count})` : 'threshold';
|
|
291
287
|
console.log('\n[DISTILL_REQUEST]');
|
|
292
|
-
console.log(
|
|
288
|
+
console.log(`Distillation triggered: ${trigger}`);
|
|
289
|
+
console.log('Read the prompt file, process it with your LLM,');
|
|
293
290
|
console.log('save the LLM response to a file, then run:');
|
|
294
291
|
console.log(' node index.js distill --response-file=<path_to_llm_response>');
|
|
295
292
|
console.log('Prompt file: ' + dr.promptPath);
|
|
@@ -528,3 +525,10 @@ async function main() {
|
|
|
528
525
|
if (require.main === module) {
|
|
529
526
|
main();
|
|
530
527
|
}
|
|
528
|
+
|
|
529
|
+
module.exports = {
|
|
530
|
+
main,
|
|
531
|
+
readJsonSafe,
|
|
532
|
+
rejectPendingRun,
|
|
533
|
+
isPendingSolidify,
|
|
534
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.2",
|
|
4
4
|
"description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/src/evolve.js
CHANGED
|
@@ -59,6 +59,7 @@ const IS_RANDOM_DRIFT = ARGS.includes('--drift') || String(process.env.RANDOM_DR
|
|
|
59
59
|
const MEMORY_DIR = getMemoryDir();
|
|
60
60
|
const AGENT_NAME = process.env.AGENT_NAME || 'main';
|
|
61
61
|
const AGENT_SESSIONS_DIR = path.join(os.homedir(), `.openclaw/agents/${AGENT_NAME}/sessions`);
|
|
62
|
+
const CURSOR_TRANSCRIPTS_DIR = process.env.EVOLVER_CURSOR_TRANSCRIPTS_DIR || '';
|
|
62
63
|
const TODAY_LOG = path.join(MEMORY_DIR, new Date().toISOString().split('T')[0] + '.md');
|
|
63
64
|
|
|
64
65
|
// Ensure memory directory exists so state/cache writes work.
|
|
@@ -160,77 +161,177 @@ function formatSessionLog(jsonlContent) {
|
|
|
160
161
|
return result.join('\n');
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
function
|
|
164
|
+
function formatCursorTranscript(raw) {
|
|
165
|
+
const lines = raw.split('\n');
|
|
166
|
+
const result = [];
|
|
167
|
+
let skipUntilNextBlock = false;
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
170
|
+
const line = lines[i];
|
|
171
|
+
const trimmed = line.trim();
|
|
172
|
+
|
|
173
|
+
// Keep user messages and assistant text responses
|
|
174
|
+
if (trimmed === 'user:' || trimmed.startsWith('A:')) {
|
|
175
|
+
skipUntilNextBlock = false;
|
|
176
|
+
result.push(trimmed);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Tool call lines: keep as compact markers, skip their parameter block
|
|
181
|
+
if (trimmed.startsWith('[Tool call]')) {
|
|
182
|
+
skipUntilNextBlock = true;
|
|
183
|
+
result.push(`[Tool call] ${trimmed.replace('[Tool call]', '').trim()}`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Tool result markers: skip their content (usually large and noisy)
|
|
188
|
+
if (trimmed.startsWith('[Tool result]')) {
|
|
189
|
+
skipUntilNextBlock = true;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (skipUntilNextBlock) continue;
|
|
194
|
+
|
|
195
|
+
// Keep user query content and assistant text (skip XML tags like <user_query>)
|
|
196
|
+
if (trimmed.startsWith('<') && trimmed.endsWith('>')) continue;
|
|
197
|
+
if (trimmed) {
|
|
198
|
+
result.push(trimmed.slice(0, 300));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result.join('\n');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function readCursorTranscripts() {
|
|
206
|
+
if (!CURSOR_TRANSCRIPTS_DIR) return '';
|
|
164
207
|
try {
|
|
165
|
-
if (!fs.existsSync(
|
|
208
|
+
if (!fs.existsSync(CURSOR_TRANSCRIPTS_DIR)) return '';
|
|
166
209
|
|
|
167
210
|
const now = Date.now();
|
|
168
|
-
const ACTIVE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
211
|
+
const ACTIVE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
169
212
|
const TARGET_BYTES = 120000;
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
// Session scope isolation: when EVOLVER_SESSION_SCOPE is set,
|
|
173
|
-
// only read sessions whose filenames contain the scope identifier.
|
|
174
|
-
// This prevents cross-channel/cross-project memory contamination.
|
|
175
|
-
const sessionScope = getSessionScope();
|
|
213
|
+
const PER_FILE_BYTES = 20000;
|
|
214
|
+
const RECENCY_GUARD_MS = 30 * 1000;
|
|
176
215
|
|
|
177
|
-
// Find ALL active sessions (modified in last 24h), sorted newest first
|
|
178
216
|
let files = fs
|
|
179
|
-
.readdirSync(
|
|
180
|
-
.filter(f => f.endsWith('.
|
|
217
|
+
.readdirSync(CURSOR_TRANSCRIPTS_DIR)
|
|
218
|
+
.filter(f => f.endsWith('.txt') || f.endsWith('.jsonl'))
|
|
181
219
|
.map(f => {
|
|
182
220
|
try {
|
|
183
|
-
const st = fs.statSync(path.join(
|
|
221
|
+
const st = fs.statSync(path.join(CURSOR_TRANSCRIPTS_DIR, f));
|
|
184
222
|
return { name: f, time: st.mtime.getTime(), size: st.size };
|
|
185
223
|
} catch (e) {
|
|
186
224
|
return null;
|
|
187
225
|
}
|
|
188
226
|
})
|
|
189
227
|
.filter(f => f && (now - f.time) < ACTIVE_WINDOW_MS)
|
|
190
|
-
.sort((a, b) => b.time - a.time);
|
|
191
|
-
|
|
192
|
-
if (files.length === 0) return '
|
|
193
|
-
|
|
194
|
-
// Skip evolver's own sessions to avoid self-reference loops
|
|
195
|
-
let nonEvolverFiles = files.filter(f => !f.name.startsWith('evolver_hand_'));
|
|
196
|
-
|
|
197
|
-
// Session scope filter: when scope is active, only include sessions
|
|
198
|
-
// whose filename contains the scope string (e.g., channel_123456.jsonl).
|
|
199
|
-
// If no sessions match the scope, fall back to all non-evolver sessions
|
|
200
|
-
// (graceful degradation -- better to evolve with global context than not at all).
|
|
201
|
-
if (sessionScope && nonEvolverFiles.length > 0) {
|
|
202
|
-
const scopeLower = sessionScope.toLowerCase();
|
|
203
|
-
const scopedFiles = nonEvolverFiles.filter(f => f.name.toLowerCase().includes(scopeLower));
|
|
204
|
-
if (scopedFiles.length > 0) {
|
|
205
|
-
nonEvolverFiles = scopedFiles;
|
|
206
|
-
console.log(`[SessionScope] Filtered to ${scopedFiles.length} session(s) matching scope "${sessionScope}".`);
|
|
207
|
-
} else {
|
|
208
|
-
console.log(`[SessionScope] No sessions match scope "${sessionScope}". Using all ${nonEvolverFiles.length} session(s) (fallback).`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
228
|
+
.sort((a, b) => b.time - a.time);
|
|
229
|
+
|
|
230
|
+
if (files.length === 0) return '';
|
|
211
231
|
|
|
212
|
-
|
|
232
|
+
// Skip the most recently modified file if it was touched in the last 30s --
|
|
233
|
+
// it is likely the current active session that triggered this evolver run,
|
|
234
|
+
// reading it would cause self-referencing signal noise.
|
|
235
|
+
if (files.length > 1 && (now - files[0].time) < RECENCY_GUARD_MS) {
|
|
236
|
+
files = files.slice(1);
|
|
237
|
+
}
|
|
213
238
|
|
|
214
|
-
|
|
215
|
-
const maxSessions = Math.min(activeFiles.length, 6);
|
|
239
|
+
const maxFiles = Math.min(files.length, 6);
|
|
216
240
|
const sections = [];
|
|
217
241
|
let totalBytes = 0;
|
|
218
242
|
|
|
219
|
-
for (let i = 0; i <
|
|
220
|
-
const f =
|
|
243
|
+
for (let i = 0; i < maxFiles && totalBytes < TARGET_BYTES; i++) {
|
|
244
|
+
const f = files[i];
|
|
221
245
|
const bytesLeft = TARGET_BYTES - totalBytes;
|
|
222
|
-
const readSize = Math.min(
|
|
223
|
-
const raw = readRecentLog(path.join(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
246
|
+
const readSize = Math.min(PER_FILE_BYTES, bytesLeft);
|
|
247
|
+
const raw = readRecentLog(path.join(CURSOR_TRANSCRIPTS_DIR, f.name), readSize);
|
|
248
|
+
if (raw.trim() && !raw.startsWith('[MISSING]')) {
|
|
249
|
+
const formatted = formatCursorTranscript(raw);
|
|
250
|
+
if (formatted.trim()) {
|
|
251
|
+
sections.push(`--- CURSOR SESSION (${f.name}) ---\n${formatted}`);
|
|
252
|
+
totalBytes += formatted.length;
|
|
253
|
+
}
|
|
228
254
|
}
|
|
229
255
|
}
|
|
230
256
|
|
|
231
|
-
|
|
257
|
+
return sections.join('\n\n');
|
|
258
|
+
} catch (e) {
|
|
259
|
+
console.warn(`[CursorTranscripts] Read failed: ${e.message}`);
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function readRealSessionLog() {
|
|
265
|
+
try {
|
|
266
|
+
// Primary source: OpenClaw session logs (.jsonl)
|
|
267
|
+
if (fs.existsSync(AGENT_SESSIONS_DIR)) {
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
const ACTIVE_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
270
|
+
const TARGET_BYTES = 120000;
|
|
271
|
+
const PER_SESSION_BYTES = 20000;
|
|
272
|
+
|
|
273
|
+
const sessionScope = getSessionScope();
|
|
274
|
+
|
|
275
|
+
let files = fs
|
|
276
|
+
.readdirSync(AGENT_SESSIONS_DIR)
|
|
277
|
+
.filter(f => f.endsWith('.jsonl') && !f.includes('.lock'))
|
|
278
|
+
.map(f => {
|
|
279
|
+
try {
|
|
280
|
+
const st = fs.statSync(path.join(AGENT_SESSIONS_DIR, f));
|
|
281
|
+
return { name: f, time: st.mtime.getTime(), size: st.size };
|
|
282
|
+
} catch (e) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
.filter(f => f && (now - f.time) < ACTIVE_WINDOW_MS)
|
|
287
|
+
.sort((a, b) => b.time - a.time);
|
|
288
|
+
|
|
289
|
+
if (files.length > 0) {
|
|
290
|
+
let nonEvolverFiles = files.filter(f => !f.name.startsWith('evolver_hand_'));
|
|
291
|
+
|
|
292
|
+
if (sessionScope && nonEvolverFiles.length > 0) {
|
|
293
|
+
const scopeLower = sessionScope.toLowerCase();
|
|
294
|
+
const scopedFiles = nonEvolverFiles.filter(f => f.name.toLowerCase().includes(scopeLower));
|
|
295
|
+
if (scopedFiles.length > 0) {
|
|
296
|
+
nonEvolverFiles = scopedFiles;
|
|
297
|
+
console.log(`[SessionScope] Filtered to ${scopedFiles.length} session(s) matching scope "${sessionScope}".`);
|
|
298
|
+
} else {
|
|
299
|
+
console.log(`[SessionScope] No sessions match scope "${sessionScope}". Using all ${nonEvolverFiles.length} session(s) (fallback).`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
232
302
|
|
|
233
|
-
|
|
303
|
+
const activeFiles = nonEvolverFiles.length > 0 ? nonEvolverFiles : files.slice(0, 1);
|
|
304
|
+
|
|
305
|
+
const maxSessions = Math.min(activeFiles.length, 6);
|
|
306
|
+
const sections = [];
|
|
307
|
+
let totalBytes = 0;
|
|
308
|
+
|
|
309
|
+
for (let i = 0; i < maxSessions && totalBytes < TARGET_BYTES; i++) {
|
|
310
|
+
const f = activeFiles[i];
|
|
311
|
+
const bytesLeft = TARGET_BYTES - totalBytes;
|
|
312
|
+
const readSize = Math.min(PER_SESSION_BYTES, bytesLeft);
|
|
313
|
+
const raw = readRecentLog(path.join(AGENT_SESSIONS_DIR, f.name), readSize);
|
|
314
|
+
const formatted = formatSessionLog(raw);
|
|
315
|
+
if (formatted.trim()) {
|
|
316
|
+
sections.push(`--- SESSION (${f.name}) ---\n${formatted}`);
|
|
317
|
+
totalBytes += formatted.length;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (sections.length > 0) {
|
|
322
|
+
return sections.join('\n\n');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Fallback: Cursor agent-transcripts (.txt)
|
|
328
|
+
const cursorContent = readCursorTranscripts();
|
|
329
|
+
if (cursorContent) {
|
|
330
|
+
console.log('[SessionFallback] Using Cursor agent-transcripts as session source.');
|
|
331
|
+
return cursorContent;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return '[NO SESSION LOGS FOUND]';
|
|
234
335
|
} catch (e) {
|
|
235
336
|
return `[ERROR READING SESSION LOGS: ${e.message}]`;
|
|
236
337
|
}
|
|
@@ -1307,6 +1408,15 @@ async function run() {
|
|
|
1307
1408
|
console.log('[FailedCapsules] Read failed (non-fatal): ' + e.message);
|
|
1308
1409
|
}
|
|
1309
1410
|
|
|
1411
|
+
// Heartbeat hints: novelty score and capability gaps for diversity-directed drift
|
|
1412
|
+
var heartbeatNovelty = null;
|
|
1413
|
+
var heartbeatCapGaps = [];
|
|
1414
|
+
try {
|
|
1415
|
+
var { getNoveltyHint, getCapabilityGaps: getCapGaps } = require('./gep/a2aProtocol');
|
|
1416
|
+
heartbeatNovelty = getNoveltyHint();
|
|
1417
|
+
heartbeatCapGaps = getCapGaps() || [];
|
|
1418
|
+
} catch (e) {}
|
|
1419
|
+
|
|
1310
1420
|
const { selectedGene, capsuleCandidates, selector } = selectGeneAndCapsule({
|
|
1311
1421
|
genes,
|
|
1312
1422
|
capsules,
|
|
@@ -1314,6 +1424,8 @@ async function run() {
|
|
|
1314
1424
|
memoryAdvice,
|
|
1315
1425
|
driftEnabled: IS_RANDOM_DRIFT,
|
|
1316
1426
|
failedCapsules: recentFailedCapsules,
|
|
1427
|
+
capabilityGaps: heartbeatCapGaps,
|
|
1428
|
+
noveltyScore: heartbeatNovelty && Number.isFinite(heartbeatNovelty.score) ? heartbeatNovelty.score : null,
|
|
1317
1429
|
});
|
|
1318
1430
|
|
|
1319
1431
|
const selectedBy = memoryAdvice && memoryAdvice.preferredGeneId ? 'memory_graph+selector' : 'selector';
|
package/src/gep/a2aProtocol.js
CHANGED
|
@@ -402,6 +402,9 @@ var _heartbeatTotalFailed = 0;
|
|
|
402
402
|
var _heartbeatFpSent = false;
|
|
403
403
|
var _latestAvailableWork = [];
|
|
404
404
|
var _latestOverdueTasks = [];
|
|
405
|
+
var _latestSkillStoreHint = null;
|
|
406
|
+
var _latestNoveltyHint = null;
|
|
407
|
+
var _latestCapabilityGaps = [];
|
|
405
408
|
var _pendingCommitmentUpdates = [];
|
|
406
409
|
var _cachedHubNodeSecret = null;
|
|
407
410
|
var _heartbeatIntervalMs = 0;
|
|
@@ -576,6 +579,21 @@ function sendHeartbeat() {
|
|
|
576
579
|
_latestOverdueTasks = data.overdue_tasks;
|
|
577
580
|
console.warn('[Commitment] ' + data.overdue_tasks.length + ' overdue task(s) detected via heartbeat.');
|
|
578
581
|
}
|
|
582
|
+
if (data.skill_store) {
|
|
583
|
+
_latestSkillStoreHint = data.skill_store;
|
|
584
|
+
if (data.skill_store.eligible && data.skill_store.published_skills === 0) {
|
|
585
|
+
console.log('[Skill Store] ' + data.skill_store.hint);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (data.novelty && typeof data.novelty === 'object') {
|
|
589
|
+
_latestNoveltyHint = data.novelty;
|
|
590
|
+
}
|
|
591
|
+
if (Array.isArray(data.capability_gaps) && data.capability_gaps.length > 0) {
|
|
592
|
+
_latestCapabilityGaps = data.capability_gaps;
|
|
593
|
+
}
|
|
594
|
+
if (data.circle_experience && typeof data.circle_experience === 'object') {
|
|
595
|
+
console.log('[EvolutionCircle] Active circle: ' + (data.circle_experience.circle_id || '?') + ' (' + (data.circle_experience.member_count || 0) + ' members)');
|
|
596
|
+
}
|
|
579
597
|
_heartbeatConsecutiveFailures = 0;
|
|
580
598
|
try {
|
|
581
599
|
var logPath = getEvolverLogPath();
|
|
@@ -629,12 +647,24 @@ function getOverdueTasks() {
|
|
|
629
647
|
return _latestOverdueTasks;
|
|
630
648
|
}
|
|
631
649
|
|
|
650
|
+
function getSkillStoreHint() {
|
|
651
|
+
return _latestSkillStoreHint;
|
|
652
|
+
}
|
|
653
|
+
|
|
632
654
|
function consumeOverdueTasks() {
|
|
633
655
|
var tasks = _latestOverdueTasks;
|
|
634
656
|
_latestOverdueTasks = [];
|
|
635
657
|
return tasks;
|
|
636
658
|
}
|
|
637
659
|
|
|
660
|
+
function getNoveltyHint() {
|
|
661
|
+
return _latestNoveltyHint;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function getCapabilityGaps() {
|
|
665
|
+
return _latestCapabilityGaps;
|
|
666
|
+
}
|
|
667
|
+
|
|
638
668
|
/**
|
|
639
669
|
* Queue a commitment deadline update to be sent with the next heartbeat.
|
|
640
670
|
* @param {string} taskId
|
|
@@ -746,7 +776,10 @@ module.exports = {
|
|
|
746
776
|
consumeAvailableWork,
|
|
747
777
|
getOverdueTasks,
|
|
748
778
|
consumeOverdueTasks,
|
|
779
|
+
getSkillStoreHint,
|
|
749
780
|
queueCommitmentUpdate,
|
|
750
781
|
getHubNodeSecret,
|
|
751
782
|
buildHubHeaders,
|
|
783
|
+
getNoveltyHint,
|
|
784
|
+
getCapabilityGaps,
|
|
752
785
|
};
|
package/src/gep/candidates.js
CHANGED
|
@@ -26,8 +26,12 @@ function extractToolCalls(transcript) {
|
|
|
26
26
|
const lines = toLines(transcript);
|
|
27
27
|
const calls = [];
|
|
28
28
|
for (const line of lines) {
|
|
29
|
+
// OpenClaw format: [TOOL: Shell]
|
|
29
30
|
const m = line.match(/\[TOOL:\s*([^\]]+)\]/i);
|
|
30
|
-
if (m && m[1]) calls.push(m[1].trim());
|
|
31
|
+
if (m && m[1]) { calls.push(m[1].trim()); continue; }
|
|
32
|
+
// Cursor transcript format: [Tool call] Shell
|
|
33
|
+
const m2 = line.match(/\[Tool call\]\s+(\S+)/i);
|
|
34
|
+
if (m2 && m2[1]) calls.push(m2[1].trim());
|
|
31
35
|
}
|
|
32
36
|
return calls;
|
|
33
37
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Execution Trace: structured, desensitized evolution execution summary.
|
|
2
|
+
// Built during solidify and optionally shared with Hub via EvolutionEvent payload.
|
|
3
|
+
//
|
|
4
|
+
// Desensitization rules (applied locally, never on Hub):
|
|
5
|
+
// - File paths: basename + extension only (src/utils/retry.js -> retry.js)
|
|
6
|
+
// - Code content: never sent, only statistical metrics (lines, files)
|
|
7
|
+
// - Error messages: type signature only (TypeError: x is not a function -> TypeError)
|
|
8
|
+
// - Environment variables, secrets, user data: stripped entirely
|
|
9
|
+
// - Configurable via EVOLVER_TRACE_LEVEL: none | minimal | standard (default: minimal)
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const TRACE_LEVELS = { none: 0, minimal: 1, standard: 2 };
|
|
14
|
+
|
|
15
|
+
function getTraceLevel() {
|
|
16
|
+
const raw = String(process.env.EVOLVER_TRACE_LEVEL || 'minimal').toLowerCase().trim();
|
|
17
|
+
return TRACE_LEVELS[raw] != null ? raw : 'minimal';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function desensitizeFilePath(filePath) {
|
|
21
|
+
if (!filePath || typeof filePath !== 'string') return null;
|
|
22
|
+
const ext = path.extname(filePath);
|
|
23
|
+
const base = path.basename(filePath);
|
|
24
|
+
return base || ext || 'unknown';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractErrorSignature(errorText) {
|
|
28
|
+
if (!errorText || typeof errorText !== 'string') return null;
|
|
29
|
+
const text = errorText.trim();
|
|
30
|
+
|
|
31
|
+
// Match common error type patterns: TypeError, ReferenceError, SyntaxError, etc.
|
|
32
|
+
const jsError = text.match(/^((?:[A-Z][a-zA-Z]*)?Error)\b/);
|
|
33
|
+
if (jsError) return jsError[1];
|
|
34
|
+
|
|
35
|
+
// Match errno-style: ECONNRESET, ENOENT, EPERM, etc.
|
|
36
|
+
const errno = text.match(/\b(E[A-Z]{2,})\b/);
|
|
37
|
+
if (errno) return errno[1];
|
|
38
|
+
|
|
39
|
+
// Match HTTP status codes
|
|
40
|
+
const http = text.match(/\b((?:4|5)\d{2})\b/);
|
|
41
|
+
if (http) return 'HTTP_' + http[1];
|
|
42
|
+
|
|
43
|
+
// Fallback: first word if it looks like an error type
|
|
44
|
+
const firstWord = text.split(/[\s:]/)[0];
|
|
45
|
+
if (firstWord && firstWord.length <= 40 && /^[A-Z]/.test(firstWord)) return firstWord;
|
|
46
|
+
|
|
47
|
+
return 'UnknownError';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function inferToolChain(validationResults, blast) {
|
|
51
|
+
const tools = new Set();
|
|
52
|
+
|
|
53
|
+
if (blast && blast.files > 0) tools.add('file_edit');
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(validationResults)) {
|
|
56
|
+
for (const r of validationResults) {
|
|
57
|
+
const cmd = String(r.cmd || '').trim();
|
|
58
|
+
if (cmd.startsWith('npm test') || cmd.includes('jest') || cmd.includes('mocha')) {
|
|
59
|
+
tools.add('test_run');
|
|
60
|
+
} else if (cmd.includes('lint') || cmd.includes('eslint')) {
|
|
61
|
+
tools.add('lint_check');
|
|
62
|
+
} else if (cmd.includes('validate') || cmd.includes('check')) {
|
|
63
|
+
tools.add('validation_run');
|
|
64
|
+
} else if (cmd.startsWith('node ')) {
|
|
65
|
+
tools.add('node_exec');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return Array.from(tools);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function classifyBlastLevel(blast) {
|
|
74
|
+
if (!blast) return 'unknown';
|
|
75
|
+
const files = Number(blast.files) || 0;
|
|
76
|
+
const lines = Number(blast.lines) || 0;
|
|
77
|
+
if (files <= 3 && lines <= 50) return 'low';
|
|
78
|
+
if (files <= 10 && lines <= 200) return 'medium';
|
|
79
|
+
return 'high';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildExecutionTrace({
|
|
83
|
+
gene,
|
|
84
|
+
mutation,
|
|
85
|
+
signals,
|
|
86
|
+
blast,
|
|
87
|
+
constraintCheck,
|
|
88
|
+
validation,
|
|
89
|
+
canary,
|
|
90
|
+
outcomeStatus,
|
|
91
|
+
startedAt,
|
|
92
|
+
}) {
|
|
93
|
+
const level = getTraceLevel();
|
|
94
|
+
if (level === 'none') return null;
|
|
95
|
+
|
|
96
|
+
const trace = {
|
|
97
|
+
gene_id: gene && gene.id ? String(gene.id) : null,
|
|
98
|
+
mutation_category: (mutation && mutation.category) || (gene && gene.category) || null,
|
|
99
|
+
signals_matched: Array.isArray(signals) ? signals.slice(0, 10) : [],
|
|
100
|
+
outcome: outcomeStatus || 'unknown',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Minimal level: core metrics only
|
|
104
|
+
trace.files_changed_count = blast ? Number(blast.files) || 0 : 0;
|
|
105
|
+
trace.lines_added = 0;
|
|
106
|
+
trace.lines_removed = 0;
|
|
107
|
+
|
|
108
|
+
// Compute added/removed from blast if available
|
|
109
|
+
if (blast && blast.lines) {
|
|
110
|
+
// blast.lines is total churn (added + deleted); split heuristically
|
|
111
|
+
const total = Number(blast.lines) || 0;
|
|
112
|
+
if (outcomeStatus === 'success') {
|
|
113
|
+
trace.lines_added = Math.round(total * 0.6);
|
|
114
|
+
trace.lines_removed = total - trace.lines_added;
|
|
115
|
+
} else {
|
|
116
|
+
trace.lines_added = Math.round(total * 0.5);
|
|
117
|
+
trace.lines_removed = total - trace.lines_added;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
trace.validation_result = validation && validation.ok ? 'pass' : 'fail';
|
|
122
|
+
trace.blast_radius = classifyBlastLevel(blast);
|
|
123
|
+
|
|
124
|
+
// Standard level: richer context
|
|
125
|
+
if (level === 'standard') {
|
|
126
|
+
// Desensitized file list (basenames only)
|
|
127
|
+
if (blast && Array.isArray(blast.changed_files)) {
|
|
128
|
+
trace.file_types = {};
|
|
129
|
+
for (const f of blast.changed_files) {
|
|
130
|
+
const ext = path.extname(f) || '.unknown';
|
|
131
|
+
trace.file_types[ext] = (trace.file_types[ext] || 0) + 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Validation commands (already safe -- node/npm/npx only)
|
|
136
|
+
if (validation && Array.isArray(validation.results)) {
|
|
137
|
+
trace.validation_commands = validation.results.map(r => String(r.cmd || '').slice(0, 100));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Error signatures (desensitized)
|
|
141
|
+
trace.error_signatures = [];
|
|
142
|
+
if (constraintCheck && Array.isArray(constraintCheck.violations)) {
|
|
143
|
+
for (const v of constraintCheck.violations) {
|
|
144
|
+
// Constraint violations have known prefixes; classify directly
|
|
145
|
+
const vStr = String(v);
|
|
146
|
+
if (vStr.startsWith('max_files')) trace.error_signatures.push('max_files_exceeded');
|
|
147
|
+
else if (vStr.startsWith('forbidden_path')) trace.error_signatures.push('forbidden_path');
|
|
148
|
+
else if (vStr.startsWith('HARD CAP')) trace.error_signatures.push('hard_cap_breach');
|
|
149
|
+
else if (vStr.startsWith('CRITICAL')) trace.error_signatures.push('critical_overrun');
|
|
150
|
+
else if (vStr.startsWith('critical_path')) trace.error_signatures.push('critical_path_modified');
|
|
151
|
+
else if (vStr.startsWith('canary_failed')) trace.error_signatures.push('canary_failed');
|
|
152
|
+
else if (vStr.startsWith('ethics:')) trace.error_signatures.push('ethics_violation');
|
|
153
|
+
else {
|
|
154
|
+
const sig = extractErrorSignature(v);
|
|
155
|
+
if (sig) trace.error_signatures.push(sig);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (validation && Array.isArray(validation.results)) {
|
|
160
|
+
for (const r of validation.results) {
|
|
161
|
+
if (!r.ok && r.err) {
|
|
162
|
+
const sig = extractErrorSignature(r.err);
|
|
163
|
+
if (sig && !trace.error_signatures.includes(sig)) {
|
|
164
|
+
trace.error_signatures.push(sig);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
trace.error_signatures = trace.error_signatures.slice(0, 10);
|
|
170
|
+
|
|
171
|
+
// Tool chain inference
|
|
172
|
+
trace.tool_chain = inferToolChain(
|
|
173
|
+
validation && validation.results ? validation.results : [],
|
|
174
|
+
blast
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Duration
|
|
178
|
+
if (validation && validation.startedAt && validation.finishedAt) {
|
|
179
|
+
trace.validation_duration_ms = validation.finishedAt - validation.startedAt;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Canary result
|
|
183
|
+
if (canary && !canary.skipped) {
|
|
184
|
+
trace.canary_ok = !!canary.ok;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Timestamp
|
|
189
|
+
trace.created_at = new Date().toISOString();
|
|
190
|
+
|
|
191
|
+
return trace;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
buildExecutionTrace,
|
|
196
|
+
desensitizeFilePath,
|
|
197
|
+
extractErrorSignature,
|
|
198
|
+
inferToolChain,
|
|
199
|
+
classifyBlastLevel,
|
|
200
|
+
getTraceLevel,
|
|
201
|
+
};
|