@yemi33/minions 0.1.96 → 0.1.98
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/js/render-prd.js +2 -3
- package/dashboard/js/render-prs.js +4 -2
- package/dashboard.js +76 -0
- package/engine/ado.js +8 -1
- package/engine/github.js +6 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.98 (2026-04-01)
|
|
4
|
+
|
|
5
|
+
### Engine
|
|
6
|
+
- engine/ado.js
|
|
7
|
+
- engine/github.js
|
|
8
|
+
|
|
9
|
+
## 0.1.97 (2026-04-01)
|
|
10
|
+
|
|
11
|
+
### Engine
|
|
12
|
+
- engine/ado.js
|
|
13
|
+
- engine/github.js
|
|
14
|
+
|
|
15
|
+
### Dashboard
|
|
16
|
+
- dashboard.js
|
|
17
|
+
- dashboard/js/render-prd.js
|
|
18
|
+
- dashboard/js/render-prs.js
|
|
19
|
+
|
|
3
20
|
## 0.1.96 (2026-04-01)
|
|
4
21
|
|
|
5
22
|
### Engine
|
|
@@ -238,9 +238,7 @@ function renderPrdProgress(prog) {
|
|
|
238
238
|
: isCompleted
|
|
239
239
|
? '<span onclick="event.stopPropagation();triggerVerify(\'' + escHtml(g.file) + '\',this)" style="color:var(--green);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">Verify</span>'
|
|
240
240
|
: '<span onclick="event.stopPropagation();planPause(\'' + escHtml(g.file) + '\',this)" style="color:var(--yellow);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.3);border-radius:3px">Pause</span>';
|
|
241
|
-
const archiveBtn = isCompleted
|
|
242
|
-
? '<span onclick="event.stopPropagation();planArchive(\'' + escHtml(g.file) + '\',this)" style="color:var(--muted);cursor:pointer;font-size:9px;padding:1px 6px;background:var(--surface);border:1px solid var(--border);border-radius:3px">Archive</span>'
|
|
243
|
-
: '';
|
|
241
|
+
const archiveBtn = (isCompleted || isPaused) ? '<span onclick="event.stopPropagation();planArchive(\'' + escHtml(g.file) + '\',this)" style="color:var(--muted);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(139,148,158,0.1);border:1px solid rgba(139,148,158,0.3);border-radius:3px">Archive</span>' : '';
|
|
244
242
|
const deleteBtn = '<span onclick="event.stopPropagation();planDelete(\'' + escHtml(g.file) + '\')" style="color:var(--red);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.3);border-radius:3px">Delete</span>';
|
|
245
243
|
const sourcePlanLink = g.sourcePlan
|
|
246
244
|
? '<span onclick="event.stopPropagation();planView(\'' + escHtml(g.sourcePlan) + '\')" style="color:var(--blue);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(56,139,253,0.1);border:1px solid rgba(56,139,253,0.3);border-radius:3px" title="View source plan">📄 Plan</span>'
|
|
@@ -509,6 +507,7 @@ function showArchivedPrdDetail(idx) {
|
|
|
509
507
|
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' + (isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'graph\';showArchivedPrdDetail(' + idx + ')">Graph</button>' +
|
|
510
508
|
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' + (!isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'list\';showArchivedPrdDetail(' + idx + ')">List</button>' +
|
|
511
509
|
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);margin-left:auto" onclick="triggerVerify(\'' + escHtml(g.file) + '\')">Trigger Verify</button>' +
|
|
510
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="planUnarchive(\'' + escHtml(g.file) + '\',this)">Unarchive</button>' +
|
|
512
511
|
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="openArchivedPrdModal()">Back</button>' +
|
|
513
512
|
'</div>';
|
|
514
513
|
|
|
@@ -7,9 +7,11 @@ const PR_PER_PAGE = 25;
|
|
|
7
7
|
function prRow(pr) {
|
|
8
8
|
// Minions review (agent) state — separate from ADO human review
|
|
9
9
|
const sq = pr.minionsReview || {};
|
|
10
|
-
|
|
10
|
+
// If PR is merged/abandoned, treat 'waiting' review as resolved
|
|
11
|
+
const effectiveReviewStatus = (pr.status === 'merged' || pr.status === 'abandoned') && pr.reviewStatus === 'waiting' ? (pr.status === 'merged' ? 'approved' : 'pending') : pr.reviewStatus;
|
|
12
|
+
const reviewSource = sq.status || effectiveReviewStatus || 'pending';
|
|
11
13
|
const reviewClass = reviewSource === 'approved' ? 'approved' : (reviewSource === 'changes-requested' || reviewSource === 'rejected') ? 'rejected' : reviewSource === 'waiting' ? 'building' : 'draft';
|
|
12
|
-
const reviewLabel = sq.status === 'waiting' ? 'reviewing (minions)' : sq.status ? sq.status + ' (minions)' : (
|
|
14
|
+
const reviewLabel = sq.status === 'waiting' ? 'reviewing (minions)' : sq.status ? sq.status + ' (minions)' : (effectiveReviewStatus || 'pending');
|
|
13
15
|
const buildClass = pr.buildStatus === 'passing' ? 'build-pass' : pr.buildStatus === 'failing' ? 'build-fail' : pr.buildStatus === 'running' ? 'building' : 'no-build';
|
|
14
16
|
const buildLabel = pr.buildStatus || 'none';
|
|
15
17
|
const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
|
package/dashboard.js
CHANGED
|
@@ -1647,6 +1647,80 @@ If nothing to do, return: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1647
1647
|
return jsonReply(res, 200, plans);
|
|
1648
1648
|
}
|
|
1649
1649
|
|
|
1650
|
+
async function handlePlansArchiveMove(req, res) {
|
|
1651
|
+
try {
|
|
1652
|
+
const body = await readBody(req);
|
|
1653
|
+
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
1654
|
+
const file = body.file;
|
|
1655
|
+
if (file.includes('..') || file.includes('\0')) return jsonReply(res, 400, { error: 'invalid' });
|
|
1656
|
+
|
|
1657
|
+
const isJson = file.endsWith('.json');
|
|
1658
|
+
const sourceDir = isJson ? PRD_DIR : PLANS_DIR;
|
|
1659
|
+
const archiveDir = path.join(sourceDir, 'archive');
|
|
1660
|
+
const sourcePath = path.join(sourceDir, file);
|
|
1661
|
+
|
|
1662
|
+
if (!fs.existsSync(sourcePath)) return jsonReply(res, 404, { error: 'File not found' });
|
|
1663
|
+
if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
|
|
1664
|
+
|
|
1665
|
+
fs.renameSync(sourcePath, path.join(archiveDir, file));
|
|
1666
|
+
|
|
1667
|
+
// If archiving a PRD .json, also archive its source .md plan
|
|
1668
|
+
let archivedSource = null;
|
|
1669
|
+
if (isJson) {
|
|
1670
|
+
try {
|
|
1671
|
+
const prd = safeJson(path.join(archiveDir, file));
|
|
1672
|
+
if (prd?.source_plan) {
|
|
1673
|
+
const mdPath = path.join(PLANS_DIR, prd.source_plan);
|
|
1674
|
+
if (fs.existsSync(mdPath)) {
|
|
1675
|
+
const planArchive = path.join(PLANS_DIR, 'archive');
|
|
1676
|
+
if (!fs.existsSync(planArchive)) fs.mkdirSync(planArchive, { recursive: true });
|
|
1677
|
+
fs.renameSync(mdPath, path.join(planArchive, prd.source_plan));
|
|
1678
|
+
archivedSource = prd.source_plan;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
} catch { /* optional — source plan may not exist */ }
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
invalidateStatusCache();
|
|
1685
|
+
return jsonReply(res, 200, { ok: true, archivedSource });
|
|
1686
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
async function handlePlansUnarchive(req, res) {
|
|
1690
|
+
try {
|
|
1691
|
+
const body = await readBody(req);
|
|
1692
|
+
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
1693
|
+
const file = body.file;
|
|
1694
|
+
if (file.includes('..') || file.includes('\0')) return jsonReply(res, 400, { error: 'invalid' });
|
|
1695
|
+
|
|
1696
|
+
const isJson = file.endsWith('.json');
|
|
1697
|
+
const targetDir = isJson ? PRD_DIR : PLANS_DIR;
|
|
1698
|
+
const archiveDir = path.join(targetDir, 'archive');
|
|
1699
|
+
const archivePath = path.join(archiveDir, file);
|
|
1700
|
+
|
|
1701
|
+
if (!fs.existsSync(archivePath)) return jsonReply(res, 404, { error: 'File not found in archive' });
|
|
1702
|
+
fs.renameSync(archivePath, path.join(targetDir, file));
|
|
1703
|
+
|
|
1704
|
+
// If unarchiving a PRD .json, also unarchive its source .md plan
|
|
1705
|
+
let unarchivedSource = null;
|
|
1706
|
+
if (isJson) {
|
|
1707
|
+
try {
|
|
1708
|
+
const prd = safeJson(path.join(targetDir, file));
|
|
1709
|
+
if (prd?.source_plan) {
|
|
1710
|
+
const mdArchivePath = path.join(PLANS_DIR, 'archive', prd.source_plan);
|
|
1711
|
+
if (fs.existsSync(mdArchivePath)) {
|
|
1712
|
+
fs.renameSync(mdArchivePath, path.join(PLANS_DIR, prd.source_plan));
|
|
1713
|
+
unarchivedSource = prd.source_plan;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
} catch { /* optional */ }
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
invalidateStatusCache();
|
|
1720
|
+
return jsonReply(res, 200, { ok: true, unarchivedSource });
|
|
1721
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1650
1724
|
async function handlePlansArchiveRead(req, res, match) {
|
|
1651
1725
|
const file = decodeURIComponent(match[1]);
|
|
1652
1726
|
if (file.includes('..') || file.includes('\0')) return jsonReply(res, 400, { error: 'invalid' });
|
|
@@ -3162,6 +3236,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3162
3236
|
{ method: 'POST', path: '/api/plans/unarchive', desc: 'Restore a plan/PRD from archive', params: 'file', handler: handlePlansUnarchive },
|
|
3163
3237
|
{ method: 'POST', path: '/api/plans/revise', desc: 'Request revision with feedback, dispatches agent to revise', params: 'file, feedback, requestedBy?', handler: handlePlansRevise },
|
|
3164
3238
|
{ method: 'POST', path: '/api/plans/discuss', desc: 'Generate a plan discussion session script for Claude CLI', params: 'file', handler: handlePlansDiscuss },
|
|
3239
|
+
{ method: 'POST', path: '/api/plans/archive', desc: 'Archive a plan/PRD (move to archive folder)', params: 'file', handler: handlePlansArchiveMove },
|
|
3240
|
+
{ method: 'POST', path: '/api/plans/unarchive', desc: 'Unarchive a plan/PRD (restore from archive folder)', params: 'file', handler: handlePlansUnarchive },
|
|
3165
3241
|
{ method: 'GET', path: /^\/api\/plans\/archive\/([^?]+)$/, desc: 'Read an archived plan file', handler: handlePlansArchiveRead },
|
|
3166
3242
|
{ method: 'GET', path: /^\/api\/plans\/([^?]+)$/, desc: 'Read a full plan (JSON from prd/ or markdown from plans/)', handler: handlePlansRead },
|
|
3167
3243
|
|
package/engine/ado.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Extracted from engine.js: ADO token management, PR status polling, human comment polling.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const path = require('path');
|
|
6
7
|
const shared = require('./shared');
|
|
7
8
|
const { exec, getAdoOrgBase, addPrLink } = shared;
|
|
8
9
|
const { getPrs } = require('./queries');
|
|
@@ -26,6 +27,8 @@ function getAdoToken() {
|
|
|
26
27
|
// If recent fetch failed, don't retry until backoff expires (avoids repeated browser popups)
|
|
27
28
|
if (Date.now() < _adoTokenFailedUntil) return null;
|
|
28
29
|
try {
|
|
30
|
+
// azureauth supports multiple --mode flags as an ordered fallback chain:
|
|
31
|
+
// tries IWA (Integrated Windows Auth) first, falls back to broker if unavailable.
|
|
29
32
|
const token = exec('azureauth ado token --mode iwa --mode broker --output token --timeout 1', {
|
|
30
33
|
timeout: 15000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }).trim();
|
|
31
34
|
if (token && token.startsWith('eyJ')) {
|
|
@@ -131,6 +134,10 @@ async function pollPrStatus(config) {
|
|
|
131
134
|
updated = true;
|
|
132
135
|
|
|
133
136
|
if (newStatus === 'merged' || newStatus === 'abandoned') {
|
|
137
|
+
if (pr.reviewStatus === 'waiting') {
|
|
138
|
+
pr.reviewStatus = newStatus === 'merged' ? 'approved' : 'pending';
|
|
139
|
+
e.log('info', `PR ${pr.id} reviewStatus: waiting → ${pr.reviewStatus} (${newStatus})`);
|
|
140
|
+
}
|
|
134
141
|
await engine().handlePostMerge(pr, project, config, newStatus);
|
|
135
142
|
}
|
|
136
143
|
}
|
|
@@ -334,7 +341,7 @@ async function reconcilePrs(config) {
|
|
|
334
341
|
// Load work items to match branches to work item IDs
|
|
335
342
|
const wiPath = shared.projectWorkItemsPath(project);
|
|
336
343
|
const workItems = shared.safeJson(wiPath) || [];
|
|
337
|
-
const centralWiPath =
|
|
344
|
+
const centralWiPath = path.join(shared.MINIONS_DIR, 'work-items.json');
|
|
338
345
|
const centralItems = shared.safeJson(centralWiPath) || [];
|
|
339
346
|
const allItems = [...workItems, ...centralItems];
|
|
340
347
|
|
package/engine/github.js
CHANGED
|
@@ -135,6 +135,11 @@ async function pollPrStatus(config) {
|
|
|
135
135
|
updated = true;
|
|
136
136
|
|
|
137
137
|
if (newStatus === 'merged' || newStatus === 'abandoned') {
|
|
138
|
+
// Resolve stale 'waiting' review status — won't be polled again after this
|
|
139
|
+
if (pr.reviewStatus === 'waiting') {
|
|
140
|
+
pr.reviewStatus = newStatus === 'merged' ? 'approved' : 'pending';
|
|
141
|
+
e.log('info', `PR ${pr.id} reviewStatus: waiting → ${pr.reviewStatus} (${newStatus})`);
|
|
142
|
+
}
|
|
138
143
|
await engine().handlePostMerge(pr, project, config, newStatus);
|
|
139
144
|
}
|
|
140
145
|
}
|
|
@@ -234,7 +239,7 @@ async function pollPrHumanComments(config) {
|
|
|
234
239
|
const reviewComments = ghApi(`/pulls/${prNum}/comments`, slug);
|
|
235
240
|
const allComments = [
|
|
236
241
|
...(comments || []).map(c => ({ ...c, _type: 'issue' })),
|
|
237
|
-
...((reviewComments
|
|
242
|
+
...(Array.isArray(reviewComments) ? reviewComments : []).map(c => ({ ...c, _type: 'review' }))
|
|
238
243
|
];
|
|
239
244
|
|
|
240
245
|
// Filter out bot comments and minions's own comments
|
package/package.json
CHANGED