@yemi33/minions 0.1.1637 → 0.1.1638
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/dashboard/js/render-prs.js +5 -1
- package/dashboard.js +6 -2
- package/engine/ado.js +13 -1
- package/engine/copilot-models.json +1 -1
- package/engine/github.js +11 -2
- package/engine.js +59 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -20,11 +20,15 @@ function prRow(pr) {
|
|
|
20
20
|
const statusLabel = pr.status || 'active';
|
|
21
21
|
const url = pr.url || '#';
|
|
22
22
|
const prId = pr.id || '—';
|
|
23
|
+
const pendingReason = pr._pendingReason ? String(pr._pendingReason) : '';
|
|
24
|
+
const pendingReasonHtml = pendingReason
|
|
25
|
+
? '<div style="font-size:9px;color:var(--muted);margin-top:2px" title="Pending reason: ' + escapeHtml(pendingReason) + '">' + escapeHtml(pendingReason.replace(/_/g, ' ')) + '</div>'
|
|
26
|
+
: '';
|
|
23
27
|
return '<tr>' +
|
|
24
28
|
'<td><span class="pr-id">' + escapeHtml(String(prId)) + '</span></td>' +
|
|
25
29
|
'<td><a class="pr-title" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(pr.title || 'Untitled') + '</a>' + (pr.description ? '<div class="pr-desc">' + escapeHtml(pr.description.length > 120 ? pr.description.slice(0, 120) + '...' : pr.description) + '</div>' : '') + '</td>' +
|
|
26
30
|
'<td><span class="pr-agent">' + escapeHtml(pr.agent || '—') + '</span></td>' +
|
|
27
|
-
'<td><span class="pr-branch">' + escapeHtml(pr.branch || '—') + '</span
|
|
31
|
+
'<td><span class="pr-branch">' + escapeHtml(pr.branch || '—') + '</span>' + pendingReasonHtml + '</td>' +
|
|
28
32
|
'<td><span class="pr-badge ' + reviewClass + '"' + (reviewTitle ? ' title="' + escapeHtml(reviewTitle) + '"' : '') + '>' + escapeHtml(reviewLabel) + '</span></td>' +
|
|
29
33
|
'<td>' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' + escapeHtml(sq.note || '') + '">' + escapeHtml(sq.reviewer) + '</span>' : sq.reviewer && sq.status === 'waiting' ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escapeHtml(sq.reviewer) + '…</span>' : pr.reviewedBy && pr.reviewedBy.length ? '<span class="pr-agent">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
|
|
30
34
|
'<td><span class="pr-badge ' + buildClass + '">' + escapeHtml(buildLabel) + '</span></td>' +
|
package/dashboard.js
CHANGED
|
@@ -5853,6 +5853,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5853
5853
|
const prNumMatch = url.match(/\/pull\/(\d+)|pullrequest\/(\d+)/);
|
|
5854
5854
|
const prNum = prNumMatch ? (prNumMatch[1] || prNumMatch[2]) : Date.now().toString().slice(-6);
|
|
5855
5855
|
const prId = shared.getCanonicalPrId(targetProject, prNum, url);
|
|
5856
|
+
const contextText = typeof context === 'string' ? context : (context == null ? '' : JSON.stringify(context));
|
|
5856
5857
|
|
|
5857
5858
|
// Atomic check-and-insert to prevent duplicates and races with polling loops
|
|
5858
5859
|
let duplicate = false;
|
|
@@ -5874,7 +5875,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5874
5875
|
_manual: true,
|
|
5875
5876
|
_contextOnly: !autoObserve,
|
|
5876
5877
|
_autoObserve: !!autoObserve,
|
|
5877
|
-
_context:
|
|
5878
|
+
_context: contextText,
|
|
5878
5879
|
});
|
|
5879
5880
|
return prs;
|
|
5880
5881
|
}, { defaultValue: [] });
|
|
@@ -5906,7 +5907,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5906
5907
|
// Remote title always wins — any user-supplied title is a placeholder (closes #1283)
|
|
5907
5908
|
if (prData.title) pr.title = prData.title.slice(0, 120);
|
|
5908
5909
|
if (prData.description) pr.description = prData.description.slice(0, 500);
|
|
5909
|
-
if (!pr.branch && prData.branch)
|
|
5910
|
+
if (!pr.branch && prData.branch) {
|
|
5911
|
+
pr.branch = prData.branch;
|
|
5912
|
+
if (pr._pendingReason === 'missing_pr_branch') delete pr._pendingReason;
|
|
5913
|
+
}
|
|
5910
5914
|
if (pr.agent === 'human' && prData.author) pr.agent = prData.author;
|
|
5911
5915
|
return prs;
|
|
5912
5916
|
}, { defaultValue: [] });
|
package/engine/ado.js
CHANGED
|
@@ -405,6 +405,12 @@ async function pollPrStatus(config) {
|
|
|
405
405
|
pr._adoSourceCommit = sourceCommit;
|
|
406
406
|
updated = true;
|
|
407
407
|
}
|
|
408
|
+
const sourceBranch = stripRefsHeads(prData.sourceRefName);
|
|
409
|
+
if (!pr.branch && sourceBranch) {
|
|
410
|
+
pr.branch = sourceBranch;
|
|
411
|
+
if (pr._pendingReason === 'missing_pr_branch') delete pr._pendingReason;
|
|
412
|
+
updated = true;
|
|
413
|
+
}
|
|
408
414
|
|
|
409
415
|
const reviewers = prData.reviewers || [];
|
|
410
416
|
const votes = reviewers.map(r => r.vote).filter(v => v !== undefined);
|
|
@@ -759,6 +765,7 @@ async function reconcilePrs(config) {
|
|
|
759
765
|
shared.normalizePrRecords(existingPrs, project);
|
|
760
766
|
const existingIds = new Set(existingPrs.map(p => p.id));
|
|
761
767
|
let projectAdded = 0;
|
|
768
|
+
let metadataUpdated = 0;
|
|
762
769
|
|
|
763
770
|
// Load work items to match branches to work item IDs
|
|
764
771
|
const wiPath = shared.projectWorkItemsPath(project);
|
|
@@ -786,6 +793,11 @@ async function reconcilePrs(config) {
|
|
|
786
793
|
if (existing && existing.prNumber == null) {
|
|
787
794
|
existing.prNumber = adoPr.pullRequestId;
|
|
788
795
|
}
|
|
796
|
+
if (existing && !existing.branch && branch) {
|
|
797
|
+
existing.branch = branch;
|
|
798
|
+
if (existing._pendingReason === 'missing_pr_branch') delete existing._pendingReason;
|
|
799
|
+
metadataUpdated++;
|
|
800
|
+
}
|
|
789
801
|
// PR already tracked — write link to pr-links.json if we can extract an ID
|
|
790
802
|
if (confirmedItemId) {
|
|
791
803
|
addPrLink(prId, confirmedItemId, { project, prNumber: adoPr.pullRequestId, url: prUrl });
|
|
@@ -832,7 +844,7 @@ async function reconcilePrs(config) {
|
|
|
832
844
|
// Backfill prdItems from pr-links for any PR with empty array
|
|
833
845
|
const backfilled = shared.backfillPrPrdItems(existingPrs, shared.getPrLinks());
|
|
834
846
|
|
|
835
|
-
if (projectAdded > 0 || projectUpdated > 0 || backfilled > 0) {
|
|
847
|
+
if (projectAdded > 0 || projectUpdated > 0 || backfilled > 0 || metadataUpdated > 0) {
|
|
836
848
|
mutateJsonFileLocked(prPath, (currentPrs) => {
|
|
837
849
|
// Merge reconciled PRs into the locked copy by ID
|
|
838
850
|
for (const pr of existingPrs) {
|
package/engine/github.js
CHANGED
|
@@ -282,7 +282,10 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
282
282
|
}
|
|
283
283
|
if (pr.description === undefined) pr.description = (prData.body || '').slice(0, 500);
|
|
284
284
|
if (pr.agent === 'human' && prData.user?.login) pr.agent = prData.user.login;
|
|
285
|
-
if (!pr.branch && prData.head?.ref)
|
|
285
|
+
if (!pr.branch && prData.head?.ref) {
|
|
286
|
+
pr.branch = prData.head.ref;
|
|
287
|
+
if (pr._pendingReason === 'missing_pr_branch') delete pr._pendingReason;
|
|
288
|
+
}
|
|
286
289
|
}
|
|
287
290
|
}
|
|
288
291
|
centralUpdated++;
|
|
@@ -673,6 +676,7 @@ async function reconcilePrs(config) {
|
|
|
673
676
|
const currentPrs = safeJson(prPath) || [];
|
|
674
677
|
shared.normalizePrRecords(currentPrs, project);
|
|
675
678
|
const existingIds = new Set(currentPrs.map(p => p.id));
|
|
679
|
+
let metadataUpdated = 0;
|
|
676
680
|
let projectAdded = 0;
|
|
677
681
|
|
|
678
682
|
// Load work items to match branches
|
|
@@ -697,6 +701,11 @@ async function reconcilePrs(config) {
|
|
|
697
701
|
if (existing && existing.prNumber == null) {
|
|
698
702
|
existing.prNumber = ghPr.number;
|
|
699
703
|
}
|
|
704
|
+
if (existing && !existing.branch && branch) {
|
|
705
|
+
existing.branch = branch;
|
|
706
|
+
if (existing._pendingReason === 'missing_pr_branch') delete existing._pendingReason;
|
|
707
|
+
metadataUpdated++;
|
|
708
|
+
}
|
|
700
709
|
if (confirmedItemId) {
|
|
701
710
|
addPrLink(prId, confirmedItemId, { project, prNumber: ghPr.number, url: prUrl });
|
|
702
711
|
if (existing && !(existing.prdItems || []).includes(confirmedItemId)) {
|
|
@@ -743,7 +752,7 @@ async function reconcilePrs(config) {
|
|
|
743
752
|
// Backfill prdItems from pr-links for any PR with empty array
|
|
744
753
|
const backfilled = backfillPrPrdItems(currentPrs, getPrLinks());
|
|
745
754
|
|
|
746
|
-
if (projectAdded > 0 || backfilled > 0) {
|
|
755
|
+
if (projectAdded > 0 || backfilled > 0 || metadataUpdated > 0) {
|
|
747
756
|
mutateJsonFileLocked(prPath, (lockedPrs) => {
|
|
748
757
|
for (const pr of currentPrs) {
|
|
749
758
|
const idx = lockedPrs.findIndex(p => p.id === pr.id);
|
package/engine.js
CHANGED
|
@@ -1980,6 +1980,58 @@ function clearPendingHumanFeedbackFlag(projectMeta, prId) {
|
|
|
1980
1980
|
} catch (e) { log('warn', 'clear pending human feedback flag: ' + e.message); }
|
|
1981
1981
|
}
|
|
1982
1982
|
|
|
1983
|
+
const PR_PENDING_MISSING_BRANCH = 'missing_pr_branch';
|
|
1984
|
+
|
|
1985
|
+
function getPrDispatchBranch(pr) {
|
|
1986
|
+
return typeof pr?.branch === 'string' ? pr.branch.trim() : '';
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function mutatePrPendingReason(project, pr, mutator) {
|
|
1990
|
+
const prPath = projectPrPath(project);
|
|
1991
|
+
let changed = false;
|
|
1992
|
+
mutatePullRequests(prPath, prs => {
|
|
1993
|
+
const target = shared.findPrRecord(prs, pr, project);
|
|
1994
|
+
if (!target) return prs;
|
|
1995
|
+
changed = mutator(target) === true;
|
|
1996
|
+
return prs;
|
|
1997
|
+
});
|
|
1998
|
+
return changed;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
function markPrMissingBranch(project, pr, action) {
|
|
2002
|
+
if (pr._pendingReason === PR_PENDING_MISSING_BRANCH) return false;
|
|
2003
|
+
const changed = mutatePrPendingReason(project, pr, target => {
|
|
2004
|
+
if (target._pendingReason === PR_PENDING_MISSING_BRANCH) return false;
|
|
2005
|
+
target._pendingReason = PR_PENDING_MISSING_BRANCH;
|
|
2006
|
+
return true;
|
|
2007
|
+
});
|
|
2008
|
+
if (changed) {
|
|
2009
|
+
pr._pendingReason = PR_PENDING_MISSING_BRANCH;
|
|
2010
|
+
log('warn', `PR ${pr.id}: cannot dispatch ${action} — missing pr_branch; waiting for PR metadata enrichment`);
|
|
2011
|
+
}
|
|
2012
|
+
return changed;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
function clearPrMissingBranch(project, pr) {
|
|
2016
|
+
if (pr._pendingReason !== PR_PENDING_MISSING_BRANCH) return false;
|
|
2017
|
+
const changed = mutatePrPendingReason(project, pr, target => {
|
|
2018
|
+
if (target._pendingReason !== PR_PENDING_MISSING_BRANCH) return false;
|
|
2019
|
+
delete target._pendingReason;
|
|
2020
|
+
return true;
|
|
2021
|
+
});
|
|
2022
|
+
if (changed) delete pr._pendingReason;
|
|
2023
|
+
return changed;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
function canDispatchPrBranch(project, pr, action) {
|
|
2027
|
+
if (getPrDispatchBranch(pr)) {
|
|
2028
|
+
clearPrMissingBranch(project, pr);
|
|
2029
|
+
return true;
|
|
2030
|
+
}
|
|
2031
|
+
markPrMissingBranch(project, pr, action);
|
|
2032
|
+
return false;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1983
2035
|
/**
|
|
1984
2036
|
* Scan pull-requests.json for PRs needing review or fixes
|
|
1985
2037
|
*/
|
|
@@ -2021,6 +2073,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2021
2073
|
const prDisplayId = shared.getPrDisplayId(pr);
|
|
2022
2074
|
const prCanonicalId = shared.getCanonicalPrId(project, pr, pr.url || '');
|
|
2023
2075
|
if (activePrIds.has(prCanonicalId)) continue; // Skip PRs with active dispatch (prevent race)
|
|
2076
|
+
if (getPrDispatchBranch(pr)) clearPrMissingBranch(project, pr);
|
|
2024
2077
|
// Branch mutex: skip if PR branch is locked by any active dispatch (cross-type collision)
|
|
2025
2078
|
if (pr.branch && isBranchActive(pr.branch)) {
|
|
2026
2079
|
log('info', `Branch mutex: skipping PR ${pr.id} dispatch — branch ${pr.branch} locked by another agent`);
|
|
@@ -2061,6 +2114,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2061
2114
|
if (needsReview) {
|
|
2062
2115
|
const key = `review-${project?.name || 'default'}-${prDisplayId}`;
|
|
2063
2116
|
if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2117
|
+
if (!canDispatchPrBranch(project, pr, 'review')) continue;
|
|
2064
2118
|
|
|
2065
2119
|
// Pre-dispatch live vote check — cached reviewStatus may be stale (poll lag ~6 min)
|
|
2066
2120
|
try {
|
|
@@ -2104,6 +2158,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2104
2158
|
// Skip isAlreadyDispatched — fixedAfterReview/lastReviewedAt already dedupe; the 1hr
|
|
2105
2159
|
// completed-dispatch window would block legitimate re-reviews within the hour after a fix
|
|
2106
2160
|
if (isOnCooldown(key, cooldownMs)) continue;
|
|
2161
|
+
if (!canDispatchPrBranch(project, pr, 're-review')) continue;
|
|
2107
2162
|
|
|
2108
2163
|
// Pre-dispatch live vote check — cached 'waiting' may be stale if reviewer already acted
|
|
2109
2164
|
try {
|
|
@@ -2140,6 +2195,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2140
2195
|
if (evalLoopEnabled && reviewStatus === 'changes-requested' && !awaitingReReview && !evalEscalated) {
|
|
2141
2196
|
const key = `fix-${project?.name || 'default'}-${prDisplayId}`;
|
|
2142
2197
|
if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2198
|
+
if (!canDispatchPrBranch(project, pr, 'fix')) continue;
|
|
2143
2199
|
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
2144
2200
|
if (!agentId) continue;
|
|
2145
2201
|
|
|
@@ -2179,6 +2235,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2179
2235
|
}
|
|
2180
2236
|
continue;
|
|
2181
2237
|
}
|
|
2238
|
+
if (!canDispatchPrBranch(project, pr, 'human-feedback fix')) continue;
|
|
2182
2239
|
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
2183
2240
|
if (!agentId) continue;
|
|
2184
2241
|
|
|
@@ -2226,6 +2283,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2226
2283
|
|
|
2227
2284
|
const key = `build-fix-${project?.name || 'default'}-${prDisplayId}`;
|
|
2228
2285
|
if (fixThrottled || isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2286
|
+
if (!canDispatchPrBranch(project, pr, 'build fix')) continue;
|
|
2229
2287
|
|
|
2230
2288
|
// Pre-dispatch live build check — cached buildStatus may be stale: ADO can
|
|
2231
2289
|
// recompute the merge commit when master moves and pollPrStatus deliberately
|
|
@@ -2310,6 +2368,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2310
2368
|
const conflictFixedAt = pr._conflictFixedAt;
|
|
2311
2369
|
const withinLag = conflictFixedAt && Date.now() - new Date(conflictFixedAt).getTime() < 10 * 60 * 1000;
|
|
2312
2370
|
if (!withinLag && !fixThrottled && !isAlreadyDispatched(key) && !isOnCooldown(key, cooldownMs)) {
|
|
2371
|
+
if (!canDispatchPrBranch(project, pr, 'conflict fix')) continue;
|
|
2313
2372
|
// Pre-dispatch live conflict check — cached `_mergeConflict` may be
|
|
2314
2373
|
// stale: ADO/GitHub recompute mergeStatus asynchronously (1–5 min lag),
|
|
2315
2374
|
// so a successful upstream merge can leave the flag set even after the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1638",
|
|
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"
|