@yemi33/minions 0.1.54 → 0.1.56

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.56 (2026-03-30)
4
+
5
+ ### Other
6
+ - bin/minions.js
7
+
8
+ ## 0.1.55 (2026-03-30)
9
+
10
+ ### Engine
11
+ - engine/cli.js
12
+
13
+ ### Dashboard
14
+ - dashboard.js
15
+ - dashboard/js/refresh.js
16
+
3
17
  ## 0.1.54 (2026-03-30)
4
18
 
5
19
  ### Engine
package/bin/minions.js CHANGED
@@ -472,6 +472,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
472
472
  minions spawn <agent> <prompt> Manually spawn an agent
473
473
  minions plan <file|text> [proj] Run a plan
474
474
  minions cleanup Clean temp files, worktrees, zombies
475
+ minions reset --confirm Factory reset (delete all state, keep config)
475
476
 
476
477
  Dashboard:
477
478
  minions dash Start web dashboard (default :7331)
@@ -500,6 +501,68 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
500
501
  dashProc.unref();
501
502
  console.log(` Dashboard started (PID: ${dashProc.pid})`);
502
503
  console.log(' Dashboard: http://localhost:7331\n');
504
+ } else if (cmd === 'reset') {
505
+ ensureInstalled();
506
+ if (!rest.includes('--confirm')) {
507
+ console.log(`
508
+ This will DELETE all runtime state:
509
+ - Work items, dispatch queue, PRDs, plans
510
+ - Agent history, sessions, output logs
511
+ - Notes, knowledge base, pinned notes
512
+ - Metrics, cooldowns, schedules
513
+
514
+ Config.json and agent charters are PRESERVED.
515
+
516
+ Run: minions reset --confirm
517
+ `);
518
+ process.exit(0);
519
+ }
520
+ // Stop engine + dashboard
521
+ try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME }); } catch {}
522
+ const glob = (dir, pattern) => { try { return fs.readdirSync(dir).filter(f => pattern.test(f)).map(f => path.join(dir, f)); } catch { return []; } };
523
+ const rm = (f) => { try { fs.unlinkSync(f); } catch {} };
524
+ const rmDir = (d) => { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} };
525
+ const engineDir = path.join(MINIONS_HOME, 'engine');
526
+ // Engine state
527
+ for (const f of ['dispatch.json', 'control.json', 'log.json', 'metrics.json', 'cooldowns.json', 'schedule-runs.json', 'kb-checkpoint.json', 'cc-session.json', 'doc-sessions.json']) rm(path.join(engineDir, f));
528
+ glob(engineDir, /^pid-.*\.pid$/).forEach(rm);
529
+ rmDir(path.join(engineDir, 'tmp'));
530
+ // Work items + PRs
531
+ rm(path.join(MINIONS_HOME, 'work-items.json'));
532
+ rm(path.join(MINIONS_HOME, 'work-items-archive.json'));
533
+ rm(path.join(MINIONS_HOME, 'pull-requests.json'));
534
+ // Plans + PRDs
535
+ glob(path.join(MINIONS_HOME, 'plans'), /\.md$/).forEach(rm);
536
+ glob(path.join(MINIONS_HOME, 'prd'), /\.json$/).forEach(rm);
537
+ rmDir(path.join(MINIONS_HOME, 'prd', 'archive'));
538
+ rmDir(path.join(MINIONS_HOME, 'prd', 'guides'));
539
+ // Notes + KB
540
+ rm(path.join(MINIONS_HOME, 'notes.md'));
541
+ rm(path.join(MINIONS_HOME, 'pinned.md'));
542
+ rmDir(path.join(MINIONS_HOME, 'notes', 'inbox'));
543
+ rmDir(path.join(MINIONS_HOME, 'notes', 'archive'));
544
+ fs.mkdirSync(path.join(MINIONS_HOME, 'notes', 'inbox'), { recursive: true });
545
+ fs.mkdirSync(path.join(MINIONS_HOME, 'notes', 'archive'), { recursive: true });
546
+ for (const cat of ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews']) {
547
+ const catDir = path.join(MINIONS_HOME, 'knowledge', cat);
548
+ glob(catDir, /\.md$/).forEach(rm);
549
+ }
550
+ // Agent state (preserve charters)
551
+ const agentsDir = path.join(MINIONS_HOME, 'agents');
552
+ try {
553
+ for (const agent of fs.readdirSync(agentsDir)) {
554
+ const agentDir = path.join(agentsDir, agent);
555
+ if (!fs.statSync(agentDir).isDirectory()) continue;
556
+ for (const f of ['live-output.log', 'session.json', 'history.md', 'steer.md', 'status.json']) rm(path.join(agentDir, f));
557
+ glob(agentDir, /^output.*\.log$/).forEach(rm);
558
+ }
559
+ } catch {}
560
+ // Projects state
561
+ rmDir(path.join(MINIONS_HOME, 'projects'));
562
+ fs.mkdirSync(path.join(MINIONS_HOME, 'projects'), { recursive: true });
563
+
564
+ console.log('\n Reset complete. Config and charters preserved.');
565
+ console.log(' Run: minions up\n');
503
566
  } else if (cmd === 'doctor') {
504
567
  ensureInstalled();
505
568
  const { doctor } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
@@ -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.56",
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"