@yemi33/minions 0.1.2 → 0.1.4

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,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4 (2026-03-24)
4
+
5
+ ### Engine
6
+ - engine.js
7
+ - engine/ado.js
8
+ - engine/github.js
9
+ - engine/lifecycle.js
10
+
11
+ ## 0.1.3 (2026-03-24)
12
+
13
+ ### Engine
14
+ - engine/lifecycle.js
15
+
16
+ ### Dashboard
17
+ - dashboard.html
18
+ - dashboard.js
19
+
3
20
  ## 0.1.1 (2026-03-23)
4
21
 
5
22
  ### Engine
package/dashboard.html CHANGED
@@ -3675,9 +3675,17 @@ function renderPlans(plans) {
3675
3675
  : (statusLabels[status] || status);
3676
3676
  const needsAction = (status === 'awaiting-approval' || status === 'paused' || isPrdAwaitingApproval || isPrdPaused) && !isArchived;
3677
3677
  const isRevision = status === 'revision-requested' && !isArchived;
3678
+ const isCompleted = status === 'completed';
3679
+ const isDraft = (p.format === 'draft' || status === 'draft') && !isCompleted;
3680
+ const isAwaitingApproval = status === 'awaiting-approval';
3681
+ const isPaused = status === 'paused';
3682
+ const isApproved = status === 'approved' || status === 'active';
3683
+ // For .md drafts: show Execute only if no PRD exists yet (not already executed)
3684
+ const prdFile = planToPrdFile[p.file] || (p.file.endsWith('.json') ? p.file : '');
3678
3685
 
3679
3686
  let actions = '';
3680
- if (needsAction) {
3687
+ const resumeVisible = ((isPrdBlocked || isAwaitingApproval || isPaused) && prdFile && !isArchived);
3688
+ if (needsAction && !resumeVisible) {
3681
3689
  actions = '<div class="plan-card-actions" onclick="event.stopPropagation()">' +
3682
3690
  '<button class="plan-btn approve" onclick="planApprove(\'' + escHtml(p.file) + '\')">Approve</button>' +
3683
3691
  '<button class="plan-btn" style="color:var(--blue);border-color:var(--blue)" onclick="planDiscuss(\'' + escHtml(p.file) + '\')">Discuss &amp; Revise</button>' +
@@ -3694,13 +3702,6 @@ function renderPlans(plans) {
3694
3702
  actions = '<div class="plan-card-meta" style="margin-top:6px;color:var(--purple,#a855f7)">Revision in progress: ' + escHtml((p.revisionFeedback || '').slice(0, 100)) + '</div>';
3695
3703
  }
3696
3704
 
3697
- const isCompleted = status === 'completed';
3698
- const isDraft = (p.format === 'draft' || status === 'draft') && !isCompleted;
3699
- const isAwaitingApproval = status === 'awaiting-approval';
3700
- const isPaused = status === 'paused';
3701
- const isApproved = status === 'approved' || status === 'active';
3702
- // For .md drafts: show Execute only if no PRD exists yet (not already executed)
3703
- const prdFile = planToPrdFile[p.file] || (p.file.endsWith('.json') ? p.file : '');
3704
3705
  const executeBtn = isDraft && !isWorking && !isPrdBlocked && !isArchived && !prdFile ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);font-weight:600" ' +
3705
3706
  'onclick="event.stopPropagation();planExecute(\'' + escHtml(p.file) + '\',\'' + escHtml(p.project) + '\',this)">Execute</button>' : '';
3706
3707
  // Pause/Resume: target the PRD .json file if it exists, otherwise the plan itself
@@ -4061,7 +4062,8 @@ async function planView(file) {
4061
4062
  const prdCompleted = wi.some(w => w.type === 'plan-to-prd' && w.status === 'done' && w.planFile === normalizedFile);
4062
4063
  // Check if a PRD already exists for this plan (via plans list sourcePlan linkage)
4063
4064
  const hasPrd = (window._lastStatus?.plans || []).some(p => p.sourcePlan === normalizedFile && p.format === 'prd');
4064
- const modalExecuteBtn = isMdPlan && !hasActiveWork && !prdCompleted && !hasPrd ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green);font-weight:600" ' +
4065
+ const modalShowResume = isPaused;
4066
+ const modalExecuteBtn = isMdPlan && !modalShowResume && !hasActiveWork && !prdCompleted && !hasPrd ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green);font-weight:600" ' +
4065
4067
  'onclick="planExecute(\'' + escHtml(normalizedFile) + '\',\'\',this)">Execute</button>' : '';
4066
4068
  const modalCompletedLabel = prdCompleted && !hasActiveWork ? '<span style="font-size:10px;color:var(--green);font-weight:600">Completed</span>' : '';
4067
4069
  const modalInProgressLabel = hasActiveWork ? '<span style="font-size:10px;color:var(--blue)">In Progress</span>' : '';
package/dashboard.js CHANGED
@@ -1477,7 +1477,7 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1477
1477
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1478
1478
  }
1479
1479
 
1480
- // POST /api/plans/pause — pause a plan (stops new item materialization + pauses work items)
1480
+ // POST /api/plans/pause — pause a plan (stops new item materialization + resets active items to pending)
1481
1481
  if (req.method === 'POST' && req.url === '/api/plans/pause') {
1482
1482
  try {
1483
1483
  const body = await readBody(req);
@@ -1488,25 +1488,17 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1488
1488
  plan.pausedAt = new Date().toISOString();
1489
1489
  safeWrite(planPath, plan);
1490
1490
 
1491
- // Propagate pause to materialized work items across all projects
1492
- // But skip items that already have active PRs those are past the point of pausing
1493
- let paused = 0;
1491
+ // Propagate pause to materialized work items across all projects:
1492
+ // kill any active agent process and reset non-completed items back to pending.
1493
+ let reset = 0;
1494
1494
  const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
1495
- const allPrItemIds = new Set();
1496
1495
  for (const proj of PROJECTS) {
1497
1496
  wiPaths.push(shared.projectWorkItemsPath(proj));
1498
- try {
1499
- const prs = safeJson(shared.projectPrPath(proj)) || [];
1500
- for (const pr of prs) {
1501
- if (pr.status === 'active' && pr.prdItems?.length) {
1502
- pr.prdItems.forEach(id => allPrItemIds.add(id));
1503
- }
1504
- }
1505
- } catch {}
1506
1497
  }
1507
1498
  const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
1508
1499
  const dispatch = JSON.parse(safeRead(dispatchPath) || '{}');
1509
- const killedAgents = [];
1500
+ const killedAgents = new Set();
1501
+ const resetItemIds = new Set();
1510
1502
 
1511
1503
  for (const wiPath of wiPaths) {
1512
1504
  try {
@@ -1515,17 +1507,12 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1515
1507
  let changed = false;
1516
1508
  for (const w of items) {
1517
1509
  if (w.sourcePlan !== body.file) continue;
1518
- // Don't pause if this item already has an active PR
1519
- if (allPrItemIds.has(w.id) || allPrItemIds.has(w.sourcePlanItem)) continue;
1510
+ // Keep completed items as-is, reset everything else to pending.
1511
+ if (w.status === 'done' || w.status === 'implemented' || w.status === 'complete' || w.status === 'in-pr') continue;
1520
1512
 
1521
- if (w.status === 'pending') {
1522
- w.status = 'paused';
1523
- w._pausedBy = 'prd-pause';
1524
- paused++;
1525
- changed = true;
1526
- } else if (w.status === 'dispatched') {
1527
- // Kill the agent working on this item
1528
- const activeEntry = (dispatch.active || []).find(d => d.meta?.dispatchKey?.includes(w.id));
1513
+ if (w.status === 'dispatched') {
1514
+ // Kill the agent working on this item, if any.
1515
+ const activeEntry = (dispatch.active || []).find(d => d.meta?.item?.id === w.id || d.meta?.dispatchKey?.includes(w.id));
1529
1516
  if (activeEntry) {
1530
1517
  const statusPath = path.join(MINIONS_DIR, 'agents', activeEntry.agent, 'status.json');
1531
1518
  try {
@@ -1542,30 +1529,41 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
1542
1529
  delete agentStatus.dispatched;
1543
1530
  safeWrite(statusPath, agentStatus);
1544
1531
  } catch {}
1545
- killedAgents.push(activeEntry.agent);
1532
+ killedAgents.add(activeEntry.agent);
1546
1533
  }
1547
- w.status = 'paused';
1548
- w._pausedBy = 'prd-pause';
1549
- paused++;
1550
- changed = true;
1551
1534
  }
1535
+
1536
+ if (w.status !== 'pending') reset++;
1537
+ w.status = 'pending';
1538
+ delete w._pausedBy;
1539
+ delete w._resumedAt;
1540
+ delete w.dispatched_at;
1541
+ delete w.dispatched_to;
1542
+ delete w.failReason;
1543
+ delete w.failedAt;
1544
+ changed = true;
1545
+ if (w.id) resetItemIds.add(w.id);
1552
1546
  }
1553
1547
  if (changed) safeWrite(wiPath, items);
1554
1548
  } catch {}
1555
1549
  }
1556
1550
 
1557
- // Remove killed agents from dispatch active
1558
- if (killedAgents.length > 0) {
1559
- const killedSet = new Set(killedAgents);
1551
+ // Remove dispatch active entries for reset items or killed agents.
1552
+ if (resetItemIds.size > 0 || killedAgents.size > 0) {
1560
1553
  mutateJsonFileLocked(dispatchPath, (dp) => {
1561
1554
  dp.active = Array.isArray(dp.active) ? dp.active : [];
1562
- dp.active = dp.active.filter(d => !killedSet.has(d.agent));
1555
+ dp.active = dp.active.filter(d => {
1556
+ const itemId = d.meta?.item?.id;
1557
+ if (itemId && resetItemIds.has(itemId)) return false;
1558
+ if (killedAgents.has(d.agent)) return false;
1559
+ return true;
1560
+ });
1563
1561
  return dp;
1564
1562
  }, { defaultValue: { pending: [], active: [], completed: [] } });
1565
1563
  }
1566
1564
 
1567
1565
  invalidateStatusCache();
1568
- return jsonReply(res, 200, { ok: true, status: 'paused', pausedWorkItems: paused });
1566
+ return jsonReply(res, 200, { ok: true, status: 'paused', resetWorkItems: reset });
1569
1567
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1570
1568
  }
1571
1569
 
package/engine/ado.js CHANGED
@@ -339,6 +339,11 @@ async function reconcilePrs(config) {
339
339
  // PR already tracked — write link to pr-links.json if we can extract an ID
340
340
  if (confirmedItemId) {
341
341
  addPrLink(prId, confirmedItemId);
342
+ const existing = existingPrs.find(p => p.id === prId);
343
+ if (existing && !(existing.prdItems || []).includes(confirmedItemId)) {
344
+ existing.prdItems = Array.isArray(existing.prdItems) ? existing.prdItems : [];
345
+ existing.prdItems.push(confirmedItemId);
346
+ }
342
347
  projectUpdated++;
343
348
  }
344
349
  continue;
@@ -354,6 +359,7 @@ async function reconcilePrs(config) {
354
359
  status: 'active',
355
360
  created: (adoPr.creationDate || '').slice(0, 10) || e.dateStamp(),
356
361
  url: prUrl,
362
+ prdItems: confirmedItemId ? [confirmedItemId] : [],
357
363
  });
358
364
  if (confirmedItemId) addPrLink(prId, confirmedItemId);
359
365
  existingIds.add(prId);
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 } = shared;
8
+ const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, MINIONS_DIR, addPrLink } = shared;
9
9
  const { getPrs } = require('./queries');
10
10
  const path = require('path');
11
11
 
@@ -285,14 +285,24 @@ async function reconcilePrs(config) {
285
285
 
286
286
  for (const ghPr of ghPrs) {
287
287
  const prId = `PR-${ghPr.number}`;
288
- if (existingIds.has(prId)) continue;
289
-
290
288
  const branch = ghPr.head?.ref || '';
291
289
  const wiMatch = branch.match(/(P-[a-f0-9]{6,})/i) || branch.match(/(PL-W\d+)/i);
292
290
  const linkedItemId = wiMatch ? wiMatch[1] : null;
293
291
  const linkedItem = linkedItemId ? allItems.find(i => i.id === linkedItemId) : null;
294
292
  const confirmedItemId = linkedItem ? linkedItemId : null;
295
293
 
294
+ if (existingIds.has(prId)) {
295
+ if (confirmedItemId) {
296
+ addPrLink(prId, confirmedItemId);
297
+ const existing = existingPrs.find(p => p.id === prId);
298
+ if (existing && !(existing.prdItems || []).includes(confirmedItemId)) {
299
+ existing.prdItems = Array.isArray(existing.prdItems) ? existing.prdItems : [];
300
+ existing.prdItems.push(confirmedItemId);
301
+ }
302
+ }
303
+ continue;
304
+ }
305
+
296
306
  const prUrl = project.prUrlBase ? project.prUrlBase + ghPr.number : ghPr.html_url || '';
297
307
 
298
308
  existingPrs.push({
@@ -306,6 +316,7 @@ async function reconcilePrs(config) {
306
316
  url: prUrl,
307
317
  prdItems: confirmedItemId ? [confirmedItemId] : [],
308
318
  });
319
+ if (confirmedItemId) addPrLink(prId, confirmedItemId);
309
320
  existingIds.add(prId);
310
321
  projectAdded++;
311
322
 
@@ -519,7 +519,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
519
519
  const resultText = parsed.result;
520
520
  const createdPattern = /(?:created|opened|submitted|new PR|PR created)[^\n]*?(?:(?:visualstudio\.com|dev\.azure\.com)[^\s"]*?pullrequest\/(\d+)|github\.com\/[^\s"]*?\/pull\/(\d+))/gi;
521
521
  while ((match = createdPattern.exec(resultText)) !== null) prMatches.add(match[1] || match[2]);
522
- const createdIdPattern = /(?:created|opened|submitted|new)\s+PR[# -]*(\d{5,})/gi;
522
+ const createdIdPattern = /(?:created|opened|submitted|new)\s+PR[# -]*(\d{1,})/gi;
523
523
  while ((match = createdIdPattern.exec(resultText)) !== null) prMatches.add(match[1]);
524
524
  }
525
525
  } catch {}
@@ -586,6 +586,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
586
586
  status: 'active',
587
587
  created: e.dateStamp(),
588
588
  url: targetProject.prUrlBase ? targetProject.prUrlBase + prId : '',
589
+ prdItems: meta?.item?.id ? [meta.item.id] : [],
589
590
  sourcePlan: meta?.item?.sourcePlan || '',
590
591
  itemType: meta?.item?.itemType || ''
591
592
  });
package/engine.js CHANGED
@@ -1211,17 +1211,22 @@ function writeInboxAlert(slug, content) {
1211
1211
  } catch {}
1212
1212
  }
1213
1213
 
1214
- // Reconciles work items against known PRs via exact prdItems match only.
1215
- // reconcilePrs (ado.js) and syncPrsFromOutput (lifecycle.js) are responsible for
1216
- // correctly populating prdItems on PRs this function just reads that linkage.
1214
+ // Reconciles work items against known PRs.
1215
+ // Primary linkage comes from prdItems in pull-requests.json; fallback linkage
1216
+ // uses engine/pr-links.json so matching does not depend on branch/title parsing.
1217
1217
  // onlyIds: if provided, only items whose ID is in this Set are eligible.
1218
1218
  function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
1219
+ const prLinks = shared.getPrLinks();
1219
1220
  let reconciled = 0;
1220
1221
  for (const wi of items) {
1221
1222
  if (wi.status !== 'pending' || wi._pr) continue;
1222
1223
  if (onlyIds && !onlyIds.has(wi.id)) continue;
1223
1224
 
1224
- const exactPr = allPrs.find(pr => (pr.prdItems || []).includes(wi.id));
1225
+ let exactPr = allPrs.find(pr => (pr.prdItems || []).includes(wi.id));
1226
+ if (!exactPr) {
1227
+ const linkedPrId = Object.keys(prLinks).find(prId => prLinks[prId] === wi.id);
1228
+ if (linkedPrId) exactPr = allPrs.find(pr => pr.id === linkedPrId) || { id: linkedPrId };
1229
+ }
1225
1230
  if (exactPr) {
1226
1231
  wi.status = 'done';
1227
1232
  wi._pr = exactPr.id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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"