@yemi33/minions 0.1.1551 → 0.1.1553

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1553 (2026-04-27)
4
+
5
+ ### Other
6
+ - refactor(queries): cache PR list, fold ghost-project scan into single readdir pass
7
+
8
+ ## 0.1.1552 (2026-04-27)
9
+
10
+ ### Fixes
11
+ - scan all project subdirs for PR data in PRD view
12
+
3
13
  ## 0.1.1551 (2026-04-27)
4
14
 
5
15
  ### Fixes
@@ -231,8 +231,9 @@ function evaluateCondition(condition, ctx) {
231
231
  return pipelineRuns.length >= threshold;
232
232
  }
233
233
  case 'allBuildsGreen': {
234
- // True when all PRs in monitoredResources (pipeline-level or stage-level) or run artifacts have buildStatus 'passing'
235
- const allPrs = queries.getPullRequests(config);
234
+ // True when all PRs in monitoredResources (pipeline-level or stage-level) or run artifacts have buildStatus 'passing'.
235
+ // Exclude ghost PRs (records from project subdirs not in config) — their buildStatus is stale because no poller updates them.
236
+ const allPrs = queries.getPullRequests(config).filter(pr => !pr._ghost);
236
237
  const prRefs = collectPipelinePrRefs(pipeline, run);
237
238
  if (prRefs.length === 0) return false; // no PRs to check = can't confirm green
238
239
  for (const prRef of prRefs) {
package/engine/queries.js CHANGED
@@ -11,7 +11,7 @@ const shared = require('./shared');
11
11
 
12
12
  const { safeRead, safeReadDir, safeJson, safeWrite, getProjects, mutateJsonFileLocked,
13
13
  projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES,
14
- WI_STATUS, DONE_STATUSES, PRD_ITEM_STATUS, ENGINE_DEFAULTS } = shared;
14
+ WI_STATUS, DONE_STATUSES, PRD_ITEM_STATUS, PR_STATUS, ENGINE_DEFAULTS } = shared;
15
15
 
16
16
  /**
17
17
  * Read the first `bytes` and last `bytes` of a file efficiently using byte offsets.
@@ -472,34 +472,57 @@ function getPrs(project) {
472
472
  return all;
473
473
  }
474
474
 
475
+ // Cache: getPullRequests is called 3-5x per /api/status (getMetrics, getWorkItems,
476
+ // getPrdInfo, dashboard.js status + count). 1s TTL eliminates redundant fs reads
477
+ // within a single request without masking real updates from polling.
478
+ let _prsCache = null;
479
+ let _prsCacheAt = 0;
480
+
475
481
  function getPullRequests(config) {
482
+ const now = Date.now();
483
+ if (_prsCache && (now - _prsCacheAt) < 1000) return _prsCache;
476
484
  config = config || getConfig();
477
485
  const projects = getProjects(config);
486
+ const projectByName = new Map(projects.map(p => [p.name, p]));
478
487
  const allPrs = [];
479
- for (const project of projects) {
480
- const prs = safeJson(projectPrPath(project));
481
- if (!prs) continue;
488
+ const seenIds = new Set();
489
+ // Single pass over projects/* — configured projects use their full config
490
+ // (prUrlBase fill-in, _project name); unconfigured subdirs are tagged _ghost
491
+ // so engine code can filter them out. Mirrors what shared.getPrLinks scans,
492
+ // so PRD links and PR records stay in sync after a project is removed.
493
+ let projectDirs = [];
494
+ try {
495
+ projectDirs = fs.readdirSync(path.join(MINIONS_DIR, 'projects'), { withFileTypes: true })
496
+ .filter(d => d.isDirectory()).map(d => d.name);
497
+ } catch { /* projects dir missing */ }
498
+ for (const dirName of projectDirs) {
499
+ const project = projectByName.get(dirName) || null;
500
+ const prPath = project ? projectPrPath(project) : path.join(MINIONS_DIR, 'projects', dirName, 'pull-requests.json');
501
+ const prs = safeJson(prPath);
502
+ if (!Array.isArray(prs)) continue;
482
503
  shared.normalizePrRecords(prs, project);
483
- const base = project.prUrlBase || '';
504
+ const base = project?.prUrlBase || '';
484
505
  for (const pr of prs) {
485
- if (!pr.url && base) {
506
+ if (!pr?.id || seenIds.has(pr.id)) continue;
507
+ if (project && !pr.url && base) {
486
508
  const prNumber = shared.getPrNumber(pr);
487
509
  if (prNumber != null) pr.url = base + prNumber;
488
510
  }
489
- pr._project = project.name || 'Project';
511
+ pr._project = project ? (project.name || 'Project') : dirName;
512
+ if (!project) pr._ghost = true;
490
513
  allPrs.push(pr);
514
+ seenIds.add(pr.id);
491
515
  }
492
516
  }
493
- // Also read central pull-requests.json (for manually linked PRs without a project)
494
- const centralPath = path.join(MINIONS_DIR, 'pull-requests.json');
495
- const centralPrs = safeJson(centralPath);
517
+ // Central pull-requests.json manually linked PRs without a project
518
+ const centralPrs = safeJson(path.join(MINIONS_DIR, 'pull-requests.json'));
496
519
  if (centralPrs) {
497
520
  shared.normalizePrRecords(centralPrs, null);
498
521
  for (const pr of centralPrs) {
499
- if (!allPrs.some(p => p.id === pr.id)) {
500
- pr._project = 'central';
501
- allPrs.push(pr);
502
- }
522
+ if (!pr?.id || seenIds.has(pr.id)) continue;
523
+ pr._project = 'central';
524
+ allPrs.push(pr);
525
+ seenIds.add(pr.id);
503
526
  }
504
527
  }
505
528
  allPrs.sort((a, b) => {
@@ -513,9 +536,35 @@ function getPullRequests(config) {
513
536
  const bNum = parseInt((b.id || '').replace(/\D/g, '')) || 0;
514
537
  return bNum - aNum;
515
538
  });
539
+ _prsCache = allPrs;
540
+ _prsCacheAt = now;
516
541
  return allPrs;
517
542
  }
518
543
 
544
+ // Resolve a PR URL by preferring the canonical PR ID's own scope (e.g.
545
+ // `github:owner/repo#N`) so a github PR never gets an ADO URL just because the
546
+ // only configured project happens to be ADO. Falls back to a name-matched
547
+ // project's prUrlBase only when the ID is legacy (`PR-N`) and a real project
548
+ // owns it. Never blindly uses projects[0].
549
+ function buildPrUrlFromId(prId, pr, projects) {
550
+ if (pr?.url) return pr.url;
551
+ const canonical = shared.parseCanonicalPrId(prId);
552
+ if (canonical) {
553
+ const [host, rest] = canonical.scope.split(':');
554
+ if (host === 'github') return `https://github.com/${rest}/pull/${canonical.prNumber}`;
555
+ if (host === 'ado') {
556
+ const [org, adoProject, repo] = rest.split('/');
557
+ if (org && adoProject && repo) {
558
+ return `https://dev.azure.com/${org}/${adoProject}/_git/${repo}/pullrequest/${canonical.prNumber}`;
559
+ }
560
+ }
561
+ }
562
+ const project = pr?._project ? projects.find(p => p.name === pr._project) : null;
563
+ const prNumber = shared.getPrNumber(pr || prId);
564
+ if (project?.prUrlBase && prNumber != null) return project.prUrlBase + prNumber;
565
+ return '';
566
+ }
567
+
519
568
  // ── Skills ──────────────────────────────────────────────────────────────────
520
569
 
521
570
  function collectSkillFiles(config) {
@@ -1017,36 +1066,13 @@ function getPrdInfo(config) {
1017
1066
  for (const wi of centralWi) { if (!wi?.id) { console.warn('[queries] Skipping central work item without id:', JSON.stringify(wi).slice(0, 120)); continue; } if (wi.sourcePlan && !wiById[wi.id]) wiById[wi.id] = wi; }
1018
1067
  } catch { /* optional */ }
1019
1068
 
1020
- // PR-to-PRD linking — derived from PR.prdItems (single source of truth)
1069
+ // PR-to-PRD linking — derived from PR.prdItems (single source of truth).
1070
+ // getPullRequests includes records from unconfigured project subdirs so PRD
1071
+ // links can resolve to last-known status even after a project is removed.
1021
1072
  const allPrs = getPullRequests(config);
1022
1073
  const prById = {};
1023
1074
  for (const pr of allPrs) prById[pr.id] = pr;
1024
1075
 
1025
- // Build URL for a PR when pr.url isn't set. Prefer the PR ID's own scope
1026
- // (e.g. `github:owner/repo#123` → github.com URL) so a github PR never gets
1027
- // an ADO URL just because the only configured project happens to be ADO.
1028
- // Only use a project's prUrlBase when its host actually matches the PR.
1029
- const _buildPrUrlFromId = (prId, pr) => {
1030
- if (pr?.url) return pr.url;
1031
- const canonical = shared.parseCanonicalPrId(prId);
1032
- if (canonical) {
1033
- const [host, rest] = canonical.scope.split(':');
1034
- if (host === 'github') return `https://github.com/${rest}/pull/${canonical.prNumber}`;
1035
- if (host === 'ado') {
1036
- const [org, adoProject, repo] = rest.split('/');
1037
- if (org && adoProject && repo) {
1038
- return `https://dev.azure.com/${org}/${adoProject}/_git/${repo}/pullrequest/${canonical.prNumber}`;
1039
- }
1040
- }
1041
- }
1042
- // Legacy `PR-N` ID with no host scope: only use project's prUrlBase if a
1043
- // matching project (by name) exists. Never blindly fall back to projects[0].
1044
- const project = pr?._project ? projects.find(p => p.name === pr._project) : null;
1045
- const prNumber = shared.getPrNumber(pr || prId);
1046
- if (project?.prUrlBase && prNumber != null) return project.prUrlBase + prNumber;
1047
- return '';
1048
- };
1049
-
1050
1076
  const prdToPr = {};
1051
1077
  const prLinks = shared.getPrLinks(); // { "PR-xxxx": ["P-xxxx", "P-yyyy"] }
1052
1078
  for (const [prId, itemIds] of Object.entries(prLinks)) {
@@ -1055,10 +1081,10 @@ function getPrdInfo(config) {
1055
1081
  // (or are typed as verify) and would bleed through as duplicate entries on every
1056
1082
  // constituent item. They are surfaced via renderE2eSection instead. (#1220)
1057
1083
  if ((itemIds || []).length > 1 || pr?.itemType === 'verify' || pr?.title?.startsWith('[E2E]')) continue;
1058
- const url = _buildPrUrlFromId(prId, pr);
1084
+ const url = buildPrUrlFromId(prId, pr, projects);
1059
1085
  for (const itemId of (itemIds || [])) {
1060
1086
  if (!prdToPr[itemId]) prdToPr[itemId] = [];
1061
- prdToPr[itemId].push({ id: prId, url, title: pr?.title || '', status: pr?.status || 'active', _project: pr?._project || '' });
1087
+ prdToPr[itemId].push({ id: prId, url, title: pr?.title || '', status: pr?.status || PR_STATUS.ACTIVE, _project: pr?._project || '' });
1062
1088
  }
1063
1089
  }
1064
1090
  // Fallback: work item _pr field for anything still missing
@@ -1069,21 +1095,20 @@ function getPrdInfo(config) {
1069
1095
  const exactPr = prById[canonicalPrId] || null;
1070
1096
  const displayMatches = exactPr ? [] : Object.values(prById).filter(candidate => shared.getPrDisplayId(candidate) === shared.getPrDisplayId(wi._pr));
1071
1097
  const pr = exactPr || (displayMatches.length === 1 ? displayMatches[0] : null);
1072
- const url = _buildPrUrlFromId(canonicalPrId || wi._pr, pr);
1073
- prdToPr[wi.id] = [{ id: pr?.id || canonicalPrId || wi._pr, url, title: pr?.title || '', status: pr?.status || 'active', _project: project?.name || '' }];
1098
+ const url = buildPrUrlFromId(canonicalPrId || wi._pr, pr, projects);
1099
+ prdToPr[wi.id] = [{ id: pr?.id || canonicalPrId || wi._pr, url, title: pr?.title || '', status: pr?.status || PR_STATUS.ACTIVE, _project: project?.name || '' }];
1074
1100
  }
1075
1101
  // Aggregate sub-task PRs to decomposed parent (sub-tasks aren't PRD items but their PRs should show)
1076
1102
  for (const pr of allPrs) {
1077
1103
  for (const itemId of (pr.prdItems || [])) {
1078
- // Find if this is a sub-task with a parent
1079
1104
  const allItems = Object.values(wiById);
1080
1105
  const wi = allItems.find(w => w.id === itemId && w.parent_id);
1081
1106
  if (!wi) continue;
1082
1107
  const parentId = wi.parent_id;
1083
1108
  if (!prdToPr[parentId]) prdToPr[parentId] = [];
1084
1109
  if (!prdToPr[parentId].some(p => p.id === pr.id)) {
1085
- const url = _buildPrUrlFromId(pr.id, pr);
1086
- prdToPr[parentId].push({ id: pr.id, url, title: pr.title || '', status: pr.status || 'active', _project: pr._project || '' });
1110
+ const url = buildPrUrlFromId(pr.id, pr, projects);
1111
+ prdToPr[parentId].push({ id: pr.id, url, title: pr.title || '', status: pr.status || PR_STATUS.ACTIVE, _project: pr._project || '' });
1087
1112
  }
1088
1113
  }
1089
1114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1551",
3
+ "version": "0.1.1553",
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"