@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 CHANGED
@@ -1,15 +1,20 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.289 (2026-04-03)
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 showVerify = effectiveStatus === 'completed' && prdFile && !isArchived;
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 modalVerifyBtn = isModalCompleted ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green)" ' +
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 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
  }
@@ -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
- `- **${name}**: \`${p.localPath}/../worktrees/verify-${planSlug}\``
232
- ).join('\n');
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: move PRD .json to prd/archive/ and source .md plan to plans/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
- shared.safeWrite(planPath, plan); // save completed status first
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
- // Also archive the source .md plan if it exists (use source_plan field, not content matching)
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
- // 6. Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
331
+ // Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
318
332
  try {
319
- // Collect all branch slugs: shared-branch + per-item branches + item IDs
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 doneItems) {
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 uniquePrs) {
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 completion: cleaned ${cleanedWt} worktree(s)`);
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,
@@ -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}`
@@ -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.289",
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.
@@ -13,11 +13,14 @@ Repo: {{repo_name}} | Org: {{ado_org}} | ADO Project: {{ado_project}}
13
13
 
14
14
  ## Your Task
15
15
 
16
- Build, test, and verify a set of related changes. Your job is to:
17
- 1. **Set up one worktree per project** with all PR branches merged in
18
- 2. **Build and test** from each worktree
19
- 3. **Start the webapp** on localhost (keep it running **detached so it survives after your process exits**)
20
- 4. **Write a manual testing guide**
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: Build Each Project
35
+ ## Step 2: Understand the Project
33
36
 
34
- For each project worktree listed above:
35
- 1. `cd` into the worktree path
36
- 2. Read its CLAUDE.md / package.json / README for build instructions
37
- 3. Install dependencies (`yarn install`, `npm install`, etc.)
38
- 4. Run the build (`yarn build`, `npm run build`, etc.)
39
- 5. Record: PASS or FAIL with error output
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
- If a build fails, **do NOT fix it** report the error and continue with other projects.
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: Run Tests
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
- For each project that built successfully:
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 Webapp
59
+ ## Step 4: Start the Application (if applicable)
50
60
 
51
- Determine which project is the **user-facing webapp** (has a dev server, UI):
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 the dev server **detached from your process** so it survives after you exit:
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
- node -e "
60
- const { spawn } = require('child_process');
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
- 2. Wait a few seconds, then verify it's responding: `curl -s -o /dev/null -w "%{http_code}" http://localhost:<PORT>`
73
- 3. Note the localhost URL, port, and PID
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: `taskkill //PID <PID> //F`
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
- ## Step 5: Write the Manual Testing Guide
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
- Create the testing guide in TWO locations:
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
- # Manual Testing Guide
90
+ # Verification Report & Testing Guide
87
91
 
88
92
  **Date:** {{date}}
89
- **Plan:** <plan file>
90
- **Local Server:** http://localhost:XXXX (or N/A)
91
- **Restart Command:** `cd <absolute-worktree-path> && <command>`
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
- ## Build Status
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 | notes |
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
- ## What to Test
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. Navigate to http://localhost:XXXX/path
105
- 2. Click on / interact with ...
106
- 3. You should see ...
138
+ 1. Step-by-step instructions
139
+ 2. With concrete actions (URLs, buttons, inputs)
140
+ 3. And expected outcomes
107
141
 
108
- **Expected behavior:**
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., "Bebop sends message via AugLoop OfficeAgent receives and responds"
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. Open http://localhost:XXXX
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 of everything built.
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. You're already in the worktree with all branches merged. Push this combined branch:
171
+ 1. Push the combined branch:
139
172
  ```bash
140
173
  cd <worktree-path>
141
- git checkout -b e2e/<plan-slug>
142
- git push origin e2e/<plan-slug>
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 that are merged into this branch
150
- - The testing guide (copy from Step 5)
151
- - Build/test status from Step 2-3
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/<plan-slug>',
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 visual results
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 local server MUST be started **detached** (using `spawn` with `detached: true` + `child.unref()`) so it keeps running after your process exits. Save the PID to `dev-server.pid` in the worktree.
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