@yemi33/minions 0.1.2003 → 0.1.2004

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/dashboard.js CHANGED
@@ -38,7 +38,7 @@ const os = require('os');
38
38
  const { safeRead, safeReadOrNull, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeJsonNoRestore, safeUnlink, mutateJsonFileLocked, mutateTextFileLocked, mutateControl, mutateCooldowns, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, WORK_TYPE, WORKTREE_REQUIRING_TYPES, reopenWorkItem } = shared;
39
39
  const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
40
40
  getSkills, getInbox, getNotesWithMeta, getPullRequests,
41
- getEngineLog, getMetrics, getKnowledgeBaseEntries, getProjectGitStatus, timeSince,
41
+ getEngineLog, getMetrics, getKnowledgeBaseEntries, getKnowledgeBaseEntriesSnapshot, getProjectGitStatus, timeSince,
42
42
  MINIONS_DIR, AGENTS_DIR, ENGINE_DIR, INBOX_DIR, DISPATCH_PATH, PRD_DIR } = queries;
43
43
 
44
44
  // Dev vs binary differentiation. When two dashboards run side-by-side (npm
@@ -912,6 +912,16 @@ function _steeringDeliveryState(agentId) {
912
912
 
913
913
  const PLANS_DIR = path.join(MINIONS_DIR, 'plans');
914
914
 
915
+ // W-mpetru8a000s123a — /api/plans cache. Dashboard auto-polls every 4s; the
916
+ // pre-cache cold path walked 4 directories (plans/, prd/, archive variants),
917
+ // sync-stat'd every file, and parsed/regex-scanned each .md → 2-3s blocking.
918
+ // 5s TTL means external .md edits surface within 5s. Mutation handlers must
919
+ // call invalidatePlansCache() for immediate visibility.
920
+ let _plansCache = null;
921
+ let _plansCacheTs = 0;
922
+ const PLANS_CACHE_TTL_MS = 5000;
923
+ function invalidatePlansCache() { _plansCache = null; _plansCacheTs = 0; }
924
+
915
925
  // Resolve a plan/PRD file path: .json files live in prd/, .md files in plans/
916
926
  // Validates that the file stays within the expected directory to prevent path traversal.
917
927
  function resolvePlanPath(file) {
@@ -4279,6 +4289,7 @@ const server = http.createServer(async (req, res) => {
4279
4289
  });
4280
4290
  if (existingVerify) {
4281
4291
  invalidateStatusCache();
4292
+ invalidatePlansCache();
4282
4293
  return jsonReply(res, 200, { ok: true, verifyId: existingVerify.id });
4283
4294
  }
4284
4295
  }
@@ -4298,6 +4309,7 @@ const server = http.createServer(async (req, res) => {
4298
4309
  const verify = items.find(w => w.sourcePlan === body.file && w.itemType === 'verify');
4299
4310
  if (verify) {
4300
4311
  invalidateStatusCache();
4312
+ invalidatePlansCache();
4301
4313
  return jsonReply(res, 200, { ok: true, verifyId: verify.id });
4302
4314
  }
4303
4315
  }
@@ -4894,6 +4906,7 @@ const server = http.createServer(async (req, res) => {
4894
4906
 
4895
4907
  const planFile = 'manual-' + shared.uid() + '.json';
4896
4908
  safeWrite(path.join(PRD_DIR, planFile), manualPrd.plan);
4909
+ invalidatePlansCache();
4897
4910
  return jsonReply(res, 200, { ok: true, id: manualPrd.id, file: planFile });
4898
4911
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
4899
4912
  }
@@ -4949,6 +4962,7 @@ const server = http.createServer(async (req, res) => {
4949
4962
  } catch (e) { console.error('work item sync:', e.message); }
4950
4963
  }
4951
4964
 
4965
+ invalidatePlansCache();
4952
4966
  return jsonReply(res, 200, { ok: true, item, workItemSynced });
4953
4967
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
4954
4968
  }
@@ -4995,6 +5009,7 @@ const server = http.createServer(async (req, res) => {
4995
5009
  d.meta?.item?.sourcePlan === body.source && d.meta?.item?.id === body.itemId
4996
5010
  );
4997
5011
 
5012
+ invalidatePlansCache();
4998
5013
  return jsonReply(res, 200, { ok: true, cancelled });
4999
5014
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
5000
5015
  }
@@ -5339,7 +5354,7 @@ const server = http.createServer(async (req, res) => {
5339
5354
  }
5340
5355
 
5341
5356
  async function handleKnowledgeList(req, res) {
5342
- const entries = getKnowledgeBaseEntries();
5357
+ const entries = await getKnowledgeBaseEntries();
5343
5358
  const result = {};
5344
5359
  for (const cat of shared.KB_CATEGORIES) result[cat] = [];
5345
5360
  for (const e of entries) {
@@ -5353,7 +5368,11 @@ const server = http.createServer(async (req, res) => {
5353
5368
  // Source of truth: kb-sweep-state.json + PID liveness — the in-process
5354
5369
  // sweep moved to a detached runner so in-memory globals are no longer
5355
5370
  // authoritative (they die with the dashboard).
5371
+ // W-mpetru8a000s123a: yield event loop before readSweepLiveness (sync
5372
+ // process.kill + safeJson) so a single /api/knowledge handler can't
5373
+ // chain three sync blocks back-to-back during a stall window.
5356
5374
  try {
5375
+ await new Promise(r => setImmediate(r));
5357
5376
  const { readSweepLiveness } = require('./engine/kb-sweep');
5358
5377
  const liveness = readSweepLiveness({ entryCount: entries.length });
5359
5378
  if (liveness.inFlight && liveness.alive) {
@@ -5386,7 +5405,7 @@ const server = http.createServer(async (req, res) => {
5386
5405
  const {
5387
5406
  readSweepLiveness, staleGuardMs, KB_SWEEP_STATE_PATH, KB_SWEEP_LOG_PATH, KB_SWEEP_RUNNER_PATH,
5388
5407
  } = require('./engine/kb-sweep');
5389
- const entryCount = (queries.getKnowledgeBaseEntries() || []).length;
5408
+ const entryCount = ((await queries.getKnowledgeBaseEntries()) || []).length;
5390
5409
  const guardMs = staleGuardMs(entryCount);
5391
5410
 
5392
5411
  // Synchronous pre-claim BEFORE awaiting the body so a concurrent POST
@@ -5482,11 +5501,11 @@ const server = http.createServer(async (req, res) => {
5482
5501
  }
5483
5502
 
5484
5503
 
5485
- function handleKnowledgeSweepStatus(req, res) {
5504
+ async function handleKnowledgeSweepStatus(req, res) {
5486
5505
  // Source of truth = kb-sweep-state.json + PID liveness. Globals are gone —
5487
5506
  // the runner is detached, so its lifecycle is independent of this process.
5488
5507
  const { readSweepLiveness } = require('./engine/kb-sweep');
5489
- const entries = queries.getKnowledgeBaseEntries() || [];
5508
+ const entries = (await queries.getKnowledgeBaseEntries()) || [];
5490
5509
  const liveness = readSweepLiveness({ entryCount: entries.length });
5491
5510
  const diskState = safeJson(path.join(ENGINE_DIR, 'kb-sweep-state.json'));
5492
5511
  let inFlight = false;
@@ -5513,13 +5532,19 @@ const server = http.createServer(async (req, res) => {
5513
5532
  }
5514
5533
 
5515
5534
  async function handlePlansList(req, res) {
5535
+ const now = Date.now();
5536
+ if (_plansCache && (now - _plansCacheTs) < PLANS_CACHE_TTL_MS) {
5537
+ return jsonReply(res, 200, _plansCache);
5538
+ }
5539
+ const fsp = fs.promises;
5516
5540
  const dirs = [
5517
5541
  { dir: PLANS_DIR, archived: false },
5518
5542
  { dir: path.join(PLANS_DIR, 'archive'), archived: true },
5519
5543
  { dir: PRD_DIR, archived: false },
5520
5544
  { dir: path.join(PRD_DIR, 'archive'), archived: true },
5521
5545
  ];
5522
- // Load work items to check for completed plan-to-prd conversions
5546
+ // Load work items to check for completed plan-to-prd conversions.
5547
+ // safeJsonArr is sync but reads a single small file — leave as is.
5523
5548
  const centralWi = safeJsonArr(path.join(MINIONS_DIR, 'work-items.json'));
5524
5549
  const completedPrdFiles = new Set(
5525
5550
  centralWi.filter(w => w.type === 'plan-to-prd' && DONE_STATUSES.has(w.status) && w.planFile)
@@ -5527,18 +5552,21 @@ const server = http.createServer(async (req, res) => {
5527
5552
  );
5528
5553
  const plans = [];
5529
5554
  for (const { dir, archived } of dirs) {
5530
- const allFiles = safeReadDir(dir).filter(f => f.endsWith('.json') || f.endsWith('.md'));
5531
- for (const f of allFiles) {
5555
+ const allFiles = (await fsp.readdir(dir).catch(() => []))
5556
+ .filter(f => f.endsWith('.json') || f.endsWith('.md'));
5557
+ const dirResults = await Promise.all(allFiles.map(async f => {
5532
5558
  const filePath = path.join(dir, f);
5533
- const content = safeRead(filePath) || '';
5534
- let updatedAt = '';
5535
- try { updatedAt = new Date(fs.statSync(filePath).mtimeMs).toISOString(); } catch { /* optional */ }
5559
+ const [content, stat] = await Promise.all([
5560
+ fsp.readFile(filePath, 'utf8').catch(() => ''),
5561
+ fsp.stat(filePath).catch(() => null),
5562
+ ]);
5563
+ const updatedAt = stat ? new Date(stat.mtimeMs).toISOString() : '';
5536
5564
  const isJson = f.endsWith('.json');
5537
5565
  if (isJson) {
5538
5566
  try {
5539
5567
  const plan = JSON.parse(content);
5540
5568
  const status = plan.status || 'active';
5541
- plans.push({
5569
+ return {
5542
5570
  file: f, format: 'prd', archived,
5543
5571
  project: plan.project || '',
5544
5572
  summary: plan.plan_summary || '',
@@ -5556,15 +5584,15 @@ const server = http.createServer(async (req, res) => {
5556
5584
  archiveReady: plan._archiveReady || false,
5557
5585
  archiveReadyAt: plan._archiveReadyAt || null,
5558
5586
  planStale: plan.planStale || false,
5559
- });
5560
- } catch { /* JSON parse fallback */ }
5587
+ };
5588
+ } catch { return null; /* JSON parse fallback */ }
5561
5589
  } else {
5562
5590
  const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
5563
5591
  const projectMatch = content.match(/\*\*Project:\*\*\s*(.+)/m);
5564
5592
  const authorMatch = content.match(/\*\*Author:\*\*\s*(.+)/m);
5565
5593
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/m);
5566
5594
  const versionMatch = f.match(/-v(\d+)/);
5567
- plans.push({
5595
+ return {
5568
5596
  file: f, format: 'draft', archived,
5569
5597
  project: projectMatch ? projectMatch[1].trim() : '',
5570
5598
  summary: titleMatch ? titleMatch[1].trim() : f.replace('.md', ''),
@@ -5578,11 +5606,14 @@ const server = http.createServer(async (req, res) => {
5578
5606
  requiresApproval: false,
5579
5607
  revisionFeedback: null,
5580
5608
  version: versionMatch ? parseInt(versionMatch[1]) : null,
5581
- });
5609
+ };
5582
5610
  }
5583
- }
5611
+ }));
5612
+ for (const r of dirResults) if (r) plans.push(r);
5584
5613
  }
5585
5614
  plans.sort((a, b) => (b.generatedAt || '').localeCompare(a.generatedAt || ''));
5615
+ _plansCache = plans;
5616
+ _plansCacheTs = Date.now();
5586
5617
  return jsonReply(res, 200, plans);
5587
5618
  }
5588
5619
 
@@ -5617,6 +5648,7 @@ const server = http.createServer(async (req, res) => {
5617
5648
  }
5618
5649
 
5619
5650
  invalidateStatusCache();
5651
+ invalidatePlansCache();
5620
5652
  return jsonReply(res, 200, { ok: true, unarchivedSource });
5621
5653
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
5622
5654
  }
@@ -5747,6 +5779,7 @@ const server = http.createServer(async (req, res) => {
5747
5779
  }
5748
5780
 
5749
5781
  invalidateStatusCache();
5782
+ invalidatePlansCache();
5750
5783
  return jsonReply(res, 200, { ok: true, status: 'approved', resumedWorkItems: resumed, diffAwareUpdate: diffAwareQueued });
5751
5784
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
5752
5785
  }
@@ -5862,6 +5895,7 @@ const server = http.createServer(async (req, res) => {
5862
5895
  }, { defaultValue: { pending: [], active: [], completed: [] } });
5863
5896
 
5864
5897
  invalidateStatusCache();
5898
+ invalidatePlansCache();
5865
5899
  return jsonReply(res, 200, { ok: true, status: 'paused', resetWorkItems: reset });
5866
5900
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
5867
5901
  }
@@ -5895,6 +5929,7 @@ const server = http.createServer(async (req, res) => {
5895
5929
  });
5896
5930
  if (!queueResult.queued) return jsonReply(res, 200, { ok: true, alreadyQueued: true, id: queueResult.id });
5897
5931
  invalidateStatusCache();
5932
+ invalidatePlansCache();
5898
5933
  return jsonReply(res, 200, { ok: true, id: queueResult.id });
5899
5934
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
5900
5935
  }
@@ -5913,6 +5948,7 @@ const server = http.createServer(async (req, res) => {
5913
5948
  return data;
5914
5949
  }, { defaultValue: {} });
5915
5950
 
5951
+ invalidatePlansCache();
5916
5952
  return jsonReply(res, 200, { ok: true, status: 'rejected' });
5917
5953
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
5918
5954
  }
@@ -6040,6 +6076,7 @@ const server = http.createServer(async (req, res) => {
6040
6076
  }
6041
6077
 
6042
6078
  invalidateStatusCache();
6079
+ invalidatePlansCache();
6043
6080
  return jsonReply(res, 200, { ok: true, cleanedWorkItems: cleaned, cleanedDispatches: dispatchCleaned });
6044
6081
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
6045
6082
  }
@@ -6115,6 +6152,7 @@ const server = http.createServer(async (req, res) => {
6115
6152
  } catch (e) { console.error('plan worktree cleanup:', e.message); }
6116
6153
 
6117
6154
  invalidateStatusCache();
6155
+ invalidatePlansCache();
6118
6156
  const payload = { ok: true, archived: body.file, archivedSource, cancelledItems };
6119
6157
  if (archiveWarnings.length > 0) payload.warnings = archiveWarnings;
6120
6158
  return jsonReply(res, 200, payload);
@@ -6149,6 +6187,7 @@ const server = http.createServer(async (req, res) => {
6149
6187
  }
6150
6188
 
6151
6189
  invalidateStatusCache();
6190
+ invalidatePlansCache();
6152
6191
  return jsonReply(res, 200, { ok: true, unarchivedSource });
6153
6192
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
6154
6193
  }
@@ -6180,6 +6219,7 @@ const server = http.createServer(async (req, res) => {
6180
6219
  planFile: body.file,
6181
6220
  });
6182
6221
  });
6222
+ invalidatePlansCache();
6183
6223
  return jsonReply(res, 200, { ok: true, status: 'revision-requested', workItemId: id });
6184
6224
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
6185
6225
  }
@@ -10071,6 +10111,12 @@ if (require.main === module) {
10071
10111
  console.log(` Projects: ${PROJECTS.map(p => `${p.name} (${p.localPath})`).join(', ')}`);
10072
10112
  console.log(`\n Auto-refreshes every 4s. Ctrl+C to stop.\n`);
10073
10113
 
10114
+ // W-mpetru8a000s123a — warm the async KB cache so synchronous callers
10115
+ // (getWorkItems) see real data on first /api/status instead of an empty
10116
+ // snapshot. Fire-and-forget; tolerant of warming failure.
10117
+ Promise.resolve(queries.getKnowledgeBaseEntries())
10118
+ .catch(err => console.warn(`[dashboard] KB cache warm failed: ${err && err.message}`));
10119
+
10074
10120
  // Auto-open the browser unless suppressed. `minions restart` and the
10075
10121
  // upgrade path set MINIONS_NO_AUTO_OPEN=1 because the CLI orchestrates the
10076
10122
  // open itself after observing whether an existing tab reconnected.
package/engine/cli.js CHANGED
@@ -895,6 +895,21 @@ const commands = {
895
895
  }
896
896
  })();
897
897
 
898
+ // W-mpetru8a000s123a — warm the async KB cache so synchronous callers
899
+ // (getWorkItems, getKnowledgeBaseIndex, playbook render) see real data
900
+ // on first read instead of the empty snapshot. Fire-and-forget; the cache
901
+ // updates as soon as the scan resolves and any inflight async caller
902
+ // shares the same promise.
903
+ (function warmKnowledgeBaseCache() {
904
+ try {
905
+ const queries = require('./queries');
906
+ Promise.resolve(queries.getKnowledgeBaseEntries())
907
+ .catch(err => e.log('warn', `KB cache warm failed: ${err && err.message}`));
908
+ } catch (err) {
909
+ e.log('warn', `KB cache warm setup failed: ${err.message}`);
910
+ }
911
+ })();
912
+
898
913
  // Initial tick
899
914
  e.tick();
900
915
 
@@ -484,7 +484,7 @@ async function _runKbSweepImpl(opts = {}) {
484
484
  };
485
485
  const t0 = Date.now();
486
486
 
487
- const entries = queries.getKnowledgeBaseEntries();
487
+ const entries = await queries.getKnowledgeBaseEntries();
488
488
  if (entries.length < 2) { summary.summary = 'nothing to sweep (< 2 entries)'; summary.durationMs = Date.now() - t0; return summary; }
489
489
 
490
490
  const requestPinned = Array.isArray(opts.pinnedKeys)
@@ -535,7 +535,7 @@ async function _runKbSweepImpl(opts = {}) {
535
535
  summary.sweptArchivePruned = _pruneOldSwept();
536
536
 
537
537
  // Final tallies — re-walk surviving entries for accurate bytesAfter
538
- const finalEntries = queries.getKnowledgeBaseEntries();
538
+ const finalEntries = await queries.getKnowledgeBaseEntries();
539
539
  for (const e of finalEntries) {
540
540
  if (pinned.has(`knowledge/${e.cat}/${e.file}`)) continue;
541
541
  const fp = path.join(KB_DIR, e.cat, e.file);
package/engine/queries.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  const fs = require('fs');
8
+ const fsp = require('fs').promises;
8
9
  const path = require('path');
9
10
  const os = require('os');
10
11
  const shared = require('./shared');
@@ -1058,35 +1059,52 @@ function getCommandIndex(config) {
1058
1059
 
1059
1060
  // ── Knowledge Base ──────────────────────────────────────────────────────────
1060
1061
 
1061
- let _kbCache = null;
1062
+ // W-mpetru8a000s123a async KB scan + stale-while-revalidate cache.
1063
+ // Async fs prevents event-loop stalls on /api/knowledge cold-cache hits
1064
+ // (previously ~11s of blocking readFileSync/statSync). Sync callers
1065
+ // (getWorkItems, getKnowledgeBaseIndex, playbook render) use the in-memory
1066
+ // snapshot via getKnowledgeBaseEntriesSnapshot() to avoid forcing async
1067
+ // propagation through ~17 callers of getWorkItems.
1068
+ let _kbCache = null; // last good snapshot — never nulled by invalidate
1062
1069
  let _kbCacheTs = 0;
1070
+ let _kbCacheStale = true; // invalidate marks stale; snapshot kept for sync readers
1071
+ let _kbRefreshPromise = null; // in-flight scan dedupe
1063
1072
  const KB_CACHE_TTL = 30000; // 30s — KB changes infrequently
1064
1073
 
1065
1074
  function invalidateKnowledgeBaseCache() {
1066
- _kbCache = null;
1075
+ _kbCacheStale = true;
1067
1076
  _kbCacheTs = 0;
1068
1077
  }
1069
1078
 
1070
- function getKnowledgeBaseEntries() {
1071
- const now = Date.now();
1072
- if (_kbCache && (now - _kbCacheTs) < KB_CACHE_TTL) return _kbCache;
1079
+ /**
1080
+ * Synchronous snapshot — returns last-known KB entries from memory, never
1081
+ * touches disk. Returns [] only until the first async getKnowledgeBaseEntries()
1082
+ * resolves. Used by sync callers (getWorkItems, getKnowledgeBaseIndex,
1083
+ * playbook render) that historically called the sync version.
1084
+ */
1085
+ function getKnowledgeBaseEntriesSnapshot() {
1086
+ return Array.isArray(_kbCache) ? _kbCache : [];
1087
+ }
1073
1088
 
1089
+ async function _scanKnowledgeBase() {
1074
1090
  const entries = [];
1075
1091
  for (const cat of KB_CATEGORIES) {
1076
1092
  const catDir = path.join(KNOWLEDGE_DIR, cat);
1077
- const files = safeReadDir(catDir).filter(f => f.endsWith('.md'));
1078
- for (const f of files) {
1093
+ const files = (await fsp.readdir(catDir).catch(() => [])).filter(f => f.endsWith('.md'));
1094
+ const fileResults = await Promise.all(files.map(async f => {
1079
1095
  const filePath = path.join(catDir, f);
1080
- const content = safeRead(filePath) || '';
1096
+ const [content, stat] = await Promise.all([
1097
+ fsp.readFile(filePath, 'utf8').catch(() => ''),
1098
+ fsp.stat(filePath).catch(() => null),
1099
+ ]);
1081
1100
  const titleMatch = content.match(/^#\s+(.+)/m);
1082
1101
  const title = titleMatch ? titleMatch[1].trim() : f.replace(/\.md$/, '');
1083
1102
  const agentMatch = f.match(/^\d{4}-\d{2}-\d{2}-(\w+)-/);
1084
1103
  const dateMatch = f.match(/^(\d{4}-\d{2}-\d{2})/) || content.match(/^date:\s*(\d{4}-\d{2}-\d{2})$/m);
1085
1104
  const sourceMatch = content.match(/^source:\s*(.+)/m);
1086
- let sortTs = 0;
1087
- try { sortTs = fs.statSync(filePath).mtimeMs || 0; } catch {}
1105
+ const sortTs = (stat && stat.mtimeMs) || 0;
1088
1106
  const displayDate = dateMatch ? dateMatch[1] : (sortTs ? new Date(sortTs).toISOString().slice(0, 10) : '');
1089
- entries.push({
1107
+ return {
1090
1108
  cat, file: f, title,
1091
1109
  agent: agentMatch ? agentMatch[1] : '',
1092
1110
  date: displayDate,
@@ -1094,22 +1112,36 @@ function getKnowledgeBaseEntries() {
1094
1112
  source: sourceMatch ? sourceMatch[1].trim() : '',
1095
1113
  preview: content.slice(0, 200),
1096
1114
  size: content.length,
1097
- });
1098
- }
1115
+ };
1116
+ }));
1117
+ entries.push(...fileResults);
1099
1118
  }
1100
1119
  entries.sort((a, b) =>
1101
1120
  (b.sortTs || 0) - (a.sortTs || 0) ||
1102
1121
  (b.date || '').localeCompare(a.date || '') ||
1103
1122
  a.title.localeCompare(b.title)
1104
1123
  );
1105
- _kbCache = entries;
1106
- _kbCacheTs = now;
1107
1124
  return entries;
1108
1125
  }
1109
1126
 
1127
+ async function getKnowledgeBaseEntries() {
1128
+ const now = Date.now();
1129
+ if (!_kbCacheStale && _kbCache && (now - _kbCacheTs) < KB_CACHE_TTL) return _kbCache;
1130
+ if (_kbRefreshPromise) return _kbRefreshPromise;
1131
+ _kbRefreshPromise = _scanKnowledgeBase()
1132
+ .then(entries => {
1133
+ _kbCache = entries;
1134
+ _kbCacheTs = Date.now();
1135
+ _kbCacheStale = false;
1136
+ return _kbCache;
1137
+ })
1138
+ .finally(() => { _kbRefreshPromise = null; });
1139
+ return _kbRefreshPromise;
1140
+ }
1141
+
1110
1142
  function getKnowledgeBaseIndex() {
1111
1143
  try {
1112
- const entries = getKnowledgeBaseEntries();
1144
+ const entries = getKnowledgeBaseEntriesSnapshot();
1113
1145
  if (entries.length === 0) return '';
1114
1146
  let index = '## Knowledge Base Reference\n\n';
1115
1147
  index += 'Deep-reference docs from past work. Read the file if you need detail.\n\n';
@@ -1227,8 +1259,9 @@ function getWorkItems(config) {
1227
1259
  const _agentDirCache = {};
1228
1260
  const _inboxFiles = safeReadDir(INBOX_DIR);
1229
1261
  const _archiveFiles = safeReadDir(ARCHIVE_DIR);
1230
- // Use cached KB entries (includes source frontmatter field)
1231
- const _kbEntries = getKnowledgeBaseEntries();
1262
+ // Use snapshot sync access; cold start before any async warm returns [].
1263
+ // Best-effort enrichment for work item _artifacts.notes, not correctness-critical.
1264
+ const _kbEntries = getKnowledgeBaseEntriesSnapshot();
1232
1265
  for (const item of allItems) {
1233
1266
  const arts = {};
1234
1267
  const agentId = item.dispatched_to || item.agent;
@@ -1754,7 +1787,7 @@ module.exports = {
1754
1787
  collectCommandFiles, getCommandIndex,
1755
1788
 
1756
1789
  // Knowledge base
1757
- getKnowledgeBaseEntries, getKnowledgeBaseIndex,
1790
+ getKnowledgeBaseEntries, getKnowledgeBaseEntriesSnapshot, getKnowledgeBaseIndex,
1758
1791
 
1759
1792
  // Work items & PRD
1760
1793
  getWorkItems, getPrdInfo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2003",
3
+ "version": "0.1.2004",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"