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.
- package/bin/kernel.js +5 -5
- package/config.example.yaml +1 -1
- package/package.json +1 -1
- package/skills/business/business-analyst.md +32 -0
- package/skills/business/product-manager.md +32 -0
- package/skills/business/project-manager.md +32 -0
- package/skills/business/startup-advisor.md +32 -0
- package/skills/creative/music-producer.md +32 -0
- package/skills/creative/photographer.md +32 -0
- package/skills/creative/video-producer.md +32 -0
- package/skills/data/bi-analyst.md +37 -0
- package/skills/data/data-scientist.md +38 -0
- package/skills/data/ml-engineer.md +38 -0
- package/skills/design/graphic-designer.md +38 -0
- package/skills/design/product-designer.md +41 -0
- package/skills/design/ui-ux.md +38 -0
- package/skills/education/curriculum-designer.md +32 -0
- package/skills/education/language-teacher.md +32 -0
- package/skills/education/tutor.md +32 -0
- package/skills/engineering/data-eng.md +55 -0
- package/skills/engineering/devops.md +56 -0
- package/skills/engineering/mobile-dev.md +55 -0
- package/skills/engineering/security-eng.md +55 -0
- package/skills/engineering/sr-backend.md +55 -0
- package/skills/engineering/sr-frontend.md +55 -0
- package/skills/finance/accountant.md +35 -0
- package/skills/finance/crypto-defi.md +39 -0
- package/skills/finance/financial-analyst.md +35 -0
- package/skills/healthcare/health-wellness.md +32 -0
- package/skills/healthcare/medical-researcher.md +33 -0
- package/skills/legal/contract-reviewer.md +35 -0
- package/skills/legal/legal-advisor.md +36 -0
- package/skills/marketing/content-marketer.md +38 -0
- package/skills/marketing/growth.md +38 -0
- package/skills/marketing/seo.md +43 -0
- package/skills/marketing/social-media.md +43 -0
- package/skills/writing/academic-writer.md +33 -0
- package/skills/writing/copywriter.md +32 -0
- package/skills/writing/creative-writer.md +32 -0
- package/skills/writing/tech-writer.md +33 -0
- package/src/agent.js +153 -118
- package/src/automation/scheduler.js +36 -3
- package/src/bot.js +147 -64
- package/src/coder.js +30 -8
- package/src/conversation.js +96 -19
- package/src/dashboard/dashboard.css +6 -0
- package/src/dashboard/dashboard.js +28 -1
- package/src/dashboard/index.html +12 -0
- package/src/dashboard/server.js +77 -15
- package/src/life/codebase.js +2 -1
- package/src/life/daydream_engine.js +386 -0
- package/src/life/engine.js +1 -0
- package/src/life/evolution.js +4 -3
- package/src/prompts/orchestrator.js +1 -1
- package/src/prompts/system.js +1 -1
- package/src/prompts/workers.js +8 -1
- package/src/providers/anthropic.js +3 -1
- package/src/providers/base.js +33 -0
- package/src/providers/index.js +1 -1
- package/src/providers/models.js +22 -0
- package/src/providers/openai-compat.js +3 -0
- package/src/services/x-api.js +14 -3
- package/src/skills/loader.js +382 -0
- package/src/swarm/worker-registry.js +2 -2
- package/src/tools/browser.js +10 -3
- package/src/tools/coding.js +16 -0
- package/src/tools/docker.js +13 -0
- package/src/tools/git.js +31 -29
- package/src/tools/jira.js +11 -2
- package/src/tools/monitor.js +9 -1
- package/src/tools/network.js +34 -0
- package/src/tools/orchestrator-tools.js +2 -1
- package/src/tools/os.js +20 -6
- package/src/utils/config.js +1 -1
- package/src/utils/display.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/utils/timeAwareness.js +72 -0
- package/src/worker.js +26 -33
- package/src/skills/catalog.js +0 -506
- 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.
|
|
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');
|
package/src/dashboard/index.html
CHANGED
|
@@ -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>
|
package/src/dashboard/server.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
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
|
-
|
|
509
|
+
const sseHeaders = {
|
|
451
510
|
'Content-Type': 'text/event-stream',
|
|
452
511
|
'Cache-Control': 'no-cache',
|
|
453
512
|
'Connection': 'keep-alive',
|
|
454
|
-
|
|
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 }));
|
package/src/life/codebase.js
CHANGED
|
@@ -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', '.
|
|
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
|
+
}
|
package/src/life/engine.js
CHANGED
|
@@ -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`);
|
package/src/life/evolution.js
CHANGED
|
@@ -32,13 +32,14 @@ export class EvolutionTracker {
|
|
|
32
32
|
return {
|
|
33
33
|
proposals: raw.proposals || [],
|
|
34
34
|
lessons: raw.lessons || [],
|
|
35
|
-
|
|
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 {
|
|
39
|
+
return { proposals: [], lessons: [], stats: structuredClone(DEFAULT_DATA.stats) };
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
|
-
return {
|
|
42
|
+
return { proposals: [], lessons: [], stats: structuredClone(DEFAULT_DATA.stats) };
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
_save() {
|