@yemi33/minions 0.1.1824 → 0.1.1826

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1826 (2026-05-09)
4
+
5
+ ### Fixes
6
+ - always open dashboard if no live tab reconnects after restart
7
+
8
+ ## 0.1.1825 (2026-05-09)
9
+
10
+ ### Fixes
11
+ - isolate test artifacts under MINIONS_TEST_DIR + dashboard TEST badge
12
+
3
13
  ## 0.1.1824 (2026-05-09)
4
14
 
5
15
  ### Other
package/bin/minions.js CHANGED
@@ -86,6 +86,43 @@ function hasRecentDashboardBrowserTab(minionsHome, now = Date.now()) {
86
86
  } catch { return false; }
87
87
  }
88
88
 
89
+ // Clear the browser-presence cache. Pre-restart beacons can outlive the
90
+ // browser window that produced them (closed Edge, locked-screen RDP session,
91
+ // stale heuristic timer) and falsely tell `restart` to skip the auto-open. We
92
+ // wipe it during restart so the post-restart probe only counts beacons that
93
+ // arrive AFTER the new dashboard is up — i.e. from a still-living tab.
94
+ function _clearDashboardBrowserState(minionsHome) {
95
+ try {
96
+ const fp = path.join(minionsHome, 'engine', 'dashboard-browser.json');
97
+ fs.writeFileSync(fp, JSON.stringify({ tabs: {}, updatedAt: new Date().toISOString() }));
98
+ } catch {}
99
+ }
100
+
101
+ async function _waitForBrowserReconnect(minionsHome, { afterMs, timeoutMs = 5000, pollMs = 500 } = {}) {
102
+ const start = Date.now();
103
+ while (Date.now() - start < timeoutMs) {
104
+ try {
105
+ const fp = path.join(minionsHome, 'engine', 'dashboard-browser.json');
106
+ const state = JSON.parse(fs.readFileSync(fp, 'utf8'));
107
+ const tabs = state && state.tabs ? Object.values(state.tabs) : [];
108
+ if (tabs.some(t => Number(t && t.lastSeen) > afterMs)) return true;
109
+ } catch { /* file may be temporarily missing during write */ }
110
+ await new Promise(r => setTimeout(r, pollMs));
111
+ }
112
+ return false;
113
+ }
114
+
115
+ function _openInBrowser(url) {
116
+ try {
117
+ if (process.platform === 'win32') execSync(`start "" "${url}"`, { stdio: 'ignore', windowsHide: true });
118
+ else if (process.platform === 'darwin') execSync(`open "${url}"`, { stdio: 'ignore' });
119
+ else execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
120
+ } catch (e) {
121
+ console.log(` Could not auto-open browser: ${e.message}`);
122
+ console.log(` Please open ${url} manually.`);
123
+ }
124
+ }
125
+
89
126
  /**
90
127
  * Read the engine's recorded PID from engine/control.json. Returns null if
91
128
  * the file is missing/corrupt or the PID isn't a positive integer.
@@ -411,12 +448,12 @@ function init() {
411
448
  if (isUpgrade && skipStart) return;
412
449
 
413
450
  // Auto-start on fresh install; direct force-upgrade restarts automatically.
414
- // Probe before kill so we suppress browser auto-open only when a browser tab
415
- // was recently polling the dashboard. A bare/orphan dashboard process on the
416
- // port is not enough; cold starts should still open the UI.
417
451
  const dashWasUp = isPortListening(DASH_PORT);
418
- const suppressDashboardOpen = !forceOpen && dashWasUp && hasRecentDashboardBrowserTab(MINIONS_HOME);
452
+ const restartStartMs = Date.now();
419
453
  if (isUpgrade) {
454
+ // Clear stale beacons so the post-restart probe only counts tabs that
455
+ // reconnect to the NEW dashboard.
456
+ _clearDashboardBrowserState(MINIONS_HOME);
420
457
  try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
421
458
  // Free the dashboard port too — without this the new dashboard EADDRINUSE-dies
422
459
  // silently and the user keeps running stale code from the old dashboard process.
@@ -431,10 +468,28 @@ function init() {
431
468
  engineProc.unref();
432
469
  console.log(` Engine started (PID: ${engineProc.pid})`);
433
470
 
434
- const dashProc = spawnDashboard(suppressDashboardOpen);
471
+ // Always suppress dashboard's self-open — we decide here after the health
472
+ // check based on whether an existing browser tab actually reconnects.
473
+ const dashProc = spawnDashboard(true);
435
474
  console.log(` Dashboard started (PID: ${dashProc.pid})`);
436
475
  console.log(` Dashboard: http://localhost:${DASH_PORT}`);
437
476
 
477
+ void (async () => {
478
+ let shouldOpen = forceOpen || !dashWasUp;
479
+ if (!shouldOpen) {
480
+ // Hot upgrade: give an existing browser tab up to 5s to reconnect via
481
+ // its 4s auto-refresh poll. If nothing beacons, the tab is gone — open.
482
+ const reconnected = await _waitForBrowserReconnect(MINIONS_HOME, {
483
+ afterMs: restartStartMs, timeoutMs: 5000,
484
+ });
485
+ shouldOpen = !reconnected;
486
+ }
487
+ if (shouldOpen) {
488
+ console.log(` Opening dashboard in browser...`);
489
+ _openInBrowser(`http://localhost:${DASH_PORT}`);
490
+ }
491
+ })();
492
+
438
493
  // Next steps guidance
439
494
  console.log(`
440
495
  Next steps:
@@ -689,11 +744,11 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
689
744
  // `--cli` / `--model` flags forward to `engine.js start` so the runtime
690
745
  // fleet flips before the daemon spawns (P-6b3f9c2e AC: works on restart).
691
746
  ensureInstalled();
692
- // Probe before kill so we suppress browser auto-open only when a browser tab
693
- // was recently polling the dashboard. A bare/orphan dashboard process on the
694
- // port is not enough; cold starts should still open the UI.
695
747
  const dashWasUp = isPortListening(DASH_PORT);
696
- const suppressDashboardOpen = !forceOpen && dashWasUp && hasRecentDashboardBrowserTab(MINIONS_HOME);
748
+ // Mark the restart boundary so post-restart beacon timestamps are unambiguous,
749
+ // and clear stale beacons (browsers that closed without notifying).
750
+ const restartStartMs = Date.now();
751
+ _clearDashboardBrowserState(MINIONS_HOME);
697
752
  // Layered kill — each step is best-effort, layered so the next still runs if
698
753
  // one fails. Goal: the old engine is gone before we spawn a new one, even if
699
754
  // PowerShell is unavailable, the engine is hung, or its cmdline doesn't match.
@@ -712,7 +767,9 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
712
767
  });
713
768
  engineProc.unref();
714
769
  console.log(`\n Engine started (PID: ${engineProc.pid})`);
715
- const dashProc = spawnDashboard(suppressDashboardOpen);
770
+ // Always tell the dashboard to skip its own auto-open — we'll decide here
771
+ // after observing whether an existing browser tab reconnects.
772
+ const dashProc = spawnDashboard(true);
716
773
  console.log(` Dashboard started (PID: ${dashProc.pid})`);
717
774
  console.log(` Dashboard: http://localhost:${DASH_PORT}`);
718
775
  console.log(' Verifying restart health...');
@@ -725,7 +782,20 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
725
782
  console.error(formatRestartHealthError(result));
726
783
  process.exit(1);
727
784
  }
728
- console.log(` Restart verified: engine PID ${result.engine.pid}; dashboard healthy.\n`);
785
+ console.log(` Restart verified: engine PID ${result.engine.pid}; dashboard healthy.`);
786
+
787
+ let shouldOpen = forceOpen || !dashWasUp;
788
+ if (!shouldOpen) {
789
+ const reconnected = await _waitForBrowserReconnect(MINIONS_HOME, {
790
+ afterMs: restartStartMs, timeoutMs: 5000,
791
+ });
792
+ shouldOpen = !reconnected;
793
+ }
794
+ if (shouldOpen) {
795
+ console.log(` Opening dashboard in browser...`);
796
+ _openInBrowser(`http://localhost:${DASH_PORT}`);
797
+ }
798
+ console.log('');
729
799
  })().catch(err => {
730
800
  console.error(`\n ERROR: Restart verification failed: ${err.message}\n`);
731
801
  process.exit(1);
@@ -516,6 +516,12 @@
516
516
  .engine-badge.running { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
517
517
  .engine-badge.paused { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); }
518
518
  .engine-badge.stopped { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
519
+ .test-mode-badge {
520
+ display: inline-block; margin-left: var(--space-6); padding: var(--space-1) var(--space-5);
521
+ background: var(--orange); color: #1a1a1a; font-size: var(--text-md); font-weight: 700;
522
+ letter-spacing: 0.5px; border-radius: var(--radius-sm); vertical-align: middle;
523
+ }
524
+ body.minions-test-mode > header { box-shadow: inset 0 -3px 0 var(--orange); }
519
525
  .engine-alert {
520
526
  display: none; margin: 8px 24px 0; padding: 8px 10px; border: 1px solid rgba(248,81,73,0.45);
521
527
  background: rgba(248,81,73,0.1); border-radius: var(--radius-sm); font-size: 11px; color: var(--text);
package/dashboard.js CHANGED
@@ -805,9 +805,25 @@ function resolvePlanPath(file) {
805
805
  return active;
806
806
  }
807
807
 
808
- // Assemble dashboard HTML from fragments (canonical source: dashboard/)
808
+ // Test-mode banner: surfaced in <title> + <h1> + body class when the dashboard
809
+ // is started under MINIONS_TEST_DIR or on a non-default port. Makes it visually
810
+ // obvious that the user is looking at a sandboxed instance, not their live
811
+ // fleet — prevents acting on test fixtures by mistake.
812
+ function _isTestMode() {
813
+ return !!process.env.MINIONS_TEST_DIR || (typeof PORT === 'number' && PORT !== 7331);
814
+ }
815
+
816
+ function _testBadgeLabel() {
817
+ if (process.env.MINIONS_TEST_DIR) return 'TEST';
818
+ if (typeof PORT === 'number' && PORT !== 7331) return `TEST :${PORT}`;
819
+ return '';
820
+ }
821
+
822
+ // Assemble dashboard HTML from fragments (canonical source: dashboard/).
823
+ // Resolves dashboard/ relative to __dirname (the checkout root) so test
824
+ // invocations under MINIONS_TEST_DIR still find the static layout/CSS/JS.
809
825
  function buildDashboardHtml() {
810
- const dashDir = path.join(MINIONS_DIR, 'dashboard');
826
+ const dashDir = path.join(__dirname, 'dashboard');
811
827
  const layoutPath = path.join(dashDir, 'layout.html');
812
828
 
813
829
  if (!fs.existsSync(layoutPath)) {
@@ -854,10 +870,22 @@ function buildDashboardHtml() {
854
870
 
855
871
  const featuresBootstrap = `window.MINIONS_FEATURES = ${JSON.stringify(featuresBoot)};\n`;
856
872
 
857
- return layout
873
+ let assembled = layout
858
874
  .replace('/* __CSS__ */', () => css)
859
875
  .replace('<!-- __PAGES__ -->', () => pageHtml)
860
876
  .replace('/* __JS__ */', () => `window.__MINIONS_HOME = ${JSON.stringify(os.homedir())};\n${featuresBootstrap}${jsHtml}`);
877
+
878
+ if (_isTestMode()) {
879
+ const label = _testBadgeLabel();
880
+ assembled = assembled
881
+ .replace('<title>Minions Mission Control</title>',
882
+ `<title>[${label}] Minions Mission Control</title>`)
883
+ .replace('<h1>Minions Mission Control</h1>',
884
+ `<h1>Minions Mission Control <span class="test-mode-badge">${label}</span></h1>`)
885
+ .replace('<body>',
886
+ '<body class="minions-test-mode">');
887
+ }
888
+ return assembled;
861
889
  }
862
890
 
863
891
  let HTML_RAW = buildDashboardHtml();
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-09T18:23:44.280Z"
4
+ "cachedAt": "2026-05-09T22:56:52.587Z"
5
5
  }
@@ -28,10 +28,11 @@
28
28
  const fs = require('fs');
29
29
  const os = require('os');
30
30
  const path = require('path');
31
- const { FAILURE_CLASS, safeWrite, ts } = require('../shared');
31
+ const { FAILURE_CLASS, safeWrite, ts, resolveEngineCacheDir } = require('../shared');
32
32
 
33
33
  const ENGINE_DIR = __dirname.replace(/[\\/]runtimes$/, '');
34
34
  const MINIONS_DIR = path.resolve(ENGINE_DIR, '..');
35
+ const _CACHE_DIR = resolveEngineCacheDir(ENGINE_DIR);
35
36
 
36
37
  const isWin = process.platform === 'win32';
37
38
 
@@ -40,7 +41,7 @@ const isWin = process.platform === 'win32';
40
41
  // repeated path-probe (PATH / npm-global / npm-root-g) only happens once per
41
42
  // install.
42
43
 
43
- const CAPS_FILE = path.join(ENGINE_DIR, 'claude-caps.json');
44
+ const CAPS_FILE = path.join(_CACHE_DIR, 'claude-caps.json');
44
45
 
45
46
  function _safeJson(p) {
46
47
  try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
@@ -185,7 +186,7 @@ function listModels() {
185
186
  return null;
186
187
  }
187
188
 
188
- const MODELS_CACHE = path.join(ENGINE_DIR, 'claude-models.json');
189
+ const MODELS_CACHE = path.join(_CACHE_DIR, 'claude-models.json');
189
190
 
190
191
  // ── Argument Construction ────────────────────────────────────────────────────
191
192
 
@@ -32,9 +32,10 @@ const https = require('https');
32
32
  const os = require('os');
33
33
  const path = require('path');
34
34
  const { execSync } = require('child_process');
35
- const { FAILURE_CLASS, safeWrite, ts } = require('../shared');
35
+ const { FAILURE_CLASS, safeWrite, ts, resolveEngineCacheDir } = require('../shared');
36
36
 
37
37
  const ENGINE_DIR = __dirname.replace(/[\\/]runtimes$/, '');
38
+ const _CACHE_DIR = resolveEngineCacheDir(ENGINE_DIR);
38
39
  const isWin = process.platform === 'win32';
39
40
 
40
41
  // ── Binary Resolution ───────────────────────────────────────────────────────
@@ -51,8 +52,8 @@ const isWin = process.platform === 'win32';
51
52
  // We deliberately do NOT npm-probe — Copilot is not an npm package. Doing so
52
53
  // would be confusing dead code that suggests an install path that doesn't exist.
53
54
 
54
- const CAPS_FILE = path.join(ENGINE_DIR, 'copilot-caps.json');
55
- const MODELS_CACHE = path.join(ENGINE_DIR, 'copilot-models.json');
55
+ const CAPS_FILE = path.join(_CACHE_DIR, 'copilot-caps.json');
56
+ const MODELS_CACHE = path.join(_CACHE_DIR, 'copilot-models.json');
56
57
 
57
58
  function _safeJson(p) {
58
59
  try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
package/engine/shared.js CHANGED
@@ -240,6 +240,17 @@ function _currentLogPath() {
240
240
  return path.join(root, 'engine', 'log.json');
241
241
  }
242
242
 
243
+ // MINIONS_TEST_DIR-aware engine dir for cache files (runtime caps, models,
244
+ // spawn-debug.log). Returns `<MINIONS_TEST_DIR>/engine` under tests; otherwise
245
+ // the caller's source-adjacent fallback (typically `__dirname`-derived). Never
246
+ // consults MINIONS_HOME — tests must not leak into the install root.
247
+ function resolveEngineCacheDir(fallbackEngineDir) {
248
+ if (process.env.MINIONS_TEST_DIR) {
249
+ return path.join(path.resolve(process.env.MINIONS_TEST_DIR), 'engine');
250
+ }
251
+ return fallbackEngineDir;
252
+ }
253
+
243
254
  function _flushLogBuffer() {
244
255
  if (_logBuffer.length === 0) return;
245
256
  const drained = _logBuffer.splice(0);
@@ -3368,6 +3379,7 @@ function createThrottleTracker({ label, baseBackoffMs = 60000, maxBackoffMs = 32
3368
3379
  module.exports = {
3369
3380
  MINIONS_DIR,
3370
3381
  ENGINE_DIR,
3382
+ resolveEngineCacheDir,
3371
3383
  CONTROL_PATH,
3372
3384
  COOLDOWNS_PATH,
3373
3385
  PR_LINKS_PATH,
@@ -35,7 +35,7 @@
35
35
  const fs = require('fs');
36
36
  const os = require('os');
37
37
  const path = require('path');
38
- const { runFile, cleanChildEnv, killGracefully, killImmediate, ts } = require('./shared');
38
+ const { runFile, cleanChildEnv, killGracefully, killImmediate, ts, resolveEngineCacheDir } = require('./shared');
39
39
  const { resolveRuntime } = require('./runtimes');
40
40
  const { acquireAdoTokenSync, isLikelyAdoToken } = require('./ado-token');
41
41
 
@@ -321,8 +321,8 @@ function main() {
321
321
  opts, passthrough, addDirs,
322
322
  });
323
323
 
324
- // Debug log (async — not on critical path)
325
- const tmpDir = path.join(__dirname, 'tmp');
324
+ // Debug log (async — not on critical path).
325
+ const tmpDir = path.join(resolveEngineCacheDir(__dirname), 'tmp');
326
326
  if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
327
327
  const debugPath = path.join(tmpDir, 'spawn-debug.log');
328
328
  fs.writeFile(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1824",
3
+ "version": "0.1.1826",
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"