@yemi33/minions 0.1.143 → 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 +21 -0
- package/dashboard/js/render-other.js +24 -1
- package/dashboard/pages/engine.html +4 -0
- package/engine/ado.js +20 -23
- package/engine/cleanup.js +2 -3
- package/engine/cooldown.js +3 -7
- package/engine/github.js +16 -21
- package/engine/lifecycle.js +56 -0
- package/engine/meeting.js +11 -10
- package/engine/playbook.js +34 -4
- package/engine/routing.js +3 -7
- package/engine/timeout.js +2 -3
- package/engine.js +30 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.145 (2026-04-01)
|
|
4
|
+
|
|
5
|
+
### Engine
|
|
6
|
+
- engine.js
|
|
7
|
+
- engine/ado.js
|
|
8
|
+
- engine/cleanup.js
|
|
9
|
+
- engine/cooldown.js
|
|
10
|
+
- engine/github.js
|
|
11
|
+
- engine/lifecycle.js
|
|
12
|
+
- engine/meeting.js
|
|
13
|
+
- engine/playbook.js
|
|
14
|
+
- engine/routing.js
|
|
15
|
+
- engine/timeout.js
|
|
16
|
+
|
|
17
|
+
### Dashboard
|
|
18
|
+
- dashboard/js/render-other.js
|
|
19
|
+
- dashboard/pages/engine.html
|
|
20
|
+
|
|
21
|
+
### Other
|
|
22
|
+
- test/unit.test.js
|
|
23
|
+
|
|
3
24
|
## 0.1.143 (2026-04-01)
|
|
4
25
|
|
|
5
26
|
### Engine
|
|
@@ -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/ado.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const shared = require('./shared');
|
|
8
|
-
const { exec, getAdoOrgBase, addPrLink } = shared;
|
|
8
|
+
const { exec, getAdoOrgBase, addPrLink, log, dateStamp } = shared;
|
|
9
9
|
const { getPrs } = require('./queries');
|
|
10
10
|
|
|
11
|
-
// Lazy require to avoid circular dependency
|
|
11
|
+
// Lazy require to avoid circular dependency — only needed for engine().handlePostMerge
|
|
12
12
|
let _engine = null;
|
|
13
13
|
function engine() {
|
|
14
14
|
if (!_engine) _engine = require('../engine');
|
|
@@ -37,7 +37,7 @@ function getAdoToken() {
|
|
|
37
37
|
return token;
|
|
38
38
|
}
|
|
39
39
|
} catch (e) {
|
|
40
|
-
|
|
40
|
+
log('warn', `Failed to get ADO token: ${e.message}`);
|
|
41
41
|
}
|
|
42
42
|
// Back off for 10 minutes to avoid spamming browser auth popups
|
|
43
43
|
_adoTokenFailedUntil = Date.now() + 10 * 60 * 1000;
|
|
@@ -57,7 +57,7 @@ async function adoFetch(url, token, _retryCount = 0) {
|
|
|
57
57
|
if (_retryCount < MAX_RETRIES) {
|
|
58
58
|
const freshToken = getAdoToken();
|
|
59
59
|
if (freshToken) {
|
|
60
|
-
|
|
60
|
+
log('info', 'ADO token expired mid-session — refreshed and retrying');
|
|
61
61
|
return adoFetch(url, freshToken, _retryCount + 1);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -94,7 +94,7 @@ async function forEachActivePr(config, token, callback) {
|
|
|
94
94
|
const updated = await callback(project, pr, prNum, orgBase);
|
|
95
95
|
if (updated) projectUpdated++;
|
|
96
96
|
} catch (err) {
|
|
97
|
-
|
|
97
|
+
log('warn', `Failed to poll status for ${pr.id}: ${err.message}`);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -110,10 +110,9 @@ async function forEachActivePr(config, token, callback) {
|
|
|
110
110
|
// ─── PR Status Polling ───────────────────────────────────────────────────────
|
|
111
111
|
|
|
112
112
|
async function pollPrStatus(config) {
|
|
113
|
-
const e = engine();
|
|
114
113
|
const token = getAdoToken();
|
|
115
114
|
if (!token) {
|
|
116
|
-
|
|
115
|
+
log('warn', 'Skipping PR status poll — no ADO token available');
|
|
117
116
|
return;
|
|
118
117
|
}
|
|
119
118
|
|
|
@@ -129,14 +128,14 @@ async function pollPrStatus(config) {
|
|
|
129
128
|
else if (prData.status === 'active') newStatus = 'active';
|
|
130
129
|
|
|
131
130
|
if (pr.status !== newStatus) {
|
|
132
|
-
|
|
131
|
+
log('info', `PR ${pr.id} status: ${pr.status} → ${newStatus}`);
|
|
133
132
|
pr.status = newStatus;
|
|
134
133
|
updated = true;
|
|
135
134
|
|
|
136
135
|
if (newStatus === 'merged' || newStatus === 'abandoned') {
|
|
137
136
|
if (pr.reviewStatus === 'waiting') {
|
|
138
137
|
pr.reviewStatus = newStatus === 'merged' ? 'approved' : 'pending';
|
|
139
|
-
|
|
138
|
+
log('info', `PR ${pr.id} reviewStatus: waiting → ${pr.reviewStatus} (${newStatus})`);
|
|
140
139
|
}
|
|
141
140
|
await engine().handlePostMerge(pr, project, config, newStatus);
|
|
142
141
|
}
|
|
@@ -153,7 +152,7 @@ async function pollPrStatus(config) {
|
|
|
153
152
|
}
|
|
154
153
|
|
|
155
154
|
if (pr.reviewStatus !== newReviewStatus) {
|
|
156
|
-
|
|
155
|
+
log('info', `PR ${pr.id} reviewStatus: ${pr.reviewStatus} → ${newReviewStatus}`);
|
|
157
156
|
pr.reviewStatus = newReviewStatus;
|
|
158
157
|
updated = true;
|
|
159
158
|
// Update author metrics when verdict changes to approved/rejected
|
|
@@ -167,7 +166,7 @@ async function pollPrStatus(config) {
|
|
|
167
166
|
if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
|
|
168
167
|
else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
|
|
169
168
|
shared.safeWrite(metricsPath, metrics);
|
|
170
|
-
} catch (err) {
|
|
169
|
+
} catch (err) { log('warn', `Metrics update: ${err.message}`); }
|
|
171
170
|
}
|
|
172
171
|
}
|
|
173
172
|
}
|
|
@@ -209,7 +208,7 @@ async function pollPrStatus(config) {
|
|
|
209
208
|
}
|
|
210
209
|
|
|
211
210
|
if (pr.buildStatus !== buildStatus) {
|
|
212
|
-
|
|
211
|
+
log('info', `PR ${pr.id} build: ${pr.buildStatus || 'none'} → ${buildStatus}${buildFailReason ? ' (' + buildFailReason + ')' : ''}`);
|
|
213
212
|
pr.buildStatus = buildStatus;
|
|
214
213
|
if (buildFailReason) pr.buildFailReason = buildFailReason;
|
|
215
214
|
else delete pr.buildFailReason;
|
|
@@ -221,14 +220,13 @@ async function pollPrStatus(config) {
|
|
|
221
220
|
});
|
|
222
221
|
|
|
223
222
|
if (totalUpdated > 0) {
|
|
224
|
-
|
|
223
|
+
log('info', `PR status poll: updated ${totalUpdated} PR(s)`);
|
|
225
224
|
}
|
|
226
225
|
}
|
|
227
226
|
|
|
228
227
|
// ─── Poll Human Comments on PRs ──────────────────────────────────────────────
|
|
229
228
|
|
|
230
229
|
async function pollPrHumanComments(config) {
|
|
231
|
-
const e = engine();
|
|
232
230
|
const token = getAdoToken();
|
|
233
231
|
if (!token) return;
|
|
234
232
|
|
|
@@ -285,12 +283,12 @@ async function pollPrHumanComments(config) {
|
|
|
285
283
|
feedbackContent
|
|
286
284
|
};
|
|
287
285
|
|
|
288
|
-
|
|
286
|
+
log('info', `PR ${pr.id}: ${newHumanComments.length} new comment(s), ${allHumanComments.length} total — full thread context provided`);
|
|
289
287
|
return true;
|
|
290
288
|
});
|
|
291
289
|
|
|
292
290
|
if (totalUpdated > 0) {
|
|
293
|
-
|
|
291
|
+
log('info', `PR comment poll: found human feedback on ${totalUpdated} PR(s)`);
|
|
294
292
|
}
|
|
295
293
|
}
|
|
296
294
|
|
|
@@ -301,10 +299,9 @@ async function pollPrHumanComments(config) {
|
|
|
301
299
|
* in pull-requests.json, and add them. Matches PRs to work items by branch name.
|
|
302
300
|
*/
|
|
303
301
|
async function reconcilePrs(config) {
|
|
304
|
-
const e = engine();
|
|
305
302
|
const token = getAdoToken();
|
|
306
303
|
if (!token) {
|
|
307
|
-
|
|
304
|
+
log('warn', 'Skipping PR reconciliation — no ADO token available');
|
|
308
305
|
return;
|
|
309
306
|
}
|
|
310
307
|
|
|
@@ -322,7 +319,7 @@ async function reconcilePrs(config) {
|
|
|
322
319
|
try {
|
|
323
320
|
prData = await adoFetch(url, token);
|
|
324
321
|
} catch (err) {
|
|
325
|
-
|
|
322
|
+
log('warn', `PR reconciliation failed for ${project.name}: ${err.message}`);
|
|
326
323
|
continue;
|
|
327
324
|
}
|
|
328
325
|
|
|
@@ -379,14 +376,14 @@ async function reconcilePrs(config) {
|
|
|
379
376
|
branch,
|
|
380
377
|
reviewStatus: 'pending',
|
|
381
378
|
status: 'active',
|
|
382
|
-
created: (adoPr.creationDate || '').slice(0, 10) ||
|
|
379
|
+
created: (adoPr.creationDate || '').slice(0, 10) || dateStamp(),
|
|
383
380
|
url: prUrl,
|
|
384
381
|
prdItems: confirmedItemId ? [confirmedItemId] : [],
|
|
385
382
|
});
|
|
386
383
|
if (confirmedItemId) addPrLink(prId, confirmedItemId);
|
|
387
384
|
existingIds.add(prId);
|
|
388
385
|
projectAdded++;
|
|
389
|
-
|
|
386
|
+
log('info', `PR reconciliation: added ${prId} (branch: ${branch}${confirmedItemId ? ', linked to ' + confirmedItemId : ''}) to ${project.name}`);
|
|
390
387
|
}
|
|
391
388
|
|
|
392
389
|
// Backfill prdItems from pr-links for any PR with empty array
|
|
@@ -404,12 +401,12 @@ async function reconcilePrs(config) {
|
|
|
404
401
|
if (projectAdded > 0 || projectUpdated > 0 || backfilled > 0) {
|
|
405
402
|
shared.safeWrite(prPath, existingPrs);
|
|
406
403
|
totalAdded += projectAdded;
|
|
407
|
-
if (projectUpdated > 0)
|
|
404
|
+
if (projectUpdated > 0) log('info', `PR reconciliation: linked ${projectUpdated} existing PR(s) to PRD items in ${project.name}`);
|
|
408
405
|
}
|
|
409
406
|
}
|
|
410
407
|
|
|
411
408
|
if (totalAdded > 0) {
|
|
412
|
-
|
|
409
|
+
log('info', `PR reconciliation: added ${totalAdded} missing PR(s) across projects`);
|
|
413
410
|
}
|
|
414
411
|
}
|
|
415
412
|
|
package/engine/cleanup.js
CHANGED
|
@@ -8,7 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const shared = require('./shared');
|
|
9
9
|
const queries = require('./queries');
|
|
10
10
|
|
|
11
|
-
const { exec, execSilent } = shared;
|
|
11
|
+
const { exec, execSilent, log, ts } = shared;
|
|
12
12
|
const { safeJson, safeWrite, safeReadDir, getProjects, projectWorkItemsPath, projectPrPath,
|
|
13
13
|
sanitizeBranch, KB_CATEGORIES } = shared;
|
|
14
14
|
const { getDispatch, getAgentStatus } = queries;
|
|
@@ -20,10 +20,9 @@ const PRD_DIR = queries.PRD_DIR;
|
|
|
20
20
|
const PLANS_DIR = queries.PLANS_DIR;
|
|
21
21
|
|
|
22
22
|
// Lazy require to break circular dependency with engine.js
|
|
23
|
+
// Only needed for engine().activeProcesses — log/ts come from shared.js
|
|
23
24
|
let _engine = null;
|
|
24
25
|
function engine() { if (!_engine) _engine = require('../engine'); return _engine; }
|
|
25
|
-
function log(level, msg, meta) { return engine().log(level, msg, meta); }
|
|
26
|
-
function ts() { return engine().ts(); }
|
|
27
26
|
|
|
28
27
|
// Lazy require for dispatch module
|
|
29
28
|
let _dispatch = null;
|
package/engine/cooldown.js
CHANGED
|
@@ -7,13 +7,9 @@ const path = require('path');
|
|
|
7
7
|
const shared = require('./shared');
|
|
8
8
|
const queries = require('./queries');
|
|
9
9
|
|
|
10
|
-
const { safeJson, safeWrite } = shared;
|
|
10
|
+
const { safeJson, safeWrite, log } = shared;
|
|
11
11
|
const { ENGINE_DIR } = queries;
|
|
12
12
|
|
|
13
|
-
// Lazy require to avoid circular dependency with engine.js
|
|
14
|
-
let _engine = null;
|
|
15
|
-
function engine() { if (!_engine) _engine = require('../engine'); return _engine; }
|
|
16
|
-
|
|
17
13
|
const COOLDOWN_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
|
|
18
14
|
const dispatchCooldowns = new Map(); // key → { timestamp, failures }
|
|
19
15
|
|
|
@@ -27,7 +23,7 @@ function loadCooldowns() {
|
|
|
27
23
|
dispatchCooldowns.set(k, v);
|
|
28
24
|
}
|
|
29
25
|
}
|
|
30
|
-
|
|
26
|
+
log('info', `Loaded ${dispatchCooldowns.size} cooldowns from disk`);
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
let _cooldownWriteTimer = null;
|
|
@@ -85,7 +81,7 @@ function setCooldownFailure(key) {
|
|
|
85
81
|
const failures = (existing?.failures || 0) + 1;
|
|
86
82
|
dispatchCooldowns.set(key, { timestamp: Date.now(), failures });
|
|
87
83
|
if (failures >= 3) {
|
|
88
|
-
|
|
84
|
+
log('warn', `${key} has failed ${failures} times — cooldown is now ${Math.min(Math.pow(2, failures), 8)}x`);
|
|
89
85
|
}
|
|
90
86
|
saveCooldowns();
|
|
91
87
|
}
|
package/engine/github.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const shared = require('./shared');
|
|
8
|
-
const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, MINIONS_DIR, addPrLink, getPrLinks } = shared;
|
|
8
|
+
const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, MINIONS_DIR, addPrLink, getPrLinks, log, dateStamp } = shared;
|
|
9
9
|
const { getPrs } = require('./queries');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
12
|
-
// Lazy require to avoid circular dependency
|
|
12
|
+
// Lazy require to avoid circular dependency — only needed for engine().handlePostMerge
|
|
13
13
|
let _engine = null;
|
|
14
14
|
function engine() {
|
|
15
15
|
if (!_engine) _engine = require('../engine');
|
|
@@ -37,7 +37,7 @@ function ghApi(endpoint, slug) {
|
|
|
37
37
|
const result = exec(cmd, { timeout: 30000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
38
38
|
return JSON.parse(result);
|
|
39
39
|
} catch (e) {
|
|
40
|
-
|
|
40
|
+
log('warn', `GitHub API error (${endpoint}): ${e.message}`);
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -66,7 +66,7 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
66
66
|
const updated = await callback(project, pr, prNum, slug);
|
|
67
67
|
if (updated) projectUpdated++;
|
|
68
68
|
} catch (err) {
|
|
69
|
-
|
|
69
|
+
log('warn', `GitHub: failed to poll PR ${pr.id}: ${err.message}`);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -101,7 +101,7 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
101
101
|
centralUpdated++;
|
|
102
102
|
}
|
|
103
103
|
} catch (err) {
|
|
104
|
-
|
|
104
|
+
log('warn', `GitHub: failed to poll central PR ${pr.id}: ${err.message}`);
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
if (centralUpdated > 0) {
|
|
@@ -115,8 +115,6 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
115
115
|
// ─── PR Status Polling ──────────────────────────────────────────────────────
|
|
116
116
|
|
|
117
117
|
async function pollPrStatus(config) {
|
|
118
|
-
const e = engine();
|
|
119
|
-
|
|
120
118
|
const totalUpdated = await forEachActiveGhPr(config, async (project, pr, prNum, slug) => {
|
|
121
119
|
const prData = ghApi(`/pulls/${prNum}`, slug);
|
|
122
120
|
if (!prData) return false;
|
|
@@ -130,7 +128,7 @@ async function pollPrStatus(config) {
|
|
|
130
128
|
else if (prData.state === 'open') newStatus = 'active';
|
|
131
129
|
|
|
132
130
|
if (pr.status !== newStatus) {
|
|
133
|
-
|
|
131
|
+
log('info', `PR ${pr.id} status: ${pr.status} → ${newStatus}`);
|
|
134
132
|
pr.status = newStatus;
|
|
135
133
|
updated = true;
|
|
136
134
|
|
|
@@ -138,7 +136,7 @@ async function pollPrStatus(config) {
|
|
|
138
136
|
// Resolve stale 'waiting' review status — won't be polled again after this
|
|
139
137
|
if (pr.reviewStatus === 'waiting') {
|
|
140
138
|
pr.reviewStatus = newStatus === 'merged' ? 'approved' : 'pending';
|
|
141
|
-
|
|
139
|
+
log('info', `PR ${pr.id} reviewStatus: waiting → ${pr.reviewStatus} (${newStatus})`);
|
|
142
140
|
}
|
|
143
141
|
await engine().handlePostMerge(pr, project, config, newStatus);
|
|
144
142
|
}
|
|
@@ -162,7 +160,7 @@ async function pollPrStatus(config) {
|
|
|
162
160
|
else if (states.length > 0) newReviewStatus = 'pending';
|
|
163
161
|
|
|
164
162
|
if (pr.reviewStatus !== newReviewStatus) {
|
|
165
|
-
|
|
163
|
+
log('info', `PR ${pr.id} reviewStatus: ${pr.reviewStatus} → ${newReviewStatus}`);
|
|
166
164
|
pr.reviewStatus = newReviewStatus;
|
|
167
165
|
updated = true;
|
|
168
166
|
// Update author metrics when verdict changes to approved/rejected
|
|
@@ -176,7 +174,7 @@ async function pollPrStatus(config) {
|
|
|
176
174
|
if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
|
|
177
175
|
else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
|
|
178
176
|
shared.safeWrite(metricsPath, metrics);
|
|
179
|
-
} catch (err) {
|
|
177
|
+
} catch (err) { log('warn', `Metrics update: ${err.message}`); }
|
|
180
178
|
}
|
|
181
179
|
}
|
|
182
180
|
}
|
|
@@ -207,7 +205,7 @@ async function pollPrStatus(config) {
|
|
|
207
205
|
}
|
|
208
206
|
|
|
209
207
|
if (pr.buildStatus !== buildStatus) {
|
|
210
|
-
|
|
208
|
+
log('info', `PR ${pr.id} build: ${pr.buildStatus || 'none'} → ${buildStatus}${buildFailReason ? ' (' + buildFailReason + ')' : ''}`);
|
|
211
209
|
pr.buildStatus = buildStatus;
|
|
212
210
|
if (buildFailReason) pr.buildFailReason = buildFailReason;
|
|
213
211
|
else delete pr.buildFailReason;
|
|
@@ -221,15 +219,13 @@ async function pollPrStatus(config) {
|
|
|
221
219
|
});
|
|
222
220
|
|
|
223
221
|
if (totalUpdated > 0) {
|
|
224
|
-
|
|
222
|
+
log('info', `GitHub PR status poll: updated ${totalUpdated} PR(s)`);
|
|
225
223
|
}
|
|
226
224
|
}
|
|
227
225
|
|
|
228
226
|
// ─── Poll Human Comments on PRs ─────────────────────────────────────────────
|
|
229
227
|
|
|
230
228
|
async function pollPrHumanComments(config) {
|
|
231
|
-
const e = engine();
|
|
232
|
-
|
|
233
229
|
const totalUpdated = await forEachActiveGhPr(config, async (project, pr, prNum, slug) => {
|
|
234
230
|
// Get issue comments (general PR comments)
|
|
235
231
|
const comments = ghApi(`/issues/${prNum}/comments`, slug);
|
|
@@ -292,19 +288,18 @@ async function pollPrHumanComments(config) {
|
|
|
292
288
|
feedbackContent
|
|
293
289
|
};
|
|
294
290
|
|
|
295
|
-
|
|
291
|
+
log('info', `PR ${pr.id}: ${newComments.length} new comment(s), ${allCommentEntries.length} total — full thread context provided`);
|
|
296
292
|
return true;
|
|
297
293
|
});
|
|
298
294
|
|
|
299
295
|
if (totalUpdated > 0) {
|
|
300
|
-
|
|
296
|
+
log('info', `GitHub PR comment poll: found human feedback on ${totalUpdated} PR(s)`);
|
|
301
297
|
}
|
|
302
298
|
}
|
|
303
299
|
|
|
304
300
|
// ─── PR Reconciliation ──────────────────────────────────────────────────────
|
|
305
301
|
|
|
306
302
|
async function reconcilePrs(config) {
|
|
307
|
-
const e = engine();
|
|
308
303
|
const projects = getProjects(config).filter(isGitHub);
|
|
309
304
|
const branchPatterns = [/^work\//i, /^feat\//i, /^user\/yemishin\//i];
|
|
310
305
|
let totalAdded = 0;
|
|
@@ -365,7 +360,7 @@ async function reconcilePrs(config) {
|
|
|
365
360
|
branch,
|
|
366
361
|
reviewStatus: 'pending',
|
|
367
362
|
status: 'active',
|
|
368
|
-
created: (ghPr.created_at || '').slice(0, 10) ||
|
|
363
|
+
created: (ghPr.created_at || '').slice(0, 10) || dateStamp(),
|
|
369
364
|
url: prUrl,
|
|
370
365
|
prdItems: confirmedItemId ? [confirmedItemId] : [],
|
|
371
366
|
});
|
|
@@ -373,7 +368,7 @@ async function reconcilePrs(config) {
|
|
|
373
368
|
existingIds.add(prId);
|
|
374
369
|
projectAdded++;
|
|
375
370
|
|
|
376
|
-
|
|
371
|
+
log('info', `GitHub PR reconciliation: added ${prId} (branch: ${branch}${confirmedItemId ? ', linked to ' + confirmedItemId : ''}) to ${project.name}`);
|
|
377
372
|
}
|
|
378
373
|
|
|
379
374
|
// Backfill prdItems from pr-links for any PR with empty array
|
|
@@ -395,7 +390,7 @@ async function reconcilePrs(config) {
|
|
|
395
390
|
}
|
|
396
391
|
|
|
397
392
|
if (totalAdded > 0) {
|
|
398
|
-
|
|
393
|
+
log('info', `GitHub PR reconciliation: added ${totalAdded} missing PR(s)`);
|
|
399
394
|
}
|
|
400
395
|
}
|
|
401
396
|
|
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
|
@@ -14,8 +14,7 @@ const { renderPlaybook } = require('./playbook');
|
|
|
14
14
|
/** Patterns that indicate an agent returned no meaningful output */
|
|
15
15
|
const EMPTY_OUTPUT_PATTERNS = ['(no output)', '(no findings)', '(no response)'];
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
function engine() { if (!_engine) _engine = require('../engine'); return _engine; }
|
|
17
|
+
// No lazy require needed — log comes from shared.js, no engine-specific APIs used
|
|
19
18
|
|
|
20
19
|
const MEETINGS_DIR = path.join(__dirname, '..', 'meetings');
|
|
21
20
|
|
|
@@ -79,8 +78,12 @@ function discoverMeetingWork(config) {
|
|
|
79
78
|
const agents = config.agents || {};
|
|
80
79
|
|
|
81
80
|
if (roundName === 'concluding') {
|
|
82
|
-
//
|
|
83
|
-
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];
|
|
84
87
|
if (!concluder) continue;
|
|
85
88
|
const key = `meeting-${meeting.id}-r${round}-${concluder}`;
|
|
86
89
|
if (activeKeys.has(key)) continue;
|
|
@@ -179,7 +182,6 @@ function discoverMeetingWork(config) {
|
|
|
179
182
|
* Called from runPostCompletionHooks when type === 'meeting'.
|
|
180
183
|
*/
|
|
181
184
|
function collectMeetingFindings(meetingId, agentId, roundName, output) {
|
|
182
|
-
const e = engine();
|
|
183
185
|
const meeting = getMeeting(meetingId);
|
|
184
186
|
if (!meeting) return;
|
|
185
187
|
|
|
@@ -188,7 +190,7 @@ function collectMeetingFindings(meetingId, agentId, roundName, output) {
|
|
|
188
190
|
|
|
189
191
|
// Validate output — reject empty or placeholder responses
|
|
190
192
|
if (!rawContent || EMPTY_OUTPUT_PATTERNS.includes(rawContent)) {
|
|
191
|
-
|
|
193
|
+
log('warn', `Meeting ${meetingId}: agent ${agentId} returned empty output for ${roundName} — rejecting`);
|
|
192
194
|
// Don't record it — agent will be re-dispatched on next tick
|
|
193
195
|
saveMeeting(meeting);
|
|
194
196
|
return;
|
|
@@ -304,7 +306,6 @@ function deleteMeeting(id) {
|
|
|
304
306
|
* Called from engine.js tick cycle.
|
|
305
307
|
*/
|
|
306
308
|
function checkMeetingTimeouts(config) {
|
|
307
|
-
const e = engine();
|
|
308
309
|
const meetings = getMeetings();
|
|
309
310
|
const timeout = (config.engine || {}).meetingRoundTimeout
|
|
310
311
|
|| ENGINE_DEFAULTS.meetingRoundTimeout;
|
|
@@ -324,21 +325,21 @@ function checkMeetingTimeouts(config) {
|
|
|
324
325
|
const totalCount = meeting.participants.length;
|
|
325
326
|
|
|
326
327
|
if (meeting.status === 'investigating') {
|
|
327
|
-
|
|
328
|
+
log('warn', `Meeting ${meeting.id}: round 1 timed out after ${Math.round(elapsed / 60000)}min — ${respondedCount}/${totalCount} responded, advancing to debate`);
|
|
328
329
|
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round 1 timed out — ${respondedCount}/${totalCount} findings received`, at: new Date().toISOString() });
|
|
329
330
|
meeting.status = 'debating';
|
|
330
331
|
meeting.round = 2;
|
|
331
332
|
meeting.roundStartedAt = new Date().toISOString();
|
|
332
333
|
saveMeeting(meeting);
|
|
333
334
|
} else if (meeting.status === 'debating') {
|
|
334
|
-
|
|
335
|
+
log('warn', `Meeting ${meeting.id}: round 2 timed out after ${Math.round(elapsed / 60000)}min — ${respondedCount}/${totalCount} responded, advancing to conclusion`);
|
|
335
336
|
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round 2 timed out — ${respondedCount}/${totalCount} debate responses received`, at: new Date().toISOString() });
|
|
336
337
|
meeting.status = 'concluding';
|
|
337
338
|
meeting.round = 3;
|
|
338
339
|
meeting.roundStartedAt = new Date().toISOString();
|
|
339
340
|
saveMeeting(meeting);
|
|
340
341
|
} else if (meeting.status === 'concluding') {
|
|
341
|
-
|
|
342
|
+
log('warn', `Meeting ${meeting.id}: conclusion round timed out after ${Math.round(elapsed / 60000)}min — ending meeting without conclusion`);
|
|
342
343
|
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: 'Conclusion round timed out — meeting ended without conclusion', at: new Date().toISOString() });
|
|
343
344
|
meeting.status = 'completed';
|
|
344
345
|
meeting.completedAt = new Date().toISOString();
|
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 {
|
|
@@ -290,15 +306,27 @@ function renderPlaybook(type, vars) {
|
|
|
290
306
|
.filter(([, val]) => String(val) === '')
|
|
291
307
|
.map(([key]) => key);
|
|
292
308
|
if (emptyVars.length > 0) {
|
|
293
|
-
|
|
294
|
-
try { engine().log('warn', msg); } catch { /* engine not ready */ }
|
|
309
|
+
log('warn', `Playbook "${type}": template variables resolved to empty string: ${emptyVars.join(', ')}`);
|
|
295
310
|
}
|
|
296
311
|
|
|
297
312
|
// Warn on any remaining unresolved {{variable}} placeholders
|
|
298
313
|
const unresolved = [...new Set((content.match(/\{\{(\w+)\}\}/g) || []).map(m => m.slice(2, -2)))];
|
|
299
314
|
if (unresolved.length > 0) {
|
|
300
|
-
|
|
301
|
-
|
|
315
|
+
log('warn', `Playbook "${type}": unresolved template variables: ${unresolved.join(', ')}`);
|
|
316
|
+
}
|
|
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
|
+
}
|
|
302
330
|
}
|
|
303
331
|
|
|
304
332
|
return content;
|
|
@@ -472,6 +500,8 @@ function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabe
|
|
|
472
500
|
|
|
473
501
|
module.exports = {
|
|
474
502
|
renderPlaybook,
|
|
503
|
+
getLastRenderError,
|
|
504
|
+
CRITICAL_VARS,
|
|
475
505
|
buildSystemPrompt,
|
|
476
506
|
buildAgentContext,
|
|
477
507
|
selectPlaybook,
|
package/engine/routing.js
CHANGED
|
@@ -8,16 +8,12 @@ const path = require('path');
|
|
|
8
8
|
const shared = require('./shared');
|
|
9
9
|
const queries = require('./queries');
|
|
10
10
|
|
|
11
|
-
const { safeJson, safeRead } = shared;
|
|
11
|
+
const { safeJson, safeRead, log, ts } = shared;
|
|
12
12
|
const { ENGINE_DIR, DISPATCH_PATH } = queries;
|
|
13
13
|
|
|
14
14
|
const MINIONS_DIR = path.resolve(__dirname, '..');
|
|
15
15
|
const ROUTING_PATH = path.join(MINIONS_DIR, 'routing.md');
|
|
16
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
17
|
// ─── Temp Agents ─────────────────────────────────────────────────────────────
|
|
22
18
|
|
|
23
19
|
const tempAgents = new Map(); // tempAgentId → { name, role, createdAt }
|
|
@@ -140,8 +136,8 @@ function resolveAgent(workType, config, authorAgent = null) {
|
|
|
140
136
|
if (config.engine?.allowTempAgents) {
|
|
141
137
|
const tempId = `temp-${shared.uid()}`;
|
|
142
138
|
_claimedAgents.add(tempId);
|
|
143
|
-
tempAgents.set(tempId, { name: `Temp-${tempId.slice(5, 9)}`, role: 'Temporary Agent', createdAt:
|
|
144
|
-
|
|
139
|
+
tempAgents.set(tempId, { name: `Temp-${tempId.slice(5, 9)}`, role: 'Temporary Agent', createdAt: ts() });
|
|
140
|
+
log('info', `Spawning temp agent ${tempId} — all permanent agents busy`);
|
|
145
141
|
return tempId;
|
|
146
142
|
}
|
|
147
143
|
|
package/engine/timeout.js
CHANGED
|
@@ -8,16 +8,15 @@ const path = require('path');
|
|
|
8
8
|
const shared = require('./shared');
|
|
9
9
|
const queries = require('./queries');
|
|
10
10
|
|
|
11
|
-
const { safeRead, safeWrite, safeJson, getProjects, projectWorkItemsPath, ENGINE_DEFAULTS: DEFAULTS } = shared;
|
|
11
|
+
const { safeRead, safeWrite, safeJson, getProjects, projectWorkItemsPath, log, ts, ENGINE_DEFAULTS: DEFAULTS } = shared;
|
|
12
12
|
const { getDispatch, getAgentStatus } = queries;
|
|
13
13
|
const AGENTS_DIR = queries.AGENTS_DIR;
|
|
14
14
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
15
15
|
|
|
16
16
|
// Lazy require to break circular dependency with engine.js
|
|
17
|
+
// Only needed for engine().activeProcesses and engine().engineRestartGraceUntil — log/ts come from shared.js
|
|
17
18
|
let _engine = null;
|
|
18
19
|
function engine() { if (!_engine) _engine = require('../engine'); return _engine; }
|
|
19
|
-
function log(level, msg, meta) { return engine().log(level, msg, meta); }
|
|
20
|
-
function ts() { return engine().ts(); }
|
|
21
20
|
|
|
22
21
|
// Lazy require for dispatch module (also circular via engine)
|
|
23
22
|
let _dispatch = null;
|
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"
|