@yemi33/minions 0.1.739 → 0.1.741

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.741 (2026-04-09)
4
+
5
+ ### Features
6
+ - add prNumber field to pull-requests.json records (#711)
7
+
8
+ ### Fixes
9
+ - handle NUL pseudo-file in Windows worktree cleanup (#731)
10
+
3
11
  ## 0.1.739 (2026-04-09)
4
12
 
5
13
  ### Fixes
package/engine/ado.js CHANGED
@@ -561,10 +561,14 @@ async function reconcilePrs(config) {
561
561
  const confirmedItemId = linkedItem ? linkedItemId : null;
562
562
 
563
563
  if (existingIds.has(prId)) {
564
+ const existing = existingPrs.find(p => p.id === prId);
565
+ // Backfill prNumber for existing records missing it
566
+ if (existing && existing.prNumber == null) {
567
+ existing.prNumber = adoPr.pullRequestId;
568
+ }
564
569
  // PR already tracked — write link to pr-links.json if we can extract an ID
565
570
  if (confirmedItemId) {
566
571
  addPrLink(prId, confirmedItemId);
567
- const existing = existingPrs.find(p => p.id === prId);
568
572
  if (existing && !(existing.prdItems || []).includes(confirmedItemId)) {
569
573
  existing.prdItems = Array.isArray(existing.prdItems) ? existing.prdItems : [];
570
574
  existing.prdItems.push(confirmedItemId);
@@ -582,6 +586,7 @@ async function reconcilePrs(config) {
582
586
  const prUrl = project.prUrlBase ? project.prUrlBase + adoPr.pullRequestId : '';
583
587
  existingPrs.push({
584
588
  id: prId,
589
+ prNumber: adoPr.pullRequestId,
585
590
  title: (adoPr.title || `PR #${adoPr.pullRequestId}`).slice(0, 120),
586
591
  agent: (linkedItem?.dispatched_to || adoPr.createdBy?.displayName || 'unknown').toLowerCase(),
587
592
  branch,
@@ -597,6 +602,14 @@ async function reconcilePrs(config) {
597
602
  log('info', `PR reconciliation: added ${prId} (branch: ${branch}, linked to ${confirmedItemId}) to ${project.name}`);
598
603
  }
599
604
 
605
+ // Backfill prNumber from pr.id for any PR missing it (e.g. created before prNumber was stored)
606
+ for (const pr of existingPrs) {
607
+ if (pr.prNumber == null) {
608
+ const derived = parseInt((pr.id || '').replace(/^PR-/, ''), 10);
609
+ if (derived) pr.prNumber = derived;
610
+ }
611
+ }
612
+
600
613
  // Backfill prdItems from pr-links for any PR with empty array
601
614
  const prLinks = shared.getPrLinks();
602
615
  let backfilled = 0;
package/engine/github.js CHANGED
@@ -562,9 +562,13 @@ async function reconcilePrs(config) {
562
562
  const confirmedItemId = linkedItem ? linkedItemId : null;
563
563
 
564
564
  if (existingIds.has(prId)) {
565
+ const existing = currentPrs.find(p => p.id === prId);
566
+ // Backfill prNumber for existing records missing it
567
+ if (existing && existing.prNumber == null) {
568
+ existing.prNumber = ghPr.number;
569
+ }
565
570
  if (confirmedItemId) {
566
571
  addPrLink(prId, confirmedItemId);
567
- const existing = currentPrs.find(p => p.id === prId);
568
572
  if (existing && !(existing.prdItems || []).includes(confirmedItemId)) {
569
573
  existing.prdItems = Array.isArray(existing.prdItems) ? existing.prdItems : [];
570
574
  existing.prdItems.push(confirmedItemId);
@@ -580,6 +584,7 @@ async function reconcilePrs(config) {
580
584
 
581
585
  currentPrs.push({
582
586
  id: prId,
587
+ prNumber: ghPr.number,
583
588
  title: (ghPr.title || `PR #${ghPr.number}`).slice(0, 120),
584
589
  agent: (linkedItem?.dispatched_to || ghPr.user?.login || 'unknown').toLowerCase(),
585
590
  branch,
@@ -596,6 +601,14 @@ async function reconcilePrs(config) {
596
601
  log('info', `GitHub PR reconciliation: added ${prId} (branch: ${branch}, linked to ${confirmedItemId}) to ${project.name}`);
597
602
  }
598
603
 
604
+ // Backfill prNumber from pr.id for any PR missing it (e.g. created before prNumber was stored)
605
+ for (const pr of currentPrs) {
606
+ if (pr.prNumber == null) {
607
+ const derived = parseInt((pr.id || '').replace(/^PR-/, ''), 10);
608
+ if (derived) pr.prNumber = derived;
609
+ }
610
+ }
611
+
599
612
  // Backfill prdItems from pr-links for any PR with empty array
600
613
  const prLinks = getPrLinks();
601
614
  let backfilled = 0;
@@ -666,6 +666,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
666
666
  prId, fullId,
667
667
  entry: {
668
668
  id: fullId,
669
+ prNumber: parseInt(prId, 10) || prId,
669
670
  title: (title || `PR created by ${agentName}`).slice(0, 120),
670
671
  agent: agentName,
671
672
  branch: meta?.branch || '',
package/engine/shared.js CHANGED
@@ -857,8 +857,49 @@ function mutatePullRequests(filePath, mutator) {
857
857
  * Remove a git worktree, falling back to fs.rmSync if git fails (e.g., locked on Windows).
858
858
  * Only removes directories under worktreeRoot to prevent accidental deletion.
859
859
  * Tracks persistent failures to avoid retrying locked paths every cleanup cycle.
860
+ *
861
+ * On Windows, reserved device-name files (NUL, CON, PRN, AUX, etc.) can appear in
862
+ * worktree directories when shell redirections run under Git Bash/WSL. These block
863
+ * git worktree remove, fs.rmSync, and PowerShell Remove-Item. Two mitigations:
864
+ * 1. _purgeReservedFiles() deletes them via the \\?\ extended path prefix before removal
865
+ * 2. cmd /c rd /s /q as final fallback handles any remaining reserved names
860
866
  */
861
867
  const _removeWorktreeFailures = new Map(); // path → { count, lastAttempt }
868
+
869
+ // Windows reserved device names that cannot be deleted via normal paths
870
+ const _WIN_RESERVED_NAMES = new Set([
871
+ 'CON', 'PRN', 'AUX', 'NUL',
872
+ 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
873
+ 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
874
+ ]);
875
+
876
+ /**
877
+ * Recursively purge Windows reserved-name pseudo-files (NUL, CON, PRN, AUX, etc.)
878
+ * using the \\?\ extended path prefix that bypasses reserved-name interpretation.
879
+ * Called before normal deletion attempts on Windows to unblock git/fs operations.
880
+ */
881
+ function _purgeReservedFiles(dirPath) {
882
+ let entries;
883
+ try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); } catch { return; }
884
+ for (const entry of entries) {
885
+ const fullPath = path.join(dirPath, entry.name);
886
+ try {
887
+ if (entry.isDirectory()) {
888
+ _purgeReservedFiles(fullPath);
889
+ } else {
890
+ // Match NUL, NUL.txt, con, con.log, etc.
891
+ const baseName = entry.name.toUpperCase().split('.')[0];
892
+ if (_WIN_RESERVED_NAMES.has(baseName)) {
893
+ // \\?\ prefix bypasses Win32 reserved-name interpretation
894
+ fs.unlinkSync('\\\\?\\' + fullPath);
895
+ }
896
+ }
897
+ } catch {
898
+ // Best-effort: file may already be gone or inaccessible
899
+ }
900
+ }
901
+ }
902
+
862
903
  function removeWorktree(wtPath, gitRoot, worktreeRoot) {
863
904
  const resolved = path.resolve(wtPath);
864
905
  const resolvedRoot = path.resolve(worktreeRoot) + path.sep;
@@ -870,6 +911,11 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot) {
870
911
  const prior = _removeWorktreeFailures.get(resolved);
871
912
  if (prior && prior.count >= 3 && Date.now() - prior.lastAttempt < 3600000) return false;
872
913
 
914
+ // Windows: purge reserved-name pseudo-files (NUL, CON, etc.) that block normal deletion
915
+ if (process.platform === 'win32') {
916
+ _purgeReservedFiles(resolved);
917
+ }
918
+
873
919
  try {
874
920
  exec(`git worktree remove "${wtPath}" --force`, { cwd: gitRoot, stdio: 'pipe', timeout: 15000, windowsHide: true });
875
921
  _removeWorktreeFailures.delete(resolved);
@@ -881,14 +927,17 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot) {
881
927
  _removeWorktreeFailures.delete(resolved);
882
928
  return true;
883
929
  } catch (rmErr) {
884
- // Windows EPERM: a process may hold file handles — try rd /s /q as fallback
885
- if (process.platform === 'win32' && rmErr.code === 'EPERM') {
930
+ // Windows: try cmd /c rd /s /q for any error — handles reserved device names,
931
+ // locked files, and partially-deleted directories (not just EPERM)
932
+ if (process.platform === 'win32') {
886
933
  try {
887
- exec(`rd /s /q "${resolved}"`, { stdio: 'pipe', timeout: 15000, windowsHide: true });
934
+ exec(`cmd /c rd /s /q "${resolved}"`, { stdio: 'pipe', timeout: 15000, windowsHide: true });
888
935
  try { exec('git worktree prune', { cwd: gitRoot, stdio: 'pipe', timeout: 10000, windowsHide: true }); } catch {}
889
936
  _removeWorktreeFailures.delete(resolved);
890
937
  return true;
891
- } catch {}
938
+ } catch (rdErr) {
939
+ log('warn', `removeWorktree: rd /s /q fallback failed for ${wtPath}: ${rdErr.message}`);
940
+ }
892
941
  }
893
942
  const fail = _removeWorktreeFailures.get(resolved) || { count: 0, lastAttempt: 0 };
894
943
  fail.count++;
@@ -963,6 +1012,8 @@ module.exports = {
963
1012
  killGracefully,
964
1013
  killImmediate,
965
1014
  removeWorktree,
1015
+ _purgeReservedFiles, // exported for testing
1016
+ _WIN_RESERVED_NAMES, // exported for testing
966
1017
  LOCK_STALE_MS,
967
1018
  flushLogs,
968
1019
  slugify,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.739",
3
+ "version": "0.1.741",
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"