@yemi33/minions 0.1.1003 → 0.1.1005
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 +3 -2
- package/dashboard.js +13 -3
- package/engine/ado-status.js +2 -1
- package/engine/ado.js +22 -21
- package/engine/consolidation.js +2 -2
- package/engine/dispatch.js +4 -3
- package/engine/github.js +11 -20
- package/engine/lifecycle.js +102 -55
- package/engine/pipeline.js +1 -1
- package/engine/queries.js +39 -16
- package/engine/shared.js +287 -12
- package/engine/teams.js +1 -1
- package/engine/watches.js +10 -6
- package/engine.js +52 -42
- package/package.json +1 -1
package/engine/lifecycle.js
CHANGED
|
@@ -111,8 +111,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
111
111
|
const prs = safeJson(prPath) || [];
|
|
112
112
|
const prLinks = getPrLinks();
|
|
113
113
|
for (const pr of prs) {
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
114
|
+
const linkedItemIds = prLinks[pr.id] || [];
|
|
115
|
+
if (linkedItemIds.some(itemId => doneItems.find(w => w.id === itemId))) {
|
|
116
116
|
prsCreated.push(pr);
|
|
117
117
|
}
|
|
118
118
|
}
|
|
@@ -199,8 +199,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
199
199
|
const prLinks = getPrLinks();
|
|
200
200
|
const prs = (safeJson(shared.projectPrPath(p)) || [])
|
|
201
201
|
.filter(pr => {
|
|
202
|
-
const
|
|
203
|
-
return pr.status === PR_STATUS.ACTIVE &&
|
|
202
|
+
const linkedIds = prLinks[pr.id] || [];
|
|
203
|
+
return pr.status === PR_STATUS.ACTIVE && linkedIds.some(itemId => doneItems.find(w => w.id === itemId));
|
|
204
204
|
});
|
|
205
205
|
if (prs.length > 0) {
|
|
206
206
|
projectPrs[p.name] = { project: p, prs, mainBranch: shared.resolveMainBranch(p.localPath, p.mainBranch) };
|
|
@@ -413,8 +413,8 @@ function cleanupPlanWorktrees(planFile, plan, projects, config) {
|
|
|
413
413
|
const prs = safeJson(shared.projectPrPath(p)) || [];
|
|
414
414
|
const prLinks = getPrLinks();
|
|
415
415
|
for (const pr of prs) {
|
|
416
|
-
const
|
|
417
|
-
if (
|
|
416
|
+
const linkedIds = prLinks[pr.id] || [];
|
|
417
|
+
if (linkedIds.some(itemId => planItems.find(w => w.id === itemId)) && pr.branch) {
|
|
418
418
|
branchSlugs.add(shared.sanitizeBranch(pr.branch).toLowerCase());
|
|
419
419
|
}
|
|
420
420
|
}
|
|
@@ -781,10 +781,11 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
781
781
|
const newPrsByPath = new Map(); // prPath -> [{ prId, newEntry }]
|
|
782
782
|
|
|
783
783
|
for (const prId of prMatches) {
|
|
784
|
-
const fullId = `PR-${prId}`;
|
|
785
784
|
const targetProject = useCentral ? null : resolveProjectForPr(prId);
|
|
786
785
|
const targetName = targetProject ? targetProject.name : '_central';
|
|
787
786
|
const prPath = targetProject ? shared.projectPrPath(targetProject) : centralPrPath;
|
|
787
|
+
const prUrl = extractPrUrl(prId);
|
|
788
|
+
const fullId = shared.getCanonicalPrId(targetProject, prId, prUrl);
|
|
788
789
|
|
|
789
790
|
let title = meta?.item?.title || '';
|
|
790
791
|
const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
|
|
@@ -793,7 +794,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
793
794
|
title = meta?.item?.title || '';
|
|
794
795
|
}
|
|
795
796
|
|
|
796
|
-
if (!newPrsByPath.has(prPath)) newPrsByPath.set(prPath, { name: targetName, entries: [] });
|
|
797
|
+
if (!newPrsByPath.has(prPath)) newPrsByPath.set(prPath, { name: targetName, project: targetProject, entries: [] });
|
|
797
798
|
newPrsByPath.get(prPath).entries.push({
|
|
798
799
|
prId, fullId,
|
|
799
800
|
entry: {
|
|
@@ -805,7 +806,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
805
806
|
reviewStatus: 'pending',
|
|
806
807
|
status: PR_STATUS.ACTIVE,
|
|
807
808
|
created: ts(),
|
|
808
|
-
url:
|
|
809
|
+
url: prUrl,
|
|
809
810
|
prdItems: meta?.item?.id ? [meta.item.id] : [],
|
|
810
811
|
sourcePlan: meta?.item?.sourcePlan || '',
|
|
811
812
|
itemType: meta?.item?.itemType || ''
|
|
@@ -813,7 +814,8 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
813
814
|
});
|
|
814
815
|
}
|
|
815
816
|
|
|
816
|
-
for (const [prPath, { name, entries }] of newPrsByPath) {
|
|
817
|
+
for (const [prPath, { name, project: targetProject, entries }] of newPrsByPath) {
|
|
818
|
+
const linksToPersist = [];
|
|
817
819
|
mutateJsonFileLocked(prPath, (data) => {
|
|
818
820
|
const prs = Array.isArray(data) ? data : [];
|
|
819
821
|
// Normalize legacy YYYY-MM-DD created dates to ISO
|
|
@@ -821,13 +823,18 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
821
823
|
if (p.created && p.created.length === 10) p.created = p.created + 'T00:00:00.000Z';
|
|
822
824
|
}
|
|
823
825
|
for (const { prId, fullId, entry } of entries) {
|
|
824
|
-
if (prs.some(p => p.id === fullId ||
|
|
826
|
+
if (prs.some(p => p.id === fullId || (p.url && p.url === entry.url))) continue;
|
|
825
827
|
prs.push(entry);
|
|
826
|
-
if (meta?.item?.id)
|
|
828
|
+
if (meta?.item?.id) {
|
|
829
|
+
linksToPersist.push({ prId: fullId, itemId: meta.item.id, project: targetProject, prNumber: entry.prNumber, url: entry.url });
|
|
830
|
+
}
|
|
827
831
|
added++;
|
|
828
832
|
}
|
|
829
833
|
return prs;
|
|
830
834
|
});
|
|
835
|
+
for (const { prId, itemId, project, prNumber, url } of linksToPersist) {
|
|
836
|
+
addPrLink(prId, itemId, { project, prNumber, url });
|
|
837
|
+
}
|
|
831
838
|
log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
|
|
832
839
|
}
|
|
833
840
|
return added;
|
|
@@ -894,7 +901,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary)
|
|
|
894
901
|
let updatedTarget = null;
|
|
895
902
|
shared.mutateJsonFileLocked(prPath, (prs) => {
|
|
896
903
|
if (!Array.isArray(prs)) return prs;
|
|
897
|
-
const target =
|
|
904
|
+
const target = shared.findPrRecord(prs, pr, project);
|
|
898
905
|
if (!target) return prs;
|
|
899
906
|
// Once approved, stays approved — only changes-requested can override
|
|
900
907
|
if (postReviewStatus) {
|
|
@@ -938,7 +945,7 @@ function updatePrAfterFix(pr, project, source) {
|
|
|
938
945
|
const prPath = project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json');
|
|
939
946
|
shared.mutateJsonFileLocked(prPath, (prs) => {
|
|
940
947
|
if (!Array.isArray(prs)) return prs;
|
|
941
|
-
const target =
|
|
948
|
+
const target = shared.findPrRecord(prs, pr, project);
|
|
942
949
|
if (!target) return prs;
|
|
943
950
|
// Never downgrade from approved — fix was dispatched but PR is already approved
|
|
944
951
|
if (target.reviewStatus !== 'approved') target.reviewStatus = 'waiting';
|
|
@@ -1083,7 +1090,7 @@ async function processPendingRebases(config) {
|
|
|
1083
1090
|
|
|
1084
1091
|
async function handlePostMerge(pr, project, config, newStatus) {
|
|
1085
1092
|
|
|
1086
|
-
const prNum = (pr
|
|
1093
|
+
const prNum = shared.getPrNumber(pr);
|
|
1087
1094
|
|
|
1088
1095
|
if (pr.branch && project) {
|
|
1089
1096
|
const root = path.resolve(project.localPath);
|
|
@@ -1109,13 +1116,14 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
1109
1116
|
if (newStatus !== PR_STATUS.MERGED) return;
|
|
1110
1117
|
|
|
1111
1118
|
// Resolve linked work item from pr-links or PR branch name
|
|
1112
|
-
|
|
1113
|
-
if (
|
|
1119
|
+
const mergedItemIds = [...(getPrLinks()[pr.id] || [])];
|
|
1120
|
+
if (mergedItemIds.length === 0 && pr.branch) {
|
|
1114
1121
|
const branchMatch = pr.branch.match(/(P-[a-z0-9]{6,})/i) || pr.branch.match(/(W-[a-z0-9]{6,})/i) || pr.branch.match(/(PL-[a-z0-9]{6,})/i);
|
|
1115
|
-
if (branchMatch)
|
|
1122
|
+
if (branchMatch) mergedItemIds.push(branchMatch[1]);
|
|
1116
1123
|
}
|
|
1117
1124
|
|
|
1118
|
-
if (
|
|
1125
|
+
if (mergedItemIds.length > 0) {
|
|
1126
|
+
const mergedItemSet = new Set(mergedItemIds);
|
|
1119
1127
|
// Mark PRD feature as implemented
|
|
1120
1128
|
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
1121
1129
|
try {
|
|
@@ -1124,49 +1132,62 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
1124
1132
|
for (const pf of planFiles) {
|
|
1125
1133
|
const plan = safeJson(path.join(prdDir, pf));
|
|
1126
1134
|
if (!plan?.missing_features) continue;
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
feature.status
|
|
1135
|
+
let changed = false;
|
|
1136
|
+
for (const feature of plan.missing_features) {
|
|
1137
|
+
if (mergedItemSet.has(feature.id) && feature.status !== WI_STATUS.DONE) {
|
|
1138
|
+
feature.status = WI_STATUS.DONE;
|
|
1139
|
+
changed = true;
|
|
1140
|
+
updated++;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (changed) {
|
|
1130
1144
|
shared.safeWrite(path.join(prdDir, pf), plan);
|
|
1131
|
-
updated++;
|
|
1132
1145
|
}
|
|
1133
1146
|
}
|
|
1134
|
-
if (updated > 0) log('info', `Post-merge: marked ${
|
|
1147
|
+
if (updated > 0) log('info', `Post-merge: marked ${mergedItemIds.join(', ')} as done for ${pr.id}`);
|
|
1135
1148
|
} catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
|
|
1136
1149
|
|
|
1137
1150
|
// Mark work item as done
|
|
1138
1151
|
const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
|
|
1139
1152
|
for (const p of shared.getProjects(config)) wiPaths.push(shared.projectWorkItemsPath(p));
|
|
1153
|
+
const remainingMergedIds = new Set(mergedItemIds);
|
|
1140
1154
|
for (const wiPath of wiPaths) {
|
|
1141
1155
|
try {
|
|
1142
|
-
let found = false;
|
|
1143
1156
|
mutateWorkItems(wiPath, items => {
|
|
1144
|
-
const item
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1157
|
+
for (const item of items) {
|
|
1158
|
+
if (!remainingMergedIds.has(item.id)) continue;
|
|
1159
|
+
if (item.status !== WI_STATUS.DONE) {
|
|
1160
|
+
log('info', `Post-merge: marking work item ${item.id} as done (was ${item.status}) for ${pr.id}`);
|
|
1161
|
+
item.status = WI_STATUS.DONE;
|
|
1162
|
+
item.completedAt = ts();
|
|
1163
|
+
item._mergedVia = pr.id;
|
|
1164
|
+
}
|
|
1165
|
+
remainingMergedIds.delete(item.id);
|
|
1151
1166
|
}
|
|
1152
1167
|
});
|
|
1153
|
-
if (
|
|
1168
|
+
if (remainingMergedIds.size === 0) break;
|
|
1154
1169
|
} catch (err) { log('warn', `Post-merge work item update: ${err.message}`); }
|
|
1155
1170
|
}
|
|
1156
1171
|
|
|
1157
1172
|
// Rebase dependent PRs onto main now that this dependency is merged
|
|
1158
1173
|
try {
|
|
1159
|
-
const
|
|
1160
|
-
for (const
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
continue;
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1174
|
+
const rebasedPrs = new Set();
|
|
1175
|
+
for (const mergedItemId of mergedItemIds) {
|
|
1176
|
+
const dependentPrs = findDependentActivePrs(mergedItemId, config);
|
|
1177
|
+
for (const { pr: depPr, project: depProject } of dependentPrs) {
|
|
1178
|
+
const rebaseKey = `${depProject.name}:${depPr.id}`;
|
|
1179
|
+
if (rebasedPrs.has(rebaseKey)) continue;
|
|
1180
|
+
rebasedPrs.add(rebaseKey);
|
|
1181
|
+
if (isBranchActive(depPr.branch)) {
|
|
1182
|
+
queuePendingRebase(depPr, depProject, mergedItemId);
|
|
1183
|
+
log('info', `Post-merge rebase deferred: ${depPr.branch} locked by active agent`);
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
const result = await rebaseBranchOntoMain(depPr, depProject, config);
|
|
1187
|
+
if (!result.success) {
|
|
1188
|
+
shared.writeToInbox('engine', `rebase-fail-${depPr.id}`,
|
|
1189
|
+
`# Rebase Failed: ${depPr.id}\n\nBranch \`${depPr.branch}\` could not be rebased onto main after dependency ${mergedItemId} merged.\n\nError: ${result.error}\n\nManual rebase may be needed.`);
|
|
1190
|
+
}
|
|
1170
1191
|
}
|
|
1171
1192
|
}
|
|
1172
1193
|
} catch (err) { log('warn', `Post-merge rebase phase error: ${err.message}`); }
|
|
@@ -1315,7 +1336,8 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
|
|
|
1315
1336
|
const reviewFiles = inboxFiles.filter(f => f.includes(reviewerAgentId) && f.includes(today));
|
|
1316
1337
|
if (reviewFiles.length === 0) return;
|
|
1317
1338
|
const reviewContent = reviewFiles.map(f => safeRead(path.join(INBOX_DIR, f))).filter(Boolean).join('\n\n');
|
|
1318
|
-
const
|
|
1339
|
+
const prSlug = shared.safeSlugComponent(pr.id, 60);
|
|
1340
|
+
const feedbackFile = `feedback-${authorAgentId}-from-${reviewerAgentId}-${prSlug}-${today}.md`;
|
|
1319
1341
|
const feedbackPath = shared.uniquePath(path.join(INBOX_DIR, feedbackFile));
|
|
1320
1342
|
const content = `# Review Feedback for ${config.agents[authorAgentId]?.name || authorAgentId}\n\n` +
|
|
1321
1343
|
`**PR:** ${pr.id} — ${pr.title || ''}\n` +
|
|
@@ -1780,7 +1802,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1780
1802
|
// Detect implement tasks that completed without creating a PR
|
|
1781
1803
|
if (effectiveSuccess && (type === WORK_TYPE.IMPLEMENT || type === WORK_TYPE.IMPLEMENT_LARGE || type === WORK_TYPE.FIX) && prsCreatedCount === 0 && meta?.item?.id && !meta?.item?.skipPr && meta?.project?.localPath) {
|
|
1782
1804
|
// Check if a PR already exists linked to this work item (from a previous attempt)
|
|
1783
|
-
let existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
|
|
1805
|
+
let existingPrFound = Object.values(getPrLinks()).some(linkedIds => (linkedIds || []).includes(meta.item.id));
|
|
1784
1806
|
// Also check pull-requests.json for PRs with matching prdItems or branch
|
|
1785
1807
|
if (!existingPrFound) {
|
|
1786
1808
|
const allProjects = shared.getProjects(config);
|
|
@@ -1800,22 +1822,49 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1800
1822
|
if (projectObj) {
|
|
1801
1823
|
try {
|
|
1802
1824
|
let found = null;
|
|
1803
|
-
|
|
1825
|
+
const host = projectObj.repoHost || 'ado';
|
|
1826
|
+
if (host === 'github') {
|
|
1804
1827
|
const ghSlug = projectObj.prUrlBase?.match(/github\.com\/([^/]+\/[^/]+)\/pull/)?.[1];
|
|
1805
1828
|
if (ghSlug) {
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1829
|
+
// Retry up to 3 times — newly created PRs can take a few seconds to appear in the API
|
|
1830
|
+
for (let attempt = 0; attempt < 3 && !found; attempt++) {
|
|
1831
|
+
if (attempt > 0) await new Promise(r => setTimeout(r, 3000));
|
|
1832
|
+
let raw = '';
|
|
1833
|
+
try {
|
|
1834
|
+
raw = await execAsync(`gh pr list --head "${meta.branch}" --repo ${ghSlug} --json number,url,state --limit 1`, { timeout: 15000, windowsHide: true });
|
|
1835
|
+
const parsed = JSON.parse(raw || '[]');
|
|
1836
|
+
const hits = Array.isArray(parsed) ? parsed : [];
|
|
1837
|
+
if (hits.length > 0 && hits[0].state === 'OPEN') {
|
|
1838
|
+
found = { prNumber: hits[0].number, url: hits[0].url };
|
|
1839
|
+
} else if (attempt === 2) {
|
|
1840
|
+
log('warn', `Auto-link fallback: no open PR found on branch ${meta.branch} after 3 attempts (raw: ${(raw || '').slice(0, 200)})`);
|
|
1841
|
+
}
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
if (attempt === 2) {
|
|
1844
|
+
const rawSuffix = raw ? ` (raw: ${raw.slice(0, 200)})` : '';
|
|
1845
|
+
log('warn', `Auto-link fallback: gh pr list lookup failed on branch ${meta.branch} after 3 attempts: ${err.message}${rawSuffix}`);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1809
1849
|
}
|
|
1810
|
-
} else {
|
|
1850
|
+
} else if (host === 'ado') {
|
|
1811
1851
|
found = await require('./ado').findOpenPrOnBranch(projectObj, meta.branch);
|
|
1852
|
+
} else {
|
|
1853
|
+
log('debug', `Skipping branch PR lookup for unsupported repo host "${host}" on ${projectObj.name}`);
|
|
1812
1854
|
}
|
|
1813
1855
|
if (found) {
|
|
1814
|
-
const fullId =
|
|
1856
|
+
const fullId = shared.getCanonicalPrId(projectObj, found.prNumber, found.url);
|
|
1815
1857
|
const prPath = shared.projectPrPath(projectObj);
|
|
1816
1858
|
mutateJsonFileLocked(prPath, prs => {
|
|
1817
1859
|
if (!Array.isArray(prs)) prs = [];
|
|
1818
|
-
|
|
1860
|
+
const existingPr = prs.find(p => p.id === fullId);
|
|
1861
|
+
if (existingPr) {
|
|
1862
|
+
if (meta.item?.id) {
|
|
1863
|
+
if (!Array.isArray(existingPr.prdItems)) existingPr.prdItems = [];
|
|
1864
|
+
if (!existingPr.prdItems.includes(meta.item.id)) existingPr.prdItems.push(meta.item.id);
|
|
1865
|
+
}
|
|
1866
|
+
return prs;
|
|
1867
|
+
}
|
|
1819
1868
|
prs.push({
|
|
1820
1869
|
id: fullId, prNumber: found.prNumber, title: meta.item?.title || '',
|
|
1821
1870
|
agent: agentId, branch: meta.branch, reviewStatus: 'pending',
|
|
@@ -1825,7 +1874,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1825
1874
|
});
|
|
1826
1875
|
return prs;
|
|
1827
1876
|
});
|
|
1828
|
-
if (meta.item?.id) addPrLink(fullId, meta.item.id);
|
|
1829
1877
|
log('info', `Auto-linked existing PR ${fullId} on branch ${meta.branch} for ${meta.item?.id}`);
|
|
1830
1878
|
existingPrFound = true;
|
|
1831
1879
|
}
|
|
@@ -2069,4 +2117,3 @@ module.exports = {
|
|
|
2069
2117
|
processPendingRebases,
|
|
2070
2118
|
findDependentActivePrs,
|
|
2071
2119
|
};
|
|
2072
|
-
|
package/engine/pipeline.js
CHANGED
|
@@ -703,7 +703,7 @@ function isStageComplete(stage, stageState, run, config) {
|
|
|
703
703
|
for (const project of projects) {
|
|
704
704
|
const prs = safeJson(shared.projectPrPath(project)) || [];
|
|
705
705
|
for (const prId of prIds) {
|
|
706
|
-
const pr =
|
|
706
|
+
const pr = shared.findPrRecord(prs, prId, project);
|
|
707
707
|
if (pr && pr.status !== PR_STATUS.MERGED && pr.status !== PR_STATUS.ABANDONED) return false;
|
|
708
708
|
}
|
|
709
709
|
}
|
package/engine/queries.js
CHANGED
|
@@ -419,7 +419,11 @@ function getAgentDetail(id) {
|
|
|
419
419
|
// ── Pull Requests ───────────────────────────────────────────────────────────
|
|
420
420
|
|
|
421
421
|
function getPrs(project) {
|
|
422
|
-
if (project)
|
|
422
|
+
if (project) {
|
|
423
|
+
const prs = safeJson(projectPrPath(project)) || [];
|
|
424
|
+
shared.normalizePrRecords(prs, project);
|
|
425
|
+
return prs;
|
|
426
|
+
}
|
|
423
427
|
const config = getConfig();
|
|
424
428
|
const all = [];
|
|
425
429
|
for (const p of getProjects(config)) all.push(...getPrs(p));
|
|
@@ -433,9 +437,13 @@ function getPullRequests(config) {
|
|
|
433
437
|
for (const project of projects) {
|
|
434
438
|
const prs = safeJson(projectPrPath(project));
|
|
435
439
|
if (!prs) continue;
|
|
440
|
+
shared.normalizePrRecords(prs, project);
|
|
436
441
|
const base = project.prUrlBase || '';
|
|
437
442
|
for (const pr of prs) {
|
|
438
|
-
if (!pr.url && base
|
|
443
|
+
if (!pr.url && base) {
|
|
444
|
+
const prNumber = shared.getPrNumber(pr);
|
|
445
|
+
if (prNumber != null) pr.url = base + prNumber;
|
|
446
|
+
}
|
|
439
447
|
pr._project = project.name || 'Project';
|
|
440
448
|
allPrs.push(pr);
|
|
441
449
|
}
|
|
@@ -444,6 +452,7 @@ function getPullRequests(config) {
|
|
|
444
452
|
const centralPath = path.join(MINIONS_DIR, 'pull-requests.json');
|
|
445
453
|
const centralPrs = safeJson(centralPath);
|
|
446
454
|
if (centralPrs) {
|
|
455
|
+
shared.normalizePrRecords(centralPrs, null);
|
|
447
456
|
for (const pr of centralPrs) {
|
|
448
457
|
if (!allPrs.some(p => p.id === pr.id)) {
|
|
449
458
|
pr._project = 'central';
|
|
@@ -737,9 +746,16 @@ function getWorkItems(config) {
|
|
|
737
746
|
const allPrs = getPullRequests(config);
|
|
738
747
|
for (const item of allItems) {
|
|
739
748
|
if (item._pr && !item._prUrl) {
|
|
740
|
-
const
|
|
741
|
-
const
|
|
742
|
-
|
|
749
|
+
const project = projects.find(p => p.name === item.project || p.name === item._source) || null;
|
|
750
|
+
const canonicalPrId = shared.getCanonicalPrId(project, item._pr);
|
|
751
|
+
const displayPrId = shared.getPrDisplayId(item._pr);
|
|
752
|
+
const exactPr = allPrs.find(p => p.id === canonicalPrId);
|
|
753
|
+
const displayMatches = exactPr ? [] : allPrs.filter(p => shared.getPrDisplayId(p) === displayPrId);
|
|
754
|
+
const pr = exactPr || (displayMatches.length === 1 ? displayMatches[0] : null);
|
|
755
|
+
if (pr) {
|
|
756
|
+
item._pr = pr.id;
|
|
757
|
+
item._prUrl = pr.url;
|
|
758
|
+
}
|
|
743
759
|
}
|
|
744
760
|
if (!item._pr) {
|
|
745
761
|
// Derive from PR.prdItems (single source of truth)
|
|
@@ -949,21 +965,28 @@ function getPrdInfo(config) {
|
|
|
949
965
|
for (const pr of allPrs) prById[pr.id] = pr;
|
|
950
966
|
|
|
951
967
|
const prdToPr = {};
|
|
952
|
-
const prLinks = shared.getPrLinks(); // { "PR-xxxx": "P-xxxx" }
|
|
953
|
-
for (const [prId,
|
|
968
|
+
const prLinks = shared.getPrLinks(); // { "PR-xxxx": ["P-xxxx", "P-yyyy"] }
|
|
969
|
+
for (const [prId, itemIds] of Object.entries(prLinks)) {
|
|
954
970
|
const pr = prById[prId];
|
|
955
971
|
const project = projects.find(p => p.name === pr?._project) || projects[0] || null;
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
972
|
+
const prNumber = shared.getPrNumber(pr || prId);
|
|
973
|
+
const url = pr?.url || (project?.prUrlBase && prNumber != null ? project.prUrlBase + prNumber : '');
|
|
974
|
+
for (const itemId of (itemIds || [])) {
|
|
975
|
+
if (!prdToPr[itemId]) prdToPr[itemId] = [];
|
|
976
|
+
prdToPr[itemId].push({ id: prId, url, title: pr?.title || '', status: pr?.status || 'active', _project: pr?._project || '' });
|
|
977
|
+
}
|
|
959
978
|
}
|
|
960
979
|
// Fallback: work item _pr field for anything still missing
|
|
961
980
|
for (const wi of Object.values(wiById)) {
|
|
962
981
|
if (!wi._pr || prdToPr[wi.id]?.length) continue;
|
|
963
|
-
const
|
|
964
|
-
const
|
|
965
|
-
const
|
|
966
|
-
|
|
982
|
+
const project = projects.find(p => p.name === wi.project || p.name === wi._source) || null;
|
|
983
|
+
const canonicalPrId = shared.getCanonicalPrId(project, wi._pr);
|
|
984
|
+
const exactPr = prById[canonicalPrId] || null;
|
|
985
|
+
const displayMatches = exactPr ? [] : Object.values(prById).filter(candidate => shared.getPrDisplayId(candidate) === shared.getPrDisplayId(wi._pr));
|
|
986
|
+
const pr = exactPr || (displayMatches.length === 1 ? displayMatches[0] : null);
|
|
987
|
+
const prNumber = shared.getPrNumber(pr || wi._pr);
|
|
988
|
+
const url = pr?.url || (project?.prUrlBase && prNumber != null ? project.prUrlBase + prNumber : '');
|
|
989
|
+
prdToPr[wi.id] = [{ id: pr?.id || canonicalPrId || wi._pr, url, title: pr?.title || '', status: pr?.status || 'active', _project: project?.name || '' }];
|
|
967
990
|
}
|
|
968
991
|
// Aggregate sub-task PRs to decomposed parent (sub-tasks aren't PRD items but their PRs should show)
|
|
969
992
|
for (const pr of allPrs) {
|
|
@@ -976,7 +999,8 @@ function getPrdInfo(config) {
|
|
|
976
999
|
if (!prdToPr[parentId]) prdToPr[parentId] = [];
|
|
977
1000
|
if (!prdToPr[parentId].some(p => p.id === pr.id)) {
|
|
978
1001
|
const project = projects.find(p => p.name === pr._project) || projects[0] || null;
|
|
979
|
-
const
|
|
1002
|
+
const prNumber = shared.getPrNumber(pr);
|
|
1003
|
+
const url = pr.url || (project?.prUrlBase && prNumber != null ? project.prUrlBase + prNumber : '');
|
|
980
1004
|
prdToPr[parentId].push({ id: pr.id, url, title: pr.title || '', status: pr.status || 'active', _project: pr._project || '' });
|
|
981
1005
|
}
|
|
982
1006
|
}
|
|
@@ -1091,4 +1115,3 @@ module.exports = {
|
|
|
1091
1115
|
// Work items & PRD
|
|
1092
1116
|
getWorkItems, getPrdInfo,
|
|
1093
1117
|
};
|
|
1094
|
-
|