@yemi33/minions 0.1.1769 → 0.1.1771
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 +12 -0
- package/bin/minions.js +24 -7
- package/dashboard/js/state.js +49 -0
- package/dashboard.js +99 -34
- package/engine/cleanup.js +111 -1
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +10 -4
- package/engine/shared.js +41 -6
- package/minions.js +2 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1771 (2026-05-07)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- clean merged pr local branches (#2165)
|
|
7
|
+
|
|
8
|
+
## 0.1.1770 (2026-05-07)
|
|
9
|
+
|
|
10
|
+
### Fixes
|
|
11
|
+
- open dashboard on update cold start
|
|
12
|
+
- silence generated Claude defaults warning
|
|
13
|
+
- drop TTL and turn-count cap so users can resume any time
|
|
14
|
+
|
|
3
15
|
## 0.1.1769 (2026-05-07)
|
|
4
16
|
|
|
5
17
|
### Fixes
|
package/bin/minions.js
CHANGED
|
@@ -29,6 +29,7 @@ const { spawn, spawnSync, execSync } = require('child_process');
|
|
|
29
29
|
|
|
30
30
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
31
31
|
const DASH_PORT = 7331;
|
|
32
|
+
const DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS = 45000;
|
|
32
33
|
|
|
33
34
|
/** Returns PIDs (as strings) of processes LISTENING on `port`. Empty on no match
|
|
34
35
|
* or when the platform tool (netstat/findstr/lsof) is unavailable. */
|
|
@@ -60,6 +61,18 @@ function killByPort(port) {
|
|
|
60
61
|
|
|
61
62
|
const isPortListening = (port) => getListeningPids(port).length > 0;
|
|
62
63
|
|
|
64
|
+
function hasRecentDashboardBrowserTab(minionsHome, now = Date.now()) {
|
|
65
|
+
try {
|
|
66
|
+
const fp = path.join(minionsHome, 'engine', 'dashboard-browser.json');
|
|
67
|
+
const state = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
68
|
+
const tabs = state && state.tabs && typeof state.tabs === 'object' ? state.tabs : {};
|
|
69
|
+
return Object.values(tabs).some(tab => {
|
|
70
|
+
const lastSeen = Number(tab && tab.lastSeen);
|
|
71
|
+
return Number.isFinite(lastSeen) && now - lastSeen <= DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS;
|
|
72
|
+
});
|
|
73
|
+
} catch { return false; }
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
/**
|
|
64
77
|
* Read the engine's recorded PID from engine/control.json. Returns null if
|
|
65
78
|
* the file is missing/corrupt or the PID isn't a positive integer.
|
|
@@ -118,7 +131,7 @@ function killMinionsProcesses(patterns) {
|
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
/** Spawn a detached dashboard. When `suppressOpen` is true, the new dashboard
|
|
121
|
-
* skips its auto-open of the browser — the existing tab will
|
|
134
|
+
* skips its auto-open of the browser — the existing tab will reconnect. */
|
|
122
135
|
function spawnDashboard(suppressOpen) {
|
|
123
136
|
const env = suppressOpen ? { ...process.env, MINIONS_NO_AUTO_OPEN: '1' } : process.env;
|
|
124
137
|
const proc = spawn(process.execPath, [path.join(MINIONS_HOME, 'dashboard.js')], {
|
|
@@ -397,9 +410,11 @@ function init() {
|
|
|
397
410
|
if (isUpgrade && skipStart) return;
|
|
398
411
|
|
|
399
412
|
// Auto-start on fresh install; direct force-upgrade restarts automatically.
|
|
400
|
-
// Probe before kill so we
|
|
401
|
-
//
|
|
413
|
+
// Probe before kill so we suppress browser auto-open only when a browser tab
|
|
414
|
+
// was recently polling the dashboard. A bare/orphan dashboard process on the
|
|
415
|
+
// port is not enough; cold starts should still open the UI.
|
|
402
416
|
const dashWasUp = isPortListening(DASH_PORT);
|
|
417
|
+
const suppressDashboardOpen = dashWasUp && hasRecentDashboardBrowserTab(MINIONS_HOME);
|
|
403
418
|
if (isUpgrade) {
|
|
404
419
|
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
|
|
405
420
|
// Free the dashboard port too — without this the new dashboard EADDRINUSE-dies
|
|
@@ -415,7 +430,7 @@ function init() {
|
|
|
415
430
|
engineProc.unref();
|
|
416
431
|
console.log(` Engine started (PID: ${engineProc.pid})`);
|
|
417
432
|
|
|
418
|
-
const dashProc = spawnDashboard(
|
|
433
|
+
const dashProc = spawnDashboard(suppressDashboardOpen);
|
|
419
434
|
console.log(` Dashboard started (PID: ${dashProc.pid})`);
|
|
420
435
|
console.log(` Dashboard: http://localhost:${DASH_PORT}`);
|
|
421
436
|
|
|
@@ -670,9 +685,11 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
670
685
|
// `--cli` / `--model` flags forward to `engine.js start` so the runtime
|
|
671
686
|
// fleet flips before the daemon spawns (P-6b3f9c2e AC: works on restart).
|
|
672
687
|
ensureInstalled();
|
|
673
|
-
// Probe before kill so we
|
|
674
|
-
//
|
|
688
|
+
// Probe before kill so we suppress browser auto-open only when a browser tab
|
|
689
|
+
// was recently polling the dashboard. A bare/orphan dashboard process on the
|
|
690
|
+
// port is not enough; cold starts should still open the UI.
|
|
675
691
|
const dashWasUp = isPortListening(DASH_PORT);
|
|
692
|
+
const suppressDashboardOpen = dashWasUp && hasRecentDashboardBrowserTab(MINIONS_HOME);
|
|
676
693
|
// Layered kill — each step is best-effort, layered so the next still runs if
|
|
677
694
|
// one fails. Goal: the old engine is gone before we spawn a new one, even if
|
|
678
695
|
// PowerShell is unavailable, the engine is hung, or its cmdline doesn't match.
|
|
@@ -691,7 +708,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
691
708
|
});
|
|
692
709
|
engineProc.unref();
|
|
693
710
|
console.log(`\n Engine started (PID: ${engineProc.pid})`);
|
|
694
|
-
const dashProc = spawnDashboard(
|
|
711
|
+
const dashProc = spawnDashboard(suppressDashboardOpen);
|
|
695
712
|
console.log(` Dashboard started (PID: ${dashProc.pid})`);
|
|
696
713
|
console.log(` Dashboard: http://localhost:${DASH_PORT}\n`);
|
|
697
714
|
} else if (cmd === 'nuke') {
|
package/dashboard/js/state.js
CHANGED
|
@@ -4,6 +4,17 @@ let inboxData = [];
|
|
|
4
4
|
let agentData = [];
|
|
5
5
|
let currentAgentId = null;
|
|
6
6
|
let currentTab = 'thought-process';
|
|
7
|
+
const DASHBOARD_TAB_ID = (function() {
|
|
8
|
+
try {
|
|
9
|
+
var existing = sessionStorage.getItem('minions-dashboard-tab-id');
|
|
10
|
+
if (existing) return existing;
|
|
11
|
+
var id = 'tab-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
|
12
|
+
sessionStorage.setItem('minions-dashboard-tab-id', id);
|
|
13
|
+
return id;
|
|
14
|
+
} catch {
|
|
15
|
+
return 'tab-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
|
16
|
+
}
|
|
17
|
+
})();
|
|
7
18
|
|
|
8
19
|
// Sidebar page navigation — URL-routed
|
|
9
20
|
function getPageFromUrl() {
|
|
@@ -87,6 +98,37 @@ function prunePrdRequeueState(workItems) {
|
|
|
87
98
|
}
|
|
88
99
|
}
|
|
89
100
|
|
|
101
|
+
function _dashboardPresencePayload(closed) {
|
|
102
|
+
return JSON.stringify({
|
|
103
|
+
tabId: DASHBOARD_TAB_ID,
|
|
104
|
+
closed: !!closed,
|
|
105
|
+
url: location.pathname + location.search + location.hash,
|
|
106
|
+
visibility: document.visibilityState || '',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _sendDashboardPresence(closed) {
|
|
111
|
+
var payload = _dashboardPresencePayload(closed);
|
|
112
|
+
try {
|
|
113
|
+
if (navigator.sendBeacon) {
|
|
114
|
+
var blob = new Blob([payload], { type: 'application/json' });
|
|
115
|
+
if (navigator.sendBeacon('/api/browser-presence', blob)) return;
|
|
116
|
+
}
|
|
117
|
+
} catch {}
|
|
118
|
+
try {
|
|
119
|
+
fetch('/api/browser-presence', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
body: payload,
|
|
123
|
+
keepalive: true,
|
|
124
|
+
}).catch(function() {});
|
|
125
|
+
} catch {}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
window.addEventListener('pagehide', function() { _sendDashboardPresence(true); });
|
|
129
|
+
window.addEventListener('beforeunload', function() { _sendDashboardPresence(true); });
|
|
130
|
+
document.addEventListener('visibilitychange', function() { _sendDashboardPresence(false); });
|
|
131
|
+
|
|
90
132
|
function rerenderPrdFromCache() {
|
|
91
133
|
if (!window._lastStatus || !window._lastStatus.prdProgress) return;
|
|
92
134
|
renderPrdProgress(window._lastStatus.prdProgress);
|
|
@@ -100,6 +142,13 @@ function safeFetch(url, opts) {
|
|
|
100
142
|
var controller = new AbortController();
|
|
101
143
|
var timer = setTimeout(function() { controller.abort(); }, timeout);
|
|
102
144
|
var fetchOpts = Object.assign({}, opts, { signal: controller.signal });
|
|
145
|
+
if (typeof url === 'string' && url.indexOf('/api/') === 0) {
|
|
146
|
+
var headers = Object.assign({}, fetchOpts.headers || {});
|
|
147
|
+
headers['X-Minions-Dashboard-Tab'] = DASHBOARD_TAB_ID;
|
|
148
|
+
headers['X-Minions-Dashboard-Url'] = location.pathname + location.search + location.hash;
|
|
149
|
+
headers['X-Minions-Dashboard-Visibility'] = document.visibilityState || '';
|
|
150
|
+
fetchOpts.headers = headers;
|
|
151
|
+
}
|
|
103
152
|
delete fetchOpts.timeout;
|
|
104
153
|
return fetch(url, fetchOpts).finally(function() { clearTimeout(timer); });
|
|
105
154
|
}
|
package/dashboard.js
CHANGED
|
@@ -60,6 +60,8 @@ const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
|
|
|
60
60
|
const PORT = parseInt(process.env.PORT || process.argv[2]) || 7331;
|
|
61
61
|
let CONFIG = queries.getConfig();
|
|
62
62
|
let PROJECTS = _getProjects(CONFIG);
|
|
63
|
+
const DASHBOARD_BROWSER_PRESENCE_PATH = path.join(ENGINE_DIR, 'dashboard-browser.json');
|
|
64
|
+
const DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS = 45000;
|
|
63
65
|
|
|
64
66
|
function ensureConfiguredProjectStateFiles() {
|
|
65
67
|
for (const p of PROJECTS) {
|
|
@@ -571,6 +573,66 @@ function _trackSseClient(clientSet, req, res, { heartbeatMs = SSE_CLIENT_HEARTBE
|
|
|
571
573
|
return cleanup;
|
|
572
574
|
}
|
|
573
575
|
|
|
576
|
+
function _normalizeDashboardTabId(value) {
|
|
577
|
+
const id = typeof value === 'string' ? value.trim() : '';
|
|
578
|
+
return /^[A-Za-z0-9._:-]{1,96}$/.test(id) ? id : null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function _pruneDashboardBrowserTabs(tabs, now = Date.now()) {
|
|
582
|
+
for (const [id, tab] of Object.entries(tabs || {})) {
|
|
583
|
+
const lastSeen = Number(tab && tab.lastSeen);
|
|
584
|
+
if (!Number.isFinite(lastSeen) || now - lastSeen > DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS) delete tabs[id];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function _recordDashboardBrowserPresence(tabId, { closed = false, url = '', visibility = '' } = {}) {
|
|
589
|
+
const id = _normalizeDashboardTabId(tabId);
|
|
590
|
+
if (!id) return false;
|
|
591
|
+
const now = Date.now();
|
|
592
|
+
mutateJsonFileLocked(DASHBOARD_BROWSER_PRESENCE_PATH, (state) => {
|
|
593
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) state = {};
|
|
594
|
+
if (!state.tabs || typeof state.tabs !== 'object' || Array.isArray(state.tabs)) state.tabs = {};
|
|
595
|
+
_pruneDashboardBrowserTabs(state.tabs, now);
|
|
596
|
+
if (closed) {
|
|
597
|
+
delete state.tabs[id];
|
|
598
|
+
} else {
|
|
599
|
+
state.tabs[id] = {
|
|
600
|
+
id,
|
|
601
|
+
lastSeen: now,
|
|
602
|
+
lastSeenAt: new Date(now).toISOString(),
|
|
603
|
+
url: typeof url === 'string' ? url.slice(0, 512) : '',
|
|
604
|
+
visibility: typeof visibility === 'string' ? visibility.slice(0, 32) : '',
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
state.updatedAt = new Date(now).toISOString();
|
|
608
|
+
return state;
|
|
609
|
+
}, { defaultValue: { tabs: {} } });
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function _recordDashboardBrowserPresenceFromRequest(req) {
|
|
614
|
+
const tabId = req && req.headers && req.headers['x-minions-dashboard-tab'];
|
|
615
|
+
if (!tabId) return;
|
|
616
|
+
try {
|
|
617
|
+
_recordDashboardBrowserPresence(tabId, {
|
|
618
|
+
url: req.headers['x-minions-dashboard-url'] || '',
|
|
619
|
+
visibility: req.headers['x-minions-dashboard-visibility'] || '',
|
|
620
|
+
});
|
|
621
|
+
} catch (e) {
|
|
622
|
+
shared.log('warn', `Dashboard browser presence update failed: ${e.message}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function _getDashboardBrowserPresence(now = Date.now()) {
|
|
627
|
+
const state = safeJsonObj(DASHBOARD_BROWSER_PRESENCE_PATH);
|
|
628
|
+
const tabs = state.tabs && typeof state.tabs === 'object' && !Array.isArray(state.tabs) ? state.tabs : {};
|
|
629
|
+
const activeTabs = Object.values(tabs).filter(tab => {
|
|
630
|
+
const lastSeen = Number(tab && tab.lastSeen);
|
|
631
|
+
return Number.isFinite(lastSeen) && now - lastSeen <= DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS;
|
|
632
|
+
});
|
|
633
|
+
return { active: activeTabs.length > 0, activeTabs: activeTabs.length, maxAgeMs: DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS };
|
|
634
|
+
}
|
|
635
|
+
|
|
574
636
|
// Hot-reload: watch dashboard/ directory for changes, rebuild, and push reload to browsers
|
|
575
637
|
const _hotReloadClients = new Set();
|
|
576
638
|
|
|
@@ -989,12 +1051,7 @@ setInterval(() => {
|
|
|
989
1051
|
|
|
990
1052
|
// CC chat sessions do NOT auto-expire. A tab is removed only via explicit user
|
|
991
1053
|
// deletion (DELETE /api/cc-sessions/:id, wired from ccCloseTab). Doc-chat
|
|
992
|
-
// sessions
|
|
993
|
-
//
|
|
994
|
-
// CC_SESSION_MAX_TURNS is reused by the doc-chat session pruner to cap
|
|
995
|
-
// per-session turn growth there; CC chat sessions are not capped because
|
|
996
|
-
// users are expected to keep long-running tabs alive indefinitely.
|
|
997
|
-
const CC_SESSION_MAX_TURNS = shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
1054
|
+
// sessions follow the same policy — see DOC_SESSIONS section below.
|
|
998
1055
|
let ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
999
1056
|
const ccInFlightTabs = new Map(); // tabId → timestamp — per-tab in-flight tracking for parallel CC requests
|
|
1000
1057
|
const ccInFlightAborts = new Map(); // tabId → abortFn — lets a new request kill the stale LLM
|
|
@@ -2177,11 +2234,14 @@ async function executeDocChatActions(actions) {
|
|
|
2177
2234
|
|
|
2178
2235
|
// ── Shared LLM call core — used by CC panel and doc modals ──────────────────
|
|
2179
2236
|
|
|
2180
|
-
// Session store for doc modals — keyed by filePath or title, persisted to disk
|
|
2237
|
+
// Session store for doc modals — keyed by filePath or title, persisted to disk.
|
|
2238
|
+
// Doc-chat sessions do NOT auto-expire (parity with CC tabs). The only
|
|
2239
|
+
// invalidation paths are prompt-hash mismatch (correctness — a stale system
|
|
2240
|
+
// prompt would carry the old persona/rules into resume turns) and the
|
|
2241
|
+
// LRU cap below (storage hygiene). Users can resume a doc-chat any time.
|
|
2181
2242
|
const CC_SESSIONS_PATH = path.join(ENGINE_DIR, 'cc-sessions.json');
|
|
2182
2243
|
const DOC_SESSIONS_PATH = path.join(ENGINE_DIR, 'doc-sessions.json');
|
|
2183
2244
|
const CC_SESSION_PATH = path.join(ENGINE_DIR, 'cc-session.json');
|
|
2184
|
-
const DOC_SESSION_TTL_MS = shared.ENGINE_DEFAULTS.docSessionTtlMs;
|
|
2185
2245
|
const DOC_SESSION_MAX_ENTRIES = shared.ENGINE_DEFAULTS.docSessionMaxEntries;
|
|
2186
2246
|
const docSessions = new Map(); // key → { sessionId, lastActiveAt, turnCount }
|
|
2187
2247
|
|
|
@@ -2193,9 +2253,7 @@ function _docSessionLastActiveMs(session) {
|
|
|
2193
2253
|
function pruneDocSessions() {
|
|
2194
2254
|
let changed = false;
|
|
2195
2255
|
for (const [key, s] of docSessions.entries()) {
|
|
2196
|
-
if (!s ||
|
|
2197
|
-
_sessionExpired(s.lastActiveAt || s.createdAt, DOC_SESSION_TTL_MS) ||
|
|
2198
|
-
s._promptHash !== _docChatPromptHash) {
|
|
2256
|
+
if (!s || s._promptHash !== _docChatPromptHash) {
|
|
2199
2257
|
docSessions.delete(key);
|
|
2200
2258
|
changed = true;
|
|
2201
2259
|
}
|
|
@@ -2212,13 +2270,12 @@ function pruneDocSessions() {
|
|
|
2212
2270
|
return changed;
|
|
2213
2271
|
}
|
|
2214
2272
|
|
|
2215
|
-
// Load persisted doc sessions on startup
|
|
2273
|
+
// Load persisted doc sessions on startup — no TTL/turn filtering, sessions
|
|
2274
|
+
// are non-expiring. Stale prompt-hash entries are dropped by pruneDocSessions.
|
|
2216
2275
|
try {
|
|
2217
2276
|
const saved = safeJson(DOC_SESSIONS_PATH);
|
|
2218
2277
|
if (saved && typeof saved === 'object') {
|
|
2219
2278
|
for (const [key, s] of Object.entries(saved)) {
|
|
2220
|
-
if (s.turnCount >= CC_SESSION_MAX_TURNS) continue;
|
|
2221
|
-
if (_sessionExpired(s.lastActiveAt || s.createdAt, DOC_SESSION_TTL_MS)) continue;
|
|
2222
2279
|
docSessions.set(key, s);
|
|
2223
2280
|
}
|
|
2224
2281
|
pruneDocSessions();
|
|
@@ -2233,9 +2290,11 @@ function persistDocSessions() {
|
|
|
2233
2290
|
mutateJsonFileLocked(DOC_SESSIONS_PATH, () => obj, { defaultValue: {} });
|
|
2234
2291
|
}
|
|
2235
2292
|
|
|
2293
|
+
// Hourly hygiene sweep — drops prompt-hash mismatches and trims the LRU cap.
|
|
2294
|
+
// No TTL involved; sessions live until explicitly evicted by these two policies.
|
|
2236
2295
|
const _docSessionPruneTimer = setInterval(() => {
|
|
2237
2296
|
if (pruneDocSessions()) persistDocSessions();
|
|
2238
|
-
},
|
|
2297
|
+
}, 60 * 60 * 1000);
|
|
2239
2298
|
if (_docSessionPruneTimer.unref) _docSessionPruneTimer.unref();
|
|
2240
2299
|
|
|
2241
2300
|
// Debounced variant — coalesces rapid writes (e.g. back-to-back doc-chat turns)
|
|
@@ -2270,16 +2329,6 @@ function resolveSession(store, key) {
|
|
|
2270
2329
|
persistDocSessions();
|
|
2271
2330
|
return null;
|
|
2272
2331
|
}
|
|
2273
|
-
if (s.turnCount >= CC_SESSION_MAX_TURNS) {
|
|
2274
|
-
docSessions.delete(key);
|
|
2275
|
-
persistDocSessions();
|
|
2276
|
-
return null;
|
|
2277
|
-
}
|
|
2278
|
-
if (_sessionExpired(s.lastActiveAt || s.createdAt, DOC_SESSION_TTL_MS)) {
|
|
2279
|
-
docSessions.delete(key);
|
|
2280
|
-
persistDocSessions();
|
|
2281
|
-
return null;
|
|
2282
|
-
}
|
|
2283
2332
|
return s;
|
|
2284
2333
|
}
|
|
2285
2334
|
|
|
@@ -6348,9 +6397,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6348
6397
|
const configPath = path.join(MINIONS_DIR, 'config.json');
|
|
6349
6398
|
const config = safeJson(configPath) || {};
|
|
6350
6399
|
if (!config.engine) config.engine = {};
|
|
6351
|
-
if (!config.claude) config.claude = {};
|
|
6352
6400
|
if (!config.agents) config.agents = {};
|
|
6353
|
-
|
|
6401
|
+
shared.pruneDefaultClaudeConfig(config);
|
|
6354
6402
|
|
|
6355
6403
|
const _clamped = [];
|
|
6356
6404
|
const _engineModelDiscovery = require('./engine/model-discovery');
|
|
@@ -6518,9 +6566,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6518
6566
|
}
|
|
6519
6567
|
|
|
6520
6568
|
if (body.claude) {
|
|
6569
|
+
if (!config.claude) config.claude = {};
|
|
6521
6570
|
for (const key of ['allowedTools', 'outputFormat']) {
|
|
6522
6571
|
if (body.claude[key] !== undefined) config.claude[key] = String(body.claude[key]);
|
|
6523
6572
|
}
|
|
6573
|
+
shared.pruneDefaultClaudeConfig(config);
|
|
6524
6574
|
}
|
|
6525
6575
|
|
|
6526
6576
|
if (body.agents) {
|
|
@@ -6647,6 +6697,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6647
6697
|
}
|
|
6648
6698
|
}
|
|
6649
6699
|
|
|
6700
|
+
shared.pruneDefaultClaudeConfig(config);
|
|
6650
6701
|
safeWrite(configPath, config);
|
|
6651
6702
|
// Refresh in-memory CONFIG so subsequent reads see the update
|
|
6652
6703
|
reloadConfig();
|
|
@@ -6672,7 +6723,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6672
6723
|
try {
|
|
6673
6724
|
const config = queries.getConfig();
|
|
6674
6725
|
config.engine = { ...shared.ENGINE_DEFAULTS };
|
|
6675
|
-
config.claude
|
|
6726
|
+
delete config.claude;
|
|
6676
6727
|
config.agents = { ...shared.DEFAULT_AGENTS };
|
|
6677
6728
|
safeWrite(path.join(MINIONS_DIR, 'config.json'), config);
|
|
6678
6729
|
reloadConfig();
|
|
@@ -6737,6 +6788,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6737
6788
|
|
|
6738
6789
|
async function handleStatus(req, res) {
|
|
6739
6790
|
try {
|
|
6791
|
+
_recordDashboardBrowserPresenceFromRequest(req);
|
|
6740
6792
|
// Use pre-serialized JSON and pre-computed gzip buffer — zero per-request compression
|
|
6741
6793
|
const json = getStatusJson();
|
|
6742
6794
|
res.setHeader('Content-Type', 'application/json');
|
|
@@ -6876,6 +6928,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6876
6928
|
|
|
6877
6929
|
// Status & health
|
|
6878
6930
|
{ method: 'GET', path: '/api/status', desc: 'Full dashboard status snapshot (agents, PRDs, work items, dispatch, etc.)', handler: handleStatus },
|
|
6931
|
+
{ method: 'GET', path: '/api/browser-presence', desc: 'Whether a dashboard browser tab was recently active', handler: (req, res) => {
|
|
6932
|
+
return jsonReply(res, 200, _getDashboardBrowserPresence(), req);
|
|
6933
|
+
}},
|
|
6934
|
+
{ method: 'POST', path: '/api/browser-presence', desc: 'Record dashboard browser tab heartbeat or close', params: 'tabId, closed?, url?, visibility?', handler: async (req, res) => {
|
|
6935
|
+
const body = await readBody(req);
|
|
6936
|
+
const tabId = _normalizeDashboardTabId(body.tabId);
|
|
6937
|
+
if (!tabId) return jsonReply(res, 400, { error: 'valid tabId required' }, req);
|
|
6938
|
+
_recordDashboardBrowserPresence(tabId, {
|
|
6939
|
+
closed: !!body.closed,
|
|
6940
|
+
url: body.url || '',
|
|
6941
|
+
visibility: body.visibility || '',
|
|
6942
|
+
});
|
|
6943
|
+
return jsonReply(res, 200, { ok: true }, req);
|
|
6944
|
+
}},
|
|
6879
6945
|
{ method: 'GET', path: '/api/status-stream', desc: 'SSE stream of real-time status updates', handler: (req, res) => {
|
|
6880
6946
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
6881
6947
|
res.write('data: ' + getStatusJson() + '\n\n');
|
|
@@ -7517,14 +7583,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7517
7583
|
if (route.path === '/api/skill') {
|
|
7518
7584
|
if (!req.url.startsWith('/api/skill?') && req.url !== '/api/skill') continue;
|
|
7519
7585
|
const _result = await route.handler(req, res, {});
|
|
7520
|
-
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream')) {
|
|
7586
|
+
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream') && !pathname.includes('/browser-presence')) {
|
|
7521
7587
|
console.log(` ${req.method} ${pathname} ${Date.now() - _reqStart}ms`);
|
|
7522
7588
|
}
|
|
7523
7589
|
return _result;
|
|
7524
7590
|
}
|
|
7525
7591
|
if (pathname !== route.path) continue;
|
|
7526
7592
|
const _result = await route.handler(req, res, {});
|
|
7527
|
-
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream')) {
|
|
7593
|
+
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream') && !pathname.includes('/browser-presence')) {
|
|
7528
7594
|
console.log(` ${req.method} ${pathname} ${Date.now() - _reqStart}ms`);
|
|
7529
7595
|
}
|
|
7530
7596
|
return _result;
|
|
@@ -7532,7 +7598,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7532
7598
|
const m = pathname.match(route.path);
|
|
7533
7599
|
if (m) {
|
|
7534
7600
|
const _result = await route.handler(req, res, m);
|
|
7535
|
-
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream')) {
|
|
7601
|
+
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream') && !pathname.includes('/browser-presence')) {
|
|
7536
7602
|
console.log(` ${req.method} ${pathname} ${Date.now() - _reqStart}ms`);
|
|
7537
7603
|
}
|
|
7538
7604
|
return _result;
|
|
@@ -7654,9 +7720,8 @@ if (require.main === module) {
|
|
|
7654
7720
|
console.log(`\n Auto-refreshes every 4s. Ctrl+C to stop.\n`);
|
|
7655
7721
|
|
|
7656
7722
|
// Auto-open the browser unless suppressed. `minions restart` and the
|
|
7657
|
-
// upgrade path set MINIONS_NO_AUTO_OPEN=1 when
|
|
7658
|
-
//
|
|
7659
|
-
// would just be a duplicate.
|
|
7723
|
+
// upgrade path set MINIONS_NO_AUTO_OPEN=1 only when a browser tab was
|
|
7724
|
+
// recently polling the old dashboard, so a new tab would just be a duplicate.
|
|
7660
7725
|
if (!process.env.MINIONS_NO_AUTO_OPEN) {
|
|
7661
7726
|
const { exec } = require('child_process');
|
|
7662
7727
|
try {
|
package/engine/cleanup.js
CHANGED
|
@@ -48,6 +48,105 @@ function worktreeMatchesBranch(dirLower, branch, actualBranch = '') {
|
|
|
48
48
|
return worktreeBranchMatches(actualBranch, branch) || worktreeDirMatchesBranch(dirLower, branch);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function normalizeLocalBranchName(branch) {
|
|
52
|
+
return String(branch || '').trim().replace(/^refs\/heads\//i, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isSafeLocalBranchName(branch) {
|
|
56
|
+
if (!branch || branch !== sanitizeBranch(branch)) return false;
|
|
57
|
+
if (branch.startsWith('-') || branch.includes('..') || branch.includes('@{')) return false;
|
|
58
|
+
if (branch.endsWith('/') || branch.endsWith('.lock')) return false;
|
|
59
|
+
return branch.split('/').every(part => part && part !== '.' && part !== '..' && !part.endsWith('.lock'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isProtectedLocalBranch(branch, project = {}) {
|
|
63
|
+
const protectedBranches = new Set(['main', 'master', 'trunk', 'develop', 'development', 'head']);
|
|
64
|
+
const configuredMain = normalizeLocalBranchName(project.mainBranch);
|
|
65
|
+
if (configuredMain) protectedBranches.add(configuredMain.toLowerCase());
|
|
66
|
+
return protectedBranches.has(branch.toLowerCase());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function localBranchWorktreeInUse(root, branch) {
|
|
70
|
+
try {
|
|
71
|
+
const out = String(shared.execSilent('git worktree list --porcelain', {
|
|
72
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 10000, windowsHide: true,
|
|
73
|
+
}) || '');
|
|
74
|
+
return out.split(/\r?\n/).some(line => line.trim() === `branch refs/heads/${branch}`);
|
|
75
|
+
} catch {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cleanupMergedPrLocalBranch(root, project, pr) {
|
|
81
|
+
const branch = normalizeLocalBranchName(pr?.branch);
|
|
82
|
+
const result = { deleted: false, forced: false, skipped: null };
|
|
83
|
+
if (pr?.status !== shared.PR_STATUS.MERGED) { result.skipped = 'not-merged'; return result; }
|
|
84
|
+
if (!root || !branch) { result.skipped = 'missing-branch'; return result; }
|
|
85
|
+
if (!isSafeLocalBranchName(branch)) { result.skipped = 'unsafe-branch-name'; return result; }
|
|
86
|
+
if (isProtectedLocalBranch(branch, project)) { result.skipped = 'protected-branch'; return result; }
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const current = String(shared.execSilent('git branch --show-current', {
|
|
90
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 10000, windowsHide: true,
|
|
91
|
+
}) || '').trim();
|
|
92
|
+
if (current === branch) { result.skipped = 'current-branch'; return result; }
|
|
93
|
+
} catch {
|
|
94
|
+
result.skipped = 'current-branch-unknown';
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (localBranchWorktreeInUse(root, branch)) { result.skipped = 'branch-in-worktree'; return result; }
|
|
99
|
+
|
|
100
|
+
let localHead = '';
|
|
101
|
+
try {
|
|
102
|
+
localHead = String(shared.execSilent(`git rev-parse --verify "refs/heads/${branch}"`, {
|
|
103
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 10000, windowsHide: true,
|
|
104
|
+
}) || '').trim();
|
|
105
|
+
} catch {
|
|
106
|
+
result.skipped = 'missing-local-branch';
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
shared.execSilent(`git branch -d -- "${branch}"`, {
|
|
112
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 15000, windowsHide: true,
|
|
113
|
+
});
|
|
114
|
+
log('info', `Post-merge cleanup: deleted local branch ${branch}`);
|
|
115
|
+
return { deleted: true, forced: false, skipped: null };
|
|
116
|
+
} catch (deleteErr) {
|
|
117
|
+
const localHeadLower = localHead.toLowerCase();
|
|
118
|
+
// Only use -D when the local tip still matches the merged PR head (or its remote-tracking ref);
|
|
119
|
+
// otherwise a reused local branch could lose unrelated work after the PR merged.
|
|
120
|
+
const proofHeads = [pr.headSha, pr._adoSourceCommit, pr.sourceCommit]
|
|
121
|
+
.map(v => String(v || '').trim().toLowerCase())
|
|
122
|
+
.filter(Boolean);
|
|
123
|
+
let safeToForce = proofHeads.includes(localHeadLower);
|
|
124
|
+
if (!safeToForce) {
|
|
125
|
+
try {
|
|
126
|
+
const remoteHead = String(shared.execSilent(`git rev-parse --verify "refs/remotes/origin/${branch}"`, {
|
|
127
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 10000, windowsHide: true,
|
|
128
|
+
}) || '').trim().toLowerCase();
|
|
129
|
+
safeToForce = !!remoteHead && remoteHead === localHeadLower;
|
|
130
|
+
} catch { /* no matching remote-tracking branch */ }
|
|
131
|
+
}
|
|
132
|
+
if (!safeToForce) {
|
|
133
|
+
result.skipped = 'unproven-force-delete';
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
shared.execSilent(`git branch -D -- "${branch}"`, {
|
|
138
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 15000, windowsHide: true,
|
|
139
|
+
});
|
|
140
|
+
log('info', `Post-merge cleanup: force-deleted local branch ${branch} after merged PR confirmation`);
|
|
141
|
+
return { deleted: true, forced: true, skipped: null };
|
|
142
|
+
} catch (forceErr) {
|
|
143
|
+
log('warn', `Post-merge cleanup: failed to delete local branch ${branch}: ${forceErr.message || deleteErr.message}`);
|
|
144
|
+
result.skipped = 'delete-failed';
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
51
150
|
/**
|
|
52
151
|
* Sweep leaked test-fixture meetings from a `meetings/` directory.
|
|
53
152
|
*
|
|
@@ -342,9 +441,11 @@ async function runCleanup(config, verbose = false) {
|
|
|
342
441
|
// Check if this worktree's branch is merged/abandoned
|
|
343
442
|
// Prefer actual git branch metadata; compact Windows dirs intentionally omit branch names.
|
|
344
443
|
const dirLower = dir.toLowerCase();
|
|
444
|
+
let matchedMergedBranch = '';
|
|
345
445
|
for (const branch of mergedBranches) {
|
|
346
446
|
if (worktreeMatchesBranch(dirLower, branch, actualBranch)) {
|
|
347
447
|
shouldClean = true;
|
|
448
|
+
matchedMergedBranch = branch;
|
|
348
449
|
break;
|
|
349
450
|
}
|
|
350
451
|
}
|
|
@@ -392,7 +493,7 @@ async function runCleanup(config, verbose = false) {
|
|
|
392
493
|
} catch (e) { log('warn', 'check shared-branch protection: ' + e.message); }
|
|
393
494
|
}
|
|
394
495
|
|
|
395
|
-
wtEntries.push({ dir, wtPath, mtime, shouldClean, isProtected, actualBranch });
|
|
496
|
+
wtEntries.push({ dir, wtPath, mtime, shouldClean, isProtected, actualBranch, matchedMergedBranch });
|
|
396
497
|
}
|
|
397
498
|
|
|
398
499
|
// Enforce max worktree cap — if over limit, mark oldest unprotected for cleanup
|
|
@@ -412,10 +513,14 @@ async function runCleanup(config, verbose = false) {
|
|
|
412
513
|
// the initial status check and the actual deletion (Bug #15: TOCTOU race)
|
|
413
514
|
const freshPrs = safeJson(projectPrPath(project)) || [];
|
|
414
515
|
const freshMergedBranches = new Set();
|
|
516
|
+
const freshMergedPrByBranch = new Map();
|
|
415
517
|
for (const pr of freshPrs) {
|
|
416
518
|
if (pr.status === shared.PR_STATUS.MERGED || pr.status === shared.PR_STATUS.ABANDONED || pr.status === shared.PLAN_STATUS.COMPLETED) {
|
|
417
519
|
if (pr.branch) freshMergedBranches.add(pr.branch);
|
|
418
520
|
}
|
|
521
|
+
if (pr.status === shared.PR_STATUS.MERGED && pr.branch) {
|
|
522
|
+
freshMergedPrByBranch.set(sanitizeBranch(normalizeLocalBranchName(pr.branch)).toLowerCase(), pr);
|
|
523
|
+
}
|
|
419
524
|
}
|
|
420
525
|
|
|
421
526
|
for (const entry of wtEntries) {
|
|
@@ -446,6 +551,10 @@ async function runCleanup(config, verbose = false) {
|
|
|
446
551
|
_killProcessInWorktree(entry.dir, activeProcesses, activeDispatchIds);
|
|
447
552
|
if (shared.removeWorktree(entry.wtPath, root, worktreeRoot)) {
|
|
448
553
|
cleaned.worktrees++;
|
|
554
|
+
const mergedPr = entry.matchedMergedBranch
|
|
555
|
+
? freshMergedPrByBranch.get(sanitizeBranch(normalizeLocalBranchName(entry.matchedMergedBranch)).toLowerCase())
|
|
556
|
+
: null;
|
|
557
|
+
if (mergedPr) cleanupMergedPrLocalBranch(root, project, mergedPr);
|
|
449
558
|
if (verbose) console.log(` Removed worktree: ${entry.wtPath}`);
|
|
450
559
|
} else {
|
|
451
560
|
if (verbose) console.log(` Failed to remove worktree ${entry.wtPath}`);
|
|
@@ -935,4 +1044,5 @@ module.exports = {
|
|
|
935
1044
|
worktreeDirMatchesBranch, // exported for testing
|
|
936
1045
|
worktreeMatchesBranch, // exported for testing
|
|
937
1046
|
getWorktreeBranch, // exported for lifecycle cleanup
|
|
1047
|
+
cleanupMergedPrLocalBranch, // exported for lifecycle cleanup and testing
|
|
938
1048
|
};
|
package/engine/lifecycle.js
CHANGED
|
@@ -7,14 +7,14 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const shared = require('./shared');
|
|
10
|
-
const { safeRead, safeJson, safeJsonNoRestore, safeWrite, mutateJsonFileLocked, mutateWorkItems,
|
|
10
|
+
const { safeRead, safeJson, safeJsonNoRestore, safeWrite, mutateJsonFileLocked, mutateWorkItems, execAsync, projectPrPath, getPrLinks,
|
|
11
11
|
log, ts, dateStamp, WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
12
12
|
ENGINE_DEFAULTS, DEFAULT_AGENT_METRICS, FAILURE_CLASS } = shared;
|
|
13
13
|
const { trackEngineUsage } = require('./llm');
|
|
14
14
|
const { resolveRuntime } = require('./runtimes');
|
|
15
15
|
const queries = require('./queries');
|
|
16
16
|
const { isBranchActive } = require('./cooldown');
|
|
17
|
-
const { worktreeMatchesBranch, getWorktreeBranch } = require('./cleanup');
|
|
17
|
+
const { worktreeMatchesBranch, getWorktreeBranch, cleanupMergedPrLocalBranch } = require('./cleanup');
|
|
18
18
|
const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
|
|
19
19
|
MINIONS_DIR, ENGINE_DIR, PLANS_DIR, PRD_DIR, INBOX_DIR, AGENTS_DIR } = queries;
|
|
20
20
|
|
|
@@ -1838,6 +1838,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
1838
1838
|
if (pr.branch && project) {
|
|
1839
1839
|
const root = path.resolve(project.localPath);
|
|
1840
1840
|
const wtRoot = path.resolve(root, config.engine?.worktreeRoot || '../worktrees');
|
|
1841
|
+
let removedBranchWorktree = false;
|
|
1841
1842
|
// Find worktrees matching this branch; compact Windows dirs require branch metadata.
|
|
1842
1843
|
try {
|
|
1843
1844
|
const dirs = require('fs').readdirSync(wtRoot);
|
|
@@ -1847,11 +1848,16 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
1847
1848
|
if (worktreeMatchesBranch(dirLower, pr.branch, getWorktreeBranch(wtPath)) || dir === pr.branch || dir === `bt-${prNum}`) {
|
|
1848
1849
|
try {
|
|
1849
1850
|
if (!require('fs').statSync(wtPath).isDirectory()) continue;
|
|
1850
|
-
|
|
1851
|
-
|
|
1851
|
+
if (shared.removeWorktree(wtPath, root, wtRoot)) {
|
|
1852
|
+
removedBranchWorktree = true;
|
|
1853
|
+
log('info', `Post-merge cleanup: removed worktree ${dir}`);
|
|
1854
|
+
}
|
|
1852
1855
|
} catch (err) { log('warn', `Failed to remove worktree ${dir}: ${err.message}`); }
|
|
1853
1856
|
}
|
|
1854
1857
|
}
|
|
1858
|
+
if (removedBranchWorktree && newStatus === PR_STATUS.MERGED) {
|
|
1859
|
+
cleanupMergedPrLocalBranch(root, project, pr);
|
|
1860
|
+
}
|
|
1855
1861
|
} catch (err) { log('warn', `Post-merge worktree cleanup: ${err.message}`); }
|
|
1856
1862
|
}
|
|
1857
1863
|
|
package/engine/shared.js
CHANGED
|
@@ -1029,9 +1029,8 @@ const ENGINE_DEFAULTS = {
|
|
|
1029
1029
|
mainBranchCacheMaxEntries: 100, // bound repo/branch detection cache in long-lived dashboard/engine processes
|
|
1030
1030
|
removeWorktreeFailureTtlMs: 24 * 60 * 60 * 1000, // stale failed paths are forgotten after a day
|
|
1031
1031
|
removeWorktreeFailureMaxEntries: 1000, // bound failed-worktree retry suppression cache
|
|
1032
|
-
ccMaxTurns: 50, // max tool-use turns
|
|
1033
|
-
|
|
1034
|
-
docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity
|
|
1032
|
+
ccMaxTurns: 50, // max tool-use turns per CC/doc-chat call before CLI stops (per response, not per session)
|
|
1033
|
+
docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity (LRU; sessions are non-expiring otherwise)
|
|
1035
1034
|
ccLiveStreamMaxAgeMs: 30 * 60 * 1000, // hard cap reconnect buffers if abort/cleanup stalls
|
|
1036
1035
|
metricsFlushIntervalMs: 10000, // batch trackEngineUsage writes to metrics.json — flushed every 10s instead of per-call to cut lock contention and dashboard mtime churn
|
|
1037
1036
|
maxLlmRawBytes: 256 * 1024, // keep only a bounded stdout tail from direct Claude calls
|
|
@@ -1244,7 +1243,7 @@ function _resetLegacyCcModelMigrationFlag() {
|
|
|
1244
1243
|
* Warnings emitted:
|
|
1245
1244
|
* - Unknown CLI: any `cli` value (per-agent, ccCli, defaultCli) not in
|
|
1246
1245
|
* `registeredRuntimes`. Each unknown value produces one entry.
|
|
1247
|
-
* - Deprecated `config.claude.*`
|
|
1246
|
+
* - Deprecated `config.claude.*` overrides: non-default fields listed in
|
|
1248
1247
|
* `ENGINE_DEFAULTS._deprecatedConfigClaudeFields` under `config.claude`.
|
|
1249
1248
|
* - Bare-mode misconfig: `engine.claudeBareMode === true` paired with
|
|
1250
1249
|
* CC running on the Claude runtime (resolved via `resolveCcCli`) and no
|
|
@@ -1283,11 +1282,15 @@ function runtimeConfigWarnings(config, registeredRuntimes) {
|
|
|
1283
1282
|
if (agent && typeof agent === 'object') checkCli(`agents.${agentId}.cli`, agent.cli);
|
|
1284
1283
|
}
|
|
1285
1284
|
|
|
1286
|
-
// 2. Deprecated `config.claude.*`
|
|
1285
|
+
// 2. Deprecated `config.claude.*` overrides. Generated defaults from older
|
|
1286
|
+
// init versions are ignored here and pruned the next time config is saved.
|
|
1287
1287
|
const claude = config.claude;
|
|
1288
1288
|
if (claude && typeof claude === 'object') {
|
|
1289
1289
|
const deprecatedKeys = ENGINE_DEFAULTS._deprecatedConfigClaudeFields || [];
|
|
1290
|
-
const present = deprecatedKeys.filter(k =>
|
|
1290
|
+
const present = deprecatedKeys.filter(k => {
|
|
1291
|
+
if (!Object.prototype.hasOwnProperty.call(claude, k)) return false;
|
|
1292
|
+
return !(Object.prototype.hasOwnProperty.call(DEFAULT_CLAUDE, k) && claude[k] === DEFAULT_CLAUDE[k]);
|
|
1293
|
+
});
|
|
1291
1294
|
if (present.length > 0) {
|
|
1292
1295
|
warnings.push({
|
|
1293
1296
|
id: 'deprecated-config-claude',
|
|
@@ -1573,6 +1576,37 @@ const DEFAULT_CLAUDE = {
|
|
|
1573
1576
|
allowedTools: 'Edit,Write,Read,Bash,Glob,Grep,Agent,WebFetch,WebSearch',
|
|
1574
1577
|
};
|
|
1575
1578
|
|
|
1579
|
+
function pruneDefaultClaudeConfig(config) {
|
|
1580
|
+
if (!config || typeof config !== 'object') return false;
|
|
1581
|
+
const claude = config.claude;
|
|
1582
|
+
if (claude === undefined || claude === null) return false;
|
|
1583
|
+
if (typeof claude !== 'object' || Array.isArray(claude)) {
|
|
1584
|
+
delete config.claude;
|
|
1585
|
+
return true;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
let changed = false;
|
|
1589
|
+
const removeKey = (key) => {
|
|
1590
|
+
if (Object.prototype.hasOwnProperty.call(claude, key)) {
|
|
1591
|
+
delete claude[key];
|
|
1592
|
+
changed = true;
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
|
|
1596
|
+
removeKey('permissionMode');
|
|
1597
|
+
for (const key of ['binary', 'outputFormat', 'allowedTools']) {
|
|
1598
|
+
if (Object.prototype.hasOwnProperty.call(claude, key) && claude[key] === DEFAULT_CLAUDE[key]) {
|
|
1599
|
+
removeKey(key);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (Object.keys(claude).length === 0) {
|
|
1604
|
+
delete config.claude;
|
|
1605
|
+
changed = true;
|
|
1606
|
+
}
|
|
1607
|
+
return changed;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1576
1610
|
// ── Project Helpers ──────────────────────────────────────────────────────────
|
|
1577
1611
|
|
|
1578
1612
|
function getProjects(config) {
|
|
@@ -3068,6 +3102,7 @@ module.exports = {
|
|
|
3068
3102
|
DEFAULT_AGENT_METRICS,
|
|
3069
3103
|
DEFAULT_AGENTS,
|
|
3070
3104
|
DEFAULT_CLAUDE,
|
|
3105
|
+
pruneDefaultClaudeConfig,
|
|
3071
3106
|
getProjects,
|
|
3072
3107
|
projectRoot,
|
|
3073
3108
|
projectStateDir,
|
package/minions.js
CHANGED
|
@@ -17,7 +17,7 @@ const path = require('path');
|
|
|
17
17
|
const readline = require('readline');
|
|
18
18
|
const { execSync } = require('child_process');
|
|
19
19
|
const shared = require('./engine/shared');
|
|
20
|
-
const { ENGINE_DEFAULTS, DEFAULT_AGENTS
|
|
20
|
+
const { ENGINE_DEFAULTS, DEFAULT_AGENTS } = shared;
|
|
21
21
|
const projectDiscovery = require('./engine/project-discovery');
|
|
22
22
|
|
|
23
23
|
const MINIONS_HOME = process.env.MINIONS_HOME ? path.resolve(process.env.MINIONS_HOME) : __dirname;
|
|
@@ -385,11 +385,7 @@ async function initMinions({ skipScan = false, scanRoot, scanDepth } = {}) {
|
|
|
385
385
|
if (k === 'defaultCli') continue;
|
|
386
386
|
if (config.engine[k] === undefined) config.engine[k] = v;
|
|
387
387
|
}
|
|
388
|
-
|
|
389
|
-
delete config.claude.permissionMode;
|
|
390
|
-
for (const [k, v] of Object.entries(DEFAULT_CLAUDE)) {
|
|
391
|
-
if (config.claude[k] === undefined) config.claude[k] = v;
|
|
392
|
-
}
|
|
388
|
+
shared.pruneDefaultClaudeConfig(config);
|
|
393
389
|
if (!config.agents || Object.keys(config.agents).length === 0) {
|
|
394
390
|
config.agents = { ...DEFAULT_AGENTS };
|
|
395
391
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1771",
|
|
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"
|