@yemi33/minions 0.1.2375 → 0.1.2376
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/engine/github.js +97 -73
- package/package.json +1 -1
package/engine/github.js
CHANGED
|
@@ -99,6 +99,28 @@ function getRepoSlug(project) {
|
|
|
99
99
|
return `${org}/${repo}`;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
function getPrRepoSlug(project, pr) {
|
|
103
|
+
const scope = shared.getPrScopeInfo(pr, pr?.url || '')?.scope || '';
|
|
104
|
+
if (scope && scope === shared.getProjectPrScope(project)) return getRepoSlug(project);
|
|
105
|
+
if (scope.startsWith('github:')) return scope.slice('github:'.length);
|
|
106
|
+
return getRepoSlug(project);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getProjectRepoSlugs(project) {
|
|
110
|
+
const primary = getRepoSlug(project);
|
|
111
|
+
const slugs = primary ? [primary] : [];
|
|
112
|
+
const seen = new Set(slugs.map(slug => slug.toLowerCase()));
|
|
113
|
+
for (const scope of shared.getProjectAllPrScopes(project)) {
|
|
114
|
+
if (!scope.startsWith('github:')) continue;
|
|
115
|
+
const slug = scope.slice('github:'.length);
|
|
116
|
+
if (!seen.has(slug)) {
|
|
117
|
+
slugs.push(slug);
|
|
118
|
+
seen.add(slug);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return slugs;
|
|
122
|
+
}
|
|
123
|
+
|
|
102
124
|
function getConfiguredGitHubAuthorLogins(config = {}) {
|
|
103
125
|
const accounts = config?.engine?.ghAccounts;
|
|
104
126
|
if (!accounts || typeof accounts !== 'object') return new Set();
|
|
@@ -520,40 +542,40 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
520
542
|
const src = project?.workSources?.pullRequests || config?.workSources?.pullRequests;
|
|
521
543
|
if (src && src.enabled === false) continue;
|
|
522
544
|
|
|
523
|
-
const slug = getRepoSlug(project);
|
|
524
|
-
if (!slug) continue;
|
|
525
|
-
|
|
526
|
-
// Skip projects in backoff (inaccessible repo)
|
|
527
|
-
if (isSlugInBackoff(slug)) continue;
|
|
528
|
-
|
|
529
545
|
const prs = getPrs(project);
|
|
530
546
|
const activePrs = prs.filter(pr => PR_POLLABLE_STATUSES.has(pr.status)
|
|
531
547
|
&& shared.isPrCompatibleWithProject(project, pr, pr.url || ''));
|
|
532
548
|
if (activePrs.length === 0) continue;
|
|
533
549
|
|
|
534
|
-
// Probe repo accessibility before iterating PRs — avoids N warnings per inaccessible repo.
|
|
535
|
-
// W-mp5trwh60008386d: ghApi returns the GH_NOT_FOUND sentinel on 404 (a frozen object,
|
|
536
|
-
// *not* null). The pre-fix gate only matched `null`, so a 404 on the base repo (caused by
|
|
537
|
-
// a multi-account `gh auth` switch, network blip, or token rotation) fell through and every
|
|
538
|
-
// per-PR call below 404'd, permanently flipping all active PRs to `abandoned`. We now treat
|
|
539
|
-
// both null and GH_NOT_FOUND as "skip the project for this tick" and explicitly do NOT
|
|
540
|
-
// increment per-PR `_consecutive404s` counters since no per-PR call was made.
|
|
541
|
-
const probe = await ghApi('', slug);
|
|
542
|
-
if (probe === null || probe === GH_NOT_FOUND) {
|
|
543
|
-
if (probe === GH_NOT_FOUND) {
|
|
544
|
-
log('warn', `GitHub repo probe for ${slug} returned 404 — skipping all per-PR polls for this project this tick (avoids spurious abandonments). Per-PR 404 counters NOT incremented.`);
|
|
545
|
-
}
|
|
546
|
-
recordSlugFailure(slug);
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
resetSlugBackoff(slug);
|
|
550
|
-
|
|
551
550
|
let projectUpdated = 0;
|
|
552
551
|
const updatedRecords = [];
|
|
552
|
+
const slugProbes = new Map();
|
|
553
553
|
|
|
554
554
|
for (const pr of activePrs) {
|
|
555
555
|
const prNum = shared.getPrNumber(pr);
|
|
556
556
|
if (!prNum) continue;
|
|
557
|
+
const slug = getPrRepoSlug(project, pr);
|
|
558
|
+
if (!slug) continue;
|
|
559
|
+
|
|
560
|
+
let probeResult = slugProbes.get(slug);
|
|
561
|
+
if (probeResult === undefined) {
|
|
562
|
+
if (isSlugInBackoff(slug)) {
|
|
563
|
+
probeResult = 'fail';
|
|
564
|
+
} else {
|
|
565
|
+
const probe = await ghApi('', slug);
|
|
566
|
+
probeResult = (probe === null || probe === GH_NOT_FOUND) ? 'fail' : 'ok';
|
|
567
|
+
if (probeResult === 'fail') {
|
|
568
|
+
if (probe === GH_NOT_FOUND) {
|
|
569
|
+
log('warn', `GitHub repo probe for ${slug} returned 404 — skipping all per-PR polls for this repository this tick (avoids spurious abandonments). Per-PR 404 counters NOT incremented.`);
|
|
570
|
+
}
|
|
571
|
+
recordSlugFailure(slug);
|
|
572
|
+
} else {
|
|
573
|
+
resetSlugBackoff(slug);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
slugProbes.set(slug, probeResult);
|
|
577
|
+
}
|
|
578
|
+
if (probeResult !== 'ok') continue;
|
|
557
579
|
|
|
558
580
|
try {
|
|
559
581
|
const before = shared.snapshotPrRecord(pr);
|
|
@@ -612,7 +634,10 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
612
634
|
const centralPrs = safeJsonArr(centralPath);
|
|
613
635
|
// Build a slug→project map for configured GitHub projects so we can detect
|
|
614
636
|
// PRs that are actually owned by the project loop (present in per-project file).
|
|
615
|
-
const configuredGhProjectsBySlug = new Map(
|
|
637
|
+
const configuredGhProjectsBySlug = new Map();
|
|
638
|
+
for (const project of projects) {
|
|
639
|
+
for (const slug of getProjectRepoSlugs(project)) configuredGhProjectsBySlug.set(slug.toLowerCase(), project);
|
|
640
|
+
}
|
|
616
641
|
const activeCentral = centralPrs.filter(pr => {
|
|
617
642
|
if (!PR_POLLABLE_STATUSES.has(pr.status)) return false;
|
|
618
643
|
if (!pr.url) return false;
|
|
@@ -621,7 +646,7 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
621
646
|
// the project loop will never see it — the central branch must poll it.
|
|
622
647
|
const ghMatch = pr.url.match(/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/);
|
|
623
648
|
if (ghMatch) {
|
|
624
|
-
const owningProject = configuredGhProjectsBySlug.get(ghMatch[1]);
|
|
649
|
+
const owningProject = configuredGhProjectsBySlug.get(ghMatch[1].toLowerCase());
|
|
625
650
|
if (owningProject) {
|
|
626
651
|
const projectPrs = safeJsonArr(projectPrPath(owningProject));
|
|
627
652
|
if (projectPrs.some(p => p.id === pr.id)) return false; // present → project loop handles it
|
|
@@ -1425,12 +1450,6 @@ async function reconcilePrs(config) {
|
|
|
1425
1450
|
let totalAdded = 0;
|
|
1426
1451
|
|
|
1427
1452
|
for (const project of projects) {
|
|
1428
|
-
const slug = getRepoSlug(project);
|
|
1429
|
-
if (!slug) continue;
|
|
1430
|
-
|
|
1431
|
-
// Skip projects in backoff (inaccessible repo)
|
|
1432
|
-
if (isSlugInBackoff(slug)) continue;
|
|
1433
|
-
|
|
1434
1453
|
// Skip projects with no tracked PRs and no work items — nothing to reconcile
|
|
1435
1454
|
const existingPrs = getPrs(project);
|
|
1436
1455
|
if (existingPrs.length === 0) {
|
|
@@ -1441,18 +1460,21 @@ async function reconcilePrs(config) {
|
|
|
1441
1460
|
} catch { continue; }
|
|
1442
1461
|
}
|
|
1443
1462
|
|
|
1444
|
-
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1463
|
+
const ghPrs = [];
|
|
1464
|
+
for (const slug of getProjectRepoSlugs(project)) {
|
|
1465
|
+
if (isSlugInBackoff(slug)) continue;
|
|
1466
|
+
// Fetch open PRs — paginate to handle repos with >100 open PRs.
|
|
1467
|
+
const prsData = await ghApi('/pulls?state=open&per_page=100', slug, { paginate: true, timeout: 60000 });
|
|
1468
|
+
if (!prsData || !Array.isArray(prsData)) {
|
|
1469
|
+
recordSlugFailure(slug);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
resetSlugBackoff(slug);
|
|
1473
|
+
for (const pr of prsData) {
|
|
1474
|
+
const branch = pr.head?.ref || '';
|
|
1475
|
+
if (branchPatterns.some(pat => pat.test(branch))) ghPrs.push({ pr, slug });
|
|
1476
|
+
}
|
|
1449
1477
|
}
|
|
1450
|
-
resetSlugBackoff(slug);
|
|
1451
|
-
|
|
1452
|
-
const ghPrs = prsData.filter(pr => {
|
|
1453
|
-
const branch = pr.head?.ref || '';
|
|
1454
|
-
return branchPatterns.some(pat => pat.test(branch));
|
|
1455
|
-
});
|
|
1456
1478
|
|
|
1457
1479
|
if (ghPrs.length === 0) continue;
|
|
1458
1480
|
|
|
@@ -1470,8 +1492,12 @@ async function reconcilePrs(config) {
|
|
|
1470
1492
|
const centralItems = safeJsonArr(centralWiPath);
|
|
1471
1493
|
const allItems = [...workItems, ...centralItems];
|
|
1472
1494
|
|
|
1473
|
-
for (const ghPr of ghPrs) {
|
|
1474
|
-
const
|
|
1495
|
+
for (const { pr: ghPr, slug } of ghPrs) {
|
|
1496
|
+
const primarySlug = getRepoSlug(project);
|
|
1497
|
+
const prUrl = ghPr.html_url
|
|
1498
|
+
|| (slug.toLowerCase() === primarySlug?.toLowerCase() && project.prUrlBase
|
|
1499
|
+
? project.prUrlBase + ghPr.number
|
|
1500
|
+
: `https://github.com/${slug}/pull/${ghPr.number}`);
|
|
1475
1501
|
const prId = shared.getCanonicalPrId(project, ghPr.number, prUrl);
|
|
1476
1502
|
// P-a7c4d2e8 (F3): validate API-derived branch ref before persistence
|
|
1477
1503
|
// / regex matching. Defensive degrade — log and treat as missing on
|
|
@@ -1604,7 +1630,7 @@ async function reconcilePrs(config) {
|
|
|
1604
1630
|
*/
|
|
1605
1631
|
async function checkLiveReviewStatus(pr, project) {
|
|
1606
1632
|
try {
|
|
1607
|
-
const slug =
|
|
1633
|
+
const slug = getPrRepoSlug(project, pr);
|
|
1608
1634
|
if (!slug) return null;
|
|
1609
1635
|
const prNum = shared.getPrNumber(pr);
|
|
1610
1636
|
if (!prNum) return null;
|
|
@@ -1654,7 +1680,7 @@ async function checkLiveReviewStatus(pr, project) {
|
|
|
1654
1680
|
async function dismissPriorViewerChangesRequestedReviews(pr, project) {
|
|
1655
1681
|
let tmpFile = null;
|
|
1656
1682
|
try {
|
|
1657
|
-
const slug =
|
|
1683
|
+
const slug = getPrRepoSlug(project, pr);
|
|
1658
1684
|
if (!slug) return null;
|
|
1659
1685
|
const prNum = shared.getPrNumber(pr);
|
|
1660
1686
|
if (!prNum) return null;
|
|
@@ -1758,7 +1784,7 @@ async function dismissPriorViewerChangesRequestedReviews(pr, project) {
|
|
|
1758
1784
|
*/
|
|
1759
1785
|
async function checkLiveBuildAndConflict(pr, project) {
|
|
1760
1786
|
try {
|
|
1761
|
-
const slug =
|
|
1787
|
+
const slug = getPrRepoSlug(project, pr);
|
|
1762
1788
|
if (!slug) return null;
|
|
1763
1789
|
const prNum = shared.getPrNumber(pr);
|
|
1764
1790
|
if (!prNum) return null;
|
|
@@ -1846,9 +1872,6 @@ async function reconcileAbandonedPrs(config) {
|
|
|
1846
1872
|
const slugProbeCache = new Map(); // slug → 'ok' | 'fail'
|
|
1847
1873
|
|
|
1848
1874
|
for (const project of projects) {
|
|
1849
|
-
const slug = getRepoSlug(project);
|
|
1850
|
-
if (!slug) continue;
|
|
1851
|
-
|
|
1852
1875
|
const prPath = projectPrPath(project);
|
|
1853
1876
|
const prs = safeJsonArr(prPath);
|
|
1854
1877
|
// Filter: only abandoned PRs that don't already have the confirmed-404
|
|
@@ -1862,37 +1885,35 @@ async function reconcileAbandonedPrs(config) {
|
|
|
1862
1885
|
);
|
|
1863
1886
|
if (abandonedPrs.length === 0) continue;
|
|
1864
1887
|
|
|
1865
|
-
//
|
|
1866
|
-
//
|
|
1867
|
-
//
|
|
1868
|
-
let probeResult = slugProbeCache.get(slug);
|
|
1869
|
-
if (probeResult === undefined) {
|
|
1870
|
-
const probe = await ghApi('', slug);
|
|
1871
|
-
probeResult = (probe === null || probe === GH_NOT_FOUND) ? 'fail' : 'ok';
|
|
1872
|
-
slugProbeCache.set(slug, probeResult);
|
|
1873
|
-
}
|
|
1874
|
-
if (probeResult === 'fail') {
|
|
1875
|
-
log('warn', `Abandoned PR reconciliation: skipping ${slug} (${abandonedPrs.length} PR${abandonedPrs.length === 1 ? '' : 's'}) — base-repo probe failed, retry next startup`);
|
|
1876
|
-
skipped += abandonedPrs.length;
|
|
1877
|
-
continue;
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
// Per-PR re-probe. Collect updates first, then apply via mutatePullRequests
|
|
1881
|
-
// for atomic single-writer semantics. We match by prNumber on writeback
|
|
1882
|
-
// (not id) because mutatePullRequests calls normalizePrRecords which can
|
|
1883
|
-
// lowercase the id slug — prNumber is the stable key within a project's
|
|
1884
|
-
// pull-requests.json.
|
|
1885
|
-
const updates = []; // { prNumber, action, newStatus?, mergedAt? }
|
|
1888
|
+
// Per-PR re-probe. Canonical ids keep same-number PRs from different
|
|
1889
|
+
// repository scopes isolated during writeback.
|
|
1890
|
+
const updates = []; // { prId, prNumber, action, newStatus?, mergedAt? }
|
|
1886
1891
|
for (const pr of abandonedPrs) {
|
|
1887
1892
|
const prNum = shared.getPrNumber(pr);
|
|
1888
1893
|
if (!prNum) continue;
|
|
1894
|
+
const slug = getPrRepoSlug(project, pr);
|
|
1895
|
+
if (!slug) continue;
|
|
1896
|
+
const prId = shared.getCanonicalPrId(project, pr, pr.url || '');
|
|
1897
|
+
if (!prId) continue;
|
|
1898
|
+
|
|
1899
|
+
let probeResult = slugProbeCache.get(slug);
|
|
1900
|
+
if (probeResult === undefined) {
|
|
1901
|
+
const probe = await ghApi('', slug);
|
|
1902
|
+
probeResult = (probe === null || probe === GH_NOT_FOUND) ? 'fail' : 'ok';
|
|
1903
|
+
slugProbeCache.set(slug, probeResult);
|
|
1904
|
+
}
|
|
1905
|
+
if (probeResult === 'fail') {
|
|
1906
|
+
log('warn', `Abandoned PR reconciliation: skipping ${slug} — base-repo probe failed, retry next startup`);
|
|
1907
|
+
skipped++;
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1889
1910
|
|
|
1890
1911
|
try {
|
|
1891
1912
|
const prData = await ghApi(`/pulls/${prNum}`, slug);
|
|
1892
1913
|
if (prData === GH_NOT_FOUND) {
|
|
1893
1914
|
// 404 with base-probe OK → genuinely deleted. Mark so we don't
|
|
1894
1915
|
// re-probe this PR on future startups.
|
|
1895
|
-
updates.push({ prNumber: prNum, action: 'confirm404' });
|
|
1916
|
+
updates.push({ prId, prNumber: prNum, action: 'confirm404' });
|
|
1896
1917
|
confirmedDeleted++;
|
|
1897
1918
|
log('info', `Confirmed PR #${prNum} (${slug}): truly deleted, leaving abandoned`);
|
|
1898
1919
|
} else if (prData) {
|
|
@@ -1915,6 +1936,7 @@ async function reconcileAbandonedPrs(config) {
|
|
|
1915
1936
|
continue;
|
|
1916
1937
|
}
|
|
1917
1938
|
updates.push({
|
|
1939
|
+
prId,
|
|
1918
1940
|
prNumber: prNum,
|
|
1919
1941
|
action: 'flip',
|
|
1920
1942
|
newStatus,
|
|
@@ -1938,7 +1960,9 @@ async function reconcileAbandonedPrs(config) {
|
|
|
1938
1960
|
const reconciledAt = ts();
|
|
1939
1961
|
mutatePullRequests(prPath, (currentPrs) => {
|
|
1940
1962
|
for (const upd of updates) {
|
|
1941
|
-
const pr = currentPrs.find(p =>
|
|
1963
|
+
const pr = currentPrs.find(p =>
|
|
1964
|
+
shared.getCanonicalPrId(project, p, p.url || '') === upd.prId
|
|
1965
|
+
);
|
|
1942
1966
|
if (!pr) continue;
|
|
1943
1967
|
// Defensive: never downgrade a merged record. Should already be
|
|
1944
1968
|
// filtered by the abandoned-only scan above, but a concurrent writer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2376",
|
|
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"
|