@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 +10 -0
- package/engine/pipeline.js +3 -2
- package/engine/queries.js +72 -47
- package/package.json +1 -1
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
|
package/engine/pipeline.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
504
|
+
const base = project?.prUrlBase || '';
|
|
484
505
|
for (const pr of prs) {
|
|
485
|
-
if (!pr
|
|
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
|
-
//
|
|
494
|
-
const
|
|
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 (!
|
|
500
|
-
|
|
501
|
-
|
|
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 =
|
|
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 ||
|
|
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 =
|
|
1073
|
-
prdToPr[wi.id] = [{ id: pr?.id || canonicalPrId || wi._pr, url, title: pr?.title || '', status: pr?.status ||
|
|
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 =
|
|
1086
|
-
prdToPr[parentId].push({ id: pr.id, url, title: pr.title || '', status: pr.status ||
|
|
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.
|
|
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"
|