kernelbot 1.0.39 → 1.0.40

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 (80) hide show
  1. package/bin/kernel.js +5 -5
  2. package/config.example.yaml +1 -1
  3. package/package.json +1 -1
  4. package/skills/business/business-analyst.md +32 -0
  5. package/skills/business/product-manager.md +32 -0
  6. package/skills/business/project-manager.md +32 -0
  7. package/skills/business/startup-advisor.md +32 -0
  8. package/skills/creative/music-producer.md +32 -0
  9. package/skills/creative/photographer.md +32 -0
  10. package/skills/creative/video-producer.md +32 -0
  11. package/skills/data/bi-analyst.md +37 -0
  12. package/skills/data/data-scientist.md +38 -0
  13. package/skills/data/ml-engineer.md +38 -0
  14. package/skills/design/graphic-designer.md +38 -0
  15. package/skills/design/product-designer.md +41 -0
  16. package/skills/design/ui-ux.md +38 -0
  17. package/skills/education/curriculum-designer.md +32 -0
  18. package/skills/education/language-teacher.md +32 -0
  19. package/skills/education/tutor.md +32 -0
  20. package/skills/engineering/data-eng.md +55 -0
  21. package/skills/engineering/devops.md +56 -0
  22. package/skills/engineering/mobile-dev.md +55 -0
  23. package/skills/engineering/security-eng.md +55 -0
  24. package/skills/engineering/sr-backend.md +55 -0
  25. package/skills/engineering/sr-frontend.md +55 -0
  26. package/skills/finance/accountant.md +35 -0
  27. package/skills/finance/crypto-defi.md +39 -0
  28. package/skills/finance/financial-analyst.md +35 -0
  29. package/skills/healthcare/health-wellness.md +32 -0
  30. package/skills/healthcare/medical-researcher.md +33 -0
  31. package/skills/legal/contract-reviewer.md +35 -0
  32. package/skills/legal/legal-advisor.md +36 -0
  33. package/skills/marketing/content-marketer.md +38 -0
  34. package/skills/marketing/growth.md +38 -0
  35. package/skills/marketing/seo.md +43 -0
  36. package/skills/marketing/social-media.md +43 -0
  37. package/skills/writing/academic-writer.md +33 -0
  38. package/skills/writing/copywriter.md +32 -0
  39. package/skills/writing/creative-writer.md +32 -0
  40. package/skills/writing/tech-writer.md +33 -0
  41. package/src/agent.js +153 -118
  42. package/src/automation/scheduler.js +36 -3
  43. package/src/bot.js +147 -64
  44. package/src/coder.js +30 -8
  45. package/src/conversation.js +96 -19
  46. package/src/dashboard/dashboard.css +6 -0
  47. package/src/dashboard/dashboard.js +28 -1
  48. package/src/dashboard/index.html +12 -0
  49. package/src/dashboard/server.js +77 -15
  50. package/src/life/codebase.js +2 -1
  51. package/src/life/daydream_engine.js +386 -0
  52. package/src/life/engine.js +1 -0
  53. package/src/life/evolution.js +4 -3
  54. package/src/prompts/orchestrator.js +1 -1
  55. package/src/prompts/system.js +1 -1
  56. package/src/prompts/workers.js +8 -1
  57. package/src/providers/anthropic.js +3 -1
  58. package/src/providers/base.js +33 -0
  59. package/src/providers/index.js +1 -1
  60. package/src/providers/models.js +22 -0
  61. package/src/providers/openai-compat.js +3 -0
  62. package/src/services/x-api.js +14 -3
  63. package/src/skills/loader.js +382 -0
  64. package/src/swarm/worker-registry.js +2 -2
  65. package/src/tools/browser.js +10 -3
  66. package/src/tools/coding.js +16 -0
  67. package/src/tools/docker.js +13 -0
  68. package/src/tools/git.js +31 -29
  69. package/src/tools/jira.js +11 -2
  70. package/src/tools/monitor.js +9 -1
  71. package/src/tools/network.js +34 -0
  72. package/src/tools/orchestrator-tools.js +2 -1
  73. package/src/tools/os.js +20 -6
  74. package/src/utils/config.js +1 -1
  75. package/src/utils/display.js +1 -1
  76. package/src/utils/logger.js +1 -1
  77. package/src/utils/timeAwareness.js +72 -0
  78. package/src/worker.js +26 -33
  79. package/src/skills/catalog.js +0 -506
  80. package/src/skills/custom.js +0 -128
@@ -41,6 +41,7 @@
41
41
  renderCharacter(snap.character);
42
42
  lastSharesData = snap.shares || { pending: [], shared: [], todayCount: 0 };
43
43
  renderShares(lastSharesData);
44
+ renderSkills(snap.skills);
44
45
  renderCapabilities(snap.capabilities);
45
46
  renderIdeas(snap.life?.ideas || []);
46
47
  renderLogs(snap.logs);
@@ -102,6 +103,9 @@
102
103
 
103
104
  const rbConvs = $('rb-convs');
104
105
  if (rbConvs) { const c = (snap.conversations || []).length; rbConvs.textContent = c; rbConvs.className = 'r-val' + (c > 0 ? '' : ' zero'); }
106
+
107
+ const rbSkills = $('rb-skills');
108
+ if (rbSkills) { const c = snap.skills?.total || 0; rbSkills.textContent = c; rbSkills.className = 'r-val' + (c > 0 ? '' : ' zero'); }
105
109
  }
106
110
 
107
111
  function renderSystem(sys) {
@@ -395,7 +399,7 @@
395
399
  h += `<div class="conv-item"><span class="chat-id ${c.chatId==='__life__'?'life':''}">${esc(c.chatId)}</span><span style="color:var(--dim)">${c.messageCount} msgs · ${timeAgo(c.lastTimestamp)}</span></div>`;
396
400
  if (c.userMessages || c.assistantMessages) {
397
401
  h += `<div style="font-size:9px;color:var(--dim);padding-left:4px">USR:${c.userMessages||0} BOT:${c.assistantMessages||0}`;
398
- if (c.activeSkill) h += ` <span style="color:var(--magenta)">SKILL:${esc(c.activeSkill)}</span>`;
402
+ if (c.activeSkills?.length) h += ` <span style="color:var(--magenta)">SKILLS:${c.activeSkills.map(s => esc(s)).join(',')}</span>`;
399
403
  h += '</div>';
400
404
  }
401
405
  }
@@ -442,6 +446,29 @@
442
446
  $('caps-body').innerHTML = h;
443
447
  }
444
448
 
449
+ function renderSkills(data) {
450
+ const el = $('skills-body');
451
+ if (!el) return;
452
+ if (!data || !data.skills?.length) { el.innerHTML = '<div class="empty-msg">NO SKILLS</div>'; return; }
453
+ const tag = $('skills-tag');
454
+ if (tag) tag.textContent = `SKILLS // ${data.total} LOADED`;
455
+ let h = '';
456
+ if (data.categories?.length) {
457
+ for (const cat of data.categories) {
458
+ const catSkills = data.skills.filter(s => s.category === cat.key);
459
+ h += `<div style="margin-bottom:6px"><div style="font-family:var(--font-hud);font-size:8px;letter-spacing:1.5px;color:var(--dim);margin-bottom:2px">${esc(cat.emoji)} ${esc(cat.name.toUpperCase())} (${cat.count})</div>`;
460
+ for (const s of catSkills) {
461
+ h += `<div class="skill-item"><span class="skill-emoji">${esc(s.emoji)}</span><span class="skill-name">${esc(s.name)}</span>`;
462
+ if (s.isCustom) h += '<span style="color:var(--amber);font-size:8px;margin-left:4px">CUSTOM</span>';
463
+ if (s.description) h += `<div class="skill-desc">${esc(s.description.slice(0, 80))}</div>`;
464
+ h += '</div>';
465
+ }
466
+ h += '</div>';
467
+ }
468
+ }
469
+ el.innerHTML = h;
470
+ }
471
+
445
472
  function renderKnowledge(topics) {
446
473
  if (!topics || !topics.length) { $('mem-body').innerHTML = '<div class="empty-msg">NO KNOWLEDGE</div>'; return; }
447
474
  const tag = $('mem-tag');
@@ -141,6 +141,11 @@
141
141
  <span class="r-val zero" id="rb-convs">0</span>
142
142
  <span class="r-lbl">CHATS</span>
143
143
  </div>
144
+ <div class="right-divider"></div>
145
+ <div class="right-stat" title="Skills">
146
+ <span class="r-val zero" id="rb-skills">0</span>
147
+ <span class="r-lbl">SKILLS</span>
148
+ </div>
144
149
  <div class="right-bar-clock" id="rb-clock">--:--</div>
145
150
  </aside>
146
151
 
@@ -225,6 +230,13 @@
225
230
  <div class="panel-body" id="caps-body"></div>
226
231
  </div>
227
232
 
233
+ <!-- SKILLS -->
234
+ <div class="panel span-4" id="p-skills">
235
+ <div class="sweep"></div>
236
+ <div class="panel-header"><span class="dot"></span>SKILLS<span class="tag" id="skills-tag">SKILLS</span></div>
237
+ <div class="panel-body" id="skills-body"></div>
238
+ </div>
239
+
228
240
  <!-- LIFE -->
229
241
  <div class="panel span-4" id="p-life">
230
242
  <div class="sweep"></div>
@@ -13,6 +13,7 @@ import { loadavg, totalmem, freemem, cpus } from 'os';
13
13
  import { getLogger } from '../utils/logger.js';
14
14
  import { WORKER_TYPES } from '../swarm/worker-registry.js';
15
15
  import { TOOL_CATEGORIES } from '../tools/categories.js';
16
+ import { loadAllSkills, getCategoryList, SKILL_CATEGORIES } from '../skills/loader.js';
16
17
 
17
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
19
 
@@ -39,6 +40,34 @@ export function startDashboard(deps) {
39
40
 
40
41
  const logger = getLogger();
41
42
 
43
+ // --- Authentication ---
44
+ const dashboardToken = config.dashboard?.token || process.env.DASHBOARD_TOKEN || null;
45
+ const allowedOrigins = config.dashboard?.allowed_origins || null;
46
+
47
+ function getCorOrigin(req) {
48
+ if (!allowedOrigins) return null; // No CORS headers when no origins configured
49
+ const origin = req.headers.origin;
50
+ if (origin && allowedOrigins.includes(origin)) return origin;
51
+ return null;
52
+ }
53
+
54
+ function authenticate(req, res) {
55
+ if (!dashboardToken) return true; // No token configured = no auth required
56
+
57
+ // Check Authorization header or query param
58
+ const authHeader = req.headers.authorization;
59
+ const url = new URL(req.url, `http://${req.headers.host}`);
60
+ const queryToken = url.searchParams.get('token');
61
+
62
+ if (authHeader === `Bearer ${dashboardToken}` || queryToken === dashboardToken) {
63
+ return true;
64
+ }
65
+
66
+ res.writeHead(401, { 'Content-Type': 'application/json' });
67
+ res.end(JSON.stringify({ error: 'Unauthorized — provide a valid dashboard token' }));
68
+ return false;
69
+ }
70
+
42
71
  // --- Static file serving ---
43
72
  const MIME_TYPES = {
44
73
  '.html': 'text/html; charset=utf-8',
@@ -283,7 +312,8 @@ export function startDashboard(deps) {
283
312
  const summaries = [];
284
313
  for (const [chatId, messages] of conversationManager.conversations) {
285
314
  const last = messages.length > 0 ? messages[messages.length - 1] : null;
286
- const skill = conversationManager.activeSkills?.get(chatId) || null;
315
+ const skills = conversationManager.activeSkills?.get(chatId) || [];
316
+ const skillList = Array.isArray(skills) ? skills : (typeof skills === 'string' ? [skills] : []);
287
317
  const userMsgs = messages.filter(m => m.role === 'user').length;
288
318
  const assistantMsgs = messages.filter(m => m.role === 'assistant').length;
289
319
  summaries.push({
@@ -292,7 +322,7 @@ export function startDashboard(deps) {
292
322
  userMessages: userMsgs,
293
323
  assistantMessages: assistantMsgs,
294
324
  lastTimestamp: last?.timestamp || null,
295
- activeSkill: skill,
325
+ activeSkills: skillList,
296
326
  });
297
327
  }
298
328
  return summaries.sort((a, b) => (b.lastTimestamp || 0) - (a.lastTimestamp || 0));
@@ -355,6 +385,24 @@ export function startDashboard(deps) {
355
385
  return { workers, categories: Object.keys(TOOL_CATEGORIES), totalTools };
356
386
  }
357
387
 
388
+ function getSkillsData() {
389
+ try {
390
+ const allSkills = loadAllSkills();
391
+ const categories = getCategoryList();
392
+ const skills = [...allSkills.values()].map(s => ({
393
+ id: s.id,
394
+ name: s.name,
395
+ emoji: s.emoji,
396
+ category: s.category,
397
+ description: s.description,
398
+ isCustom: s.isCustom,
399
+ tags: s.tags || [],
400
+ workerAffinity: s.worker_affinity || null,
401
+ }));
402
+ return { skills, categories, total: skills.length };
403
+ } catch { return { skills: [], categories: [], total: 0 }; }
404
+ }
405
+
358
406
  function getKnowledgeData() {
359
407
  try {
360
408
  const data = memoryManager._loadSemantic();
@@ -383,6 +431,7 @@ export function startDashboard(deps) {
383
431
  character: getCharacterData(),
384
432
  shares: getSharesData(),
385
433
  logs: parseLogs(tailLog(100)),
434
+ skills: getSkillsData(),
386
435
  capabilities: getCapabilitiesData(),
387
436
  knowledge: getKnowledgeData(),
388
437
  };
@@ -408,13 +457,18 @@ export function startDashboard(deps) {
408
457
  }, 3000);
409
458
 
410
459
  // --- HTTP routing ---
411
- function sendJson(res, data) {
460
+ function sendJson(res, data, req) {
412
461
  const body = JSON.stringify(data);
413
- res.writeHead(200, {
462
+ const headers = {
414
463
  'Content-Type': 'application/json',
415
- 'Access-Control-Allow-Origin': '*',
416
464
  'Cache-Control': 'no-cache',
417
- });
465
+ };
466
+ // Only set CORS header for explicitly allowed origins (not wildcard)
467
+ if (req) {
468
+ const corsOrigin = getCorOrigin(req);
469
+ if (corsOrigin) headers['Access-Control-Allow-Origin'] = corsOrigin;
470
+ }
471
+ res.writeHead(200, headers);
418
472
  res.end(body);
419
473
  }
420
474
 
@@ -424,16 +478,18 @@ export function startDashboard(deps) {
424
478
 
425
479
  // CORS preflight
426
480
  if (req.method === 'OPTIONS') {
427
- res.writeHead(204, {
428
- 'Access-Control-Allow-Origin': '*',
481
+ const corsOrigin = getCorOrigin(req);
482
+ const headers = {
429
483
  'Access-Control-Allow-Methods': 'GET, OPTIONS',
430
- 'Access-Control-Allow-Headers': 'Content-Type',
431
- });
484
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
485
+ };
486
+ if (corsOrigin) headers['Access-Control-Allow-Origin'] = corsOrigin;
487
+ res.writeHead(204, headers);
432
488
  res.end();
433
489
  return;
434
490
  }
435
491
 
436
- // Serve index.html
492
+ // Serve index.html (static pages don't require auth)
437
493
  if (path === '/' || path === '/index.html') {
438
494
  serveStaticFile(res, 'index.html');
439
495
  return;
@@ -445,14 +501,19 @@ export function startDashboard(deps) {
445
501
  return;
446
502
  }
447
503
 
504
+ // All API and SSE endpoints require authentication
505
+ if (!authenticate(req, res)) return;
506
+
448
507
  // SSE endpoint
449
508
  if (path === '/events') {
450
- res.writeHead(200, {
509
+ const sseHeaders = {
451
510
  'Content-Type': 'text/event-stream',
452
511
  'Cache-Control': 'no-cache',
453
512
  'Connection': 'keep-alive',
454
- 'Access-Control-Allow-Origin': '*',
455
- });
513
+ };
514
+ const corsOrigin = getCorOrigin(req);
515
+ if (corsOrigin) sseHeaders['Access-Control-Allow-Origin'] = corsOrigin;
516
+ res.writeHead(200, sseHeaders);
456
517
  res.write(`data: ${JSON.stringify(getSnapshot())}\n\n`);
457
518
  sseClients.add(res);
458
519
  req.on('close', () => sseClients.delete(res));
@@ -474,13 +535,14 @@ export function startDashboard(deps) {
474
535
  '/api/logs': () => parseLogs(tailLog(100)),
475
536
  '/api/shares': getSharesData,
476
537
  '/api/self': getSelfData,
538
+ '/api/skills': getSkillsData,
477
539
  '/api/capabilities': getCapabilitiesData,
478
540
  '/api/knowledge': getKnowledgeData,
479
541
  };
480
542
 
481
543
  if (routes[path]) {
482
544
  try {
483
- sendJson(res, routes[path]());
545
+ sendJson(res, routes[path](), req);
484
546
  } catch (err) {
485
547
  res.writeHead(500, { 'Content-Type': 'application/json' });
486
548
  res.end(JSON.stringify({ error: err.message }));
@@ -13,7 +13,8 @@ const ARCHITECTURE_FILE = join(CODEBASE_DIR, 'architecture.md');
13
13
  // Files to always skip during scanning
14
14
  const SKIP_PATTERNS = [
15
15
  'node_modules', '.git', 'package-lock.json', 'yarn.lock',
16
- '.env', '.DS_Store', 'dist/', 'build/', 'coverage/',
16
+ '.env', '.env.local', '.env.production', '.env.staging', '.env.development', '.env.test',
17
+ '.DS_Store', 'dist/', 'build/', 'coverage/',
17
18
  ];
18
19
 
19
20
  export class CodebaseKnowledge {
@@ -0,0 +1,386 @@
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
+ import { genId } from '../utils/ids.js';
6
+ import { todayDateStr } from '../utils/date.js';
7
+
8
+ /**
9
+ * DaydreamEngine — Artificial Daydreaming via MAP-Elites
10
+ *
11
+ * Implements a quality-diversity search over the space of creative thoughts.
12
+ * Instead of optimising for a single "best" idea, MAP-Elites maintains an
13
+ * archive of the best idea *per niche*, producing a diverse repertoire of
14
+ * high-quality insights that Rachel can draw on.
15
+ *
16
+ * ─── How It Works ────────────────────────────────────────────────────
17
+ *
18
+ * 1. **generateThought()** — Produce a candidate thought by cross-
19
+ * pollinating concepts from the knowledge base (via LLM).
20
+ *
21
+ * 2. **evaluateFitness()** — Score the thought on novelty, depth,
22
+ * and actionability (via LLM or heuristic).
23
+ *
24
+ * 3. **classifyNiche()** — Map the thought to a cell in the
25
+ * behaviour-descriptor grid (domain × cognitiveStrategy).
26
+ *
27
+ * 4. **storeInArchive()** — If the cell is empty or the new thought
28
+ * beats the incumbent, replace it.
29
+ *
30
+ * 5. **runCycle()** — Orchestrate one full MAP-Elites iteration
31
+ * (steps 1–4), called periodically by the LifeEngine.
32
+ *
33
+ * ─── Behaviour Descriptors ──────────────────────────────────────────
34
+ *
35
+ * Axis 1 — Domain:
36
+ * technical | creative | philosophical | interpersonal | strategic | self_improvement
37
+ *
38
+ * Axis 2 — Cognitive Strategy (from MetacognitionMonitor):
39
+ * analytical_decomposition | analogical_reasoning | first_principles |
40
+ * lateral_thinking | divergent_exploration | dialectical_reasoning |
41
+ * counterfactual_thinking | abductive_inference
42
+ *
43
+ * Archive size = 6 domains × 8 strategies = 48 cells.
44
+ *
45
+ * ─── Persistence ────────────────────────────────────────────────────
46
+ *
47
+ * Archive and run history are stored as JSON in ~/.kernelbot/life/daydream/.
48
+ *
49
+ * @see MetacognitionMonitor — provides cognitive-strategy vocabulary and self-awareness context
50
+ * @see LifeEngine — triggers daydream cycles during idle "think" activities
51
+ */
52
+
53
+ const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
54
+
55
+ /** The domain axis of the MAP-Elites grid. */
56
+ const DOMAINS = [
57
+ 'technical',
58
+ 'creative',
59
+ 'philosophical',
60
+ 'interpersonal',
61
+ 'strategic',
62
+ 'self_improvement',
63
+ ];
64
+
65
+ /** The cognitive-strategy axis of the MAP-Elites grid. */
66
+ const STRATEGIES = [
67
+ 'analytical_decomposition',
68
+ 'analogical_reasoning',
69
+ 'first_principles',
70
+ 'lateral_thinking',
71
+ 'divergent_exploration',
72
+ 'dialectical_reasoning',
73
+ 'counterfactual_thinking',
74
+ 'abductive_inference',
75
+ ];
76
+
77
+ /**
78
+ * A single thought stored in the archive.
79
+ * @typedef {object} DaydreamThought
80
+ * @property {string} id — Unique ID (prefix 'dd')
81
+ * @property {string} date — ISO date string (YYYY-MM-DD)
82
+ * @property {number} timestamp — Epoch ms
83
+ * @property {string} content — The synthesised thought / insight
84
+ * @property {string} domain — Which domain axis cell
85
+ * @property {string} strategy — Which strategy axis cell
86
+ * @property {number} fitness — Composite fitness score (0–1)
87
+ * @property {object} fitnessBreakdown — { novelty, depth, actionability } each 0–1
88
+ * @property {string[]} seedConcepts — Knowledge-base concepts that seeded this thought
89
+ * @property {number} generation — Which MAP-Elites cycle produced this
90
+ */
91
+
92
+ /**
93
+ * Run-level stats for a single MAP-Elites cycle.
94
+ * @typedef {object} CycleResult
95
+ * @property {number} generation — Cycle number
96
+ * @property {string} date — ISO date string
97
+ * @property {boolean} stored — Whether the thought entered the archive
98
+ * @property {string|null} replacedId — ID of the thought it displaced (if any)
99
+ * @property {DaydreamThought} thought — The candidate thought
100
+ */
101
+
102
+ export class DaydreamEngine {
103
+ /**
104
+ * @param {object} [opts]
105
+ * @param {string} [opts.basePath] — Override base directory (for testing)
106
+ * @param {object} [opts.metacognition] — MetacognitionMonitor instance (for context)
107
+ * @param {object} [opts.knowledgeBasePath] — Path to the knowledge-base directory
108
+ */
109
+ constructor(opts = {}) {
110
+ const lifeDir = opts.basePath || LIFE_DIR;
111
+ this._dir = join(lifeDir, 'daydream');
112
+ this._archiveFile = join(this._dir, 'archive.json');
113
+ this._historyFile = join(this._dir, 'history.json');
114
+ this._metacognition = opts.metacognition || null;
115
+ this._knowledgeBasePath = opts.knowledgeBasePath || '/root/kernelbot/knowledge_base';
116
+
117
+ mkdirSync(this._dir, { recursive: true });
118
+
119
+ this._archive = this._loadFile(this._archiveFile, {});
120
+ this._history = this._loadFile(this._historyFile, []);
121
+ this._generation = this._history.length;
122
+ }
123
+
124
+ // ── MAP-Elites Core Loop ─────────────────────────────────────────
125
+
126
+ /**
127
+ * Run one full MAP-Elites cycle: generate → evaluate → classify → store.
128
+ *
129
+ * This is the top-level method that the LifeEngine should call during
130
+ * idle "think" activities. Each call produces one candidate thought and
131
+ * attempts to place it in the archive.
132
+ *
133
+ * @returns {Promise<CycleResult>} Result of this cycle
134
+ */
135
+ async runCycle() {
136
+ const logger = getLogger();
137
+ this._generation++;
138
+ logger.info(`[Daydream] Starting MAP-Elites cycle #${this._generation}`);
139
+
140
+ // Step 1 — Generate a candidate thought
141
+ const thought = await this.generateThought();
142
+
143
+ // Step 2 — Evaluate its fitness
144
+ const fitness = await this.evaluateFitness(thought);
145
+ thought.fitness = fitness.composite;
146
+ thought.fitnessBreakdown = fitness;
147
+
148
+ // Step 3 — Classify into a niche
149
+ const niche = this.classifyNiche(thought);
150
+ thought.domain = niche.domain;
151
+ thought.strategy = niche.strategy;
152
+
153
+ // Step 4 — Attempt to store in the archive
154
+ const storeResult = this.storeInArchive(thought);
155
+
156
+ // Record history
157
+ const result = {
158
+ generation: this._generation,
159
+ date: todayDateStr(),
160
+ stored: storeResult.stored,
161
+ replacedId: storeResult.replacedId,
162
+ thought,
163
+ };
164
+ this._history.push(result);
165
+
166
+ // Cap history at 200 entries
167
+ if (this._history.length > 200) {
168
+ this._history = this._history.slice(-200);
169
+ }
170
+ this._saveFile(this._historyFile, this._history);
171
+
172
+ logger.info(
173
+ `[Daydream] Cycle #${this._generation} complete: ` +
174
+ `domain=${thought.domain}, strategy=${thought.strategy}, ` +
175
+ `fitness=${thought.fitness.toFixed(2)}, stored=${storeResult.stored}`
176
+ );
177
+
178
+ return result;
179
+ }
180
+
181
+ // ── Step 1: Thought Generation ───────────────────────────────────
182
+
183
+ /**
184
+ * Generate a candidate thought by cross-pollinating concepts from the
185
+ * knowledge base.
186
+ *
187
+ * Phase 1 (skeleton): Returns a placeholder thought structure.
188
+ * Phase 2 (future): Will sample random knowledge-base entries, build a
189
+ * creative prompt, and call the LLM to synthesise a
190
+ * novel insight that bridges the sampled concepts.
191
+ *
192
+ * @returns {Promise<DaydreamThought>}
193
+ */
194
+ async generateThought() {
195
+ // TODO Phase 2: Sample 2–3 random entries from the knowledge base
196
+ // TODO Phase 2: Build a creative cross-pollination prompt
197
+ // TODO Phase 2: Call LLM to generate a synthesised insight
198
+ // TODO Phase 2: Parse the LLM response into a DaydreamThought
199
+
200
+ return {
201
+ id: genId('dd'),
202
+ date: todayDateStr(),
203
+ timestamp: Date.now(),
204
+ content: '', // Will be filled by LLM in Phase 2
205
+ domain: '', // Will be classified in classifyNiche()
206
+ strategy: '', // Will be classified in classifyNiche()
207
+ fitness: 0,
208
+ fitnessBreakdown: { novelty: 0, depth: 0, actionability: 0 },
209
+ seedConcepts: [], // Will hold sampled KB entry titles
210
+ generation: this._generation,
211
+ };
212
+ }
213
+
214
+ // ── Step 2: Fitness Evaluation ───────────────────────────────────
215
+
216
+ /**
217
+ * Evaluate a thought's fitness across three dimensions:
218
+ * - **Novelty**: How different is this from existing archive entries?
219
+ * - **Depth**: How substantive and well-reasoned is the insight?
220
+ * - **Actionability**: Could this lead to a concrete goal, project, or behaviour change?
221
+ *
222
+ * Phase 1 (skeleton): Returns zeroed scores.
223
+ * Phase 2 (future): Will use LLM-as-judge and/or heuristic comparison
224
+ * against existing archive entries to score each axis.
225
+ *
226
+ * @param {DaydreamThought} thought — The candidate thought to evaluate
227
+ * @returns {Promise<{ novelty: number, depth: number, actionability: number, composite: number }>}
228
+ */
229
+ async evaluateFitness(thought) {
230
+ // TODO Phase 2: Compare thought.content against existing archive for novelty
231
+ // TODO Phase 2: LLM-as-judge scoring for depth and actionability
232
+ // TODO Phase 2: Compute weighted composite score
233
+
234
+ return {
235
+ novelty: 0,
236
+ depth: 0,
237
+ actionability: 0,
238
+ composite: 0,
239
+ };
240
+ }
241
+
242
+ // ── Step 3: Niche Classification ─────────────────────────────────
243
+
244
+ /**
245
+ * Map a thought to a cell in the MAP-Elites behaviour-descriptor grid.
246
+ *
247
+ * The grid has two axes:
248
+ * - Domain (6 values): technical, creative, philosophical, interpersonal, strategic, self_improvement
249
+ * - Strategy (8 values): analytical_decomposition, analogical_reasoning, etc.
250
+ *
251
+ * Phase 1 (skeleton): Uses the thought's pre-set domain/strategy or defaults.
252
+ * Phase 2 (future): Will use LLM classification or keyword heuristics to
253
+ * determine the most fitting niche.
254
+ *
255
+ * @param {DaydreamThought} thought — The thought to classify
256
+ * @returns {{ domain: string, strategy: string }}
257
+ */
258
+ classifyNiche(thought) {
259
+ // TODO Phase 2: LLM or heuristic classification of domain + strategy
260
+
261
+ const domain = DOMAINS.includes(thought.domain) ? thought.domain : DOMAINS[0];
262
+ const strategy = STRATEGIES.includes(thought.strategy) ? thought.strategy : STRATEGIES[0];
263
+
264
+ return { domain, strategy };
265
+ }
266
+
267
+ // ── Step 4: Archive Storage ──────────────────────────────────────
268
+
269
+ /**
270
+ * Attempt to store a thought in the MAP-Elites archive.
271
+ *
272
+ * Archive is keyed by "domain::strategy". If the cell is empty, the thought
273
+ * is inserted. If occupied, the thought replaces the incumbent only if its
274
+ * fitness is strictly higher.
275
+ *
276
+ * @param {DaydreamThought} thought — The thought to store
277
+ * @returns {{ stored: boolean, replacedId: string|null }}
278
+ */
279
+ storeInArchive(thought) {
280
+ const key = `${thought.domain}::${thought.strategy}`;
281
+ const existing = this._archive[key];
282
+
283
+ if (!existing || thought.fitness > existing.fitness) {
284
+ const replacedId = existing?.id || null;
285
+ this._archive[key] = thought;
286
+ this._saveFile(this._archiveFile, this._archive);
287
+ return { stored: true, replacedId };
288
+ }
289
+
290
+ return { stored: false, replacedId: null };
291
+ }
292
+
293
+ // ── Queries ──────────────────────────────────────────────────────
294
+
295
+ /**
296
+ * Get the full archive as an object keyed by "domain::strategy".
297
+ * @returns {Record<string, DaydreamThought>}
298
+ */
299
+ getArchive() {
300
+ return { ...this._archive };
301
+ }
302
+
303
+ /**
304
+ * Get the number of filled cells out of the total grid size.
305
+ * @returns {{ filled: number, total: number, coverage: number }}
306
+ */
307
+ getCoverage() {
308
+ const total = DOMAINS.length * STRATEGIES.length;
309
+ const filled = Object.keys(this._archive).length;
310
+ return { filled, total, coverage: total > 0 ? Math.round((filled / total) * 100) : 0 };
311
+ }
312
+
313
+ /**
314
+ * Get the top N thoughts across the archive, ranked by fitness.
315
+ * @param {number} n
316
+ * @returns {DaydreamThought[]}
317
+ */
318
+ getTopThoughts(n = 5) {
319
+ return Object.values(this._archive)
320
+ .sort((a, b) => b.fitness - a.fitness)
321
+ .slice(0, n);
322
+ }
323
+
324
+ /**
325
+ * Get recent cycle history.
326
+ * @param {number} limit
327
+ * @returns {CycleResult[]}
328
+ */
329
+ getHistory(limit = 10) {
330
+ return this._history.slice(-limit);
331
+ }
332
+
333
+ /**
334
+ * Build a context block summarising the daydream archive for use in LLM prompts.
335
+ * Gives the thinking self a window into its creative repertoire.
336
+ *
337
+ * @returns {string|null}
338
+ */
339
+ buildContextBlock() {
340
+ const coverage = this.getCoverage();
341
+ if (coverage.filled === 0) return null;
342
+
343
+ const top = this.getTopThoughts(3);
344
+ const sections = ['## Creative Repertoire (Daydream Archive)'];
345
+ sections.push(`Coverage: ${coverage.filled}/${coverage.total} niches (${coverage.coverage}%)`);
346
+ sections.push(`Total cycles: ${this._generation}`);
347
+
348
+ if (top.length > 0) {
349
+ sections.push('Top insights:');
350
+ for (const t of top) {
351
+ const label = `${t.domain} × ${t.strategy.replace(/_/g, ' ')}`;
352
+ const snippet = t.content ? t.content.slice(0, 120) : '(pending)';
353
+ sections.push(`- [${label}] (fitness ${t.fitness.toFixed(2)}): ${snippet}`);
354
+ }
355
+ }
356
+
357
+ let block = sections.join('\n');
358
+ if (block.length > 600) block = block.slice(0, 600) + '\n...';
359
+ return block;
360
+ }
361
+
362
+ /**
363
+ * Get the grid dimensions — useful for external visualisation or testing.
364
+ * @returns {{ domains: string[], strategies: string[] }}
365
+ */
366
+ static getGridDimensions() {
367
+ return { domains: [...DOMAINS], strategies: [...STRATEGIES] };
368
+ }
369
+
370
+ // ── File Helpers ─────────────────────────────────────────────────
371
+
372
+ _loadFile(filePath, defaultValue) {
373
+ if (existsSync(filePath)) {
374
+ try {
375
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
376
+ } catch {
377
+ return structuredClone(defaultValue);
378
+ }
379
+ }
380
+ return structuredClone(defaultValue);
381
+ }
382
+
383
+ _saveFile(filePath, data) {
384
+ writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
385
+ }
386
+ }
@@ -308,6 +308,7 @@ export class LifeEngine {
308
308
  // Auto-recover after 1 hour since last failure
309
309
  if (info.lastFailure && now - info.lastFailure > 3600_000) {
310
310
  delete failures[type];
311
+ this._saveState(); // Persist the deletion so it survives restarts
311
312
  } else {
312
313
  weights[type] = 0;
313
314
  logger.debug(`[LifeEngine] Suppressing "${type}" due to ${info.count} consecutive failures`);
@@ -32,13 +32,14 @@ export class EvolutionTracker {
32
32
  return {
33
33
  proposals: raw.proposals || [],
34
34
  lessons: raw.lessons || [],
35
- stats: { ...DEFAULT_DATA.stats, ...raw.stats },
35
+ // Deep-copy DEFAULT_DATA.stats to avoid mutating the module-level default
36
+ stats: { ...structuredClone(DEFAULT_DATA.stats), ...raw.stats },
36
37
  };
37
38
  } catch {
38
- return { ...DEFAULT_DATA, proposals: [], lessons: [] };
39
+ return { proposals: [], lessons: [], stats: structuredClone(DEFAULT_DATA.stats) };
39
40
  }
40
41
  }
41
- return { ...DEFAULT_DATA, proposals: [], lessons: [] };
42
+ return { proposals: [], lessons: [], stats: structuredClone(DEFAULT_DATA.stats) };
42
43
  }
43
44
 
44
45
  _save() {