@yemi33/minions 0.1.58 → 0.1.60
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 +39 -0
- package/dashboard/js/command-center.js +1 -1
- package/dashboard/js/live-stream.js +3 -3
- package/dashboard/js/modal-qa.js +4 -4
- package/dashboard/js/refresh.js +2 -2
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-kb.js +1 -1
- package/dashboard/js/render-plans.js +3 -3
- package/dashboard/js/render-prs.js +2 -2
- package/dashboard/js/render-work-items.js +2 -2
- package/dashboard/js/settings.js +1 -1
- package/dashboard.js +46 -46
- package/engine/ado.js +2 -2
- package/engine/cli.js +16 -16
- package/engine/consolidation.js +4 -4
- package/engine/cooldown.js +117 -0
- package/engine/github.js +3 -3
- package/engine/lifecycle.js +14 -14
- package/engine/llm.js +2 -2
- package/engine/playbook.js +479 -0
- package/engine/preflight.js +2 -2
- package/engine/queries.js +13 -13
- package/engine/routing.js +163 -0
- package/engine/shared.js +6 -6
- package/engine/spawn-agent.js +3 -3
- package/engine.js +106 -733
- package/package.json +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/routing.js — Agent routing, budget checks, and routing table parsing.
|
|
3
|
+
* Extracted from engine.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const shared = require('./shared');
|
|
9
|
+
const queries = require('./queries');
|
|
10
|
+
|
|
11
|
+
const { safeJson, safeRead } = shared;
|
|
12
|
+
const { ENGINE_DIR, DISPATCH_PATH } = queries;
|
|
13
|
+
|
|
14
|
+
const MINIONS_DIR = path.resolve(__dirname, '..');
|
|
15
|
+
const ROUTING_PATH = path.join(MINIONS_DIR, 'routing.md');
|
|
16
|
+
|
|
17
|
+
// Lazy require to avoid circular dependency with engine.js
|
|
18
|
+
let _engine = null;
|
|
19
|
+
function engine() { if (!_engine) _engine = require('../engine'); return _engine; }
|
|
20
|
+
|
|
21
|
+
// ─── Temp Agents ─────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const tempAgents = new Map(); // tempAgentId → { name, role, createdAt }
|
|
24
|
+
|
|
25
|
+
// ─── Routing Parser ─────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function getRouting() {
|
|
28
|
+
return safeRead(ROUTING_PATH);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let _routingCache = null;
|
|
32
|
+
let _routingCacheMtime = 0;
|
|
33
|
+
|
|
34
|
+
function parseRoutingTable() {
|
|
35
|
+
const content = getRouting();
|
|
36
|
+
const routes = {};
|
|
37
|
+
const lines = content.split('\n');
|
|
38
|
+
let inTable = false;
|
|
39
|
+
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
if (line.startsWith('| Work Type')) { inTable = true; continue; }
|
|
42
|
+
if (line.startsWith('|---')) continue;
|
|
43
|
+
if (!inTable || !line.startsWith('|')) {
|
|
44
|
+
if (inTable && !line.startsWith('|')) inTable = false;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
48
|
+
if (cells.length >= 3) {
|
|
49
|
+
routes[cells[0].toLowerCase()] = {
|
|
50
|
+
preferred: cells[1].toLowerCase(),
|
|
51
|
+
fallback: cells[2].toLowerCase()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return routes;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getRoutingTableCached() {
|
|
59
|
+
let mtime = 0;
|
|
60
|
+
try { mtime = fs.statSync(ROUTING_PATH).mtimeMs; } catch { /* optional */ }
|
|
61
|
+
if (_routingCache && _routingCacheMtime === mtime) return _routingCache;
|
|
62
|
+
_routingCache = parseRoutingTable();
|
|
63
|
+
_routingCacheMtime = mtime;
|
|
64
|
+
return _routingCache;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Budget ──────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function getMonthlySpend(agentId) {
|
|
70
|
+
const metrics = safeJson(path.join(ENGINE_DIR, 'metrics.json')) || {};
|
|
71
|
+
const daily = metrics._daily || {};
|
|
72
|
+
const now = new Date();
|
|
73
|
+
const monthPrefix = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
74
|
+
let total = 0;
|
|
75
|
+
for (const [date, data] of Object.entries(daily)) {
|
|
76
|
+
if (date.startsWith(monthPrefix)) {
|
|
77
|
+
total += (data.perAgent?.[agentId]?.costUsd || 0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Fallback: if no per-agent daily data, use cumulative (less accurate for monthly)
|
|
81
|
+
if (total === 0 && metrics[agentId]?.totalCostUsd) {
|
|
82
|
+
// Can't distinguish monthly from cumulative — treat as monthly estimate
|
|
83
|
+
// This path is for backward compat before per-agent daily tracking was added
|
|
84
|
+
}
|
|
85
|
+
return total;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getAgentErrorRate(agentId) {
|
|
89
|
+
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
90
|
+
const metrics = safeJson(metricsPath) || {};
|
|
91
|
+
const m = metrics[agentId];
|
|
92
|
+
if (!m) return 0;
|
|
93
|
+
const total = m.tasksCompleted + m.tasksErrored;
|
|
94
|
+
return total > 0 ? m.tasksErrored / total : 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isAgentIdle(agentId) {
|
|
98
|
+
// Dispatch queue is the single source of truth for agent availability
|
|
99
|
+
const dispatch = safeJson(DISPATCH_PATH) || {};
|
|
100
|
+
return !(dispatch.active || []).some(d => d.agent === agentId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Agent Resolution ────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
// Track agents claimed during a single discovery pass to distribute work
|
|
106
|
+
const _claimedAgents = new Set();
|
|
107
|
+
function resetClaimedAgents() { _claimedAgents.clear(); }
|
|
108
|
+
|
|
109
|
+
function resolveAgent(workType, config, authorAgent = null) {
|
|
110
|
+
const routes = getRoutingTableCached();
|
|
111
|
+
const route = routes[workType] || routes['implement'];
|
|
112
|
+
const agents = config.agents || {};
|
|
113
|
+
|
|
114
|
+
// Resolve _author_ token
|
|
115
|
+
let preferred = route.preferred === '_author_' ? authorAgent : route.preferred;
|
|
116
|
+
let fallback = route.fallback === '_author_' ? authorAgent : route.fallback;
|
|
117
|
+
|
|
118
|
+
const isAvailable = (id) => {
|
|
119
|
+
if (!agents[id] || !isAgentIdle(id) || _claimedAgents.has(id)) return false;
|
|
120
|
+
// Budget check — no budget means infinite (no limit)
|
|
121
|
+
const budget = agents[id].monthlyBudgetUsd;
|
|
122
|
+
if (budget && budget > 0) {
|
|
123
|
+
if (getMonthlySpend(id) >= budget) return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Check preferred and fallback first (routing table order)
|
|
129
|
+
if (preferred && isAvailable(preferred)) { _claimedAgents.add(preferred); return preferred; }
|
|
130
|
+
if (fallback && isAvailable(fallback)) { _claimedAgents.add(fallback); return fallback; }
|
|
131
|
+
|
|
132
|
+
// Fall back to any idle agent, preferring lower error rates
|
|
133
|
+
const idle = Object.keys(agents)
|
|
134
|
+
.filter(id => id !== preferred && id !== fallback && isAvailable(id))
|
|
135
|
+
.sort((a, b) => getAgentErrorRate(a) - getAgentErrorRate(b));
|
|
136
|
+
|
|
137
|
+
if (idle[0]) { _claimedAgents.add(idle[0]); return idle[0]; }
|
|
138
|
+
|
|
139
|
+
// No idle configured agent — try temp agent if enabled
|
|
140
|
+
if (config.engine?.allowTempAgents) {
|
|
141
|
+
const tempId = `temp-${shared.uid()}`;
|
|
142
|
+
_claimedAgents.add(tempId);
|
|
143
|
+
tempAgents.set(tempId, { name: `Temp-${tempId.slice(5, 9)}`, role: 'Temporary Agent', createdAt: engine().ts() });
|
|
144
|
+
engine().log('info', `Spawning temp agent ${tempId} — all permanent agents busy`);
|
|
145
|
+
return tempId;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// No idle agent available — return null, item stays pending until next tick
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
tempAgents,
|
|
154
|
+
getRouting,
|
|
155
|
+
parseRoutingTable,
|
|
156
|
+
getRoutingTableCached,
|
|
157
|
+
getMonthlySpend,
|
|
158
|
+
getAgentErrorRate,
|
|
159
|
+
isAgentIdle,
|
|
160
|
+
_claimedAgents,
|
|
161
|
+
resetClaimedAgents,
|
|
162
|
+
resolveAgent,
|
|
163
|
+
};
|
package/engine/shared.js
CHANGED
|
@@ -38,24 +38,24 @@ function safeWrite(p, data) {
|
|
|
38
38
|
} catch (e) {
|
|
39
39
|
if (e.code === 'EPERM' && attempt < 4) {
|
|
40
40
|
const delay = 50 * (attempt + 1); // 50, 100, 150, 200ms
|
|
41
|
-
try { const ab = new SharedArrayBuffer(4); Atomics.wait(new Int32Array(ab), 0, 0, delay); } catch { const start = Date.now(); while (Date.now() - start < delay) {} }
|
|
41
|
+
try { const ab = new SharedArrayBuffer(4); Atomics.wait(new Int32Array(ab), 0, 0, delay); } catch { /* fallback busy-wait */ const start = Date.now(); while (Date.now() - start < delay) {} }
|
|
42
42
|
continue;
|
|
43
43
|
}
|
|
44
44
|
// Final attempt failed — fall through to direct write
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
// All rename attempts failed — direct write as fallback (not atomic but won't lose data)
|
|
48
|
-
try { fs.unlinkSync(tmp); } catch {}
|
|
48
|
+
try { fs.unlinkSync(tmp); } catch { /* cleanup */ }
|
|
49
49
|
fs.writeFileSync(p, content);
|
|
50
50
|
} catch (err) {
|
|
51
51
|
// Even direct write failed — log and clean up tmp
|
|
52
52
|
console.error(`[safeWrite] FAILED to write ${p}: ${err.message}`);
|
|
53
|
-
try { fs.unlinkSync(tmp); } catch {}
|
|
53
|
+
try { fs.unlinkSync(tmp); } catch { /* cleanup */ }
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function safeUnlink(p) {
|
|
58
|
-
try { fs.unlinkSync(p); } catch {}
|
|
58
|
+
try { fs.unlinkSync(p); } catch { /* cleanup */ }
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function sleepMs(ms) {
|
|
@@ -90,8 +90,8 @@ function withFileLock(lockPath, fn, {
|
|
|
90
90
|
try {
|
|
91
91
|
return fn();
|
|
92
92
|
} finally {
|
|
93
|
-
try { fs.closeSync(fd); } catch {}
|
|
94
|
-
try { fs.unlinkSync(lockPath); } catch {}
|
|
93
|
+
try { fs.closeSync(fd); } catch { /* cleanup */ }
|
|
94
|
+
try { fs.unlinkSync(lockPath); } catch { /* cleanup */ }
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
package/engine/spawn-agent.js
CHANGED
|
@@ -45,7 +45,7 @@ if (!claudeBin) {
|
|
|
45
45
|
const basedir = path.dirname(which.replace(/^\/c\//, 'C:/').replace(/\//g, path.sep));
|
|
46
46
|
claudeBin = path.join(basedir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
|
|
47
47
|
}
|
|
48
|
-
} catch {}
|
|
48
|
+
} catch { /* optional */ }
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Debug log
|
|
@@ -86,7 +86,7 @@ if (_sysPromptFileSupported === null) {
|
|
|
86
86
|
const { spawnSync } = require('child_process');
|
|
87
87
|
const testResult = spawnSync(process.execPath, [claudeBin, '--help'], { encoding: 'utf8', timeout: 10000, windowsHide: true });
|
|
88
88
|
_sysPromptFileSupported = (testResult.stdout || '').includes('system-prompt-file');
|
|
89
|
-
try { fs.writeFileSync(capsCachePath, JSON.stringify({ claudeBin, sysPromptFile: _sysPromptFileSupported, checkedAt: new Date().toISOString() })); } catch {}
|
|
89
|
+
try { fs.writeFileSync(capsCachePath, JSON.stringify({ claudeBin, sysPromptFile: _sysPromptFileSupported, checkedAt: new Date().toISOString() })); } catch { /* optional */ }
|
|
90
90
|
} catch { _sysPromptFileSupported = true; /* assume supported */ }
|
|
91
91
|
}
|
|
92
92
|
if (!isResume) try {
|
|
@@ -131,7 +131,7 @@ if (!isResume && Buffer.byteLength(sysPrompt) >= 30000) {
|
|
|
131
131
|
proc.stdin.end();
|
|
132
132
|
|
|
133
133
|
// Clean up temp file (only created for non-resume sessions)
|
|
134
|
-
if (!isResume) setTimeout(() => { try { fs.unlinkSync(sysTmpPath); } catch {} }, 5000);
|
|
134
|
+
if (!isResume) setTimeout(() => { try { fs.unlinkSync(sysTmpPath); } catch { /* cleanup */ } }, 5000);
|
|
135
135
|
|
|
136
136
|
// Capture stderr separately for debugging
|
|
137
137
|
let stderrBuf = '';
|