@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 +17 -0
- package/dashboard.html +11 -9
- package/dashboard.js +32 -34
- package/engine/ado.js +6 -0
- package/engine/github.js +14 -3
- package/engine/lifecycle.js +2 -1
- package/engine.js +9 -4
- package/package.json +1 -1
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
|
-
|
|
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 & 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
|
|
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 +
|
|
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
|
-
//
|
|
1493
|
-
let
|
|
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
|
-
//
|
|
1519
|
-
if (
|
|
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 === '
|
|
1522
|
-
|
|
1523
|
-
w.
|
|
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.
|
|
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
|
|
1558
|
-
if (killedAgents.
|
|
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 =>
|
|
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',
|
|
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
|
|
package/engine/lifecycle.js
CHANGED
|
@@ -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{
|
|
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
|
|
1215
|
-
//
|
|
1216
|
-
//
|
|
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
|
-
|
|
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