@yemi33/minions 0.1.1552 → 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 +5 -0
- package/engine/pipeline.js +3 -2
- package/engine/queries.js +72 -68
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
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,56 +1066,12 @@ 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
|
-
// Also scan project subdirectories that aren't in config (e.g. removed projects
|
|
1025
|
-
// whose pull-requests.json still holds last-known status). getPrLinks already
|
|
1026
|
-
// reads all subdirs, so without this PRs from those dirs would have status/title
|
|
1027
|
-
// missing in the PRD view and fall back to literal 'active'/'' defaults.
|
|
1028
|
-
try {
|
|
1029
|
-
const projectsDir = path.join(MINIONS_DIR, 'projects');
|
|
1030
|
-
const projectsByName = new Map(projects.map(p => [p.name, p]));
|
|
1031
|
-
for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
|
|
1032
|
-
if (!d.isDirectory() || projectsByName.has(d.name)) continue;
|
|
1033
|
-
const ghostPath = path.join(projectsDir, d.name, 'pull-requests.json');
|
|
1034
|
-
const ghostPrs = safeJson(ghostPath);
|
|
1035
|
-
if (!Array.isArray(ghostPrs)) continue;
|
|
1036
|
-
shared.normalizePrRecords(ghostPrs, null);
|
|
1037
|
-
for (const pr of ghostPrs) {
|
|
1038
|
-
if (pr?.id && !prById[pr.id]) {
|
|
1039
|
-
pr._project = d.name;
|
|
1040
|
-
prById[pr.id] = pr;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
} catch { /* projects dir missing */ }
|
|
1045
|
-
|
|
1046
|
-
// Build URL for a PR when pr.url isn't set. Prefer the PR ID's own scope
|
|
1047
|
-
// (e.g. `github:owner/repo#123` → github.com URL) so a github PR never gets
|
|
1048
|
-
// an ADO URL just because the only configured project happens to be ADO.
|
|
1049
|
-
// Only use a project's prUrlBase when its host actually matches the PR.
|
|
1050
|
-
const _buildPrUrlFromId = (prId, pr) => {
|
|
1051
|
-
if (pr?.url) return pr.url;
|
|
1052
|
-
const canonical = shared.parseCanonicalPrId(prId);
|
|
1053
|
-
if (canonical) {
|
|
1054
|
-
const [host, rest] = canonical.scope.split(':');
|
|
1055
|
-
if (host === 'github') return `https://github.com/${rest}/pull/${canonical.prNumber}`;
|
|
1056
|
-
if (host === 'ado') {
|
|
1057
|
-
const [org, adoProject, repo] = rest.split('/');
|
|
1058
|
-
if (org && adoProject && repo) {
|
|
1059
|
-
return `https://dev.azure.com/${org}/${adoProject}/_git/${repo}/pullrequest/${canonical.prNumber}`;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
// Legacy `PR-N` ID with no host scope: only use project's prUrlBase if a
|
|
1064
|
-
// matching project (by name) exists. Never blindly fall back to projects[0].
|
|
1065
|
-
const project = pr?._project ? projects.find(p => p.name === pr._project) : null;
|
|
1066
|
-
const prNumber = shared.getPrNumber(pr || prId);
|
|
1067
|
-
if (project?.prUrlBase && prNumber != null) return project.prUrlBase + prNumber;
|
|
1068
|
-
return '';
|
|
1069
|
-
};
|
|
1070
1075
|
|
|
1071
1076
|
const prdToPr = {};
|
|
1072
1077
|
const prLinks = shared.getPrLinks(); // { "PR-xxxx": ["P-xxxx", "P-yyyy"] }
|
|
@@ -1076,10 +1081,10 @@ function getPrdInfo(config) {
|
|
|
1076
1081
|
// (or are typed as verify) and would bleed through as duplicate entries on every
|
|
1077
1082
|
// constituent item. They are surfaced via renderE2eSection instead. (#1220)
|
|
1078
1083
|
if ((itemIds || []).length > 1 || pr?.itemType === 'verify' || pr?.title?.startsWith('[E2E]')) continue;
|
|
1079
|
-
const url =
|
|
1084
|
+
const url = buildPrUrlFromId(prId, pr, projects);
|
|
1080
1085
|
for (const itemId of (itemIds || [])) {
|
|
1081
1086
|
if (!prdToPr[itemId]) prdToPr[itemId] = [];
|
|
1082
|
-
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 || '' });
|
|
1083
1088
|
}
|
|
1084
1089
|
}
|
|
1085
1090
|
// Fallback: work item _pr field for anything still missing
|
|
@@ -1090,21 +1095,20 @@ function getPrdInfo(config) {
|
|
|
1090
1095
|
const exactPr = prById[canonicalPrId] || null;
|
|
1091
1096
|
const displayMatches = exactPr ? [] : Object.values(prById).filter(candidate => shared.getPrDisplayId(candidate) === shared.getPrDisplayId(wi._pr));
|
|
1092
1097
|
const pr = exactPr || (displayMatches.length === 1 ? displayMatches[0] : null);
|
|
1093
|
-
const url =
|
|
1094
|
-
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 || '' }];
|
|
1095
1100
|
}
|
|
1096
1101
|
// Aggregate sub-task PRs to decomposed parent (sub-tasks aren't PRD items but their PRs should show)
|
|
1097
1102
|
for (const pr of allPrs) {
|
|
1098
1103
|
for (const itemId of (pr.prdItems || [])) {
|
|
1099
|
-
// Find if this is a sub-task with a parent
|
|
1100
1104
|
const allItems = Object.values(wiById);
|
|
1101
1105
|
const wi = allItems.find(w => w.id === itemId && w.parent_id);
|
|
1102
1106
|
if (!wi) continue;
|
|
1103
1107
|
const parentId = wi.parent_id;
|
|
1104
1108
|
if (!prdToPr[parentId]) prdToPr[parentId] = [];
|
|
1105
1109
|
if (!prdToPr[parentId].some(p => p.id === pr.id)) {
|
|
1106
|
-
const url =
|
|
1107
|
-
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 || '' });
|
|
1108
1112
|
}
|
|
1109
1113
|
}
|
|
1110
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"
|