@yemi33/minions 0.1.406 → 0.1.407

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,8 +1,14 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.406 (2026-04-06)
3
+ ## 0.1.407 (2026-04-06)
4
+
5
+ ### Features
6
+ - Add missing return in mutateJsonFileLocked metrics callbacks
7
+ - Fix SSE resource leak in handleAgentLiveStream
8
+ - Add projects[0] length guards in engine.js
4
9
 
5
10
  ### Fixes
11
+ - npm version check uses npm view instead of raw https.get
6
12
  - re-check npm registry every 4 hours via setInterval
7
13
  - PR delete is optimistic — row removed immediately, reverted on failure
8
14
  - PR delete searches all project files, not just the first project
package/dashboard.js CHANGED
@@ -179,18 +179,14 @@ async function checkNpmVersion() {
179
179
  const now = Date.now();
180
180
  if (_npmVersionCache && (now - _npmVersionCacheTs) < NPM_CHECK_INTERVAL) return _npmVersionCache;
181
181
  try {
182
- const https = require('https');
183
- const data = await new Promise((resolve, reject) => {
184
- const req = https.get(`https://registry.npmjs.org/${PKG_NAME}/latest`, { timeout: 5000 }, (resp) => {
185
- if (resp.statusCode !== 200) { reject(new Error('npm registry ' + resp.statusCode)); resp.resume(); return; }
186
- let body = '';
187
- resp.on('data', c => body += c);
188
- resp.on('end', () => { try { resolve(JSON.parse(body)); } catch { reject(new Error('bad json')); } });
182
+ // Use npm view — respects user's .npmrc proxy/registry config (unlike raw https.get)
183
+ const { execFile } = require('child_process');
184
+ const version = await new Promise((resolve, reject) => {
185
+ execFile('npm', ['view', PKG_NAME, 'version'], { timeout: 15000, windowsHide: true }, (err, stdout) => {
186
+ if (err) reject(err); else resolve((stdout || '').trim());
189
187
  });
190
- req.on('error', reject);
191
- req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
192
188
  });
193
- _npmVersionCache = { latest: data.version || null, checkedAt: new Date().toISOString() };
189
+ _npmVersionCache = { latest: version || null, checkedAt: new Date().toISOString() };
194
190
  _npmVersionCacheTs = now;
195
191
  } catch {
196
192
  _npmVersionCache = _npmVersionCache || { latest: null, checkedAt: null, error: 'check failed' };
@@ -1458,6 +1454,13 @@ const server = http.createServer(async (req, res) => {
1458
1454
  const agentId = match[1];
1459
1455
  const agentDir = path.join(MINIONS_DIR, 'agents', agentId);
1460
1456
  const liveLogPath = path.join(agentDir, 'live-output.log');
1457
+ let _cleanedUp = false;
1458
+
1459
+ // Safe res.write wrapper — guards against writes after cleanup and EPIPE/ERR_STREAM_DESTROYED
1460
+ const safeWrite = (data) => {
1461
+ if (_cleanedUp) return;
1462
+ try { res.write(data); } catch { /* EPIPE or ERR_STREAM_DESTROYED — client gone */ }
1463
+ };
1461
1464
 
1462
1465
  // Check if agent directory exists — avoid dangling watchers on nonexistent paths
1463
1466
  if (!fs.existsSync(agentDir)) {
@@ -1479,13 +1482,14 @@ const server = http.createServer(async (req, res) => {
1479
1482
  try {
1480
1483
  const content = fs.readFileSync(liveLogPath, 'utf8');
1481
1484
  if (content.length > 0) {
1482
- res.write(`data: ${JSON.stringify(content)}\n\n`);
1485
+ safeWrite(`data: ${JSON.stringify(content)}\n\n`);
1483
1486
  offset = Buffer.byteLength(content, 'utf8');
1484
1487
  }
1485
1488
  } catch { /* optional */ }
1486
1489
 
1487
1490
  // Watch for changes using fs.watchFile (cross-platform, works on Windows)
1488
1491
  const watcher = () => {
1492
+ if (_cleanedUp) return;
1489
1493
  try {
1490
1494
  const stat = fs.statSync(liveLogPath);
1491
1495
  if (stat.size > offset) {
@@ -1495,29 +1499,32 @@ const server = http.createServer(async (req, res) => {
1495
1499
  fs.closeSync(fd);
1496
1500
  offset = stat.size;
1497
1501
  const chunk = buf.toString('utf8');
1498
- if (chunk) res.write(`data: ${JSON.stringify(chunk)}\n\n`);
1502
+ if (chunk) safeWrite(`data: ${JSON.stringify(chunk)}\n\n`);
1499
1503
  }
1500
1504
  } catch { /* optional */ }
1501
1505
  };
1502
1506
 
1503
1507
  fs.watchFile(liveLogPath, { interval: 500 }, watcher);
1504
1508
 
1505
- // Cleanup helper to prevent handle leaks
1509
+ // Idempotent cleanup helper to prevent handle leaks
1506
1510
  const cleanup = () => {
1511
+ if (_cleanedUp) return;
1512
+ _cleanedUp = true;
1507
1513
  try { clearInterval(doneCheck); } catch { /* optional */ }
1508
1514
  try { fs.unwatchFile(liveLogPath, watcher); } catch { /* optional */ }
1509
1515
  };
1510
1516
 
1511
1517
  // Check if agent is still active (poll every 5s)
1512
1518
  const doneCheck = setInterval(() => {
1519
+ if (_cleanedUp) return;
1513
1520
  try {
1514
1521
  const dispatch = getDispatchQueue();
1515
1522
  const isActive = (dispatch.active || []).some(d => d.agent === agentId);
1516
1523
  if (!isActive) {
1517
1524
  watcher(); // flush final content
1518
- res.write(`event: done\ndata: complete\n\n`);
1525
+ safeWrite(`event: done\ndata: complete\n\n`);
1519
1526
  cleanup();
1520
- res.end();
1527
+ try { res.end(); } catch { /* optional */ }
1521
1528
  }
1522
1529
  } catch (e) {
1523
1530
  cleanup();
package/engine/ado.js CHANGED
@@ -194,6 +194,7 @@ async function pollPrStatus(config) {
194
194
  if (!metrics[authorId]) metrics[authorId] = {};
195
195
  if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
196
196
  else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
197
+ return metrics;
197
198
  });
198
199
  } catch (err) { log('warn', `Metrics update: ${err.message}`); }
199
200
  }
package/engine/github.js CHANGED
@@ -216,6 +216,7 @@ async function pollPrStatus(config) {
216
216
  if (!metrics[authorId]) metrics[authorId] = {};
217
217
  if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
218
218
  else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
219
+ return metrics;
219
220
  });
220
221
  } catch (err) { log('warn', `Metrics update: ${err.message}`); }
221
222
  }
package/engine.js CHANGED
@@ -1827,7 +1827,8 @@ function discoverCentralWorkItems(config) {
1827
1827
  const fanKey = `${key}-${agent.id}`;
1828
1828
  if (isAlreadyDispatched(fanKey)) continue;
1829
1829
 
1830
- const ap = assignedProject || projects[0];
1830
+ const ap = assignedProject || (projects.length > 0 ? projects[0] : null);
1831
+ if (!ap) { log('warn', `Fan-out: skipping ${fanKey} — no projects configured`); continue; }
1831
1832
  const vars = {
1832
1833
  ...buildBaseVars(agent.id, config, ap),
1833
1834
  item_id: item.id,
@@ -1897,7 +1898,8 @@ function discoverCentralWorkItems(config) {
1897
1898
 
1898
1899
  const agentName = config.agents[agentId]?.name || agentId;
1899
1900
  const agentRole = config.agents[agentId]?.role || 'Agent';
1900
- const firstProject = projects[0];
1901
+ const firstProject = projects.length > 0 ? projects[0] : null;
1902
+ if (!firstProject) { log('warn', `Dispatch: skipping ${item.id} — no projects configured`); continue; }
1901
1903
 
1902
1904
  const vars = {
1903
1905
  ...buildBaseVars(agentId, config, firstProject),
@@ -2291,7 +2293,7 @@ async function tickInner() {
2291
2293
  if (pendingWithBlockedDeps.length > 0) {
2292
2294
  // Auto-retry failed items that are blocking others (transient errors)
2293
2295
  for (const item of items) {
2294
- if (item.status !== 'failed' || isItemCompleted(item)) continue;
2296
+ if (item.status !== WI_STATUS.FAILED || isItemCompleted(item)) continue;
2295
2297
  // Only retry if something depends on this item
2296
2298
  const isBlocking = items.some(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(item.id));
2297
2299
  if (!isBlocking) continue;
@@ -2333,7 +2335,7 @@ async function tickInner() {
2333
2335
  const blockers = (dep.depends_on || []).filter(d => retriedIds.has(d));
2334
2336
  if (blockers.length > 0) {
2335
2337
  log('info', `Stall recovery: un-failing ${dep.id} (blocker ${blockers.join(',')} retried)`);
2336
- dep.status = 'pending';
2338
+ dep.status = WI_STATUS.PENDING;
2337
2339
  dep._retryCount = 0;
2338
2340
  delete dep.failReason;
2339
2341
  delete dep.failedAt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.406",
3
+ "version": "0.1.407",
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"