@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/shared.js
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const crypto = require('crypto');
|
|
8
9
|
|
|
9
10
|
const MINIONS_DIR = process.env.MINIONS_TEST_DIR || path.resolve(__dirname, '..');
|
|
10
11
|
const PR_LINKS_PATH = path.join(MINIONS_DIR, 'engine', 'pr-links.json');
|
|
12
|
+
const PINNED_ITEMS_PATH = path.join(MINIONS_DIR, 'engine', 'kb-pins.json');
|
|
11
13
|
const LOG_PATH = path.join(MINIONS_DIR, 'engine', 'log.json');
|
|
12
14
|
|
|
13
15
|
// ── Timestamps & Logging ────────────────────────────────────────────────────
|
|
@@ -244,6 +246,9 @@ function mutateJsonFileLocked(filePath, mutateFn, {
|
|
|
244
246
|
// Back up last-known-good state before mutation (best-effort)
|
|
245
247
|
const backupPath = filePath + '.backup';
|
|
246
248
|
try { if (fs.existsSync(filePath)) fs.copyFileSync(filePath, backupPath); } catch { /* backup is best-effort */ }
|
|
249
|
+
if (path.basename(filePath) === 'pull-requests.json' && Array.isArray(data)) {
|
|
250
|
+
normalizePrRecords(data, resolveProjectForPrPath(filePath));
|
|
251
|
+
}
|
|
247
252
|
const next = mutateFn(data);
|
|
248
253
|
const finalData = next === undefined ? data : next;
|
|
249
254
|
safeWrite(filePath, finalData);
|
|
@@ -298,7 +303,8 @@ function parseNoteId(content) {
|
|
|
298
303
|
function writeToInbox(agentId, slug, content, _inboxDir, metadata) {
|
|
299
304
|
try {
|
|
300
305
|
const inboxDir = _inboxDir || path.join(MINIONS_DIR, 'notes', 'inbox');
|
|
301
|
-
const
|
|
306
|
+
const safeSlug = safeSlugComponent(slug, 80);
|
|
307
|
+
const prefix = `${agentId}-${safeSlug}-${dateStamp()}`;
|
|
302
308
|
const existing = safeReadDir(inboxDir).find(f => f.startsWith(prefix));
|
|
303
309
|
if (existing) return false;
|
|
304
310
|
const noteId = `NOTE-${uid()}`;
|
|
@@ -738,6 +744,7 @@ const DEFAULT_CLAUDE = {
|
|
|
738
744
|
// ── Project Helpers ──────────────────────────────────────────────────────────
|
|
739
745
|
|
|
740
746
|
function getProjects(config) {
|
|
747
|
+
if (!config) config = safeJson(path.join(MINIONS_DIR, 'config.json')) || {};
|
|
741
748
|
if (config && config.projects && Array.isArray(config.projects)) {
|
|
742
749
|
return config.projects.filter(p => {
|
|
743
750
|
if (!p || typeof p !== 'object') return false;
|
|
@@ -771,6 +778,14 @@ function projectPrPath(project) {
|
|
|
771
778
|
return path.join(projectStateDir(project), 'pull-requests.json');
|
|
772
779
|
}
|
|
773
780
|
|
|
781
|
+
function resolveProjectForPrPath(filePath, config = null) {
|
|
782
|
+
const resolvedPath = path.resolve(filePath);
|
|
783
|
+
for (const project of getProjects(config)) {
|
|
784
|
+
if (path.resolve(projectPrPath(project)) === resolvedPath) return project;
|
|
785
|
+
}
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
|
|
774
789
|
// ── ID Generation ────────────────────────────────────────────────────────────
|
|
775
790
|
|
|
776
791
|
function nextWorkItemId(items, prefix) {
|
|
@@ -859,40 +874,275 @@ function parseSkillFrontmatter(content, filename) {
|
|
|
859
874
|
// Stable single-writer file: maps PR IDs → PRD item IDs.
|
|
860
875
|
// Never touched by polling loops — only written when a PR is first linked to a PRD item.
|
|
861
876
|
|
|
877
|
+
function normalizePrScopeSegment(value) {
|
|
878
|
+
return String(value || '').trim().replace(/^\/+|\/+$/g, '').toLowerCase();
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function parseCanonicalPrId(value) {
|
|
882
|
+
const match = String(value || '').trim().match(/^(github|ado):(.+?)#(\d+)$/i);
|
|
883
|
+
if (!match) return null;
|
|
884
|
+
const scope = `${match[1].toLowerCase()}:${match[2].split('/').map(normalizePrScopeSegment).join('/')}`;
|
|
885
|
+
return { scope, prNumber: parseInt(match[3], 10) };
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function parseGitHubPrUrl(url) {
|
|
889
|
+
const match = String(url || '').match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/i);
|
|
890
|
+
if (!match) return null;
|
|
891
|
+
return {
|
|
892
|
+
scope: `github:${normalizePrScopeSegment(match[1])}/${normalizePrScopeSegment(match[2])}`,
|
|
893
|
+
prNumber: parseInt(match[3], 10),
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function parseAdoPrUrl(url) {
|
|
898
|
+
const devAzure = String(url || '').match(/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+)\/pullrequest\/(\d+)/i);
|
|
899
|
+
if (devAzure) {
|
|
900
|
+
return {
|
|
901
|
+
scope: `ado:${normalizePrScopeSegment(devAzure[1])}/${normalizePrScopeSegment(devAzure[2])}/${normalizePrScopeSegment(decodeURIComponent(devAzure[3]))}`,
|
|
902
|
+
prNumber: parseInt(devAzure[4], 10),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
const visualStudio = String(url || '').match(/https?:\/\/([^/.]+)\.visualstudio\.com\/(?:DefaultCollection\/)?([^/]+)\/_git\/([^/]+)\/pullrequest\/(\d+)/i);
|
|
906
|
+
if (!visualStudio) return null;
|
|
907
|
+
return {
|
|
908
|
+
scope: `ado:${normalizePrScopeSegment(visualStudio[1])}/${normalizePrScopeSegment(visualStudio[2])}/${normalizePrScopeSegment(decodeURIComponent(visualStudio[3]))}`,
|
|
909
|
+
prNumber: parseInt(visualStudio[4], 10),
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function getProjectPrScope(project) {
|
|
914
|
+
if (!project) return '';
|
|
915
|
+
const host = String(project.repoHost || '').toLowerCase();
|
|
916
|
+
if (host === 'github') {
|
|
917
|
+
const parsed = parseGitHubPrUrl(project.prUrlBase || '');
|
|
918
|
+
if (parsed?.scope) return parsed.scope;
|
|
919
|
+
const owner = normalizePrScopeSegment(project.adoOrg);
|
|
920
|
+
const repo = normalizePrScopeSegment(project.repoName);
|
|
921
|
+
return owner && repo ? `github:${owner}/${repo}` : '';
|
|
922
|
+
}
|
|
923
|
+
if (host === 'ado' || !host) {
|
|
924
|
+
const parsed = parseAdoPrUrl(project.prUrlBase || '');
|
|
925
|
+
if (parsed?.scope) return parsed.scope;
|
|
926
|
+
const org = normalizePrScopeSegment(project.adoOrg);
|
|
927
|
+
const adoProject = normalizePrScopeSegment(project.adoProject);
|
|
928
|
+
const repo = normalizePrScopeSegment(project.repoName || project.repositoryId);
|
|
929
|
+
return org && adoProject && repo ? `ado:${org}/${adoProject}/${repo}` : '';
|
|
930
|
+
}
|
|
931
|
+
return '';
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function getPrNumber(value) {
|
|
935
|
+
if (value && typeof value === 'object') {
|
|
936
|
+
if (value.prNumber != null && /^\d+$/.test(String(value.prNumber))) {
|
|
937
|
+
return parseInt(String(value.prNumber), 10);
|
|
938
|
+
}
|
|
939
|
+
return getPrNumber(value.id || value.url || '');
|
|
940
|
+
}
|
|
941
|
+
const raw = String(value || '').trim();
|
|
942
|
+
if (!raw) return null;
|
|
943
|
+
const canonical = parseCanonicalPrId(raw);
|
|
944
|
+
if (canonical) return canonical.prNumber;
|
|
945
|
+
const parsedUrl = parseGitHubPrUrl(raw) || parseAdoPrUrl(raw);
|
|
946
|
+
if (parsedUrl) return parsedUrl.prNumber;
|
|
947
|
+
const legacy = raw.match(/^PR-(\d+)$/i) || raw.match(/^(\d+)$/);
|
|
948
|
+
return legacy ? parseInt(legacy[1], 10) : null;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function getPrDisplayId(value, fallbackPrNumber = null) {
|
|
952
|
+
const prNumber = getPrNumber(value) ?? getPrNumber(fallbackPrNumber);
|
|
953
|
+
if (prNumber != null) return `PR-${prNumber}`;
|
|
954
|
+
return typeof value === 'object' ? String(value?.id || '') : String(value || '');
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function getCanonicalPrId(project, prRef, url = '') {
|
|
958
|
+
const isObjectRef = !!prRef && typeof prRef === 'object';
|
|
959
|
+
const rawId = isObjectRef ? (prRef.id || '') : String(prRef || '');
|
|
960
|
+
const canonical = parseCanonicalPrId(rawId);
|
|
961
|
+
if (canonical) return `${canonical.scope}#${canonical.prNumber}`;
|
|
962
|
+
const parsedUrl = parseGitHubPrUrl(url || (isObjectRef ? prRef.url || '' : ''))
|
|
963
|
+
|| parseAdoPrUrl(url || (isObjectRef ? prRef.url || '' : ''));
|
|
964
|
+
const prNumber = getPrNumber(isObjectRef ? (prRef.prNumber ?? prRef.id ?? prRef.url) : prRef);
|
|
965
|
+
if (prNumber == null) return rawId;
|
|
966
|
+
const scope = getProjectPrScope(project) || parsedUrl?.scope || '';
|
|
967
|
+
return scope ? `${scope}#${prNumber}` : `PR-${prNumber}`;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function findPrRecord(prs, prRef, project = null) {
|
|
971
|
+
if (!Array.isArray(prs) || !prRef) return null;
|
|
972
|
+
const isObjectRef = typeof prRef === 'object';
|
|
973
|
+
const rawId = isObjectRef ? String(prRef.id || '') : String(prRef || '');
|
|
974
|
+
const refUrl = isObjectRef ? String(prRef.url || '') : '';
|
|
975
|
+
const canonicalId = getCanonicalPrId(project, prRef, refUrl);
|
|
976
|
+
if (canonicalId) {
|
|
977
|
+
const canonicalMatch = prs.find(pr => pr?.id === canonicalId);
|
|
978
|
+
if (canonicalMatch) return canonicalMatch;
|
|
979
|
+
}
|
|
980
|
+
if (rawId) {
|
|
981
|
+
const rawMatch = prs.find(pr => pr?.id === rawId);
|
|
982
|
+
if (rawMatch) return rawMatch;
|
|
983
|
+
}
|
|
984
|
+
if (refUrl) {
|
|
985
|
+
const urlMatch = prs.find(pr => pr?.url === refUrl);
|
|
986
|
+
if (urlMatch) return urlMatch;
|
|
987
|
+
}
|
|
988
|
+
const refNumber = getPrNumber(isObjectRef ? (prRef.prNumber ?? prRef.id ?? prRef.url) : prRef);
|
|
989
|
+
if (refNumber == null) return null;
|
|
990
|
+
const numberMatches = prs.filter(pr => getPrNumber(pr) === refNumber);
|
|
991
|
+
return numberMatches.length === 1 ? numberMatches[0] : null;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function normalizePrRecord(pr, project = null) {
|
|
995
|
+
if (!pr || typeof pr !== 'object') return false;
|
|
996
|
+
let changed = false;
|
|
997
|
+
const prNumber = getPrNumber(pr.prNumber ?? pr.id ?? pr.url);
|
|
998
|
+
if (prNumber != null && pr.prNumber !== prNumber) {
|
|
999
|
+
pr.prNumber = prNumber;
|
|
1000
|
+
changed = true;
|
|
1001
|
+
}
|
|
1002
|
+
const canonicalId = getCanonicalPrId(project, pr, pr.url || '');
|
|
1003
|
+
if (canonicalId && pr.id !== canonicalId) {
|
|
1004
|
+
pr.id = canonicalId;
|
|
1005
|
+
changed = true;
|
|
1006
|
+
}
|
|
1007
|
+
return changed;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function normalizePrRecords(prs, project = null) {
|
|
1011
|
+
if (!Array.isArray(prs)) return 0;
|
|
1012
|
+
let changed = 0;
|
|
1013
|
+
for (const pr of prs) {
|
|
1014
|
+
if (normalizePrRecord(pr, project)) changed++;
|
|
1015
|
+
}
|
|
1016
|
+
return changed;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function normalizePrLinkItems(value) {
|
|
1020
|
+
const items = Array.isArray(value) ? value : [value];
|
|
1021
|
+
return [...new Set(items.filter(item => typeof item === 'string' && item))];
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function mergePrLinkItems(links, prId, itemIds) {
|
|
1025
|
+
if (!prId) return;
|
|
1026
|
+
const merged = new Set([...(links[prId] || []), ...normalizePrLinkItems(itemIds)]);
|
|
1027
|
+
if (merged.size > 0) links[prId] = [...merged];
|
|
1028
|
+
}
|
|
1029
|
+
|
|
862
1030
|
function getPrLinks() {
|
|
863
1031
|
const links = {};
|
|
1032
|
+
const knownPrIdsByDisplay = new Map();
|
|
1033
|
+
const registerPrId = (pr) => {
|
|
1034
|
+
if (!pr?.id) return;
|
|
1035
|
+
const displayId = getPrDisplayId(pr);
|
|
1036
|
+
if (!displayId) return;
|
|
1037
|
+
if (!knownPrIdsByDisplay.has(displayId)) knownPrIdsByDisplay.set(displayId, new Set());
|
|
1038
|
+
knownPrIdsByDisplay.get(displayId).add(pr.id);
|
|
1039
|
+
};
|
|
864
1040
|
// Primary source: derive from all projects/*/pull-requests.json prdItems
|
|
865
1041
|
const projectsDir = path.join(MINIONS_DIR, 'projects');
|
|
1042
|
+
const projectsByName = new Map(getProjects().map(project => [project.name || path.basename(project.localPath || ''), project]));
|
|
866
1043
|
try {
|
|
867
1044
|
for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
|
|
868
1045
|
if (!d.isDirectory()) continue;
|
|
869
1046
|
try {
|
|
870
1047
|
const prs = JSON.parse(fs.readFileSync(path.join(projectsDir, d.name, 'pull-requests.json'), 'utf8'));
|
|
1048
|
+
const project = projectsByName.get(d.name) || null;
|
|
1049
|
+
normalizePrRecords(prs, project);
|
|
871
1050
|
for (const pr of prs) {
|
|
872
1051
|
if (!pr.id) continue;
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}
|
|
1052
|
+
registerPrId(pr);
|
|
1053
|
+
mergePrLinkItems(links, pr.id, pr.prdItems || []);
|
|
876
1054
|
}
|
|
877
1055
|
} catch { /* missing or invalid */ }
|
|
878
1056
|
}
|
|
879
1057
|
} catch { /* projects dir missing */ }
|
|
1058
|
+
try {
|
|
1059
|
+
const centralPrs = JSON.parse(fs.readFileSync(path.join(MINIONS_DIR, 'pull-requests.json'), 'utf8'));
|
|
1060
|
+
normalizePrRecords(centralPrs, null);
|
|
1061
|
+
for (const pr of centralPrs) {
|
|
1062
|
+
if (!pr.id) continue;
|
|
1063
|
+
registerPrId(pr);
|
|
1064
|
+
mergePrLinkItems(links, pr.id, pr.prdItems || []);
|
|
1065
|
+
}
|
|
1066
|
+
} catch { /* central file optional */ }
|
|
880
1067
|
// Fallback: static pr-links.json for entries not covered above
|
|
881
1068
|
try {
|
|
882
1069
|
const static_ = JSON.parse(fs.readFileSync(PR_LINKS_PATH, 'utf8'));
|
|
883
1070
|
for (const [k, v] of Object.entries(static_)) {
|
|
884
|
-
|
|
1071
|
+
const canonical = parseCanonicalPrId(k);
|
|
1072
|
+
let normalizedKey = canonical ? `${canonical.scope}#${canonical.prNumber}` : k;
|
|
1073
|
+
if (!canonical) {
|
|
1074
|
+
const candidates = knownPrIdsByDisplay.get(getPrDisplayId(k));
|
|
1075
|
+
if (candidates?.size === 1) normalizedKey = [...candidates][0];
|
|
1076
|
+
else if (candidates?.size > 1) {
|
|
1077
|
+
log('warn', `Skipping ambiguous legacy PR link "${k}" — multiple canonical PR IDs share display ID ${getPrDisplayId(k)}`);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (!links[normalizedKey]) mergePrLinkItems(links, normalizedKey, v);
|
|
885
1082
|
}
|
|
886
1083
|
} catch { /* missing */ }
|
|
887
1084
|
return links;
|
|
888
1085
|
}
|
|
889
1086
|
|
|
890
|
-
function
|
|
891
|
-
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1087
|
+
function backfillPrPrdItems(prs, prLinks) {
|
|
1088
|
+
let backfilled = 0;
|
|
1089
|
+
for (const pr of prs) {
|
|
1090
|
+
const linkedItems = prLinks[pr.id] || [];
|
|
1091
|
+
if (linkedItems.length > 0) {
|
|
1092
|
+
pr.prdItems = Array.isArray(pr.prdItems) ? pr.prdItems : [];
|
|
1093
|
+
for (const linked of linkedItems) {
|
|
1094
|
+
if (!pr.prdItems.includes(linked)) {
|
|
1095
|
+
pr.prdItems.push(linked);
|
|
1096
|
+
backfilled++;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return backfilled;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } = {}) {
|
|
1105
|
+
const canonicalPrId = getCanonicalPrId(project, prNumber ?? prId, url);
|
|
1106
|
+
const effectivePrId = canonicalPrId || prId;
|
|
1107
|
+
if (!effectivePrId || !itemId) return;
|
|
1108
|
+
const legacyPrId = String(prId || '');
|
|
1109
|
+
mutateJsonFileLocked(PR_LINKS_PATH, (links) => {
|
|
1110
|
+
if (!links || Array.isArray(links) || typeof links !== 'object') links = {};
|
|
1111
|
+
const mergedCurrent = new Set(normalizePrLinkItems(links[effectivePrId]));
|
|
1112
|
+
if (legacyPrId && legacyPrId !== effectivePrId && links[legacyPrId]) {
|
|
1113
|
+
for (const linkedItem of normalizePrLinkItems(links[legacyPrId])) mergedCurrent.add(linkedItem);
|
|
1114
|
+
delete links[legacyPrId];
|
|
1115
|
+
}
|
|
1116
|
+
if (!mergedCurrent.has(itemId)) mergedCurrent.add(itemId);
|
|
1117
|
+
links[effectivePrId] = [...mergedCurrent];
|
|
1118
|
+
return links;
|
|
1119
|
+
}, { defaultValue: {} });
|
|
1120
|
+
|
|
1121
|
+
if (!project) return;
|
|
1122
|
+
const prPath = projectPrPath(project);
|
|
1123
|
+
const effectivePrNumber = getPrNumber(prNumber ?? effectivePrId);
|
|
1124
|
+
const prLockPath = `${prPath}.lock`;
|
|
1125
|
+
withFileLock(prLockPath, () => {
|
|
1126
|
+
if (!fs.existsSync(prPath)) return;
|
|
1127
|
+
let prs = safeJson(prPath);
|
|
1128
|
+
if (!Array.isArray(prs)) prs = [];
|
|
1129
|
+
normalizePrRecords(prs, project);
|
|
1130
|
+
const existingPr = prs.find(pr =>
|
|
1131
|
+
pr?.id === effectivePrId
|
|
1132
|
+
|| (url && pr?.url === url)
|
|
1133
|
+
|| (effectivePrNumber != null && getPrNumber(pr) === effectivePrNumber)
|
|
1134
|
+
);
|
|
1135
|
+
if (!existingPr) return;
|
|
1136
|
+
const backupPath = prPath + '.backup';
|
|
1137
|
+
try { if (fs.existsSync(prPath)) fs.copyFileSync(prPath, backupPath); } catch { /* backup is best-effort */ }
|
|
1138
|
+
existingPr.prdItems = Array.isArray(existingPr.prdItems) ? existingPr.prdItems : [];
|
|
1139
|
+
if (existingPr.prdItems.includes(itemId)) return;
|
|
1140
|
+
existingPr.prdItems.push(itemId);
|
|
1141
|
+
safeWrite(prPath, prs);
|
|
1142
|
+
}, {
|
|
1143
|
+
retries: ENGINE_DEFAULTS.lockRetries,
|
|
1144
|
+
retryBackoffMs: ENGINE_DEFAULTS.lockRetryBackoffMs
|
|
1145
|
+
});
|
|
896
1146
|
}
|
|
897
1147
|
|
|
898
1148
|
// ─── Cross-Platform Process Kill Helpers ─────────────────────────────────────
|
|
@@ -1062,10 +1312,24 @@ function slugify(text, maxLen = 50) {
|
|
|
1062
1312
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, maxLen);
|
|
1063
1313
|
}
|
|
1064
1314
|
|
|
1315
|
+
function safeSlugComponent(text, maxLen = 80) {
|
|
1316
|
+
const raw = String(text || '').trim();
|
|
1317
|
+
if (!raw) return 'item';
|
|
1318
|
+
if (/^[A-Za-z0-9._-]+$/.test(raw) && raw.length <= maxLen) return raw;
|
|
1319
|
+
const hash = crypto.createHash('md5').update(raw).digest('hex').slice(0, 8);
|
|
1320
|
+
const base = slugify(raw, Math.max(8, maxLen - 9)) || 'item';
|
|
1321
|
+
return `${base}-${hash}`.slice(0, maxLen);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1065
1324
|
function formatTranscriptEntry(t) {
|
|
1066
1325
|
return '### ' + (t.agent || 'agent') + ' (' + (t.type || '') + ', Round ' + (t.round || '?') + ')\n\n' + (t.content || '');
|
|
1067
1326
|
}
|
|
1068
1327
|
|
|
1328
|
+
function getPinnedItems() {
|
|
1329
|
+
const pins = safeJson(PINNED_ITEMS_PATH);
|
|
1330
|
+
return Array.isArray(pins) ? pins.filter(item => typeof item === 'string' && item) : [];
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1069
1333
|
// ── Throttle Tracker Factory ────────────────────────────────────────────────
|
|
1070
1334
|
// Generic rate-limit tracker reusable by both ADO and GitHub integrations.
|
|
1071
1335
|
// Returns an object with recordThrottle, recordSuccess, isThrottled, getState.
|
|
@@ -1122,6 +1386,7 @@ function createThrottleTracker({ label, baseBackoffMs = 60000, maxBackoffMs = 32
|
|
|
1122
1386
|
module.exports = {
|
|
1123
1387
|
MINIONS_DIR,
|
|
1124
1388
|
PR_LINKS_PATH,
|
|
1389
|
+
PINNED_ITEMS_PATH,
|
|
1125
1390
|
LOG_PATH,
|
|
1126
1391
|
ts,
|
|
1127
1392
|
logTs,
|
|
@@ -1166,6 +1431,14 @@ module.exports = {
|
|
|
1166
1431
|
projectPrPath,
|
|
1167
1432
|
getPrLinks,
|
|
1168
1433
|
addPrLink,
|
|
1434
|
+
parseCanonicalPrId,
|
|
1435
|
+
getProjectPrScope,
|
|
1436
|
+
getPrNumber,
|
|
1437
|
+
getPrDisplayId,
|
|
1438
|
+
getCanonicalPrId,
|
|
1439
|
+
findPrRecord,
|
|
1440
|
+
normalizePrRecord,
|
|
1441
|
+
normalizePrRecords,
|
|
1169
1442
|
nextWorkItemId,
|
|
1170
1443
|
getAdoOrgBase,
|
|
1171
1444
|
sanitizePath,
|
|
@@ -1181,8 +1454,10 @@ module.exports = {
|
|
|
1181
1454
|
LOCK_STALE_MS,
|
|
1182
1455
|
flushLogs,
|
|
1183
1456
|
slugify,
|
|
1457
|
+
safeSlugComponent,
|
|
1184
1458
|
formatTranscriptEntry,
|
|
1459
|
+
getPinnedItems,
|
|
1185
1460
|
_logBuffer, // exported for testing
|
|
1186
1461
|
createThrottleTracker,
|
|
1462
|
+
backfillPrPrdItems,
|
|
1187
1463
|
};
|
|
1188
|
-
|
package/engine/teams.js
CHANGED
|
@@ -543,7 +543,7 @@ async function teamsNotifyPrEvent(pr, event, project, prFilePath) {
|
|
|
543
543
|
if (prFilePath) {
|
|
544
544
|
mutateJsonFileLocked(prFilePath, (prs) => {
|
|
545
545
|
if (!Array.isArray(prs)) return prs;
|
|
546
|
-
const target =
|
|
546
|
+
const target = shared.findPrRecord(prs, pr);
|
|
547
547
|
if (target) {
|
|
548
548
|
if (!target._teamsNotifiedEvents) target._teamsNotifiedEvents = [];
|
|
549
549
|
if (!target._teamsNotifiedEvents.includes(event)) {
|
package/engine/watches.js
CHANGED
|
@@ -20,6 +20,14 @@ function _watchesPath() { return path.join(shared.MINIONS_DIR, 'engine', 'watche
|
|
|
20
20
|
// so watches are checked every N ticks where N = ceil(interval / tickInterval).
|
|
21
21
|
const DEFAULT_WATCH_INTERVAL = 300000;
|
|
22
22
|
|
|
23
|
+
/** Find a PR by target (prNumber, canonical ID, or display ID). */
|
|
24
|
+
function findPrByTarget(pullRequests, target) {
|
|
25
|
+
const t = String(target);
|
|
26
|
+
return (pullRequests || []).find(p =>
|
|
27
|
+
String(p.prNumber) === t || p.id === t || shared.getPrDisplayId(p) === t
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
/**
|
|
24
32
|
* Read all watches from disk.
|
|
25
33
|
* @returns {Array<object>}
|
|
@@ -131,9 +139,7 @@ function evaluateWatch(watch, state) {
|
|
|
131
139
|
const { target, targetType, condition } = watch;
|
|
132
140
|
|
|
133
141
|
if (targetType === WATCH_TARGET_TYPE.PR) {
|
|
134
|
-
const pr = (state.pullRequests
|
|
135
|
-
String(p.prNumber) === String(target) || p.id === target
|
|
136
|
-
);
|
|
142
|
+
const pr = findPrByTarget(state.pullRequests, target);
|
|
137
143
|
if (!pr) return { triggered: false, message: `PR ${target} not found` };
|
|
138
144
|
|
|
139
145
|
// Store previous state for status-change detection
|
|
@@ -290,9 +296,7 @@ function checkWatches(config, state) {
|
|
|
290
296
|
*/
|
|
291
297
|
function _captureState(watch, state) {
|
|
292
298
|
if (watch.targetType === WATCH_TARGET_TYPE.PR) {
|
|
293
|
-
const pr = (state.pullRequests
|
|
294
|
-
String(p.prNumber) === String(watch.target) || p.id === watch.target
|
|
295
|
-
);
|
|
299
|
+
const pr = findPrByTarget(state.pullRequests, watch.target);
|
|
296
300
|
if (pr) return { status: pr.status, buildStatus: pr.buildStatus, reviewStatus: pr.reviewStatus, lastCommentDate: pr.humanFeedback?.lastProcessedCommentDate || null };
|
|
297
301
|
}
|
|
298
302
|
if (watch.targetType === WATCH_TARGET_TYPE.WORK_ITEM) {
|