@yemi33/minions 0.1.2002 → 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/fre.js +196 -0
- package/dashboard/js/refresh.js +6 -0
- package/dashboard/js/render-prs.js +13 -1
- package/dashboard/layout.html +4 -1
- package/dashboard-build.js +1 -1
- package/dashboard.js +65 -18
- package/engine/cli.js +15 -0
- package/engine/kb-sweep.js +2 -2
- package/engine/queries.js +52 -19
- package/package.json +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// fre.js — First-Run Experience banner for empty-state dashboards (W-mpehchnt000w46da).
|
|
2
|
+
//
|
|
3
|
+
// Renders a two-step onboarding banner at the top of the main content area when
|
|
4
|
+
// the operator has zero projects configured AND has not explicitly dismissed the
|
|
5
|
+
// banner. Self-contained and idempotent — safe to call renderFre() every tick.
|
|
6
|
+
//
|
|
7
|
+
// Trigger: projects.length === 0 AND localStorage.minions_fre_dismissed !== '1'.
|
|
8
|
+
// Auto-hide (no dismissal flag set) when projects.length >= 1 — the banner has
|
|
9
|
+
// served its purpose. If the user later removes all projects we re-show.
|
|
10
|
+
|
|
11
|
+
const FRE_DISMISS_KEY = 'minions_fre_dismissed';
|
|
12
|
+
const FRE_MOUNT_ID = 'fre-banner';
|
|
13
|
+
|
|
14
|
+
function _freIsDismissed() {
|
|
15
|
+
try { return localStorage.getItem(FRE_DISMISS_KEY) === '1'; } catch { return false; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function dismissFre() {
|
|
19
|
+
try { localStorage.setItem(FRE_DISMISS_KEY, '1'); } catch { /* expected */ }
|
|
20
|
+
const mount = document.getElementById(FRE_MOUNT_ID);
|
|
21
|
+
if (mount) mount.innerHTML = '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Reset helper (exposed for tests / debugging only). Not wired to UI.
|
|
25
|
+
function _freReset() {
|
|
26
|
+
try { localStorage.removeItem(FRE_DISMISS_KEY); } catch { /* expected */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Open the Settings modal and scroll to the Default CLI control (id=set-defaultCli).
|
|
30
|
+
// The modal lives in a separate modal layer, not a sidebar page — openSettings()
|
|
31
|
+
// is async (loads /api/settings) so we await it before scrolling.
|
|
32
|
+
async function openSettingsToDefaultCli() {
|
|
33
|
+
if (typeof openSettings !== 'function') return;
|
|
34
|
+
try {
|
|
35
|
+
await openSettings();
|
|
36
|
+
} catch { /* settings fetch failure already toasts */ return; }
|
|
37
|
+
// openSettings injects innerHTML synchronously after the fetch resolves, so
|
|
38
|
+
// set-defaultCli is in the DOM by now. initRuntimeFleetUI() runs separately
|
|
39
|
+
// and hydrates options — but the <select> element itself is already present.
|
|
40
|
+
setTimeout(function() {
|
|
41
|
+
const el = document.getElementById('set-defaultCli');
|
|
42
|
+
if (!el) return;
|
|
43
|
+
try { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { /* old browser */ }
|
|
44
|
+
try { el.focus({ preventScroll: true }); } catch { /* expected */ }
|
|
45
|
+
const prevOutline = el.style.outline;
|
|
46
|
+
el.style.outline = '2px solid var(--blue)';
|
|
47
|
+
el.style.outlineOffset = '2px';
|
|
48
|
+
setTimeout(function() { el.style.outline = prevOutline; el.style.outlineOffset = ''; }, 2000);
|
|
49
|
+
}, 50);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Render the FRE banner into #fre-banner. Idempotent — bails out early when:
|
|
53
|
+
// • mount point missing (layout didn't load yet)
|
|
54
|
+
// • banner explicitly dismissed
|
|
55
|
+
// • projects.length >= 1 (auto-hide)
|
|
56
|
+
// Otherwise renders the two-step card.
|
|
57
|
+
//
|
|
58
|
+
// Accepts the current /api/status payload (or just a projects array, for tests).
|
|
59
|
+
// When passed the full payload, autoMode.defaultCli drives the runtime label.
|
|
60
|
+
function renderFre(statusOrProjects) {
|
|
61
|
+
const mount = document.getElementById(FRE_MOUNT_ID);
|
|
62
|
+
if (!mount) return;
|
|
63
|
+
const isArr = Array.isArray(statusOrProjects);
|
|
64
|
+
const projects = isArr
|
|
65
|
+
? statusOrProjects
|
|
66
|
+
: (statusOrProjects && Array.isArray(statusOrProjects.projects) ? statusOrProjects.projects : []);
|
|
67
|
+
const status = isArr ? (window._lastStatus || {}) : (statusOrProjects || {});
|
|
68
|
+
const projectCount = projects.length;
|
|
69
|
+
// Bail-out paths assign an empty string literal on its own line so the
|
|
70
|
+
// SEC-03 dynamic-innerHTML counter (test/unit.test.js
|
|
71
|
+
// DYNAMIC_INNERHTML_BASELINE) treats them as exempt.
|
|
72
|
+
if (projectCount >= 1) {
|
|
73
|
+
mount.innerHTML = '';
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (_freIsDismissed()) {
|
|
77
|
+
mount.innerHTML = '';
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Resolve the currently-configured runtime CLI for the explainer copy.
|
|
82
|
+
// /api/status surfaces this as autoMode.defaultCli (resolveAgentCli(null, engine)).
|
|
83
|
+
// Fall back to autoMode.ccCli (also defaultCli-derived when ccCli unset) then 'claude'.
|
|
84
|
+
const auto = (status && status.autoMode) || {};
|
|
85
|
+
const runtimeCli = String(auto.defaultCli || auto.ccCli || 'claude');
|
|
86
|
+
|
|
87
|
+
const cardStyle = [
|
|
88
|
+
'margin:12px 24px',
|
|
89
|
+
'padding:16px 20px',
|
|
90
|
+
'background:var(--surface2)',
|
|
91
|
+
'border:1px solid var(--blue)',
|
|
92
|
+
'border-radius:var(--radius-lg)',
|
|
93
|
+
'color:var(--text)',
|
|
94
|
+
'font-size:13px',
|
|
95
|
+
'box-shadow:var(--shadow-md)',
|
|
96
|
+
].join(';');
|
|
97
|
+
|
|
98
|
+
const stepBox = [
|
|
99
|
+
'display:flex',
|
|
100
|
+
'gap:12px',
|
|
101
|
+
'align-items:flex-start',
|
|
102
|
+
'padding:10px 12px',
|
|
103
|
+
'background:var(--surface)',
|
|
104
|
+
'border:1px solid var(--border)',
|
|
105
|
+
'border-radius:var(--radius-sm)',
|
|
106
|
+
].join(';');
|
|
107
|
+
|
|
108
|
+
const stepNum = [
|
|
109
|
+
'display:inline-flex',
|
|
110
|
+
'align-items:center',
|
|
111
|
+
'justify-content:center',
|
|
112
|
+
'width:22px',
|
|
113
|
+
'height:22px',
|
|
114
|
+
'border-radius:var(--radius-full)',
|
|
115
|
+
'background:var(--blue)',
|
|
116
|
+
'color:#fff',
|
|
117
|
+
'font-weight:700',
|
|
118
|
+
'font-size:11px',
|
|
119
|
+
'flex-shrink:0',
|
|
120
|
+
].join(';');
|
|
121
|
+
|
|
122
|
+
const btnPrimary = [
|
|
123
|
+
'padding:6px 14px',
|
|
124
|
+
'background:var(--blue)',
|
|
125
|
+
'color:#fff',
|
|
126
|
+
'border:none',
|
|
127
|
+
'border-radius:var(--radius-sm)',
|
|
128
|
+
'cursor:pointer',
|
|
129
|
+
'font-size:12px',
|
|
130
|
+
'font-weight:600',
|
|
131
|
+
'margin-top:8px',
|
|
132
|
+
].join(';');
|
|
133
|
+
|
|
134
|
+
const btnGhost = [
|
|
135
|
+
'padding:6px 14px',
|
|
136
|
+
'background:transparent',
|
|
137
|
+
'color:var(--blue)',
|
|
138
|
+
'border:1px solid var(--blue)',
|
|
139
|
+
'border-radius:var(--radius-sm)',
|
|
140
|
+
'cursor:pointer',
|
|
141
|
+
'font-size:12px',
|
|
142
|
+
'font-weight:600',
|
|
143
|
+
'margin-top:8px',
|
|
144
|
+
].join(';');
|
|
145
|
+
|
|
146
|
+
const dismissBtn = [
|
|
147
|
+
'background:none',
|
|
148
|
+
'border:none',
|
|
149
|
+
'color:var(--muted)',
|
|
150
|
+
'cursor:pointer',
|
|
151
|
+
'font-size:11px',
|
|
152
|
+
'padding:2px 8px',
|
|
153
|
+
].join(';');
|
|
154
|
+
|
|
155
|
+
// Use textContent-safe interpolation for the runtime label (config-derived, but defensive).
|
|
156
|
+
const safeRuntime = String(runtimeCli).replace(/[<>&"']/g, function(c) {
|
|
157
|
+
return ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' })[c];
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
mount.innerHTML =
|
|
161
|
+
'<div id="fre-card" style="' + cardStyle + '">' +
|
|
162
|
+
'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">' +
|
|
163
|
+
'<div style="font-size:15px;font-weight:700;color:var(--blue)">👋 Welcome to Minions</div>' +
|
|
164
|
+
'<button onclick="dismissFre()" style="' + dismissBtn + '" title="Hide this banner — clear localStorage.minions_fre_dismissed to re-show">Dismiss</button>' +
|
|
165
|
+
'</div>' +
|
|
166
|
+
'<div style="color:var(--muted);font-size:12px;margin-bottom:12px">Two quick steps to get your fleet ready.</div>' +
|
|
167
|
+
'<div style="display:flex;flex-direction:column;gap:10px">' +
|
|
168
|
+
|
|
169
|
+
'<div style="' + stepBox + '">' +
|
|
170
|
+
'<span style="' + stepNum + '">1</span>' +
|
|
171
|
+
'<div style="flex:1">' +
|
|
172
|
+
'<div style="font-weight:600;margin-bottom:2px">Pick your runtime CLI</div>' +
|
|
173
|
+
'<div style="color:var(--muted);font-size:12px;line-height:1.5">' +
|
|
174
|
+
'Minions spawns agents through this CLI. Switch via Settings → Engine → Default CLI (<code>claude</code> or <code>copilot</code>). ' +
|
|
175
|
+
'Currently: <code style="background:var(--bg);padding:1px 6px;border-radius:3px;color:var(--text)">' + safeRuntime + '</code>' +
|
|
176
|
+
'</div>' +
|
|
177
|
+
'<button onclick="openSettingsToDefaultCli()" style="' + btnGhost + '">Open Settings → Default CLI</button>' +
|
|
178
|
+
'</div>' +
|
|
179
|
+
'</div>' +
|
|
180
|
+
|
|
181
|
+
'<div style="' + stepBox + '">' +
|
|
182
|
+
'<span style="' + stepNum + '">2</span>' +
|
|
183
|
+
'<div style="flex:1">' +
|
|
184
|
+
'<div style="font-weight:600;margin-bottom:2px">Add your first project</div>' +
|
|
185
|
+
'<div style="color:var(--muted);font-size:12px;line-height:1.5">' +
|
|
186
|
+
'Projects map a local git worktree to a remote repo. Without a project, agents have nowhere to run.' +
|
|
187
|
+
'</div>' +
|
|
188
|
+
'<button onclick="addProject()" style="' + btnPrimary + '">+ Add Project</button>' +
|
|
189
|
+
'</div>' +
|
|
190
|
+
'</div>' +
|
|
191
|
+
|
|
192
|
+
'</div>' +
|
|
193
|
+
'</div>';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
window.MinionsFre = { renderFre, dismissFre, openSettingsToDefaultCli, _freReset, FRE_DISMISS_KEY };
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -79,6 +79,12 @@ function _processStatusUpdate(data) {
|
|
|
79
79
|
if (_changed('prdProgress', data.prdProgress) || _changed('prdPrs', data.pullRequests?.length)) { renderPrdProgress(data.prdProgress); _cachePrdItems(data.prdProgress); }
|
|
80
80
|
if (_changed('inbox', data.inbox)) renderInbox(data.inbox || []);
|
|
81
81
|
if (_changed('projects', data.projects)) { cmdUpdateProjectList(data.projects || []); renderProjects(data.projects || []); }
|
|
82
|
+
// FRE banner — safe to call every tick (idempotent + cheap). Pass the full
|
|
83
|
+
// status payload so the runtime-CLI explainer reads autoMode.defaultCli from
|
|
84
|
+
// THIS tick rather than the previous one (window._lastStatus is set later).
|
|
85
|
+
if (typeof renderFre === 'function') {
|
|
86
|
+
try { renderFre(data); } catch { /* expected on first load */ }
|
|
87
|
+
}
|
|
82
88
|
if (_changed('notes', data.notes)) renderNotes(data.notes);
|
|
83
89
|
if (_changed('prd', [data.prd, data.prdProgress])) renderPrd(data.prd, data.prdProgress);
|
|
84
90
|
if (_changed('prs', data.pullRequests)) renderPrs(data.pullRequests || []);
|
|
@@ -135,15 +135,27 @@ function openModal(i) {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
function openAddPrModal() {
|
|
138
|
-
const
|
|
138
|
+
const projects = (typeof cmdProjects !== 'undefined' ? cmdProjects : []) || [];
|
|
139
|
+
const projOpts = projects.map(p => {
|
|
139
140
|
const name = typeof p === 'object' ? p.name : p;
|
|
140
141
|
return '<option value="' + escapeHtml(name) + '">' + escapeHtml(name) + '</option>';
|
|
141
142
|
}).join('');
|
|
142
143
|
const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
|
|
143
144
|
|
|
145
|
+
// No-projects warning (W-mpehchnt000w46da) — surfaced when the operator hasn't
|
|
146
|
+
// added any project yet. We still allow the link (read-only tracking is valid),
|
|
147
|
+
// but warn that auto-fix / auto-review can't dispatch without a project worktree.
|
|
148
|
+
const noProjectsWarning = projects.length === 0
|
|
149
|
+
? '<div id="pr-link-no-projects-warning" style="padding:8px 12px;background:rgba(210,153,34,0.15);border:1px solid rgba(210,153,34,0.3);border-radius:var(--radius-sm);color:var(--yellow);font-size:11px;line-height:1.5;margin-bottom:4px">' +
|
|
150
|
+
'<div style="font-weight:700;margin-bottom:2px">⚠ No project configured.</div>' +
|
|
151
|
+
'<div style="color:var(--text)">This PR will be tracked, but auto-fix and auto-review cannot dispatch against it — agents need a project worktree to operate in. Add a project first (Projects tab) to enable automation.</div>' +
|
|
152
|
+
'</div>'
|
|
153
|
+
: '';
|
|
154
|
+
|
|
144
155
|
document.getElementById('modal-title').textContent = 'Link Pull Request';
|
|
145
156
|
document.getElementById('modal-body').innerHTML =
|
|
146
157
|
'<div style="display:flex;flex-direction:column;gap:10px">' +
|
|
158
|
+
noProjectsWarning +
|
|
147
159
|
'<label style="color:var(--text);font-size:var(--text-md)">PR URL <input id="pr-link-url" style="' + inputStyle + '" placeholder="https://github.com/org/repo/pull/123"></label>' +
|
|
148
160
|
'<label style="color:var(--text);font-size:var(--text-md)">Title <input id="pr-link-title" style="' + inputStyle + '" placeholder="Short description (optional — auto-detected from URL)"></label>' +
|
|
149
161
|
'<label style="color:var(--text);font-size:var(--text-md)">Project <select id="pr-link-project" style="' + inputStyle + '"><option value="">Auto-detect from URL (central if no unique match)</option>' + projOpts + '</select></label>' +
|
package/dashboard/layout.html
CHANGED
|
@@ -78,7 +78,10 @@
|
|
|
78
78
|
<a class="sidebar-link" data-page="meetings" href="/meetings">Meetings</a>
|
|
79
79
|
<a class="sidebar-link" data-page="engine" href="/engine">Engine</a>
|
|
80
80
|
</nav>
|
|
81
|
-
<div class="page-content" id="page-content"
|
|
81
|
+
<div class="page-content" id="page-content">
|
|
82
|
+
<div id="fre-banner"></div>
|
|
83
|
+
<!-- __PAGES__ -->
|
|
84
|
+
</div>
|
|
82
85
|
</div>
|
|
83
86
|
|
|
84
87
|
<!-- Floating toast — lives outside .page divs so showToast() works on every page -->
|
package/dashboard-build.js
CHANGED
|
@@ -34,7 +34,7 @@ function buildDashboardHtml() {
|
|
|
34
34
|
'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
|
|
35
35
|
'render-other', 'render-managed', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
|
|
36
36
|
'command-parser', 'command-input', 'command-center', 'command-history',
|
|
37
|
-
'modal', 'modal-qa', 'settings', 'qa', 'refresh'
|
|
37
|
+
'modal', 'modal-qa', 'settings', 'qa', 'fre', 'refresh'
|
|
38
38
|
];
|
|
39
39
|
let jsHtml = '';
|
|
40
40
|
for (const f of jsFiles) {
|
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) {
|
|
@@ -980,7 +990,7 @@ function buildDashboardHtml() {
|
|
|
980
990
|
'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
|
|
981
991
|
'render-other', 'render-managed', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
|
|
982
992
|
'command-parser', 'command-input', 'command-center', 'command-history',
|
|
983
|
-
'modal', 'modal-qa', 'settings', 'qa', 'refresh'
|
|
993
|
+
'modal', 'modal-qa', 'settings', 'qa', 'fre', 'refresh'
|
|
984
994
|
];
|
|
985
995
|
let jsHtml = '';
|
|
986
996
|
for (const f of jsFiles) {
|
|
@@ -1640,6 +1650,7 @@ function _buildStatusSlowState() {
|
|
|
1640
1650
|
ccCli: shared.resolveCcCli(CONFIG.engine),
|
|
1641
1651
|
ccModel: shared.resolveCcModel(CONFIG.engine),
|
|
1642
1652
|
ccEffort: CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort,
|
|
1653
|
+
defaultCli: shared.resolveAgentCli(null, CONFIG.engine),
|
|
1643
1654
|
},
|
|
1644
1655
|
initialized: !!(CONFIG.agents && Object.keys(CONFIG.agents).length > 0),
|
|
1645
1656
|
installId: safeRead(path.join(MINIONS_DIR, '.install-id')).trim() || null,
|
|
@@ -4278,6 +4289,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4278
4289
|
});
|
|
4279
4290
|
if (existingVerify) {
|
|
4280
4291
|
invalidateStatusCache();
|
|
4292
|
+
invalidatePlansCache();
|
|
4281
4293
|
return jsonReply(res, 200, { ok: true, verifyId: existingVerify.id });
|
|
4282
4294
|
}
|
|
4283
4295
|
}
|
|
@@ -4297,6 +4309,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4297
4309
|
const verify = items.find(w => w.sourcePlan === body.file && w.itemType === 'verify');
|
|
4298
4310
|
if (verify) {
|
|
4299
4311
|
invalidateStatusCache();
|
|
4312
|
+
invalidatePlansCache();
|
|
4300
4313
|
return jsonReply(res, 200, { ok: true, verifyId: verify.id });
|
|
4301
4314
|
}
|
|
4302
4315
|
}
|
|
@@ -4893,6 +4906,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4893
4906
|
|
|
4894
4907
|
const planFile = 'manual-' + shared.uid() + '.json';
|
|
4895
4908
|
safeWrite(path.join(PRD_DIR, planFile), manualPrd.plan);
|
|
4909
|
+
invalidatePlansCache();
|
|
4896
4910
|
return jsonReply(res, 200, { ok: true, id: manualPrd.id, file: planFile });
|
|
4897
4911
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
4898
4912
|
}
|
|
@@ -4948,6 +4962,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4948
4962
|
} catch (e) { console.error('work item sync:', e.message); }
|
|
4949
4963
|
}
|
|
4950
4964
|
|
|
4965
|
+
invalidatePlansCache();
|
|
4951
4966
|
return jsonReply(res, 200, { ok: true, item, workItemSynced });
|
|
4952
4967
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
4953
4968
|
}
|
|
@@ -4994,6 +5009,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4994
5009
|
d.meta?.item?.sourcePlan === body.source && d.meta?.item?.id === body.itemId
|
|
4995
5010
|
);
|
|
4996
5011
|
|
|
5012
|
+
invalidatePlansCache();
|
|
4997
5013
|
return jsonReply(res, 200, { ok: true, cancelled });
|
|
4998
5014
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
4999
5015
|
}
|
|
@@ -5338,7 +5354,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5338
5354
|
}
|
|
5339
5355
|
|
|
5340
5356
|
async function handleKnowledgeList(req, res) {
|
|
5341
|
-
const entries = getKnowledgeBaseEntries();
|
|
5357
|
+
const entries = await getKnowledgeBaseEntries();
|
|
5342
5358
|
const result = {};
|
|
5343
5359
|
for (const cat of shared.KB_CATEGORIES) result[cat] = [];
|
|
5344
5360
|
for (const e of entries) {
|
|
@@ -5352,7 +5368,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
5352
5368
|
// Source of truth: kb-sweep-state.json + PID liveness — the in-process
|
|
5353
5369
|
// sweep moved to a detached runner so in-memory globals are no longer
|
|
5354
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.
|
|
5355
5374
|
try {
|
|
5375
|
+
await new Promise(r => setImmediate(r));
|
|
5356
5376
|
const { readSweepLiveness } = require('./engine/kb-sweep');
|
|
5357
5377
|
const liveness = readSweepLiveness({ entryCount: entries.length });
|
|
5358
5378
|
if (liveness.inFlight && liveness.alive) {
|
|
@@ -5385,7 +5405,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5385
5405
|
const {
|
|
5386
5406
|
readSweepLiveness, staleGuardMs, KB_SWEEP_STATE_PATH, KB_SWEEP_LOG_PATH, KB_SWEEP_RUNNER_PATH,
|
|
5387
5407
|
} = require('./engine/kb-sweep');
|
|
5388
|
-
const entryCount = (queries.getKnowledgeBaseEntries() || []).length;
|
|
5408
|
+
const entryCount = ((await queries.getKnowledgeBaseEntries()) || []).length;
|
|
5389
5409
|
const guardMs = staleGuardMs(entryCount);
|
|
5390
5410
|
|
|
5391
5411
|
// Synchronous pre-claim BEFORE awaiting the body so a concurrent POST
|
|
@@ -5481,11 +5501,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
5481
5501
|
}
|
|
5482
5502
|
|
|
5483
5503
|
|
|
5484
|
-
function handleKnowledgeSweepStatus(req, res) {
|
|
5504
|
+
async function handleKnowledgeSweepStatus(req, res) {
|
|
5485
5505
|
// Source of truth = kb-sweep-state.json + PID liveness. Globals are gone —
|
|
5486
5506
|
// the runner is detached, so its lifecycle is independent of this process.
|
|
5487
5507
|
const { readSweepLiveness } = require('./engine/kb-sweep');
|
|
5488
|
-
const entries = queries.getKnowledgeBaseEntries() || [];
|
|
5508
|
+
const entries = (await queries.getKnowledgeBaseEntries()) || [];
|
|
5489
5509
|
const liveness = readSweepLiveness({ entryCount: entries.length });
|
|
5490
5510
|
const diskState = safeJson(path.join(ENGINE_DIR, 'kb-sweep-state.json'));
|
|
5491
5511
|
let inFlight = false;
|
|
@@ -5512,13 +5532,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
5512
5532
|
}
|
|
5513
5533
|
|
|
5514
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;
|
|
5515
5540
|
const dirs = [
|
|
5516
5541
|
{ dir: PLANS_DIR, archived: false },
|
|
5517
5542
|
{ dir: path.join(PLANS_DIR, 'archive'), archived: true },
|
|
5518
5543
|
{ dir: PRD_DIR, archived: false },
|
|
5519
5544
|
{ dir: path.join(PRD_DIR, 'archive'), archived: true },
|
|
5520
5545
|
];
|
|
5521
|
-
// 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.
|
|
5522
5548
|
const centralWi = safeJsonArr(path.join(MINIONS_DIR, 'work-items.json'));
|
|
5523
5549
|
const completedPrdFiles = new Set(
|
|
5524
5550
|
centralWi.filter(w => w.type === 'plan-to-prd' && DONE_STATUSES.has(w.status) && w.planFile)
|
|
@@ -5526,18 +5552,21 @@ const server = http.createServer(async (req, res) => {
|
|
|
5526
5552
|
);
|
|
5527
5553
|
const plans = [];
|
|
5528
5554
|
for (const { dir, archived } of dirs) {
|
|
5529
|
-
const allFiles =
|
|
5530
|
-
|
|
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 => {
|
|
5531
5558
|
const filePath = path.join(dir, f);
|
|
5532
|
-
const content =
|
|
5533
|
-
|
|
5534
|
-
|
|
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() : '';
|
|
5535
5564
|
const isJson = f.endsWith('.json');
|
|
5536
5565
|
if (isJson) {
|
|
5537
5566
|
try {
|
|
5538
5567
|
const plan = JSON.parse(content);
|
|
5539
5568
|
const status = plan.status || 'active';
|
|
5540
|
-
|
|
5569
|
+
return {
|
|
5541
5570
|
file: f, format: 'prd', archived,
|
|
5542
5571
|
project: plan.project || '',
|
|
5543
5572
|
summary: plan.plan_summary || '',
|
|
@@ -5555,15 +5584,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
5555
5584
|
archiveReady: plan._archiveReady || false,
|
|
5556
5585
|
archiveReadyAt: plan._archiveReadyAt || null,
|
|
5557
5586
|
planStale: plan.planStale || false,
|
|
5558
|
-
}
|
|
5559
|
-
} catch { /* JSON parse fallback */ }
|
|
5587
|
+
};
|
|
5588
|
+
} catch { return null; /* JSON parse fallback */ }
|
|
5560
5589
|
} else {
|
|
5561
5590
|
const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
|
|
5562
5591
|
const projectMatch = content.match(/\*\*Project:\*\*\s*(.+)/m);
|
|
5563
5592
|
const authorMatch = content.match(/\*\*Author:\*\*\s*(.+)/m);
|
|
5564
5593
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/m);
|
|
5565
5594
|
const versionMatch = f.match(/-v(\d+)/);
|
|
5566
|
-
|
|
5595
|
+
return {
|
|
5567
5596
|
file: f, format: 'draft', archived,
|
|
5568
5597
|
project: projectMatch ? projectMatch[1].trim() : '',
|
|
5569
5598
|
summary: titleMatch ? titleMatch[1].trim() : f.replace('.md', ''),
|
|
@@ -5577,11 +5606,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
5577
5606
|
requiresApproval: false,
|
|
5578
5607
|
revisionFeedback: null,
|
|
5579
5608
|
version: versionMatch ? parseInt(versionMatch[1]) : null,
|
|
5580
|
-
}
|
|
5609
|
+
};
|
|
5581
5610
|
}
|
|
5582
|
-
}
|
|
5611
|
+
}));
|
|
5612
|
+
for (const r of dirResults) if (r) plans.push(r);
|
|
5583
5613
|
}
|
|
5584
5614
|
plans.sort((a, b) => (b.generatedAt || '').localeCompare(a.generatedAt || ''));
|
|
5615
|
+
_plansCache = plans;
|
|
5616
|
+
_plansCacheTs = Date.now();
|
|
5585
5617
|
return jsonReply(res, 200, plans);
|
|
5586
5618
|
}
|
|
5587
5619
|
|
|
@@ -5616,6 +5648,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5616
5648
|
}
|
|
5617
5649
|
|
|
5618
5650
|
invalidateStatusCache();
|
|
5651
|
+
invalidatePlansCache();
|
|
5619
5652
|
return jsonReply(res, 200, { ok: true, unarchivedSource });
|
|
5620
5653
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
5621
5654
|
}
|
|
@@ -5746,6 +5779,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5746
5779
|
}
|
|
5747
5780
|
|
|
5748
5781
|
invalidateStatusCache();
|
|
5782
|
+
invalidatePlansCache();
|
|
5749
5783
|
return jsonReply(res, 200, { ok: true, status: 'approved', resumedWorkItems: resumed, diffAwareUpdate: diffAwareQueued });
|
|
5750
5784
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
5751
5785
|
}
|
|
@@ -5861,6 +5895,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5861
5895
|
}, { defaultValue: { pending: [], active: [], completed: [] } });
|
|
5862
5896
|
|
|
5863
5897
|
invalidateStatusCache();
|
|
5898
|
+
invalidatePlansCache();
|
|
5864
5899
|
return jsonReply(res, 200, { ok: true, status: 'paused', resetWorkItems: reset });
|
|
5865
5900
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
5866
5901
|
}
|
|
@@ -5894,6 +5929,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5894
5929
|
});
|
|
5895
5930
|
if (!queueResult.queued) return jsonReply(res, 200, { ok: true, alreadyQueued: true, id: queueResult.id });
|
|
5896
5931
|
invalidateStatusCache();
|
|
5932
|
+
invalidatePlansCache();
|
|
5897
5933
|
return jsonReply(res, 200, { ok: true, id: queueResult.id });
|
|
5898
5934
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
5899
5935
|
}
|
|
@@ -5912,6 +5948,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5912
5948
|
return data;
|
|
5913
5949
|
}, { defaultValue: {} });
|
|
5914
5950
|
|
|
5951
|
+
invalidatePlansCache();
|
|
5915
5952
|
return jsonReply(res, 200, { ok: true, status: 'rejected' });
|
|
5916
5953
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
5917
5954
|
}
|
|
@@ -6039,6 +6076,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
6039
6076
|
}
|
|
6040
6077
|
|
|
6041
6078
|
invalidateStatusCache();
|
|
6079
|
+
invalidatePlansCache();
|
|
6042
6080
|
return jsonReply(res, 200, { ok: true, cleanedWorkItems: cleaned, cleanedDispatches: dispatchCleaned });
|
|
6043
6081
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
6044
6082
|
}
|
|
@@ -6114,6 +6152,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
6114
6152
|
} catch (e) { console.error('plan worktree cleanup:', e.message); }
|
|
6115
6153
|
|
|
6116
6154
|
invalidateStatusCache();
|
|
6155
|
+
invalidatePlansCache();
|
|
6117
6156
|
const payload = { ok: true, archived: body.file, archivedSource, cancelledItems };
|
|
6118
6157
|
if (archiveWarnings.length > 0) payload.warnings = archiveWarnings;
|
|
6119
6158
|
return jsonReply(res, 200, payload);
|
|
@@ -6148,6 +6187,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
6148
6187
|
}
|
|
6149
6188
|
|
|
6150
6189
|
invalidateStatusCache();
|
|
6190
|
+
invalidatePlansCache();
|
|
6151
6191
|
return jsonReply(res, 200, { ok: true, unarchivedSource });
|
|
6152
6192
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
6153
6193
|
}
|
|
@@ -6179,6 +6219,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
6179
6219
|
planFile: body.file,
|
|
6180
6220
|
});
|
|
6181
6221
|
});
|
|
6222
|
+
invalidatePlansCache();
|
|
6182
6223
|
return jsonReply(res, 200, { ok: true, status: 'revision-requested', workItemId: id });
|
|
6183
6224
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
6184
6225
|
}
|
|
@@ -10070,6 +10111,12 @@ if (require.main === module) {
|
|
|
10070
10111
|
console.log(` Projects: ${PROJECTS.map(p => `${p.name} (${p.localPath})`).join(', ')}`);
|
|
10071
10112
|
console.log(`\n Auto-refreshes every 4s. Ctrl+C to stop.\n`);
|
|
10072
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
|
+
|
|
10073
10120
|
// Auto-open the browser unless suppressed. `minions restart` and the
|
|
10074
10121
|
// upgrade path set MINIONS_NO_AUTO_OPEN=1 because the CLI orchestrates the
|
|
10075
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"
|