@yemi33/minions 0.1.145 → 0.1.147

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,6 +1,19 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.145 (2026-04-01)
3
+ ## 0.1.147 (2026-04-01)
4
+
5
+ ### Engine
6
+ - engine/lifecycle.js
7
+ - engine/shared.js
8
+
9
+ ### Dashboard
10
+ - dashboard/js/render-work-items.js
11
+
12
+ ### Other
13
+ - .github/workflows/pr-tests.yml
14
+ - test/unit.test.js
15
+
16
+ ## 0.1.146 (2026-04-01)
4
17
 
5
18
  ### Engine
6
19
  - engine.js
@@ -12,13 +25,23 @@
12
25
  - engine/meeting.js
13
26
  - engine/playbook.js
14
27
  - engine/routing.js
28
+ - engine/shared.js
15
29
  - engine/timeout.js
16
30
 
17
31
  ### Dashboard
18
32
  - dashboard/js/render-other.js
33
+ - dashboard/js/render-work-items.js
19
34
  - dashboard/pages/engine.html
35
+ - dashboard/styles.css
36
+
37
+ ### Playbooks
38
+ - evaluate.md
39
+ - fix.md
40
+ - implement.md
20
41
 
21
42
  ### Other
43
+ - CLAUDE.md
44
+ - routing.md
22
45
  - test/unit.test.js
23
46
 
24
47
  ## 0.1.143 (2026-04-01)
@@ -6,7 +6,7 @@ const WI_PER_PAGE = 20;
6
6
 
7
7
  function wiRow(item) {
8
8
  const statusBadge = (s) => {
9
- const cls = s === 'failed' ? 'rejected' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : 'draft';
9
+ const cls = s === 'failed' ? 'rejected' : s === 'needs-human-review' ? 'needs-review' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : 'draft';
10
10
  return '<span class="pr-badge ' + cls + '">' + escHtml(s) + '</span>';
11
11
  };
12
12
  const typeBadge = (t) => '<span class="dispatch-type ' + (t || 'implement') + '">' + escHtml(t || 'implement') + '</span>';
@@ -38,9 +38,9 @@ function wiRow(item) {
38
38
  (item.acceptanceCriteria && item.acceptanceCriteria.length ? '<span title="' + item.acceptanceCriteria.length + ' acceptance criteria">&#x2611;' + item.acceptanceCriteria.length + '</span>' : '') +
39
39
  '</td>' +
40
40
  '<td style="white-space:nowrap">' +
41
- ((item.status === 'pending' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Edit work item">&#x270E;</button>' : '') +
42
- ((item.status === 'done' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Archive work item">&#x1F4E6;</button>' : '') +
43
- ((item.status === 'done' || item.status === 'failed') && !item._humanFeedback ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();feedbackWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Give feedback">&#x1F44D;&#x1F44E;</button>' : (item._humanFeedback ? '<span style="font-size:9px" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '&#x1F44D;' : '&#x1F44E;') + '</span> ' : '')) +
41
+ ((item.status === 'pending' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Edit work item">&#x270E;</button>' : '') +
42
+ ((item.status === 'done' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Archive work item">&#x1F4E6;</button>' : '') +
43
+ ((item.status === 'done' || item.status === 'failed' || item.status === 'needs-human-review') && !item._humanFeedback ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();feedbackWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Give feedback">&#x1F44D;&#x1F44E;</button>' : (item._humanFeedback ? '<span style="font-size:9px" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '&#x1F44D;' : '&#x1F44E;') + '</span> ' : '')) +
44
44
  '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Delete work item and kill agent">&#x2715;</button>' +
45
45
  '</td>' +
46
46
  '</tr>';
@@ -49,7 +49,7 @@ function wiRow(item) {
49
49
  function renderWorkItems(items) {
50
50
  items = items.filter(function(w) { return !isDeleted('wi:' + w.id); });
51
51
  // Sort: active/dispatched first, then by most recent activity
52
- const statusOrder = { dispatched: 0, pending: 1, queued: 1, failed: 2, done: 3 };
52
+ const statusOrder = { dispatched: 0, pending: 1, queued: 1, 'needs-human-review': 2, failed: 2, done: 3 };
53
53
  items.sort((a, b) => {
54
54
  const sa = statusOrder[a.status] ?? 2, sb = statusOrder[b.status] ?? 2;
55
55
  if (sa !== sb) return sa - sb;
@@ -388,6 +388,9 @@ function openWorkItemDetail(id) {
388
388
  if (item.references?.length) html += field('References', item.references.map(r => '<a href="' + escHtml(r.url) + '" target="_blank" style="color:var(--blue)">' + escHtml(r.title || r.url) + '</a>' + (r.type ? ' <span style="color:var(--muted);font-size:10px">(' + escHtml(r.type) + ')</span>' : '')).join('<br>'));
389
389
  if (item._humanFeedback) html += field('Human Feedback', (item._humanFeedback.rating === 'up' ? '👍' : '👎') + (item._humanFeedback.comment ? ' — ' + escHtml(item._humanFeedback.comment) : ''));
390
390
  if (item._pr) html += field('Pull Request', '<a href="' + escHtml(item._prUrl || '#') + '" target="_blank" style="color:var(--blue)">' + escHtml(item._pr) + '</a>');
391
+ if (item._totalCostUsd != null) html += field('Cumulative Cost', '$' + Number(item._totalCostUsd).toFixed(4));
392
+ if (item._totalInputTokens) html += field('Total Input Tokens', Number(item._totalInputTokens).toLocaleString());
393
+ if (item._totalOutputTokens) html += field('Total Output Tokens', Number(item._totalOutputTokens).toLocaleString());
391
394
  html += '</div>';
392
395
 
393
396
  document.getElementById('modal-title').textContent = item.title || item.id;
@@ -186,6 +186,7 @@
186
186
  .prd-item-row.st-in-progress { border-left-color: var(--yellow); animation: prdWipPulse 2s infinite; }
187
187
  @keyframes prdWipPulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(210,153,34,0); } 50% { box-shadow: 0 0 0 4px rgba(210,153,34,0.2); } }
188
188
  .prd-item-row.st-failed { border-left-color: var(--red); }
189
+ .prd-item-row.st-needs-human-review { border-left-color: var(--orange); }
189
190
  .prd-item-row.st-paused { border-left-color: var(--muted); opacity: 0.5; }
190
191
  .prd-item-id { font-family: Consolas, monospace; color: var(--muted); min-width: 36px; font-size: 0.9em; }
191
192
  .prd-item-name { flex: 1; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -232,6 +233,7 @@
232
233
  .pr-badge.active { background: rgba(88,166,255,0.15); color: var(--blue); border: 1px solid var(--blue); }
233
234
  .pr-badge.approved { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
234
235
  .pr-badge.rejected { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
236
+ .pr-badge.needs-review { background: rgba(227,179,65,0.15); color: var(--orange); border: 1px solid var(--orange); }
235
237
  .pr-badge.merged { background: rgba(188,140,255,0.15); color: var(--purple); border: 1px solid var(--purple); }
236
238
  .pr-badge.building { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
237
239
  .pr-badge.build-pass { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
@@ -1025,6 +1025,40 @@ function parseAgentOutput(stdout) {
1025
1025
  return { resultSummary: text, taskUsage: usage, sessionId, model };
1026
1026
  }
1027
1027
 
1028
+ /**
1029
+ * Resolve work-items.json path from dispatch meta.
1030
+ * Central items → MINIONS_DIR/work-items.json; project items → projects/<name>/work-items.json.
1031
+ */
1032
+ function resolveWiPath(meta) {
1033
+ if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
1034
+ return path.join(MINIONS_DIR, 'work-items.json');
1035
+ }
1036
+ if (meta.project?.name) {
1037
+ return path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json');
1038
+ }
1039
+ return null;
1040
+ }
1041
+
1042
+ /**
1043
+ * Parse structured eval verdict from evaluate agent output.
1044
+ * Looks for a JSON block with { pass, build, tests, criteria_met, criteria_failed, feedback }.
1045
+ * Returns parsed object or null if not found.
1046
+ */
1047
+ function parseEvalVerdict(text) {
1048
+ if (!text) return null;
1049
+ // Look for JSON fenced block first, then bare JSON
1050
+ const fenced = text.match(/```(?:json)?\s*\n(\{[\s\S]*?"pass"\s*:[\s\S]*?\})\s*\n```/);
1051
+ if (fenced) {
1052
+ try { return JSON.parse(fenced[1]); } catch { /* fall through */ }
1053
+ }
1054
+ // Try bare JSON with "pass" key
1055
+ const bare = text.match(/(\{[\s\S]*?"pass"\s*:[\s\S]*?\})/);
1056
+ if (bare) {
1057
+ try { return JSON.parse(bare[1]); } catch { /* ignore */ }
1058
+ }
1059
+ return null;
1060
+ }
1061
+
1028
1062
  /**
1029
1063
  * Handle decomposition result — parse sub-items from agent output and create child work items.
1030
1064
  * Called from runPostCompletionHooks when type === 'decompose'.
@@ -1119,6 +1153,47 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1119
1153
  } catch (err) { log('warn', `Session save: ${err.message}`); }
1120
1154
  }
1121
1155
 
1156
+ // ── Accumulate per-work-item cost tracking ──────────────────────────────────
1157
+ if (taskUsage && meta?.item?.id) {
1158
+ try {
1159
+ const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
1160
+ ? path.join(MINIONS_DIR, 'work-items.json')
1161
+ : meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
1162
+ if (wiPath) {
1163
+ mutateJsonFileLocked(wiPath, (items) => {
1164
+ if (!Array.isArray(items)) return items;
1165
+ const wi = items.find(i => i.id === meta.item.id);
1166
+ if (wi) {
1167
+ wi._totalCostUsd = (wi._totalCostUsd || 0) + (taskUsage.costUsd || 0);
1168
+ wi._totalInputTokens = (wi._totalInputTokens || 0) + (taskUsage.inputTokens || 0);
1169
+ wi._totalOutputTokens = (wi._totalOutputTokens || 0) + (taskUsage.outputTokens || 0);
1170
+ }
1171
+ return items;
1172
+ }, { defaultValue: [] });
1173
+
1174
+ // Cost ceiling circuit breaker — treat like evalMaxIterations exceeded
1175
+ const engineCfg = config?.engine || {};
1176
+ const evalMaxCost = engineCfg.evalMaxCost != null ? engineCfg.evalMaxCost : shared.ENGINE_DEFAULTS.evalMaxCost;
1177
+ if (evalMaxCost != null && evalMaxCost > 0) {
1178
+ const freshItems = safeJson(wiPath) || [];
1179
+ const wi = freshItems.find(i => i.id === meta.item.id);
1180
+ if (wi && wi._totalCostUsd > evalMaxCost && wi.status !== 'needs-human-review') {
1181
+ mutateJsonFileLocked(wiPath, (items) => {
1182
+ if (!Array.isArray(items)) return items;
1183
+ const target = items.find(i => i.id === meta.item.id);
1184
+ if (target) {
1185
+ target.status = 'needs-human-review';
1186
+ target.failReason = `Cumulative cost $${wi._totalCostUsd.toFixed(2)} exceeds evalMaxCost ceiling $${evalMaxCost.toFixed(2)}`;
1187
+ log('warn', `Work item ${meta.item.id} exceeded cost ceiling ($${wi._totalCostUsd.toFixed(2)} > $${evalMaxCost.toFixed(2)}) — needs-human-review`);
1188
+ }
1189
+ return items;
1190
+ }, { defaultValue: [] });
1191
+ }
1192
+ }
1193
+ }
1194
+ } catch (err) { log('warn', `Cost accumulation: ${err.message}`); }
1195
+ }
1196
+
1122
1197
  // Handle decomposition results — create sub-items from decompose agent output
1123
1198
  let skipDoneStatus = false;
1124
1199
  if (type === 'decompose' && isSuccess && meta?.item?.id) {
@@ -1145,6 +1220,106 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1145
1220
  }
1146
1221
 
1147
1222
  if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, 'done', '');
1223
+
1224
+ // Auto-dispatch evaluate work item after implement completes successfully
1225
+ if (isSuccess && !skipDoneStatus && type === 'implement' && meta?.item?.id) {
1226
+ const evalLoop = config.engine?.evalLoop ?? shared.ENGINE_DEFAULTS.evalLoop;
1227
+ if (evalLoop) {
1228
+ try {
1229
+ const wiPath = resolveWiPath(meta);
1230
+ if (wiPath) {
1231
+ const items = safeJson(wiPath) || [];
1232
+ // Dedup: skip if an evaluate item already exists for this parent
1233
+ const existing = items.find(i => i._evalParentId === meta.item.id && i.type === 'evaluate');
1234
+ if (existing) {
1235
+ log('info', `Eval loop: evaluate item ${existing.id} already exists for ${meta.item.id}, skipping`);
1236
+ } else {
1237
+ const parentItem = items.find(i => i.id === meta.item.id);
1238
+ const evalItem = {
1239
+ id: 'W-' + shared.uid(),
1240
+ title: `Evaluate: ${meta.item.title || meta.item.id}`,
1241
+ type: 'evaluate',
1242
+ priority: meta.item.priority || 'high',
1243
+ status: 'pending',
1244
+ created: ts(),
1245
+ createdBy: 'engine:eval-loop',
1246
+ project: meta.project?.name || meta.item.project,
1247
+ branch_name: parentItem?.branch_name || meta.branch || null,
1248
+ pr_url: parentItem?.pr_url || null,
1249
+ acceptance_criteria: parentItem?.acceptance_criteria || meta.item.acceptance_criteria || null,
1250
+ _evalParentId: meta.item.id,
1251
+ };
1252
+ if (parentItem?.sourcePlan) evalItem.sourcePlan = parentItem.sourcePlan;
1253
+ // Mark parent as eval-dispatched before writing to prevent duplicates on re-run
1254
+ if (parentItem) parentItem._evalDispatched = true;
1255
+ items.push(evalItem);
1256
+ shared.safeWrite(wiPath, items);
1257
+ log('info', `Eval loop: created ${evalItem.id} for completed implement ${meta.item.id}`);
1258
+ }
1259
+ }
1260
+ } catch (err) {
1261
+ log('warn', `Eval loop dispatch error: ${err.message}`);
1262
+ }
1263
+ }
1264
+ }
1265
+
1266
+ // Evaluate completion: parse verdict and handle eval→fix iteration loop
1267
+ if (isSuccess && type === 'evaluate' && meta?.item?._evalParentId) {
1268
+ try {
1269
+ const verdict = parseEvalVerdict(resultSummary || stdout);
1270
+ const evalLoop = config.engine?.evalLoop ?? shared.ENGINE_DEFAULTS.evalLoop;
1271
+ const maxIter = config.engine?.evalMaxIterations ?? shared.ENGINE_DEFAULTS.evalMaxIterations;
1272
+
1273
+ if (verdict && !verdict.pass && evalLoop) {
1274
+ const wiPath = resolveWiPath(meta);
1275
+ if (wiPath) {
1276
+ const items = safeJson(wiPath) || [];
1277
+ const parent = items.find(i => i.id === meta.item._evalParentId);
1278
+ if (parent) {
1279
+ const iterations = (parent._evalIterations || 0) + 1;
1280
+ parent._evalIterations = iterations;
1281
+
1282
+ if (iterations >= maxIter) {
1283
+ // Max iterations reached — escalate to human review
1284
+ parent.status = 'needs-human-review';
1285
+ parent._evalEscalatedAt = ts();
1286
+ shared.safeWrite(wiPath, items);
1287
+ log('info', `Eval loop: ${parent.id} reached ${iterations}/${maxIter} iterations — escalated to needs-human-review`);
1288
+ } else {
1289
+ // Create fix work item with evaluator feedback
1290
+ const fixItem = {
1291
+ id: 'W-' + shared.uid(),
1292
+ title: `Fix: ${parent.title || parent.id} (eval iteration ${iterations})`,
1293
+ type: 'fix',
1294
+ priority: parent.priority || 'high',
1295
+ status: 'pending',
1296
+ created: ts(),
1297
+ createdBy: 'engine:eval-loop',
1298
+ project: meta.project?.name || meta.item.project,
1299
+ branch_name: parent.branch_name || meta.item.branch_name || null,
1300
+ pr_url: parent.pr_url || meta.item.pr_url || null,
1301
+ acceptance_criteria: parent.acceptance_criteria || null,
1302
+ _evalParentId: parent.id,
1303
+ _evalFeedback: verdict.feedback || null,
1304
+ _evalCriteriaFailed: verdict.criteria_failed || null,
1305
+ };
1306
+ if (parent.sourcePlan) fixItem.sourcePlan = parent.sourcePlan;
1307
+ // Clear eval-dispatched flag so next fix→eval cycle can dispatch
1308
+ parent._evalDispatched = false;
1309
+ // Parent stays 'done' — fix item picks up from here
1310
+ parent.status = 'done';
1311
+ items.push(fixItem);
1312
+ shared.safeWrite(wiPath, items);
1313
+ log('info', `Eval loop: created fix ${fixItem.id} for failed eval on ${parent.id} (iteration ${iterations}/${maxIter})`);
1314
+ }
1315
+ }
1316
+ }
1317
+ }
1318
+ } catch (err) {
1319
+ log('warn', `Eval verdict processing error: ${err.message}`);
1320
+ }
1321
+ }
1322
+
1148
1323
  if (!isSuccess && meta?.item?.id) {
1149
1324
  // Auto-retry: read fresh _retryCount from file (not stale dispatch-time snapshot)
1150
1325
  let retries = (meta.item._retryCount || 0);
@@ -1346,6 +1521,7 @@ module.exports = {
1346
1521
  updateMetrics,
1347
1522
  recordContextPressureOnWorkItem,
1348
1523
  parseAgentOutput,
1524
+ parseEvalVerdict,
1349
1525
  runPostCompletionHooks,
1350
1526
  syncPrdFromPrs,
1351
1527
  };
package/engine/shared.js CHANGED
@@ -356,6 +356,9 @@ const ENGINE_DEFAULTS = {
356
356
  autoApprovePlans: false, // auto-approve PRDs without waiting for human approval
357
357
  autoReview: true, // auto-dispatch review agents for new PRs (disable for manual review workflow)
358
358
  meetingRoundTimeout: 600000, // 10min per meeting round before auto-advance
359
+ evalLoop: true, // enable evaluate→fix loop after implementation completes
360
+ evalMaxIterations: 3, // max evaluate→fix cycles before escalating to human
361
+ evalMaxCost: null, // USD ceiling per work item across all eval iterations; null = no limit (gather baseline data first)
359
362
  };
360
363
 
361
364
  const DEFAULT_AGENTS = {
package/engine.js CHANGED
@@ -1474,6 +1474,38 @@ function discoverFromWorkItems(config, project) {
1474
1474
  const ac = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
1475
1475
  vars.acceptance_criteria = ac ? '## Acceptance Criteria\n\n' + ac : '';
1476
1476
 
1477
+ // Inject checkpoint context if agent left a checkpoint.json from a prior run
1478
+ vars.checkpoint_context = '';
1479
+ try {
1480
+ const wtPath = vars.worktree_path || root;
1481
+ const cpPath = path.join(wtPath, 'checkpoint.json');
1482
+ if (fs.existsSync(cpPath)) {
1483
+ const cpData = JSON.parse(fs.readFileSync(cpPath, 'utf8'));
1484
+ const cpCount = (item._checkpointCount || 0) + 1;
1485
+ if (cpCount > 3) {
1486
+ log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
1487
+ item.status = 'needs-human-review';
1488
+ item._checkpointCount = cpCount;
1489
+ needsWrite = true;
1490
+ continue;
1491
+ }
1492
+ item._checkpointCount = cpCount;
1493
+ needsWrite = true;
1494
+ const cpSummary = [
1495
+ `## Checkpoint (Resume #${cpCount}/3)`,
1496
+ '',
1497
+ 'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
1498
+ '',
1499
+ cpData.completed && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
1500
+ cpData.remaining && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
1501
+ cpData.blockers && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
1502
+ cpData.branch_state ? `### Branch State\n${cpData.branch_state}` : '',
1503
+ ].filter(Boolean).join('\n');
1504
+ vars.checkpoint_context = cpSummary;
1505
+ log('info', `Injecting checkpoint context for ${item.id} (resume #${cpCount})`);
1506
+ }
1507
+ } catch (e) { log('warn', `checkpoint read for ${item.id}: ${e.message}`); }
1508
+
1477
1509
  // Inject ask-specific variables for the ask playbook
1478
1510
  if (workType === 'ask') {
1479
1511
  vars.question = item.title + (item.description ? '\n\n' + item.description : '');
@@ -1859,6 +1891,39 @@ function discoverCentralWorkItems(config) {
1859
1891
  const normAc = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
1860
1892
  vars.acceptance_criteria = normAc ? '## Acceptance Criteria\n\n' + normAc : '';
1861
1893
 
1894
+ // Inject checkpoint context if agent left a checkpoint.json from a prior run
1895
+ vars.checkpoint_context = '';
1896
+ try {
1897
+ const centralBranch = item.branch || `work/${item.id}`;
1898
+ const centralWtPath = firstProject?.localPath
1899
+ ? path.resolve(firstProject.localPath, config.engine?.worktreeRoot || '../worktrees', centralBranch)
1900
+ : '';
1901
+ const cpPath = centralWtPath ? path.join(centralWtPath, 'checkpoint.json') : '';
1902
+ if (cpPath && fs.existsSync(cpPath)) {
1903
+ const cpData = JSON.parse(fs.readFileSync(cpPath, 'utf8'));
1904
+ const cpCount = (item._checkpointCount || 0) + 1;
1905
+ if (cpCount > 3) {
1906
+ log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
1907
+ item.status = 'needs-human-review';
1908
+ item._checkpointCount = cpCount;
1909
+ continue;
1910
+ }
1911
+ item._checkpointCount = cpCount;
1912
+ const cpSummary = [
1913
+ `## Checkpoint (Resume #${cpCount}/3)`,
1914
+ '',
1915
+ 'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
1916
+ '',
1917
+ cpData.completed && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
1918
+ cpData.remaining && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
1919
+ cpData.blockers && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
1920
+ cpData.branch_state ? `### Branch State\n${cpData.branch_state}` : '',
1921
+ ].filter(Boolean).join('\n');
1922
+ vars.checkpoint_context = cpSummary;
1923
+ log('info', `Injecting checkpoint context for ${item.id} (resume #${cpCount})`);
1924
+ }
1925
+ } catch (e) { log('warn', `checkpoint read for ${item.id}: ${e.message}`); }
1926
+
1862
1927
  // Inject plan-specific variables for the plan playbook
1863
1928
  if (workType === 'plan') {
1864
1929
  // Ensure plans directory exists before agent tries to write
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.145",
3
+ "version": "0.1.147",
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"
@@ -0,0 +1,114 @@
1
+ # Evaluate: {{item_name}}
2
+
3
+ > Agent: {{agent_name}} ({{agent_role}}) | Team root: {{team_root}}
4
+
5
+ ## Context
6
+
7
+ Project: {{project_name}}
8
+ Repo: {{repo_name}} | Org: {{ado_org}} | ADO Project: {{ado_project}}
9
+ PR: {{pr_url}}
10
+ Work Item: {{item_id}}
11
+
12
+ ## Acceptance Criteria
13
+
14
+ {{acceptance_criteria}}
15
+
16
+ ## Task Description
17
+
18
+ {{task_description}}
19
+
20
+ ## Your Task
21
+
22
+ You are the **Evaluator** in the Planner-Generator-Evaluator pattern. Your job is to independently verify whether the implementation in the PR branch meets the acceptance criteria. You are NOT the implementer — you are the skeptic.
23
+
24
+ **Mindset: Do not pass unless build succeeds AND all acceptance criteria are demonstrably met.** Assume the implementation is incomplete or wrong until proven otherwise. Look for edge cases, missing requirements, and silent failures.
25
+
26
+ ## Step 1: Check Out the PR Branch
27
+
28
+ ```bash
29
+ cd {{project_path}}
30
+ git fetch origin
31
+ git checkout {{branch_name}}
32
+ git pull origin {{branch_name}}
33
+ ```
34
+
35
+ ## Step 2: Build
36
+
37
+ Run the project build. Check `CLAUDE.md`, `package.json`, or `README` for build instructions.
38
+
39
+ ```bash
40
+ # Typical:
41
+ npm install && npm run build
42
+ # Or whatever the project uses
43
+ ```
44
+
45
+ Record: **PASS** or **FAIL** with error output.
46
+
47
+ If the build fails, **stop here** — the verdict is `pass: false`. Include the build error in feedback.
48
+
49
+ ## Step 3: Run Tests
50
+
51
+ Run the full test suite:
52
+
53
+ ```bash
54
+ npm test
55
+ ```
56
+
57
+ Record: **X passed / Y failed / Z skipped**.
58
+
59
+ If any tests fail, note which ones and whether they are related to the changes.
60
+
61
+ ## Step 4: Diff Review Against Acceptance Criteria
62
+
63
+ Review the actual code changes:
64
+
65
+ ```bash
66
+ git diff {{main_branch}}...{{branch_name}} --stat
67
+ git diff {{main_branch}}...{{branch_name}}
68
+ ```
69
+
70
+ For **each** acceptance criterion, determine:
71
+ - **Met**: The diff demonstrably satisfies this criterion. Cite the specific file/line.
72
+ - **Not met**: The diff does not satisfy this criterion, or satisfies it only partially. Explain what's missing.
73
+
74
+ Be precise. "Looks good" is not an evaluation — cite file paths and line numbers.
75
+
76
+ ## Step 5: Output Structured Verdict
77
+
78
+ After completing your evaluation, output the following JSON block as your final output. This MUST be valid JSON wrapped in a `json` fenced code block:
79
+
80
+ ```json
81
+ {
82
+ "pass": false,
83
+ "build": true,
84
+ "tests": "42/42",
85
+ "criteria_met": [
86
+ "criterion 1 — met because X (source: path/to/file.js:42)"
87
+ ],
88
+ "criteria_failed": [
89
+ "criterion 2 — not met because Y is missing"
90
+ ],
91
+ "feedback": "Summary of what needs to change for this to pass. Be specific — file names, line numbers, what to add/fix."
92
+ }
93
+ ```
94
+
95
+ Field definitions:
96
+ - `pass`: `true` only if build succeeds AND **all** acceptance criteria are met. Otherwise `false`.
97
+ - `build`: `true` if the build completed without errors, `false` otherwise.
98
+ - `tests`: String in format `"passed/total"` (e.g., `"38/40"`). Use `"N/A"` if no test suite exists.
99
+ - `criteria_met`: Array of strings — one per criterion that IS met. Include source references.
100
+ - `criteria_failed`: Array of strings — one per criterion that is NOT met. Explain why.
101
+ - `feedback`: Actionable feedback for the implementer. Be specific about what to fix. If `pass` is `true`, use this for minor suggestions or "LGTM".
102
+
103
+ ## Rules
104
+
105
+ - **No Playwright / browser testing** — this phase evaluates build, tests, and code review only.
106
+ - **Do NOT fix code** — only evaluate and report. You are the evaluator, not the implementer.
107
+ - **Do NOT rubber-stamp** — if a criterion is ambiguous, evaluate conservatively (fail it and explain).
108
+ - **Build failure is an automatic fail** — do not evaluate criteria if the build doesn't pass.
109
+ - **Every criterion must be addressed** — `criteria_met` + `criteria_failed` should cover all acceptance criteria.
110
+ - **Cite sources** — reference file paths and line numbers for every met/failed criterion.
111
+
112
+ {{references}}
113
+
114
+ **Note:** Do NOT write to `agents/*/status.json` — the engine manages your status automatically.
package/playbooks/fix.md CHANGED
@@ -11,6 +11,8 @@ Repo: {{repo_name}} | Org: {{ado_org}} | Project: {{ado_project}}
11
11
  Fix issues found by {{reviewer}} on **{{pr_id}}**: {{pr_title}}
12
12
  Branch: `{{pr_branch}}`
13
13
 
14
+ {{checkpoint_context}}
15
+
14
16
  ## Review Findings to Address
15
17
 
16
18
  {{review_note}}
@@ -18,6 +18,8 @@ Implement PRD item **{{item_id}}: {{item_name}}**
18
18
  - Complexity: {{item_complexity}}
19
19
  - Description: {{item_description}}
20
20
 
21
+ {{checkpoint_context}}
22
+
21
23
  ## Projects
22
24
 
23
25
  Primary repo: **{{repo_name}}** ({{ado_org}}/{{ado_project}}) at `{{project_path}}`
package/routing.md CHANGED
@@ -17,6 +17,7 @@ How the engine decides who handles what. Parsed by engine.js — keep the table
17
17
  | test | dallas | ralph |
18
18
  | ask | ripley | rebecca |
19
19
  | verify | dallas | ralph |
20
+ | evaluate | ripley | lambert |
20
21
  | decompose | ripley | rebecca |
21
22
  | meeting | ripley | rebecca |
22
23