@yemi33/minions 0.1.1638 → 0.1.1640
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 +4 -1
- package/dashboard/styles.css +1 -0
- package/dashboard.js +22 -3
- package/engine/ado.js +9 -6
- package/engine/copilot-models.json +1 -1
- package/engine/github.js +10 -0
- package/engine/lifecycle.js +27 -4
- package/engine.js +108 -59
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,9 @@ function prRow(pr) {
|
|
|
18
18
|
const buildLabel = pr.buildFixEscalated ? 'escalated (' + (pr.buildFixAttempts || '?') + ' fixes)' : (pr.buildStatus || 'none') + (pr._buildStatusStale ? ' (stale)' : '');
|
|
19
19
|
const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
|
|
20
20
|
const statusLabel = pr.status || 'active';
|
|
21
|
+
const branchError = pr._branchResolutionError?.reason || '';
|
|
22
|
+
const branchLabel = pr.branch || (branchError ? 'missing branch' : '—');
|
|
23
|
+
const branchClass = 'pr-branch' + (branchError ? ' branch-missing' : '');
|
|
21
24
|
const url = pr.url || '#';
|
|
22
25
|
const prId = pr.id || '—';
|
|
23
26
|
const pendingReason = pr._pendingReason ? String(pr._pendingReason) : '';
|
|
@@ -28,7 +31,7 @@ function prRow(pr) {
|
|
|
28
31
|
'<td><span class="pr-id">' + escapeHtml(String(prId)) + '</span></td>' +
|
|
29
32
|
'<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>' +
|
|
30
33
|
'<td><span class="pr-agent">' + escapeHtml(pr.agent || '—') + '</span></td>' +
|
|
31
|
-
'<td><span class="
|
|
34
|
+
'<td><span class="' + branchClass + '"' + (branchError ? ' title="' + escapeHtml(branchError) + '"' : '') + '>' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
|
|
32
35
|
'<td><span class="pr-badge ' + reviewClass + '"' + (reviewTitle ? ' title="' + escapeHtml(reviewTitle) + '"' : '') + '>' + escapeHtml(reviewLabel) + '</span></td>' +
|
|
33
36
|
'<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>' +
|
|
34
37
|
'<td><span class="pr-badge ' + buildClass + '">' + escapeHtml(buildLabel) + '</span></td>' +
|
package/dashboard/styles.css
CHANGED
|
@@ -262,6 +262,7 @@
|
|
|
262
262
|
.pr-id { color: var(--muted); font-family: Consolas, monospace; font-size: var(--text-base); }
|
|
263
263
|
.pr-agent { font-size: var(--text-base); color: var(--text); }
|
|
264
264
|
.pr-branch { font-family: Consolas, monospace; font-size: var(--text-sm); color: var(--muted); background: var(--bg); padding: var(--space-1) var(--space-3); border-radius: 3px; border: 1px solid var(--border); }
|
|
265
|
+
.pr-branch.branch-missing { color: var(--red); border-color: var(--red); background: rgba(248,81,73,0.12); }
|
|
265
266
|
.pr-badge { font-size: var(--text-sm); font-weight: 600; padding: var(--space-1) var(--space-4); border-radius: var(--radius-xl); text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; }
|
|
266
267
|
.pr-badge.draft { background: rgba(139,148,158,0.15); color: var(--muted); border: 1px solid var(--border); }
|
|
267
268
|
.pr-badge.active { background: rgba(88,166,255,0.15); color: var(--blue); border: 1px solid var(--blue); }
|
package/dashboard.js
CHANGED
|
@@ -5838,9 +5838,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5838
5838
|
// /api/prd/regenerate removed — use /api/plans/approve which does diff-aware update
|
|
5839
5839
|
|
|
5840
5840
|
// Agents
|
|
5841
|
-
{ method: 'POST', path: '/api/pull-requests/link', desc: 'Manually link an external PR for tracking', params: 'url, title?, project?, autoObserve?, context?', handler: async (req, res) => {
|
|
5841
|
+
{ method: 'POST', path: '/api/pull-requests/link', desc: 'Manually link an external PR for tracking', params: 'url, title?, project?, autoObserve?, context?, workItemId?', handler: async (req, res) => {
|
|
5842
5842
|
const body = await readBody(req);
|
|
5843
|
-
const { url, title, project: projectName, autoObserve, context } = body;
|
|
5843
|
+
const { url, title, project: projectName, autoObserve, context, workItemId } = body;
|
|
5844
5844
|
if (!url) return jsonReply(res, 400, { error: 'url required' });
|
|
5845
5845
|
|
|
5846
5846
|
// Determine project
|
|
@@ -5855,6 +5855,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5855
5855
|
const prId = shared.getCanonicalPrId(targetProject, prNum, url);
|
|
5856
5856
|
const contextText = typeof context === 'string' ? context : (context == null ? '' : JSON.stringify(context));
|
|
5857
5857
|
|
|
5858
|
+
// Resolve a work-item association from either the top-level workItemId
|
|
5859
|
+
// field (preferred) or context.workItemId (legacy CC payload shape).
|
|
5860
|
+
// Without this, manually-linked PRs end up with prdItems=[] and the
|
|
5861
|
+
// Work Items page renders no PR even though _context records the ID.
|
|
5862
|
+
const linkedItemId = (typeof workItemId === 'string' && workItemId.trim())
|
|
5863
|
+
|| (context && typeof context === 'object' && typeof context.workItemId === 'string' && context.workItemId.trim())
|
|
5864
|
+
|| '';
|
|
5865
|
+
|
|
5858
5866
|
// Atomic check-and-insert to prevent duplicates and races with polling loops
|
|
5859
5867
|
let duplicate = false;
|
|
5860
5868
|
mutateJsonFileLocked(prPath, (prs) => {
|
|
@@ -5871,7 +5879,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5871
5879
|
status: 'active',
|
|
5872
5880
|
created: new Date().toISOString(),
|
|
5873
5881
|
url,
|
|
5874
|
-
prdItems: [],
|
|
5882
|
+
prdItems: linkedItemId ? [linkedItemId] : [],
|
|
5875
5883
|
_manual: true,
|
|
5876
5884
|
_contextOnly: !autoObserve,
|
|
5877
5885
|
_autoObserve: !!autoObserve,
|
|
@@ -5880,6 +5888,16 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5880
5888
|
return prs;
|
|
5881
5889
|
}, { defaultValue: [] });
|
|
5882
5890
|
if (duplicate) return jsonReply(res, 400, { error: 'PR already tracked' });
|
|
5891
|
+
// Persist the work-item ↔ PR association in pr-links.json so
|
|
5892
|
+
// queries.getWorkItems() can render item._pr / item._prUrl. addPrLink
|
|
5893
|
+
// is idempotent and handles the central / project-scoped split.
|
|
5894
|
+
if (linkedItemId) {
|
|
5895
|
+
try {
|
|
5896
|
+
shared.addPrLink(prId, linkedItemId, { project: targetProject, prNumber: parseInt(prNum, 10) || null, url });
|
|
5897
|
+
} catch (e) {
|
|
5898
|
+
shared.log('warn', `PR link addPrLink failed for ${prId} → ${linkedItemId}: ${e.message}`);
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5883
5901
|
invalidateStatusCache();
|
|
5884
5902
|
jsonReply(res, 200, { ok: true, id: prId });
|
|
5885
5903
|
|
|
@@ -5909,6 +5927,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5909
5927
|
if (prData.description) pr.description = prData.description.slice(0, 500);
|
|
5910
5928
|
if (!pr.branch && prData.branch) {
|
|
5911
5929
|
pr.branch = prData.branch;
|
|
5930
|
+
if (pr._branchResolutionError) delete pr._branchResolutionError;
|
|
5912
5931
|
if (pr._pendingReason === 'missing_pr_branch') delete pr._pendingReason;
|
|
5913
5932
|
}
|
|
5914
5933
|
if (pr.agent === 'human' && prData.author) pr.agent = prData.author;
|
package/engine/ado.js
CHANGED
|
@@ -357,6 +357,14 @@ async function pollPrStatus(config) {
|
|
|
357
357
|
|
|
358
358
|
const prData = await adoFetch(`${repoBase}?api-version=7.1`, token);
|
|
359
359
|
|
|
360
|
+
const sourceBranch = stripRefsHeads(prData.sourceRefName);
|
|
361
|
+
if (sourceBranch && pr.branch !== sourceBranch) {
|
|
362
|
+
pr.branch = sourceBranch;
|
|
363
|
+
if (pr._branchResolutionError) delete pr._branchResolutionError;
|
|
364
|
+
if (pr._pendingReason === 'missing_pr_branch') delete pr._pendingReason;
|
|
365
|
+
updated = true;
|
|
366
|
+
}
|
|
367
|
+
|
|
360
368
|
let newStatus = pr.status;
|
|
361
369
|
if (prData.status === 'completed') newStatus = PR_STATUS.MERGED;
|
|
362
370
|
else if (prData.status === 'abandoned') newStatus = PR_STATUS.ABANDONED;
|
|
@@ -405,12 +413,6 @@ async function pollPrStatus(config) {
|
|
|
405
413
|
pr._adoSourceCommit = sourceCommit;
|
|
406
414
|
updated = true;
|
|
407
415
|
}
|
|
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
|
-
}
|
|
414
416
|
|
|
415
417
|
const reviewers = prData.reviewers || [];
|
|
416
418
|
const votes = reviewers.map(r => r.vote).filter(v => v !== undefined);
|
|
@@ -795,6 +797,7 @@ async function reconcilePrs(config) {
|
|
|
795
797
|
}
|
|
796
798
|
if (existing && !existing.branch && branch) {
|
|
797
799
|
existing.branch = branch;
|
|
800
|
+
if (existing._branchResolutionError) delete existing._branchResolutionError;
|
|
798
801
|
if (existing._pendingReason === 'missing_pr_branch') delete existing._pendingReason;
|
|
799
802
|
metadataUpdated++;
|
|
800
803
|
}
|
package/engine/github.js
CHANGED
|
@@ -284,6 +284,7 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
284
284
|
if (pr.agent === 'human' && prData.user?.login) pr.agent = prData.user.login;
|
|
285
285
|
if (!pr.branch && prData.head?.ref) {
|
|
286
286
|
pr.branch = prData.head.ref;
|
|
287
|
+
if (pr._branchResolutionError) delete pr._branchResolutionError;
|
|
287
288
|
if (pr._pendingReason === 'missing_pr_branch') delete pr._pendingReason;
|
|
288
289
|
}
|
|
289
290
|
}
|
|
@@ -327,6 +328,14 @@ async function pollPrStatus(config) {
|
|
|
327
328
|
|
|
328
329
|
let updated = false;
|
|
329
330
|
|
|
331
|
+
const headBranch = prData.head?.ref ? String(prData.head.ref).trim() : '';
|
|
332
|
+
if (headBranch && pr.branch !== headBranch) {
|
|
333
|
+
pr.branch = headBranch;
|
|
334
|
+
if (pr._branchResolutionError) delete pr._branchResolutionError;
|
|
335
|
+
if (pr._pendingReason === 'missing_pr_branch') delete pr._pendingReason;
|
|
336
|
+
updated = true;
|
|
337
|
+
}
|
|
338
|
+
|
|
330
339
|
// Map GitHub PR state to minions status
|
|
331
340
|
let newStatus = pr.status;
|
|
332
341
|
if (prData.merged) newStatus = PR_STATUS.MERGED;
|
|
@@ -703,6 +712,7 @@ async function reconcilePrs(config) {
|
|
|
703
712
|
}
|
|
704
713
|
if (existing && !existing.branch && branch) {
|
|
705
714
|
existing.branch = branch;
|
|
715
|
+
if (existing._branchResolutionError) delete existing._branchResolutionError;
|
|
706
716
|
if (existing._pendingReason === 'missing_pr_branch') delete existing._pendingReason;
|
|
707
717
|
metadataUpdated++;
|
|
708
718
|
}
|
package/engine/lifecycle.js
CHANGED
|
@@ -744,13 +744,33 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
744
744
|
}
|
|
745
745
|
} catch {}
|
|
746
746
|
|
|
747
|
+
// prId → URL captured from inbox notes. Populated alongside prMatches so
|
|
748
|
+
// extractPrUrl below has a fallback when the agent's stdout doesn't contain
|
|
749
|
+
// the URL (the W-moljyu60wuzr / #1902 case — gh pr create ran in a sibling
|
|
750
|
+
// dispatch and only the inbox note carries the link).
|
|
751
|
+
const inboxUrls = new Map();
|
|
747
752
|
const today = dateStamp();
|
|
748
753
|
const inboxFiles = getInboxFiles().filter(f => f.includes(agentId) && f.includes(today));
|
|
749
754
|
for (const f of inboxFiles) {
|
|
750
755
|
const content = safeRead(path.join(INBOX_DIR, f));
|
|
751
756
|
if (!content) continue;
|
|
752
|
-
|
|
753
|
-
|
|
757
|
+
// Match a PR declaration line in the agent's findings note: optional bold,
|
|
758
|
+
// optional "Pull Request" spelling, line-anchored so "see PR https://..."
|
|
759
|
+
// mid-paragraph mentions don't trigger a false-positive. The protocol
|
|
760
|
+
// and host prefix is optional so "PR: https://github.com/..." ,
|
|
761
|
+
// "**PR:** github.com/...", etc. all match.
|
|
762
|
+
const prHeaderPattern = /(?:^|\n)\s*\*{0,2}(?:PR|Pull\s+Request)[:\*]*\*?\s*[#-]*\s*(?:https?:\/\/)?[^\s"]*?(?:(?:visualstudio\.com|dev\.azure\.com)[^\s"]*?pullrequest\/(\d+)|github\.com\/[^\s"]*?\/pull\/(\d+))/gi;
|
|
763
|
+
while ((match = prHeaderPattern.exec(content)) !== null) {
|
|
764
|
+
const prId = match[1] || match[2];
|
|
765
|
+
prMatches.add(prId);
|
|
766
|
+
// Pull the URL substring out of the matched chunk so we can hand it to
|
|
767
|
+
// extractPrUrl as a fallback. Prefer the first inbox URL we see for a
|
|
768
|
+
// given prId — later notes don't override the canonical record.
|
|
769
|
+
if (!inboxUrls.has(prId)) {
|
|
770
|
+
const urlMatch = match[0].match(/https?:\/\/[^\s"\\)]+/);
|
|
771
|
+
if (urlMatch) inboxUrls.set(prId, urlMatch[0].replace(/[.,;:]+$/, ''));
|
|
772
|
+
}
|
|
773
|
+
}
|
|
754
774
|
}
|
|
755
775
|
|
|
756
776
|
if (prMatches.size === 0) return 0;
|
|
@@ -773,7 +793,10 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
773
793
|
return defaultProject;
|
|
774
794
|
}
|
|
775
795
|
|
|
776
|
-
// Extract PR URL directly from agent output — no manual construction
|
|
796
|
+
// Extract PR URL directly from agent output — no manual construction.
|
|
797
|
+
// Falls back to the URL captured from the inbox note when the agent stdout
|
|
798
|
+
// doesn't contain the link (gh pr create may have run in a sibling dispatch
|
|
799
|
+
// whose stdout was rotated; the inbox note is the durable artifact).
|
|
777
800
|
function extractPrUrl(prId) {
|
|
778
801
|
// Stop at backslash in addition to whitespace/quotes — raw JSONL encodes newlines as \n (literal
|
|
779
802
|
// backslash-n), so without this the regex would capture e.g. "pull/1804\n/usr/bin/bash".
|
|
@@ -781,7 +804,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
781
804
|
if (ghMatch) return ghMatch[0].replace(/[.,;:]+$/, '');
|
|
782
805
|
const adoMatch = output.match(new RegExp(`https?://(?:dev\\.azure\\.com|[^/]+\\.visualstudio\\.com)[^\\s"'\\)\\]\\\\]*?pullrequest/${prId}(?:[^\\s"'\\)\\]\\\\]*)`, 'i'));
|
|
783
806
|
if (adoMatch) return adoMatch[0].replace(/[.,;:]+$/, '');
|
|
784
|
-
return '';
|
|
807
|
+
return inboxUrls.get(prId) || '';
|
|
785
808
|
}
|
|
786
809
|
|
|
787
810
|
const agentName = config.agents?.[agentId]?.name || agentId;
|
package/engine.js
CHANGED
|
@@ -1982,56 +1982,99 @@ function clearPendingHumanFeedbackFlag(projectMeta, prId) {
|
|
|
1982
1982
|
|
|
1983
1983
|
const PR_PENDING_MISSING_BRANCH = 'missing_pr_branch';
|
|
1984
1984
|
|
|
1985
|
-
function
|
|
1986
|
-
|
|
1985
|
+
function normalizePrBranch(value) {
|
|
1986
|
+
const raw = value == null ? '' : String(value).trim();
|
|
1987
|
+
if (!raw) return '';
|
|
1988
|
+
return raw.replace(/^refs\/heads\//i, '');
|
|
1987
1989
|
}
|
|
1988
1990
|
|
|
1989
|
-
function
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1991
|
+
function resolvePrBranch(pr) {
|
|
1992
|
+
if (!pr || typeof pr !== 'object') return '';
|
|
1993
|
+
const candidates = [
|
|
1994
|
+
pr.branch,
|
|
1995
|
+
pr.pr_branch,
|
|
1996
|
+
pr.prBranch,
|
|
1997
|
+
pr.sourceRefName,
|
|
1998
|
+
pr.sourceBranch,
|
|
1999
|
+
pr.sourceRef,
|
|
2000
|
+
pr.headRefName,
|
|
2001
|
+
pr.head?.ref,
|
|
2002
|
+
];
|
|
2003
|
+
for (const candidate of candidates) {
|
|
2004
|
+
const branch = normalizePrBranch(candidate);
|
|
2005
|
+
if (branch) return branch;
|
|
2006
|
+
}
|
|
2007
|
+
return '';
|
|
1999
2008
|
}
|
|
2000
2009
|
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2010
|
+
// Unified branch-resolution state writer: when branch is found, persist it and
|
|
2011
|
+
// clear BOTH the structured _branchResolutionError (red-badge UI) and the
|
|
2012
|
+
// _pendingReason marker (dashboard pending-reason vocabulary). When branch is
|
|
2013
|
+
// missing, set BOTH fields so the dashboard can surface the gate via either
|
|
2014
|
+
// rendering path.
|
|
2015
|
+
function updatePrBranchResolutionState(project, pr, { branch = '', reason = '' } = {}) {
|
|
2016
|
+
let changed = false;
|
|
2017
|
+
try {
|
|
2018
|
+
mutatePullRequests(projectPrPath(project), prs => {
|
|
2019
|
+
const target = shared.findPrRecord(prs, pr, project);
|
|
2020
|
+
if (!target) return;
|
|
2021
|
+
if (branch) {
|
|
2022
|
+
if (target.branch !== branch) {
|
|
2023
|
+
target.branch = branch;
|
|
2024
|
+
changed = true;
|
|
2025
|
+
}
|
|
2026
|
+
if (target._branchResolutionError) {
|
|
2027
|
+
delete target._branchResolutionError;
|
|
2028
|
+
changed = true;
|
|
2029
|
+
}
|
|
2030
|
+
if (target._pendingReason === PR_PENDING_MISSING_BRANCH) {
|
|
2031
|
+
delete target._pendingReason;
|
|
2032
|
+
changed = true;
|
|
2033
|
+
}
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
if (reason) {
|
|
2037
|
+
const currentReason = target._branchResolutionError?.reason || '';
|
|
2038
|
+
if (currentReason !== reason) {
|
|
2039
|
+
target._branchResolutionError = { reason, at: ts() };
|
|
2040
|
+
changed = true;
|
|
2041
|
+
}
|
|
2042
|
+
if (target._pendingReason !== PR_PENDING_MISSING_BRANCH) {
|
|
2043
|
+
target._pendingReason = PR_PENDING_MISSING_BRANCH;
|
|
2044
|
+
changed = true;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
} catch (e) {
|
|
2049
|
+
log('warn', `mark PR branch resolution state for ${pr?.id || 'unknown PR'}: ${e.message}`);
|
|
2050
|
+
}
|
|
2051
|
+
if (branch) {
|
|
2052
|
+
pr.branch = branch;
|
|
2053
|
+
if (pr._branchResolutionError) delete pr._branchResolutionError;
|
|
2054
|
+
if (pr._pendingReason === PR_PENDING_MISSING_BRANCH) delete pr._pendingReason;
|
|
2055
|
+
} else if (reason && changed) {
|
|
2056
|
+
pr._branchResolutionError = { reason, at: ts() };
|
|
2009
2057
|
pr._pendingReason = PR_PENDING_MISSING_BRANCH;
|
|
2010
|
-
log('warn', `PR ${pr.id}: cannot dispatch ${action} — missing pr_branch; waiting for PR metadata enrichment`);
|
|
2011
2058
|
}
|
|
2012
2059
|
return changed;
|
|
2013
2060
|
}
|
|
2014
2061
|
|
|
2015
|
-
function
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
if (
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
function canDispatchPrBranch(project, pr, action) {
|
|
2027
|
-
if (getPrDispatchBranch(pr)) {
|
|
2028
|
-
clearPrMissingBranch(project, pr);
|
|
2029
|
-
return true;
|
|
2062
|
+
function ensurePrBranchForDispatch(project, pr, automationType) {
|
|
2063
|
+
const branch = resolvePrBranch(pr);
|
|
2064
|
+
if (branch) {
|
|
2065
|
+
if (pr.branch !== branch || pr._branchResolutionError || pr._pendingReason === PR_PENDING_MISSING_BRANCH) {
|
|
2066
|
+
updatePrBranchResolutionState(project, pr, { branch });
|
|
2067
|
+
}
|
|
2068
|
+
return branch;
|
|
2069
|
+
}
|
|
2070
|
+
const reason = `Cannot dispatch ${automationType} for ${shared.getPrDisplayId(pr)}: missing pr_branch/source branch metadata. Link or refresh the PR so the source branch is known.`;
|
|
2071
|
+
if (updatePrBranchResolutionState(project, pr, { reason })) {
|
|
2072
|
+
log('warn', `PR ${pr.id}: ${reason}`);
|
|
2030
2073
|
}
|
|
2031
|
-
|
|
2032
|
-
return false;
|
|
2074
|
+
return '';
|
|
2033
2075
|
}
|
|
2034
2076
|
|
|
2077
|
+
|
|
2035
2078
|
/**
|
|
2036
2079
|
* Scan pull-requests.json for PRs needing review or fixes
|
|
2037
2080
|
*/
|
|
@@ -2073,10 +2116,10 @@ async function discoverFromPrs(config, project) {
|
|
|
2073
2116
|
const prDisplayId = shared.getPrDisplayId(pr);
|
|
2074
2117
|
const prCanonicalId = shared.getCanonicalPrId(project, pr, pr.url || '');
|
|
2075
2118
|
if (activePrIds.has(prCanonicalId)) continue; // Skip PRs with active dispatch (prevent race)
|
|
2076
|
-
|
|
2119
|
+
const prBranchForMutex = resolvePrBranch(pr);
|
|
2077
2120
|
// Branch mutex: skip if PR branch is locked by any active dispatch (cross-type collision)
|
|
2078
|
-
if (
|
|
2079
|
-
log('info', `Branch mutex: skipping PR ${pr.id} dispatch — branch ${
|
|
2121
|
+
if (prBranchForMutex && isBranchActive(prBranchForMutex)) {
|
|
2122
|
+
log('info', `Branch mutex: skipping PR ${pr.id} dispatch — branch ${prBranchForMutex} locked by another agent`);
|
|
2080
2123
|
continue;
|
|
2081
2124
|
}
|
|
2082
2125
|
// Skip human-authored PRs not linked to any work item — only auto-manage agent PRs
|
|
@@ -2114,7 +2157,6 @@ async function discoverFromPrs(config, project) {
|
|
|
2114
2157
|
if (needsReview) {
|
|
2115
2158
|
const key = `review-${project?.name || 'default'}-${prDisplayId}`;
|
|
2116
2159
|
if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2117
|
-
if (!canDispatchPrBranch(project, pr, 'review')) continue;
|
|
2118
2160
|
|
|
2119
2161
|
// Pre-dispatch live vote check — cached reviewStatus may be stale (poll lag ~6 min)
|
|
2120
2162
|
try {
|
|
@@ -2139,11 +2181,13 @@ async function discoverFromPrs(config, project) {
|
|
|
2139
2181
|
|
|
2140
2182
|
const agentId = resolveAgent('review', config);
|
|
2141
2183
|
if (!agentId) continue;
|
|
2184
|
+
const prBranch = ensurePrBranchForDispatch(project, pr, 'review');
|
|
2185
|
+
if (!prBranch) continue;
|
|
2142
2186
|
|
|
2143
2187
|
const item = buildPrDispatch(agentId, config, project, pr, 'review', {
|
|
2144
|
-
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch:
|
|
2188
|
+
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: prBranch,
|
|
2145
2189
|
pr_author: pr.agent || '', pr_url: pr.url || '',
|
|
2146
|
-
}, `Review ${pr.id}: ${pr.title}`, { dispatchKey: key, source: 'pr', pr, branch:
|
|
2190
|
+
}, `Review ${pr.id}: ${pr.title}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2147
2191
|
if (item) { newWork.push(item); }
|
|
2148
2192
|
}
|
|
2149
2193
|
|
|
@@ -2158,7 +2202,6 @@ async function discoverFromPrs(config, project) {
|
|
|
2158
2202
|
// Skip isAlreadyDispatched — fixedAfterReview/lastReviewedAt already dedupe; the 1hr
|
|
2159
2203
|
// completed-dispatch window would block legitimate re-reviews within the hour after a fix
|
|
2160
2204
|
if (isOnCooldown(key, cooldownMs)) continue;
|
|
2161
|
-
if (!canDispatchPrBranch(project, pr, 're-review')) continue;
|
|
2162
2205
|
|
|
2163
2206
|
// Pre-dispatch live vote check — cached 'waiting' may be stale if reviewer already acted
|
|
2164
2207
|
try {
|
|
@@ -2181,11 +2224,13 @@ async function discoverFromPrs(config, project) {
|
|
|
2181
2224
|
|
|
2182
2225
|
const agentId = resolveAgent('review', config);
|
|
2183
2226
|
if (!agentId) continue;
|
|
2227
|
+
const prBranch = ensurePrBranchForDispatch(project, pr, 're-review');
|
|
2228
|
+
if (!prBranch) continue;
|
|
2184
2229
|
|
|
2185
2230
|
const item = buildPrDispatch(agentId, config, project, pr, 'review', {
|
|
2186
|
-
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch:
|
|
2231
|
+
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: prBranch,
|
|
2187
2232
|
pr_author: pr.agent || '', pr_url: pr.url || '',
|
|
2188
|
-
}, `Review ${pr.id}: ${pr.title}`, { dispatchKey: key, source: 'pr', pr, branch:
|
|
2233
|
+
}, `Review ${pr.id}: ${pr.title}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2189
2234
|
if (item) { newWork.push(item); }
|
|
2190
2235
|
}
|
|
2191
2236
|
|
|
@@ -2195,14 +2240,15 @@ async function discoverFromPrs(config, project) {
|
|
|
2195
2240
|
if (evalLoopEnabled && reviewStatus === 'changes-requested' && !awaitingReReview && !evalEscalated) {
|
|
2196
2241
|
const key = `fix-${project?.name || 'default'}-${prDisplayId}`;
|
|
2197
2242
|
if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2198
|
-
if (!canDispatchPrBranch(project, pr, 'fix')) continue;
|
|
2199
2243
|
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
2200
2244
|
if (!agentId) continue;
|
|
2245
|
+
const prBranch = ensurePrBranchForDispatch(project, pr, 'fix');
|
|
2246
|
+
if (!prBranch) continue;
|
|
2201
2247
|
|
|
2202
2248
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2203
|
-
pr_id: pr.id, pr_branch:
|
|
2249
|
+
pr_id: pr.id, pr_branch: prBranch,
|
|
2204
2250
|
review_note: pr.minionsReview?.note || pr.reviewNote || 'See PR thread comments',
|
|
2205
|
-
}, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, { dispatchKey: key, source: 'pr', pr, branch:
|
|
2251
|
+
}, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2206
2252
|
if (item) {
|
|
2207
2253
|
newWork.push(item); setCooldown(key); fixDispatched = true;
|
|
2208
2254
|
// Increment review→fix cycle counter
|
|
@@ -2235,9 +2281,10 @@ async function discoverFromPrs(config, project) {
|
|
|
2235
2281
|
}
|
|
2236
2282
|
continue;
|
|
2237
2283
|
}
|
|
2238
|
-
if (!canDispatchPrBranch(project, pr, 'human-feedback fix')) continue;
|
|
2239
2284
|
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
2240
2285
|
if (!agentId) continue;
|
|
2286
|
+
const prBranch = ensurePrBranchForDispatch(project, pr, 'human-feedback fix');
|
|
2287
|
+
if (!prBranch) continue;
|
|
2241
2288
|
|
|
2242
2289
|
const coalesced = [...staleCoalesced, ...getCoalescedContexts(key)];
|
|
2243
2290
|
let reviewNote = pr.humanFeedback.feedbackContent || 'See PR thread comments';
|
|
@@ -2247,10 +2294,10 @@ async function discoverFromPrs(config, project) {
|
|
|
2247
2294
|
}
|
|
2248
2295
|
|
|
2249
2296
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2250
|
-
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch:
|
|
2297
|
+
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: prBranch,
|
|
2251
2298
|
reviewer: 'Human Reviewer',
|
|
2252
2299
|
review_note: reviewNote,
|
|
2253
|
-
}, `Fix ${pr.id}: ${pr.title || ''} — human feedback`, { dispatchKey: key, source: 'pr-human-feedback', pr, branch:
|
|
2300
|
+
}, `Fix ${pr.id}: ${pr.title || ''} — human feedback`, { dispatchKey: key, source: 'pr-human-feedback', pr, branch: prBranch, project: projMeta });
|
|
2254
2301
|
if (item) { newWork.push(item); fixDispatched = true; }
|
|
2255
2302
|
}
|
|
2256
2303
|
|
|
@@ -2283,7 +2330,6 @@ async function discoverFromPrs(config, project) {
|
|
|
2283
2330
|
|
|
2284
2331
|
const key = `build-fix-${project?.name || 'default'}-${prDisplayId}`;
|
|
2285
2332
|
if (fixThrottled || isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2286
|
-
if (!canDispatchPrBranch(project, pr, 'build fix')) continue;
|
|
2287
2333
|
|
|
2288
2334
|
// Pre-dispatch live build check — cached buildStatus may be stale: ADO can
|
|
2289
2335
|
// recompute the merge commit when master moves and pollPrStatus deliberately
|
|
@@ -2319,6 +2365,8 @@ async function discoverFromPrs(config, project) {
|
|
|
2319
2365
|
|
|
2320
2366
|
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
2321
2367
|
if (!agentId) continue;
|
|
2368
|
+
const prBranch = ensurePrBranchForDispatch(project, pr, 'build-fix');
|
|
2369
|
+
if (!prBranch) continue;
|
|
2322
2370
|
|
|
2323
2371
|
let reviewNote = `Build is failing: ${pr.buildFailReason || 'Check CI pipeline for details'}. Fix the build errors and push.`;
|
|
2324
2372
|
if (pr.buildErrorLog) {
|
|
@@ -2326,9 +2374,9 @@ async function discoverFromPrs(config, project) {
|
|
|
2326
2374
|
}
|
|
2327
2375
|
|
|
2328
2376
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2329
|
-
pr_id: pr.id, pr_branch:
|
|
2377
|
+
pr_id: pr.id, pr_branch: prBranch,
|
|
2330
2378
|
review_note: reviewNote,
|
|
2331
|
-
}, `Fix build failure on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch:
|
|
2379
|
+
}, `Fix build failure on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2332
2380
|
if (item) {
|
|
2333
2381
|
newWork.push(item); setCooldown(key); fixDispatched = true;
|
|
2334
2382
|
// Increment build fix attempts counter
|
|
@@ -2368,7 +2416,6 @@ async function discoverFromPrs(config, project) {
|
|
|
2368
2416
|
const conflictFixedAt = pr._conflictFixedAt;
|
|
2369
2417
|
const withinLag = conflictFixedAt && Date.now() - new Date(conflictFixedAt).getTime() < 10 * 60 * 1000;
|
|
2370
2418
|
if (!withinLag && !fixThrottled && !isAlreadyDispatched(key) && !isOnCooldown(key, cooldownMs)) {
|
|
2371
|
-
if (!canDispatchPrBranch(project, pr, 'conflict fix')) continue;
|
|
2372
2419
|
// Pre-dispatch live conflict check — cached `_mergeConflict` may be
|
|
2373
2420
|
// stale: ADO/GitHub recompute mergeStatus asynchronously (1–5 min lag),
|
|
2374
2421
|
// so a successful upstream merge can leave the flag set even after the
|
|
@@ -2395,10 +2442,12 @@ async function discoverFromPrs(config, project) {
|
|
|
2395
2442
|
if (!liveSkip) {
|
|
2396
2443
|
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
2397
2444
|
if (agentId) {
|
|
2445
|
+
const prBranch = ensurePrBranchForDispatch(project, pr, 'conflict-fix');
|
|
2446
|
+
if (!prBranch) continue;
|
|
2398
2447
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2399
|
-
pr_id: pr.id, pr_branch:
|
|
2448
|
+
pr_id: pr.id, pr_branch: prBranch,
|
|
2400
2449
|
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`,
|
|
2401
|
-
}, `Fix merge conflicts on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch:
|
|
2450
|
+
}, `Fix merge conflicts on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2402
2451
|
if (item) {
|
|
2403
2452
|
newWork.push(item);
|
|
2404
2453
|
setCooldown(key);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1640",
|
|
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"
|