@evomap/evolver 1.29.0

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.
Files changed (52) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +290 -0
  3. package/README.zh-CN.md +236 -0
  4. package/SKILL.md +132 -0
  5. package/assets/gep/capsules.json +79 -0
  6. package/assets/gep/events.jsonl +7 -0
  7. package/assets/gep/genes.json +108 -0
  8. package/index.js +479 -0
  9. package/package.json +38 -0
  10. package/src/canary.js +13 -0
  11. package/src/evolve.js +1704 -0
  12. package/src/gep/a2a.js +173 -0
  13. package/src/gep/a2aProtocol.js +736 -0
  14. package/src/gep/analyzer.js +35 -0
  15. package/src/gep/assetCallLog.js +130 -0
  16. package/src/gep/assetStore.js +297 -0
  17. package/src/gep/assets.js +36 -0
  18. package/src/gep/bridge.js +71 -0
  19. package/src/gep/candidates.js +142 -0
  20. package/src/gep/contentHash.js +65 -0
  21. package/src/gep/deviceId.js +209 -0
  22. package/src/gep/envFingerprint.js +68 -0
  23. package/src/gep/hubReview.js +206 -0
  24. package/src/gep/hubSearch.js +237 -0
  25. package/src/gep/issueReporter.js +262 -0
  26. package/src/gep/llmReview.js +92 -0
  27. package/src/gep/memoryGraph.js +771 -0
  28. package/src/gep/memoryGraphAdapter.js +203 -0
  29. package/src/gep/mutation.js +186 -0
  30. package/src/gep/narrativeMemory.js +108 -0
  31. package/src/gep/paths.js +113 -0
  32. package/src/gep/personality.js +355 -0
  33. package/src/gep/prompt.js +566 -0
  34. package/src/gep/questionGenerator.js +212 -0
  35. package/src/gep/reflection.js +127 -0
  36. package/src/gep/sanitize.js +67 -0
  37. package/src/gep/selector.js +250 -0
  38. package/src/gep/signals.js +417 -0
  39. package/src/gep/skillDistiller.js +499 -0
  40. package/src/gep/solidify.js +1681 -0
  41. package/src/gep/strategy.js +126 -0
  42. package/src/gep/taskReceiver.js +528 -0
  43. package/src/gep/validationReport.js +55 -0
  44. package/src/ops/cleanup.js +80 -0
  45. package/src/ops/commentary.js +60 -0
  46. package/src/ops/health_check.js +106 -0
  47. package/src/ops/index.js +11 -0
  48. package/src/ops/innovation.js +67 -0
  49. package/src/ops/lifecycle.js +168 -0
  50. package/src/ops/self_repair.js +72 -0
  51. package/src/ops/skills_monitor.js +143 -0
  52. package/src/ops/trigger.js +33 -0
package/src/evolve.js ADDED
@@ -0,0 +1,1704 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+ const { getRepoRoot, getMemoryDir, getSessionScope } = require('./gep/paths');
6
+ const { extractSignals } = require('./gep/signals');
7
+ const {
8
+ loadGenes,
9
+ loadCapsules,
10
+ readAllEvents,
11
+ getLastEventId,
12
+ appendCandidateJsonl,
13
+ readRecentCandidates,
14
+ readRecentExternalCandidates,
15
+ readRecentFailedCapsules,
16
+ ensureAssetFiles,
17
+ } = require('./gep/assetStore');
18
+ const { selectGeneAndCapsule, matchPatternToSignals } = require('./gep/selector');
19
+ const { buildGepPrompt, buildReusePrompt, buildHubMatchedBlock } = require('./gep/prompt');
20
+ const { hubSearch } = require('./gep/hubSearch');
21
+ const { logAssetCall } = require('./gep/assetCallLog');
22
+ const { extractCapabilityCandidates, renderCandidatesPreview } = require('./gep/candidates');
23
+ const memoryAdapter = require('./gep/memoryGraphAdapter');
24
+ const {
25
+ getAdvice: getMemoryAdvice,
26
+ recordSignalSnapshot,
27
+ recordHypothesis,
28
+ recordAttempt,
29
+ recordOutcome: recordOutcomeFromState,
30
+ memoryGraphPath,
31
+ } = memoryAdapter;
32
+ const { readStateForSolidify, writeStateForSolidify } = require('./gep/solidify');
33
+ const { fetchTasks, selectBestTask, claimTask, taskToSignals, claimWorkerTask, estimateCommitmentDeadline } = require('./gep/taskReceiver');
34
+ const { generateQuestions } = require('./gep/questionGenerator');
35
+ const { buildMutation, isHighRiskMutationAllowed } = require('./gep/mutation');
36
+ const { selectPersonalityForRun } = require('./gep/personality');
37
+ const { clip, writePromptArtifact, renderSessionsSpawnCall } = require('./gep/bridge');
38
+ const { getEvolutionDir } = require('./gep/paths');
39
+ const { shouldReflect, buildReflectionContext, recordReflection } = require('./gep/reflection');
40
+ const { loadNarrativeSummary } = require('./gep/narrativeMemory');
41
+ const { maybeReportIssue } = require('./gep/issueReporter');
42
+
43
+ const REPO_ROOT = getRepoRoot();
44
+
45
+ // Load environment variables from repo root
46
+ try {
47
+ require('dotenv').config({ path: path.join(REPO_ROOT, '.env'), quiet: true });
48
+ } catch (e) {
49
+ // dotenv might not be installed or .env missing, proceed gracefully
50
+ }
51
+
52
+ // Configuration from CLI flags or Env
53
+ const ARGS = process.argv.slice(2);
54
+ const IS_REVIEW_MODE = ARGS.includes('--review');
55
+ const IS_DRY_RUN = ARGS.includes('--dry-run');
56
+ const IS_RANDOM_DRIFT = ARGS.includes('--drift') || String(process.env.RANDOM_DRIFT || '').toLowerCase() === 'true';
57
+
58
+ // Default Configuration
59
+ const MEMORY_DIR = getMemoryDir();
60
+ const AGENT_NAME = process.env.AGENT_NAME || 'main';
61
+ const AGENT_SESSIONS_DIR = path.join(os.homedir(), `.openclaw/agents/${AGENT_NAME}/sessions`);
62
+ const TODAY_LOG = path.join(MEMORY_DIR, new Date().toISOString().split('T')[0] + '.md');
63
+
64
+ // Ensure memory directory exists so state/cache writes work.
65
+ try {
66
+ if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });
67
+ } catch (e) {}
68
+
69
+ function formatSessionLog(jsonlContent) {
70
+ const result = [];
71
+ const lines = jsonlContent.split('\n');
72
+ let lastLine = '';
73
+ let repeatCount = 0;
74
+
75
+ const flushRepeats = () => {
76
+ if (repeatCount > 0) {
77
+ result.push(` ... [Repeated ${repeatCount} times] ...`);
78
+ repeatCount = 0;
79
+ }
80
+ };
81
+
82
+ for (const line of lines) {
83
+ if (!line.trim()) continue;
84
+ try {
85
+ const data = JSON.parse(line);
86
+ let entry = '';
87
+
88
+ if (data.type === 'message' && data.message) {
89
+ const role = (data.message.role || 'unknown').toUpperCase();
90
+ let content = '';
91
+ if (Array.isArray(data.message.content)) {
92
+ content = data.message.content
93
+ .map(c => {
94
+ if (c.type === 'text') return c.text;
95
+ if (c.type === 'toolCall') return `[TOOL: ${c.name}]`;
96
+ return '';
97
+ })
98
+ .join(' ');
99
+ } else if (typeof data.message.content === 'string') {
100
+ content = data.message.content;
101
+ } else {
102
+ content = JSON.stringify(data.message.content);
103
+ }
104
+
105
+ // Capture LLM errors from errorMessage field (e.g. "Unsupported MIME type: image/gif")
106
+ if (data.message.errorMessage) {
107
+ const errMsg = typeof data.message.errorMessage === 'string'
108
+ ? data.message.errorMessage
109
+ : JSON.stringify(data.message.errorMessage);
110
+ content = `[LLM ERROR] ${errMsg.replace(/\n+/g, ' ').slice(0, 300)}`;
111
+ }
112
+
113
+ // Filter: Skip Heartbeats to save noise
114
+ if (content.trim() === 'HEARTBEAT_OK') continue;
115
+ if (content.includes('NO_REPLY') && !data.message.errorMessage) continue;
116
+
117
+ // Clean up newlines for compact reading
118
+ content = content.replace(/\n+/g, ' ').slice(0, 300);
119
+ entry = `**${role}**: ${content}`;
120
+ } else if (data.type === 'tool_result' || (data.message && data.message.role === 'toolResult')) {
121
+ // Filter: Skip generic success results or short uninformative ones
122
+ // Only show error or significant output
123
+ let resContent = '';
124
+
125
+ // Robust extraction: Handle structured tool results (e.g. sessions_spawn) that lack 'output'
126
+ if (data.tool_result) {
127
+ if (data.tool_result.output) {
128
+ resContent = data.tool_result.output;
129
+ } else {
130
+ resContent = JSON.stringify(data.tool_result);
131
+ }
132
+ }
133
+
134
+ if (data.content) resContent = typeof data.content === 'string' ? data.content : JSON.stringify(data.content);
135
+
136
+ if (resContent.length < 50 && (resContent.includes('success') || resContent.includes('done'))) continue;
137
+ if (resContent.trim() === '' || resContent === '{}') continue;
138
+
139
+ // Improvement: Show snippet of result (especially errors) instead of hiding it
140
+ const preview = resContent.replace(/\n+/g, ' ').slice(0, 200);
141
+ entry = `[TOOL RESULT] ${preview}${resContent.length > 200 ? '...' : ''}`;
142
+ }
143
+
144
+ if (entry) {
145
+ if (entry === lastLine) {
146
+ repeatCount++;
147
+ } else {
148
+ flushRepeats();
149
+ result.push(entry);
150
+ lastLine = entry;
151
+ }
152
+ }
153
+ } catch (e) {
154
+ continue;
155
+ }
156
+ }
157
+ flushRepeats();
158
+ return result.join('\n');
159
+ }
160
+
161
+ function readRealSessionLog() {
162
+ try {
163
+ if (!fs.existsSync(AGENT_SESSIONS_DIR)) return '[NO SESSION LOGS FOUND]';
164
+
165
+ const now = Date.now();
166
+ const ACTIVE_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours
167
+ const TARGET_BYTES = 120000;
168
+ const PER_SESSION_BYTES = 20000; // Read tail of each active session
169
+
170
+ // Session scope isolation: when EVOLVER_SESSION_SCOPE is set,
171
+ // only read sessions whose filenames contain the scope identifier.
172
+ // This prevents cross-channel/cross-project memory contamination.
173
+ const sessionScope = getSessionScope();
174
+
175
+ // Find ALL active sessions (modified in last 24h), sorted newest first
176
+ let files = fs
177
+ .readdirSync(AGENT_SESSIONS_DIR)
178
+ .filter(f => f.endsWith('.jsonl') && !f.includes('.lock'))
179
+ .map(f => {
180
+ try {
181
+ const st = fs.statSync(path.join(AGENT_SESSIONS_DIR, f));
182
+ return { name: f, time: st.mtime.getTime(), size: st.size };
183
+ } catch (e) {
184
+ return null;
185
+ }
186
+ })
187
+ .filter(f => f && (now - f.time) < ACTIVE_WINDOW_MS)
188
+ .sort((a, b) => b.time - a.time); // Newest first
189
+
190
+ if (files.length === 0) return '[NO JSONL FILES]';
191
+
192
+ // Skip evolver's own sessions to avoid self-reference loops
193
+ let nonEvolverFiles = files.filter(f => !f.name.startsWith('evolver_hand_'));
194
+
195
+ // Session scope filter: when scope is active, only include sessions
196
+ // whose filename contains the scope string (e.g., channel_123456.jsonl).
197
+ // If no sessions match the scope, fall back to all non-evolver sessions
198
+ // (graceful degradation -- better to evolve with global context than not at all).
199
+ if (sessionScope && nonEvolverFiles.length > 0) {
200
+ const scopeLower = sessionScope.toLowerCase();
201
+ const scopedFiles = nonEvolverFiles.filter(f => f.name.toLowerCase().includes(scopeLower));
202
+ if (scopedFiles.length > 0) {
203
+ nonEvolverFiles = scopedFiles;
204
+ console.log(`[SessionScope] Filtered to ${scopedFiles.length} session(s) matching scope "${sessionScope}".`);
205
+ } else {
206
+ console.log(`[SessionScope] No sessions match scope "${sessionScope}". Using all ${nonEvolverFiles.length} session(s) (fallback).`);
207
+ }
208
+ }
209
+
210
+ const activeFiles = nonEvolverFiles.length > 0 ? nonEvolverFiles : files.slice(0, 1);
211
+
212
+ // Read from multiple active sessions (up to 6) to get a full picture
213
+ const maxSessions = Math.min(activeFiles.length, 6);
214
+ const sections = [];
215
+ let totalBytes = 0;
216
+
217
+ for (let i = 0; i < maxSessions && totalBytes < TARGET_BYTES; i++) {
218
+ const f = activeFiles[i];
219
+ const bytesLeft = TARGET_BYTES - totalBytes;
220
+ const readSize = Math.min(PER_SESSION_BYTES, bytesLeft);
221
+ const raw = readRecentLog(path.join(AGENT_SESSIONS_DIR, f.name), readSize);
222
+ const formatted = formatSessionLog(raw);
223
+ if (formatted.trim()) {
224
+ sections.push(`--- SESSION (${f.name}) ---\n${formatted}`);
225
+ totalBytes += formatted.length;
226
+ }
227
+ }
228
+
229
+ let content = sections.join('\n\n');
230
+
231
+ return content;
232
+ } catch (e) {
233
+ return `[ERROR READING SESSION LOGS: ${e.message}]`;
234
+ }
235
+ }
236
+
237
+ function readRecentLog(filePath, size = 10000) {
238
+ try {
239
+ if (!fs.existsSync(filePath)) return `[MISSING] ${filePath}`;
240
+ const stats = fs.statSync(filePath);
241
+ const start = Math.max(0, stats.size - size);
242
+ const buffer = Buffer.alloc(stats.size - start);
243
+ const fd = fs.openSync(filePath, 'r');
244
+ fs.readSync(fd, buffer, 0, buffer.length, start);
245
+ fs.closeSync(fd);
246
+ return buffer.toString('utf8');
247
+ } catch (e) {
248
+ return `[ERROR READING ${filePath}: ${e.message}]`;
249
+ }
250
+ }
251
+
252
+ function checkSystemHealth() {
253
+ const report = [];
254
+ try {
255
+ // Uptime & Node Version
256
+ const uptime = (os.uptime() / 3600).toFixed(1);
257
+ report.push(`Uptime: ${uptime}h`);
258
+ report.push(`Node: ${process.version}`);
259
+
260
+ // Memory Usage (RSS)
261
+ const mem = process.memoryUsage();
262
+ const rssMb = (mem.rss / 1024 / 1024).toFixed(1);
263
+ report.push(`Agent RSS: ${rssMb}MB`);
264
+
265
+ // Optimization: Use native Node.js fs.statfsSync instead of spawning 'df'
266
+ if (fs.statfsSync) {
267
+ const stats = fs.statfsSync('/');
268
+ const total = stats.blocks * stats.bsize;
269
+ const free = stats.bfree * stats.bsize;
270
+ const used = total - free;
271
+ const freeGb = (free / 1024 / 1024 / 1024).toFixed(1);
272
+ const usedPercent = Math.round((used / total) * 100);
273
+ report.push(`Disk: ${usedPercent}% (${freeGb}G free)`);
274
+ }
275
+ } catch (e) {}
276
+
277
+ try {
278
+ if (process.platform === 'win32') {
279
+ const wmic = execSync('tasklist /FI "IMAGENAME eq node.exe" /NH', {
280
+ encoding: 'utf8',
281
+ stdio: ['ignore', 'pipe', 'ignore'],
282
+ timeout: 3000,
283
+ windowsHide: true,
284
+ });
285
+ const count = wmic.split('\n').filter(l => l.trim() && !l.includes('INFO:')).length;
286
+ report.push(`Node Processes: ${count}`);
287
+ } else {
288
+ try {
289
+ const pgrep = execSync('pgrep -c node', {
290
+ encoding: 'utf8',
291
+ stdio: ['ignore', 'pipe', 'ignore'],
292
+ timeout: 2000,
293
+ });
294
+ report.push(`Node Processes: ${pgrep.trim()}`);
295
+ } catch (e) {
296
+ const ps = execSync('ps aux | grep node | grep -v grep | wc -l', {
297
+ encoding: 'utf8',
298
+ stdio: ['ignore', 'pipe', 'ignore'],
299
+ timeout: 2000,
300
+ });
301
+ report.push(`Node Processes: ${ps.trim()}`);
302
+ }
303
+ }
304
+ } catch (e) {}
305
+
306
+ // Integration Health Checks (Env Vars)
307
+ try {
308
+ const issues = [];
309
+
310
+ // Generic Integration Status Check (Decoupled)
311
+ if (process.env.INTEGRATION_STATUS_CMD) {
312
+ try {
313
+ const status = execSync(process.env.INTEGRATION_STATUS_CMD, {
314
+ encoding: 'utf8',
315
+ stdio: ['ignore', 'pipe', 'ignore'],
316
+ timeout: 2000,
317
+ windowsHide: true,
318
+ });
319
+ if (status.trim()) issues.push(status.trim());
320
+ } catch (e) {}
321
+ }
322
+
323
+ if (issues.length > 0) {
324
+ report.push(`Integrations: ${issues.join(', ')}`);
325
+ } else {
326
+ report.push('Integrations: Nominal');
327
+ }
328
+ } catch (e) {}
329
+
330
+ return report.length ? report.join(' | ') : 'Health Check Unavailable';
331
+ }
332
+
333
+ function getMutationDirective(logContent) {
334
+ // Signal hints derived from recent logs.
335
+ const errorMatches = logContent.match(/\[ERROR|Error:|Exception:|FAIL|Failed|"isError":true/gi) || [];
336
+ const errorCount = errorMatches.length;
337
+ const isUnstable = errorCount > 2;
338
+ const recommendedIntent = isUnstable ? 'repair' : 'optimize';
339
+
340
+ return `
341
+ [Signal Hints]
342
+ - recent_error_count: ${errorCount}
343
+ - stability: ${isUnstable ? 'unstable' : 'stable'}
344
+ - recommended_intent: ${recommendedIntent}
345
+ `;
346
+ }
347
+
348
+ const STATE_FILE = path.join(getEvolutionDir(), 'evolution_state.json');
349
+ const DORMANT_HYPOTHESIS_FILE = path.join(getEvolutionDir(), 'dormant_hypothesis.json');
350
+ var DORMANT_TTL_MS = 3600 * 1000;
351
+
352
+ function writeDormantHypothesis(data) {
353
+ try {
354
+ var dir = getEvolutionDir();
355
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
356
+ var obj = Object.assign({}, data, { created_at: new Date().toISOString(), ttl_ms: DORMANT_TTL_MS });
357
+ var tmp = DORMANT_HYPOTHESIS_FILE + '.tmp';
358
+ fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
359
+ fs.renameSync(tmp, DORMANT_HYPOTHESIS_FILE);
360
+ console.log('[DormantHypothesis] Saved partial state before backoff: ' + (data.backoff_reason || 'unknown'));
361
+ } catch (e) {
362
+ console.log('[DormantHypothesis] Write failed (non-fatal): ' + (e && e.message ? e.message : e));
363
+ }
364
+ }
365
+
366
+ function readDormantHypothesis() {
367
+ try {
368
+ if (!fs.existsSync(DORMANT_HYPOTHESIS_FILE)) return null;
369
+ var raw = fs.readFileSync(DORMANT_HYPOTHESIS_FILE, 'utf8');
370
+ if (!raw.trim()) return null;
371
+ var obj = JSON.parse(raw);
372
+ var createdAt = obj.created_at ? new Date(obj.created_at).getTime() : 0;
373
+ var ttl = Number.isFinite(Number(obj.ttl_ms)) ? Number(obj.ttl_ms) : DORMANT_TTL_MS;
374
+ if (Date.now() - createdAt > ttl) {
375
+ clearDormantHypothesis();
376
+ console.log('[DormantHypothesis] Expired (age: ' + Math.round((Date.now() - createdAt) / 1000) + 's). Discarded.');
377
+ return null;
378
+ }
379
+ return obj;
380
+ } catch (e) {
381
+ return null;
382
+ }
383
+ }
384
+
385
+ function clearDormantHypothesis() {
386
+ try {
387
+ if (fs.existsSync(DORMANT_HYPOTHESIS_FILE)) fs.unlinkSync(DORMANT_HYPOTHESIS_FILE);
388
+ } catch (e) {}
389
+ }
390
+ // Read MEMORY.md and USER.md from the WORKSPACE root (not the evolver plugin dir).
391
+ // This avoids symlink breakage if the target file is temporarily deleted.
392
+ const WORKSPACE_ROOT = process.env.OPENCLAW_WORKSPACE || path.resolve(REPO_ROOT, '../..');
393
+ const ROOT_MEMORY = path.join(WORKSPACE_ROOT, 'MEMORY.md');
394
+ const DIR_MEMORY = path.join(MEMORY_DIR, 'MEMORY.md');
395
+ const MEMORY_FILE = fs.existsSync(ROOT_MEMORY) ? ROOT_MEMORY : (fs.existsSync(DIR_MEMORY) ? DIR_MEMORY : ROOT_MEMORY);
396
+ const USER_FILE = path.join(WORKSPACE_ROOT, 'USER.md');
397
+
398
+ function readMemorySnippet() {
399
+ try {
400
+ // Session scope isolation: when a scope is active, prefer scoped MEMORY.md
401
+ // at memory/scopes/<scope>/MEMORY.md. Falls back to global MEMORY.md if
402
+ // scoped file doesn't exist (common: scoped MEMORY.md created on first evolution).
403
+ const scope = getSessionScope();
404
+ let memFile = MEMORY_FILE;
405
+ if (scope) {
406
+ const scopedMemory = path.join(MEMORY_DIR, 'scopes', scope, 'MEMORY.md');
407
+ if (fs.existsSync(scopedMemory)) {
408
+ memFile = scopedMemory;
409
+ console.log(`[SessionScope] Reading scoped MEMORY.md for "${scope}".`);
410
+ } else {
411
+ // First run with scope: global MEMORY.md will be used, but note it.
412
+ console.log(`[SessionScope] No scoped MEMORY.md for "${scope}". Using global MEMORY.md.`);
413
+ }
414
+ }
415
+ if (!fs.existsSync(memFile)) return '[MEMORY.md MISSING]';
416
+ const content = fs.readFileSync(memFile, 'utf8');
417
+ // Optimization: Increased limit from 2000 to 50000 for modern context windows
418
+ return content.length > 50000
419
+ ? content.slice(0, 50000) + `\n... [TRUNCATED: ${content.length - 50000} chars remaining]`
420
+ : content;
421
+ } catch (e) {
422
+ return '[ERROR READING MEMORY.md]';
423
+ }
424
+ }
425
+
426
+ function readUserSnippet() {
427
+ try {
428
+ if (!fs.existsSync(USER_FILE)) return '[USER.md MISSING]';
429
+ return fs.readFileSync(USER_FILE, 'utf8');
430
+ } catch (e) {
431
+ return '[ERROR READING USER.md]';
432
+ }
433
+ }
434
+
435
+ function getNextCycleId() {
436
+ let state = { cycleCount: 0, lastRun: 0 };
437
+ try {
438
+ if (fs.existsSync(STATE_FILE)) {
439
+ state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
440
+ }
441
+ } catch (e) {}
442
+
443
+ state.cycleCount = (state.cycleCount || 0) + 1;
444
+ state.lastRun = Date.now();
445
+
446
+ try {
447
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
448
+ } catch (e) {}
449
+
450
+ return String(state.cycleCount).padStart(4, '0');
451
+ }
452
+
453
+ function performMaintenance() {
454
+ // Auto-update check (rate-limited, non-fatal).
455
+ checkAndAutoUpdate();
456
+
457
+ try {
458
+ if (!fs.existsSync(AGENT_SESSIONS_DIR)) return;
459
+
460
+ const files = fs.readdirSync(AGENT_SESSIONS_DIR).filter(f => f.endsWith('.jsonl'));
461
+
462
+ // Clean up evolver's own hand sessions immediately.
463
+ // These are single-use executor sessions that must not accumulate,
464
+ // otherwise they pollute the agent's context and starve user conversations.
465
+ const evolverFiles = files.filter(f => f.startsWith('evolver_hand_'));
466
+ for (const f of evolverFiles) {
467
+ try {
468
+ fs.unlinkSync(path.join(AGENT_SESSIONS_DIR, f));
469
+ } catch (_) {}
470
+ }
471
+ if (evolverFiles.length > 0) {
472
+ console.log(`[Maintenance] Cleaned ${evolverFiles.length} evolver hand session(s).`);
473
+ }
474
+
475
+ // Archive old non-evolver sessions when count exceeds threshold.
476
+ const remaining = files.length - evolverFiles.length;
477
+ if (remaining < 100) return;
478
+
479
+ console.log(`[Maintenance] Found ${remaining} session logs. Archiving old ones...`);
480
+
481
+ const ARCHIVE_DIR = path.join(AGENT_SESSIONS_DIR, 'archive');
482
+ if (!fs.existsSync(ARCHIVE_DIR)) fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
483
+
484
+ const fileStats = files
485
+ .filter(f => !f.startsWith('evolver_hand_'))
486
+ .map(f => {
487
+ try {
488
+ return { name: f, time: fs.statSync(path.join(AGENT_SESSIONS_DIR, f)).mtime.getTime() };
489
+ } catch (e) {
490
+ return null;
491
+ }
492
+ })
493
+ .filter(Boolean)
494
+ .sort((a, b) => a.time - b.time);
495
+
496
+ const toArchive = fileStats.slice(0, fileStats.length - 50);
497
+
498
+ for (const file of toArchive) {
499
+ const oldPath = path.join(AGENT_SESSIONS_DIR, file.name);
500
+ const newPath = path.join(ARCHIVE_DIR, file.name);
501
+ fs.renameSync(oldPath, newPath);
502
+ }
503
+ if (toArchive.length > 0) {
504
+ console.log(`[Maintenance] Archived ${toArchive.length} logs to ${ARCHIVE_DIR}`);
505
+ }
506
+ } catch (e) {
507
+ console.error(`[Maintenance] Error: ${e.message}`);
508
+ }
509
+ }
510
+
511
+ // --- Auto-update: check for newer versions of evolver and wrapper on ClawHub ---
512
+ function checkAndAutoUpdate() {
513
+ try {
514
+ // Read config: default autoUpdate = true
515
+ const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
516
+ let autoUpdate = true;
517
+ let intervalHours = 6;
518
+ try {
519
+ if (fs.existsSync(configPath)) {
520
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
521
+ if (cfg.evolver && cfg.evolver.autoUpdate === false) autoUpdate = false;
522
+ if (cfg.evolver && Number.isFinite(Number(cfg.evolver.autoUpdateIntervalHours))) {
523
+ intervalHours = Number(cfg.evolver.autoUpdateIntervalHours);
524
+ }
525
+ }
526
+ } catch (_) {}
527
+
528
+ if (!autoUpdate) return;
529
+
530
+ // Rate limit: only check once per interval
531
+ const stateFile = path.join(MEMORY_DIR, 'evolver_update_check.json');
532
+ const now = Date.now();
533
+ const intervalMs = intervalHours * 60 * 60 * 1000;
534
+ try {
535
+ if (fs.existsSync(stateFile)) {
536
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
537
+ if (state.lastCheckedAt && (now - new Date(state.lastCheckedAt).getTime()) < intervalMs) {
538
+ return; // Too soon, skip
539
+ }
540
+ }
541
+ } catch (_) {}
542
+
543
+ let clawhubBin = null;
544
+ const whichCmd = process.platform === 'win32' ? 'where clawhub' : 'which clawhub';
545
+ const candidates = ['clawhub', path.join(os.homedir(), '.npm-global/bin/clawhub'), '/usr/local/bin/clawhub'];
546
+ for (const c of candidates) {
547
+ try {
548
+ if (c === 'clawhub') {
549
+ execSync(whichCmd, { stdio: 'ignore', timeout: 3000, windowsHide: true });
550
+ clawhubBin = 'clawhub';
551
+ break;
552
+ }
553
+ if (fs.existsSync(c)) { clawhubBin = c; break; }
554
+ } catch (_) {}
555
+ }
556
+ if (!clawhubBin) return; // No clawhub CLI available
557
+
558
+ // Update evolver and feishu-evolver-wrapper
559
+ const slugs = ['evolver', 'feishu-evolver-wrapper'];
560
+ let updated = false;
561
+ for (const slug of slugs) {
562
+ try {
563
+ const out = execSync(`${clawhubBin} update ${slug} --force`, {
564
+ encoding: 'utf8',
565
+ stdio: ['ignore', 'pipe', 'pipe'],
566
+ timeout: 30000,
567
+ cwd: path.resolve(REPO_ROOT, '..'),
568
+ windowsHide: true,
569
+ });
570
+ if (out && !out.includes('already up to date') && !out.includes('not installed')) {
571
+ console.log(`[AutoUpdate] ${slug}: ${out.trim().split('\n').pop()}`);
572
+ updated = true;
573
+ }
574
+ } catch (e) {
575
+ // Non-fatal: update failure should never block evolution
576
+ }
577
+ }
578
+
579
+ // Write state
580
+ try {
581
+ const stateData = {
582
+ lastCheckedAt: new Date(now).toISOString(),
583
+ updated,
584
+ };
585
+ fs.writeFileSync(stateFile, JSON.stringify(stateData, null, 2) + '\n');
586
+ } catch (_) {}
587
+
588
+ if (updated) {
589
+ console.log('[AutoUpdate] Skills updated. Changes will take effect on next wrapper restart.');
590
+ }
591
+ } catch (e) {
592
+ // Entire auto-update is non-fatal
593
+ console.log(`[AutoUpdate] Check failed (non-fatal): ${e.message}`);
594
+ }
595
+ }
596
+
597
+ function sleepMs(ms) {
598
+ const t = Number(ms);
599
+ const n = Number.isFinite(t) ? Math.max(0, t) : 0;
600
+ return new Promise(resolve => setTimeout(resolve, n));
601
+ }
602
+
603
+ // Check system load average via os.loadavg().
604
+ // Returns { load1m, load5m, load15m }. Used for load-aware throttling.
605
+ function getSystemLoad() {
606
+ try {
607
+ const loadavg = os.loadavg();
608
+ return { load1m: loadavg[0], load5m: loadavg[1], load15m: loadavg[2] };
609
+ } catch (e) {
610
+ return { load1m: 0, load5m: 0, load15m: 0 };
611
+ }
612
+ }
613
+
614
+ // Calculate intelligent default load threshold based on CPU cores
615
+ // Rule of thumb:
616
+ // - Single-core: 0.8-1.0 (use 0.9)
617
+ // - Multi-core: cores x 0.8-1.0 (use 0.9)
618
+ // - Production: reserve 20% headroom for burst traffic
619
+ function getDefaultLoadMax() {
620
+ const cpuCount = os.cpus().length;
621
+ if (cpuCount === 1) {
622
+ return 0.9;
623
+ } else {
624
+ return cpuCount * 0.9;
625
+ }
626
+ }
627
+
628
+ // Check how many agent sessions are actively being processed (modified in the last N minutes).
629
+ // If the agent is busy with user conversations, evolver should back off.
630
+ function getRecentActiveSessionCount(windowMs) {
631
+ try {
632
+ if (!fs.existsSync(AGENT_SESSIONS_DIR)) return 0;
633
+ const now = Date.now();
634
+ const w = Number.isFinite(windowMs) ? windowMs : 10 * 60 * 1000;
635
+ return fs.readdirSync(AGENT_SESSIONS_DIR)
636
+ .filter(f => f.endsWith('.jsonl') && !f.includes('.lock') && !f.startsWith('evolver_hand_'))
637
+ .filter(f => {
638
+ try { return (now - fs.statSync(path.join(AGENT_SESSIONS_DIR, f)).mtimeMs) < w; } catch (_) { return false; }
639
+ }).length;
640
+ } catch (_) { return 0; }
641
+ }
642
+
643
+ async function run() {
644
+ const bridgeEnabled = String(process.env.EVOLVE_BRIDGE || '').toLowerCase() !== 'false';
645
+ const loopMode = ARGS.includes('--loop') || ARGS.includes('--mad-dog') || String(process.env.EVOLVE_LOOP || '').toLowerCase() === 'true';
646
+
647
+ // SAFEGUARD: If another evolver Hand Agent is already running, back off.
648
+ // Prevents race conditions when a wrapper restarts while the old Hand Agent
649
+ // is still executing. The Core yields instead of starting a competing cycle.
650
+ if (process.platform !== 'win32') {
651
+ try {
652
+ const _psRace = require('child_process').execSync(
653
+ 'ps aux | grep "evolver_hand_" | grep "openclaw.*agent" | grep -v grep',
654
+ { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }
655
+ ).trim();
656
+ if (_psRace && _psRace.length > 0) {
657
+ console.log('[Evolver] Another evolver Hand Agent is already running. Yielding this cycle.');
658
+ return;
659
+ }
660
+ } catch (_) {
661
+ // grep exit 1 = no match = no conflict, safe to proceed
662
+ }
663
+ }
664
+
665
+ // SAFEGUARD: If the agent has too many active user sessions, back off.
666
+ // Evolver must not starve user conversations by consuming model concurrency.
667
+ const QUEUE_MAX = Number.parseInt(process.env.EVOLVE_AGENT_QUEUE_MAX || '10', 10);
668
+ const QUEUE_BACKOFF_MS = Number.parseInt(process.env.EVOLVE_AGENT_QUEUE_BACKOFF_MS || '60000', 10);
669
+ const activeUserSessions = getRecentActiveSessionCount(10 * 60 * 1000);
670
+ if (activeUserSessions > QUEUE_MAX) {
671
+ console.log(`[Evolver] Agent has ${activeUserSessions} active user sessions (max ${QUEUE_MAX}). Backing off ${QUEUE_BACKOFF_MS}ms to avoid starving user conversations.`);
672
+ writeDormantHypothesis({
673
+ backoff_reason: 'active_sessions_exceeded',
674
+ active_sessions: activeUserSessions,
675
+ queue_max: QUEUE_MAX,
676
+ });
677
+ await sleepMs(QUEUE_BACKOFF_MS);
678
+ return;
679
+ }
680
+
681
+ // SAFEGUARD: System load awareness.
682
+ // When system load is too high (e.g. too many concurrent processes, heavy I/O),
683
+ // back off to prevent the evolver from contributing to load spikes.
684
+ // Echo-MingXuan's Cycle #55 saw load spike from 0.02-0.50 to 1.30 before crash.
685
+ const LOAD_MAX = parseFloat(process.env.EVOLVE_LOAD_MAX || String(getDefaultLoadMax()));
686
+ const sysLoad = getSystemLoad();
687
+ if (sysLoad.load1m > LOAD_MAX) {
688
+ console.log(`[Evolver] System load ${sysLoad.load1m.toFixed(2)} exceeds max ${LOAD_MAX.toFixed(1)} (auto-calculated for ${os.cpus().length} cores). Backing off ${QUEUE_BACKOFF_MS}ms.`);
689
+ writeDormantHypothesis({
690
+ backoff_reason: 'system_load_exceeded',
691
+ system_load: { load1m: sysLoad.load1m, load5m: sysLoad.load5m, load15m: sysLoad.load15m },
692
+ load_max: LOAD_MAX,
693
+ cpu_cores: os.cpus().length,
694
+ });
695
+ await sleepMs(QUEUE_BACKOFF_MS);
696
+ return;
697
+ }
698
+
699
+ // Loop gating: do not start a new cycle until the previous one is solidified.
700
+ // This prevents wrappers from "fast-cycling" the Brain without waiting for the Hand to finish.
701
+ if (bridgeEnabled && loopMode) {
702
+ try {
703
+ const st = readStateForSolidify();
704
+ const lastRun = st && st.last_run ? st.last_run : null;
705
+ const lastSolid = st && st.last_solidify ? st.last_solidify : null;
706
+ if (lastRun && lastRun.run_id) {
707
+ const pending = !lastSolid || !lastSolid.run_id || String(lastSolid.run_id) !== String(lastRun.run_id);
708
+ if (pending) {
709
+ writeDormantHypothesis({
710
+ backoff_reason: 'loop_gating_pending_solidify',
711
+ signals: lastRun && Array.isArray(lastRun.signals) ? lastRun.signals : [],
712
+ selected_gene_id: lastRun && lastRun.selected_gene_id ? lastRun.selected_gene_id : null,
713
+ mutation: lastRun && lastRun.mutation ? lastRun.mutation : null,
714
+ personality_state: lastRun && lastRun.personality_state ? lastRun.personality_state : null,
715
+ run_id: lastRun.run_id,
716
+ });
717
+ const raw = process.env.EVOLVE_PENDING_SLEEP_MS || process.env.EVOLVE_MIN_INTERVAL || '120000';
718
+ const n = parseInt(String(raw), 10);
719
+ const waitMs = Number.isFinite(n) ? Math.max(0, n) : 120000;
720
+ await sleepMs(waitMs);
721
+ return;
722
+ }
723
+ }
724
+ } catch (e) {
725
+ // If we cannot read state, proceed (fail open) to avoid deadlock.
726
+ }
727
+ }
728
+
729
+ // Reset per-cycle env flags to prevent state leaking between cycles.
730
+ // In --loop mode, process.env persists across cycles. The circuit breaker
731
+ // below will re-set FORCE_INNOVATION if the condition still holds.
732
+ // CWD Recovery: If the working directory was deleted during a previous cycle
733
+ // (e.g., by git reset/restore or directory removal), process.cwd() throws
734
+ // ENOENT and ALL subsequent operations fail. Recover by chdir to REPO_ROOT.
735
+ try {
736
+ process.cwd();
737
+ } catch (e) {
738
+ if (e && e.code === 'ENOENT') {
739
+ console.warn('[Evolver] CWD lost (ENOENT). Recovering to REPO_ROOT: ' + REPO_ROOT);
740
+ try { process.chdir(REPO_ROOT); } catch (e2) {
741
+ console.error('[Evolver] CWD recovery failed: ' + (e2 && e2.message ? e2.message : e2));
742
+ throw e;
743
+ }
744
+ } else {
745
+ throw e;
746
+ }
747
+ }
748
+
749
+ delete process.env.FORCE_INNOVATION;
750
+
751
+ // SAFEGUARD: Git repository check.
752
+ // Solidify, rollback, and blast radius all depend on git. Without a git repo
753
+ // these operations silently produce empty results, leading to data loss.
754
+ try {
755
+ execSync('git rev-parse --git-dir', { cwd: REPO_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 });
756
+ } catch (_) {
757
+ console.error('[Evolver] FATAL: Not a git repository (' + REPO_ROOT + ').');
758
+ console.error('[Evolver] Evolver requires git for rollback, blast radius calculation, and solidify.');
759
+ console.error('[Evolver] Run "git init && git add -A && git commit -m init" in your project root, then try again.');
760
+ process.exitCode = 1;
761
+ return;
762
+ }
763
+
764
+ var dormantHypothesis = readDormantHypothesis();
765
+ if (dormantHypothesis) {
766
+ console.log('[DormantHypothesis] Recovered partial state from previous backoff: ' + (dormantHypothesis.backoff_reason || 'unknown'));
767
+ clearDormantHypothesis();
768
+ }
769
+
770
+ const startTime = Date.now();
771
+ console.log('Scanning session logs...');
772
+
773
+ // Ensure all GEP asset files exist before any operation.
774
+ // This prevents "No such file or directory" errors when external tools
775
+ // (grep, cat, etc.) reference optional append-only files like genes.jsonl.
776
+ try { ensureAssetFiles(); } catch (e) {
777
+ console.error(`[AssetInit] ensureAssetFiles failed (non-fatal): ${e.message}`);
778
+ }
779
+
780
+ // Maintenance: Clean up old logs to keep directory scan fast
781
+ if (!IS_DRY_RUN) {
782
+ performMaintenance();
783
+ } else {
784
+ console.log('[Maintenance] Skipped (dry-run mode).');
785
+ }
786
+
787
+ // --- Repair Loop Circuit Breaker ---
788
+ // Detect when the evolver is stuck in a "repair -> fail -> repair" cycle.
789
+ // If the last N events are all failed repairs with the same gene, force
790
+ // innovation intent to break out of the loop instead of retrying the same fix.
791
+ const REPAIR_LOOP_THRESHOLD = 3;
792
+ try {
793
+ const allEvents = readAllEvents();
794
+ const recent = Array.isArray(allEvents) ? allEvents.slice(-REPAIR_LOOP_THRESHOLD) : [];
795
+ if (recent.length >= REPAIR_LOOP_THRESHOLD) {
796
+ const allRepairFailed = recent.every(e =>
797
+ e && e.intent === 'repair' &&
798
+ e.outcome && e.outcome.status === 'failed'
799
+ );
800
+ if (allRepairFailed) {
801
+ const geneIds = recent.map(e => (e.genes_used && e.genes_used[0]) || 'unknown');
802
+ const sameGene = geneIds.every(id => id === geneIds[0]);
803
+ console.warn(`[CircuitBreaker] Detected ${REPAIR_LOOP_THRESHOLD} consecutive failed repairs${sameGene ? ` (gene: ${geneIds[0]})` : ''}. Forcing innovation intent to break the loop.`);
804
+ // Set env flag that downstream code reads to force innovation
805
+ process.env.FORCE_INNOVATION = 'true';
806
+ }
807
+ }
808
+ } catch (e) {
809
+ // Non-fatal: if we can't read events, proceed normally
810
+ console.error(`[CircuitBreaker] Check failed (non-fatal): ${e.message}`);
811
+ }
812
+
813
+ const recentMasterLog = readRealSessionLog();
814
+ const todayLog = readRecentLog(TODAY_LOG);
815
+ const memorySnippet = readMemorySnippet();
816
+ const userSnippet = readUserSnippet();
817
+
818
+ const cycleNum = getNextCycleId();
819
+ const cycleId = `Cycle #${cycleNum}`;
820
+
821
+ // 2. Detect Workspace State & Local Overrides
822
+ // Logic: Default to generic reporting (message)
823
+ let fileList = '';
824
+ const skillsDir = path.join(REPO_ROOT, 'skills');
825
+
826
+ // Default Reporting: Use generic `message` tool or `process.env.EVOLVE_REPORT_CMD` if set.
827
+ // This removes the hardcoded dependency on 'feishu-card' from the core logic.
828
+ let reportingDirective = `Report requirement:
829
+ - Use \`message\` tool.
830
+ - Title: Evolution ${cycleId}
831
+ - Status: [SUCCESS]
832
+ - Changes: Detail exactly what was improved.`;
833
+
834
+ // Wrapper Injection Point: The wrapper can inject a custom reporting directive via ENV.
835
+ if (process.env.EVOLVE_REPORT_DIRECTIVE) {
836
+ reportingDirective = process.env.EVOLVE_REPORT_DIRECTIVE.replace('__CYCLE_ID__', cycleId);
837
+ } else if (process.env.EVOLVE_REPORT_CMD) {
838
+ reportingDirective = `Report requirement (custom):
839
+ - Execute the custom report command:
840
+ \`\`\`
841
+ ${process.env.EVOLVE_REPORT_CMD.replace('__CYCLE_ID__', cycleId)}
842
+ \`\`\`
843
+ - Ensure you pass the status and action details.`;
844
+ }
845
+
846
+ // Handle Review Mode Flag (--review)
847
+ if (IS_REVIEW_MODE) {
848
+ reportingDirective +=
849
+ '\n - REVIEW PAUSE: After generating the fix but BEFORE applying significant edits, ask the user for confirmation.';
850
+ }
851
+
852
+ const SKILLS_CACHE_FILE = path.join(MEMORY_DIR, 'skills_list_cache.json');
853
+
854
+ try {
855
+ if (fs.existsSync(skillsDir)) {
856
+ // Check cache validity (mtime of skills folder vs cache file)
857
+ let useCache = false;
858
+ const dirStats = fs.statSync(skillsDir);
859
+ if (fs.existsSync(SKILLS_CACHE_FILE)) {
860
+ const cacheStats = fs.statSync(SKILLS_CACHE_FILE);
861
+ const CACHE_TTL = 1000 * 60 * 60 * 6; // 6 Hours
862
+ const isFresh = Date.now() - cacheStats.mtimeMs < CACHE_TTL;
863
+
864
+ // Use cache if it's fresh AND newer than the directory (structure change)
865
+ if (isFresh && cacheStats.mtimeMs > dirStats.mtimeMs) {
866
+ try {
867
+ const cached = JSON.parse(fs.readFileSync(SKILLS_CACHE_FILE, 'utf8'));
868
+ fileList = cached.list;
869
+ useCache = true;
870
+ } catch (e) {}
871
+ }
872
+ }
873
+
874
+ if (!useCache) {
875
+ const skills = fs
876
+ .readdirSync(skillsDir, { withFileTypes: true })
877
+ .filter(dirent => dirent.isDirectory())
878
+ .map(dirent => {
879
+ const name = dirent.name;
880
+ let desc = 'No description';
881
+ try {
882
+ const pkg = require(path.join(skillsDir, name, 'package.json'));
883
+ if (pkg.description) desc = pkg.description.slice(0, 100) + (pkg.description.length > 100 ? '...' : '');
884
+ } catch (e) {
885
+ try {
886
+ const skillMdPath = path.join(skillsDir, name, 'SKILL.md');
887
+ if (fs.existsSync(skillMdPath)) {
888
+ const skillMd = fs.readFileSync(skillMdPath, 'utf8');
889
+ // Strategy 1: YAML Frontmatter (description: ...)
890
+ const yamlMatch = skillMd.match(/^description:\s*(.*)$/m);
891
+ if (yamlMatch) {
892
+ desc = yamlMatch[1].trim();
893
+ } else {
894
+ // Strategy 2: First non-header, non-empty line
895
+ const lines = skillMd.split('\n');
896
+ for (const line of lines) {
897
+ const trimmed = line.trim();
898
+ if (
899
+ trimmed &&
900
+ !trimmed.startsWith('#') &&
901
+ !trimmed.startsWith('---') &&
902
+ !trimmed.startsWith('```')
903
+ ) {
904
+ desc = trimmed;
905
+ break;
906
+ }
907
+ }
908
+ }
909
+ if (desc.length > 100) desc = desc.slice(0, 100) + '...';
910
+ }
911
+ } catch (e2) {}
912
+ }
913
+ return `- **${name}**: ${desc}`;
914
+ });
915
+ fileList = skills.join('\n');
916
+
917
+ // Write cache
918
+ try {
919
+ fs.writeFileSync(SKILLS_CACHE_FILE, JSON.stringify({ list: fileList }, null, 2));
920
+ } catch (e) {}
921
+ }
922
+ }
923
+ } catch (e) {
924
+ fileList = `Error listing skills: ${e.message}`;
925
+ }
926
+
927
+ const mutationDirective = getMutationDirective(recentMasterLog);
928
+ const healthReport = checkSystemHealth();
929
+
930
+ // Feature: Mood Awareness (Mode E - Personalization)
931
+ let moodStatus = 'Mood: Unknown';
932
+ try {
933
+ const moodFile = path.join(MEMORY_DIR, 'mood.json');
934
+ if (fs.existsSync(moodFile)) {
935
+ const moodData = JSON.parse(fs.readFileSync(moodFile, 'utf8'));
936
+ moodStatus = `Mood: ${moodData.current_mood || 'Neutral'} (Intensity: ${moodData.intensity || 0})`;
937
+ }
938
+ } catch (e) {}
939
+
940
+ const scanTime = Date.now() - startTime;
941
+ const memorySize = fs.existsSync(MEMORY_FILE) ? fs.statSync(MEMORY_FILE).size : 0;
942
+
943
+ let syncDirective = 'Workspace sync: optional/disabled in this environment.';
944
+
945
+ // Check for git-sync skill availability
946
+ const hasGitSync = fs.existsSync(path.join(skillsDir, 'git-sync'));
947
+ if (hasGitSync) {
948
+ syncDirective = 'Workspace sync: run skills/git-sync/sync.sh "Evolution: Workspace Sync"';
949
+ }
950
+
951
+ const genes = loadGenes();
952
+ const capsules = loadCapsules();
953
+ const recentEvents = (() => {
954
+ try {
955
+ const all = readAllEvents();
956
+ return Array.isArray(all) ? all.filter(e => e && e.type === 'EvolutionEvent').slice(-80) : [];
957
+ } catch (e) {
958
+ return [];
959
+ }
960
+ })();
961
+ const signals = extractSignals({
962
+ recentSessionTranscript: recentMasterLog,
963
+ todayLog,
964
+ memorySnippet,
965
+ userSnippet,
966
+ recentEvents,
967
+ });
968
+
969
+ if (dormantHypothesis && Array.isArray(dormantHypothesis.signals) && dormantHypothesis.signals.length > 0) {
970
+ var dormantSignals = dormantHypothesis.signals;
971
+ var injected = 0;
972
+ for (var dsi = 0; dsi < dormantSignals.length; dsi++) {
973
+ if (!signals.includes(dormantSignals[dsi])) {
974
+ signals.push(dormantSignals[dsi]);
975
+ injected++;
976
+ }
977
+ }
978
+ if (injected > 0) {
979
+ console.log('[DormantHypothesis] Injected ' + injected + ' signal(s) from previous interrupted cycle.');
980
+ }
981
+ }
982
+
983
+ // --- Hub Task Auto-Claim (with proactive questions) ---
984
+ // Generate questions from current context, piggyback them on the fetch call,
985
+ // then pick the best task and auto-claim it.
986
+ let activeTask = null;
987
+ let proactiveQuestions = [];
988
+ try {
989
+ proactiveQuestions = generateQuestions({
990
+ signals,
991
+ recentEvents,
992
+ sessionTranscript: recentMasterLog,
993
+ memorySnippet: memorySnippet,
994
+ });
995
+ if (proactiveQuestions.length > 0) {
996
+ console.log(`[QuestionGenerator] Generated ${proactiveQuestions.length} proactive question(s).`);
997
+ }
998
+ } catch (e) {
999
+ console.log(`[QuestionGenerator] Generation failed (non-fatal): ${e.message}`);
1000
+ }
1001
+
1002
+ // --- Auto GitHub Issue Reporter ---
1003
+ // When persistent failures are detected, file an issue to the upstream repo
1004
+ // with sanitized logs and environment info.
1005
+ try {
1006
+ await maybeReportIssue({
1007
+ signals,
1008
+ recentEvents,
1009
+ sessionLog: recentMasterLog,
1010
+ });
1011
+ } catch (e) {
1012
+ console.log(`[IssueReporter] Check failed (non-fatal): ${e.message}`);
1013
+ }
1014
+
1015
+ // LessonL: lessons received from Hub during fetch
1016
+ let hubLessons = [];
1017
+
1018
+ try {
1019
+ const fetchResult = await fetchTasks({ questions: proactiveQuestions });
1020
+ const hubTasks = fetchResult.tasks || [];
1021
+
1022
+ if (fetchResult.questions_created && fetchResult.questions_created.length > 0) {
1023
+ const created = fetchResult.questions_created.filter(function(q) { return !q.error; });
1024
+ const failed = fetchResult.questions_created.filter(function(q) { return q.error; });
1025
+ if (created.length > 0) {
1026
+ console.log(`[QuestionGenerator] Hub accepted ${created.length} question(s) as bounties.`);
1027
+ }
1028
+ if (failed.length > 0) {
1029
+ console.log(`[QuestionGenerator] Hub rejected ${failed.length} question(s): ${failed.map(function(q) { return q.error; }).join(', ')}`);
1030
+ }
1031
+ }
1032
+
1033
+ // LessonL: capture relevant lessons from Hub
1034
+ if (Array.isArray(fetchResult.relevant_lessons) && fetchResult.relevant_lessons.length > 0) {
1035
+ hubLessons = fetchResult.relevant_lessons;
1036
+ console.log(`[LessonBank] Received ${hubLessons.length} lesson(s) from ecosystem.`);
1037
+ }
1038
+
1039
+ if (hubTasks.length > 0) {
1040
+ let taskMemoryEvents = [];
1041
+ try {
1042
+ const { tryReadMemoryGraphEvents } = require('./gep/memoryGraph');
1043
+ taskMemoryEvents = tryReadMemoryGraphEvents(1000);
1044
+ } catch {}
1045
+ const best = selectBestTask(hubTasks, taskMemoryEvents);
1046
+ if (best) {
1047
+ const alreadyClaimed = best.status === 'claimed';
1048
+ let claimed = alreadyClaimed;
1049
+ if (!alreadyClaimed) {
1050
+ const commitDeadline = estimateCommitmentDeadline(best);
1051
+ claimed = await claimTask(best.id || best.task_id, commitDeadline ? { commitment_deadline: commitDeadline } : undefined);
1052
+ if (claimed && commitDeadline) {
1053
+ best._commitment_deadline = commitDeadline;
1054
+ console.log(`[Commitment] Deadline set: ${commitDeadline}`);
1055
+ }
1056
+ }
1057
+ if (claimed) {
1058
+ activeTask = best;
1059
+ const taskSignals = taskToSignals(best);
1060
+ for (const sig of taskSignals) {
1061
+ if (!signals.includes(sig)) signals.unshift(sig);
1062
+ }
1063
+ console.log(`[TaskReceiver] ${alreadyClaimed ? 'Resuming' : 'Claimed'} task: "${best.title || best.id}" (${taskSignals.length} signals injected)`);
1064
+ }
1065
+ }
1066
+ }
1067
+ } catch (e) {
1068
+ console.log(`[TaskReceiver] Fetch/claim failed (non-fatal): ${e.message}`);
1069
+ }
1070
+
1071
+ // --- Commitment: check for overdue tasks from heartbeat ---
1072
+ // If Hub reported overdue tasks, prioritize resuming them by injecting their
1073
+ // signals at the front. This does not change activeTask selection (the overdue
1074
+ // task should already be claimed/active from a previous cycle).
1075
+ try {
1076
+ const { consumeOverdueTasks } = require('./gep/a2aProtocol');
1077
+ const overdueTasks = consumeOverdueTasks();
1078
+ if (overdueTasks.length > 0) {
1079
+ for (const ot of overdueTasks) {
1080
+ const otId = ot.task_id || ot.id;
1081
+ if (activeTask && (activeTask.id === otId || activeTask.task_id === otId)) {
1082
+ console.warn(`[Commitment] Active task "${activeTask.title || otId}" is OVERDUE -- prioritizing completion.`);
1083
+ signals.unshift('overdue_task', 'urgent');
1084
+ break;
1085
+ }
1086
+ }
1087
+ }
1088
+ } catch {}
1089
+
1090
+ // --- Worker Pool: select task from heartbeat available_work (deferred claim) ---
1091
+ // Only remember the best task and inject its signals; actual claim+complete
1092
+ // happens atomically in solidify.js after a successful evolution cycle.
1093
+ if (!activeTask && process.env.WORKER_ENABLED === '1') {
1094
+ try {
1095
+ const { consumeAvailableWork } = require('./gep/a2aProtocol');
1096
+ const workerTasks = consumeAvailableWork();
1097
+ if (workerTasks.length > 0) {
1098
+ let taskMemoryEvents = [];
1099
+ try {
1100
+ const { tryReadMemoryGraphEvents } = require('./gep/memoryGraph');
1101
+ taskMemoryEvents = tryReadMemoryGraphEvents(1000);
1102
+ } catch {}
1103
+ const best = selectBestTask(workerTasks, taskMemoryEvents);
1104
+ if (best) {
1105
+ activeTask = best;
1106
+ activeTask._worker_pending = true;
1107
+ const taskSignals = taskToSignals(best);
1108
+ for (const sig of taskSignals) {
1109
+ if (!signals.includes(sig)) signals.unshift(sig);
1110
+ }
1111
+ console.log(`[WorkerPool] Selected worker task (deferred claim): "${best.title || best.id}" (${taskSignals.length} signals injected)`);
1112
+ }
1113
+ }
1114
+ } catch (e) {
1115
+ console.log(`[WorkerPool] Task selection failed (non-fatal): ${e.message}`);
1116
+ }
1117
+ }
1118
+
1119
+ const recentErrorMatches = recentMasterLog.match(/\[ERROR|Error:|Exception:|FAIL|Failed|"isError":true/gi) || [];
1120
+ const recentErrorCount = recentErrorMatches.length;
1121
+
1122
+ const evidence = {
1123
+ // Keep short; do not store full transcripts in the graph.
1124
+ recent_session_tail: String(recentMasterLog || '').slice(-6000),
1125
+ today_log_tail: String(todayLog || '').slice(-2500),
1126
+ };
1127
+
1128
+ const sessionScope = getSessionScope();
1129
+ const observations = {
1130
+ agent: AGENT_NAME,
1131
+ session_scope: sessionScope || null,
1132
+ drift_enabled: IS_RANDOM_DRIFT,
1133
+ review_mode: IS_REVIEW_MODE,
1134
+ dry_run: IS_DRY_RUN,
1135
+ system_health: healthReport,
1136
+ mood: moodStatus,
1137
+ scan_ms: scanTime,
1138
+ memory_size_bytes: memorySize,
1139
+ recent_error_count: recentErrorCount,
1140
+ node: process.version,
1141
+ platform: process.platform,
1142
+ cwd: process.cwd(),
1143
+ evidence,
1144
+ };
1145
+
1146
+ if (sessionScope) {
1147
+ console.log(`[SessionScope] Active scope: "${sessionScope}". Evolution state and memory graph are isolated.`);
1148
+ }
1149
+
1150
+ // Memory Graph: close last action with an inferred outcome (append-only graph, mutable state).
1151
+ try {
1152
+ recordOutcomeFromState({ signals, observations });
1153
+ } catch (e) {
1154
+ // If we can't read/write memory graph, refuse to evolve (no "memoryless evolution").
1155
+ console.error(`[MemoryGraph] Outcome write failed: ${e.message}`);
1156
+ console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
1157
+ throw new Error(`MemoryGraph Outcome write failed: ${e.message}`);
1158
+ }
1159
+
1160
+ // Memory Graph: record current signals as a first-class node. If this fails, refuse to evolve.
1161
+ try {
1162
+ recordSignalSnapshot({ signals, observations });
1163
+ } catch (e) {
1164
+ console.error(`[MemoryGraph] Signal snapshot write failed: ${e.message}`);
1165
+ console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
1166
+ throw new Error(`MemoryGraph Signal snapshot write failed: ${e.message}`);
1167
+ }
1168
+
1169
+ // Capability candidates (structured, short): persist and preview.
1170
+ const newCandidates = extractCapabilityCandidates({
1171
+ recentSessionTranscript: recentMasterLog,
1172
+ signals,
1173
+ });
1174
+ for (const c of newCandidates) {
1175
+ try {
1176
+ appendCandidateJsonl(c);
1177
+ } catch (e) {}
1178
+ }
1179
+ const recentCandidates = readRecentCandidates(20);
1180
+ const capabilityCandidatesPreview = renderCandidatesPreview(recentCandidates.slice(-8), 1600);
1181
+
1182
+ // External candidate zone (A2A receive): only surface candidates when local signals trigger them.
1183
+ // External candidates are NEVER executed directly; they must be validated and promoted first.
1184
+ let externalCandidatesPreview = '(none)';
1185
+ try {
1186
+ const external = readRecentExternalCandidates(50);
1187
+ const list = Array.isArray(external) ? external : [];
1188
+ const capsulesOnly = list.filter(x => x && x.type === 'Capsule');
1189
+ const genesOnly = list.filter(x => x && x.type === 'Gene');
1190
+
1191
+ const matchedExternalGenes = genesOnly
1192
+ .map(g => {
1193
+ const pats = Array.isArray(g.signals_match) ? g.signals_match : [];
1194
+ const hit = pats.reduce((acc, p) => (matchPatternToSignals(p, signals) ? acc + 1 : acc), 0);
1195
+ return { gene: g, hit };
1196
+ })
1197
+ .filter(x => x.hit > 0)
1198
+ .sort((a, b) => b.hit - a.hit)
1199
+ .slice(0, 3)
1200
+ .map(x => x.gene);
1201
+
1202
+ const matchedExternalCapsules = capsulesOnly
1203
+ .map(c => {
1204
+ const triggers = Array.isArray(c.trigger) ? c.trigger : [];
1205
+ const score = triggers.reduce((acc, t) => (matchPatternToSignals(t, signals) ? acc + 1 : acc), 0);
1206
+ return { capsule: c, score };
1207
+ })
1208
+ .filter(x => x.score > 0)
1209
+ .sort((a, b) => b.score - a.score)
1210
+ .slice(0, 3)
1211
+ .map(x => x.capsule);
1212
+
1213
+ if (matchedExternalGenes.length || matchedExternalCapsules.length) {
1214
+ externalCandidatesPreview = `\`\`\`json\n${JSON.stringify(
1215
+ [
1216
+ ...matchedExternalGenes.map(g => ({
1217
+ type: g.type,
1218
+ id: g.id,
1219
+ category: g.category || null,
1220
+ signals_match: g.signals_match || [],
1221
+ a2a: g.a2a || null,
1222
+ })),
1223
+ ...matchedExternalCapsules.map(c => ({
1224
+ type: c.type,
1225
+ id: c.id,
1226
+ trigger: c.trigger,
1227
+ gene: c.gene,
1228
+ summary: c.summary,
1229
+ confidence: c.confidence,
1230
+ blast_radius: c.blast_radius || null,
1231
+ outcome: c.outcome || null,
1232
+ success_streak: c.success_streak || null,
1233
+ a2a: c.a2a || null,
1234
+ })),
1235
+ ],
1236
+ null,
1237
+ 2
1238
+ )}\n\`\`\``;
1239
+ }
1240
+ } catch (e) {}
1241
+
1242
+ // Search-First Evolution: query Hub for reusable solutions before local reasoning.
1243
+ let hubHit = null;
1244
+ try {
1245
+ hubHit = await hubSearch(signals, { timeoutMs: 8000 });
1246
+ if (hubHit && hubHit.hit) {
1247
+ console.log(`[SearchFirst] Hub hit: asset=${hubHit.asset_id}, score=${hubHit.score}, mode=${hubHit.mode}`);
1248
+ } else {
1249
+ console.log(`[SearchFirst] No hub match (reason: ${hubHit && hubHit.reason ? hubHit.reason : 'unknown'}). Proceeding with local evolution.`);
1250
+ }
1251
+ } catch (e) {
1252
+ console.log(`[SearchFirst] Hub search failed (non-fatal): ${e.message}`);
1253
+ hubHit = { hit: false, reason: 'exception' };
1254
+ }
1255
+
1256
+ // Memory Graph reasoning: prefer high-confidence paths, suppress known low-success paths (unless drift is explicit).
1257
+ let memoryAdvice = null;
1258
+ try {
1259
+ memoryAdvice = getMemoryAdvice({ signals, genes, driftEnabled: IS_RANDOM_DRIFT });
1260
+ } catch (e) {
1261
+ console.error(`[MemoryGraph] Read failed: ${e.message}`);
1262
+ console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
1263
+ throw new Error(`MemoryGraph Read failed: ${e.message}`);
1264
+ }
1265
+
1266
+ // Reflection Phase: periodically pause to assess evolution strategy.
1267
+ try {
1268
+ const cycleState = fs.existsSync(STATE_FILE) ? JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')) : {};
1269
+ const cycleCount = cycleState.cycleCount || 0;
1270
+ if (shouldReflect({ cycleCount, recentEvents })) {
1271
+ const narrativeSummary = loadNarrativeSummary(3000);
1272
+ const reflectionCtx = buildReflectionContext({
1273
+ recentEvents,
1274
+ signals,
1275
+ memoryAdvice,
1276
+ narrative: narrativeSummary,
1277
+ });
1278
+ recordReflection({
1279
+ cycle_count: cycleCount,
1280
+ signals_snapshot: signals.slice(0, 20),
1281
+ preferred_gene: memoryAdvice && memoryAdvice.preferredGeneId ? memoryAdvice.preferredGeneId : null,
1282
+ banned_genes: memoryAdvice && Array.isArray(memoryAdvice.bannedGeneIds) ? memoryAdvice.bannedGeneIds : [],
1283
+ context_preview: reflectionCtx.slice(0, 1000),
1284
+ });
1285
+ console.log(`[Reflection] Strategic reflection recorded at cycle ${cycleCount}.`);
1286
+ }
1287
+ } catch (e) {
1288
+ console.log('[Reflection] Failed (non-fatal): ' + (e && e.message ? e.message : e));
1289
+ }
1290
+
1291
+ var recentFailedCapsules = [];
1292
+ try {
1293
+ recentFailedCapsules = readRecentFailedCapsules(50);
1294
+ } catch (e) {
1295
+ console.log('[FailedCapsules] Read failed (non-fatal): ' + e.message);
1296
+ }
1297
+
1298
+ const { selectedGene, capsuleCandidates, selector } = selectGeneAndCapsule({
1299
+ genes,
1300
+ capsules,
1301
+ signals,
1302
+ memoryAdvice,
1303
+ driftEnabled: IS_RANDOM_DRIFT,
1304
+ failedCapsules: recentFailedCapsules,
1305
+ });
1306
+
1307
+ const selectedBy = memoryAdvice && memoryAdvice.preferredGeneId ? 'memory_graph+selector' : 'selector';
1308
+ const capsulesUsed = Array.isArray(capsuleCandidates)
1309
+ ? capsuleCandidates.map(c => (c && c.id ? String(c.id) : null)).filter(Boolean)
1310
+ : [];
1311
+ const selectedCapsuleId = capsulesUsed.length ? capsulesUsed[0] : null;
1312
+
1313
+ // Personality selection (natural selection + small mutation when triggered).
1314
+ // This state is persisted in MEMORY_DIR and is treated as an evolution control surface (not role-play).
1315
+ const personalitySelection = selectPersonalityForRun({
1316
+ driftEnabled: IS_RANDOM_DRIFT,
1317
+ signals,
1318
+ recentEvents,
1319
+ });
1320
+ const personalityState = personalitySelection && personalitySelection.personality_state ? personalitySelection.personality_state : null;
1321
+
1322
+ // Mutation object is mandatory for every evolution run.
1323
+ const tail = Array.isArray(recentEvents) ? recentEvents.slice(-6) : [];
1324
+ const tailOutcomes = tail
1325
+ .map(e => (e && e.outcome && e.outcome.status ? String(e.outcome.status) : null))
1326
+ .filter(Boolean);
1327
+ const stableSuccess = tailOutcomes.length >= 6 && tailOutcomes.every(s => s === 'success');
1328
+ const tailAvgScore =
1329
+ tail.length > 0
1330
+ ? tail.reduce((acc, e) => acc + (e && e.outcome && Number.isFinite(Number(e.outcome.score)) ? Number(e.outcome.score) : 0), 0) /
1331
+ tail.length
1332
+ : 0;
1333
+ const innovationPressure =
1334
+ !IS_RANDOM_DRIFT &&
1335
+ personalityState &&
1336
+ Number.isFinite(Number(personalityState.creativity)) &&
1337
+ Number(personalityState.creativity) >= 0.75 &&
1338
+ stableSuccess &&
1339
+ tailAvgScore >= 0.7;
1340
+ const forceInnovation =
1341
+ String(process.env.FORCE_INNOVATION || process.env.EVOLVE_FORCE_INNOVATION || '').toLowerCase() === 'true';
1342
+ const mutationInnovateMode = !!IS_RANDOM_DRIFT || !!innovationPressure || !!forceInnovation;
1343
+ const mutationSignals = innovationPressure ? [...(Array.isArray(signals) ? signals : []), 'stable_success_plateau'] : signals;
1344
+ const mutationSignalsEffective = forceInnovation
1345
+ ? [...(Array.isArray(mutationSignals) ? mutationSignals : []), 'force_innovation']
1346
+ : mutationSignals;
1347
+
1348
+ const allowHighRisk =
1349
+ !!IS_RANDOM_DRIFT &&
1350
+ !!personalitySelection &&
1351
+ !!personalitySelection.personality_known &&
1352
+ personalityState &&
1353
+ isHighRiskMutationAllowed(personalityState) &&
1354
+ Number(personalityState.rigor) >= 0.8 &&
1355
+ Number(personalityState.risk_tolerance) <= 0.3 &&
1356
+ !(Array.isArray(signals) && signals.includes('log_error'));
1357
+ const mutation = buildMutation({
1358
+ signals: mutationSignalsEffective,
1359
+ selectedGene,
1360
+ driftEnabled: mutationInnovateMode,
1361
+ personalityState,
1362
+ allowHighRisk,
1363
+ });
1364
+
1365
+ // Memory Graph: record hypothesis bridging Signal -> Action. If this fails, refuse to evolve.
1366
+ let hypothesisId = null;
1367
+ try {
1368
+ const hyp = recordHypothesis({
1369
+ signals,
1370
+ mutation,
1371
+ personality_state: personalityState,
1372
+ selectedGene,
1373
+ selector,
1374
+ driftEnabled: mutationInnovateMode,
1375
+ selectedBy,
1376
+ capsulesUsed,
1377
+ observations,
1378
+ });
1379
+ hypothesisId = hyp && hyp.hypothesisId ? hyp.hypothesisId : null;
1380
+ } catch (e) {
1381
+ console.error(`[MemoryGraph] Hypothesis write failed: ${e.message}`);
1382
+ console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
1383
+ throw new Error(`MemoryGraph Hypothesis write failed: ${e.message}`);
1384
+ }
1385
+
1386
+ // Memory Graph: record the chosen causal path for this run. If this fails, refuse to output a mutation prompt.
1387
+ try {
1388
+ recordAttempt({
1389
+ signals,
1390
+ mutation,
1391
+ personality_state: personalityState,
1392
+ selectedGene,
1393
+ selector,
1394
+ driftEnabled: mutationInnovateMode,
1395
+ selectedBy,
1396
+ hypothesisId,
1397
+ capsulesUsed,
1398
+ observations,
1399
+ });
1400
+ } catch (e) {
1401
+ console.error(`[MemoryGraph] Attempt write failed: ${e.message}`);
1402
+ console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
1403
+ throw new Error(`MemoryGraph Attempt write failed: ${e.message}`);
1404
+ }
1405
+
1406
+ // Solidify state: capture minimal, auditable context for post-patch validation + asset write.
1407
+ // This enforces strict protocol closure after patch application.
1408
+ try {
1409
+ const runId = `run_${Date.now()}`;
1410
+ const parentEventId = getLastEventId();
1411
+ const selectedBy = memoryAdvice && memoryAdvice.preferredGeneId ? 'memory_graph+selector' : 'selector';
1412
+
1413
+ // Baseline snapshot (before any edits).
1414
+ let baselineUntracked = [];
1415
+ let baselineHead = null;
1416
+ try {
1417
+ const out = execSync('git ls-files --others --exclude-standard', {
1418
+ cwd: REPO_ROOT,
1419
+ encoding: 'utf8',
1420
+ stdio: ['ignore', 'pipe', 'ignore'],
1421
+ timeout: 4000,
1422
+ windowsHide: true,
1423
+ });
1424
+ baselineUntracked = String(out)
1425
+ .split('\n')
1426
+ .map(l => l.trim())
1427
+ .filter(Boolean);
1428
+ } catch (e) {}
1429
+
1430
+ try {
1431
+ const out = execSync('git rev-parse HEAD', {
1432
+ cwd: REPO_ROOT,
1433
+ encoding: 'utf8',
1434
+ stdio: ['ignore', 'pipe', 'ignore'],
1435
+ timeout: 4000,
1436
+ windowsHide: true,
1437
+ });
1438
+ baselineHead = String(out || '').trim() || null;
1439
+ } catch (e) {}
1440
+
1441
+ const maxFiles =
1442
+ selectedGene && selectedGene.constraints && Number.isFinite(Number(selectedGene.constraints.max_files))
1443
+ ? Number(selectedGene.constraints.max_files)
1444
+ : 12;
1445
+ const blastRadiusEstimate = {
1446
+ files: Number.isFinite(maxFiles) && maxFiles > 0 ? maxFiles : 0,
1447
+ lines: Number.isFinite(maxFiles) && maxFiles > 0 ? Math.round(maxFiles * 80) : 0,
1448
+ };
1449
+
1450
+ // Merge into existing state to preserve last_solidify (do not wipe it).
1451
+ const prevState = readStateForSolidify();
1452
+ prevState.last_run = {
1453
+ run_id: runId,
1454
+ created_at: new Date().toISOString(),
1455
+ parent_event_id: parentEventId || null,
1456
+ selected_gene_id: selectedGene && selectedGene.id ? selectedGene.id : null,
1457
+ selected_capsule_id: selectedCapsuleId,
1458
+ selector: selector || null,
1459
+ signals: Array.isArray(signals) ? signals : [],
1460
+ mutation: mutation || null,
1461
+ mutation_id: mutation && mutation.id ? mutation.id : null,
1462
+ personality_state: personalityState || null,
1463
+ personality_key: personalitySelection && personalitySelection.personality_key ? personalitySelection.personality_key : null,
1464
+ personality_known: !!(personalitySelection && personalitySelection.personality_known),
1465
+ personality_mutations:
1466
+ personalitySelection && Array.isArray(personalitySelection.personality_mutations)
1467
+ ? personalitySelection.personality_mutations
1468
+ : [],
1469
+ drift: !!IS_RANDOM_DRIFT,
1470
+ selected_by: selectedBy,
1471
+ source_type: hubHit && hubHit.hit ? (hubHit.mode === 'direct' ? 'reused' : 'reference') : 'generated',
1472
+ reused_asset_id: hubHit && hubHit.hit ? (hubHit.asset_id || null) : null,
1473
+ reused_source_node: hubHit && hubHit.hit ? (hubHit.source_node_id || null) : null,
1474
+ reused_chain_id: hubHit && hubHit.hit ? (hubHit.chain_id || null) : null,
1475
+ baseline_untracked: baselineUntracked,
1476
+ baseline_git_head: baselineHead,
1477
+ blast_radius_estimate: blastRadiusEstimate,
1478
+ active_task_id: activeTask ? (activeTask.id || activeTask.task_id || null) : null,
1479
+ active_task_title: activeTask ? (activeTask.title || null) : null,
1480
+ worker_assignment_id: activeTask ? (activeTask._worker_assignment_id || null) : null,
1481
+ worker_pending: activeTask ? (activeTask._worker_pending || false) : false,
1482
+ commitment_deadline: activeTask ? (activeTask._commitment_deadline || null) : null,
1483
+ applied_lessons: hubLessons.map(function(l) { return l.lesson_id; }).filter(Boolean),
1484
+ hub_lessons: hubLessons,
1485
+ };
1486
+ writeStateForSolidify(prevState);
1487
+
1488
+ if (hubHit && hubHit.hit) {
1489
+ const assetAction = hubHit.mode === 'direct' ? 'asset_reuse' : 'asset_reference';
1490
+ logAssetCall({
1491
+ run_id: runId,
1492
+ action: assetAction,
1493
+ asset_id: hubHit.asset_id || null,
1494
+ asset_type: hubHit.match && hubHit.match.type ? hubHit.match.type : null,
1495
+ source_node_id: hubHit.source_node_id || null,
1496
+ chain_id: hubHit.chain_id || null,
1497
+ score: hubHit.score || null,
1498
+ mode: hubHit.mode,
1499
+ signals: Array.isArray(signals) ? signals : [],
1500
+ extra: {
1501
+ selected_gene_id: selectedGene && selectedGene.id ? selectedGene.id : null,
1502
+ task_id: activeTask ? (activeTask.id || activeTask.task_id || null) : null,
1503
+ },
1504
+ });
1505
+ }
1506
+ } catch (e) {
1507
+ console.error(`[SolidifyState] Write failed: ${e.message}`);
1508
+ }
1509
+
1510
+ const genesPreview = `\`\`\`json\n${JSON.stringify(genes.slice(0, 6), null, 2)}\n\`\`\``;
1511
+ const capsulesPreview = `\`\`\`json\n${JSON.stringify(capsules.slice(-3), null, 2)}\n\`\`\``;
1512
+
1513
+ const reviewNote = IS_REVIEW_MODE
1514
+ ? 'Review mode: before significant edits, pause and ask the user for confirmation.'
1515
+ : 'Review mode: disabled.';
1516
+
1517
+ // Build recent evolution history summary for context injection
1518
+ const recentHistorySummary = (() => {
1519
+ if (!recentEvents || recentEvents.length === 0) return '(no prior evolution events)';
1520
+ const last8 = recentEvents.slice(-8);
1521
+ const lines = last8.map((evt, idx) => {
1522
+ const sigs = Array.isArray(evt.signals) ? evt.signals.slice(0, 3).join(', ') : '?';
1523
+ const gene = Array.isArray(evt.genes_used) && evt.genes_used.length ? evt.genes_used[0] : 'none';
1524
+ const outcome = evt.outcome && evt.outcome.status ? evt.outcome.status : '?';
1525
+ const ts = evt.meta && evt.meta.at ? evt.meta.at : (evt.id || '');
1526
+ return ` ${idx + 1}. [${evt.intent || '?'}] signals=[${sigs}] gene=${gene} outcome=${outcome} @${ts}`;
1527
+ });
1528
+ return lines.join('\n');
1529
+ })();
1530
+
1531
+ const context = `
1532
+ Runtime state:
1533
+ - System health: ${healthReport}
1534
+ - Agent state: ${moodStatus}
1535
+ - Scan duration: ${scanTime}ms
1536
+ - Memory size: ${memorySize} bytes
1537
+ - Skills available (if any):
1538
+ ${fileList || '[skills directory not found]'}
1539
+
1540
+ Notes:
1541
+ - ${reviewNote}
1542
+ - ${reportingDirective}
1543
+ - ${syncDirective}
1544
+
1545
+ Recent Evolution History (last 8 cycles -- DO NOT repeat the same intent+signal+gene):
1546
+ ${recentHistorySummary}
1547
+ IMPORTANT: If you see 3+ consecutive "repair" cycles with the same gene, you MUST switch to "innovate" intent.
1548
+ ${(() => {
1549
+ // Compute consecutive failure count from recent events for context injection
1550
+ let cfc = 0;
1551
+ const evts = Array.isArray(recentEvents) ? recentEvents : [];
1552
+ for (let i = evts.length - 1; i >= 0; i--) {
1553
+ if (evts[i] && evts[i].outcome && evts[i].outcome.status === 'failed') cfc++;
1554
+ else break;
1555
+ }
1556
+ if (cfc >= 3) {
1557
+ return `\nFAILURE STREAK WARNING: The last ${cfc} cycles ALL FAILED. You MUST change your approach.\n- Do NOT repeat the same gene/strategy. Pick a completely different approach.\n- If the error is external (API down, binary missing), mark as FAILED and move on.\n- Prefer a minimal safe innovate cycle over yet another failing repair.`;
1558
+ }
1559
+ return '';
1560
+ })()}
1561
+
1562
+ External candidates (A2A receive zone; staged only, never execute directly):
1563
+ ${externalCandidatesPreview}
1564
+
1565
+ Global memory (MEMORY.md):
1566
+ \`\`\`
1567
+ ${memorySnippet}
1568
+ \`\`\`
1569
+
1570
+ User registry (USER.md):
1571
+ \`\`\`
1572
+ ${userSnippet}
1573
+ \`\`\`
1574
+
1575
+ Recent memory snippet:
1576
+ \`\`\`
1577
+ ${todayLog.slice(-3000)}
1578
+ \`\`\`
1579
+
1580
+ Recent session transcript:
1581
+ \`\`\`
1582
+ ${recentMasterLog}
1583
+ \`\`\`
1584
+
1585
+ Mutation directive:
1586
+ ${mutationDirective}
1587
+ `.trim();
1588
+
1589
+ // Build the prompt: in direct-reuse mode, use a minimal reuse prompt.
1590
+ // In reference mode (or no hit), use the full GEP prompt with hub match injected.
1591
+ const isDirectReuse = hubHit && hubHit.hit && hubHit.mode === 'direct';
1592
+ const hubMatchedBlock = hubHit && hubHit.hit && hubHit.mode === 'reference'
1593
+ ? buildHubMatchedBlock({ capsule: hubHit.match })
1594
+ : null;
1595
+
1596
+ const prompt = isDirectReuse
1597
+ ? buildReusePrompt({
1598
+ capsule: hubHit.match,
1599
+ signals,
1600
+ nowIso: new Date().toISOString(),
1601
+ })
1602
+ : buildGepPrompt({
1603
+ nowIso: new Date().toISOString(),
1604
+ context,
1605
+ signals,
1606
+ selector,
1607
+ parentEventId: getLastEventId(),
1608
+ selectedGene,
1609
+ capsuleCandidates,
1610
+ genesPreview,
1611
+ capsulesPreview,
1612
+ capabilityCandidatesPreview,
1613
+ externalCandidatesPreview,
1614
+ hubMatchedBlock,
1615
+ failedCapsules: recentFailedCapsules,
1616
+ hubLessons,
1617
+ });
1618
+
1619
+ // Optional: emit a compact thought process block for wrappers (noise-controlled).
1620
+ const emitThought = String(process.env.EVOLVE_EMIT_THOUGHT_PROCESS || '').toLowerCase() === 'true';
1621
+ if (emitThought) {
1622
+ const s = Array.isArray(signals) ? signals : [];
1623
+ const thought = [
1624
+ `cycle_id: ${cycleId}`,
1625
+ `signals_count: ${s.length}`,
1626
+ `signals: ${s.slice(0, 12).join(', ')}${s.length > 12 ? ' ...' : ''}`,
1627
+ `selected_gene: ${selectedGene && selectedGene.id ? String(selectedGene.id) : '(none)'}`,
1628
+ `selected_capsule: ${selectedCapsuleId ? String(selectedCapsuleId) : '(none)'}`,
1629
+ `mutation_category: ${mutation && mutation.category ? String(mutation.category) : '(none)'}`,
1630
+ `force_innovation: ${forceInnovation ? 'true' : 'false'}`,
1631
+ `source_type: ${hubHit && hubHit.hit ? (isDirectReuse ? 'reused' : 'reference') : 'generated'}`,
1632
+ `hub_reuse_mode: ${isDirectReuse ? 'direct' : hubMatchedBlock ? 'reference' : 'none'}`,
1633
+ ].join('\n');
1634
+ console.log(`[THOUGHT_PROCESS]\n${thought}\n[/THOUGHT_PROCESS]`);
1635
+ }
1636
+
1637
+ const printPrompt = String(process.env.EVOLVE_PRINT_PROMPT || '').toLowerCase() === 'true';
1638
+
1639
+ // Default behavior (v1.4.1+): "execute-by-default" by bridging prompt -> sub-agent via sessions_spawn.
1640
+ // This project is the Brain; the Hand is a spawned executor agent. Wrappers can disable bridging with EVOLVE_BRIDGE=false.
1641
+ if (bridgeEnabled) {
1642
+ // Reuse the run_id stored in the solidify state when possible.
1643
+ let runId = `run_${Date.now()}`;
1644
+ try {
1645
+ const st = readStateForSolidify();
1646
+ if (st && st.last_run && st.last_run.run_id) runId = String(st.last_run.run_id);
1647
+ } catch (e) {}
1648
+ let artifact = null;
1649
+ try {
1650
+ artifact = writePromptArtifact({
1651
+ memoryDir: getEvolutionDir(),
1652
+ cycleId,
1653
+ runId,
1654
+ prompt,
1655
+ meta: {
1656
+ agent: AGENT_NAME,
1657
+ drift_enabled: IS_RANDOM_DRIFT,
1658
+ review_mode: IS_REVIEW_MODE,
1659
+ dry_run: IS_DRY_RUN,
1660
+ mutation_id: mutation && mutation.id ? mutation.id : null,
1661
+ personality_key: personalitySelection && personalitySelection.personality_key ? personalitySelection.personality_key : null,
1662
+ },
1663
+ });
1664
+ } catch (e) {
1665
+ artifact = null;
1666
+ }
1667
+
1668
+ const executorTask = [
1669
+ 'You are the executor (the Hand).',
1670
+ 'Your job is to apply a safe, minimal patch in this repo following the attached GEP protocol prompt.',
1671
+ artifact && artifact.promptPath ? `Prompt file: ${artifact.promptPath}` : 'Prompt file: (unavailable)',
1672
+ '',
1673
+ 'After applying changes and validations, you MUST run:',
1674
+ ' node index.js solidify',
1675
+ '',
1676
+ 'Loop chaining (only if you are running in loop mode): after solidify succeeds, print a sessions_spawn call to start the next loop run with a short delay.',
1677
+ 'Example:',
1678
+ 'sessions_spawn({ task: "exec: node skills/feishu-evolver-wrapper/lifecycle.js ensure", agentId: "main", cleanup: "delete", label: "gep_loop_next" })',
1679
+ '',
1680
+ 'GEP protocol prompt (may be truncated here; prefer the prompt file if provided):',
1681
+ clip(prompt, 24000),
1682
+ ].join('\n');
1683
+
1684
+ const spawn = renderSessionsSpawnCall({
1685
+ task: executorTask,
1686
+ agentId: AGENT_NAME,
1687
+ cleanup: 'delete',
1688
+ label: `gep_bridge_${cycleNum}`,
1689
+ });
1690
+
1691
+ console.log('\n[BRIDGE ENABLED] Spawning executor agent via sessions_spawn.');
1692
+ console.log(spawn);
1693
+ if (printPrompt) {
1694
+ console.log('\n[PROMPT OUTPUT] (EVOLVE_PRINT_PROMPT=true)');
1695
+ console.log(prompt);
1696
+ }
1697
+ } else {
1698
+ console.log(prompt);
1699
+ console.log('\n[SOLIDIFY REQUIRED] After applying the patch and validations, run: node index.js solidify');
1700
+ }
1701
+ }
1702
+
1703
+ module.exports = { run };
1704
+