@yemi33/minions 0.1.85 → 0.1.87

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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.87 (2026-04-01)
4
+
5
+ ### Engine
6
+ - engine.js
7
+ - engine/consolidation.js
8
+ - engine/dispatch.js
9
+ - engine/lifecycle.js
10
+ - engine/meeting.js
11
+ - engine/playbook.js
12
+ - engine/shared.js
13
+
14
+ ### Other
15
+ - test/unit.test.js
16
+ - tools/generate-pixel-art.js
17
+ - tools/pixel-robot.bmp
18
+
19
+ ## 0.1.86 (2026-03-31)
20
+
21
+ ### Dashboard
22
+ - dashboard/js/render-prd.js
23
+
3
24
  ## 0.1.85 (2026-03-31)
4
25
 
5
26
  ### Dashboard
@@ -13,37 +13,54 @@ function renderPrd(prd, prog) {
13
13
  return;
14
14
  }
15
15
 
16
- // Derive status from work items
17
- const allWi = window._lastWorkItems || [];
18
- const prdItems = (prog?.items || []).filter(i => !i._archived);
19
- const implementItems = allWi.filter(w => prdItems.some(pi => pi.id === w.id));
20
- const allDone = implementItems.length > 0 && implementItems.every(w => w.status === 'done' || w.status === 'in-pr' || w.status === 'implemented' || w.status === 'complete');
21
- const hasActive = implementItems.some(w => w.status === 'pending' || w.status === 'dispatched');
22
-
23
- // Find the active PRD file for action buttons
24
- const prdFile = prd.existing?.[0]?.file || '';
25
- const prdStatus = prd.existing?.[0]?.status || '';
26
- const effectiveStatus = allDone && !hasActive ? 'completed' : hasActive ? 'in-progress' : prdStatus || 'active';
27
-
28
16
  const statusColors = { 'completed': 'var(--green)', 'in-progress': 'var(--blue)', 'awaiting-approval': 'var(--yellow)', 'paused': 'var(--muted)', 'approved': 'var(--green)' };
29
17
  const statusLabels = { 'completed': 'Completed', 'in-progress': 'In Progress', 'awaiting-approval': 'Awaiting Approval', 'paused': 'Paused', 'approved': 'Approved' };
30
18
 
31
- let actions = '';
32
- if (prdFile) {
33
- if (effectiveStatus === 'awaiting-approval') {
34
- 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>';
35
- } else if (effectiveStatus === 'completed') {
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="triggerVerify(\'' + escHtml(prdFile) + '\',this)">Verify</button>' +
37
- ' <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>';
38
- } else if (effectiveStatus === 'in-progress') {
39
- 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>';
40
- } else if (effectiveStatus === 'paused') {
41
- 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)">Resume</button>';
19
+ // Show per-PRD status summary in header when multiple PRDs exist
20
+ const existing = prd.existing || [];
21
+ const allWi = window._lastWorkItems || [];
22
+ const prdItems = (prog?.items || []).filter(i => !i._archived);
23
+
24
+ if (existing.length <= 1) {
25
+ // Single PRD show status + actions in header
26
+ const implementItems = allWi.filter(w => prdItems.some(pi => pi.id === w.id));
27
+ const allDone = implementItems.length > 0 && implementItems.every(w => w.status === 'done' || w.status === 'in-pr' || w.status === 'implemented' || w.status === 'complete');
28
+ const hasActive = implementItems.some(w => w.status === 'pending' || w.status === 'dispatched');
29
+ const prdFile = existing[0]?.file || '';
30
+ const prdStatus = existing[0]?.status || '';
31
+ const effectiveStatus = allDone && !hasActive ? 'completed' : hasActive ? 'in-progress' : prdStatus || 'active';
32
+
33
+ let actions = '';
34
+ if (prdFile) {
35
+ if (effectiveStatus === 'awaiting-approval') {
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
+ } else if (effectiveStatus === 'completed') {
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>';
40
+ } else if (effectiveStatus === 'in-progress') {
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
+ } else if (effectiveStatus === 'paused') {
43
+ 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)">Resume</button>';
44
+ }
45
+ }
46
+ badge.innerHTML = '<span style="font-weight:600;font-size:11px;color:' + (statusColors[effectiveStatus] || 'var(--muted)') + '">' + (statusLabels[effectiveStatus] || effectiveStatus) + '</span>' +
47
+ ' <span style="color:var(--muted);font-size:10px">' + (prd.age || '') + '</span>' + actions;
48
+ } else {
49
+ // Multiple PRDs — show count summary, per-PRD details are in renderPrdProgress groups
50
+ const counts = { completed: 0, 'in-progress': 0, 'awaiting-approval': 0, paused: 0 };
51
+ for (const p of existing) {
52
+ const items = prdItems.filter(i => i.source === p.file);
53
+ const wiForPrd = allWi.filter(w => items.some(pi => pi.id === w.id));
54
+ const allDone = wiForPrd.length > 0 && wiForPrd.every(w => w.status === 'done' || w.status === 'in-pr' || w.status === 'implemented' || w.status === 'complete');
55
+ const hasActive = wiForPrd.some(w => w.status === 'pending' || w.status === 'dispatched');
56
+ const s = allDone && !hasActive ? 'completed' : hasActive ? 'in-progress' : p.status || 'active';
57
+ counts[s] = (counts[s] || 0) + 1;
42
58
  }
59
+ const parts = Object.entries(counts).filter(([, n]) => n > 0).map(([s, n]) =>
60
+ '<span style="font-size:10px;color:' + (statusColors[s] || 'var(--muted)') + '">' + n + ' ' + (statusLabels[s] || s).toLowerCase() + '</span>'
61
+ );
62
+ badge.innerHTML = '<span style="font-weight:600;font-size:11px;color:var(--text)">' + existing.length + ' PRDs</span> ' + parts.join(' · ');
43
63
  }
44
-
45
- badge.innerHTML = '<span style="font-weight:600;font-size:11px;color:' + (statusColors[effectiveStatus] || 'var(--muted)') + '">' + (statusLabels[effectiveStatus] || effectiveStatus) + '</span>' +
46
- ' <span style="color:var(--muted);font-size:10px">' + (prd.age || '') + '</span>' + actions;
47
64
  section.innerHTML = '';
48
65
  }
49
66
 
@@ -52,10 +69,10 @@ function renderPrdProgress(prog) {
52
69
  const countEl = document.getElementById('prd-progress-count');
53
70
  if (!prog) { el.innerHTML = ''; countEl.textContent = '—'; return; }
54
71
 
55
- // Compute progress from active (non-archived) items only
72
+ // Compute overall progress from active (non-archived) items
56
73
  const activeItems = (prog.items || []).filter(i => !i._archived);
57
74
  if (activeItems.length > 0) {
58
- const activeDone = activeItems.filter(i => i.status === 'done' || i.status === 'implemented' || i.status === 'in-pr').length; // in-pr counted as done for backward compat
75
+ const activeDone = activeItems.filter(i => i.status === 'done' || i.status === 'implemented' || i.status === 'in-pr').length;
59
76
  countEl.textContent = Math.round((activeDone / activeItems.length) * 100) + '%';
60
77
  } else {
61
78
  countEl.textContent = '—';
@@ -159,7 +176,7 @@ function renderPrdProgress(prog) {
159
176
  (prLinks ? '<span>' + prLinks + '</span>' : '') +
160
177
  '<span class="prd-item-priority ' + (i.priority || '') + '">' + escHtml(i.priority || '') + '</span>' +
161
178
  '<span onclick="event.stopPropagation();prdItemRemove(\'' + src + '\',\'' + iid + '\')" style="color:var(--red);cursor:pointer;font-size:10px;padding:0 4px" title="Remove item">x</span>' +
162
- (i.description ? '<div style="width:100%;font-size:11px;color:var(--muted);padding:2px 0 2px 42px;line-height:1.4">' + escHtml(i.description) + '</div>' : '') +
179
+ (i.description ? '<div style="width:100%;font-size:11px;color:var(--muted);padding:2px 0 2px 42px;line-height:1.4">' + renderMd(i.description) + '</div>' : '') +
163
180
  '</div>';
164
181
  };
165
182
 
@@ -220,6 +237,9 @@ function renderPrdProgress(prog) {
220
237
  : isCompleted
221
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>'
222
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
+ 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>'
242
+ : '';
223
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>';
224
244
  const sourcePlanLink = g.sourcePlan
225
245
  ? '<span onclick="event.stopPropagation();planView(\'' + escHtml(g.sourcePlan) + '\')" style="color:var(--blue);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(56,139,253,0.1);border:1px solid rgba(56,139,253,0.3);border-radius:3px" title="View source plan">&#x1F4C4; Plan</span>'
@@ -244,6 +264,7 @@ function renderPrdProgress(prog) {
244
264
  : '<span style="color:var(--muted);font-weight:400;font-size:10px;margin-left:auto;display:flex;align-items:center;gap:6px">' +
245
265
  sourcePlanLink +
246
266
  pauseResumeBtn +
267
+ archiveBtn +
247
268
  deleteBtn +
248
269
  '</span>') +
249
270
  staleRecovery +
@@ -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) {