@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 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">&#x1F4C4; 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
- const reviewSource = sq.status || pr.reviewStatus || 'pending';
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)' : (pr.reviewStatus || 'pending');
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 = require('path').join(shared.MINIONS_DIR, 'work-items.json');
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 || []).map(c => ({ ...c, _type: 'review' })))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
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"