@yemi33/minions 0.1.289 → 0.1.291
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/dashboard/js/render-plans.js +19 -4
- package/engine/ado.js +21 -18
- package/engine/cooldown.js +1 -51
- package/engine/github.js +16 -2
- package/engine/lifecycle.js +54 -19
- package/engine/pipeline.js +2 -0
- package/engine/playbook.js +3 -38
- package/engine/routing.js +1 -1
- package/engine.js +77 -5
- package/package.json +1 -1
- package/playbooks/evaluate.md +114 -0
- package/playbooks/verify.md +103 -68
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.291 (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
|
+
- defer plan archiving until verify completes, add 20 verify tests
|
|
10
|
+
- enforce worktree isolation — 4 code paths fixed
|
|
9
11
|
- ' not 'Evaluate:'
|
|
10
12
|
- cross-platform compatibility — signal handling, paths, home dir
|
|
11
13
|
- engine sidebar badge only triggers on new dispatch errors
|
|
12
14
|
|
|
15
|
+
### Other
|
|
16
|
+
- resolve merge conflicts — accept agent changes, keep worktree isolation fix
|
|
17
|
+
|
|
13
18
|
## 0.1.285 (2026-04-03)
|
|
14
19
|
|
|
15
20
|
### Fixes
|
|
@@ -227,7 +227,9 @@ function renderPlans(plans) {
|
|
|
227
227
|
'onclick="event.stopPropagation();planExecute(\'' + escHtml(p.file) + '\',\'' + escHtml(p.project) + '\',this)">Execute</button>' : '';
|
|
228
228
|
const showPause = effectiveStatus === 'in-progress' && prdFile && !isArchived;
|
|
229
229
|
const showResume = (effectiveStatus === 'paused' || effectiveStatus === 'awaiting-approval') && prdFile && !isArchived;
|
|
230
|
-
const
|
|
230
|
+
const verifyWi = allWi.find(w => w.itemType === 'verify' && w.sourcePlan === prdFile);
|
|
231
|
+
const hasVerifyWi = !!verifyWi;
|
|
232
|
+
const showVerify = effectiveStatus === 'completed' && prdFile && !isArchived && !hasVerifyWi;
|
|
231
233
|
const pauseBtn = showPause ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--yellow)" ' +
|
|
232
234
|
'onclick="event.stopPropagation();planPause(\'' + escHtml(prdFile) + '\',this)">Pause</button>' : '';
|
|
233
235
|
const resumeBtn = showResume
|
|
@@ -256,7 +258,7 @@ function renderPlans(plans) {
|
|
|
256
258
|
(p.updatedAt ? '<span title="Last updated: ' + p.updatedAt + '">Updated ' + timeAgo(p.updatedAt) + '</span>' : '') +
|
|
257
259
|
(p.completedAt ? '<span>' + p.completedAt.slice(0, 10) + '</span>' : '') +
|
|
258
260
|
(p.generatedBy ? '<span>by ' + escHtml(p.generatedBy) + '</span>' : '') +
|
|
259
|
-
executeBtn + pauseBtn + resumeBtn + verifyBtn + archiveBtn + deleteBtn +
|
|
261
|
+
executeBtn + pauseBtn + resumeBtn + verifyBtn + (hasVerifyWi ? _renderVerifyBadge(verifyWi) : '') + archiveBtn + deleteBtn +
|
|
260
262
|
'</div>' +
|
|
261
263
|
'</div>' +
|
|
262
264
|
'</div>' +
|
|
@@ -442,13 +444,15 @@ function _renderPlanModal(normalizedFile, raw, lastMod) {
|
|
|
442
444
|
'onclick="planPause(\'' + escHtml(normalizedFile) + '\',this)">Pause</button>' : '';
|
|
443
445
|
const modalResumeBtn = isPaused ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green)" ' +
|
|
444
446
|
'onclick="planApprove(\'' + escHtml(normalizedFile) + '\',this)">Resume</button>' : '';
|
|
445
|
-
const
|
|
447
|
+
const modalVerifyWi = (window._lastWorkItems || []).find(w => w.itemType === 'verify' && w.sourcePlan === normalizedFile);
|
|
448
|
+
const modalVerifyBtn = isModalCompleted && !modalVerifyWi ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green)" ' +
|
|
446
449
|
'onclick="triggerVerify(\'' + escHtml(normalizedFile) + '\',this)">Verify</button>' : '';
|
|
450
|
+
const modalVerifyInfo = modalVerifyWi ? _renderVerifyBadge(modalVerifyWi) : '';
|
|
447
451
|
const modalArchiveBtn = '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--muted)" ' +
|
|
448
452
|
'onclick="planArchive(\'' + escHtml(normalizedFile) + '\')">Archive</button>';
|
|
449
453
|
const lastModLabel = lastMod ? '<div style="font-size:10px;color:var(--muted);font-weight:400;margin-top:2px">Last updated: ' + new Date(lastMod).toLocaleString() + '</div>' : '';
|
|
450
454
|
const actionBtns = '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">' +
|
|
451
|
-
(modalCompletedLabel || '') + (modalInProgressLabel || '') + (modalExecuteBtn || '') + (modalPauseBtn || '') + (modalResumeBtn || '') + (modalVerifyBtn || '') +
|
|
455
|
+
(modalCompletedLabel || '') + (modalInProgressLabel || '') + (modalExecuteBtn || '') + (modalPauseBtn || '') + (modalResumeBtn || '') + (modalVerifyBtn || '') + (modalVerifyInfo || '') +
|
|
452
456
|
' ' + modalArchiveBtn +
|
|
453
457
|
' <button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--red)" ' +
|
|
454
458
|
'onclick="planDelete(\'' + escHtml(normalizedFile) + '\')">Delete</button>' +
|
|
@@ -664,6 +668,17 @@ async function planRegeneratePRD(source) {
|
|
|
664
668
|
} catch (e) { alert('Error: ' + e.message); }
|
|
665
669
|
}
|
|
666
670
|
|
|
671
|
+
function _renderVerifyBadge(verifyWi) {
|
|
672
|
+
const statusColors = { pending: 'var(--muted)', dispatched: 'var(--blue)', done: 'var(--green)', failed: 'var(--red)' };
|
|
673
|
+
const color = statusColors[verifyWi.status] || 'var(--muted)';
|
|
674
|
+
const label = verifyWi.status === 'dispatched' ? 'Verifying...' : verifyWi.status === 'done' ? 'Verified' : verifyWi.status === 'failed' ? 'Verify failed' : 'Verify pending';
|
|
675
|
+
const allPrs = (window._lastStatus?.pullRequests) || [];
|
|
676
|
+
const verifyPr = allPrs.find(pr => (pr.prdItems || []).includes(verifyWi.id));
|
|
677
|
+
const prLink = verifyPr?.url ? ' <a href="' + escHtml(verifyPr.url) + '" target="_blank" onclick="event.stopPropagation()" style="color:var(--blue);text-decoration:none;font-size:9px">E2E PR</a>' : '';
|
|
678
|
+
const branchInfo = verifyPr?.branch ? ' <span style="font-size:8px;color:var(--muted)" title="' + escHtml(verifyPr.branch) + '">(' + escHtml(verifyPr.branch.slice(0, 25)) + ')</span>' : '';
|
|
679
|
+
return '<span style="font-size:9px;font-weight:600;color:' + color + ';padding:0 4px">' + label + '</span>' + prLink + branchInfo;
|
|
680
|
+
}
|
|
681
|
+
|
|
667
682
|
async function openVerifyGuide(file) {
|
|
668
683
|
try {
|
|
669
684
|
const normalizedFile = normalizePlanFile(file);
|
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
|
@@ -148,7 +148,10 @@ function checkPlanCompletion(meta, config) {
|
|
|
148
148
|
// existingPrItem/existingVerify guards, so the flag does NOT block crash recovery of those.
|
|
149
149
|
plan._completionNotified = true;
|
|
150
150
|
mutateJsonFileLocked(planPath, (data) => {
|
|
151
|
+
data.status = 'completed';
|
|
152
|
+
data.completedAt = plan.completedAt;
|
|
151
153
|
data._completionNotified = true;
|
|
154
|
+
if (plan._timing) data._timing = plan._timing;
|
|
152
155
|
return data;
|
|
153
156
|
});
|
|
154
157
|
|
|
@@ -227,9 +230,10 @@ function checkPlanCompletion(meta, config) {
|
|
|
227
230
|
).join('\n');
|
|
228
231
|
|
|
229
232
|
// List projects and their worktree paths for the agent
|
|
230
|
-
const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) =>
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) => {
|
|
234
|
+
const lp = p.localPath.replace(/\\/g, '/');
|
|
235
|
+
return `- **${name}**: see setup commands below (\`${lp}/../worktrees/verify-${name}-${planSlug}-*\`)`;
|
|
236
|
+
}).join('\n');
|
|
233
237
|
|
|
234
238
|
const description = [
|
|
235
239
|
`Verification task for completed plan \`${planFile}\`.`,
|
|
@@ -274,19 +278,30 @@ function checkPlanCompletion(meta, config) {
|
|
|
274
278
|
log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
|
|
275
279
|
}
|
|
276
280
|
|
|
277
|
-
// 5. Archive
|
|
281
|
+
// 5. Archive deferred until verify completes (see runPostCompletionHooks).
|
|
282
|
+
// Plan stays active until verification finishes so artifacts are visible.
|
|
283
|
+
|
|
284
|
+
log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ─── Plan Archiving (called after verify completes) ─────────────────────────
|
|
288
|
+
|
|
289
|
+
function archivePlan(planFile, plan, projects, config) {
|
|
290
|
+
const planPath = path.join(PRD_DIR, planFile);
|
|
291
|
+
const projectName = plan.project || '';
|
|
292
|
+
|
|
293
|
+
// Archive PRD .json to prd/archive/
|
|
278
294
|
const prdArchiveDir = path.join(PRD_DIR, 'archive');
|
|
279
295
|
if (!fs.existsSync(prdArchiveDir)) fs.mkdirSync(prdArchiveDir, { recursive: true });
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
|
|
283
|
-
log('info', `Archived completed PRD: prd/archive/${planFile}`);
|
|
284
|
-
} catch (err) {
|
|
285
|
-
log('warn', `Failed to archive PRD ${planFile}: ${err.message}`);
|
|
296
|
+
if (fs.existsSync(planPath)) {
|
|
286
297
|
shared.safeWrite(planPath, plan);
|
|
298
|
+
try {
|
|
299
|
+
fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
|
|
300
|
+
log('info', `Archived completed PRD: prd/archive/${planFile}`);
|
|
301
|
+
} catch (err) { log('warn', `Failed to archive PRD ${planFile}: ${err.message}`); }
|
|
287
302
|
}
|
|
288
303
|
|
|
289
|
-
//
|
|
304
|
+
// Archive the source .md plan
|
|
290
305
|
const planArchiveDir = path.join(PLANS_DIR, 'archive');
|
|
291
306
|
if (!fs.existsSync(planArchiveDir)) fs.mkdirSync(planArchiveDir, { recursive: true });
|
|
292
307
|
if (plan.source_plan) {
|
|
@@ -298,7 +313,6 @@ function checkPlanCompletion(meta, config) {
|
|
|
298
313
|
} catch (err) { log('warn', `Failed to archive source plan ${plan.source_plan}: ${err.message}`); }
|
|
299
314
|
}
|
|
300
315
|
} else {
|
|
301
|
-
// Fallback: scan for matching .md files (legacy PRDs without source_plan)
|
|
302
316
|
try {
|
|
303
317
|
const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
|
|
304
318
|
for (const md of mdFiles) {
|
|
@@ -314,16 +328,25 @@ function checkPlanCompletion(meta, config) {
|
|
|
314
328
|
} catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
|
|
315
329
|
}
|
|
316
330
|
|
|
317
|
-
//
|
|
331
|
+
// Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
|
|
318
332
|
try {
|
|
319
|
-
|
|
333
|
+
let allWi = [];
|
|
334
|
+
for (const p of projects) {
|
|
335
|
+
try { allWi = allWi.concat(safeJson(shared.projectWorkItemsPath(p)) || []); } catch {}
|
|
336
|
+
}
|
|
337
|
+
const planWi = allWi.filter(w => w.sourcePlan === planFile && w.itemType !== 'verify');
|
|
338
|
+
const allPrs = [];
|
|
339
|
+
for (const p of projects) {
|
|
340
|
+
try { allPrs.push(...(safeJson(shared.projectPrPath(p)) || [])); } catch {}
|
|
341
|
+
}
|
|
342
|
+
|
|
320
343
|
const branchSlugs = new Set();
|
|
321
344
|
if (plan.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
|
|
322
|
-
for (const w of
|
|
345
|
+
for (const w of planWi) {
|
|
323
346
|
if (w.branch) branchSlugs.add(shared.sanitizeBranch(w.branch).toLowerCase());
|
|
324
347
|
if (w.id) branchSlugs.add(w.id.toLowerCase());
|
|
325
348
|
}
|
|
326
|
-
for (const pr of
|
|
349
|
+
for (const pr of allPrs.filter(pr => (pr.prdItems || []).some(id => planWi.find(w => w.id === id)))) {
|
|
327
350
|
if (pr.branch) branchSlugs.add(shared.sanitizeBranch(pr.branch).toLowerCase());
|
|
328
351
|
}
|
|
329
352
|
|
|
@@ -345,10 +368,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
345
368
|
}
|
|
346
369
|
}
|
|
347
370
|
}
|
|
348
|
-
if (cleanedWt > 0) log('info', `Plan
|
|
371
|
+
if (cleanedWt > 0) log('info', `Plan archive: cleaned ${cleanedWt} worktree(s)`);
|
|
349
372
|
} catch (err) { log('warn', `Worktree cleanup: ${err.message}`); }
|
|
350
|
-
|
|
351
|
-
log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
|
|
352
373
|
}
|
|
353
374
|
|
|
354
375
|
// ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
|
|
@@ -1424,6 +1445,19 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1424
1445
|
let prsCreatedCount = 0;
|
|
1425
1446
|
if (isSuccess) prsCreatedCount = syncPrsFromOutput(stdout, agentId, meta, config) || 0;
|
|
1426
1447
|
|
|
1448
|
+
// Archive plan after verify task completes (AFTER PR sync so E2E PR is linked)
|
|
1449
|
+
if (meta?.item?.itemType === 'verify' && meta?.item?.sourcePlan) {
|
|
1450
|
+
try {
|
|
1451
|
+
const vPlanFile = meta.item.sourcePlan;
|
|
1452
|
+
const vPlanPath = path.join(PRD_DIR, vPlanFile);
|
|
1453
|
+
const vPlan = safeJson(vPlanPath);
|
|
1454
|
+
if (vPlan) {
|
|
1455
|
+
const vProjects = shared.getProjects(config);
|
|
1456
|
+
archivePlan(vPlanFile, vPlan, vProjects, config);
|
|
1457
|
+
}
|
|
1458
|
+
} catch (err) { log('warn', `Verify archive: ${err.message}`); }
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1427
1461
|
// Clean up worktree for non-shared-branch tasks after completion
|
|
1428
1462
|
if (meta?.branch && meta?.branchStrategy !== 'shared-branch') {
|
|
1429
1463
|
try {
|
|
@@ -1551,6 +1585,7 @@ function syncPrdFromPrs(config) {
|
|
|
1551
1585
|
|
|
1552
1586
|
module.exports = {
|
|
1553
1587
|
checkPlanCompletion,
|
|
1588
|
+
archivePlan,
|
|
1554
1589
|
updateWorkItemStatus,
|
|
1555
1590
|
syncPrdItemStatus,
|
|
1556
1591
|
syncPrsFromOutput,
|
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}`
|
|
@@ -1457,6 +1461,8 @@ function discoverFromWorkItems(config, project) {
|
|
|
1457
1461
|
task_description: item.title + (item.description ? '\n\n' + item.description : ''),
|
|
1458
1462
|
task_id: item.id,
|
|
1459
1463
|
work_type: workType,
|
|
1464
|
+
source_plan: item.sourcePlan || '',
|
|
1465
|
+
plan_slug: (item.sourcePlan || '').replace('.json', ''),
|
|
1460
1466
|
additional_context: item.prompt ? `## Additional Context\n\n${item.prompt}` : '',
|
|
1461
1467
|
scope_section: `## Scope: Project — ${project?.name || 'default'}\n\nThis task is scoped to a single project.`,
|
|
1462
1468
|
branch_name: branchName,
|
|
@@ -1475,6 +1481,38 @@ function discoverFromWorkItems(config, project) {
|
|
|
1475
1481
|
const ac = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
|
|
1476
1482
|
vars.acceptance_criteria = ac ? '## Acceptance Criteria\n\n' + ac : '';
|
|
1477
1483
|
|
|
1484
|
+
// Inject checkpoint context if agent left a checkpoint.json from a prior run
|
|
1485
|
+
vars.checkpoint_context = '';
|
|
1486
|
+
try {
|
|
1487
|
+
const wtPath = vars.worktree_path || root;
|
|
1488
|
+
const cpPath = path.join(wtPath, 'checkpoint.json');
|
|
1489
|
+
if (fs.existsSync(cpPath)) {
|
|
1490
|
+
const cpData = JSON.parse(fs.readFileSync(cpPath, 'utf8'));
|
|
1491
|
+
const cpCount = (item._checkpointCount || 0) + 1;
|
|
1492
|
+
if (cpCount > 3) {
|
|
1493
|
+
log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
|
|
1494
|
+
item.status = 'needs-human-review';
|
|
1495
|
+
item._checkpointCount = cpCount;
|
|
1496
|
+
needsWrite = true;
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
item._checkpointCount = cpCount;
|
|
1500
|
+
needsWrite = true;
|
|
1501
|
+
const cpSummary = [
|
|
1502
|
+
`## Checkpoint (Resume #${cpCount}/3)`,
|
|
1503
|
+
'',
|
|
1504
|
+
'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
|
|
1505
|
+
'',
|
|
1506
|
+
cpData.completed && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
|
|
1507
|
+
cpData.remaining && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
|
|
1508
|
+
cpData.blockers && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
|
|
1509
|
+
cpData.branch_state ? `### Branch State\n${cpData.branch_state}` : '',
|
|
1510
|
+
].filter(Boolean).join('\n');
|
|
1511
|
+
vars.checkpoint_context = cpSummary;
|
|
1512
|
+
log('info', `Injecting checkpoint context for ${item.id} (resume #${cpCount})`);
|
|
1513
|
+
}
|
|
1514
|
+
} catch (e) { log('warn', `checkpoint read for ${item.id}: ${e.message}`); }
|
|
1515
|
+
|
|
1478
1516
|
// Inject ask-specific variables for the ask playbook
|
|
1479
1517
|
if (workType === 'ask') {
|
|
1480
1518
|
vars.question = item.title + (item.description ? '\n\n' + item.description : '');
|
|
@@ -1798,6 +1836,7 @@ function discoverCentralWorkItems(config) {
|
|
|
1798
1836
|
prompt,
|
|
1799
1837
|
meta: {
|
|
1800
1838
|
dispatchKey: fanKey, source: 'central-work-item-fanout', item, parentKey: key,
|
|
1839
|
+
branch: `fan/${item.id}/${fanAgentId}`,
|
|
1801
1840
|
deadline: item.timeout ? Date.now() + item.timeout : Date.now() + (config.engine?.fanOutTimeout || config.engine?.agentTimeout || DEFAULTS.agentTimeout)
|
|
1802
1841
|
}
|
|
1803
1842
|
});
|
|
@@ -1845,6 +1884,39 @@ function discoverCentralWorkItems(config) {
|
|
|
1845
1884
|
const normAc = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
|
|
1846
1885
|
vars.acceptance_criteria = normAc ? '## Acceptance Criteria\n\n' + normAc : '';
|
|
1847
1886
|
|
|
1887
|
+
// Inject checkpoint context if agent left a checkpoint.json from a prior run
|
|
1888
|
+
vars.checkpoint_context = '';
|
|
1889
|
+
try {
|
|
1890
|
+
const centralBranch = item.branch || `work/${item.id}`;
|
|
1891
|
+
const centralWtPath = firstProject?.localPath
|
|
1892
|
+
? path.resolve(firstProject.localPath, config.engine?.worktreeRoot || '../worktrees', centralBranch)
|
|
1893
|
+
: '';
|
|
1894
|
+
const cpPath = centralWtPath ? path.join(centralWtPath, 'checkpoint.json') : '';
|
|
1895
|
+
if (cpPath && fs.existsSync(cpPath)) {
|
|
1896
|
+
const cpData = JSON.parse(fs.readFileSync(cpPath, 'utf8'));
|
|
1897
|
+
const cpCount = (item._checkpointCount || 0) + 1;
|
|
1898
|
+
if (cpCount > 3) {
|
|
1899
|
+
log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
|
|
1900
|
+
item.status = 'needs-human-review';
|
|
1901
|
+
item._checkpointCount = cpCount;
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
item._checkpointCount = cpCount;
|
|
1905
|
+
const cpSummary = [
|
|
1906
|
+
`## Checkpoint (Resume #${cpCount}/3)`,
|
|
1907
|
+
'',
|
|
1908
|
+
'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
|
|
1909
|
+
'',
|
|
1910
|
+
cpData.completed && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
|
|
1911
|
+
cpData.remaining && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
|
|
1912
|
+
cpData.blockers && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
|
|
1913
|
+
cpData.branch_state ? `### Branch State\n${cpData.branch_state}` : '',
|
|
1914
|
+
].filter(Boolean).join('\n');
|
|
1915
|
+
vars.checkpoint_context = cpSummary;
|
|
1916
|
+
log('info', `Injecting checkpoint context for ${item.id} (resume #${cpCount})`);
|
|
1917
|
+
}
|
|
1918
|
+
} catch (e) { log('warn', `checkpoint read for ${item.id}: ${e.message}`); }
|
|
1919
|
+
|
|
1848
1920
|
// Inject plan-specific variables for the plan playbook
|
|
1849
1921
|
if (workType === 'plan') {
|
|
1850
1922
|
// Ensure plans directory exists before agent tries to write
|
|
@@ -1909,7 +1981,7 @@ function discoverCentralWorkItems(config) {
|
|
|
1909
1981
|
agentRole,
|
|
1910
1982
|
task: item.title || item.description?.slice(0, 80) || item.id,
|
|
1911
1983
|
prompt,
|
|
1912
|
-
meta: { dispatchKey: key, source: 'central-work-item', item, planFileName: item.planFile || item._planFileName || null }
|
|
1984
|
+
meta: { dispatchKey: key, source: 'central-work-item', item, planFileName: item.planFile || item._planFileName || null, branch: item.branch || item.featureBranch || `work/${item.id}` }
|
|
1913
1985
|
});
|
|
1914
1986
|
|
|
1915
1987
|
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.291",
|
|
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/verify.md
CHANGED
|
@@ -13,11 +13,14 @@ Repo: {{repo_name}} | Org: {{ado_org}} | ADO Project: {{ado_project}}
|
|
|
13
13
|
|
|
14
14
|
## Your Task
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
Verify that a set of related changes work correctly together. You must **figure out** how to build, test, and run this specific project — do not assume any particular language, framework, or tooling. Your job is to:
|
|
17
|
+
|
|
18
|
+
1. **Set up worktrees** with all PR branches merged
|
|
19
|
+
2. **Understand the project** — read its docs to learn how to build, test, and run it
|
|
20
|
+
3. **Build and test** from each worktree
|
|
21
|
+
4. **Start the application** if applicable (keep it running detached)
|
|
22
|
+
5. **Write a transparent verification report and testing guide**
|
|
23
|
+
6. **Create E2E pull requests**
|
|
21
24
|
|
|
22
25
|
## Step 1: Set Up Worktrees
|
|
23
26
|
|
|
@@ -29,100 +32,130 @@ If any merge conflicts occur:
|
|
|
29
32
|
|
|
30
33
|
After setup, all changes for a project are in a single directory — no switching between branches.
|
|
31
34
|
|
|
32
|
-
## Step 2:
|
|
35
|
+
## Step 2: Understand the Project
|
|
33
36
|
|
|
34
|
-
For each project worktree
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
For each project worktree, **read its documentation** to understand:
|
|
38
|
+
- What language/framework it uses
|
|
39
|
+
- How to install dependencies
|
|
40
|
+
- How to build it
|
|
41
|
+
- How to run tests
|
|
42
|
+
- How to start it (if it has a runnable application)
|
|
43
|
+
|
|
44
|
+
Check these files: `CLAUDE.md`, `README.md`, `package.json`, `Makefile`, `Cargo.toml`, `pyproject.toml`, `build.gradle`, `CMakeLists.txt`, `docker-compose.yml`, `Podfile`, `build.gradle.kts`, `*.xcodeproj`, `*.xcworkspace`, or whatever build system the project uses.
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
**Do not assume any specific platform.** The project could be a web app, mobile app (Android/iOS/React Native/Flutter), backend service, CLI tool, library, monorepo, or anything else. Adapt your verification approach to what the project actually is.
|
|
42
47
|
|
|
43
|
-
## Step 3:
|
|
48
|
+
## Step 3: Build and Test
|
|
49
|
+
|
|
50
|
+
For each project worktree:
|
|
51
|
+
1. `cd` into the worktree path
|
|
52
|
+
2. Install dependencies using whatever the project requires
|
|
53
|
+
3. Run the build using the project's build system
|
|
54
|
+
4. Run the test suite
|
|
55
|
+
5. Record: PASS or FAIL with error output, test counts (passed/failed/skipped)
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
1. Run the test suite from the worktree
|
|
47
|
-
2. Record passed/failed/skipped counts
|
|
57
|
+
If a build or test fails, **do NOT fix it** — report the exact error and continue with other projects.
|
|
48
58
|
|
|
49
|
-
## Step 4: Start the
|
|
59
|
+
## Step 4: Start the Application (if applicable)
|
|
50
60
|
|
|
51
|
-
Determine
|
|
52
|
-
- Check for `dev`, `start`, `serve` scripts in package.json
|
|
53
|
-
- Look for web frameworks (React, Next.js, TanStack, Vite, etc.)
|
|
61
|
+
Determine if the project has a **runnable application** (web server, API, desktop app, mobile emulator, etc.) by reading its documentation and build config. For mobile apps, check if an emulator/simulator can be launched or if building an APK/IPA is the appropriate verification step.
|
|
54
62
|
|
|
55
63
|
If found:
|
|
56
|
-
1. Start
|
|
64
|
+
1. Start it **detached from your process** so it survives after you exit. Use the platform-appropriate method:
|
|
57
65
|
```bash
|
|
58
66
|
cd <worktree-path>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const fs = require('fs');
|
|
62
|
-
const child = spawn('cmd', ['/c', '<start-command>'], {
|
|
63
|
-
cwd: process.cwd(),
|
|
64
|
-
detached: true,
|
|
65
|
-
stdio: ['ignore', fs.openSync('dev-server.log', 'w'), fs.openSync('dev-server.log', 'w')]
|
|
66
|
-
});
|
|
67
|
-
child.unref();
|
|
68
|
-
fs.writeFileSync('dev-server.pid', String(child.pid));
|
|
69
|
-
console.log('Server started, PID:', child.pid);
|
|
70
|
-
"
|
|
67
|
+
nohup <start-command> > app-server.log 2>&1 &
|
|
68
|
+
echo $! > app-server.pid
|
|
71
69
|
```
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
On Windows, use `spawn` with `detached: true` and `child.unref()`.
|
|
71
|
+
|
|
72
|
+
2. Wait a few seconds, then verify it's responding (e.g. `curl -s -o /dev/null -w "%{http_code}" http://localhost:<PORT>`)
|
|
73
|
+
3. Note the URL, port, and PID
|
|
74
74
|
4. Output the exact restart command with **absolute worktree paths**
|
|
75
|
-
5. Include stop command
|
|
75
|
+
5. Include the stop command (e.g. `kill <PID>` or `taskkill /PID <PID> /F` on Windows)
|
|
76
|
+
|
|
77
|
+
If the project has no runnable application, skip this step and note that in the guide.
|
|
78
|
+
|
|
79
|
+
## Step 5: Write the Verification Report and Testing Guide
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
Create the guide in TWO locations:
|
|
82
|
+
1. **Permanent location** (linked from dashboard): `{{team_root}}/prd/guides/verify-{{plan_slug}}.md`
|
|
83
|
+
2. **Inbox copy** (for team consolidation): `{{team_root}}/notes/inbox/verify-{{plan_slug}}.md`
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
1. **Permanent location** (linked from dashboard): `{{team_root}}/prd/guides/verify-{{date}}.md`
|
|
81
|
-
2. **Inbox copy** (for team consolidation): `{{team_root}}/notes/inbox/verify-{{date}}.md`
|
|
85
|
+
**Be transparent.** The guide must clearly state what was built, what was tested, what passed, what failed, and what still needs human verification.
|
|
82
86
|
|
|
83
87
|
Structure:
|
|
84
88
|
|
|
85
89
|
```markdown
|
|
86
|
-
#
|
|
90
|
+
# Verification Report & Testing Guide
|
|
87
91
|
|
|
88
92
|
**Date:** {{date}}
|
|
89
|
-
**Plan:**
|
|
90
|
-
**
|
|
91
|
-
|
|
93
|
+
**Plan:** {{source_plan}}
|
|
94
|
+
**Verified by:** {{agent_name}}
|
|
95
|
+
|
|
96
|
+
## What Was Built
|
|
97
|
+
|
|
98
|
+
For each completed plan item, summarize:
|
|
99
|
+
- **Item ID:** what it implements
|
|
100
|
+
- **Key changes:** files modified, features added, behaviors changed
|
|
101
|
+
- **PR:** link to the individual PR
|
|
92
102
|
|
|
93
|
-
##
|
|
103
|
+
## Verification Results
|
|
104
|
+
|
|
105
|
+
### Build Status
|
|
94
106
|
|
|
95
107
|
| Project | Worktree Path | Build | Tests | Notes |
|
|
96
108
|
|---------|--------------|-------|-------|-------|
|
|
97
|
-
| name | path | PASS/FAIL | X pass, Y fail |
|
|
109
|
+
| name | path | PASS/FAIL | X pass, Y fail, Z skip | error details if any |
|
|
110
|
+
|
|
111
|
+
### Automated Test Results
|
|
112
|
+
- Total: X passed, Y failed, Z skipped
|
|
113
|
+
- Notable failures: (list any, with error messages)
|
|
114
|
+
- Test coverage notes: (are the new features covered by tests?)
|
|
115
|
+
|
|
116
|
+
### What Was Verified
|
|
117
|
+
For each plan item, state what you actually checked:
|
|
118
|
+
- Did the build pass with this change included?
|
|
119
|
+
- Did existing tests pass?
|
|
120
|
+
- Were there new tests for the new functionality?
|
|
121
|
+
- Any runtime errors observed?
|
|
122
|
+
|
|
123
|
+
### What Could NOT Be Verified Automatically
|
|
124
|
+
List anything that requires human judgment:
|
|
125
|
+
- UI/UX changes that need visual inspection
|
|
126
|
+
- Behaviors that depend on external services
|
|
127
|
+
- Performance characteristics
|
|
128
|
+
- Edge cases not covered by tests
|
|
129
|
+
|
|
130
|
+
## Manual Testing Guide
|
|
98
131
|
|
|
99
|
-
|
|
132
|
+
**How to run:** (server URL, emulator command, APK path, or N/A)
|
|
133
|
+
**Restart Command:** `cd <absolute-worktree-path> && <command>` (if applicable)
|
|
100
134
|
|
|
101
135
|
### <Feature Name> (Plan Item ID)
|
|
102
136
|
**What changed:** brief description
|
|
103
137
|
**How to test:**
|
|
104
|
-
1.
|
|
105
|
-
2.
|
|
106
|
-
3.
|
|
138
|
+
1. Step-by-step instructions
|
|
139
|
+
2. With concrete actions (URLs, buttons, inputs)
|
|
140
|
+
3. And expected outcomes
|
|
107
141
|
|
|
108
|
-
**
|
|
109
|
-
- (from acceptance criteria)
|
|
110
|
-
- (from acceptance criteria)
|
|
142
|
+
**Acceptance criteria check:**
|
|
143
|
+
- [ ] (from plan item acceptance criteria)
|
|
144
|
+
- [ ] (from plan item acceptance criteria)
|
|
111
145
|
|
|
112
146
|
### <Next Feature> ...
|
|
113
147
|
|
|
114
148
|
## Integration Points
|
|
115
149
|
|
|
116
|
-
Cross-project interactions to verify:
|
|
117
|
-
- e.g., "
|
|
118
|
-
- e.g., "Progression UI updates in real-time as WebSocket messages arrive"
|
|
150
|
+
Cross-project or cross-feature interactions to verify:
|
|
151
|
+
- e.g., "Service A calls Service B — verify the API contract"
|
|
119
152
|
|
|
120
153
|
## Known Issues
|
|
121
154
|
- Build warnings, test failures, merge conflicts, unimplemented items
|
|
122
155
|
|
|
123
156
|
## Quick Smoke Test
|
|
124
157
|
A minimal 5-step checklist to verify the core functionality:
|
|
125
|
-
1.
|
|
158
|
+
1. ...
|
|
126
159
|
2. ...
|
|
127
160
|
3. ...
|
|
128
161
|
4. ...
|
|
@@ -131,24 +164,24 @@ A minimal 5-step checklist to verify the core functionality:
|
|
|
131
164
|
|
|
132
165
|
## Step 6: Create E2E Pull Requests
|
|
133
166
|
|
|
134
|
-
For each project that has changes, create a single **aggregate PR** that combines all the plan's branches into one. This gives the human reviewer a single diff showing the full picture
|
|
167
|
+
For each project that has changes, create a single **aggregate PR** that combines all the plan's branches into one. This gives the human reviewer a single diff showing the full picture.
|
|
135
168
|
|
|
136
169
|
For each project worktree:
|
|
137
170
|
|
|
138
|
-
1.
|
|
171
|
+
1. Push the combined branch:
|
|
139
172
|
```bash
|
|
140
173
|
cd <worktree-path>
|
|
141
|
-
git checkout -b e2e
|
|
142
|
-
git push origin e2e
|
|
174
|
+
git checkout -b e2e/{{plan_slug}}
|
|
175
|
+
git push origin e2e/{{plan_slug}}
|
|
143
176
|
```
|
|
144
177
|
|
|
145
178
|
2. Create a PR targeting the project's main branch using `mcp__azure-ado__repo_create_pull_request` (or `gh pr create` for GitHub):
|
|
146
179
|
- **Title:** `[E2E] <plan summary>`
|
|
147
180
|
- **Description:** Include:
|
|
148
181
|
- The plan summary
|
|
149
|
-
- List of all individual PRs
|
|
150
|
-
-
|
|
151
|
-
-
|
|
182
|
+
- List of all individual PRs merged into this branch
|
|
183
|
+
- Build/test status from Step 3
|
|
184
|
+
- Link to the testing guide
|
|
152
185
|
- **Target branch:** the project's main branch (e.g., `main` or `master`)
|
|
153
186
|
- **Do NOT auto-complete** — this is for review only
|
|
154
187
|
- **Mark as draft** if the option is available
|
|
@@ -163,7 +196,7 @@ For each project worktree:
|
|
|
163
196
|
id: 'PR-<number>',
|
|
164
197
|
title: '[E2E] <plan summary>',
|
|
165
198
|
agent: '{{agent_name}}',
|
|
166
|
-
branch: 'e2e
|
|
199
|
+
branch: 'e2e/{{plan_slug}}',
|
|
167
200
|
reviewStatus: 'pending',
|
|
168
201
|
status: 'active',
|
|
169
202
|
created: new Date().toISOString().slice(0,10),
|
|
@@ -178,12 +211,14 @@ For each project worktree:
|
|
|
178
211
|
|
|
179
212
|
## Rules
|
|
180
213
|
|
|
214
|
+
- **Read the project docs first** — never assume a build system, language, or framework
|
|
181
215
|
- Base testing steps on the **acceptance criteria** from each plan item
|
|
182
|
-
- Include **concrete steps** — URLs, buttons to click, inputs to type, expected
|
|
216
|
+
- Include **concrete steps** — URLs, buttons to click, inputs to type, expected results
|
|
217
|
+
- Be **transparent** — clearly separate what you verified vs what needs human review
|
|
183
218
|
- If a project doesn't build, still document what SHOULD be testable once fixed
|
|
184
219
|
- Do NOT fix code — only report issues
|
|
185
220
|
- Leave all worktrees in place for the user to inspect
|
|
186
|
-
- The
|
|
221
|
+
- The application MUST be started **detached** so it keeps running after your process exits
|
|
187
222
|
- Use absolute paths everywhere so the user can copy-paste commands
|
|
188
223
|
- E2E PRs are for review only — do NOT auto-complete or merge them
|
|
189
224
|
|