@yemi33/minions 0.1.1636 → 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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1638 (2026-04-30)
4
+
5
+ ### Fixes
6
+ - Playbook 'fix' / 'review' gates items forever when pr_branch is unresolved (closes #1899) (#1901)
7
+
8
+ ## 0.1.1637 (2026-04-30)
9
+
10
+ ### Other
11
+ - test(pipeline): add unit tests for evaluateCondition, resolveTemplate, isStageComplete, _findMeetingsInRun (#1902)
12
+
3
13
  ## 0.1.1636 (2026-04-30)
4
14
 
5
15
  ### Other
@@ -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></td>' +
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: 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) 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) {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-04-30T14:01:57.688Z"
4
+ "cachedAt": "2026-04-30T16:34:18.069Z"
5
5
  }
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) 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.1636",
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"