@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 +6 -1
- package/engine/ado.js +21 -18
- package/engine/cooldown.js +1 -51
- package/engine/github.js +16 -2
- package/engine/lifecycle.js +1 -1
- package/engine/pipeline.js +2 -0
- package/engine/playbook.js +3 -38
- package/engine/routing.js +1 -1
- package/engine.js +75 -5
- package/package.json +1 -1
- package/playbooks/evaluate.md +114 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
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
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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 —
|
|
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
|
-
|
|
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}`);
|
package/engine/cooldown.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/engine/lifecycle.js
CHANGED
|
@@ -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: `
|
|
1280
|
+
title: `Review: ${parentItem?.title || meta.item.title || evalTargetId}`,
|
|
1281
1281
|
type: 'review',
|
|
1282
1282
|
priority: meta.item.priority || 'high',
|
|
1283
1283
|
status: 'pending',
|
package/engine/pipeline.js
CHANGED
|
@@ -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
|
});
|
package/engine/playbook.js
CHANGED
|
@@ -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
|
|
300
|
-
|
|
301
|
-
|
|
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,
|
|
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 (
|
|
294
|
-
//
|
|
295
|
-
|
|
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.
|
|
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.
|