@yemi33/minions 0.1.2012 → 0.1.2014
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/dashboard/js/render-prs.js +4 -2
- package/dashboard.js +2 -0
- package/engine/ado.js +170 -15
- package/package.json +1 -1
|
@@ -32,8 +32,10 @@ function prRow(pr) {
|
|
|
32
32
|
const reviewLabel = sq.status === 'waiting' ? 'reviewing (minions)' : sq.status ? sq.status + ' (minions)' : (effectiveReviewStatus || 'pending');
|
|
33
33
|
const reviewTitle = '';
|
|
34
34
|
const buildClass = pr._buildStatusStale ? 'build-stale' : pr.buildStatus === 'passing' ? 'build-pass' : pr.buildStatus === 'failing' ? 'build-fail' : pr.buildStatus === 'running' ? 'building' : 'no-build';
|
|
35
|
-
const buildLabel = (pr.buildStatus || 'none') + (pr._buildStatusStale ? ' (stale)' : '');
|
|
36
|
-
const buildTitle = pr.
|
|
35
|
+
const buildLabel = (pr.buildStatus || 'none') + (pr._buildStatusStale ? ' (stale)' : '') + (pr.buildStaleMergeCommit ? ' (stale merge commit)' : '');
|
|
36
|
+
const buildTitle = pr.buildStaleMergeCommit
|
|
37
|
+
? 'ADO has builds on this PR\'s merge ref, but none target the current merge commit (typically after a rebase or target-branch advance). Engine classified from the pre-rebase build set and queued a fresh build (rate limited 30m). See Issue #2747.'
|
|
38
|
+
: (pr._buildStatusDetail || '');
|
|
37
39
|
const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
|
|
38
40
|
const statusLabel = pr.status || 'active';
|
|
39
41
|
const branchError = pr._branchResolutionError?.reason || '';
|
package/dashboard.js
CHANGED
|
@@ -9904,6 +9904,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9904
9904
|
const { createMeeting } = require('./engine/meeting');
|
|
9905
9905
|
const meeting = createMeeting({ title: title.trim(), agenda: agenda.trim(), participants: meetingParticipants });
|
|
9906
9906
|
invalidateStatusCache();
|
|
9907
|
+
shared.mutateControl(control => ({ ...control, _wakeupAt: Date.now() }));
|
|
9907
9908
|
return jsonReply(res, 200, { ok: true, meeting });
|
|
9908
9909
|
}},
|
|
9909
9910
|
|
|
@@ -9936,6 +9937,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9936
9937
|
const meeting = advanceMeetingRound(body.id);
|
|
9937
9938
|
if (!meeting) return jsonReply(res, 404, { error: 'Meeting not found or already completed' });
|
|
9938
9939
|
invalidateStatusCache();
|
|
9940
|
+
shared.mutateControl(control => ({ ...control, _wakeupAt: Date.now() }));
|
|
9939
9941
|
return jsonReply(res, 200, { ok: true, meeting });
|
|
9940
9942
|
}},
|
|
9941
9943
|
|
package/engine/ado.js
CHANGED
|
@@ -407,6 +407,70 @@ function classifyBuildStatus(prBuilds) {
|
|
|
407
407
|
return 'none';
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Stale merge-commit fallback classifier (Issue #2747). When ADO returns
|
|
412
|
+
* builds on a PR's merge ref but none target the current merge commit
|
|
413
|
+
* (ADO recomputed merge-commit after a target-branch advance / rebase
|
|
414
|
+
* without re-queueing), classify the stale set:
|
|
415
|
+
* - Stale-failing → 'failing' + buildStaleMergeCommit:true. A stale
|
|
416
|
+
* failing signal is strictly more actionable than 'none'; a fix WI
|
|
417
|
+
* either does real work (failure still in the branch) or verifies
|
|
418
|
+
* green via push (rebase resolved it).
|
|
419
|
+
* - Stale-succeeded/other → preserve cached pr.buildStatus (or 'none')
|
|
420
|
+
* and do NOT set the stale-merge flag. Succeeded-on-pre-rebase is
|
|
421
|
+
* ambiguous and could be false confidence.
|
|
422
|
+
* Returns { buildStatus, buildStaleMergeCommit }.
|
|
423
|
+
*/
|
|
424
|
+
function classifyStaleBuilds(allBuilds, cachedBuildStatus) {
|
|
425
|
+
const staleStatus = classifyBuildStatus(allBuilds);
|
|
426
|
+
if (staleStatus === 'failing') {
|
|
427
|
+
return { buildStatus: 'failing', buildStaleMergeCommit: true };
|
|
428
|
+
}
|
|
429
|
+
return { buildStatus: cachedBuildStatus || 'none', buildStaleMergeCommit: false };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Filter for PR-validation builds — only these should be re-queued on
|
|
434
|
+
* stale-merge-commit detection. ADO records `triggerInfo.pullRequestId`
|
|
435
|
+
* when a build was triggered by a PR validation policy; some pipelines
|
|
436
|
+
* are conventionally named with "PR" in the title (e.g. "Office - PR"),
|
|
437
|
+
* which we also accept as a fallback signal.
|
|
438
|
+
*/
|
|
439
|
+
function isPrValidationBuild(build) {
|
|
440
|
+
if (!build) return false;
|
|
441
|
+
if (build.triggerInfo && build.triggerInfo.pullRequestId) return true;
|
|
442
|
+
const name = String(build?.definition?.name || '');
|
|
443
|
+
return /\bPR\b/i.test(name);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Rate-limit window for stale-merge-commit fresh-build requeue: at most one
|
|
447
|
+
// queue per PR per 30 min, tracked via pr._lastBuildRequeueAt timestamp.
|
|
448
|
+
const PR_BUILD_REQUEUE_INTERVAL_MS = 30 * 60 * 1000;
|
|
449
|
+
|
|
450
|
+
function shouldRequeueStaleBuild(pr, nowMs) {
|
|
451
|
+
if (!pr) return false;
|
|
452
|
+
const last = Number(pr._lastBuildRequeueAt) || 0;
|
|
453
|
+
const now = Number.isFinite(nowMs) ? nowMs : Date.now();
|
|
454
|
+
return (now - last) >= PR_BUILD_REQUEUE_INTERVAL_MS;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Queue a fresh PR-validation build via the ADO Builds REST API
|
|
459
|
+
* (equivalent of `az pipelines build queue --definition-id <id>
|
|
460
|
+
* --branch refs/pull/<n>/merge` but uses the engine's existing PAT and
|
|
461
|
+
* adoFetch wrapper — no external `az` CLI dependency). Forces ADO to
|
|
462
|
+
* recompute the merge commit so the next poll has real data. Failures
|
|
463
|
+
* are non-fatal — callers should try/catch and log warn.
|
|
464
|
+
*/
|
|
465
|
+
async function queueFreshAdoBuild({ orgBase, project, prNumber, definitionId, token }) {
|
|
466
|
+
const url = `${orgBase}/${project.adoProject}/_apis/build/builds?api-version=7.1`;
|
|
467
|
+
const body = JSON.stringify({
|
|
468
|
+
definition: { id: definitionId },
|
|
469
|
+
sourceBranch: `refs/pull/${prNumber}/merge`,
|
|
470
|
+
});
|
|
471
|
+
return adoFetch(url, token, { method: 'POST', body });
|
|
472
|
+
}
|
|
473
|
+
|
|
410
474
|
/** Map ADO reviewer vote array to a review status string. */
|
|
411
475
|
function votesToReviewStatus(votes) {
|
|
412
476
|
if (votes.some(v => v === -10)) return 'changes-requested';
|
|
@@ -1167,6 +1231,8 @@ async function pollPrStatus(config) {
|
|
|
1167
1231
|
let buildFailureSignature = pr.buildFailureSignature || '';
|
|
1168
1232
|
let buildStatusResolved = true;
|
|
1169
1233
|
let buildStatusStaleDetail = '';
|
|
1234
|
+
let buildStaleMergeCommit = false;
|
|
1235
|
+
let staleFailingDefinitions = []; // PR-validation defs to re-queue when stale-failing
|
|
1170
1236
|
|
|
1171
1237
|
if (prNumber && mergeCommitId) {
|
|
1172
1238
|
const buildRepositoryGuid = await resolveAdoBuildRepositoryGuid(project, token, orgBase, 'ADO build polling');
|
|
@@ -1194,17 +1260,43 @@ async function pollPrStatus(config) {
|
|
|
1194
1260
|
failed?.status,
|
|
1195
1261
|
].filter(Boolean).join('\n') || buildFailReason, 80);
|
|
1196
1262
|
}
|
|
1197
|
-
} else if (allBuilds.length > 0
|
|
1198
|
-
// Stale merge-commit
|
|
1199
|
-
//
|
|
1200
|
-
//
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1203
|
-
//
|
|
1263
|
+
} else if (allBuilds.length > 0) {
|
|
1264
|
+
// Stale merge-commit classification (Issue #2747). ADO returned
|
|
1265
|
+
// builds for this PR's merge ref but none target the current
|
|
1266
|
+
// `mergeCommitId` — most likely the target branch moved (rebase
|
|
1267
|
+
// or push), ADO recomputed the merge commit, but no new
|
|
1268
|
+
// source-side changes triggered a rebuild. Classify the stale
|
|
1269
|
+
// set: stale-failing flips to 'failing' + buildStaleMergeCommit
|
|
1270
|
+
// (strictly more actionable than 'none'); stale-succeeded/other
|
|
1271
|
+
// preserves cached state (no false confidence).
|
|
1272
|
+
const stale = classifyStaleBuilds(allBuilds, pr.buildStatus);
|
|
1273
|
+
buildStatus = stale.buildStatus;
|
|
1274
|
+
buildStaleMergeCommit = stale.buildStaleMergeCommit;
|
|
1275
|
+
if (buildStaleMergeCommit) {
|
|
1276
|
+
const failed = allBuilds.find(b => b.result === 'failed') || allBuilds.find(b => b.result === 'canceled');
|
|
1277
|
+
buildFailReason = failed?.definition?.name || 'Build failed (stale merge commit)';
|
|
1278
|
+
buildFailureSignature = shared.safeSlugComponent([
|
|
1279
|
+
failed?.definition?.name,
|
|
1280
|
+
failed?.result,
|
|
1281
|
+
failed?.status,
|
|
1282
|
+
'stale-merge-commit',
|
|
1283
|
+
].filter(Boolean).join('\n') || buildFailReason, 80);
|
|
1284
|
+
// Collect PR-validation definitions with failures for fresh-build queue.
|
|
1285
|
+
const seenDefIds = new Set();
|
|
1286
|
+
for (const b of allBuilds) {
|
|
1287
|
+
if ((b.result === 'failed' || b.result === 'canceled') && isPrValidationBuild(b)) {
|
|
1288
|
+
const defId = b?.definition?.id;
|
|
1289
|
+
if (defId != null && !seenDefIds.has(defId)) {
|
|
1290
|
+
seenDefIds.add(defId);
|
|
1291
|
+
staleFailingDefinitions.push({ id: defId, name: b?.definition?.name || `id=${defId}` });
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
} else if (pr.buildFailReason && buildStatus === pr.buildStatus) {
|
|
1296
|
+
buildFailReason = pr.buildFailReason;
|
|
1297
|
+
}
|
|
1204
1298
|
const sampleSv = (allBuilds[0]?.sourceVersion || '').slice(0, 8);
|
|
1205
|
-
log('warn', `PR ${pr.id} build: merge-commit mismatch — ${allBuilds.length} build(s) on merge ref, none target current merge commit ${String(mergeCommitId).slice(0, 8)} (sample sourceVersion ${sampleSv});
|
|
1206
|
-
buildStatus = pr.buildStatus;
|
|
1207
|
-
if (pr.buildFailReason) buildFailReason = pr.buildFailReason;
|
|
1299
|
+
log('warn', `PR ${pr.id} build: merge-commit mismatch — ${allBuilds.length} build(s) on merge ref, none target current merge commit ${String(mergeCommitId).slice(0, 8)} (sample sourceVersion ${sampleSv}); classified '${buildStatus}' from stale set${buildStaleMergeCommit ? ' (stale failing — will queue fresh build if rate limit allows)' : ''}`);
|
|
1208
1300
|
}
|
|
1209
1301
|
} catch (e) {
|
|
1210
1302
|
buildStatusResolved = false;
|
|
@@ -1272,6 +1364,47 @@ async function pollPrStatus(config) {
|
|
|
1272
1364
|
updated = true;
|
|
1273
1365
|
}
|
|
1274
1366
|
}
|
|
1367
|
+
// Persist buildStaleMergeCommit flag (Issue #2747) so the dashboard
|
|
1368
|
+
// can render the distinction and the auto-queue decision below has
|
|
1369
|
+
// a stable cross-tick signal. Clear when no longer stale.
|
|
1370
|
+
if (buildStaleMergeCommit) {
|
|
1371
|
+
if (!pr.buildStaleMergeCommit) {
|
|
1372
|
+
pr.buildStaleMergeCommit = true;
|
|
1373
|
+
updated = true;
|
|
1374
|
+
}
|
|
1375
|
+
} else if (pr.buildStaleMergeCommit) {
|
|
1376
|
+
delete pr.buildStaleMergeCommit;
|
|
1377
|
+
updated = true;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Auto-queue fresh build on stale-failing detection (Issue #2747).
|
|
1382
|
+
// Forces ADO to recompute the merge commit so the next poll has real
|
|
1383
|
+
// data instead of stuck-stale failing builds. Rate-limited to at most
|
|
1384
|
+
// one queue per PR per 30 min via pr._lastBuildRequeueAt. Only PR-
|
|
1385
|
+
// validation definitions are queued; non-PR builds are filtered out
|
|
1386
|
+
// upstream. Failures are non-fatal — logged warn and skipped.
|
|
1387
|
+
if (buildStatusResolved && buildStatus === 'failing' && buildStaleMergeCommit && staleFailingDefinitions.length > 0) {
|
|
1388
|
+
if (shouldRequeueStaleBuild(pr)) {
|
|
1389
|
+
let queuedCount = 0;
|
|
1390
|
+
for (const def of staleFailingDefinitions) {
|
|
1391
|
+
try {
|
|
1392
|
+
await queueFreshAdoBuild({ orgBase, project, prNumber, definitionId: def.id, token });
|
|
1393
|
+
queuedCount++;
|
|
1394
|
+
log('info', `PR ${pr.id} build: queued fresh build for definition '${def.name}' (id=${def.id}) on refs/pull/${prNumber}/merge to refresh stale merge-commit classification`);
|
|
1395
|
+
} catch (e) {
|
|
1396
|
+
log('warn', `PR ${pr.id} build: failed to queue fresh build for definition '${def.name}' (id=${def.id}): ${e.message}`);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
if (queuedCount > 0) {
|
|
1400
|
+
pr._lastBuildRequeueAt = Date.now();
|
|
1401
|
+
updated = true;
|
|
1402
|
+
}
|
|
1403
|
+
} else {
|
|
1404
|
+
const lastAt = Number(pr._lastBuildRequeueAt) || 0;
|
|
1405
|
+
const ageMin = Math.floor((Date.now() - lastAt) / 60000);
|
|
1406
|
+
log('info', `PR ${pr.id} build: stale-failing detected but skipping fresh-queue — last requeue ${ageMin}m ago (rate limit: 30 min)`);
|
|
1407
|
+
}
|
|
1275
1408
|
}
|
|
1276
1409
|
|
|
1277
1410
|
// Auto-complete: set auto-complete on PR when builds green + review approved
|
|
@@ -1766,12 +1899,16 @@ async function resetReviewerNegativeVote(pr, project) {
|
|
|
1766
1899
|
* mergeConflict: boolean,
|
|
1767
1900
|
* buildStatusStale?: boolean,
|
|
1768
1901
|
* buildStatusDetail?: string,
|
|
1902
|
+
* buildStaleMergeCommit?: boolean,
|
|
1769
1903
|
* }
|
|
1770
1904
|
*
|
|
1771
1905
|
* `buildStatus` is null when ADO has builds on the merge ref but none target the
|
|
1772
|
-
* current merge commit
|
|
1773
|
-
* matches pollPrStatus's "preserve previous
|
|
1774
|
-
* #1233; the caller must trust the cached
|
|
1906
|
+
* current merge commit AND the stale set is not failing (target-branch advance
|
|
1907
|
+
* with no source-side rebuild yet — matches pollPrStatus's "preserve previous
|
|
1908
|
+
* buildStatus" semantics from issue #1233; the caller must trust the cached
|
|
1909
|
+
* value). When the stale set IS failing, buildStatus flips to 'failing' and
|
|
1910
|
+
* `buildStaleMergeCommit: true` is returned (Issue #2747) — a stale failing
|
|
1911
|
+
* signal is strictly more actionable than cache fallback.
|
|
1775
1912
|
*/
|
|
1776
1913
|
async function checkLiveBuildAndConflict(pr, project) {
|
|
1777
1914
|
try {
|
|
@@ -1806,6 +1943,7 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
1806
1943
|
let buildStatus = null;
|
|
1807
1944
|
let buildStatusStale = false;
|
|
1808
1945
|
let buildStatusDetail = '';
|
|
1946
|
+
let buildStaleMergeCommit = false;
|
|
1809
1947
|
if (prData.status === 'active') {
|
|
1810
1948
|
const mergeCommitId = prData.lastMergeCommit?.commitId;
|
|
1811
1949
|
if (mergeCommitId) {
|
|
@@ -1824,9 +1962,19 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
1824
1962
|
buildStatus = classifyBuildStatus(prBuilds);
|
|
1825
1963
|
} else if (allBuilds.length === 0) {
|
|
1826
1964
|
buildStatus = 'none';
|
|
1965
|
+
} else {
|
|
1966
|
+
// Stale merge-commit (Issue #2747) — mirror pollPrStatus.
|
|
1967
|
+
// Stale-failing flips to 'failing' + buildStaleMergeCommit so
|
|
1968
|
+
// the caller skips the cache fallback and dispatches a fix.
|
|
1969
|
+
// Stale-succeeded leaves buildStatus null so caller falls
|
|
1970
|
+
// back to cached state (no false confidence).
|
|
1971
|
+
const stale = classifyStaleBuilds(allBuilds, pr?.buildStatus || null);
|
|
1972
|
+
if (stale.buildStaleMergeCommit) {
|
|
1973
|
+
buildStatus = stale.buildStatus;
|
|
1974
|
+
buildStaleMergeCommit = true;
|
|
1975
|
+
}
|
|
1976
|
+
// else: leave buildStatus null — caller falls back to cached state (issue #1233).
|
|
1827
1977
|
}
|
|
1828
|
-
// else: merge-commit mismatch — leave buildStatus null so caller
|
|
1829
|
-
// falls back to cached state (issue #1233).
|
|
1830
1978
|
} catch (e) {
|
|
1831
1979
|
buildStatusStale = true;
|
|
1832
1980
|
buildStatusDetail = `ADO live build query failed: ${e.message}`;
|
|
@@ -1844,6 +1992,7 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
1844
1992
|
buildStatus,
|
|
1845
1993
|
mergeConflict,
|
|
1846
1994
|
...(buildStatusStale ? { buildStatusStale, buildStatusDetail } : {}),
|
|
1995
|
+
...(buildStaleMergeCommit ? { buildStaleMergeCommit: true } : {}),
|
|
1847
1996
|
};
|
|
1848
1997
|
} catch (e) {
|
|
1849
1998
|
log('warn', `Live build/conflict check for ${pr.id}: ${e.message}`);
|
|
@@ -2222,4 +2371,10 @@ module.exports = {
|
|
|
2222
2371
|
VOTE_SNAPSHOT_MAX_AGE_MS,
|
|
2223
2372
|
STUCK_VOTE_AGE_THRESHOLD_MS,
|
|
2224
2373
|
STUCK_VOTE_WARN_INTERVAL_MS,
|
|
2374
|
+
// Issue #2747 — stale merge-commit classifier + auto-queue helpers, exported for unit tests.
|
|
2375
|
+
classifyBuildStatus,
|
|
2376
|
+
classifyStaleBuilds,
|
|
2377
|
+
isPrValidationBuild,
|
|
2378
|
+
shouldRequeueStaleBuild,
|
|
2379
|
+
PR_BUILD_REQUEUE_INTERVAL_MS,
|
|
2225
2380
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2014",
|
|
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"
|