@yemi33/minions 0.1.162 → 0.1.164
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 +10 -0
- package/engine/lifecycle.js +68 -61
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/engine/lifecycle.js
CHANGED
|
@@ -1188,6 +1188,11 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1188
1188
|
? path.join(MINIONS_DIR, 'work-items.json')
|
|
1189
1189
|
: meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
|
|
1190
1190
|
if (wiPath) {
|
|
1191
|
+
// Cost ceiling circuit breaker config — resolved outside lock for clarity
|
|
1192
|
+
const engineCfg = config?.engine || {};
|
|
1193
|
+
const evalMaxCost = engineCfg.evalMaxCost != null ? engineCfg.evalMaxCost : shared.ENGINE_DEFAULTS.evalMaxCost;
|
|
1194
|
+
|
|
1195
|
+
// Single lock: accumulate cost AND check ceiling atomically (no TOCTOU)
|
|
1191
1196
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
1192
1197
|
if (!Array.isArray(items)) return items;
|
|
1193
1198
|
const wi = items.find(i => i.id === meta.item.id);
|
|
@@ -1195,29 +1200,17 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1195
1200
|
wi._totalCostUsd = (wi._totalCostUsd || 0) + (taskUsage.costUsd || 0);
|
|
1196
1201
|
wi._totalInputTokens = (wi._totalInputTokens || 0) + (taskUsage.inputTokens || 0);
|
|
1197
1202
|
wi._totalOutputTokens = (wi._totalOutputTokens || 0) + (taskUsage.outputTokens || 0);
|
|
1203
|
+
|
|
1204
|
+
// Cost ceiling circuit breaker — treat like evalMaxIterations exceeded
|
|
1205
|
+
if (evalMaxCost != null && evalMaxCost > 0 &&
|
|
1206
|
+
wi._totalCostUsd > evalMaxCost && wi.status !== 'needs-human-review') {
|
|
1207
|
+
wi.status = 'needs-human-review';
|
|
1208
|
+
wi.failReason = `Cumulative cost $${wi._totalCostUsd.toFixed(2)} exceeds evalMaxCost ceiling $${evalMaxCost.toFixed(2)}`;
|
|
1209
|
+
log('warn', `Work item ${meta.item.id} exceeded cost ceiling ($${wi._totalCostUsd.toFixed(2)} > $${evalMaxCost.toFixed(2)}) — needs-human-review`);
|
|
1210
|
+
}
|
|
1198
1211
|
}
|
|
1199
1212
|
return items;
|
|
1200
1213
|
}, { defaultValue: [] });
|
|
1201
|
-
|
|
1202
|
-
// Cost ceiling circuit breaker — treat like evalMaxIterations exceeded
|
|
1203
|
-
const engineCfg = config?.engine || {};
|
|
1204
|
-
const evalMaxCost = engineCfg.evalMaxCost != null ? engineCfg.evalMaxCost : shared.ENGINE_DEFAULTS.evalMaxCost;
|
|
1205
|
-
if (evalMaxCost != null && evalMaxCost > 0) {
|
|
1206
|
-
const freshItems = safeJson(wiPath) || [];
|
|
1207
|
-
const wi = freshItems.find(i => i.id === meta.item.id);
|
|
1208
|
-
if (wi && wi._totalCostUsd > evalMaxCost && wi.status !== 'needs-human-review') {
|
|
1209
|
-
mutateJsonFileLocked(wiPath, (items) => {
|
|
1210
|
-
if (!Array.isArray(items)) return items;
|
|
1211
|
-
const target = items.find(i => i.id === meta.item.id);
|
|
1212
|
-
if (target) {
|
|
1213
|
-
target.status = 'needs-human-review';
|
|
1214
|
-
target.failReason = `Cumulative cost $${wi._totalCostUsd.toFixed(2)} exceeds evalMaxCost ceiling $${evalMaxCost.toFixed(2)}`;
|
|
1215
|
-
log('warn', `Work item ${meta.item.id} exceeded cost ceiling ($${wi._totalCostUsd.toFixed(2)} > $${evalMaxCost.toFixed(2)}) — needs-human-review`);
|
|
1216
|
-
}
|
|
1217
|
-
return items;
|
|
1218
|
-
}, { defaultValue: [] });
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
1214
|
}
|
|
1222
1215
|
} catch (err) { log('warn', `Cost accumulation: ${err.message}`); }
|
|
1223
1216
|
}
|
|
@@ -1250,40 +1243,48 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1250
1243
|
|
|
1251
1244
|
if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, 'done', '');
|
|
1252
1245
|
|
|
1253
|
-
// Auto-dispatch review work item after implement completes successfully
|
|
1254
|
-
|
|
1246
|
+
// Auto-dispatch review work item after implement or fix completes successfully
|
|
1247
|
+
// For 'fix' items, only trigger when they were created by the eval loop (_evalParentId set)
|
|
1248
|
+
const isEvalEligible = type === 'implement' || (type === 'fix' && meta?.item?._evalParentId);
|
|
1249
|
+
if (isSuccess && !skipDoneStatus && isEvalEligible && meta?.item?.id) {
|
|
1255
1250
|
const autoReview = config.engine?.autoReview ?? shared.ENGINE_DEFAULTS.autoReview;
|
|
1256
1251
|
if (autoReview) {
|
|
1257
1252
|
try {
|
|
1258
1253
|
const wiPath = resolveWiPath(meta);
|
|
1259
1254
|
if (wiPath) {
|
|
1260
1255
|
const items = safeJson(wiPath) || [];
|
|
1256
|
+
// For fix items, the eval parent is the original implement item
|
|
1257
|
+
const evalParentId = type === 'fix' ? meta.item._evalParentId : meta.item.id;
|
|
1261
1258
|
// Dedup: skip if a review item already exists for this parent
|
|
1262
|
-
const existing = items.find(i => i._evalParentId ===
|
|
1259
|
+
const existing = items.find(i => i._evalParentId === evalParentId && i.type === 'review' && i.status !== 'done' && i.status !== 'failed');
|
|
1263
1260
|
if (existing) {
|
|
1264
|
-
log('info', `Eval loop: review item ${existing.id} already exists for ${
|
|
1261
|
+
log('info', `Eval loop: review item ${existing.id} already exists for ${evalParentId}, skipping`);
|
|
1265
1262
|
} else {
|
|
1266
|
-
const parentItem = items.find(i => i.id ===
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1263
|
+
const parentItem = items.find(i => i.id === evalParentId);
|
|
1264
|
+
if (parentItem?._evalDispatched) {
|
|
1265
|
+
log('info', `Eval loop: parent ${evalParentId} already has _evalDispatched set, skipping`);
|
|
1266
|
+
} else {
|
|
1267
|
+
const evalItem = {
|
|
1268
|
+
id: 'W-' + shared.uid(),
|
|
1269
|
+
title: `Review: ${parentItem?.title || meta.item.title || evalParentId}`,
|
|
1270
|
+
type: 'review',
|
|
1271
|
+
priority: parentItem?.priority || meta.item.priority || 'high',
|
|
1272
|
+
status: 'pending',
|
|
1273
|
+
created: ts(),
|
|
1274
|
+
createdBy: 'engine:eval-loop',
|
|
1275
|
+
project: meta.project?.name || meta.item.project,
|
|
1276
|
+
branch_name: parentItem?.branch_name || meta.branch || null,
|
|
1277
|
+
pr_url: parentItem?.pr_url || null,
|
|
1278
|
+
acceptance_criteria: parentItem?.acceptance_criteria || meta.item.acceptance_criteria || null,
|
|
1279
|
+
_evalParentId: evalParentId,
|
|
1280
|
+
};
|
|
1281
|
+
if (parentItem?.sourcePlan) evalItem.sourcePlan = parentItem.sourcePlan;
|
|
1282
|
+
// Mark parent as eval-dispatched before writing to prevent duplicates on re-run
|
|
1283
|
+
if (parentItem) parentItem._evalDispatched = true;
|
|
1284
|
+
items.push(evalItem);
|
|
1285
|
+
shared.safeWrite(wiPath, items);
|
|
1286
|
+
log('info', `Eval loop: created ${evalItem.id} for completed ${type} ${meta.item.id} (parent: ${evalParentId})`);
|
|
1287
|
+
}
|
|
1287
1288
|
}
|
|
1288
1289
|
}
|
|
1289
1290
|
} catch (err) {
|
|
@@ -1350,15 +1351,15 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1350
1351
|
}
|
|
1351
1352
|
|
|
1352
1353
|
if (!isSuccess && meta?.item?.id) {
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1354
|
+
// Auto-retry with atomic read-modify-write (no TOCTOU race)
|
|
1355
|
+
try {
|
|
1356
|
+
const wiPath = resolveWiPath(meta);
|
|
1357
|
+
if (wiPath) {
|
|
1358
|
+
let retriesExhausted = false;
|
|
1357
1359
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
1358
1360
|
if (!Array.isArray(items)) return items;
|
|
1359
1361
|
const wi = items.find(i => i.id === meta.item.id);
|
|
1360
1362
|
if (!wi) return items;
|
|
1361
|
-
|
|
1362
1363
|
const retries = wi._retryCount || 0;
|
|
1363
1364
|
if (retries < 3) {
|
|
1364
1365
|
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
|
|
@@ -1366,22 +1367,28 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1366
1367
|
wi.status = 'pending';
|
|
1367
1368
|
delete wi.dispatched_at;
|
|
1368
1369
|
delete wi.dispatched_to;
|
|
1369
|
-
|
|
1370
|
+
if (type === 'decompose') delete wi._decomposing;
|
|
1370
1371
|
} else {
|
|
1371
|
-
|
|
1372
|
-
wi.failReason = 'Agent failed (3 retries exhausted)';
|
|
1373
|
-
wi.failedAt = ts();
|
|
1374
|
-
finalStatus = 'failed';
|
|
1372
|
+
retriesExhausted = true;
|
|
1375
1373
|
}
|
|
1374
|
+
// Clear _decomposing flag on any decompose failure to prevent permanent stuck
|
|
1376
1375
|
if (type === 'decompose') delete wi._decomposing;
|
|
1377
1376
|
return items;
|
|
1378
|
-
});
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1377
|
+
}, { defaultValue: [] });
|
|
1378
|
+
if (retriesExhausted) {
|
|
1379
|
+
updateWorkItemStatus(meta, 'failed', 'Agent failed (3 retries exhausted)');
|
|
1380
|
+
}
|
|
1381
|
+
} else {
|
|
1382
|
+
// No wiPath — can't read retries, fall back to meta snapshot
|
|
1383
|
+
const retries = meta.item._retryCount || 0;
|
|
1384
|
+
if (retries < 3) {
|
|
1385
|
+
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
|
|
1386
|
+
updateWorkItemStatus(meta, 'pending', '');
|
|
1387
|
+
} else {
|
|
1388
|
+
updateWorkItemStatus(meta, 'failed', 'Agent failed (3 retries exhausted)');
|
|
1389
|
+
}
|
|
1383
1390
|
}
|
|
1384
|
-
}
|
|
1391
|
+
} catch (err) { log('warn', `Retry/decompose update: ${err.message}`); }
|
|
1385
1392
|
}
|
|
1386
1393
|
// Meeting post-completion: collect findings/debate/conclusion
|
|
1387
1394
|
if (type === 'meeting' && meta?.meetingId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.164",
|
|
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"
|