@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 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
@@ -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/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
- if (!defaultProject) continue;
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
- const itemProjectName = item.project || defaultProjectName;
2383
- const itemProject = allProjects.find(p => p.name?.toLowerCase() === itemProjectName.toLowerCase()) || defaultProject;
2384
- if (!itemsByProject.has(itemProject.name)) {
2385
- itemsByProject.set(itemProject.name, { project: itemProject, items: [] });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.53",
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"