@yemi33/minions 0.1.86 → 0.1.88
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/CHANGELOG.md +23 -0
- package/dashboard/js/render-plans.js +42 -10
- package/dashboard/js/render-prd.js +3 -2
- package/dashboard.js +32 -0
- package/engine/consolidation.js +28 -35
- package/engine/dispatch.js +1 -8
- package/engine/lifecycle.js +95 -101
- package/engine/meeting.js +72 -8
- package/engine/playbook.js +21 -11
- package/engine/shared.js +36 -5
- package/engine.js +8 -16
- package/package.json +1 -1
- package/tools/generate-pixel-art.js +134 -0
- package/tools/pixel-robot.bmp +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.88 (2026-04-01)
|
|
4
|
+
|
|
5
|
+
### Dashboard
|
|
6
|
+
- dashboard.js
|
|
7
|
+
- dashboard/js/render-plans.js
|
|
8
|
+
- dashboard/js/render-prd.js
|
|
9
|
+
|
|
10
|
+
## 0.1.87 (2026-04-01)
|
|
11
|
+
|
|
12
|
+
### Engine
|
|
13
|
+
- engine.js
|
|
14
|
+
- engine/consolidation.js
|
|
15
|
+
- engine/dispatch.js
|
|
16
|
+
- engine/lifecycle.js
|
|
17
|
+
- engine/meeting.js
|
|
18
|
+
- engine/playbook.js
|
|
19
|
+
- engine/shared.js
|
|
20
|
+
|
|
21
|
+
### Other
|
|
22
|
+
- test/unit.test.js
|
|
23
|
+
- tools/generate-pixel-art.js
|
|
24
|
+
- tools/pixel-robot.bmp
|
|
25
|
+
|
|
3
26
|
## 0.1.86 (2026-03-31)
|
|
4
27
|
|
|
5
28
|
### Dashboard
|
|
@@ -461,9 +461,16 @@ async function planView(file) {
|
|
|
461
461
|
'onclick="planDelete(\'' + escHtml(normalizedFile) + '\')">Delete</button>' +
|
|
462
462
|
'</div>';
|
|
463
463
|
document.getElementById('modal-title').innerHTML = escHtml(title) + (versionLabel ? ' <span style="font-size:11px;font-weight:700;padding:1px 6px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue)">' + escHtml(versionLabel) + '</span>' : '') + lastModLabel + actionBtns;
|
|
464
|
-
document.getElementById('modal-body')
|
|
465
|
-
|
|
466
|
-
|
|
464
|
+
const modalBody = document.getElementById('modal-body');
|
|
465
|
+
if (normalizedFile.endsWith('.json')) {
|
|
466
|
+
modalBody.textContent = text;
|
|
467
|
+
modalBody.style.fontFamily = 'Consolas, monospace';
|
|
468
|
+
modalBody.style.whiteSpace = 'pre-wrap';
|
|
469
|
+
} else {
|
|
470
|
+
modalBody.innerHTML = '<div style="font-size:12px;line-height:1.6">' + renderMd(text) + '</div>';
|
|
471
|
+
modalBody.style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
472
|
+
modalBody.style.whiteSpace = 'normal';
|
|
473
|
+
}
|
|
467
474
|
_modalDocContext = { title, content: text, selection: '' };
|
|
468
475
|
_modalFilePath = resolvedPath || ((normalizedFile.endsWith('.json') ? 'prd/' : 'plans/') + normalizedFile); showModalQa();
|
|
469
476
|
// Clear notification badge when opening this document
|
|
@@ -509,6 +516,25 @@ async function planDelete(file) {
|
|
|
509
516
|
} catch (e) { alert('Error: ' + e.message); }
|
|
510
517
|
}
|
|
511
518
|
|
|
519
|
+
async function planArchive(file) {
|
|
520
|
+
if (!confirm('Archive PRD "' + file + '"? Work items will be preserved.')) return;
|
|
521
|
+
try {
|
|
522
|
+
const res = await fetch('/api/plans/archive', {
|
|
523
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
524
|
+
body: JSON.stringify({ file })
|
|
525
|
+
});
|
|
526
|
+
if (res.ok) {
|
|
527
|
+
closeModal();
|
|
528
|
+
showToast('cmd-toast', 'PRD archived', true);
|
|
529
|
+
refreshPlans();
|
|
530
|
+
refresh();
|
|
531
|
+
} else {
|
|
532
|
+
const d = await res.json();
|
|
533
|
+
alert('Archive failed: ' + (d.error || 'unknown'));
|
|
534
|
+
}
|
|
535
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
536
|
+
}
|
|
537
|
+
|
|
512
538
|
async function planPause(file, btn) {
|
|
513
539
|
if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Pausing...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
|
|
514
540
|
try {
|
|
@@ -578,9 +604,15 @@ async function planOpenInDocChat(file) {
|
|
|
578
604
|
try { title = JSON.parse(raw).plan_summary || file; } catch {}
|
|
579
605
|
}
|
|
580
606
|
document.getElementById('modal-title').textContent = 'Edit: ' + title;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
607
|
+
if (normalizedFile.endsWith('.json')) {
|
|
608
|
+
document.getElementById('modal-body').textContent = text;
|
|
609
|
+
document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
|
|
610
|
+
document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
|
|
611
|
+
} else {
|
|
612
|
+
document.getElementById('modal-body').innerHTML = '<div style="font-size:12px;line-height:1.6">' + renderMd(text) + '</div>';
|
|
613
|
+
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
614
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
615
|
+
}
|
|
584
616
|
_modalDocContext = { title: title, content: text, selection: '' };
|
|
585
617
|
_modalFilePath = resolvedPath || ((normalizedFile.endsWith('.json') ? 'prd/' : 'plans/') + normalizedFile); showModalQa();
|
|
586
618
|
const card = findCardForFile(_modalFilePath);
|
|
@@ -612,9 +644,9 @@ async function openVerifyGuide(file) {
|
|
|
612
644
|
const content = await fetch('/api/plans/' + encodeURIComponent(normalizedFile)).then(r => r.text());
|
|
613
645
|
document.getElementById('modal-title').innerHTML = 'Manual Testing Guide' +
|
|
614
646
|
' <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;margin-left:8px;vertical-align:middle" onclick="openArchivedPrdModal()">Back</button>';
|
|
615
|
-
document.getElementById('modal-body').
|
|
616
|
-
document.getElementById('modal-body').style.fontFamily = '
|
|
617
|
-
document.getElementById('modal-body').style.whiteSpace = '
|
|
647
|
+
document.getElementById('modal-body').innerHTML = '<div style="font-size:12px;line-height:1.6">' + renderMd(content) + '</div>';
|
|
648
|
+
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
649
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
618
650
|
_modalDocContext = { title: 'Manual Testing Guide', content, selection: '' };
|
|
619
651
|
_modalFilePath = 'prd/' + normalizedFile; showModalQa();
|
|
620
652
|
const card = findCardForFile(_modalFilePath);
|
|
@@ -642,4 +674,4 @@ async function triggerVerify(file, btn) {
|
|
|
642
674
|
} catch (e) { if (btn) { btn.textContent = btn.dataset.origText || 'Verify'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } alert('Error: ' + e.message); }
|
|
643
675
|
}
|
|
644
676
|
|
|
645
|
-
window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, renderPlans, openArchivedPlansModal, qaDisablePrdButtons, showPlanVersionActions, qaJustSave, planExecute, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
|
|
677
|
+
window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, renderPlans, openArchivedPlansModal, qaDisablePrdButtons, showPlanVersionActions, qaJustSave, planExecute, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planArchive, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
|
|
@@ -36,7 +36,7 @@ function renderPrd(prd, prog) {
|
|
|
36
36
|
actions = ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:4px" onclick="planApprove(\'' + escHtml(prdFile) + '\',this)">Approve</button>';
|
|
37
37
|
} else if (effectiveStatus === 'completed') {
|
|
38
38
|
actions = ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:4px" onclick="triggerVerify(\'' + escHtml(prdFile) + '\',this)">Verify</button>' +
|
|
39
|
-
' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-left:4px" onclick="
|
|
39
|
+
' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-left:4px" onclick="planArchive(\'' + escHtml(prdFile) + '\')">Archive</button>';
|
|
40
40
|
} else if (effectiveStatus === 'in-progress') {
|
|
41
41
|
actions = ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="planPause(\'' + escHtml(prdFile) + '\',this)">Pause</button>';
|
|
42
42
|
} else if (effectiveStatus === 'paused') {
|
|
@@ -238,7 +238,7 @@ function renderPrdProgress(prog) {
|
|
|
238
238
|
? '<span onclick="event.stopPropagation();triggerVerify(\'' + escHtml(g.file) + '\',this)" style="color:var(--green);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">Verify</span>'
|
|
239
239
|
: '<span onclick="event.stopPropagation();planPause(\'' + escHtml(g.file) + '\',this)" style="color:var(--yellow);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.3);border-radius:3px">Pause</span>';
|
|
240
240
|
const archiveBtn = isCompleted
|
|
241
|
-
? '<span onclick="event.stopPropagation();
|
|
241
|
+
? '<span onclick="event.stopPropagation();planArchive(\'' + escHtml(g.file) + '\')" style="color:var(--muted);cursor:pointer;font-size:9px;padding:1px 6px;background:var(--surface);border:1px solid var(--border);border-radius:3px">Archive</span>'
|
|
242
242
|
: '';
|
|
243
243
|
const deleteBtn = '<span onclick="event.stopPropagation();planDelete(\'' + escHtml(g.file) + '\')" style="color:var(--red);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.3);border-radius:3px">Delete</span>';
|
|
244
244
|
const sourcePlanLink = g.sourcePlan
|
|
@@ -250,6 +250,7 @@ function renderPrdProgress(prog) {
|
|
|
250
250
|
'<span style="color:var(--text)">' + escHtml(summary || g.file) + '</span>' +
|
|
251
251
|
pausedLabel +
|
|
252
252
|
staleLabel +
|
|
253
|
+
'<span style="font-weight:700;font-size:11px;color:' + (done === g.items.length && g.items.length > 0 ? 'var(--green)' : 'var(--text)') + '">' + (g.items.length > 0 ? Math.round((done / g.items.length) * 100) : 0) + '%</span>' +
|
|
253
254
|
'<span style="color:var(--muted);font-weight:400;font-size:10px">' + g.items.length + ' items' +
|
|
254
255
|
(done ? ' · ' + done + ' done' : '') + (wip ? ' · ' + wip + ' active' : '') +
|
|
255
256
|
'</span>' +
|
package/dashboard.js
CHANGED
|
@@ -2065,6 +2065,37 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2065
2065
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
2066
2066
|
}
|
|
2067
2067
|
|
|
2068
|
+
async function handlePlansArchive(req, res) {
|
|
2069
|
+
try {
|
|
2070
|
+
const body = await readBody(req);
|
|
2071
|
+
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
2072
|
+
if (body.file.includes('..') || body.file.includes('\0') || body.file.includes('/') || body.file.includes('\\')) {
|
|
2073
|
+
return jsonReply(res, 400, { error: 'invalid filename' });
|
|
2074
|
+
}
|
|
2075
|
+
const planPath = resolvePlanPath(body.file);
|
|
2076
|
+
if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan not found' });
|
|
2077
|
+
|
|
2078
|
+
// Move to archive directory
|
|
2079
|
+
const archiveDir = body.file.endsWith('.json') ? path.join(PRD_DIR, 'archive') : path.join(PLANS_DIR, 'archive');
|
|
2080
|
+
if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
|
|
2081
|
+
const archivePath = path.join(archiveDir, body.file);
|
|
2082
|
+
fs.renameSync(planPath, archivePath);
|
|
2083
|
+
|
|
2084
|
+
// Mark archived in JSON if PRD
|
|
2085
|
+
if (body.file.endsWith('.json')) {
|
|
2086
|
+
try {
|
|
2087
|
+
const prd = JSON.parse(safeRead(archivePath) || '{}');
|
|
2088
|
+
prd.status = 'archived';
|
|
2089
|
+
prd.archivedAt = new Date().toISOString();
|
|
2090
|
+
safeWrite(archivePath, prd);
|
|
2091
|
+
} catch { /* optional */ }
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
invalidateStatusCache();
|
|
2095
|
+
return jsonReply(res, 200, { ok: true, archived: body.file });
|
|
2096
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2068
2099
|
async function handlePlansRevise(req, res) {
|
|
2069
2100
|
try {
|
|
2070
2101
|
const body = await readBody(req);
|
|
@@ -3083,6 +3114,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3083
3114
|
{ method: 'POST', path: '/api/plans/reject', desc: 'Reject a plan', params: 'file, rejectedBy?, reason?', handler: handlePlansReject },
|
|
3084
3115
|
{ method: 'POST', path: '/api/plans/regenerate', desc: 'Reset pending/failed work items for a plan so they re-materialize', params: 'source', handler: handlePlansRegenerate },
|
|
3085
3116
|
{ method: 'POST', path: '/api/plans/delete', desc: 'Delete a plan file and clean up work items', params: 'file', handler: handlePlansDelete },
|
|
3117
|
+
{ method: 'POST', path: '/api/plans/archive', desc: 'Move a plan/PRD to archive (preserves work items)', params: 'file', handler: handlePlansArchive },
|
|
3086
3118
|
{ method: 'POST', path: '/api/plans/revise', desc: 'Request revision with feedback, dispatches agent to revise', params: 'file, feedback, requestedBy?', handler: handlePlansRevise },
|
|
3087
3119
|
{ method: 'POST', path: '/api/plans/discuss', desc: 'Generate a plan discussion session script for Claude CLI', params: 'file', handler: handlePlansDiscuss },
|
|
3088
3120
|
{ method: 'GET', path: /^\/api\/plans\/archive\/([^?]+)$/, desc: 'Read an archived plan file', handler: handlePlansArchiveRead },
|
package/engine/consolidation.js
CHANGED
|
@@ -8,39 +8,32 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const shared = require('./shared');
|
|
10
10
|
const { safeRead, safeWrite, safeUnlink, runFile, cleanChildEnv,
|
|
11
|
-
parseStreamJsonOutput, classifyInboxItem, KB_CATEGORIES } = shared;
|
|
11
|
+
parseStreamJsonOutput, classifyInboxItem, KB_CATEGORIES, log, dateStamp } = shared;
|
|
12
12
|
const { trackEngineUsage } = require('./llm');
|
|
13
13
|
const queries = require('./queries');
|
|
14
14
|
const { getInboxFiles, getNotes, INBOX_DIR, ENGINE_DIR, MINIONS_DIR,
|
|
15
15
|
NOTES_PATH, KNOWLEDGE_DIR, ARCHIVE_DIR } = queries;
|
|
16
16
|
|
|
17
|
-
// Lazy require — only for log() and dateStamp() which live on engine.js
|
|
18
|
-
let _engine = null;
|
|
19
|
-
function engine() {
|
|
20
|
-
if (!_engine) _engine = require('../engine');
|
|
21
|
-
return _engine;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
17
|
// Track in-flight LLM consolidation to prevent concurrent runs
|
|
25
18
|
let _consolidationInFlight = false;
|
|
26
19
|
let _consolidationStartedAt = 0;
|
|
27
20
|
const _processingFiles = new Set(); // files currently being consolidated (race guard)
|
|
28
21
|
|
|
29
22
|
function consolidateInbox(config) {
|
|
30
|
-
|
|
23
|
+
|
|
31
24
|
const { ENGINE_DEFAULTS } = shared;
|
|
32
25
|
const threshold = config.engine?.inboxConsolidateThreshold || ENGINE_DEFAULTS.inboxConsolidateThreshold;
|
|
33
26
|
const files = getInboxFiles().filter(f => !_processingFiles.has(f));
|
|
34
27
|
if (files.length < threshold) return;
|
|
35
28
|
// Auto-reset stale flag if consolidation has been running for >5 minutes (process died without cleanup)
|
|
36
29
|
if (_consolidationInFlight && (Date.now() - _consolidationStartedAt) > 300000) {
|
|
37
|
-
|
|
30
|
+
log('warn', 'Consolidation flag was stale (>5m) — resetting');
|
|
38
31
|
_consolidationInFlight = false;
|
|
39
32
|
_processingFiles.clear();
|
|
40
33
|
}
|
|
41
34
|
if (_consolidationInFlight) return;
|
|
42
35
|
|
|
43
|
-
|
|
36
|
+
log('info', `Consolidating ${files.length} inbox items into notes.md`);
|
|
44
37
|
|
|
45
38
|
const items = files.map(f => ({
|
|
46
39
|
name: f,
|
|
@@ -54,7 +47,7 @@ function consolidateInbox(config) {
|
|
|
54
47
|
// ─── LLM-Powered Consolidation ──────────────────────────────────────────────
|
|
55
48
|
|
|
56
49
|
function buildConsolidationPrompt(items, existingNotes, kbPaths) {
|
|
57
|
-
|
|
50
|
+
|
|
58
51
|
const kbRefBlock = kbPaths.map(p => `- \`${p.file}\` \u2192 \`${p.kbPath}\``).join('\n');
|
|
59
52
|
const notesBlock = items.map(item =>
|
|
60
53
|
`<note file="${item.name}">\n${(item.content || '').slice(0, 8000)}\n</note>`
|
|
@@ -114,11 +107,11 @@ Respond with ONLY the markdown below — no preamble, no explanation, no code fe
|
|
|
114
107
|
|
|
115
108
|
_Processed N notes, M insights extracted, K duplicates removed._
|
|
116
109
|
|
|
117
|
-
Use today's date: ${
|
|
110
|
+
Use today's date: ${dateStamp()}`;
|
|
118
111
|
}
|
|
119
112
|
|
|
120
113
|
function consolidateWithLLM(items, existingNotes, files, config) {
|
|
121
|
-
|
|
114
|
+
|
|
122
115
|
_consolidationInFlight = true;
|
|
123
116
|
_consolidationStartedAt = Date.now();
|
|
124
117
|
for (const f of files) _processingFiles.add(f);
|
|
@@ -129,7 +122,7 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
129
122
|
const agent = agentMatch ? agentMatch[1] : 'unknown';
|
|
130
123
|
const titleMatch = (item.content || '').match(/^#\s+(.+)/m);
|
|
131
124
|
const titleSlug = titleMatch ? titleMatch[1].toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50) : item.name.replace(/\.md$/, '');
|
|
132
|
-
return { file: item.name, category: cat, kbPath: path.join('knowledge', cat, `${
|
|
125
|
+
return { file: item.name, category: cat, kbPath: path.join('knowledge', cat, `${dateStamp()}-${agent}-${titleSlug}.md`) };
|
|
133
126
|
});
|
|
134
127
|
|
|
135
128
|
const prompt = buildConsolidationPrompt(items, existingNotes, kbPaths);
|
|
@@ -152,7 +145,7 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
152
145
|
'--verbose',
|
|
153
146
|
];
|
|
154
147
|
|
|
155
|
-
|
|
148
|
+
log('info', 'Spawning Haiku for LLM consolidation...');
|
|
156
149
|
|
|
157
150
|
const proc = runFile(process.execPath, [spawnScript, promptPath, sysPromptPath, ...args], {
|
|
158
151
|
cwd: MINIONS_DIR,
|
|
@@ -166,7 +159,7 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
166
159
|
proc.stderr.on('data', d => { stderr += d.toString(); if (stderr.length > 50000) stderr = stderr.slice(-25000); });
|
|
167
160
|
|
|
168
161
|
const timeout = setTimeout(() => {
|
|
169
|
-
|
|
162
|
+
log('warn', 'LLM consolidation timed out after 3m — killing and falling back to regex');
|
|
170
163
|
try { proc.kill('SIGTERM'); } catch { /* process may be dead */ }
|
|
171
164
|
// Escalate to SIGKILL after 10s if process doesn't exit
|
|
172
165
|
setTimeout(() => {
|
|
@@ -174,7 +167,7 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
174
167
|
if (_consolidationInFlight) {
|
|
175
168
|
_consolidationInFlight = false;
|
|
176
169
|
_processingFiles.clear();
|
|
177
|
-
|
|
170
|
+
log('warn', 'Consolidation flag force-reset after SIGKILL');
|
|
178
171
|
}
|
|
179
172
|
}, 10000);
|
|
180
173
|
}, 180000);
|
|
@@ -202,7 +195,7 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
202
195
|
if (sectionIdx >= 0) {
|
|
203
196
|
digest = digest.slice(sectionIdx);
|
|
204
197
|
} else {
|
|
205
|
-
|
|
198
|
+
log('warn', 'LLM consolidation output missing expected format — falling back to regex');
|
|
206
199
|
consolidateWithRegex(items, files);
|
|
207
200
|
_clearProcessingState();
|
|
208
201
|
return;
|
|
@@ -219,17 +212,17 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
219
212
|
const header = sections[0];
|
|
220
213
|
const recent = sections.slice(-8);
|
|
221
214
|
newContent = header + '\n---\n\n### ' + recent.join('\n---\n\n### ');
|
|
222
|
-
|
|
215
|
+
log('info', `Pruned notes.md: removed ${sections.length - 9} old sections`);
|
|
223
216
|
}
|
|
224
217
|
}
|
|
225
218
|
|
|
226
219
|
safeWrite(NOTES_PATH, newContent);
|
|
227
220
|
classifyToKnowledgeBase(items);
|
|
228
221
|
archiveInboxFiles(files);
|
|
229
|
-
|
|
222
|
+
log('info', `LLM consolidation complete: ${files.length} notes processed by Haiku`);
|
|
230
223
|
} else {
|
|
231
|
-
|
|
232
|
-
if (stderr)
|
|
224
|
+
log('warn', `LLM consolidation failed (code=${code}) — falling back to regex`);
|
|
225
|
+
if (stderr) log('debug', `LLM stderr: ${stderr.slice(0, 500)}`);
|
|
233
226
|
consolidateWithRegex(items, files);
|
|
234
227
|
}
|
|
235
228
|
_clearProcessingState();
|
|
@@ -237,7 +230,7 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
237
230
|
|
|
238
231
|
proc.on('error', (err) => {
|
|
239
232
|
clearTimeout(timeout);
|
|
240
|
-
|
|
233
|
+
log('warn', `LLM consolidation spawn error: ${err.message} — falling back to regex`);
|
|
241
234
|
safeUnlink(promptPath);
|
|
242
235
|
safeUnlink(sysPromptPath);
|
|
243
236
|
consolidateWithRegex(items, files);
|
|
@@ -248,7 +241,7 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
248
241
|
// ─── Regex Fallback Consolidation ────────────────────────────────────────────
|
|
249
242
|
|
|
250
243
|
function consolidateWithRegex(items, files) {
|
|
251
|
-
|
|
244
|
+
|
|
252
245
|
const allInsights = [];
|
|
253
246
|
for (const item of items) {
|
|
254
247
|
const content = item.content || '';
|
|
@@ -327,7 +320,7 @@ function consolidateWithRegex(items, files) {
|
|
|
327
320
|
const grouped = {};
|
|
328
321
|
for (const item of deduped) { if (!grouped[item.category]) grouped[item.category] = []; grouped[item.category].push(item); }
|
|
329
322
|
|
|
330
|
-
let entry = `\n\n---\n\n### ${
|
|
323
|
+
let entry = `\n\n---\n\n### ${dateStamp()}: ${title}\n`;
|
|
331
324
|
entry += '**By:** Engine (regex fallback)\n\n';
|
|
332
325
|
for (const [cat, catItems] of Object.entries(grouped)) {
|
|
333
326
|
entry += `#### ${catLabels[cat] || cat} (${catItems.length})\n`;
|
|
@@ -349,13 +342,13 @@ function consolidateWithRegex(items, files) {
|
|
|
349
342
|
safeWrite(NOTES_PATH, newContent);
|
|
350
343
|
classifyToKnowledgeBase(items);
|
|
351
344
|
archiveInboxFiles(files);
|
|
352
|
-
|
|
345
|
+
log('info', `Regex fallback: consolidated ${files.length} notes \u2192 ${deduped.length} insights into notes.md`);
|
|
353
346
|
}
|
|
354
347
|
|
|
355
348
|
// ─── Knowledge Base Classification ───────────────────────────────────────────
|
|
356
349
|
|
|
357
350
|
function classifyToKnowledgeBase(items) {
|
|
358
|
-
|
|
351
|
+
|
|
359
352
|
if (!fs.existsSync(KNOWLEDGE_DIR)) fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
|
|
360
353
|
|
|
361
354
|
const categoryDirs = {};
|
|
@@ -375,20 +368,20 @@ function classifyToKnowledgeBase(items) {
|
|
|
375
368
|
const titleSlug = titleMatch
|
|
376
369
|
? titleMatch[1].toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50)
|
|
377
370
|
: item.name.replace(/\.md$/, '');
|
|
378
|
-
const kbFilename = `${
|
|
371
|
+
const kbFilename = `${dateStamp()}-${agent}-${titleSlug}.md`;
|
|
379
372
|
const kbPath = shared.uniquePath(path.join(categoryDirs[category], kbFilename));
|
|
380
373
|
|
|
381
|
-
const frontmatter = `---\nsource: ${item.name}\nagent: ${agent}\ncategory: ${category}\ndate: ${
|
|
374
|
+
const frontmatter = `---\nsource: ${item.name}\nagent: ${agent}\ncategory: ${category}\ndate: ${dateStamp()}\n---\n\n`;
|
|
382
375
|
try {
|
|
383
376
|
safeWrite(kbPath, frontmatter + content);
|
|
384
377
|
classified++;
|
|
385
378
|
} catch (err) {
|
|
386
|
-
|
|
379
|
+
log('warn', `Failed to classify ${item.name} to knowledge base: ${err.message}`);
|
|
387
380
|
}
|
|
388
381
|
}
|
|
389
382
|
|
|
390
383
|
if (classified > 0) {
|
|
391
|
-
|
|
384
|
+
log('info', `Knowledge base: classified ${classified} note(s) into knowledge/`);
|
|
392
385
|
}
|
|
393
386
|
|
|
394
387
|
// Save KB file count checkpoint so the watchdog can detect unexpected deletions
|
|
@@ -399,14 +392,14 @@ function classifyToKnowledgeBase(items) {
|
|
|
399
392
|
if (fs.existsSync(dir)) count += fs.readdirSync(dir).length;
|
|
400
393
|
}
|
|
401
394
|
safeWrite(path.join(ENGINE_DIR, 'kb-checkpoint.json'), JSON.stringify({ count, updatedAt: new Date().toISOString() }));
|
|
402
|
-
} catch (err) {
|
|
395
|
+
} catch (err) { log('warn', `KB checkpoint: ${err.message}`); }
|
|
403
396
|
}
|
|
404
397
|
|
|
405
398
|
function archiveInboxFiles(files) {
|
|
406
|
-
|
|
399
|
+
|
|
407
400
|
if (!fs.existsSync(ARCHIVE_DIR)) fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
|
|
408
401
|
for (const f of files) {
|
|
409
|
-
try { fs.renameSync(path.join(INBOX_DIR, f), shared.uniquePath(path.join(ARCHIVE_DIR, `${
|
|
402
|
+
try { fs.renameSync(path.join(INBOX_DIR, f), shared.uniquePath(path.join(ARCHIVE_DIR, `${dateStamp()}-${f}`))); } catch (err) { log('warn', `Inbox archive: ${err.message}`); }
|
|
410
403
|
}
|
|
411
404
|
}
|
|
412
405
|
|
package/engine/dispatch.js
CHANGED
|
@@ -10,7 +10,7 @@ const queries = require('./queries');
|
|
|
10
10
|
const { setCooldownFailure } = require('./cooldown');
|
|
11
11
|
|
|
12
12
|
const { safeJson, safeWrite, safeReadDir, mutateJsonFileLocked,
|
|
13
|
-
getProjects, projectWorkItemsPath } = shared;
|
|
13
|
+
getProjects, projectWorkItemsPath, log, ts, dateStamp } = shared;
|
|
14
14
|
const { getConfig, getDispatch, DISPATCH_PATH, INBOX_DIR } = queries;
|
|
15
15
|
|
|
16
16
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
@@ -19,13 +19,6 @@ const MINIONS_DIR = shared.MINIONS_DIR;
|
|
|
19
19
|
let _lifecycle = null;
|
|
20
20
|
function lifecycle() { if (!_lifecycle) _lifecycle = require('./lifecycle'); return _lifecycle; }
|
|
21
21
|
|
|
22
|
-
// ─── Engine utilities (lazy require to avoid circular deps) ──────────────────
|
|
23
|
-
let _engine = null;
|
|
24
|
-
function engine() { if (!_engine) _engine = require('../engine'); return _engine; }
|
|
25
|
-
function log(level, msg, meta) { return engine().log(level, msg, meta); }
|
|
26
|
-
function ts() { return engine().ts(); }
|
|
27
|
-
function dateStamp() { return new Date().toISOString().slice(0, 10); }
|
|
28
|
-
|
|
29
22
|
// ─── Dispatch Mutation ───────────────────────────────────────────────────────
|
|
30
23
|
|
|
31
24
|
function mutateDispatch(mutator) {
|