@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 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').textContent = text;
465
- document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
466
- document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
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
- document.getElementById('modal-body').textContent = text;
582
- document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
583
- document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
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').textContent = content;
616
- document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
617
- document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
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="planDelete(\'' + escHtml(prdFile) + '\')">Archive</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="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();planDelete(\'' + 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>'
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 },
@@ -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
- const e = engine();
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
- e.log('warn', 'Consolidation flag was stale (>5m) — resetting');
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
- e.log('info', `Consolidating ${files.length} inbox items into notes.md`);
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
- const e = engine();
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: ${e.dateStamp()}`;
110
+ Use today's date: ${dateStamp()}`;
118
111
  }
119
112
 
120
113
  function consolidateWithLLM(items, existingNotes, files, config) {
121
- const e = engine();
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, `${e.dateStamp()}-${agent}-${titleSlug}.md`) };
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
- e.log('info', 'Spawning Haiku for LLM consolidation...');
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
- e.log('warn', 'LLM consolidation timed out after 3m — killing and falling back to regex');
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
- e.log('warn', 'Consolidation flag force-reset after SIGKILL');
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
- e.log('warn', 'LLM consolidation output missing expected format — falling back to regex');
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
- e.log('info', `Pruned notes.md: removed ${sections.length - 9} old sections`);
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
- e.log('info', `LLM consolidation complete: ${files.length} notes processed by Haiku`);
222
+ log('info', `LLM consolidation complete: ${files.length} notes processed by Haiku`);
230
223
  } else {
231
- e.log('warn', `LLM consolidation failed (code=${code}) — falling back to regex`);
232
- if (stderr) e.log('debug', `LLM stderr: ${stderr.slice(0, 500)}`);
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
- e.log('warn', `LLM consolidation spawn error: ${err.message} — falling back to regex`);
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
- const e = engine();
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### ${e.dateStamp()}: ${title}\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
- e.log('info', `Regex fallback: consolidated ${files.length} notes \u2192 ${deduped.length} insights into notes.md`);
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
- const e = engine();
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 = `${e.dateStamp()}-${agent}-${titleSlug}.md`;
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: ${e.dateStamp()}\n---\n\n`;
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
- e.log('warn', `Failed to classify ${item.name} to knowledge base: ${err.message}`);
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
- e.log('info', `Knowledge base: classified ${classified} note(s) into knowledge/`);
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) { engine().log('warn', `KB checkpoint: ${err.message}`); }
395
+ } catch (err) { log('warn', `KB checkpoint: ${err.message}`); }
403
396
  }
404
397
 
405
398
  function archiveInboxFiles(files) {
406
- const e = engine();
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, `${e.dateStamp()}-${f}`))); } catch (err) { e.log('warn', `Inbox archive: ${err.message}`); }
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
 
@@ -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) {