@yemi33/minions 0.1.521 → 0.1.523

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,10 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.521 (2026-04-07)
3
+ ## 0.1.523 (2026-04-07)
4
+
5
+ ### Other
6
+ - perf: parallelize ADO PR polling within each project
7
+
8
+ ## 0.1.522 (2026-04-07)
4
9
 
5
10
  ### Features
6
11
  - test isolation — tests no longer pollute production data
7
12
 
13
+ ### Fixes
14
+ - reconcile failed work items that have attached PRs (#407) (#430)
15
+
8
16
  ### Other
9
17
  - perf: reduce curl timeout to 5s, cache getDispatch() reads
10
18
 
package/engine/ado.js CHANGED
@@ -85,17 +85,20 @@ async function forEachActivePr(config, token, callback) {
85
85
  if (activePrs.length === 0) continue;
86
86
 
87
87
  let projectUpdated = 0;
88
-
89
- for (const pr of activePrs) {
90
- const prNum = (pr.id || '').replace('PR-', '');
91
- if (!prNum) continue;
92
-
93
- try {
94
- const orgBase = getAdoOrgBase(project);
95
- const updated = await callback(project, pr, prNum, orgBase);
96
- if (updated) projectUpdated++;
97
- } catch (err) {
98
- log('warn', `Failed to poll status for ${pr.id}: ${err.message}`);
88
+ const orgBase = getAdoOrgBase(project);
89
+
90
+ // Parallelize PR polling within each project (max 5 concurrent to avoid rate limits)
91
+ const CONCURRENCY = 5;
92
+ for (let i = 0; i < activePrs.length; i += CONCURRENCY) {
93
+ const batch = activePrs.slice(i, i + CONCURRENCY);
94
+ const results = await Promise.allSettled(batch.map(async (pr) => {
95
+ const prNum = (pr.id || '').replace('PR-', '');
96
+ if (!prNum) return false;
97
+ return callback(project, pr, prNum, orgBase);
98
+ }));
99
+ for (const r of results) {
100
+ if (r.status === 'fulfilled' && r.value) projectUpdated++;
101
+ if (r.status === 'rejected') log('warn', `PR poll error: ${r.reason?.message || r.reason}`);
99
102
  }
100
103
  }
101
104
 
package/engine/cleanup.js CHANGED
@@ -431,7 +431,30 @@ function runCleanup(config, verbose = false) {
431
431
  }
432
432
  } catch (e) { log('warn', 'KB watchdog check: ' + e.message); }
433
433
 
434
- // 6. Migrate legacy work-item statuses to canonical 'done'
434
+ // 6a. Reconcile failed work items that have an attached PR (#407)
435
+ // If a work item is 'failed' but already has _pr, it should be 'done'.
436
+ for (const project of projects) {
437
+ try {
438
+ const wiPath = projectWorkItemsPath(project);
439
+ const items = safeJson(wiPath) || [];
440
+ let reconciled = 0;
441
+ for (const item of items) {
442
+ if (item.status === shared.WI_STATUS.FAILED && item._pr) {
443
+ item.status = shared.WI_STATUS.DONE;
444
+ if (item.failReason) delete item.failReason;
445
+ if (item.failedAt) delete item.failedAt;
446
+ if (!item.completedAt) item.completedAt = shared.ts();
447
+ reconciled++;
448
+ }
449
+ }
450
+ if (reconciled > 0) {
451
+ safeWrite(wiPath, items);
452
+ log('info', `Reconciled ${reconciled} failed-with-PR item(s) → done in ${project.name}`);
453
+ }
454
+ } catch (e) { log('warn', 'reconcile failed-with-PR: ' + e.message); }
455
+ }
456
+
457
+ // 6b. Migrate legacy work-item statuses to canonical 'done'
435
458
  // in-pr, implemented, complete → done (one-time correction per item)
436
459
  const LEGACY_DONE_ALIASES = new Set(['in-pr', 'implemented', 'complete']);
437
460
  for (const project of projects) {
@@ -1372,7 +1372,8 @@ function syncPrdFromPrs(config) {
1372
1372
  for (const project of allProjects) {
1373
1373
  const wiPath = shared.projectWorkItemsPath(project);
1374
1374
  const items = safeJson(wiPath) || [];
1375
- const hasReconcilable = items.some(wi => (wi.status === WI_STATUS.PENDING || wi.status === WI_STATUS.FAILED) && !wi._pr);
1375
+ const hasReconcilable = items.some(wi =>
1376
+ (wi.status === WI_STATUS.PENDING && !wi._pr) || wi.status === WI_STATUS.FAILED);
1376
1377
  if (!hasReconcilable) continue;
1377
1378
  let reconciled = 0;
1378
1379
  const reconciledItems = mutateJsonFileLocked(wiPath, data => {
@@ -1389,7 +1390,7 @@ function syncPrdFromPrs(config) {
1389
1390
  }
1390
1391
  }
1391
1392
  if (totalReconciled > 0) {
1392
- log('info', `PR sync: reconciled ${totalReconciled} pending work item(s) to done`);
1393
+ log('info', `PR sync: reconciled ${totalReconciled} work item(s) to done`);
1393
1394
  }
1394
1395
  } catch (err) {
1395
1396
  // Non-fatal — log and continue
package/engine.js CHANGED
@@ -878,6 +878,16 @@ function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
878
878
  if (wi.status !== WI_STATUS.PENDING && wi.status !== WI_STATUS.FAILED) continue;
879
879
  if (onlyIds && !onlyIds.has(wi.id)) continue;
880
880
 
881
+ // Short-circuit: failed item already has a PR attached — mark done directly (#407)
882
+ if (wi.status === WI_STATUS.FAILED && wi._pr) {
883
+ wi.status = WI_STATUS.DONE;
884
+ if (wi.failReason) delete wi.failReason;
885
+ if (wi.failedAt) delete wi.failedAt;
886
+ if (!wi.completedAt) wi.completedAt = ts();
887
+ reconciled++;
888
+ continue;
889
+ }
890
+
881
891
  let exactPr = allPrs.find(pr => (pr.prdItems || []).includes(wi.id));
882
892
  if (!exactPr) {
883
893
  const linkedPrId = Object.keys(prLinks).find(prId => prLinks[prId] === wi.id);
@@ -893,7 +903,7 @@ function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
893
903
  // Clear failure artifacts if reconciling a previously failed item
894
904
  if (wi.failReason) delete wi.failReason;
895
905
  if (wi.failedAt) delete wi.failedAt;
896
- if (!wi.completedAt) wi.completedAt = new Date().toISOString();
906
+ if (!wi.completedAt) wi.completedAt = ts();
897
907
  reconciled++;
898
908
  }
899
909
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.521",
3
+ "version": "0.1.523",
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"