@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 CHANGED
@@ -1,17 +1,23 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.144 (2026-04-01)
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
- window.MinionsOther = { renderProjects, renderMcpServers, renderMetrics, renderTokenUsage };
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>
@@ -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
- // Only one agent concludes (first participant)
82
- const concluder = meeting.participants[0];
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;
@@ -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 prompt = item.prompt || renderPlaybook(playbookName, vars) || renderPlaybook('work-item', vars) || item.description;
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
- log('warn', `No playbook rendered for ${item.id} (type: ${workType}, playbook: ${playbookName}) — skipping`);
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 prompt = renderPlaybook(playbookName, vars) || renderPlaybook('work-item', vars);
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
- log('warn', `Fan-out: playbook '${playbookName}' failed to render for ${item.id} → ${agent.id}, skipping`);
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 prompt = renderPlaybook(playbookName, vars) || renderPlaybook('work-item', vars);
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
- log('warn', `Dispatch: playbook '${playbookName}' failed to render for ${item.id}, resetting to pending`);
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.144",
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"