@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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.164 (2026-04-02)
4
+
5
+ ### Engine
6
+ - engine/lifecycle.js
7
+
8
+ ## 0.1.163 (2026-04-02)
9
+
10
+ ### Engine
11
+ - engine/lifecycle.js
12
+
3
13
  ## 0.1.162 (2026-04-02)
4
14
 
5
15
  ### Other
@@ -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
- if (isSuccess && !skipDoneStatus && type === 'implement' && meta?.item?.id) {
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 === meta.item.id && i.type === 'review');
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 ${meta.item.id}, skipping`);
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 === meta.item.id);
1267
- const evalItem = {
1268
- id: 'W-' + shared.uid(),
1269
- title: `Review: ${meta.item.title || meta.item.id}`,
1270
- type: 'review',
1271
- 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: meta.item.id,
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 implement ${meta.item.id}`);
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
- const wiPath = resolveWiPath(meta);
1354
- if (wiPath) {
1355
- let finalStatus = null;
1356
- try {
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
- finalStatus = 'pending';
1370
+ if (type === 'decompose') delete wi._decomposing;
1370
1371
  } else {
1371
- wi.status = 'failed';
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
- } catch (err) { log('warn', `Retry update: ${err.message}`); }
1380
- // Sync status to PRD outside the work-items lock
1381
- if (finalStatus) {
1382
- syncPrdItemStatus(meta.item.id, finalStatus, meta.item?.sourcePlan);
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.162",
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"