@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 CHANGED
@@ -1,5 +1,14 @@
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
+
3
12
  ## 0.1.54 (2026-03-30)
4
13
 
5
14
  ### Engine
@@ -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
- // Detect fresh install — clear stale browser state if install ID changed
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
- setInterval(refresh, 4000);
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
- function invalidateStatusCache() { _statusCache = null; }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.54",
3
+ "version": "0.1.55",
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"