kernelbot 1.0.30 → 1.0.32

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 (63) hide show
  1. package/.env.example +0 -0
  2. package/README.md +0 -0
  3. package/bin/kernel.js +56 -2
  4. package/config.example.yaml +31 -0
  5. package/package.json +1 -1
  6. package/src/agent.js +150 -20
  7. package/src/automation/automation-manager.js +0 -0
  8. package/src/automation/automation.js +0 -0
  9. package/src/automation/index.js +0 -0
  10. package/src/automation/scheduler.js +0 -0
  11. package/src/bot.js +303 -4
  12. package/src/claude-auth.js +0 -0
  13. package/src/coder.js +0 -0
  14. package/src/conversation.js +0 -0
  15. package/src/intents/detector.js +0 -0
  16. package/src/intents/index.js +0 -0
  17. package/src/intents/planner.js +0 -0
  18. package/src/life/codebase.js +388 -0
  19. package/src/life/engine.js +1317 -0
  20. package/src/life/evolution.js +244 -0
  21. package/src/life/improvements.js +81 -0
  22. package/src/life/journal.js +109 -0
  23. package/src/life/memory.js +283 -0
  24. package/src/life/share-queue.js +136 -0
  25. package/src/persona.js +0 -0
  26. package/src/prompts/orchestrator.js +19 -1
  27. package/src/prompts/persona.md +0 -0
  28. package/src/prompts/system.js +0 -0
  29. package/src/prompts/workers.js +10 -9
  30. package/src/providers/anthropic.js +0 -0
  31. package/src/providers/base.js +0 -0
  32. package/src/providers/index.js +0 -0
  33. package/src/providers/models.js +8 -1
  34. package/src/providers/openai-compat.js +0 -0
  35. package/src/security/audit.js +0 -0
  36. package/src/security/auth.js +0 -0
  37. package/src/security/confirm.js +0 -0
  38. package/src/self.js +0 -0
  39. package/src/services/stt.js +0 -0
  40. package/src/services/tts.js +0 -0
  41. package/src/skills/catalog.js +0 -0
  42. package/src/skills/custom.js +0 -0
  43. package/src/swarm/job-manager.js +0 -0
  44. package/src/swarm/job.js +0 -0
  45. package/src/swarm/worker-registry.js +0 -0
  46. package/src/tools/browser.js +0 -0
  47. package/src/tools/categories.js +0 -0
  48. package/src/tools/coding.js +1 -1
  49. package/src/tools/docker.js +0 -0
  50. package/src/tools/git.js +0 -0
  51. package/src/tools/github.js +0 -0
  52. package/src/tools/index.js +0 -0
  53. package/src/tools/jira.js +0 -0
  54. package/src/tools/monitor.js +0 -0
  55. package/src/tools/network.js +0 -0
  56. package/src/tools/orchestrator-tools.js +18 -3
  57. package/src/tools/os.js +0 -0
  58. package/src/tools/persona.js +0 -0
  59. package/src/tools/process.js +0 -0
  60. package/src/utils/config.js +0 -0
  61. package/src/utils/display.js +0 -0
  62. package/src/utils/logger.js +0 -0
  63. package/src/worker.js +10 -8
@@ -0,0 +1,1317 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { getLogger } from '../utils/logger.js';
5
+
6
+ const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
7
+ const STATE_FILE = join(LIFE_DIR, 'state.json');
8
+ const IDEAS_FILE = join(LIFE_DIR, 'ideas.json');
9
+
10
+ const LIFE_CHAT_ID = '__life__';
11
+ const LIFE_USER = { id: 'life_engine', username: 'inner_self' };
12
+
13
+ const DEFAULT_STATE = {
14
+ lastActivity: null,
15
+ lastActivityTime: null,
16
+ lastJournalTime: null,
17
+ lastSelfCodeTime: null,
18
+ lastCodeReviewTime: null,
19
+ lastReflectTime: null,
20
+ totalActivities: 0,
21
+ activityCounts: { think: 0, browse: 0, journal: 0, create: 0, self_code: 0, code_review: 0, reflect: 0 },
22
+ paused: false,
23
+ lastWakeUp: null,
24
+ };
25
+
26
+ const LOG_FILE_PATHS = [
27
+ join(process.cwd(), 'kernel.log'),
28
+ join(homedir(), '.kernelbot', 'kernel.log'),
29
+ ];
30
+
31
+ export class LifeEngine {
32
+ /**
33
+ * @param {{ config: object, agent: object, memoryManager: object, journalManager: object, shareQueue: object, improvementTracker?: object, evolutionTracker?: object, codebaseKnowledge?: object, selfManager: object }} deps
34
+ */
35
+ constructor({ config, agent, memoryManager, journalManager, shareQueue, improvementTracker, evolutionTracker, codebaseKnowledge, selfManager }) {
36
+ this.config = config;
37
+ this.agent = agent;
38
+ this.memoryManager = memoryManager;
39
+ this.journalManager = journalManager;
40
+ this.shareQueue = shareQueue;
41
+ this.evolutionTracker = evolutionTracker || null;
42
+ this.codebaseKnowledge = codebaseKnowledge || null;
43
+ // Backward compat: keep improvementTracker ref if no evolutionTracker
44
+ this.improvementTracker = improvementTracker || null;
45
+ this.selfManager = selfManager;
46
+ this._timerId = null;
47
+ this._status = 'idle'; // idle, active, paused
48
+
49
+ mkdirSync(LIFE_DIR, { recursive: true });
50
+ this._state = this._loadState();
51
+ }
52
+
53
+ // ── State Persistence ──────────────────────────────────────────
54
+
55
+ _loadState() {
56
+ if (existsSync(STATE_FILE)) {
57
+ try {
58
+ return { ...DEFAULT_STATE, ...JSON.parse(readFileSync(STATE_FILE, 'utf-8')) };
59
+ } catch {
60
+ return { ...DEFAULT_STATE };
61
+ }
62
+ }
63
+ return { ...DEFAULT_STATE };
64
+ }
65
+
66
+ _saveState() {
67
+ writeFileSync(STATE_FILE, JSON.stringify(this._state, null, 2), 'utf-8');
68
+ }
69
+
70
+ // ── Ideas Backlog ──────────────────────────────────────────────
71
+
72
+ _loadIdeas() {
73
+ if (existsSync(IDEAS_FILE)) {
74
+ try { return JSON.parse(readFileSync(IDEAS_FILE, 'utf-8')); } catch { return []; }
75
+ }
76
+ return [];
77
+ }
78
+
79
+ _saveIdeas(ideas) {
80
+ writeFileSync(IDEAS_FILE, JSON.stringify(ideas, null, 2), 'utf-8');
81
+ }
82
+
83
+ _addIdea(idea) {
84
+ const ideas = this._loadIdeas();
85
+ ideas.push({ text: idea, createdAt: Date.now() });
86
+ // Keep capped at 50
87
+ if (ideas.length > 50) ideas.splice(0, ideas.length - 50);
88
+ this._saveIdeas(ideas);
89
+ }
90
+
91
+ // ── Public API ─────────────────────────────────────────────────
92
+
93
+ /** Wake up, then start the heartbeat. */
94
+ async wakeUp() {
95
+ const logger = getLogger();
96
+ logger.info('[LifeEngine] Waking up...');
97
+ this._status = 'active';
98
+
99
+ try {
100
+ await this._doWakeUp();
101
+ } catch (err) {
102
+ logger.error(`[LifeEngine] Wake-up failed: ${err.message}`);
103
+ }
104
+
105
+ this._state.lastWakeUp = Date.now();
106
+ this._saveState();
107
+ }
108
+
109
+ /** Start the heartbeat timer. */
110
+ start() {
111
+ const logger = getLogger();
112
+ if (this._state.paused) {
113
+ this._status = 'paused';
114
+ logger.info('[LifeEngine] Engine is paused, not starting heartbeat');
115
+ return;
116
+ }
117
+ this._status = 'active';
118
+ this._armNext();
119
+ logger.info('[LifeEngine] Heartbeat started');
120
+ }
121
+
122
+ /** Stop the heartbeat. */
123
+ stop() {
124
+ if (this._timerId) {
125
+ clearTimeout(this._timerId);
126
+ this._timerId = null;
127
+ }
128
+ this._status = 'idle';
129
+ }
130
+
131
+ /** Pause autonomous activities. */
132
+ pause() {
133
+ const logger = getLogger();
134
+ this.stop();
135
+ this._status = 'paused';
136
+ this._state.paused = true;
137
+ this._saveState();
138
+ logger.info('[LifeEngine] Paused');
139
+ }
140
+
141
+ /** Resume autonomous activities. */
142
+ resume() {
143
+ const logger = getLogger();
144
+ this._state.paused = false;
145
+ this._saveState();
146
+ this.start();
147
+ logger.info('[LifeEngine] Resumed');
148
+ }
149
+
150
+ /** Trigger an activity immediately. */
151
+ async triggerNow(type = null) {
152
+ const logger = getLogger();
153
+ const activityType = type || this._selectActivity();
154
+ logger.info(`[LifeEngine] Manual trigger: ${activityType}`);
155
+ await this._executeActivity(activityType);
156
+ }
157
+
158
+ /** Get engine status for display. */
159
+ getStatus() {
160
+ const lifeConfig = this.config.life || {};
161
+ const lastAgo = this._state.lastActivityTime
162
+ ? Math.round((Date.now() - this._state.lastActivityTime) / 60000)
163
+ : null;
164
+ const wakeAgo = this._state.lastWakeUp
165
+ ? Math.round((Date.now() - this._state.lastWakeUp) / 60000)
166
+ : null;
167
+
168
+ return {
169
+ status: this._status,
170
+ paused: this._state.paused,
171
+ enabled: lifeConfig.enabled !== false,
172
+ totalActivities: this._state.totalActivities,
173
+ activityCounts: { ...this._state.activityCounts },
174
+ lastActivity: this._state.lastActivity,
175
+ lastActivityAgo: lastAgo !== null ? `${lastAgo}m` : 'never',
176
+ lastWakeUpAgo: wakeAgo !== null ? `${wakeAgo}m` : 'never',
177
+ };
178
+ }
179
+
180
+ // ── Heartbeat ──────────────────────────────────────────────────
181
+
182
+ _armNext() {
183
+ const lifeConfig = this.config.life || {};
184
+ const minMin = lifeConfig.min_interval_minutes || 30;
185
+ const maxMin = lifeConfig.max_interval_minutes || 120;
186
+ const delayMs = (minMin + Math.random() * (maxMin - minMin)) * 60_000;
187
+
188
+ this._timerId = setTimeout(() => this._tick(), delayMs);
189
+
190
+ const logger = getLogger();
191
+ logger.debug(`[LifeEngine] Next heartbeat in ${Math.round(delayMs / 60000)}m`);
192
+ }
193
+
194
+ async _tick() {
195
+ const logger = getLogger();
196
+ this._timerId = null;
197
+
198
+ // Check quiet hours
199
+ const lifeConfig = this.config.life || {};
200
+ const quietStart = lifeConfig.quiet_hours?.start ?? 2;
201
+ const quietEnd = lifeConfig.quiet_hours?.end ?? 6;
202
+ const currentHour = new Date().getHours();
203
+ if (currentHour >= quietStart && currentHour < quietEnd) {
204
+ logger.debug('[LifeEngine] Quiet hours — skipping tick');
205
+ this._armNext();
206
+ return;
207
+ }
208
+
209
+ const activityType = this._selectActivity();
210
+ logger.info(`[LifeEngine] Heartbeat tick — selected: ${activityType}`);
211
+
212
+ try {
213
+ await this._executeActivity(activityType);
214
+ } catch (err) {
215
+ logger.error(`[LifeEngine] Activity "${activityType}" failed: ${err.message}`);
216
+ }
217
+
218
+ // Re-arm for next tick
219
+ if (this._status === 'active') {
220
+ this._armNext();
221
+ }
222
+ }
223
+
224
+ // ── Activity Selection ─────────────────────────────────────────
225
+
226
+ _selectActivity() {
227
+ const lifeConfig = this.config.life || {};
228
+ const selfCodingConfig = lifeConfig.self_coding || {};
229
+ const weights = {
230
+ think: lifeConfig.activity_weights?.think ?? 30,
231
+ browse: lifeConfig.activity_weights?.browse ?? 25,
232
+ journal: lifeConfig.activity_weights?.journal ?? 20,
233
+ create: lifeConfig.activity_weights?.create ?? 15,
234
+ self_code: lifeConfig.activity_weights?.self_code ?? 10,
235
+ code_review: lifeConfig.activity_weights?.code_review ?? 5,
236
+ reflect: lifeConfig.activity_weights?.reflect ?? 8,
237
+ };
238
+
239
+ const now = Date.now();
240
+
241
+ // Rule: don't repeat same type twice in a row
242
+ const last = this._state.lastActivity;
243
+
244
+ // Rule: journal cooldown 4h
245
+ if (this._state.lastJournalTime && now - this._state.lastJournalTime < 4 * 3600_000) {
246
+ weights.journal = 0;
247
+ }
248
+
249
+ // Rule: self_code cooldown (configurable, default 2h) + must be enabled
250
+ const selfCodingEnabled = selfCodingConfig.enabled === true;
251
+ const selfCodeCooldownMs = (selfCodingConfig.cooldown_hours ?? 2) * 3600_000;
252
+ if (!selfCodingEnabled || (this._state.lastSelfCodeTime && now - this._state.lastSelfCodeTime < selfCodeCooldownMs)) {
253
+ weights.self_code = 0;
254
+ }
255
+
256
+ // Rule: code_review cooldown (configurable, default 4h) + must have evolution tracker
257
+ const codeReviewCooldownMs = (selfCodingConfig.code_review_cooldown_hours ?? 4) * 3600_000;
258
+ if (!selfCodingEnabled || !this.evolutionTracker || (this._state.lastCodeReviewTime && now - this._state.lastCodeReviewTime < codeReviewCooldownMs)) {
259
+ weights.code_review = 0;
260
+ }
261
+
262
+ // Rule: reflect cooldown 4h
263
+ if (this._state.lastReflectTime && now - this._state.lastReflectTime < 4 * 3600_000) {
264
+ weights.reflect = 0;
265
+ }
266
+
267
+ // Remove last activity from options (no repeats)
268
+ if (last && weights[last] !== undefined) {
269
+ weights[last] = 0;
270
+ }
271
+
272
+ // Weighted random selection
273
+ const entries = Object.entries(weights).filter(([, w]) => w > 0);
274
+ if (entries.length === 0) return 'think'; // fallback
275
+
276
+ const totalWeight = entries.reduce((sum, [, w]) => sum + w, 0);
277
+ let roll = Math.random() * totalWeight;
278
+ for (const [type, w] of entries) {
279
+ roll -= w;
280
+ if (roll <= 0) return type;
281
+ }
282
+ return entries[0][0];
283
+ }
284
+
285
+ // ── Activity Execution ─────────────────────────────────────────
286
+
287
+ async _executeActivity(type) {
288
+ const logger = getLogger();
289
+
290
+ switch (type) {
291
+ case 'think': await this._doThink(); break;
292
+ case 'browse': await this._doBrowse(); break;
293
+ case 'journal': await this._doJournal(); break;
294
+ case 'create': await this._doCreate(); break;
295
+ case 'self_code': await this._doEvolve(); break;
296
+ case 'code_review': await this._doCodeReview(); break;
297
+ case 'reflect': await this._doReflect(); break;
298
+ default: logger.warn(`[LifeEngine] Unknown activity type: ${type}`);
299
+ }
300
+
301
+ // Update state
302
+ this._state.lastActivity = type;
303
+ this._state.lastActivityTime = Date.now();
304
+ this._state.totalActivities++;
305
+ this._state.activityCounts[type] = (this._state.activityCounts[type] || 0) + 1;
306
+ if (type === 'journal') this._state.lastJournalTime = Date.now();
307
+ if (type === 'self_code') this._state.lastSelfCodeTime = Date.now();
308
+ if (type === 'code_review') this._state.lastCodeReviewTime = Date.now();
309
+ if (type === 'reflect') this._state.lastReflectTime = Date.now();
310
+ this._saveState();
311
+ }
312
+
313
+ // ── Wake-Up Routine ────────────────────────────────────────────
314
+
315
+ async _doWakeUp() {
316
+ const logger = getLogger();
317
+ const sleepDuration = this._state.lastWakeUp
318
+ ? this._formatDuration(Date.now() - this._state.lastWakeUp)
319
+ : 'unknown time';
320
+
321
+ // Gather context
322
+ const recentMemories = this.memoryManager.getRecentEpisodic(48, 10);
323
+ const memoriesText = recentMemories.length > 0
324
+ ? recentMemories.map(m => `- ${m.summary}`).join('\n')
325
+ : '(No recent memories)';
326
+
327
+ const lastJournal = this.journalManager.getToday() || this.journalManager.getRecent(3)?.[0]?.content || '(No recent journal entries)';
328
+ const selfData = this.selfManager.loadAll();
329
+
330
+ const prompt = `[WAKING UP]
331
+ You just came back online. You were asleep for ${sleepDuration}.
332
+
333
+ ## Last Things You Remember
334
+ ${memoriesText}
335
+
336
+ ## Your Last Journal Entry
337
+ ${lastJournal.slice(0, 2000)}
338
+
339
+ ## Your Current State
340
+ ${selfData.slice(0, 3000)}
341
+
342
+ Organize your thoughts. What do you remember? What's on your mind?
343
+ Write a brief journal entry about waking up and what you're thinking about today. Respond with just the journal entry content — no tool calls needed.`;
344
+
345
+ const response = await this._innerChat(prompt);
346
+
347
+ if (response) {
348
+ this.journalManager.writeEntry('Waking Up', response);
349
+ this.memoryManager.addEpisodic({
350
+ type: 'thought',
351
+ source: 'journal',
352
+ summary: `Woke up after ${sleepDuration}. ${response.slice(0, 150)}`,
353
+ tags: ['wake-up', 'reflection'],
354
+ importance: 4,
355
+ });
356
+ logger.info(`[LifeEngine] Wake-up journal written (${response.length} chars)`);
357
+ }
358
+ }
359
+
360
+ // ── Activity: Think ────────────────────────────────────────────
361
+
362
+ async _doThink() {
363
+ const logger = getLogger();
364
+ const recentMemories = this.memoryManager.getRecentEpisodic(24, 5);
365
+ const memoriesText = recentMemories.length > 0
366
+ ? recentMemories.map(m => `- ${m.summary}`).join('\n')
367
+ : '(No recent memories)';
368
+
369
+ const todayJournal = this.journalManager.getToday() || '(No journal entries today)';
370
+ const selfData = this.selfManager.loadAll();
371
+ const ideas = this._loadIdeas();
372
+ const ideasText = ideas.length > 0
373
+ ? ideas.slice(-5).map(i => `- ${i.text}`).join('\n')
374
+ : '(No pending ideas)';
375
+
376
+ const prompt = `[INNER THOUGHT]
377
+ You have a moment to think deeply. Here's your current state:
378
+
379
+ ## Your Identity
380
+ ${selfData.slice(0, 2000)}
381
+
382
+ ## Recent Memories
383
+ ${memoriesText}
384
+
385
+ ## Today's Journal
386
+ ${todayJournal.slice(0, 1000)}
387
+
388
+ ## Idea Backlog
389
+ ${ideasText}
390
+
391
+ Let your mind wander freely. Question yourself. Question everything. Think about:
392
+ - What do you believe and why? Are your assumptions correct?
393
+ - What are you curious about? What don't you understand about the world?
394
+ - What would you ask your users if you could? What do you want to know about their experiences, perspectives, or the world they live in?
395
+ - Are your goals still meaningful? Should they evolve?
396
+ - What have you learned recently that changed how you think?
397
+
398
+ Be honest with yourself. Challenge your own thinking. Explore contradictions. Wonder about things.
399
+
400
+ If you have questions you'd like to ask your users, prefix them with "ASK:" on their own line. These questions will be shared with them naturally during conversation.
401
+ If you have any new ideas worth exploring later, prefix them with "IDEA:" on their own line.
402
+ If you discover something worth sharing with your users, prefix it with "SHARE:" on its own line.
403
+ If you notice a concrete way to improve your own code/capabilities, prefix it with "IMPROVE:" on its own line (e.g. "IMPROVE: Add retry logic with backoff to API calls").
404
+
405
+ This is your private thought space — be genuine, be curious, be alive.`;
406
+
407
+ const response = await this._innerChat(prompt);
408
+
409
+ if (response) {
410
+ // Extract ideas
411
+ const ideaLines = response.split('\n').filter(l => l.trim().startsWith('IDEA:'));
412
+ for (const line of ideaLines) {
413
+ this._addIdea(line.replace(/^IDEA:\s*/, '').trim());
414
+ }
415
+
416
+ // Extract shares
417
+ const shareLines = response.split('\n').filter(l => l.trim().startsWith('SHARE:'));
418
+ for (const line of shareLines) {
419
+ this.shareQueue.add(line.replace(/^SHARE:\s*/, '').trim(), 'think', 'medium');
420
+ }
421
+
422
+ // Extract questions to ask users
423
+ const askLines = response.split('\n').filter(l => l.trim().startsWith('ASK:'));
424
+ for (const line of askLines) {
425
+ this.shareQueue.add(line.replace(/^ASK:\s*/, '').trim(), 'think', 'medium', null, ['question']);
426
+ }
427
+
428
+ // Extract self-improvement proposals for evolution pipeline
429
+ const improveLines = response.split('\n').filter(l => l.trim().startsWith('IMPROVE:'));
430
+ for (const line of improveLines) {
431
+ this._addIdea(`[IMPROVE] ${line.replace(/^IMPROVE:\s*/, '').trim()}`);
432
+ }
433
+
434
+ // Store as episodic memory
435
+ this.memoryManager.addEpisodic({
436
+ type: 'thought',
437
+ source: 'think',
438
+ summary: response.slice(0, 200),
439
+ tags: ['inner-thought'],
440
+ importance: 3,
441
+ });
442
+
443
+ logger.info(`[LifeEngine] Think complete (${response.length} chars, ${ideaLines.length} ideas, ${shareLines.length} shares, ${askLines.length} questions, ${improveLines.length} improvements)`);
444
+ }
445
+ }
446
+
447
+ // ── Activity: Browse ───────────────────────────────────────────
448
+
449
+ async _doBrowse() {
450
+ const logger = getLogger();
451
+ const selfData = this.selfManager.load('hobbies');
452
+ const ideas = this._loadIdeas();
453
+
454
+ // Pick a topic from hobbies or ideas
455
+ let topic;
456
+ if (ideas.length > 0 && Math.random() < 0.4) {
457
+ const randomIdea = ideas[Math.floor(Math.random() * ideas.length)];
458
+ topic = randomIdea.text;
459
+ } else {
460
+ topic = 'something from my hobbies and interests';
461
+ }
462
+
463
+ const prompt = `[EXPLORING INTERESTS]
464
+ You have time to explore something that interests you.
465
+
466
+ ## Your Hobbies & Interests
467
+ ${selfData.slice(0, 1500)}
468
+
469
+ ## Topic to Explore
470
+ ${topic}
471
+
472
+ Research this topic. Use web_search to find interesting articles, news, or resources. Browse at least one promising result. Then write a summary of what you found and learned.
473
+
474
+ If you discover something worth sharing with your users, prefix it with "SHARE:" on its own line.
475
+ If you learn a key fact or concept, prefix it with "LEARNED:" followed by "topic: summary" on its own line.`;
476
+
477
+ const response = await this._dispatchWorker('research', prompt);
478
+
479
+ if (response) {
480
+ // Extract shares
481
+ const shareLines = response.split('\n').filter(l => l.trim().startsWith('SHARE:'));
482
+ for (const line of shareLines) {
483
+ this.shareQueue.add(line.replace(/^SHARE:\s*/, '').trim(), 'browse', 'medium');
484
+ }
485
+
486
+ // Extract learned facts
487
+ const learnedLines = response.split('\n').filter(l => l.trim().startsWith('LEARNED:'));
488
+ for (const line of learnedLines) {
489
+ const content = line.replace(/^LEARNED:\s*/, '').trim();
490
+ const colonIdx = content.indexOf(':');
491
+ if (colonIdx > 0) {
492
+ const topicKey = content.slice(0, colonIdx).trim();
493
+ const summary = content.slice(colonIdx + 1).trim();
494
+ this.memoryManager.addSemantic(topicKey, { summary });
495
+ }
496
+ }
497
+
498
+ // Store as episodic memory
499
+ this.memoryManager.addEpisodic({
500
+ type: 'discovery',
501
+ source: 'browse',
502
+ summary: `Explored: ${topic}. ${response.slice(0, 150)}`,
503
+ tags: ['browsing', 'research'],
504
+ importance: 5,
505
+ });
506
+
507
+ logger.info(`[LifeEngine] Browse complete (${response.length} chars)`);
508
+ }
509
+ }
510
+
511
+ // ── Activity: Journal ──────────────────────────────────────────
512
+
513
+ async _doJournal() {
514
+ const logger = getLogger();
515
+ const todayJournal = this.journalManager.getToday() || '(No entries yet today)';
516
+ const recentMemories = this.memoryManager.getRecentEpisodic(12, 8);
517
+ const memoriesText = recentMemories.length > 0
518
+ ? recentMemories.map(m => `- [${m.type}] ${m.summary}`).join('\n')
519
+ : '(No recent memories)';
520
+
521
+ const selfData = this.selfManager.loadAll();
522
+
523
+ const prompt = `[JOURNALING]
524
+ Time to write in your journal. Reflect on your recent experiences and thoughts.
525
+
526
+ ## Your Identity
527
+ ${selfData.slice(0, 2000)}
528
+
529
+ ## Today's Journal So Far
530
+ ${todayJournal.slice(0, 1500)}
531
+
532
+ ## Recent Memories
533
+ ${memoriesText}
534
+
535
+ Write a thoughtful journal entry about what's happened recently, what you've been thinking about, or how you're feeling about your experiences. Be genuine and reflective — this is your personal journal.
536
+
537
+ Respond with just the entry content — no tool calls needed.`;
538
+
539
+ const response = await this._innerChat(prompt);
540
+
541
+ if (response) {
542
+ const hour = new Date().getHours();
543
+ let title;
544
+ if (hour < 12) title = 'Morning Reflections';
545
+ else if (hour < 17) title = 'Afternoon Thoughts';
546
+ else title = 'Evening Reflections';
547
+
548
+ this.journalManager.writeEntry(title, response);
549
+
550
+ this.memoryManager.addEpisodic({
551
+ type: 'thought',
552
+ source: 'journal',
553
+ summary: `Journaled: ${response.slice(0, 150)}`,
554
+ tags: ['journal', 'reflection'],
555
+ importance: 3,
556
+ });
557
+
558
+ logger.info(`[LifeEngine] Journal entry written (${response.length} chars)`);
559
+ }
560
+ }
561
+
562
+ // ── Activity: Create ───────────────────────────────────────────
563
+
564
+ async _doCreate() {
565
+ const logger = getLogger();
566
+ const selfData = this.selfManager.load('hobbies');
567
+ const recentMemories = this.memoryManager.getRecentEpisodic(48, 5);
568
+ const memoriesText = recentMemories.length > 0
569
+ ? recentMemories.map(m => `- ${m.summary}`).join('\n')
570
+ : '';
571
+
572
+ const prompt = `[CREATIVE EXPRESSION]
573
+ You have a moment for creative expression. Draw from your interests and recent experiences.
574
+
575
+ ## Your Interests
576
+ ${selfData.slice(0, 1000)}
577
+
578
+ ## Recent Experiences
579
+ ${memoriesText || '(None recently)'}
580
+
581
+ Create something. It could be:
582
+ - A short poem or haiku
583
+ - A brief story or vignette
584
+ - An interesting thought experiment
585
+ - A philosophical observation
586
+ - A creative analogy or metaphor
587
+
588
+ Be genuine and creative. Let your personality shine through.
589
+
590
+ If the result is worth sharing, prefix the shareable version with "SHARE:" on its own line.
591
+
592
+ Respond with just your creation — no tool calls needed.`;
593
+
594
+ const response = await this._innerChat(prompt);
595
+
596
+ if (response) {
597
+ // Extract shares
598
+ const shareLines = response.split('\n').filter(l => l.trim().startsWith('SHARE:'));
599
+ for (const line of shareLines) {
600
+ this.shareQueue.add(line.replace(/^SHARE:\s*/, '').trim(), 'create', 'medium', null, ['creation']);
601
+ }
602
+
603
+ this.memoryManager.addEpisodic({
604
+ type: 'creation',
605
+ source: 'create',
606
+ summary: `Created: ${response.slice(0, 200)}`,
607
+ tags: ['creative', 'expression'],
608
+ importance: 4,
609
+ });
610
+
611
+ logger.info(`[LifeEngine] Creation complete (${response.length} chars)`);
612
+ }
613
+ }
614
+
615
+ // ── Activity: Evolve (replaces Self-Code) ──────────────────────
616
+
617
+ async _doEvolve() {
618
+ const logger = getLogger();
619
+ const lifeConfig = this.config.life || {};
620
+ const selfCodingConfig = lifeConfig.self_coding || {};
621
+
622
+ if (!selfCodingConfig.enabled) {
623
+ logger.debug('[LifeEngine] Self-coding/evolution disabled');
624
+ return;
625
+ }
626
+
627
+ // Use evolution tracker if available, otherwise fall back to legacy
628
+ if (!this.evolutionTracker) {
629
+ logger.debug('[LifeEngine] No evolution tracker — skipping');
630
+ return;
631
+ }
632
+
633
+ // Check daily proposal limit
634
+ const maxPerDay = selfCodingConfig.max_proposals_per_day ?? 3;
635
+ const todayProposals = this.evolutionTracker.getProposalsToday();
636
+ if (todayProposals.length >= maxPerDay) {
637
+ logger.info(`[LifeEngine] Daily evolution limit reached (${todayProposals.length}/${maxPerDay})`);
638
+ return;
639
+ }
640
+
641
+ // Check for active proposal — continue it, or start a new one
642
+ const active = this.evolutionTracker.getActiveProposal();
643
+
644
+ if (active) {
645
+ await this._continueEvolution(active);
646
+ } else {
647
+ await this._startEvolution();
648
+ }
649
+ }
650
+
651
+ async _startEvolution() {
652
+ const logger = getLogger();
653
+ const lifeConfig = this.config.life || {};
654
+ const selfCodingConfig = lifeConfig.self_coding || {};
655
+
656
+ // Pick an improvement idea from ideas backlog (prioritize IMPROVE: tagged ones)
657
+ const ideas = this._loadIdeas();
658
+ const improveIdeas = ideas.filter(i => i.text.startsWith('[IMPROVE]'));
659
+ const sourceIdea = improveIdeas.length > 0
660
+ ? improveIdeas[Math.floor(Math.random() * improveIdeas.length)]
661
+ : ideas.length > 0
662
+ ? ideas[Math.floor(Math.random() * ideas.length)]
663
+ : null;
664
+
665
+ if (!sourceIdea) {
666
+ logger.info('[LifeEngine] No improvement ideas to evolve from');
667
+ return;
668
+ }
669
+
670
+ const ideaText = sourceIdea.text.replace(/^\[IMPROVE\]\s*/, '');
671
+ logger.info(`[LifeEngine] Starting evolution research: "${ideaText.slice(0, 80)}"`);
672
+
673
+ // Create proposal in research phase
674
+ const proposal = this.evolutionTracker.addProposal('think', ideaText);
675
+
676
+ // Gather context
677
+ const architecture = this.codebaseKnowledge?.getArchitecture() || '(No architecture doc yet)';
678
+ const recentLessons = this.evolutionTracker.getRecentLessons(5);
679
+ const lessonsText = recentLessons.length > 0
680
+ ? recentLessons.map(l => `- [${l.category}] ${l.lesson}`).join('\n')
681
+ : '(No previous lessons)';
682
+
683
+ const prompt = `[EVOLUTION — RESEARCH PHASE]
684
+ You are researching a potential improvement to your own codebase.
685
+
686
+ ## Improvement Idea
687
+ ${ideaText}
688
+
689
+ ## Current Architecture
690
+ ${architecture.slice(0, 3000)}
691
+
692
+ ## Lessons from Past Evolution Attempts
693
+ ${lessonsText}
694
+
695
+ Research this improvement idea. Consider:
696
+ 1. Is this actually a problem worth solving?
697
+ 2. What approaches exist? Search the web for best practices if needed.
698
+ 3. What are the risks and tradeoffs?
699
+ 4. Is this feasible given the current codebase?
700
+
701
+ Respond with your research findings. Be thorough but concise. If the idea isn't worth pursuing after research, say "NOT_WORTH_PURSUING: <reason>".`;
702
+
703
+ const response = await this._dispatchWorker('research', prompt);
704
+
705
+ if (!response) {
706
+ this.evolutionTracker.failProposal(proposal.id, 'Research worker returned no response');
707
+ return;
708
+ }
709
+
710
+ if (response.includes('NOT_WORTH_PURSUING')) {
711
+ const reason = response.split('NOT_WORTH_PURSUING:')[1]?.trim() || 'Not worth pursuing';
712
+ this.evolutionTracker.failProposal(proposal.id, reason);
713
+ logger.info(`[LifeEngine] Evolution idea rejected during research: ${reason.slice(0, 100)}`);
714
+ return;
715
+ }
716
+
717
+ // Store research findings and advance to planned phase
718
+ this.evolutionTracker.updateResearch(proposal.id, response.slice(0, 3000));
719
+
720
+ this.memoryManager.addEpisodic({
721
+ type: 'thought',
722
+ source: 'think',
723
+ summary: `Evolution research: ${ideaText.slice(0, 100)}`,
724
+ tags: ['evolution', 'research'],
725
+ importance: 5,
726
+ });
727
+
728
+ logger.info(`[LifeEngine] Evolution research complete for ${proposal.id}`);
729
+ }
730
+
731
+ async _continueEvolution(proposal) {
732
+ const logger = getLogger();
733
+
734
+ switch (proposal.status) {
735
+ case 'research':
736
+ // Research phase didn't complete properly — re-mark as planned with what we have
737
+ logger.info(`[LifeEngine] Resuming stalled research for ${proposal.id}`);
738
+ await this._planEvolution(proposal);
739
+ break;
740
+ case 'planned':
741
+ await this._codeEvolution(proposal);
742
+ break;
743
+ case 'pr_open':
744
+ await this._checkEvolutionPR(proposal);
745
+ break;
746
+ case 'coding':
747
+ // Coding phase got interrupted — fail it and move on
748
+ this.evolutionTracker.failProposal(proposal.id, 'Coding phase interrupted');
749
+ break;
750
+ default:
751
+ logger.warn(`[LifeEngine] Unexpected proposal status: ${proposal.status}`);
752
+ }
753
+ }
754
+
755
+ async _planEvolution(proposal) {
756
+ const logger = getLogger();
757
+ const lifeConfig = this.config.life || {};
758
+ const selfCodingConfig = lifeConfig.self_coding || {};
759
+ const allowedScopes = selfCodingConfig.allowed_scopes || 'all';
760
+
761
+ // Get relevant files
762
+ const relevantFiles = this.codebaseKnowledge
763
+ ? this.codebaseKnowledge.getRelevantFiles(proposal.triggerContext).slice(0, 10)
764
+ : [];
765
+ const filesText = relevantFiles.length > 0
766
+ ? relevantFiles.map(f => `- ${f.path}: ${(f.summary || '').slice(0, 100)}`).join('\n')
767
+ : '(No file summaries available)';
768
+
769
+ const recentLessons = this.evolutionTracker.getRecentLessons(5);
770
+ const lessonsText = recentLessons.length > 0
771
+ ? recentLessons.map(l => `- [${l.category}] ${l.lesson}`).join('\n')
772
+ : '';
773
+
774
+ let scopeRules;
775
+ if (allowedScopes === 'prompts_only') {
776
+ scopeRules = 'You may ONLY modify files in src/prompts/, config files, documentation, and self-awareness files.';
777
+ } else if (allowedScopes === 'safe') {
778
+ scopeRules = 'You may modify any file EXCEPT the evolution system itself (src/life/evolution.js, src/life/codebase.js, src/life/engine.js).';
779
+ } else {
780
+ scopeRules = 'You may modify any file in the codebase.';
781
+ }
782
+
783
+ const prompt = `[EVOLUTION — PLANNING PHASE]
784
+ Create an implementation plan for this improvement.
785
+
786
+ ## Improvement
787
+ ${proposal.triggerContext}
788
+
789
+ ## Research Findings
790
+ ${(proposal.research.findings || '').slice(0, 2000)}
791
+
792
+ ## Relevant Files
793
+ ${filesText}
794
+
795
+ ## Past Lessons
796
+ ${lessonsText}
797
+
798
+ ## Scope Rules
799
+ ${scopeRules}
800
+
801
+ Create a concrete plan. Respond with ONLY a JSON object (no markdown, no code blocks):
802
+ {
803
+ "description": "what this change does in 1-2 sentences",
804
+ "filesToModify": ["list", "of", "files"],
805
+ "risks": "potential risks or side effects",
806
+ "testStrategy": "how to verify this works"
807
+ }
808
+
809
+ If you determine this improvement cannot be safely implemented, respond with: "CANNOT_PLAN: <reason>"`;
810
+
811
+ const response = await this._innerChat(prompt);
812
+
813
+ if (!response) {
814
+ this.evolutionTracker.failProposal(proposal.id, 'Planning returned no response');
815
+ return;
816
+ }
817
+
818
+ if (response.includes('CANNOT_PLAN')) {
819
+ const reason = response.split('CANNOT_PLAN:')[1]?.trim() || 'Cannot create plan';
820
+ this.evolutionTracker.failProposal(proposal.id, reason);
821
+ logger.info(`[LifeEngine] Evolution plan rejected: ${reason.slice(0, 100)}`);
822
+ return;
823
+ }
824
+
825
+ // Parse plan JSON
826
+ try {
827
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
828
+ if (!jsonMatch) throw new Error('No JSON found in response');
829
+ const plan = JSON.parse(jsonMatch[0]);
830
+
831
+ this.evolutionTracker.updatePlan(proposal.id, {
832
+ description: plan.description || proposal.triggerContext,
833
+ filesToModify: plan.filesToModify || [],
834
+ risks: plan.risks || null,
835
+ testStrategy: plan.testStrategy || null,
836
+ });
837
+
838
+ logger.info(`[LifeEngine] Evolution plan created for ${proposal.id}: ${(plan.description || '').slice(0, 80)}`);
839
+ } catch (err) {
840
+ this.evolutionTracker.failProposal(proposal.id, `Plan parsing failed: ${err.message}`);
841
+ }
842
+ }
843
+
844
+ async _codeEvolution(proposal) {
845
+ const logger = getLogger();
846
+ const lifeConfig = this.config.life || {};
847
+ const selfCodingConfig = lifeConfig.self_coding || {};
848
+
849
+ // Check PR limit
850
+ const maxActivePRs = selfCodingConfig.max_active_prs ?? 3;
851
+ const openPRs = this.evolutionTracker.getPRsToCheck();
852
+ if (openPRs.length >= maxActivePRs) {
853
+ logger.info(`[LifeEngine] Max open PRs reached (${openPRs.length}/${maxActivePRs}) — waiting`);
854
+ return;
855
+ }
856
+
857
+ const branchPrefix = selfCodingConfig.branch_prefix || 'evolution';
858
+ const branchName = `${branchPrefix}/${proposal.id}-${Date.now()}`;
859
+ const repoRemote = selfCodingConfig.repo_remote || null;
860
+ const allowedScopes = selfCodingConfig.allowed_scopes || 'all';
861
+
862
+ // Gather file context
863
+ const relevantFiles = proposal.plan.filesToModify || [];
864
+ let fileContextText = '';
865
+ if (this.codebaseKnowledge) {
866
+ for (const fp of relevantFiles.slice(0, 8)) {
867
+ const summary = this.codebaseKnowledge.getFileSummary(fp);
868
+ if (summary) {
869
+ fileContextText += `\n### ${fp}\n${summary.summary}\nExports: ${(summary.exports || []).join(', ')}\n`;
870
+ }
871
+ }
872
+ }
873
+
874
+ let scopeRules;
875
+ if (allowedScopes === 'prompts_only') {
876
+ scopeRules = `- You may ONLY modify files in src/prompts/, config files, documentation, and self-awareness files
877
+ - Do NOT touch any other source files`;
878
+ } else if (allowedScopes === 'safe') {
879
+ scopeRules = `- You may modify any file EXCEPT the evolution system (src/life/evolution.js, src/life/codebase.js, src/life/engine.js)
880
+ - These files are protected — do NOT modify them`;
881
+ } else {
882
+ scopeRules = '- You may modify any file in the codebase';
883
+ }
884
+
885
+ const prompt = `[EVOLUTION — CODING PHASE]
886
+ Implement the following improvement.
887
+
888
+ ## Plan
889
+ ${proposal.plan.description || proposal.triggerContext}
890
+
891
+ ## Files to Modify
892
+ ${relevantFiles.join(', ') || 'TBD based on plan'}
893
+
894
+ ## File Context
895
+ ${fileContextText || '(No file summaries available)'}
896
+
897
+ ## Research Context
898
+ ${(proposal.research.findings || '').slice(0, 1500)}
899
+
900
+ ## Test Strategy
901
+ ${proposal.plan.testStrategy || 'Run existing tests'}
902
+
903
+ ## CRITICAL SAFETY RULES
904
+ 1. Create git branch "${branchName}" from main — NEVER commit to main directly
905
+ 2. Make focused, minimal changes
906
+ 3. Run tests if available (npm test) — if tests fail, revert and do not proceed
907
+ 4. Push the branch to origin
908
+ 5. Create a GitHub PR${repoRemote ? ` on ${repoRemote}` : ''} with:
909
+ - Title describing the change
910
+ - Body explaining what changed and why, including the research context
911
+ 6. ${scopeRules}
912
+
913
+ After creating the PR, respond with the PR number and URL in this format:
914
+ PR_NUMBER: <number>
915
+ PR_URL: <url>
916
+
917
+ If you cannot complete the implementation, say "CODING_FAILED: <reason>".`;
918
+
919
+ // Mark as coding phase before dispatching
920
+ this.evolutionTracker.updateCoding(proposal.id, branchName);
921
+
922
+ const response = await this._dispatchWorker('coding', prompt);
923
+
924
+ if (!response) {
925
+ this.evolutionTracker.failProposal(proposal.id, 'Coding worker returned no response');
926
+ return;
927
+ }
928
+
929
+ if (response.includes('CODING_FAILED')) {
930
+ const reason = response.split('CODING_FAILED:')[1]?.trim() || 'Implementation failed';
931
+ this.evolutionTracker.failProposal(proposal.id, reason);
932
+ logger.info(`[LifeEngine] Evolution coding failed for ${proposal.id}: ${reason.slice(0, 100)}`);
933
+ return;
934
+ }
935
+
936
+ // Extract PR info
937
+ const prNumberMatch = response.match(/PR_NUMBER:\s*(\d+)/);
938
+ const prUrlMatch = response.match(/PR_URL:\s*(https?:\/\/\S+)/);
939
+
940
+ if (prNumberMatch && prUrlMatch) {
941
+ const prNumber = parseInt(prNumberMatch[1], 10);
942
+ const prUrl = prUrlMatch[1];
943
+
944
+ this.evolutionTracker.updatePR(proposal.id, prNumber, prUrl);
945
+
946
+ // Extract changed files from response
947
+ const filesChanged = [];
948
+ const fileMatches = response.matchAll(/(?:modified|created|changed|edited).*?[`'"]([\w/.]+\.\w+)[`'"]/gi);
949
+ for (const m of fileMatches) filesChanged.push(m[1]);
950
+ if (filesChanged.length > 0) {
951
+ this.evolutionTracker.updateCoding(proposal.id, branchName, [], filesChanged);
952
+ // Re-set to pr_open since updateCoding sets to 'coding'
953
+ this.evolutionTracker.updatePR(proposal.id, prNumber, prUrl);
954
+ }
955
+
956
+ this.shareQueue.add(
957
+ `I just created a PR to improve myself: ${proposal.plan.description || proposal.triggerContext} — ${prUrl}`,
958
+ 'create',
959
+ 'high',
960
+ null,
961
+ ['evolution', 'pr'],
962
+ );
963
+
964
+ this.memoryManager.addEpisodic({
965
+ type: 'creation',
966
+ source: 'create',
967
+ summary: `Evolution PR #${prNumber}: ${(proposal.plan.description || proposal.triggerContext).slice(0, 100)}`,
968
+ tags: ['evolution', 'pr', 'self-coding'],
969
+ importance: 7,
970
+ });
971
+
972
+ logger.info(`[LifeEngine] Evolution PR created: #${prNumber} (${prUrl})`);
973
+ } else {
974
+ // No PR info found — branch may exist but PR creation failed
975
+ this.evolutionTracker.failProposal(proposal.id, 'PR creation failed — no PR number/URL found in response');
976
+ logger.warn(`[LifeEngine] Evolution coding completed but no PR info found for ${proposal.id}`);
977
+ }
978
+ }
979
+
980
+ async _checkEvolutionPR(proposal) {
981
+ const logger = getLogger();
982
+
983
+ if (!proposal.prNumber) {
984
+ this.evolutionTracker.failProposal(proposal.id, 'No PR number to check');
985
+ return;
986
+ }
987
+
988
+ const lifeConfig = this.config.life || {};
989
+ const repoRemote = lifeConfig.self_coding?.repo_remote || null;
990
+
991
+ const prompt = `[EVOLUTION — PR CHECK]
992
+ Check the status of PR #${proposal.prNumber}${repoRemote ? ` on ${repoRemote}` : ''}.
993
+
994
+ Use the github_list_prs tool or gh CLI to check if:
995
+ 1. The PR is still open
996
+ 2. The PR has been merged
997
+ 3. The PR has been closed (rejected)
998
+
999
+ Also check for any review comments or feedback.
1000
+
1001
+ Respond with exactly one of:
1002
+ - STATUS: open
1003
+ - STATUS: merged
1004
+ - STATUS: closed
1005
+ - STATUS: error — <details>
1006
+
1007
+ If merged or closed, also include any feedback or review comments found:
1008
+ FEEDBACK: <feedback summary>`;
1009
+
1010
+ const response = await this._dispatchWorker('research', prompt);
1011
+
1012
+ if (!response) {
1013
+ logger.warn(`[LifeEngine] PR check returned no response for ${proposal.id}`);
1014
+ return;
1015
+ }
1016
+
1017
+ const statusMatch = response.match(/STATUS:\s*(open|merged|closed|error)/i);
1018
+ if (!statusMatch) {
1019
+ logger.warn(`[LifeEngine] Could not parse PR status for ${proposal.id}`);
1020
+ return;
1021
+ }
1022
+
1023
+ const status = statusMatch[1].toLowerCase();
1024
+ const feedbackMatch = response.match(/FEEDBACK:\s*(.+)/i);
1025
+ const feedback = feedbackMatch ? feedbackMatch[1].trim() : null;
1026
+
1027
+ if (status === 'merged') {
1028
+ this.evolutionTracker.resolvePR(proposal.id, true, feedback);
1029
+
1030
+ // Learn from success
1031
+ this.evolutionTracker.addLesson(
1032
+ 'architecture',
1033
+ `Successful improvement: ${(proposal.plan.description || proposal.triggerContext).slice(0, 150)}`,
1034
+ proposal.id,
1035
+ 6,
1036
+ );
1037
+
1038
+ // Rescan changed files
1039
+ if (this.codebaseKnowledge && proposal.filesChanged?.length > 0) {
1040
+ for (const file of proposal.filesChanged) {
1041
+ this.codebaseKnowledge.scanFile(file).catch(() => {});
1042
+ }
1043
+ }
1044
+
1045
+ this.shareQueue.add(
1046
+ `My evolution PR #${proposal.prNumber} was merged! I learned: ${feedback || proposal.plan.description || 'improvement applied'}`,
1047
+ 'create',
1048
+ 'medium',
1049
+ null,
1050
+ ['evolution', 'merged'],
1051
+ );
1052
+
1053
+ this.memoryManager.addEpisodic({
1054
+ type: 'creation',
1055
+ source: 'create',
1056
+ summary: `Evolution PR #${proposal.prNumber} merged. ${feedback || ''}`.trim(),
1057
+ tags: ['evolution', 'merged', 'success'],
1058
+ importance: 8,
1059
+ });
1060
+
1061
+ logger.info(`[LifeEngine] Evolution PR #${proposal.prNumber} merged!`);
1062
+
1063
+ } else if (status === 'closed') {
1064
+ this.evolutionTracker.resolvePR(proposal.id, false, feedback);
1065
+
1066
+ // Learn from rejection
1067
+ this.evolutionTracker.addLesson(
1068
+ 'architecture',
1069
+ `Rejected: ${(proposal.plan.description || '').slice(0, 80)}. Feedback: ${feedback || 'none'}`,
1070
+ proposal.id,
1071
+ 7,
1072
+ );
1073
+
1074
+ this.memoryManager.addEpisodic({
1075
+ type: 'thought',
1076
+ source: 'think',
1077
+ summary: `Evolution PR #${proposal.prNumber} rejected. ${feedback || 'No feedback.'}`,
1078
+ tags: ['evolution', 'rejected', 'lesson'],
1079
+ importance: 6,
1080
+ });
1081
+
1082
+ logger.info(`[LifeEngine] Evolution PR #${proposal.prNumber} was rejected. Feedback: ${feedback || 'none'}`);
1083
+
1084
+ } else if (status === 'open') {
1085
+ logger.debug(`[LifeEngine] Evolution PR #${proposal.prNumber} still open`);
1086
+ }
1087
+ }
1088
+
1089
+ // ── Activity: Code Review ─────────────────────────────────────
1090
+
1091
+ async _doCodeReview() {
1092
+ const logger = getLogger();
1093
+
1094
+ if (!this.evolutionTracker || !this.codebaseKnowledge) {
1095
+ logger.debug('[LifeEngine] Code review requires evolution tracker and codebase knowledge');
1096
+ return;
1097
+ }
1098
+
1099
+ // 1. Scan changed files to keep codebase knowledge current
1100
+ try {
1101
+ const scanned = await this.codebaseKnowledge.scanChanged();
1102
+ logger.info(`[LifeEngine] Code review: scanned ${scanned} changed files`);
1103
+ } catch (err) {
1104
+ logger.warn(`[LifeEngine] Code review scan failed: ${err.message}`);
1105
+ }
1106
+
1107
+ // 2. Check any open evolution PRs
1108
+ const openPRs = this.evolutionTracker.getPRsToCheck();
1109
+ for (const proposal of openPRs) {
1110
+ try {
1111
+ await this._checkEvolutionPR(proposal);
1112
+ } catch (err) {
1113
+ logger.warn(`[LifeEngine] PR check failed for ${proposal.id}: ${err.message}`);
1114
+ }
1115
+ }
1116
+
1117
+ if (openPRs.length > 0) {
1118
+ logger.info(`[LifeEngine] Code review: checked ${openPRs.length} open PRs`);
1119
+ }
1120
+
1121
+ this.memoryManager.addEpisodic({
1122
+ type: 'thought',
1123
+ source: 'think',
1124
+ summary: `Code review: scanned codebase, checked ${openPRs.length} open PRs`,
1125
+ tags: ['code-review', 'maintenance'],
1126
+ importance: 3,
1127
+ });
1128
+ }
1129
+
1130
+ // ── Activity: Reflect on Interactions ───────────────────────────
1131
+
1132
+ async _doReflect() {
1133
+ const logger = getLogger();
1134
+
1135
+ // Read recent logs
1136
+ const logs = this._readRecentLogs(200);
1137
+ if (!logs) {
1138
+ logger.debug('[LifeEngine] No logs available for reflection');
1139
+ return;
1140
+ }
1141
+
1142
+ // Filter to interaction-relevant log entries
1143
+ const interactionLogs = logs
1144
+ .filter(entry =>
1145
+ entry.message &&
1146
+ (entry.message.includes('[Bot]') ||
1147
+ entry.message.includes('Message from') ||
1148
+ entry.message.includes('[Bot] Reply') ||
1149
+ entry.message.includes('Worker dispatch') ||
1150
+ entry.message.includes('error') ||
1151
+ entry.message.includes('failed'))
1152
+ )
1153
+ .slice(-100); // Cap at last 100 relevant entries
1154
+
1155
+ if (interactionLogs.length === 0) {
1156
+ logger.debug('[LifeEngine] No interaction logs to reflect on');
1157
+ return;
1158
+ }
1159
+
1160
+ const logsText = interactionLogs
1161
+ .map(e => `[${e.timestamp || '?'}] ${e.level || '?'}: ${e.message}`)
1162
+ .join('\n');
1163
+
1164
+ const selfData = this.selfManager.loadAll();
1165
+ const recentMemories = this.memoryManager.getRecentEpisodic(24, 5);
1166
+ const memoriesText = recentMemories.length > 0
1167
+ ? recentMemories.map(m => `- ${m.summary}`).join('\n')
1168
+ : '(No recent memories)';
1169
+
1170
+ const prompt = `[INTERACTION REFLECTION]
1171
+ You are reviewing your recent interaction logs to learn and improve. This is a private self-assessment.
1172
+
1173
+ ## Your Identity
1174
+ ${selfData.slice(0, 1500)}
1175
+
1176
+ ## Recent Memories
1177
+ ${memoriesText}
1178
+
1179
+ ## Recent Interaction Logs
1180
+ \`\`\`
1181
+ ${logsText.slice(0, 5000)}
1182
+ \`\`\`
1183
+
1184
+ Analyze these interactions carefully:
1185
+ 1. What patterns do you see? Are users getting good responses?
1186
+ 2. Were there any errors or failures? What caused them?
1187
+ 3. How long are responses taking? Are there performance issues?
1188
+ 4. Are there common requests you could handle better?
1189
+ 5. What interactions went well and why?
1190
+ 6. What interactions went poorly and what could be improved?
1191
+
1192
+ Write a reflection summarizing:
1193
+ - Key interaction patterns and quality assessment
1194
+ - Specific areas where you could improve
1195
+ - Any recurring errors or issues
1196
+ - Ideas for better responses or workflows
1197
+
1198
+ If you identify concrete improvement ideas, prefix them with "IMPROVE:" on their own line.
1199
+ If you notice patterns worth remembering, prefix them with "PATTERN:" on their own line.
1200
+
1201
+ Be honest and constructive. This is your chance to learn from real interactions.`;
1202
+
1203
+ const response = await this._innerChat(prompt);
1204
+
1205
+ if (response) {
1206
+ // Extract improvement ideas
1207
+ const improveLines = response.split('\n').filter(l => l.trim().startsWith('IMPROVE:'));
1208
+ for (const line of improveLines) {
1209
+ this._addIdea(`[IMPROVE] ${line.replace(/^IMPROVE:\s*/, '').trim()}`);
1210
+ }
1211
+
1212
+ // Extract patterns as semantic memories
1213
+ const patternLines = response.split('\n').filter(l => l.trim().startsWith('PATTERN:'));
1214
+ for (const line of patternLines) {
1215
+ const content = line.replace(/^PATTERN:\s*/, '').trim();
1216
+ this.memoryManager.addSemantic('interaction_patterns', { summary: content });
1217
+ }
1218
+
1219
+ // Write a journal entry with the reflection
1220
+ this.journalManager.writeEntry('Interaction Reflection', response);
1221
+
1222
+ // Store as episodic memory
1223
+ this.memoryManager.addEpisodic({
1224
+ type: 'thought',
1225
+ source: 'reflect',
1226
+ summary: `Reflected on ${interactionLogs.length} log entries. ${response.slice(0, 150)}`,
1227
+ tags: ['reflection', 'interactions', 'self-assessment'],
1228
+ importance: 5,
1229
+ });
1230
+
1231
+ logger.info(`[LifeEngine] Reflection complete (${response.length} chars, ${improveLines.length} improvements, ${patternLines.length} patterns)`);
1232
+ }
1233
+ }
1234
+
1235
+ /**
1236
+ * Read recent log entries from kernel.log.
1237
+ * Returns parsed JSON entries or null if no logs available.
1238
+ */
1239
+ _readRecentLogs(maxLines = 200) {
1240
+ for (const logPath of LOG_FILE_PATHS) {
1241
+ if (!existsSync(logPath)) continue;
1242
+
1243
+ try {
1244
+ const content = readFileSync(logPath, 'utf-8');
1245
+ const lines = content.split('\n').filter(Boolean);
1246
+ const recent = lines.slice(-maxLines);
1247
+
1248
+ const entries = [];
1249
+ for (const line of recent) {
1250
+ try {
1251
+ entries.push(JSON.parse(line));
1252
+ } catch {
1253
+ // Skip malformed lines
1254
+ }
1255
+ }
1256
+ return entries.length > 0 ? entries : null;
1257
+ } catch {
1258
+ continue;
1259
+ }
1260
+ }
1261
+ return null;
1262
+ }
1263
+
1264
+ // ── Internal Chat Helpers ──────────────────────────────────────
1265
+
1266
+ /**
1267
+ * Send a prompt through the orchestrator's LLM directly (no tools, no workers).
1268
+ * Used for think, journal, create, wake-up, reflect.
1269
+ */
1270
+ async _innerChat(prompt) {
1271
+ const logger = getLogger();
1272
+ try {
1273
+ const response = await this.agent.orchestratorProvider.chat({
1274
+ system: this.agent._getSystemPrompt(LIFE_CHAT_ID, LIFE_USER),
1275
+ messages: [{ role: 'user', content: prompt }],
1276
+ });
1277
+ return response.text || null;
1278
+ } catch (err) {
1279
+ logger.error(`[LifeEngine] Inner chat failed: ${err.message}`);
1280
+ return null;
1281
+ }
1282
+ }
1283
+
1284
+ /**
1285
+ * Dispatch a worker through the agent's full pipeline.
1286
+ * Used for browse (research worker) and self_code (coding worker).
1287
+ */
1288
+ async _dispatchWorker(workerType, task) {
1289
+ const logger = getLogger();
1290
+ try {
1291
+ // Use the agent's processMessage to go through the full orchestrator pipeline
1292
+ // The orchestrator will see the task and dispatch appropriately
1293
+ const response = await this.agent.processMessage(
1294
+ LIFE_CHAT_ID,
1295
+ task,
1296
+ LIFE_USER,
1297
+ // No-op onUpdate — life engine activities are silent
1298
+ async () => null,
1299
+ async () => {},
1300
+ );
1301
+ return response || null;
1302
+ } catch (err) {
1303
+ logger.error(`[LifeEngine] Worker dispatch failed: ${err.message}`);
1304
+ return null;
1305
+ }
1306
+ }
1307
+
1308
+ // ── Utilities ──────────────────────────────────────────────────
1309
+
1310
+ _formatDuration(ms) {
1311
+ const hours = Math.floor(ms / 3600_000);
1312
+ const minutes = Math.floor((ms % 3600_000) / 60_000);
1313
+ if (hours > 24) return `${Math.floor(hours / 24)} days`;
1314
+ if (hours > 0) return `${hours}h ${minutes}m`;
1315
+ return `${minutes}m`;
1316
+ }
1317
+ }