@yemi33/minions 0.1.1714 → 0.1.1715
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 +6 -3
- package/engine/ado.js +212 -0
- package/engine/copilot-models.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1715 (2026-05-04)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- auto-repair ADO project metadata (#2052)
|
|
7
|
+
|
|
8
|
+
## 0.1.1713 (2026-05-04)
|
|
4
9
|
|
|
5
10
|
### Features
|
|
6
|
-
- loosen work item create dedupe (#2053)
|
|
7
|
-
- shorten Windows worktree paths (#2051)
|
|
8
11
|
- fix ado project metadata discovery (#2048)
|
|
9
12
|
|
|
10
13
|
## 0.1.1712 (2026-05-04)
|
package/engine/ado.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const path = require('path');
|
|
7
|
+
const childProcess = require('child_process');
|
|
7
8
|
const shared = require('./shared');
|
|
8
9
|
const { exec, execAsync, getAdoOrgBase, log, ts, dateStamp, PR_STATUS, createThrottleTracker } = shared;
|
|
9
10
|
const { getPrs } = require('./queries');
|
|
@@ -74,6 +75,207 @@ function sameAdoProject(a, b) {
|
|
|
74
75
|
&& String(a.repoHost || 'ado').toLowerCase() === String(b.repoHost || 'ado').toLowerCase();
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
function decodeUrlSegment(segment) {
|
|
79
|
+
try { return decodeURIComponent(segment); } catch { return segment; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function stripGitSuffix(value) {
|
|
83
|
+
return String(value || '').trim().replace(/\.git$/i, '');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function encodeAdoPathSegment(value) {
|
|
87
|
+
return encodeURIComponent(String(value || '').trim());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildAdoPrUrlBase(meta) {
|
|
91
|
+
const repo = meta.repoName || meta.repositoryId || '';
|
|
92
|
+
if (!meta.adoOrg || !meta.adoProject || !repo) return '';
|
|
93
|
+
if (meta.host === 'visualstudio') {
|
|
94
|
+
const collection = meta.defaultCollection ? 'DefaultCollection/' : '';
|
|
95
|
+
return `https://${meta.adoOrg}.visualstudio.com/${collection}${encodeAdoPathSegment(meta.adoProject)}/_git/${encodeAdoPathSegment(repo)}/pullrequest/`;
|
|
96
|
+
}
|
|
97
|
+
return `https://dev.azure.com/${encodeAdoPathSegment(meta.adoOrg)}/${encodeAdoPathSegment(meta.adoProject)}/_git/${encodeAdoPathSegment(repo)}/pullrequest/`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function adoRepoCandidateFromParts(parts, source) {
|
|
101
|
+
const repo = stripGitSuffix(decodeUrlSegment(parts.repo || ''));
|
|
102
|
+
const repositoryId = isAdoGuid(repo) ? repo : '';
|
|
103
|
+
const repoName = repositoryId ? '' : repo;
|
|
104
|
+
const candidate = {
|
|
105
|
+
adoOrg: decodeUrlSegment(parts.org || '').trim(),
|
|
106
|
+
adoProject: decodeUrlSegment(parts.project || '').trim(),
|
|
107
|
+
repoName,
|
|
108
|
+
repositoryId,
|
|
109
|
+
host: parts.host || 'dev.azure',
|
|
110
|
+
defaultCollection: parts.defaultCollection === true,
|
|
111
|
+
source,
|
|
112
|
+
};
|
|
113
|
+
candidate.prUrlBase = buildAdoPrUrlBase(candidate);
|
|
114
|
+
return candidate;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseAdoRepoMetadata(value, source) {
|
|
118
|
+
const raw = String(value || '').trim();
|
|
119
|
+
if (!raw) return null;
|
|
120
|
+
|
|
121
|
+
const devAzure = raw.match(/https?:\/\/(?:[^/@]+@)?dev\.azure\.com\/([^/?#]+)\/([^/?#]+)\/_git\/([^/?#\s]+)/i);
|
|
122
|
+
if (devAzure) {
|
|
123
|
+
return adoRepoCandidateFromParts({
|
|
124
|
+
host: 'dev.azure',
|
|
125
|
+
org: devAzure[1],
|
|
126
|
+
project: devAzure[2],
|
|
127
|
+
repo: devAzure[3],
|
|
128
|
+
}, source);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const visualStudio = raw.match(/https?:\/\/(?:[^/@]+@)?([^/.@]+)\.visualstudio\.com\/(?:(DefaultCollection)\/)?([^/?#]+)\/_git\/([^/?#\s]+)/i);
|
|
132
|
+
if (visualStudio) {
|
|
133
|
+
return adoRepoCandidateFromParts({
|
|
134
|
+
host: 'visualstudio',
|
|
135
|
+
defaultCollection: !!visualStudio[2],
|
|
136
|
+
org: visualStudio[1],
|
|
137
|
+
project: visualStudio[3],
|
|
138
|
+
repo: visualStudio[4],
|
|
139
|
+
}, source);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const ssh = raw.match(/^(?:ssh:\/\/)?git@ssh\.dev\.azure\.com(?::|\/)v3\/([^/]+)\/([^/]+)\/([^/\s]+)$/i);
|
|
143
|
+
if (ssh) {
|
|
144
|
+
return adoRepoCandidateFromParts({
|
|
145
|
+
host: 'dev.azure',
|
|
146
|
+
org: ssh[1],
|
|
147
|
+
project: ssh[2],
|
|
148
|
+
repo: ssh[3],
|
|
149
|
+
}, source);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseCanonicalAdoPrId(value, source) {
|
|
156
|
+
const match = String(value || '').trim().match(/^ado:([^/#]+)\/([^/#]+)\/([^/#]+)#\d+$/i);
|
|
157
|
+
if (!match) return null;
|
|
158
|
+
return adoRepoCandidateFromParts({
|
|
159
|
+
host: 'dev.azure',
|
|
160
|
+
org: match[1],
|
|
161
|
+
project: match[2],
|
|
162
|
+
repo: match[3],
|
|
163
|
+
}, source);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeAdoOrgForCompare(value) {
|
|
167
|
+
return String(value || '').trim().toLowerCase().replace(/\.visualstudio\.com$/i, '');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeAdoFieldForCompare(value) {
|
|
171
|
+
return String(value || '').trim().toLowerCase();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isAdoRepairCandidateCompatible(project, candidate) {
|
|
175
|
+
if (!project || !candidate) return false;
|
|
176
|
+
if (project.adoOrg && candidate.adoOrg
|
|
177
|
+
&& normalizeAdoOrgForCompare(project.adoOrg) !== normalizeAdoOrgForCompare(candidate.adoOrg)) return false;
|
|
178
|
+
if (project.adoProject && candidate.adoProject
|
|
179
|
+
&& normalizeAdoFieldForCompare(project.adoProject) !== normalizeAdoFieldForCompare(candidate.adoProject)) return false;
|
|
180
|
+
if (project.repoName && candidate.repoName
|
|
181
|
+
&& normalizeAdoFieldForCompare(project.repoName) !== normalizeAdoFieldForCompare(candidate.repoName)) return false;
|
|
182
|
+
if (project.repositoryId && candidate.repositoryId
|
|
183
|
+
&& normalizeAdoFieldForCompare(project.repositoryId) !== normalizeAdoFieldForCompare(candidate.repositoryId)) return false;
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getMissingAdoProjectConfigFields(project) {
|
|
188
|
+
return ['adoOrg', 'adoProject', 'repoName', 'prUrlBase', 'repositoryId']
|
|
189
|
+
.filter(field => !String(project?.[field] || '').trim());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getOriginRemoteUrl(project) {
|
|
193
|
+
const localPath = String(project?.localPath || '').trim();
|
|
194
|
+
if (!localPath) return '';
|
|
195
|
+
try {
|
|
196
|
+
const result = childProcess.spawnSync('git', ['-C', localPath, 'config', '--get', 'remote.origin.url'], {
|
|
197
|
+
encoding: 'utf8',
|
|
198
|
+
windowsHide: true,
|
|
199
|
+
timeout: 3000,
|
|
200
|
+
});
|
|
201
|
+
if (result.status !== 0) return '';
|
|
202
|
+
return String(result.stdout || '').trim().split(/\r?\n/)[0] || '';
|
|
203
|
+
} catch {
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function collectAdoRepairCandidates(project, prs = []) {
|
|
209
|
+
const candidates = [];
|
|
210
|
+
const add = (candidate) => {
|
|
211
|
+
if (candidate && isAdoRepairCandidateCompatible(project, candidate)) candidates.push(candidate);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (project?.prUrlBase) add(parseAdoRepoMetadata(project.prUrlBase, 'configured prUrlBase'));
|
|
215
|
+
add(parseAdoRepoMetadata(getOriginRemoteUrl(project), 'origin remote'));
|
|
216
|
+
|
|
217
|
+
for (const pr of Array.isArray(prs) ? prs : []) {
|
|
218
|
+
add(parseAdoRepoMetadata(pr?.url, 'tracked PR URL'));
|
|
219
|
+
add(parseCanonicalAdoPrId(pr?.id, 'tracked canonical PR ID'));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return candidates;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function persistAdoProjectRepair(project, repairs, source) {
|
|
226
|
+
Object.assign(project, repairs);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
let persisted = false;
|
|
230
|
+
mutateJsonFileLocked(adoConfigPath(), (config) => {
|
|
231
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) return config;
|
|
232
|
+
if (!Array.isArray(config.projects)) return config;
|
|
233
|
+
const target = config.projects.find(p => sameAdoProject(p, project));
|
|
234
|
+
if (!target) return config;
|
|
235
|
+
for (const [field, value] of Object.entries(repairs)) {
|
|
236
|
+
if (!String(target[field] || '').trim()) target[field] = value;
|
|
237
|
+
}
|
|
238
|
+
persisted = true;
|
|
239
|
+
return config;
|
|
240
|
+
}, { defaultValue: { projects: [] }, skipWriteIfUnchanged: true });
|
|
241
|
+
const repairedFields = Object.keys(repairs).join(', ');
|
|
242
|
+
if (persisted) {
|
|
243
|
+
log('info', `Auto-repaired ADO project config for ${getAdoProjectLabel(project)} from ${source}: ${repairedFields}`);
|
|
244
|
+
} else {
|
|
245
|
+
log('warn', `Auto-repaired ADO project config for ${getAdoProjectLabel(project)} from ${source} in memory but could not find the project in config.json to persist it`);
|
|
246
|
+
}
|
|
247
|
+
} catch (e) {
|
|
248
|
+
log('warn', `Auto-repaired ADO project config for ${getAdoProjectLabel(project)} but failed to persist it: ${e.message}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function repairAdoProjectConfig(project, purpose, prs = null) {
|
|
253
|
+
if (!project || isGitHubProject(project)) return false;
|
|
254
|
+
const missingBefore = getMissingAdoProjectConfigFields(project);
|
|
255
|
+
if (missingBefore.length === 0) return false;
|
|
256
|
+
|
|
257
|
+
const trackedPrs = prs || shared.safeJson(shared.projectPrPath(project)) || [];
|
|
258
|
+
for (const candidate of collectAdoRepairCandidates(project, trackedPrs)) {
|
|
259
|
+
const repairs = {};
|
|
260
|
+
if (!project.adoOrg && candidate.adoOrg) repairs.adoOrg = candidate.adoOrg;
|
|
261
|
+
if (!project.adoProject && candidate.adoProject) repairs.adoProject = candidate.adoProject;
|
|
262
|
+
if (!project.repoName && candidate.repoName) repairs.repoName = candidate.repoName;
|
|
263
|
+
if (!project.repositoryId && candidate.repositoryId) repairs.repositoryId = candidate.repositoryId;
|
|
264
|
+
if (!project.prUrlBase && candidate.prUrlBase) repairs.prUrlBase = candidate.prUrlBase;
|
|
265
|
+
if (Object.keys(repairs).length > 0) {
|
|
266
|
+
persistAdoProjectRepair(project, repairs, candidate.source);
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const missingAfter = getMissingAdoProjectConfigFields(project)
|
|
272
|
+
.filter(field => field !== 'repositoryId' || !project.repoName);
|
|
273
|
+
if (missingAfter.some(field => field === 'adoOrg' || field === 'adoProject' || field === 'repoName')) {
|
|
274
|
+
log('warn', `${purpose} cannot auto-repair ADO project config for ${getAdoProjectLabel(project)}: missing ${missingAfter.map(f => `project.${f}`).join(', ')} and no trusted origin remote, tracked PR URL, or canonical ADO PR ID supplied enough metadata`);
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
77
279
|
function persistAdoRepositoryGuid(project, guid, repoName) {
|
|
78
280
|
if (!isAdoGuid(guid)) return;
|
|
79
281
|
const previous = String(project?.repositoryId || '').trim();
|
|
@@ -387,6 +589,7 @@ async function forEachActivePr(config, token, callback) {
|
|
|
387
589
|
|
|
388
590
|
for (const project of projects) {
|
|
389
591
|
if (isGitHubProject(project)) continue;
|
|
592
|
+
repairAdoProjectConfig(project, 'ADO PR polling');
|
|
390
593
|
if (!project.adoOrg || !project.adoProject) continue;
|
|
391
594
|
|
|
392
595
|
const prs = getPrs(project);
|
|
@@ -870,6 +1073,7 @@ async function reconcilePrs(config) {
|
|
|
870
1073
|
|
|
871
1074
|
for (const project of projects) {
|
|
872
1075
|
if (isGitHubProject(project)) continue;
|
|
1076
|
+
repairAdoProjectConfig(project, 'ADO PR reconciliation');
|
|
873
1077
|
if (!project.adoOrg || !project.adoProject) continue;
|
|
874
1078
|
const adoRepositoryId = getAdoRepositoryId(project);
|
|
875
1079
|
if (!adoRepositoryId) {
|
|
@@ -1020,6 +1224,8 @@ async function reconcilePrs(config) {
|
|
|
1020
1224
|
*/
|
|
1021
1225
|
async function checkLiveReviewStatus(pr, project) {
|
|
1022
1226
|
try {
|
|
1227
|
+
repairAdoProjectConfig(project, 'ADO live review check', pr ? [pr] : null);
|
|
1228
|
+
if (!project.adoOrg || !project.adoProject) return null;
|
|
1023
1229
|
const token = await getAdoToken();
|
|
1024
1230
|
if (!token) return null;
|
|
1025
1231
|
const orgBase = shared.getAdoOrgBase(project);
|
|
@@ -1066,6 +1272,8 @@ async function checkLiveReviewStatus(pr, project) {
|
|
|
1066
1272
|
*/
|
|
1067
1273
|
async function checkLiveBuildAndConflict(pr, project) {
|
|
1068
1274
|
try {
|
|
1275
|
+
repairAdoProjectConfig(project, 'ADO live build/conflict check', pr ? [pr] : null);
|
|
1276
|
+
if (!project.adoOrg || !project.adoProject) return null;
|
|
1069
1277
|
const token = await getAdoToken();
|
|
1070
1278
|
if (!token) return null;
|
|
1071
1279
|
const orgBase = shared.getAdoOrgBase(project);
|
|
@@ -1163,6 +1371,9 @@ async function fetchAdoPrMetadata(prNum, adoOrg, adoProj, adoRepo) {
|
|
|
1163
1371
|
* mergeConflict, url, project } or null on auth failure.
|
|
1164
1372
|
*/
|
|
1165
1373
|
async function fetchSinglePrBuildStatus(project, prNumber) {
|
|
1374
|
+
repairAdoProjectConfig(project, 'ADO single PR status fetch');
|
|
1375
|
+
if (!project.adoOrg || !project.adoProject) return null;
|
|
1376
|
+
|
|
1166
1377
|
const token = await getAdoToken();
|
|
1167
1378
|
if (!token) return null;
|
|
1168
1379
|
|
|
@@ -1261,6 +1472,7 @@ const getAdoThrottleState = () => _adoThrottle.getState();
|
|
|
1261
1472
|
* @returns {{ prNumber: number, url: string }|null}
|
|
1262
1473
|
*/
|
|
1263
1474
|
async function findOpenPrOnBranch(project, branch) {
|
|
1475
|
+
repairAdoProjectConfig(project, 'ADO branch PR lookup');
|
|
1264
1476
|
if (!project.adoOrg || !project.adoProject || !branch) return null;
|
|
1265
1477
|
const adoRepositoryId = getAdoRepositoryId(project);
|
|
1266
1478
|
if (!adoRepositoryId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1715",
|
|
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"
|