@yemi33/minions 0.1.2278 → 0.1.2279

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/dashboard.js CHANGED
@@ -588,10 +588,18 @@ let _prRefVerifierOverride = null; // test seam
588
588
 
589
589
  // Evict stale entries when the cache exceeds _PR_REF_VERIFY_CACHE_MAX to prevent
590
590
  // unbounded growth. Called after every Map.set() in verifyLoosePrRefIsPr.
591
+ // Two-pass strategy: (1) evict TTL-expired entries; (2) if still over cap,
592
+ // evict the oldest entries (LRU) until the cache is back within bounds.
591
593
  function _evictPrRefVerifyCacheIfNeeded() {
592
594
  if (_prRefVerifyCache.size <= _PR_REF_VERIFY_CACHE_MAX) return;
595
+ // Pass 1: evict TTL-expired entries.
593
596
  const cutoff = Date.now() - _PR_REF_VERIFY_TTL_MS;
594
597
  for (const [k, v] of _prRefVerifyCache) if (v.at < cutoff) _prRefVerifyCache.delete(k);
598
+ // Pass 2: LRU fallback — evict oldest entries if still over cap (e.g. burst of fresh entries).
599
+ if (_prRefVerifyCache.size <= _PR_REF_VERIFY_CACHE_MAX) return;
600
+ const toEvict = _prRefVerifyCache.size - _PR_REF_VERIFY_CACHE_MAX;
601
+ const sorted = [..._prRefVerifyCache.entries()].sort((a, b) => a[1].at - b[1].at);
602
+ for (let i = 0; i < toEvict; i++) _prRefVerifyCache.delete(sorted[i][0]);
595
603
  }
596
604
 
597
605
  // Test seam (issue #246) — inject a fake verifier so handler tests can assert
@@ -626,14 +626,18 @@ function doctor(minionsHome) {
626
626
  runtimeResults.push({ name: 'Engine', ok: 'warn', message: 'not started — run: minions start (see docs/engine-restart.md)' });
627
627
  }
628
628
 
629
- // Check dashboard (try HTTP)
629
+ // Check dashboard (try HTTP). Read the actual bound port from the
630
+ // dashboard-port.json beacon so the probe works when the dashboard falls
631
+ // back to a non-default port after EADDRINUSE scanning.
630
632
  const http = require('http');
633
+ const beaconData = shared.readDashboardPortFile(minionsHome);
634
+ const dashPort = (beaconData && beaconData.port) || shared.DEFAULT_DASHBOARD_PORT;
631
635
  const dashCheck = new Promise(resolve => {
632
- const req = http.get('http://localhost:7331/api/health', { timeout: 2000 }, res => {
633
- resolve({ name: 'Dashboard', ok: true, message: 'running on http://localhost:7331' });
636
+ const req = http.get(`http://localhost:${dashPort}/api/health`, { timeout: 2000 }, res => {
637
+ resolve({ name: 'Dashboard', ok: true, message: `running on http://localhost:${dashPort}` });
634
638
  });
635
- req.on('error', () => resolve({ name: 'Dashboard', ok: 'warn', message: 'not reachable on :7331 — run: minions dash (see docs/engine-restart.md)' }));
636
- req.on('timeout', () => { req.destroy(); resolve({ name: 'Dashboard', ok: 'warn', message: 'not reachable on :7331 — run: minions dash (see docs/engine-restart.md)' }); });
639
+ req.on('error', () => resolve({ name: 'Dashboard', ok: 'warn', message: `not reachable on :${dashPort} — run: minions dash (see docs/engine-restart.md)` }));
640
+ req.on('timeout', () => { req.destroy(); resolve({ name: 'Dashboard', ok: 'warn', message: `not reachable on :${dashPort} — run: minions dash (see docs/engine-restart.md)` }); });
637
641
  });
638
642
 
639
643
  return dashCheck.then(async dashResult => {
@@ -649,9 +653,9 @@ function doctor(minionsHome) {
649
653
  runtimeResults.push({ name: 'Playbooks', ok: false, message: 'no playbooks found in playbooks/ — run: minions init --force (see docs/distribution.md)' });
650
654
  }
651
655
 
652
- // Check port 7331 availability (only if dashboard isn't running)
656
+ // Check port availability (only if dashboard isn't running)
653
657
  if (dashResult.ok !== true) {
654
- runtimeResults.push({ name: 'Port 7331', ok: 'warn', message: 'dashboard not running — port status unknown (see docs/engine-restart.md)' });
658
+ runtimeResults.push({ name: `Port ${dashPort}`, ok: 'warn', message: `dashboard not running — port status unknown (see docs/engine-restart.md)` });
655
659
  }
656
660
 
657
661
  // Self-check the out-of-process recovery net. A registered-but-broken
@@ -15,7 +15,7 @@ const shared = require('./shared');
15
15
  // the literal is only a fallback if shared somehow failed to load.
16
16
  const DEFAULT_RESTART_HEALTH_TIMEOUT_MS =
17
17
  (shared.ENGINE_DEFAULTS && shared.ENGINE_DEFAULTS.restartHealthTimeoutMs) || 60000;
18
- const DEFAULT_RESTART_HEALTH_INTERVAL_MS = 250;
18
+ const DEFAULT_RESTART_HEALTH_INTERVAL_MS = 100;
19
19
  // Consecutive dead-PID reads tolerated before fail-fast. A small guard avoids
20
20
  // a single flaky tasklist / kill(0) false-negative aborting a healthy boot.
21
21
  const DEFAULT_DEAD_POLLS_BEFORE_FAST_FAIL = 2;
@@ -45,7 +45,7 @@ function isProcessAlive(pid) {
45
45
  const out = execSync(`tasklist /FI "PID eq ${n}" /NH`, {
46
46
  encoding: 'utf8',
47
47
  windowsHide: true,
48
- timeout: 3000,
48
+ timeout: 1000,
49
49
  });
50
50
  return new RegExp(`\\b${n}\\b`).test(out) && out.toLowerCase().includes('node');
51
51
  }
@@ -61,8 +61,8 @@ function isPortListening(port) {
61
61
  if (!Number.isInteger(n) || n <= 0) return false;
62
62
  try {
63
63
  if (process.platform === 'win32') {
64
- const out = execSync(`netstat -ano -p TCP`, {
65
- encoding: 'utf8', windowsHide: true, timeout: 3000, maxBuffer: 4 * 1024 * 1024,
64
+ const out = execSync(`netstat -ano | findstr ":${n} "`, {
65
+ encoding: 'utf8', windowsHide: true, timeout: 1000, maxBuffer: 64 * 1024, shell: true,
66
66
  });
67
67
  const re = new RegExp(`\\s127\\.0\\.0\\.1:${n}\\s+\\S+\\s+LISTENING`, 'i');
68
68
  const re6 = new RegExp(`\\s\\[::1?\\]:${n}\\s+\\S+\\s+LISTENING`, 'i');
@@ -70,12 +70,12 @@ function isPortListening(port) {
70
70
  }
71
71
  try {
72
72
  const out = execSync(`lsof -nP -iTCP:${n} -sTCP:LISTEN`, {
73
- encoding: 'utf8', timeout: 3000,
73
+ encoding: 'utf8', timeout: 1000,
74
74
  });
75
75
  if (/\bLISTEN\b/i.test(out)) return true;
76
76
  } catch {}
77
77
  const out = execSync(`ss -ltn 'sport = :${n}' 2>/dev/null || netstat -ltn 2>/dev/null || netstat -an -p tcp 2>/dev/null`, {
78
- encoding: 'utf8', timeout: 3000, shell: true,
78
+ encoding: 'utf8', timeout: 1000, shell: true,
79
79
  });
80
80
  return new RegExp(`(?:[:.])${n}\\b[^\\n]*\\b(?:LISTEN|LISTENING)\\b`, 'i').test(out);
81
81
  } catch { return false; }
@@ -152,7 +152,6 @@ async function checkRestartHealth(options = {}) {
152
152
  dashboardKind = 'process';
153
153
  const dpid = normalizePid(dashboardPid);
154
154
  const dashAlive = dpid ? isAlive(dpid) : false;
155
- const portOpen = portCheck(dashboardPort);
156
155
  // Ownership gate (opt-in via requireBeaconOwner). "PID alive + port
157
156
  // listening" is NOT sufficient: a stale pre-restart dashboard still holding
158
157
  // the port satisfies the listening probe, so the verifier used to report
@@ -174,6 +173,10 @@ async function checkRestartHealth(options = {}) {
174
173
  beaconPid = beacon && normalizePid(beacon.pid);
175
174
  beaconOwned = !!(beacon && beaconPid === dpid && Number(beacon.port) === Number(dashboardPort));
176
175
  }
176
+ // If the beacon already confirms this PID owns the port, skip the expensive
177
+ // netstat — the dashboard writes dashboard-port.json in its listen() callback,
178
+ // so beacon.pid === dpid IS the port-bind signal.
179
+ const portOpen = (options.requireBeaconOwner && beaconOwned) ? true : portCheck(dashboardPort);
177
180
  dashboardOk = !!(dashAlive && portOpen && beaconOwned);
178
181
  dashboardDetail = `pid=${dpid || 'none'} alive=${dashAlive ? 'yes' : 'no'} port=${dashboardPort} listening=${portOpen ? 'yes' : 'no'}`
179
182
  + (options.requireBeaconOwner ? ` beaconPid=${beaconPid || 'none'} owned=${beaconOwned ? 'yes' : 'no'}` : '');
package/engine/shared.js CHANGED
@@ -975,6 +975,19 @@ function _routeJsonReadToSql(p) {
975
975
  * reads (cooldowns, archived PRDs, ephemeral session state) where reviving a
976
976
  * stale `.backup` is actively harmful. See its JSDoc for selection guidance.
977
977
  */
978
+
979
+ // PL-prd-no-backup (W-mqub65ez0004b3bd) — archived PRDs under prd/archive/ are
980
+ // permanently removed terminal artifacts. They must NOT auto-restore from a
981
+ // stale `.backup` sidecar (W-mouptdh1000h9f39: archived PRD came back and
982
+ // re-dispatched work). Intentionally NARROW to prd/archive/ only:
983
+ // - prd/archive/*.json → no backup write + no safeJson restore
984
+ // - prd/*.json (root-level canonical) → normal backup/restore lifecycle
985
+ // preserved so concurrent-sweep loss is recoverable (prd-rename-race.test.js).
986
+ const _NO_BACKUP_JSON_RE = /(?:^|[\\/])prd[\\/]archive[\\/][^\\/]+\.json$/i;
987
+ function _isNoBackupJsonPath(p) {
988
+ return typeof p === 'string' && _NO_BACKUP_JSON_RE.test(p);
989
+ }
990
+
978
991
  function safeJson(p) {
979
992
  // Internal opt-out (positional second arg from mutateJsonFileLocked):
980
993
  // when truthy, skip the SQL-routing shim and do a raw disk read. Used
@@ -1008,6 +1021,8 @@ function safeJson(p) {
1008
1021
  console.error(`[safeJson] parse failure for ${path.basename(p)}: ${parseErr.message}`);
1009
1022
  }
1010
1023
  }
1024
+ // Archived PRDs (prd/archive/) are permanently gone — skip .backup restore.
1025
+ if (_isNoBackupJsonPath(p)) return null;
1011
1026
  // Primary missing or corrupted — try restoring from .backup sidecar.
1012
1027
  const backupPath = p + '.backup';
1013
1028
  try {
@@ -1837,9 +1852,14 @@ function mutateJsonFileLocked(filePath, mutateFn, {
1837
1852
  const finalData = next === undefined ? data : next;
1838
1853
  const shouldWrite = !skipWriteIfUnchanged || parsedInvalid || JSON.stringify(finalData) !== beforeSerialized;
1839
1854
  if (shouldWrite) {
1840
- // Back up last-known-good state before mutation (best-effort)
1841
- const backupPath = filePath + '.backup';
1842
- try { if (fileExists) fs.copyFileSync(filePath, backupPath); } catch { /* backup is best-effort */ }
1855
+ // Back up last-known-good state before mutation (best-effort). SKIP for
1856
+ // archived PRDs under prd/archive/: a .backup there is resurrection fuel
1857
+ // (PL-prd-no-backup, W-mqub65ez0004b3bd). Root-level prd/*.json retains
1858
+ // the backup lifecycle — concurrent-sweep loss must be recoverable.
1859
+ if (!_isNoBackupJsonPath(filePath)) {
1860
+ const backupPath = filePath + '.backup';
1861
+ try { if (fileExists) fs.copyFileSync(filePath, backupPath); } catch { /* backup is best-effort */ }
1862
+ }
1843
1863
  safeWrite(filePath, finalData);
1844
1864
  // Side-effect hook fired only when an actual write happened. Callers
1845
1865
  // use this to emit cache-invalidation signals (events table row) so
package/engine.js CHANGED
@@ -6121,8 +6121,9 @@ function materializePlansAsWorkItems(config) {
6121
6121
  continue; // Skip — waiting for human approval
6122
6122
  }
6123
6123
  }
6124
- if (planStatus === PLAN_STATUS.PAUSED || planStatus === PLAN_STATUS.REJECTED || planStatus === PLAN_STATUS.REVISION_REQUESTED) {
6125
- continue; // Skip paused or revision requested
6124
+ if (planStatus === PLAN_STATUS.PAUSED || planStatus === PLAN_STATUS.REJECTED ||
6125
+ planStatus === PLAN_STATUS.REVISION_REQUESTED || planStatus === PLAN_STATUS.COMPLETED) {
6126
+ continue; // Skip — paused, rejected, revision-requested, or completed
6126
6127
  }
6127
6128
  // Stale PRDs: source plan was revised — don't materialize NEW items until user regenerates
6128
6129
  if (plan.planStale) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2278",
3
+ "version": "0.1.2279",
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"