@yemi33/minions 0.1.144 → 0.1.145
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 +7 -1
- package/dashboard/js/render-other.js +24 -1
- package/dashboard/pages/engine.html +4 -0
- package/engine/lifecycle.js +56 -0
- package/engine/meeting.js +6 -2
- package/engine/playbook.js +32 -0
- package/engine.js +30 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.145 (2026-04-01)
|
|
4
4
|
|
|
5
5
|
### Engine
|
|
6
|
+
- engine.js
|
|
6
7
|
- engine/ado.js
|
|
7
8
|
- engine/cleanup.js
|
|
8
9
|
- engine/cooldown.js
|
|
9
10
|
- engine/github.js
|
|
11
|
+
- engine/lifecycle.js
|
|
10
12
|
- engine/meeting.js
|
|
11
13
|
- engine/playbook.js
|
|
12
14
|
- engine/routing.js
|
|
13
15
|
- engine/timeout.js
|
|
14
16
|
|
|
17
|
+
### Dashboard
|
|
18
|
+
- dashboard/js/render-other.js
|
|
19
|
+
- dashboard/pages/engine.html
|
|
20
|
+
|
|
15
21
|
### Other
|
|
16
22
|
- test/unit.test.js
|
|
17
23
|
|
|
@@ -44,6 +44,7 @@ function renderMetrics(metrics) {
|
|
|
44
44
|
if (agents.length === 0) {
|
|
45
45
|
el.innerHTML = '<p class="empty">No metrics yet. Metrics appear after agents complete tasks.</p>';
|
|
46
46
|
renderTokenUsage(metrics);
|
|
47
|
+
renderContextPressure(metrics);
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
let html = '<table class="pr-table"><thead><tr><th>Agent</th><th>Done</th><th>Errors</th><th>PRs</th><th>Approved</th><th>Rejected</th><th>Rate</th><th>Reviews</th></tr></thead><tbody>';
|
|
@@ -64,6 +65,7 @@ function renderMetrics(metrics) {
|
|
|
64
65
|
html += '</tbody></table>';
|
|
65
66
|
el.innerHTML = html;
|
|
66
67
|
renderTokenUsage(metrics);
|
|
68
|
+
renderContextPressure(metrics);
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
function renderTokenUsage(metrics) {
|
|
@@ -183,4 +185,25 @@ function renderTokenUsage(metrics) {
|
|
|
183
185
|
el.innerHTML = html;
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
|
|
188
|
+
function renderContextPressure(metrics) {
|
|
189
|
+
const el = document.getElementById('context-pressure-content');
|
|
190
|
+
if (!el) return;
|
|
191
|
+
const cp = metrics._contextPressure;
|
|
192
|
+
if (!cp || !cp.dispatches) {
|
|
193
|
+
el.innerHTML = '<p class="empty">No context pressure data yet. Data appears after agents complete tasks.</p>';
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const avgTurns = (cp.totalTurns / cp.dispatches).toFixed(1);
|
|
197
|
+
const turnLimitPct = ((cp.turnLimitHits / cp.dispatches) * 100).toFixed(1);
|
|
198
|
+
const pctColor = parseFloat(turnLimitPct) > 20 ? 'var(--red)' : parseFloat(turnLimitPct) > 5 ? 'var(--yellow, orange)' : 'var(--green)';
|
|
199
|
+
|
|
200
|
+
let html = '<div class="token-tiles">';
|
|
201
|
+
html += '<div class="token-tile"><div class="token-tile-label">Avg Turns</div><div class="token-tile-value">' + avgTurns + '</div><div class="token-tile-sub">per dispatch</div></div>';
|
|
202
|
+
html += '<div class="token-tile"><div class="token-tile-label">Max Turns</div><div class="token-tile-value">' + cp.maxTurns + '</div></div>';
|
|
203
|
+
html += '<div class="token-tile"><div class="token-tile-label">Hit Turn Limit</div><div class="token-tile-value" style="color:' + pctColor + '">' + turnLimitPct + '%</div><div class="token-tile-sub">' + cp.turnLimitHits + ' of ' + cp.dispatches + ' dispatches</div></div>';
|
|
204
|
+
html += '<div class="token-tile"><div class="token-tile-label">Total Dispatches</div><div class="token-tile-value">' + cp.dispatches + '</div></div>';
|
|
205
|
+
html += '</div>';
|
|
206
|
+
el.innerHTML = html;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
window.MinionsOther = { renderProjects, renderMcpServers, renderMetrics, renderTokenUsage, renderContextPressure };
|
|
@@ -10,3 +10,7 @@
|
|
|
10
10
|
<h2>Token Usage</h2>
|
|
11
11
|
<div id="token-usage-content"><p class="empty">No usage data yet.</p></div>
|
|
12
12
|
</section>
|
|
13
|
+
<section>
|
|
14
|
+
<h2>Context Pressure</h2>
|
|
15
|
+
<div id="context-pressure-content"><p class="empty">No context pressure data yet. Data appears after agents complete tasks.</p></div>
|
|
16
|
+
</section>
|
package/engine/lifecycle.js
CHANGED
|
@@ -932,6 +932,30 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
|
|
|
932
932
|
if (wrote) log('info', `Created review feedback for ${authorAgentId} from ${reviewerAgentId} on ${pr.id}`);
|
|
933
933
|
}
|
|
934
934
|
|
|
935
|
+
function recordContextPressureOnWorkItem(meta, turnCount, outputLogSizeBytes, hitTurnLimit) {
|
|
936
|
+
const itemId = meta.item?.id;
|
|
937
|
+
if (!itemId) return;
|
|
938
|
+
|
|
939
|
+
let wiPath;
|
|
940
|
+
if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
|
|
941
|
+
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
942
|
+
} else if (meta.source === 'work-item' && meta.project?.name) {
|
|
943
|
+
wiPath = path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json');
|
|
944
|
+
}
|
|
945
|
+
if (!wiPath) return;
|
|
946
|
+
|
|
947
|
+
shared.mutateJsonFileLocked(wiPath, (items) => {
|
|
948
|
+
if (!Array.isArray(items)) return items;
|
|
949
|
+
const target = items.find(i => i.id === itemId);
|
|
950
|
+
if (target) {
|
|
951
|
+
target._turnCount = turnCount;
|
|
952
|
+
target._outputLogSizeBytes = outputLogSizeBytes;
|
|
953
|
+
target._hitTurnLimit = hitTurnLimit;
|
|
954
|
+
}
|
|
955
|
+
return items;
|
|
956
|
+
}, { defaultValue: [] });
|
|
957
|
+
}
|
|
958
|
+
|
|
935
959
|
function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount, model) {
|
|
936
960
|
|
|
937
961
|
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
@@ -977,6 +1001,20 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
977
1001
|
for (const day of Object.keys(metrics._daily)) {
|
|
978
1002
|
if (day < cutoffStr) delete metrics._daily[day];
|
|
979
1003
|
}
|
|
1004
|
+
|
|
1005
|
+
// Update contextPressure aggregate
|
|
1006
|
+
if (taskUsage && taskUsage.numTurns > 0) {
|
|
1007
|
+
if (!metrics._contextPressure) metrics._contextPressure = { totalTurns: 0, dispatches: 0, maxTurns: 0, turnLimitHits: 0 };
|
|
1008
|
+
const cp = metrics._contextPressure;
|
|
1009
|
+
cp.totalTurns += taskUsage.numTurns;
|
|
1010
|
+
cp.dispatches++;
|
|
1011
|
+
if (taskUsage.numTurns > cp.maxTurns) cp.maxTurns = taskUsage.numTurns;
|
|
1012
|
+
// Check if this dispatch hit the turn limit
|
|
1013
|
+
const engineConfig = require('./queries').getConfig()?.engine || {};
|
|
1014
|
+
const turnLimit = engineConfig.maxTurns || 100;
|
|
1015
|
+
if (taskUsage.numTurns >= turnLimit) cp.turnLimitHits++;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
980
1018
|
shared.safeWrite(metricsPath, metrics);
|
|
981
1019
|
}
|
|
982
1020
|
|
|
@@ -1089,6 +1127,23 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1089
1127
|
// If decomposition produced nothing, fall through to mark parent as done
|
|
1090
1128
|
}
|
|
1091
1129
|
|
|
1130
|
+
// Record context utilization metrics on the work item
|
|
1131
|
+
if (meta?.item?.id) {
|
|
1132
|
+
try {
|
|
1133
|
+
const engineConfig = (config.engine || {});
|
|
1134
|
+
const turnLimit = engineConfig.maxTurns || 100;
|
|
1135
|
+
const turnCount = taskUsage?.numTurns || 0;
|
|
1136
|
+
const hitTurnLimit = turnCount >= turnLimit;
|
|
1137
|
+
let outputLogSizeBytes = 0;
|
|
1138
|
+
try {
|
|
1139
|
+
const liveLogPath = path.join(AGENTS_DIR, agentId, 'live-output.log');
|
|
1140
|
+
const stat = fs.statSync(liveLogPath);
|
|
1141
|
+
outputLogSizeBytes = stat.size;
|
|
1142
|
+
} catch { /* file may not exist */ }
|
|
1143
|
+
recordContextPressureOnWorkItem(meta, turnCount, outputLogSizeBytes, hitTurnLimit);
|
|
1144
|
+
} catch (err) { log('warn', `Context pressure record: ${err.message}`); }
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1092
1147
|
if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, 'done', '');
|
|
1093
1148
|
if (!isSuccess && meta?.item?.id) {
|
|
1094
1149
|
// Auto-retry: read fresh _retryCount from file (not stale dispatch-time snapshot)
|
|
@@ -1289,6 +1344,7 @@ module.exports = {
|
|
|
1289
1344
|
updateAgentHistory,
|
|
1290
1345
|
createReviewFeedbackForAuthor,
|
|
1291
1346
|
updateMetrics,
|
|
1347
|
+
recordContextPressureOnWorkItem,
|
|
1292
1348
|
parseAgentOutput,
|
|
1293
1349
|
runPostCompletionHooks,
|
|
1294
1350
|
syncPrdFromPrs,
|
package/engine/meeting.js
CHANGED
|
@@ -78,8 +78,12 @@ function discoverMeetingWork(config) {
|
|
|
78
78
|
const agents = config.agents || {};
|
|
79
79
|
|
|
80
80
|
if (roundName === 'concluding') {
|
|
81
|
-
//
|
|
82
|
-
const
|
|
81
|
+
// Pick the first non-busy participant as concluder (fallback to any participant)
|
|
82
|
+
const busyAgents = new Set(
|
|
83
|
+
(dispatch.active || []).map(d => d.agent).filter(Boolean)
|
|
84
|
+
);
|
|
85
|
+
const concluder = meeting.participants.find(p => !busyAgents.has(p))
|
|
86
|
+
|| meeting.participants[0];
|
|
83
87
|
if (!concluder) continue;
|
|
84
88
|
const key = `meeting-${meeting.id}-r${round}-${concluder}`;
|
|
85
89
|
if (activeKeys.has(key)) continue;
|
package/engine/playbook.js
CHANGED
|
@@ -206,9 +206,25 @@ function resolveTaskContext(item, config) {
|
|
|
206
206
|
return resolved;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
// ─── Critical Variable Definitions ─────────────────────────────────────────
|
|
210
|
+
// Variables that MUST resolve to non-empty values for dispatch to proceed.
|
|
211
|
+
// If any critical variable is empty or unresolved, renderPlaybook returns null.
|
|
212
|
+
const CRITICAL_VARS = {
|
|
213
|
+
'implement': ['task_description', 'branch_name'],
|
|
214
|
+
'implement-shared': ['task_description', 'branch_name'],
|
|
215
|
+
'fix': ['task_description', 'branch_name'],
|
|
216
|
+
'work-item': ['task_description'],
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Module-level error state — callers check via getLastRenderError() after null return
|
|
220
|
+
let _lastRenderError = null;
|
|
221
|
+
|
|
222
|
+
function getLastRenderError() { return _lastRenderError; }
|
|
223
|
+
|
|
209
224
|
// ─── Playbook Renderer ──────────────────────────────────────────────────────
|
|
210
225
|
|
|
211
226
|
function renderPlaybook(type, vars) {
|
|
227
|
+
_lastRenderError = null;
|
|
212
228
|
const pbPath = path.join(PLAYBOOKS_DIR, `${type}.md`);
|
|
213
229
|
let content;
|
|
214
230
|
try { content = fs.readFileSync(pbPath, 'utf8'); } catch {
|
|
@@ -299,6 +315,20 @@ function renderPlaybook(type, vars) {
|
|
|
299
315
|
log('warn', `Playbook "${type}": unresolved template variables: ${unresolved.join(', ')}`);
|
|
300
316
|
}
|
|
301
317
|
|
|
318
|
+
// Block dispatch if critical variables are empty or unresolved
|
|
319
|
+
const criticalVars = CRITICAL_VARS[type] || [];
|
|
320
|
+
if (criticalVars.length > 0) {
|
|
321
|
+
const emptySet = new Set(emptyVars);
|
|
322
|
+
const unresolvedSet = new Set(unresolved);
|
|
323
|
+
const criticalMissing = criticalVars.filter(v => emptySet.has(v) || unresolvedSet.has(v));
|
|
324
|
+
if (criticalMissing.length > 0) {
|
|
325
|
+
const msg = `Playbook "${type}": critical variables empty or unresolved: ${criticalMissing.join(', ')} — blocking dispatch`;
|
|
326
|
+
log('warn', msg);
|
|
327
|
+
_lastRenderError = { reason: 'critical_vars_missing', vars: criticalMissing, message: msg };
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
302
332
|
return content;
|
|
303
333
|
}
|
|
304
334
|
|
|
@@ -470,6 +500,8 @@ function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabe
|
|
|
470
500
|
|
|
471
501
|
module.exports = {
|
|
472
502
|
renderPlaybook,
|
|
503
|
+
getLastRenderError,
|
|
504
|
+
CRITICAL_VARS,
|
|
473
505
|
buildSystemPrompt,
|
|
474
506
|
buildAgentContext,
|
|
475
507
|
selectPlaybook,
|
package/engine.js
CHANGED
|
@@ -120,7 +120,7 @@ const { getRouting, parseRoutingTable, getRoutingTableCached, getMonthlySpend,
|
|
|
120
120
|
|
|
121
121
|
// ─── Playbook, system prompt, agent context (extracted to engine/playbook.js) ─
|
|
122
122
|
|
|
123
|
-
const { renderPlaybook, buildSystemPrompt, buildAgentContext, selectPlaybook,
|
|
123
|
+
const { renderPlaybook, getLastRenderError, buildSystemPrompt, buildAgentContext, selectPlaybook,
|
|
124
124
|
buildBaseVars, buildPrDispatch, resolveTaskContext,
|
|
125
125
|
getRepoHostLabel, getRepoHostToolRule } = require('./engine/playbook');
|
|
126
126
|
|
|
@@ -1493,9 +1493,17 @@ function discoverFromWorkItems(config, project) {
|
|
|
1493
1493
|
if (playbookName === 'work-item' && workType === 'review') {
|
|
1494
1494
|
log('info', `Work item ${item.id} is type "review" but has no PR — using work-item playbook`);
|
|
1495
1495
|
}
|
|
1496
|
-
const
|
|
1496
|
+
const rendered = renderPlaybook(playbookName, vars);
|
|
1497
|
+
const renderError = getLastRenderError();
|
|
1498
|
+
// If critical vars are missing, block dispatch entirely — don't fall through to work-item playbook
|
|
1499
|
+
const prompt = item.prompt || rendered || (renderError ? null : (renderPlaybook('work-item', vars) || item.description));
|
|
1497
1500
|
if (!prompt) {
|
|
1498
|
-
|
|
1501
|
+
if (renderError) {
|
|
1502
|
+
log('warn', `Skipping ${item.id}: ${renderError.message}`);
|
|
1503
|
+
if (item._pendingReason !== 'critical_vars_missing') { item._pendingReason = 'critical_vars_missing'; needsWrite = true; }
|
|
1504
|
+
} else {
|
|
1505
|
+
log('warn', `No playbook rendered for ${item.id} (type: ${workType}, playbook: ${playbookName}) — skipping`);
|
|
1506
|
+
}
|
|
1499
1507
|
continue;
|
|
1500
1508
|
}
|
|
1501
1509
|
|
|
@@ -1782,9 +1790,16 @@ function discoverCentralWorkItems(config) {
|
|
|
1782
1790
|
}
|
|
1783
1791
|
|
|
1784
1792
|
const playbookName = selectPlaybook(workType, item);
|
|
1785
|
-
const
|
|
1793
|
+
const rendered = renderPlaybook(playbookName, vars);
|
|
1794
|
+
const renderError = getLastRenderError();
|
|
1795
|
+
const prompt = rendered || (renderError ? null : renderPlaybook('work-item', vars));
|
|
1786
1796
|
if (!prompt) {
|
|
1787
|
-
|
|
1797
|
+
if (renderError) {
|
|
1798
|
+
log('warn', `Fan-out: ${item.id} → ${agent.id}: ${renderError.message}`);
|
|
1799
|
+
if (item._pendingReason !== 'critical_vars_missing') { item._pendingReason = 'critical_vars_missing'; }
|
|
1800
|
+
} else {
|
|
1801
|
+
log('warn', `Fan-out: playbook '${playbookName}' failed to render for ${item.id} → ${agent.id}, skipping`);
|
|
1802
|
+
}
|
|
1788
1803
|
continue;
|
|
1789
1804
|
}
|
|
1790
1805
|
|
|
@@ -1894,9 +1909,16 @@ function discoverCentralWorkItems(config) {
|
|
|
1894
1909
|
}
|
|
1895
1910
|
|
|
1896
1911
|
const playbookName = selectPlaybook(workType, item);
|
|
1897
|
-
const
|
|
1912
|
+
const rendered = renderPlaybook(playbookName, vars);
|
|
1913
|
+
const renderError = getLastRenderError();
|
|
1914
|
+
const prompt = rendered || (renderError ? null : renderPlaybook('work-item', vars));
|
|
1898
1915
|
if (!prompt) {
|
|
1899
|
-
|
|
1916
|
+
if (renderError) {
|
|
1917
|
+
log('warn', `Dispatch: ${item.id}: ${renderError.message}`);
|
|
1918
|
+
item._pendingReason = 'critical_vars_missing';
|
|
1919
|
+
} else {
|
|
1920
|
+
log('warn', `Dispatch: playbook '${playbookName}' failed to render for ${item.id}, resetting to pending`);
|
|
1921
|
+
}
|
|
1900
1922
|
item.status = 'pending';
|
|
1901
1923
|
continue;
|
|
1902
1924
|
}
|
|
@@ -2378,6 +2400,7 @@ module.exports = {
|
|
|
2378
2400
|
|
|
2379
2401
|
// Playbooks
|
|
2380
2402
|
renderPlaybook,
|
|
2403
|
+
getLastRenderError,
|
|
2381
2404
|
|
|
2382
2405
|
// Timeout / Steering / Idle (re-exported from engine/timeout.js)
|
|
2383
2406
|
checkTimeouts, checkSteering, checkIdleThreshold,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.145",
|
|
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"
|