@yemi33/minions 0.1.288 → 0.1.290

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,14 +1,19 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.288 (2026-04-03)
3
+ ## 0.1.290 (2026-04-03)
4
4
 
5
5
  ### Features
6
6
  - all doc-chats use Sonnet with full tools (agent change)
7
7
 
8
8
  ### Fixes
9
+ - enforce worktree isolation — 4 code paths fixed
10
+ - ' not 'Evaluate:'
9
11
  - cross-platform compatibility — signal handling, paths, home dir
10
12
  - engine sidebar badge only triggers on new dispatch errors
11
13
 
14
+ ### Other
15
+ - resolve merge conflicts — accept agent changes, keep worktree isolation fix
16
+
12
17
  ## 0.1.285 (2026-04-03)
13
18
 
14
19
  ### Fixes
package/engine/ado.js CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  const path = require('path');
7
7
  const shared = require('./shared');
8
- const { exec, getAdoOrgBase, log, dateStamp } = shared;
8
+ const { exec, getAdoOrgBase, addPrLink, log, dateStamp } = shared;
9
9
  const { getPrs } = require('./queries');
10
10
 
11
11
  // Lazy require to avoid circular dependency — only needed for engine().handlePostMerge
@@ -29,7 +29,7 @@ function getAdoToken() {
29
29
  try {
30
30
  // azureauth supports multiple --mode flags as an ordered fallback chain:
31
31
  // tries IWA (Integrated Windows Auth) first, falls back to broker if unavailable.
32
- const token = exec('azureauth ado token --mode broker --mode iwa --output token --timeout 5', {
32
+ const token = exec('azureauth ado token --mode iwa --mode broker --output token --timeout 1', {
33
33
  timeout: 15000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }).trim();
34
34
  if (token && token.startsWith('eyJ')) {
35
35
  _adoTokenCache = { token, expiresAt: Date.now() + 30 * 60 * 1000 };
@@ -46,20 +46,9 @@ function getAdoToken() {
46
46
 
47
47
  async function adoFetch(url, token, _retryCount = 0) {
48
48
  const MAX_RETRIES = 1;
49
- const controller = new AbortController();
50
- const timer = setTimeout(() => controller.abort(), 30000);
51
- let res;
52
- try {
53
- res = await fetch(url, {
54
- signal: controller.signal,
55
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
56
- });
57
- } catch (err) {
58
- clearTimeout(timer);
59
- if (err.name === 'AbortError') throw new Error(`ADO API timeout (30s) for ${url.split('?')[0]}`);
60
- throw err;
61
- }
62
- clearTimeout(timer);
49
+ const res = await fetch(url, {
50
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
51
+ });
63
52
  if (!res.ok) throw new Error(`ADO API ${res.status}: ${res.statusText}`);
64
53
  const text = await res.text();
65
54
  if (!text || text.trimStart().startsWith('<')) {
@@ -366,8 +355,9 @@ async function reconcilePrs(config) {
366
355
  const confirmedItemId = linkedItem ? linkedItemId : null;
367
356
 
368
357
  if (existingIds.has(prId)) {
369
- // PR already tracked — update prdItems if we can extract an ID
358
+ // PR already tracked — write link to pr-links.json if we can extract an ID
370
359
  if (confirmedItemId) {
360
+ addPrLink(prId, confirmedItemId);
371
361
  const existing = existingPrs.find(p => p.id === prId);
372
362
  if (existing && !(existing.prdItems || []).includes(confirmedItemId)) {
373
363
  existing.prdItems = Array.isArray(existing.prdItems) ? existing.prdItems : [];
@@ -390,12 +380,25 @@ async function reconcilePrs(config) {
390
380
  url: prUrl,
391
381
  prdItems: confirmedItemId ? [confirmedItemId] : [],
392
382
  });
383
+ if (confirmedItemId) addPrLink(prId, confirmedItemId);
393
384
  existingIds.add(prId);
394
385
  projectAdded++;
395
386
  log('info', `PR reconciliation: added ${prId} (branch: ${branch}${confirmedItemId ? ', linked to ' + confirmedItemId : ''}) to ${project.name}`);
396
387
  }
397
388
 
398
- if (projectAdded > 0 || projectUpdated > 0) {
389
+ // Backfill prdItems from pr-links for any PR with empty array
390
+ const prLinks = shared.getPrLinks();
391
+ let backfilled = 0;
392
+ for (const pr of existingPrs) {
393
+ const linked = prLinks[pr.id];
394
+ if (linked && !(pr.prdItems || []).includes(linked)) {
395
+ pr.prdItems = Array.isArray(pr.prdItems) ? pr.prdItems : [];
396
+ pr.prdItems.push(linked);
397
+ backfilled++;
398
+ }
399
+ }
400
+
401
+ if (projectAdded > 0 || projectUpdated > 0 || backfilled > 0) {
399
402
  shared.safeWrite(prPath, existingPrs);
400
403
  totalAdded += projectAdded;
401
404
  if (projectUpdated > 0) log('info', `PR reconciliation: linked ${projectUpdated} existing PR(s) to PRD items in ${project.name}`);
@@ -7,12 +7,10 @@ const path = require('path');
7
7
  const shared = require('./shared');
8
8
  const queries = require('./queries');
9
9
 
10
- const { createHash } = require('crypto');
11
10
  const { safeJson, safeWrite, log } = shared;
12
11
  const { ENGINE_DIR } = queries;
13
12
 
14
13
  const COOLDOWN_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
15
- const PENDING_CONTEXTS_CAP = 10;
16
14
  const dispatchCooldowns = new Map(); // key → { timestamp, failures }
17
15
 
18
16
  function loadCooldowns() {
@@ -26,35 +24,6 @@ function loadCooldowns() {
26
24
  }
27
25
  }
28
26
  log('info', `Loaded ${dispatchCooldowns.size} cooldowns from disk`);
29
- // One-time purge of bloated pendingContexts on startup
30
- purgeBloatedCooldowns();
31
- }
32
-
33
- /** Deduplicate and cap pendingContexts in all loaded cooldown entries. */
34
- function purgeBloatedCooldowns() {
35
- let totalRemoved = 0;
36
- for (const [k, v] of dispatchCooldowns) {
37
- if (!Array.isArray(v.pendingContexts) || v.pendingContexts.length <= 1) continue;
38
- const seen = new Set();
39
- const deduped = [];
40
- for (const ctx of v.pendingContexts) {
41
- const hash = _contentHash(ctx);
42
- if (!seen.has(hash)) {
43
- seen.add(hash);
44
- deduped.push(ctx);
45
- }
46
- }
47
- const before = v.pendingContexts.length;
48
- // Apply FIFO cap after dedup — keep the most recent entries
49
- v.pendingContexts = deduped.length > PENDING_CONTEXTS_CAP
50
- ? deduped.slice(deduped.length - PENDING_CONTEXTS_CAP)
51
- : deduped;
52
- totalRemoved += before - v.pendingContexts.length;
53
- }
54
- if (totalRemoved > 0) {
55
- log('info', `Purged ${totalRemoved} duplicate/excess pendingContexts entries from cooldowns`);
56
- saveCooldowns();
57
- }
58
27
  }
59
28
 
60
29
  let _cooldownWriteTimer = null;
@@ -86,26 +55,10 @@ function setCooldown(key) {
86
55
  saveCooldowns();
87
56
  }
88
57
 
89
- function _contentHash(content) {
90
- const str = typeof content === 'string' ? content : JSON.stringify(content);
91
- return createHash('sha256').update(str).digest('hex');
92
- }
93
-
94
58
  function setCooldownWithContext(key, context) {
95
59
  const existing = dispatchCooldowns.get(key);
96
60
  const pendingContexts = existing?.pendingContexts || [];
97
- if (context) {
98
- // Dedup: only append if content differs from all existing entries
99
- const newHash = _contentHash(context);
100
- const isDuplicate = pendingContexts.some(c => _contentHash(c) === newHash);
101
- if (!isDuplicate) {
102
- pendingContexts.push(context);
103
- // FIFO cap: drop oldest entries when exceeding cap
104
- while (pendingContexts.length > PENDING_CONTEXTS_CAP) {
105
- pendingContexts.shift();
106
- }
107
- }
108
- }
61
+ if (context) pendingContexts.push(context);
109
62
  dispatchCooldowns.set(key, {
110
63
  timestamp: Date.now(),
111
64
  failures: existing?.failures || 0,
@@ -148,16 +101,13 @@ function isAlreadyDispatched(key) {
148
101
 
149
102
  module.exports = {
150
103
  COOLDOWN_PATH,
151
- PENDING_CONTEXTS_CAP,
152
104
  dispatchCooldowns,
153
105
  loadCooldowns,
154
106
  saveCooldowns,
155
- purgeBloatedCooldowns,
156
107
  isOnCooldown,
157
108
  setCooldown,
158
109
  setCooldownWithContext,
159
110
  getCoalescedContexts,
160
111
  setCooldownFailure,
161
112
  isAlreadyDispatched,
162
- _contentHash,
163
113
  };
package/engine/github.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  const shared = require('./shared');
8
- const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, MINIONS_DIR, log, dateStamp } = 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
 
@@ -341,6 +341,7 @@ async function reconcilePrs(config) {
341
341
 
342
342
  if (existingIds.has(prId)) {
343
343
  if (confirmedItemId) {
344
+ addPrLink(prId, confirmedItemId);
344
345
  const existing = existingPrs.find(p => p.id === prId);
345
346
  if (existing && !(existing.prdItems || []).includes(confirmedItemId)) {
346
347
  existing.prdItems = Array.isArray(existing.prdItems) ? existing.prdItems : [];
@@ -363,13 +364,26 @@ async function reconcilePrs(config) {
363
364
  url: prUrl,
364
365
  prdItems: confirmedItemId ? [confirmedItemId] : [],
365
366
  });
367
+ if (confirmedItemId) addPrLink(prId, confirmedItemId);
366
368
  existingIds.add(prId);
367
369
  projectAdded++;
368
370
 
369
371
  log('info', `GitHub PR reconciliation: added ${prId} (branch: ${branch}${confirmedItemId ? ', linked to ' + confirmedItemId : ''}) to ${project.name}`);
370
372
  }
371
373
 
372
- if (projectAdded > 0) {
374
+ // Backfill prdItems from pr-links for any PR with empty array
375
+ const prLinks = getPrLinks();
376
+ let backfilled = 0;
377
+ for (const pr of existingPrs) {
378
+ const linked = prLinks[pr.id];
379
+ if (linked && !(pr.prdItems || []).includes(linked)) {
380
+ pr.prdItems = Array.isArray(pr.prdItems) ? pr.prdItems : [];
381
+ pr.prdItems.push(linked);
382
+ backfilled++;
383
+ }
384
+ }
385
+
386
+ if (projectAdded > 0 || backfilled > 0) {
373
387
  safeWrite(prPath, existingPrs);
374
388
  totalAdded += projectAdded;
375
389
  }
@@ -1277,7 +1277,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1277
1277
  const parentItem = items.find(i => i.id === evalTargetId);
1278
1278
  const evalItem = {
1279
1279
  id: 'W-' + shared.uid(),
1280
- title: `Evaluate: ${parentItem?.title || meta.item.title || evalTargetId}`,
1280
+ title: `Review: ${parentItem?.title || meta.item.title || evalTargetId}`,
1281
1281
  type: 'review',
1282
1282
  priority: meta.item.priority || 'high',
1283
1283
  status: 'pending',
@@ -175,6 +175,7 @@ function executeTaskStage(stage, stageState, run, config) {
175
175
  status: 'pending',
176
176
  created: ts(),
177
177
  createdBy: 'pipeline:' + run.pipelineId,
178
+ branch: `pipeline/${run.pipelineId}/${stage.id}`,
178
179
  _pipelineRun: run.runId,
179
180
  _pipelineStage: stage.id,
180
181
  });
@@ -248,6 +249,7 @@ function executePlanStage(stage, stageState, run, config) {
248
249
  planFile: path.basename(filePath),
249
250
  created: ts(),
250
251
  createdBy: 'pipeline:' + run.pipelineId,
252
+ branch: `pipeline/${run.pipelineId}/${stage.id}`,
251
253
  _pipelineRun: run.runId,
252
254
  _pipelineStage: stage.id,
253
255
  });
@@ -206,25 +206,9 @@ 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
-
224
209
  // ─── Playbook Renderer ──────────────────────────────────────────────────────
225
210
 
226
211
  function renderPlaybook(type, vars) {
227
- _lastRenderError = null;
228
212
  const pbPath = path.join(PLAYBOOKS_DIR, `${type}.md`);
229
213
  let content;
230
214
  try { content = fs.readFileSync(pbPath, 'utf8'); } catch {
@@ -296,12 +280,9 @@ function renderPlaybook(type, vars) {
296
280
  };
297
281
  const allVars = { ...projectVars, ...vars };
298
282
 
299
- // Substitute variables — two passes to resolve nested templates
300
- // (e.g. pr_section contains {{pr_create_instructions}}, {{branch_name}}, etc.)
301
- for (let pass = 0; pass < 2; pass++) {
302
- for (const [key, val] of Object.entries(allVars)) {
303
- content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(val));
304
- }
283
+ // Substitute variables
284
+ for (const [key, val] of Object.entries(allVars)) {
285
+ content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(val));
305
286
  }
306
287
 
307
288
  // Warn on variables that resolved to empty string
@@ -318,20 +299,6 @@ function renderPlaybook(type, vars) {
318
299
  log('warn', `Playbook "${type}": unresolved template variables: ${unresolved.join(', ')}`);
319
300
  }
320
301
 
321
- // Block dispatch if critical variables are empty or unresolved
322
- const criticalVars = CRITICAL_VARS[type] || [];
323
- if (criticalVars.length > 0) {
324
- const emptySet = new Set(emptyVars);
325
- const unresolvedSet = new Set(unresolved);
326
- const criticalMissing = criticalVars.filter(v => emptySet.has(v) || unresolvedSet.has(v));
327
- if (criticalMissing.length > 0) {
328
- const msg = `Playbook "${type}": critical variables empty or unresolved: ${criticalMissing.join(', ')} — blocking dispatch`;
329
- log('warn', msg);
330
- _lastRenderError = { reason: 'critical_vars_missing', vars: criticalMissing, message: msg };
331
- return null;
332
- }
333
- }
334
-
335
302
  return content;
336
303
  }
337
304
 
@@ -503,8 +470,6 @@ function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabe
503
470
 
504
471
  module.exports = {
505
472
  renderPlaybook,
506
- getLastRenderError,
507
- CRITICAL_VARS,
508
473
  buildSystemPrompt,
509
474
  buildAgentContext,
510
475
  selectPlaybook,
package/engine/routing.js CHANGED
@@ -136,7 +136,7 @@ function resolveAgent(workType, config, authorAgent = null) {
136
136
  if (config.engine?.allowTempAgents) {
137
137
  const tempId = `temp-${shared.uid()}`;
138
138
  _claimedAgents.add(tempId);
139
- tempAgents.set(tempId, { name: `Temp-${tempId.slice(5, 13)}`, role: 'Temporary Agent', createdAt: ts() });
139
+ tempAgents.set(tempId, { name: `Temp-${tempId.slice(5, 9)}`, role: 'Temporary Agent', createdAt: ts() });
140
140
  log('info', `Spawning temp agent ${tempId} — all permanent agents busy`);
141
141
  return tempId;
142
142
  }
package/engine.js CHANGED
@@ -290,10 +290,9 @@ function spawnAgent(dispatchItem, config) {
290
290
  log('info', `Reusing existing worktree for ${branchName}: ${existingWt}`);
291
291
  try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
292
292
  try { exec(`git pull origin "${branchName}"`, { ..._gitOpts, cwd: existingWt }); } catch (e) { log('warn', 'git: ' + e.message); }
293
- } else if (type !== 'implement') {
294
- // Only implement tasks may create new worktrees.
295
- // Other task types are reuse-only: if no existing worktree, run in rootDir.
296
- log('info', `${type}: no existing worktree for ${branchName} — creation disabled for non-implement tasks, falling back to rootDir`);
293
+ } else if (['meeting', 'ask', 'explore'].includes(type)) {
294
+ // Read-only tasks no worktree needed, run in rootDir
295
+ log('info', `${type}: read-only task, no worktree needed — running in rootDir`);
297
296
  branchName = null;
298
297
  worktreePath = null;
299
298
  } else {
@@ -423,6 +422,11 @@ function spawnAgent(dispatchItem, config) {
423
422
  const systemPrompt = buildSystemPrompt(agentId, config, project);
424
423
  const agentContext = buildAgentContext(agentId, config, project);
425
424
 
425
+ // Safety check: warn if a write-capable task is running in the main repo without a worktree
426
+ if (cwd === rootDir && ['implement', 'implement:large', 'fix', 'test', 'verify', 'plan-to-prd'].includes(type)) {
427
+ log('warn', `Agent ${agentId} running ${type} task in main repo (no worktree) for ${id} — changes may land on master directly`);
428
+ }
429
+
426
430
  // Prepend bulk context to task prompt — keeps system prompt small and stable
427
431
  const fullTaskPrompt = agentContext
428
432
  ? `## Agent Context\n\n${agentContext}\n---\n\n## Your Task\n\n${taskPrompt}`
@@ -1475,6 +1479,38 @@ function discoverFromWorkItems(config, project) {
1475
1479
  const ac = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
1476
1480
  vars.acceptance_criteria = ac ? '## Acceptance Criteria\n\n' + ac : '';
1477
1481
 
1482
+ // Inject checkpoint context if agent left a checkpoint.json from a prior run
1483
+ vars.checkpoint_context = '';
1484
+ try {
1485
+ const wtPath = vars.worktree_path || root;
1486
+ const cpPath = path.join(wtPath, 'checkpoint.json');
1487
+ if (fs.existsSync(cpPath)) {
1488
+ const cpData = JSON.parse(fs.readFileSync(cpPath, 'utf8'));
1489
+ const cpCount = (item._checkpointCount || 0) + 1;
1490
+ if (cpCount > 3) {
1491
+ log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
1492
+ item.status = 'needs-human-review';
1493
+ item._checkpointCount = cpCount;
1494
+ needsWrite = true;
1495
+ continue;
1496
+ }
1497
+ item._checkpointCount = cpCount;
1498
+ needsWrite = true;
1499
+ const cpSummary = [
1500
+ `## Checkpoint (Resume #${cpCount}/3)`,
1501
+ '',
1502
+ 'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
1503
+ '',
1504
+ cpData.completed && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
1505
+ cpData.remaining && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
1506
+ cpData.blockers && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
1507
+ cpData.branch_state ? `### Branch State\n${cpData.branch_state}` : '',
1508
+ ].filter(Boolean).join('\n');
1509
+ vars.checkpoint_context = cpSummary;
1510
+ log('info', `Injecting checkpoint context for ${item.id} (resume #${cpCount})`);
1511
+ }
1512
+ } catch (e) { log('warn', `checkpoint read for ${item.id}: ${e.message}`); }
1513
+
1478
1514
  // Inject ask-specific variables for the ask playbook
1479
1515
  if (workType === 'ask') {
1480
1516
  vars.question = item.title + (item.description ? '\n\n' + item.description : '');
@@ -1798,6 +1834,7 @@ function discoverCentralWorkItems(config) {
1798
1834
  prompt,
1799
1835
  meta: {
1800
1836
  dispatchKey: fanKey, source: 'central-work-item-fanout', item, parentKey: key,
1837
+ branch: `fan/${item.id}/${fanAgentId}`,
1801
1838
  deadline: item.timeout ? Date.now() + item.timeout : Date.now() + (config.engine?.fanOutTimeout || config.engine?.agentTimeout || DEFAULTS.agentTimeout)
1802
1839
  }
1803
1840
  });
@@ -1845,6 +1882,39 @@ function discoverCentralWorkItems(config) {
1845
1882
  const normAc = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
1846
1883
  vars.acceptance_criteria = normAc ? '## Acceptance Criteria\n\n' + normAc : '';
1847
1884
 
1885
+ // Inject checkpoint context if agent left a checkpoint.json from a prior run
1886
+ vars.checkpoint_context = '';
1887
+ try {
1888
+ const centralBranch = item.branch || `work/${item.id}`;
1889
+ const centralWtPath = firstProject?.localPath
1890
+ ? path.resolve(firstProject.localPath, config.engine?.worktreeRoot || '../worktrees', centralBranch)
1891
+ : '';
1892
+ const cpPath = centralWtPath ? path.join(centralWtPath, 'checkpoint.json') : '';
1893
+ if (cpPath && fs.existsSync(cpPath)) {
1894
+ const cpData = JSON.parse(fs.readFileSync(cpPath, 'utf8'));
1895
+ const cpCount = (item._checkpointCount || 0) + 1;
1896
+ if (cpCount > 3) {
1897
+ log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
1898
+ item.status = 'needs-human-review';
1899
+ item._checkpointCount = cpCount;
1900
+ continue;
1901
+ }
1902
+ item._checkpointCount = cpCount;
1903
+ const cpSummary = [
1904
+ `## Checkpoint (Resume #${cpCount}/3)`,
1905
+ '',
1906
+ 'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
1907
+ '',
1908
+ cpData.completed && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
1909
+ cpData.remaining && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
1910
+ cpData.blockers && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
1911
+ cpData.branch_state ? `### Branch State\n${cpData.branch_state}` : '',
1912
+ ].filter(Boolean).join('\n');
1913
+ vars.checkpoint_context = cpSummary;
1914
+ log('info', `Injecting checkpoint context for ${item.id} (resume #${cpCount})`);
1915
+ }
1916
+ } catch (e) { log('warn', `checkpoint read for ${item.id}: ${e.message}`); }
1917
+
1848
1918
  // Inject plan-specific variables for the plan playbook
1849
1919
  if (workType === 'plan') {
1850
1920
  // Ensure plans directory exists before agent tries to write
@@ -1909,7 +1979,7 @@ function discoverCentralWorkItems(config) {
1909
1979
  agentRole,
1910
1980
  task: item.title || item.description?.slice(0, 80) || item.id,
1911
1981
  prompt,
1912
- meta: { dispatchKey: key, source: 'central-work-item', item, planFileName: item.planFile || item._planFileName || null }
1982
+ meta: { dispatchKey: key, source: 'central-work-item', item, planFileName: item.planFile || item._planFileName || null, branch: item.branch || item.featureBranch || `work/${item.id}` }
1913
1983
  });
1914
1984
 
1915
1985
  item.status = 'dispatched';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.288",
3
+ "version": "0.1.290",
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.