@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 +63 -17
- package/engine/cli.js +15 -0
- package/engine/kb-sweep.js +2 -2
- package/engine/queries.js +52 -19
- package/package.json +1 -1
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 =
|
|
5531
|
-
|
|
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 =
|
|
5534
|
-
|
|
5535
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/engine/kb-sweep.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1075
|
+
_kbCacheStale = true;
|
|
1067
1076
|
_kbCacheTs = 0;
|
|
1068
1077
|
}
|
|
1069
1078
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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 =
|
|
1078
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1231
|
-
|
|
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.
|
|
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"
|