@yemi33/minions 0.1.1616 → 0.1.1617
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/ado.js +89 -0
- package/engine/copilot-models.json +1 -1
- package/engine/github.js +68 -0
- package/engine.js +72 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/engine/ado.js
CHANGED
|
@@ -844,6 +844,77 @@ async function checkLiveReviewStatus(pr, project) {
|
|
|
844
844
|
}
|
|
845
845
|
}
|
|
846
846
|
|
|
847
|
+
/**
|
|
848
|
+
* Cheap pre-dispatch freshness check for build status and merge-conflict state.
|
|
849
|
+
* Mirrors checkLiveReviewStatus — fetches PR data once, classifies builds for the
|
|
850
|
+
* current merge commit, and reports whether ADO still considers the PR conflicted.
|
|
851
|
+
*
|
|
852
|
+
* Returns null if the check can't run (no token, no PR number, network error) so
|
|
853
|
+
* callers can fall back to cached state. Otherwise returns:
|
|
854
|
+
* {
|
|
855
|
+
* buildStatus: 'failing' | 'passing' | 'running' | 'none' | null,
|
|
856
|
+
* mergeConflict: boolean,
|
|
857
|
+
* }
|
|
858
|
+
*
|
|
859
|
+
* `buildStatus` is null when ADO has builds on the merge ref but none target the
|
|
860
|
+
* current merge commit (target-branch advance with no source-side rebuild yet —
|
|
861
|
+
* matches pollPrStatus's "preserve previous buildStatus" semantics from issue
|
|
862
|
+
* #1233; the caller must trust the cached value).
|
|
863
|
+
*/
|
|
864
|
+
async function checkLiveBuildAndConflict(pr, project) {
|
|
865
|
+
try {
|
|
866
|
+
const token = await getAdoToken();
|
|
867
|
+
if (!token) return null;
|
|
868
|
+
const orgBase = shared.getAdoOrgBase(project);
|
|
869
|
+
const prNum = shared.getPrNumber(pr);
|
|
870
|
+
if (!prNum) return null;
|
|
871
|
+
const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${project.repositoryId}`;
|
|
872
|
+
const prUrl = `${repoBase}/pullrequests/${prNum}?api-version=7.1`;
|
|
873
|
+
// 4s timeout — same budget as checkLiveReviewStatus. This is a pre-dispatch
|
|
874
|
+
// gate; we'd rather miss a freshness signal and fall back to cache than
|
|
875
|
+
// block dispatch on a slow ADO call.
|
|
876
|
+
const prData = await adoFetch(prUrl, token, { timeout: 4000 });
|
|
877
|
+
if (!prData) return null;
|
|
878
|
+
|
|
879
|
+
// Conflict signal — ADO reports `mergeStatus: 'conflicts'` when the merge
|
|
880
|
+
// would conflict; anything else means clean (or recomputing).
|
|
881
|
+
const mergeConflict = prData.mergeStatus === 'conflicts';
|
|
882
|
+
|
|
883
|
+
// Build signal — only meaningful when the PR is still open. We replicate
|
|
884
|
+
// pollPrStatus's narrowing logic so the live check and the cached poll
|
|
885
|
+
// agree on what 'failing' / 'passing' / 'running' / 'none' mean.
|
|
886
|
+
let buildStatus = null;
|
|
887
|
+
if (prData.status === 'active') {
|
|
888
|
+
const mergeCommitId = prData.lastMergeCommit?.commitId;
|
|
889
|
+
if (mergeCommitId) {
|
|
890
|
+
try {
|
|
891
|
+
const mergeRef = encodeURIComponent(`refs/pull/${prNum}/merge`);
|
|
892
|
+
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${project.repositoryId}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
893
|
+
const buildsData = await adoFetch(buildsUrl, token, { timeout: 4000 });
|
|
894
|
+
const allBuilds = buildsData?.value || [];
|
|
895
|
+
const prBuilds = allBuilds.filter(b => b.sourceVersion === mergeCommitId);
|
|
896
|
+
if (prBuilds.length > 0) {
|
|
897
|
+
buildStatus = classifyBuildStatus(prBuilds);
|
|
898
|
+
} else if (allBuilds.length === 0) {
|
|
899
|
+
buildStatus = 'none';
|
|
900
|
+
}
|
|
901
|
+
// else: merge-commit mismatch — leave buildStatus null so caller
|
|
902
|
+
// falls back to cached state (issue #1233).
|
|
903
|
+
} catch (e) { log('warn', `Live build check builds query for ${pr.id}: ${e.message}`); }
|
|
904
|
+
} else {
|
|
905
|
+
// No merge commit yet — likely conflict or fresh PR. Treat as 'none'
|
|
906
|
+
// so a stale 'failing' cache can be cleared by the caller.
|
|
907
|
+
buildStatus = 'none';
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
return { buildStatus, mergeConflict };
|
|
912
|
+
} catch (e) {
|
|
913
|
+
log('warn', `Live build/conflict check for ${pr.id}: ${e.message}`);
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
847
918
|
async function fetchAdoPrMetadata(prNum, adoOrg, adoProj, adoRepo) {
|
|
848
919
|
const token = await getAdoToken();
|
|
849
920
|
if (!token) return null;
|
|
@@ -968,6 +1039,22 @@ function _setAdoThrottleForTest(state) {
|
|
|
968
1039
|
_adoThrottle._setForTest(state);
|
|
969
1040
|
}
|
|
970
1041
|
|
|
1042
|
+
/** Inject a token into the cache — exported for testing only.
|
|
1043
|
+
* Lets tests exercise functions that call getAdoToken() without invoking azureauth.
|
|
1044
|
+
* Pass null to force getAdoToken() to return null synchronously (no exec). */
|
|
1045
|
+
function _setAdoTokenForTest(token) {
|
|
1046
|
+
if (token == null) {
|
|
1047
|
+
// Clear cache AND set a future failure backoff so getAdoToken short-circuits
|
|
1048
|
+
// to null without spawning azureauth — otherwise tests would hang on the
|
|
1049
|
+
// 15s execAsync timeout or open a real auth popup.
|
|
1050
|
+
_adoTokenCache = { token: null, expiresAt: 0 };
|
|
1051
|
+
_adoTokenFailedUntil = Date.now() + 60 * 60 * 1000;
|
|
1052
|
+
} else {
|
|
1053
|
+
_adoTokenCache = { token, expiresAt: Date.now() + 30 * 60 * 1000 };
|
|
1054
|
+
_adoTokenFailedUntil = 0;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
971
1058
|
module.exports = {
|
|
972
1059
|
getAdoToken,
|
|
973
1060
|
adoFetch,
|
|
@@ -975,6 +1062,7 @@ module.exports = {
|
|
|
975
1062
|
pollPrHumanComments,
|
|
976
1063
|
reconcilePrs,
|
|
977
1064
|
checkLiveReviewStatus,
|
|
1065
|
+
checkLiveBuildAndConflict,
|
|
978
1066
|
needsAdoPollRetry,
|
|
979
1067
|
isAdoAuthError, // exported for testing
|
|
980
1068
|
isAdoThrottled,
|
|
@@ -984,4 +1072,5 @@ module.exports = {
|
|
|
984
1072
|
findOpenPrOnBranch,
|
|
985
1073
|
_resetAdoThrottle, // exported for testing
|
|
986
1074
|
_setAdoThrottleForTest, // exported for testing
|
|
1075
|
+
_setAdoTokenForTest, // exported for testing
|
|
987
1076
|
};
|
package/engine/github.js
CHANGED
|
@@ -789,11 +789,79 @@ async function checkLiveReviewStatus(pr, project) {
|
|
|
789
789
|
}
|
|
790
790
|
}
|
|
791
791
|
|
|
792
|
+
/**
|
|
793
|
+
* Cheap pre-dispatch freshness check for build status and merge-conflict state.
|
|
794
|
+
* Mirrors checkLiveReviewStatus — fetches PR data once, classifies check-runs
|
|
795
|
+
* for the current head SHA, and reports whether GitHub still considers the PR
|
|
796
|
+
* unmergeable.
|
|
797
|
+
*
|
|
798
|
+
* Returns null if the check can't run (no slug/PR/network) so callers can fall
|
|
799
|
+
* back to cached state. Otherwise returns:
|
|
800
|
+
* {
|
|
801
|
+
* buildStatus: 'failing' | 'passing' | 'running' | 'none' | null,
|
|
802
|
+
* mergeConflict: boolean,
|
|
803
|
+
* }
|
|
804
|
+
*
|
|
805
|
+
* `mergeConflict` is true only when GitHub explicitly reports `mergeable: false`
|
|
806
|
+
* — `mergeable: null` means GitHub is still computing the merge state, so the
|
|
807
|
+
* caller should treat that as "no fresh signal" and trust the cache.
|
|
808
|
+
*
|
|
809
|
+
* `buildStatus` is null when we couldn't query check-runs or the PR isn't open;
|
|
810
|
+
* caller falls back to cached value.
|
|
811
|
+
*/
|
|
812
|
+
async function checkLiveBuildAndConflict(pr, project) {
|
|
813
|
+
try {
|
|
814
|
+
const slug = getRepoSlug(project);
|
|
815
|
+
if (!slug) return null;
|
|
816
|
+
const prNum = shared.getPrNumber(pr);
|
|
817
|
+
if (!prNum) return null;
|
|
818
|
+
const prData = await ghApi(`/pulls/${prNum}`, slug);
|
|
819
|
+
if (!prData || prData === GH_NOT_FOUND) return null;
|
|
820
|
+
|
|
821
|
+
// Conflict signal — only treat `mergeable === false` as a positive
|
|
822
|
+
// conflict. `null` means "GitHub still computing" → we have no fresh
|
|
823
|
+
// info, so fall back to whatever the cache says.
|
|
824
|
+
let mergeConflict;
|
|
825
|
+
if (prData.mergeable === false) mergeConflict = true;
|
|
826
|
+
else if (prData.mergeable === true) mergeConflict = false;
|
|
827
|
+
else mergeConflict = !!pr._mergeConflict; // computing — preserve cached view
|
|
828
|
+
|
|
829
|
+
// Build signal — only meaningful for open PRs. Mirrors pollPrStatus's
|
|
830
|
+
// check-runs classification so the live read and cached poll agree on
|
|
831
|
+
// 'failing' / 'passing' / 'running' / 'none'.
|
|
832
|
+
let buildStatus = null;
|
|
833
|
+
if (prData.state === 'open' && prData.head?.sha) {
|
|
834
|
+
try {
|
|
835
|
+
const checksData = await ghApi(`/commits/${prData.head.sha}/check-runs`, slug);
|
|
836
|
+
if (checksData && Array.isArray(checksData.check_runs)) {
|
|
837
|
+
const runs = checksData.check_runs;
|
|
838
|
+
if (runs.length === 0) {
|
|
839
|
+
buildStatus = 'none';
|
|
840
|
+
} else {
|
|
841
|
+
const hasFailed = runs.some(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
|
|
842
|
+
const allDone = runs.every(r => r.status === 'completed');
|
|
843
|
+
const allPassed = runs.every(r => r.conclusion === 'success' || r.conclusion === 'skipped' || r.conclusion === 'neutral');
|
|
844
|
+
if (hasFailed) buildStatus = 'failing';
|
|
845
|
+
else if (allDone && allPassed) buildStatus = 'passing';
|
|
846
|
+
else buildStatus = 'running';
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
} catch (e) { log('warn', `Live build check checks query for ${pr.id}: ${e.message}`); }
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return { buildStatus, mergeConflict };
|
|
853
|
+
} catch (e) {
|
|
854
|
+
log('warn', `Live build/conflict check for ${pr.id}: ${e.message}`);
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
792
859
|
module.exports = {
|
|
793
860
|
pollPrStatus,
|
|
794
861
|
pollPrHumanComments,
|
|
795
862
|
reconcilePrs,
|
|
796
863
|
checkLiveReviewStatus,
|
|
864
|
+
checkLiveBuildAndConflict,
|
|
797
865
|
isGhThrottled,
|
|
798
866
|
getGhThrottleState,
|
|
799
867
|
// Exported for testing
|
package/engine.js
CHANGED
|
@@ -1581,8 +1581,8 @@ function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
|
|
|
1581
1581
|
// ─── Inbox Consolidation (extracted to engine/consolidation.js) ──────────────
|
|
1582
1582
|
|
|
1583
1583
|
const { consolidateInbox } = require('./engine/consolidation');
|
|
1584
|
-
const { pollPrStatus, pollPrHumanComments, reconcilePrs, checkLiveReviewStatus: adoCheckLiveReview, needsAdoPollRetry, getAdoToken, isAdoThrottled } = require('./engine/ado');
|
|
1585
|
-
const { pollPrStatus: ghPollPrStatus, pollPrHumanComments: ghPollPrHumanComments, reconcilePrs: ghReconcilePrs, checkLiveReviewStatus: ghCheckLiveReview, isGhThrottled } = require('./engine/github');
|
|
1584
|
+
const { pollPrStatus, pollPrHumanComments, reconcilePrs, checkLiveReviewStatus: adoCheckLiveReview, checkLiveBuildAndConflict: adoCheckLiveBuildAndConflict, needsAdoPollRetry, getAdoToken, isAdoThrottled } = require('./engine/ado');
|
|
1585
|
+
const { pollPrStatus: ghPollPrStatus, pollPrHumanComments: ghPollPrHumanComments, reconcilePrs: ghReconcilePrs, checkLiveReviewStatus: ghCheckLiveReview, checkLiveBuildAndConflict: ghCheckLiveBuildAndConflict, isGhThrottled } = require('./engine/github');
|
|
1586
1586
|
|
|
1587
1587
|
// ─── State Snapshot ─────────────────────────────────────────────────────────
|
|
1588
1588
|
|
|
@@ -2255,6 +2255,39 @@ async function discoverFromPrs(config, project) {
|
|
|
2255
2255
|
|
|
2256
2256
|
const key = `build-fix-${project?.name || 'default'}-${prDisplayId}`;
|
|
2257
2257
|
if (fixThrottled || isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2258
|
+
|
|
2259
|
+
// Pre-dispatch live build check — cached buildStatus may be stale: ADO can
|
|
2260
|
+
// recompute the merge commit when master moves and pollPrStatus deliberately
|
|
2261
|
+
// preserves the previous 'failing' value (issue #1233); GitHub check-runs
|
|
2262
|
+
// may have flipped to 'passing' minutes before the next 12-tick poll. Mirror
|
|
2263
|
+
// the review/re-review live-vote guard so we don't dispatch a fix for a
|
|
2264
|
+
// build that has already recovered.
|
|
2265
|
+
try {
|
|
2266
|
+
const checkBcFn = project.repoHost === 'github' ? ghCheckLiveBuildAndConflict : adoCheckLiveBuildAndConflict;
|
|
2267
|
+
const live = await checkBcFn(pr, project);
|
|
2268
|
+
if (live && live.buildStatus && live.buildStatus !== 'failing') {
|
|
2269
|
+
log('info', `Pre-dispatch build check: ${pr.id} build is ${live.buildStatus} (cached was failing) — skipping build-fix`);
|
|
2270
|
+
// Persist the fresh status so subsequent ticks don't re-check on every pass
|
|
2271
|
+
try {
|
|
2272
|
+
mutatePullRequests(projectPrPath(project), prs => {
|
|
2273
|
+
const target = shared.findPrRecord(prs, pr, project);
|
|
2274
|
+
if (!target) return;
|
|
2275
|
+
target.buildStatus = live.buildStatus;
|
|
2276
|
+
if (live.buildStatus === 'passing') {
|
|
2277
|
+
delete target.buildErrorLog;
|
|
2278
|
+
delete target.buildFailReason;
|
|
2279
|
+
delete target._buildFailNotified;
|
|
2280
|
+
if (target.buildFixAttempts) {
|
|
2281
|
+
delete target.buildFixAttempts;
|
|
2282
|
+
delete target.buildFixEscalated;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
} catch {}
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
} catch (e) { log('warn', `Pre-dispatch build check for ${pr.id}: ${e.message} — skipping dispatch`); continue; }
|
|
2290
|
+
|
|
2258
2291
|
const agentId = resolveAgent('fix', config, pr.agent);
|
|
2259
2292
|
if (!agentId) continue;
|
|
2260
2293
|
|
|
@@ -2306,22 +2339,47 @@ async function discoverFromPrs(config, project) {
|
|
|
2306
2339
|
const conflictFixedAt = pr._conflictFixedAt;
|
|
2307
2340
|
const withinLag = conflictFixedAt && Date.now() - new Date(conflictFixedAt).getTime() < 10 * 60 * 1000;
|
|
2308
2341
|
if (!withinLag && !fixThrottled && !isAlreadyDispatched(key) && !isOnCooldown(key, cooldownMs)) {
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2342
|
+
// Pre-dispatch live conflict check — cached `_mergeConflict` may be
|
|
2343
|
+
// stale: ADO/GitHub recompute mergeStatus asynchronously (1–5 min lag),
|
|
2344
|
+
// so a successful upstream merge can leave the flag set even after the
|
|
2345
|
+
// conflict is gone. Mirror the review/re-review live-vote guard so we
|
|
2346
|
+
// don't dispatch a conflict-fix for a PR that's already clean.
|
|
2347
|
+
let liveSkip = false;
|
|
2348
|
+
try {
|
|
2349
|
+
const checkBcFn = project.repoHost === 'github' ? ghCheckLiveBuildAndConflict : adoCheckLiveBuildAndConflict;
|
|
2350
|
+
const live = await checkBcFn(pr, project);
|
|
2351
|
+
if (live && live.mergeConflict === false) {
|
|
2352
|
+
log('info', `Pre-dispatch conflict check: ${pr.id} reports clean merge (cached was conflict) — skipping conflict-fix`);
|
|
2319
2353
|
try {
|
|
2320
2354
|
mutatePullRequests(projectPrPath(project), prs => {
|
|
2321
2355
|
const target = shared.findPrRecord(prs, pr, project);
|
|
2322
|
-
if (target)
|
|
2356
|
+
if (!target) return;
|
|
2357
|
+
delete target._mergeConflict;
|
|
2358
|
+
delete target._conflictFixedAt;
|
|
2323
2359
|
});
|
|
2324
|
-
} catch
|
|
2360
|
+
} catch {}
|
|
2361
|
+
liveSkip = true;
|
|
2362
|
+
}
|
|
2363
|
+
} catch (e) { log('warn', `Pre-dispatch conflict check for ${pr.id}: ${e.message} — skipping dispatch`); liveSkip = true; }
|
|
2364
|
+
|
|
2365
|
+
if (!liveSkip) {
|
|
2366
|
+
const agentId = resolveAgent('fix', config, pr.agent);
|
|
2367
|
+
if (agentId) {
|
|
2368
|
+
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2369
|
+
pr_id: pr.id, pr_branch: pr.branch || '',
|
|
2370
|
+
review_note: `This PR has merge conflicts with the target branch. Resolve the conflicts:\n\n1. Pull latest from main/master\n2. Resolve all conflicts (prefer PR branch changes unless main has critical fixes)\n3. Build and test after resolving\n4. Push the resolved branch`,
|
|
2371
|
+
}, `Fix merge conflicts on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
|
|
2372
|
+
if (item) {
|
|
2373
|
+
newWork.push(item);
|
|
2374
|
+
setCooldown(key);
|
|
2375
|
+
// Record dispatch timestamp so re-dispatch is suppressed during ADO lag window
|
|
2376
|
+
try {
|
|
2377
|
+
mutatePullRequests(projectPrPath(project), prs => {
|
|
2378
|
+
const target = shared.findPrRecord(prs, pr, project);
|
|
2379
|
+
if (target) target._conflictFixedAt = new Date().toISOString();
|
|
2380
|
+
});
|
|
2381
|
+
} catch (e) { log('warn', `conflict-fix timestamp: ${e.message}`); }
|
|
2382
|
+
}
|
|
2325
2383
|
}
|
|
2326
2384
|
}
|
|
2327
2385
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1617",
|
|
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"
|