@yemi33/minions 0.1.54 → 0.1.55
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 +9 -0
- package/dashboard/js/refresh.js +64 -45
- package/dashboard.js +36 -1
- package/engine/cli.js +40 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dashboard/js/refresh.js
CHANGED
|
@@ -1,57 +1,76 @@
|
|
|
1
1
|
// refresh.js — Main refresh loop and initialization extracted from dashboard.html
|
|
2
2
|
|
|
3
|
+
function _processStatusUpdate(data) {
|
|
4
|
+
// Detect fresh install — clear stale browser state if install ID changed
|
|
5
|
+
if (data.installId) {
|
|
6
|
+
const prev = localStorage.getItem('minions-install-id');
|
|
7
|
+
if (prev && prev !== data.installId) {
|
|
8
|
+
localStorage.clear();
|
|
9
|
+
console.log('Minions: fresh install detected, cleared browser state');
|
|
10
|
+
}
|
|
11
|
+
localStorage.setItem('minions-install-id', data.installId);
|
|
12
|
+
}
|
|
13
|
+
document.getElementById('ts').textContent = new Date(data.timestamp).toLocaleTimeString();
|
|
14
|
+
const engineState = (data.engine && data.engine.state) ? data.engine.state : 'stopped';
|
|
15
|
+
document.getElementById('setup-banner').style.display = (!data.initialized && engineState !== 'stopped') ? 'block' : 'none';
|
|
16
|
+
renderAgents(data.agents);
|
|
17
|
+
renderPrdProgress(data.prdProgress);
|
|
18
|
+
_cachePrdItems(data.prdProgress);
|
|
19
|
+
renderInbox(data.inbox);
|
|
20
|
+
cmdUpdateAgentList(data.agents);
|
|
21
|
+
cmdUpdateProjectList(data.projects || []);
|
|
22
|
+
renderNotes(data.notes);
|
|
23
|
+
renderPrd(data.prd, data.prdProgress);
|
|
24
|
+
renderPrs(data.pullRequests || []);
|
|
25
|
+
renderArchiveButtons(data.archivedPrds || []);
|
|
26
|
+
renderEngineStatus(data.engine);
|
|
27
|
+
renderDispatch(data.dispatch);
|
|
28
|
+
window._lastDispatch = data.dispatch;
|
|
29
|
+
window._lastWorkItems = data.workItems || [];
|
|
30
|
+
window._lastStatus = data;
|
|
31
|
+
prunePrdRequeueState(window._lastWorkItems);
|
|
32
|
+
renderEngineLog(data.engineLog || []);
|
|
33
|
+
renderProjects(data.projects || []);
|
|
34
|
+
renderMetrics(data.metrics || {});
|
|
35
|
+
renderWorkItems(data.workItems || []);
|
|
36
|
+
renderSkills(data.skills || []);
|
|
37
|
+
renderMcpServers(data.mcpServers || []);
|
|
38
|
+
renderSchedules(data.schedules || []);
|
|
39
|
+
renderPinned(data.pinned || []);
|
|
40
|
+
// Update sidebar counts
|
|
41
|
+
const swi = document.getElementById('sidebar-wi');
|
|
42
|
+
if (swi) swi.textContent = (data.workItems || []).length || '';
|
|
43
|
+
const spr = document.getElementById('sidebar-pr');
|
|
44
|
+
if (spr) spr.textContent = (data.pullRequests || []).length || '';
|
|
45
|
+
// Refresh KB and plans less frequently (every 3rd cycle = ~12s)
|
|
46
|
+
if (!window._kbRefreshCount) window._kbRefreshCount = 0;
|
|
47
|
+
if (window._kbRefreshCount++ % 3 === 0) { refreshKnowledgeBase(); refreshPlans(); }
|
|
48
|
+
}
|
|
49
|
+
|
|
3
50
|
async function refresh() {
|
|
4
51
|
try {
|
|
5
52
|
const data = await fetch('/api/status').then(r => r.json());
|
|
6
|
-
|
|
7
|
-
if (data.installId) {
|
|
8
|
-
const prev = localStorage.getItem('minions-install-id');
|
|
9
|
-
if (prev && prev !== data.installId) {
|
|
10
|
-
localStorage.clear();
|
|
11
|
-
console.log('Minions: fresh install detected, cleared browser state');
|
|
12
|
-
}
|
|
13
|
-
localStorage.setItem('minions-install-id', data.installId);
|
|
14
|
-
}
|
|
15
|
-
document.getElementById('ts').textContent = new Date(data.timestamp).toLocaleTimeString();
|
|
16
|
-
const engineState = (data.engine && data.engine.state) ? data.engine.state : 'stopped';
|
|
17
|
-
document.getElementById('setup-banner').style.display = (!data.initialized && engineState !== 'stopped') ? 'block' : 'none';
|
|
18
|
-
renderAgents(data.agents);
|
|
19
|
-
renderPrdProgress(data.prdProgress);
|
|
20
|
-
_cachePrdItems(data.prdProgress);
|
|
21
|
-
renderInbox(data.inbox);
|
|
22
|
-
cmdUpdateAgentList(data.agents);
|
|
23
|
-
cmdUpdateProjectList(data.projects || []);
|
|
24
|
-
renderNotes(data.notes);
|
|
25
|
-
renderPrd(data.prd, data.prdProgress);
|
|
26
|
-
renderPrs(data.pullRequests || []);
|
|
27
|
-
renderArchiveButtons(data.archivedPrds || []);
|
|
28
|
-
renderEngineStatus(data.engine);
|
|
29
|
-
renderDispatch(data.dispatch);
|
|
30
|
-
window._lastDispatch = data.dispatch;
|
|
31
|
-
window._lastWorkItems = data.workItems || [];
|
|
32
|
-
window._lastStatus = data;
|
|
33
|
-
prunePrdRequeueState(window._lastWorkItems);
|
|
34
|
-
renderEngineLog(data.engineLog || []);
|
|
35
|
-
renderProjects(data.projects || []);
|
|
36
|
-
renderMetrics(data.metrics || {});
|
|
37
|
-
renderWorkItems(data.workItems || []);
|
|
38
|
-
renderSkills(data.skills || []);
|
|
39
|
-
renderMcpServers(data.mcpServers || []);
|
|
40
|
-
renderSchedules(data.schedules || []);
|
|
41
|
-
renderPinned(data.pinned || []);
|
|
42
|
-
// Update sidebar counts
|
|
43
|
-
const swi = document.getElementById('sidebar-wi');
|
|
44
|
-
if (swi) swi.textContent = (data.workItems || []).length || '';
|
|
45
|
-
const spr = document.getElementById('sidebar-pr');
|
|
46
|
-
if (spr) spr.textContent = (data.pullRequests || []).length || '';
|
|
47
|
-
// Refresh KB and plans less frequently (every 3rd cycle = ~12s)
|
|
48
|
-
if (!window._kbRefreshCount) window._kbRefreshCount = 0;
|
|
49
|
-
if (window._kbRefreshCount++ % 3 === 0) { refreshKnowledgeBase(); refreshPlans(); }
|
|
53
|
+
_processStatusUpdate(data);
|
|
50
54
|
} catch(e) { console.error('refresh error', e); }
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
refresh();
|
|
54
|
-
|
|
58
|
+
|
|
59
|
+
// SSE status stream — real-time push, falls back to 4s polling
|
|
60
|
+
let _statusStream = null;
|
|
61
|
+
try {
|
|
62
|
+
_statusStream = new EventSource('/api/status-stream');
|
|
63
|
+
_statusStream.onmessage = (e) => {
|
|
64
|
+
try { _processStatusUpdate(JSON.parse(e.data)); } catch {}
|
|
65
|
+
};
|
|
66
|
+
_statusStream.onerror = () => {
|
|
67
|
+
// Fall back to polling
|
|
68
|
+
if (_statusStream) { _statusStream.close(); _statusStream = null; }
|
|
69
|
+
setInterval(refresh, 4000);
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
setInterval(refresh, 4000);
|
|
73
|
+
}
|
|
55
74
|
|
|
56
75
|
// Wire sidebar navigation
|
|
57
76
|
document.querySelectorAll('.sidebar-link').forEach(link => {
|
package/dashboard.js
CHANGED
|
@@ -180,7 +180,23 @@ function parsePinnedEntries(content) {
|
|
|
180
180
|
let _statusCache = null;
|
|
181
181
|
let _statusCacheTs = 0;
|
|
182
182
|
const STATUS_CACHE_TTL = 10000; // 10s — reduces expensive aggregation frequency; mutations call invalidateStatusCache()
|
|
183
|
-
|
|
183
|
+
const _statusStreamClients = new Set();
|
|
184
|
+
let _statusPushTimer = null;
|
|
185
|
+
let _lastStatusHash = '';
|
|
186
|
+
|
|
187
|
+
function invalidateStatusCache() {
|
|
188
|
+
_statusCache = null;
|
|
189
|
+
// Push to SSE clients (debounced 500ms to avoid flooding during batch mutations)
|
|
190
|
+
if (_statusPushTimer) return;
|
|
191
|
+
_statusPushTimer = setTimeout(() => {
|
|
192
|
+
_statusPushTimer = null;
|
|
193
|
+
if (_statusStreamClients.size === 0) return;
|
|
194
|
+
const data = JSON.stringify(getStatus());
|
|
195
|
+
for (const res of _statusStreamClients) {
|
|
196
|
+
try { res.write('data: ' + data + '\n\n'); } catch { _statusStreamClients.delete(res); }
|
|
197
|
+
}
|
|
198
|
+
}, 500);
|
|
199
|
+
}
|
|
184
200
|
|
|
185
201
|
function getStatus() {
|
|
186
202
|
const now = Date.now();
|
|
@@ -221,6 +237,19 @@ function getStatus() {
|
|
|
221
237
|
return _statusCache;
|
|
222
238
|
}
|
|
223
239
|
|
|
240
|
+
// Periodic push for engine-driven changes (dispatch.json, control.json) that bypass invalidateStatusCache
|
|
241
|
+
setInterval(() => {
|
|
242
|
+
if (_statusStreamClients.size === 0) return;
|
|
243
|
+
const status = getStatus();
|
|
244
|
+
const hash = require('crypto').createHash('md5').update(JSON.stringify(status)).digest('hex');
|
|
245
|
+
if (hash === _lastStatusHash) return;
|
|
246
|
+
_lastStatusHash = hash;
|
|
247
|
+
const data = JSON.stringify(status);
|
|
248
|
+
for (const res of _statusStreamClients) {
|
|
249
|
+
try { res.write('data: ' + data + '\n\n'); } catch { _statusStreamClients.delete(res); }
|
|
250
|
+
}
|
|
251
|
+
}, 10000);
|
|
252
|
+
|
|
224
253
|
|
|
225
254
|
// ── Command Center: session state + helpers ─────────────────────────────────
|
|
226
255
|
|
|
@@ -2816,6 +2845,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2816
2845
|
|
|
2817
2846
|
// Status & health
|
|
2818
2847
|
{ method: 'GET', path: '/api/status', desc: 'Full dashboard status snapshot (agents, PRDs, work items, dispatch, etc.)', handler: handleStatus },
|
|
2848
|
+
{ method: 'GET', path: '/api/status-stream', desc: 'SSE stream of real-time status updates', handler: (req, res) => {
|
|
2849
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
2850
|
+
res.write('data: ' + JSON.stringify(getStatus()) + '\n\n');
|
|
2851
|
+
_statusStreamClients.add(res);
|
|
2852
|
+
req.on('close', () => _statusStreamClients.delete(res));
|
|
2853
|
+
}},
|
|
2819
2854
|
{ method: 'GET', path: '/api/health', desc: 'Lightweight health check for monitoring', handler: handleHealth },
|
|
2820
2855
|
{ method: 'GET', path: '/api/hot-reload', desc: 'SSE stream for dashboard hot-reload notifications', handler: (req, res) => {
|
|
2821
2856
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
package/engine/cli.js
CHANGED
|
@@ -320,6 +320,45 @@ const commands = {
|
|
|
320
320
|
console.log(`Tick interval: ${interval / 1000}s | Max concurrent: ${config.engine?.maxConcurrent || 5}`);
|
|
321
321
|
console.log('Press Ctrl+C to stop');
|
|
322
322
|
|
|
323
|
+
// File-change-driven work discovery — trigger tick when work-items or PRDs change
|
|
324
|
+
const _watchedFiles = new Set();
|
|
325
|
+
function watchForWorkChanges() {
|
|
326
|
+
const filesToWatch = [
|
|
327
|
+
path.join(MINIONS_DIR, 'work-items.json'),
|
|
328
|
+
path.join(ENGINE_DIR, 'dispatch.json'),
|
|
329
|
+
];
|
|
330
|
+
// Watch project-specific work-items.json
|
|
331
|
+
const { getProjects } = require('./shared');
|
|
332
|
+
for (const p of getProjects(config)) {
|
|
333
|
+
filesToWatch.push(shared.projectWorkItemsPath(p));
|
|
334
|
+
}
|
|
335
|
+
// Watch PRD files
|
|
336
|
+
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
337
|
+
try {
|
|
338
|
+
for (const f of fs.readdirSync(prdDir).filter(f => f.endsWith('.json'))) {
|
|
339
|
+
filesToWatch.push(path.join(prdDir, f));
|
|
340
|
+
}
|
|
341
|
+
} catch {}
|
|
342
|
+
|
|
343
|
+
for (const filePath of filesToWatch) {
|
|
344
|
+
if (_watchedFiles.has(filePath)) continue;
|
|
345
|
+
_watchedFiles.add(filePath);
|
|
346
|
+
try {
|
|
347
|
+
let _debounce = null;
|
|
348
|
+
fs.watchFile(filePath, { interval: 2000 }, () => {
|
|
349
|
+
// Debounce — multiple rapid writes should only trigger one tick
|
|
350
|
+
if (_debounce) return;
|
|
351
|
+
_debounce = setTimeout(() => {
|
|
352
|
+
_debounce = null;
|
|
353
|
+
e.log('info', `File change detected: ${path.basename(filePath)} — triggering tick`);
|
|
354
|
+
e.tick();
|
|
355
|
+
}, 1000);
|
|
356
|
+
});
|
|
357
|
+
} catch {}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
watchForWorkChanges();
|
|
361
|
+
|
|
323
362
|
// Graceful shutdown — wait for active agents before exiting
|
|
324
363
|
let shuttingDown = false;
|
|
325
364
|
function gracefulShutdown(signal) {
|
|
@@ -327,6 +366,7 @@ const commands = {
|
|
|
327
366
|
shuttingDown = true;
|
|
328
367
|
console.log(`\n${signal} received — initiating graceful shutdown...`);
|
|
329
368
|
clearInterval(tickTimer);
|
|
369
|
+
for (const f of _watchedFiles) { try { fs.unwatchFile(f); } catch {} }
|
|
330
370
|
safeWrite(CONTROL_PATH, { state: 'stopping', pid: process.pid, stopping_at: e.ts() });
|
|
331
371
|
e.log('info', `Graceful shutdown initiated (${signal})`);
|
|
332
372
|
|
package/package.json
CHANGED