@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 +10 -0
- package/bin/minions.js +81 -11
- package/dashboard/styles.css +6 -0
- package/dashboard.js +31 -3
- package/engine/copilot-models.json +1 -1
- package/engine/runtimes/claude.js +4 -3
- package/engine/runtimes/copilot.js +4 -3
- package/engine/shared.js +12 -0
- package/engine/spawn-agent.js +3 -3
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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);
|
package/dashboard/styles.css
CHANGED
|
@@ -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
|
-
//
|
|
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(
|
|
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
|
-
|
|
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();
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
55
|
-
const MODELS_CACHE = path.join(
|
|
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,
|
package/engine/spawn-agent.js
CHANGED
|
@@ -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.
|
|
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"
|