@yemi33/minions 0.1.53 → 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 +14 -0
- package/dashboard/js/refresh.js +64 -45
- package/dashboard.js +36 -1
- package/engine/cli.js +40 -0
- package/engine.js +19 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.55 (2026-03-30)
|
|
4
|
+
|
|
5
|
+
### Engine
|
|
6
|
+
- engine/cli.js
|
|
7
|
+
|
|
8
|
+
### Dashboard
|
|
9
|
+
- dashboard.js
|
|
10
|
+
- dashboard/js/refresh.js
|
|
11
|
+
|
|
12
|
+
## 0.1.54 (2026-03-30)
|
|
13
|
+
|
|
14
|
+
### Engine
|
|
15
|
+
- engine.js
|
|
16
|
+
|
|
3
17
|
## 0.1.53 (2026-03-30)
|
|
4
18
|
|
|
5
19
|
### Dashboard
|
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/engine.js
CHANGED
|
@@ -2361,7 +2361,8 @@ function materializePlansAsWorkItems(config) {
|
|
|
2361
2361
|
const defaultProjectName = plan.project || file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
2362
2362
|
const allProjects = getProjects(config);
|
|
2363
2363
|
const defaultProject = allProjects.find(p => p.name?.toLowerCase() === defaultProjectName.toLowerCase());
|
|
2364
|
-
|
|
2364
|
+
// No project found — use central work-items.json (engine works without projects)
|
|
2365
|
+
const useCentral = !defaultProject;
|
|
2365
2366
|
|
|
2366
2367
|
const statusFilter = ['missing', 'planned'];
|
|
2367
2368
|
// Also materialize in-pr/done items that never got a work item (race with PR status sync)
|
|
@@ -2371,20 +2372,31 @@ function materializePlansAsWorkItems(config) {
|
|
|
2371
2372
|
if (w.id) allExistingWiIds.add(w.id);
|
|
2372
2373
|
}
|
|
2373
2374
|
}
|
|
2375
|
+
// Also check central work-items.json
|
|
2376
|
+
for (const w of (safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [])) {
|
|
2377
|
+
if (w.id) allExistingWiIds.add(w.id);
|
|
2378
|
+
}
|
|
2374
2379
|
const items = plan.missing_features.filter(f =>
|
|
2375
2380
|
statusFilter.includes(f.status) ||
|
|
2376
2381
|
((f.status === 'in-pr' || f.status === 'done') && f.id && !allExistingWiIds.has(f.id))
|
|
2377
2382
|
);
|
|
2378
2383
|
|
|
2379
2384
|
// Group items by target project (per-item project field overrides plan-level project)
|
|
2385
|
+
// When no projects are configured, all items go to central work-items.json
|
|
2380
2386
|
const itemsByProject = new Map(); // projectName -> { project, items: [] }
|
|
2381
2387
|
for (const item of items) {
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2388
|
+
if (useCentral) {
|
|
2389
|
+
if (!itemsByProject.has('_central')) itemsByProject.set('_central', { project: null, items: [] });
|
|
2390
|
+
itemsByProject.get('_central').items.push(item);
|
|
2391
|
+
} else {
|
|
2392
|
+
const itemProjectName = item.project || defaultProjectName;
|
|
2393
|
+
const itemProject = allProjects.find(p => p.name?.toLowerCase() === itemProjectName.toLowerCase()) || defaultProject;
|
|
2394
|
+
if (!itemProject) continue;
|
|
2395
|
+
if (!itemsByProject.has(itemProject.name)) {
|
|
2396
|
+
itemsByProject.set(itemProject.name, { project: itemProject, items: [] });
|
|
2397
|
+
}
|
|
2398
|
+
itemsByProject.get(itemProject.name).items.push(item);
|
|
2386
2399
|
}
|
|
2387
|
-
itemsByProject.get(itemProject.name).items.push(item);
|
|
2388
2400
|
}
|
|
2389
2401
|
|
|
2390
2402
|
// Cycle detection BEFORE materialization — skip cyclic items
|
|
@@ -2406,7 +2418,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
2406
2418
|
|
|
2407
2419
|
let totalCreated = 0;
|
|
2408
2420
|
for (const [projName, { project, items: projItems }] of itemsByProject) {
|
|
2409
|
-
const wiPath = projectWorkItemsPath(project);
|
|
2421
|
+
const wiPath = project ? projectWorkItemsPath(project) : path.join(MINIONS_DIR, 'work-items.json');
|
|
2410
2422
|
const existingItems = safeJson(wiPath) || [];
|
|
2411
2423
|
let created = 0;
|
|
2412
2424
|
const newlyCreatedIds = new Set(); // tracks IDs created in this pass for reconciliation scoping
|
package/package.json
CHANGED