@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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1640 (2026-04-30)
4
+
5
+ ### Fixes
6
+ - surface unresolved pr branches
7
+
3
8
  ## 0.1.1638 (2026-04-30)
4
9
 
5
10
  ### Fixes
@@ -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="pr-branch">' + escapeHtml(pr.branch || '') + '</span>' + pendingReasonHtml + '</td>' +
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>' +
@@ -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
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-04-30T16:34:18.069Z"
4
+ "cachedAt": "2026-04-30T17:09:32.809Z"
5
5
  }
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
  }
@@ -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
- const prHeaderPattern = /\*\*PR[:\*]*\*?\s*[#-]*\s*(?:(?:visualstudio\.com|dev\.azure\.com)[^\s"]*?pullrequest\/(\d+)|github\.com\/[^\s"]*?\/pull\/(\d+))/gi;
753
- while ((match = prHeaderPattern.exec(content)) !== null) prMatches.add(match[1] || match[2]);
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 getPrDispatchBranch(pr) {
1986
- return typeof pr?.branch === 'string' ? pr.branch.trim() : '';
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 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;
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
- 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) {
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 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;
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
- markPrMissingBranch(project, pr, action);
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
- if (getPrDispatchBranch(pr)) clearPrMissingBranch(project, pr);
2119
+ const prBranchForMutex = resolvePrBranch(pr);
2077
2120
  // Branch mutex: skip if PR branch is locked by any active dispatch (cross-type collision)
2078
- if (pr.branch && isBranchActive(pr.branch)) {
2079
- log('info', `Branch mutex: skipping PR ${pr.id} dispatch — branch ${pr.branch} locked by another agent`);
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: 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: pr.branch, project: projMeta });
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: 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: pr.branch, project: projMeta });
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: 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: pr.branch, project: projMeta });
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: 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: pr.branch, project: projMeta });
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: 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: pr.branch, project: projMeta });
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: 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: pr.branch, project: projMeta });
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.1638",
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"