@yemi33/minions 0.1.1001 → 0.1.1003

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,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1001 (2026-04-15)
3
+ ## 0.1.1003 (2026-04-15)
4
4
 
5
5
  ### Features
6
6
  - fix stopAfter=0 watch expiry — run forever for all conditions (#1117)
@@ -25,6 +25,8 @@
25
25
  - gate auto-fix conflict dispatch behind autoFixConflicts flag
26
26
 
27
27
  ### Fixes
28
+ - only gate reviews/fixes when no free agent slots remain
29
+ - extend auto-link fallback to ADO projects
28
30
  - auto-link existing GitHub PR when agent completes without creating one
29
31
  - preserve ordered lists across blank lines in markdown renderer
30
32
  - restore CC rendering and tab bar
@@ -43,8 +45,6 @@
43
45
  - move review verdict check before updateWorkItemStatus(DONE)
44
46
  - address review feedback — move writeToInbox outside lock, add absolute condition auto-expire
45
47
  - fix watches feature gaps — human notifications, branch stub, status-change init, unique keys
46
- - skip isAlreadyDispatched in needsReReview to allow re-review within 1hr
47
- - skip SessionStart hook settings test on CI
48
48
 
49
49
  ### Other
50
50
  - docs: update CLAUDE.md with recent Minions architecture changes
package/engine/ado.js CHANGED
@@ -890,6 +890,29 @@ const isAdoThrottled = () => _adoThrottle.isThrottled();
890
890
  /** Returns a snapshot of the current throttle state. Calls isAdoThrottled() for a fresh value. */
891
891
  const getAdoThrottleState = () => _adoThrottle.getState();
892
892
 
893
+ /**
894
+ * Query ADO for an open PR on a specific branch.
895
+ * Used as a last-resort fallback when an agent completes without logging a PR URL
896
+ * but a PR may already exist from a prior orphaned dispatch.
897
+ * @param {object} project — project config with adoOrg, adoProject, repositoryId, prUrlBase
898
+ * @param {string} branch — source branch name (without refs/heads/ prefix)
899
+ * @returns {{ prNumber: number, url: string }|null}
900
+ */
901
+ async function findOpenPrOnBranch(project, branch) {
902
+ if (!project.adoOrg || !project.adoProject || !project.repositoryId || !branch) return null;
903
+ const token = await getAdoToken();
904
+ if (!token) return null;
905
+ const orgBase = shared.getAdoOrgBase(project);
906
+ const sourceRef = encodeURIComponent(`refs/heads/${branch}`);
907
+ const url = `${orgBase}/${project.adoProject}/_apis/git/repositories/${project.repositoryId}/pullrequests?searchCriteria.status=active&searchCriteria.sourceRefName=${sourceRef}&api-version=7.1`;
908
+ const data = await adoFetch(url, token);
909
+ const pr = (data.value || [])[0];
910
+ if (!pr) return null;
911
+ const prNumber = pr.pullRequestId;
912
+ const prUrl = project.prUrlBase ? `${project.prUrlBase}${prNumber}` : `https://dev.azure.com/${project.adoOrg}/${project.adoProject}/_git/${project.repositoryId}/pullrequest/${prNumber}`;
913
+ return { prNumber, url: prUrl };
914
+ }
915
+
893
916
  /** Reset throttle state — exported for testing only. */
894
917
  function _resetAdoThrottle() {
895
918
  _adoThrottle._reset();
@@ -913,6 +936,7 @@ module.exports = {
913
936
  getAdoThrottleState,
914
937
  fetchAdoPrMetadata,
915
938
  fetchSinglePrBuildStatus,
939
+ findOpenPrOnBranch,
916
940
  _resetAdoThrottle, // exported for testing
917
941
  _setAdoThrottleForTest, // exported for testing
918
942
  };
@@ -1792,37 +1792,44 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
1792
1792
  }
1793
1793
  }
1794
1794
  }
1795
- // Last resort: query GitHub directly for an open PR on this branch.
1795
+ // Last resort: query the platform directly for an open PR on this branch.
1796
1796
  // Handles the case where a prior orphaned dispatch created a PR but the engine
1797
- // never processed its output — so the PR exists on GitHub but not in pull-requests.json.
1797
+ // never processed its output — so the PR exists on the platform but not in pull-requests.json.
1798
1798
  if (!existingPrFound && meta?.branch) {
1799
1799
  const projectObj = shared.getProjects(config).find(p => p.name === meta?.project?.name);
1800
- const ghSlug = projectObj?.prUrlBase?.match(/github\.com\/([^/]+\/[^/]+)\/pull/)?.[1];
1801
- if (projectObj?.repoHost === 'github' && ghSlug) {
1800
+ if (projectObj) {
1802
1801
  try {
1803
- const raw = await execAsync(`gh pr list --head "${meta.branch}" --repo ${ghSlug} --json number,url,state --limit 1`, { timeout: 15000, windowsHide: true });
1804
- const found = JSON.parse(raw || '[]');
1805
- if (found.length > 0 && found[0].state === 'OPEN') {
1806
- const prNum = found[0].number;
1807
- const fullId = `PR-${prNum}`;
1802
+ let found = null;
1803
+ if (projectObj.repoHost === 'github') {
1804
+ const ghSlug = projectObj.prUrlBase?.match(/github\.com\/([^/]+\/[^/]+)\/pull/)?.[1];
1805
+ if (ghSlug) {
1806
+ const raw = await execAsync(`gh pr list --head "${meta.branch}" --repo ${ghSlug} --json number,url,state --limit 1`, { timeout: 15000, windowsHide: true });
1807
+ const hits = JSON.parse(raw || '[]');
1808
+ if (hits.length > 0 && hits[0].state === 'OPEN') found = { prNumber: hits[0].number, url: hits[0].url };
1809
+ }
1810
+ } else {
1811
+ found = await require('./ado').findOpenPrOnBranch(projectObj, meta.branch);
1812
+ }
1813
+ if (found) {
1814
+ const fullId = `PR-${found.prNumber}`;
1808
1815
  const prPath = shared.projectPrPath(projectObj);
1809
1816
  mutateJsonFileLocked(prPath, prs => {
1810
1817
  if (!Array.isArray(prs)) prs = [];
1811
1818
  if (prs.some(p => p.id === fullId)) return prs;
1812
1819
  prs.push({
1813
- id: fullId, prNumber: prNum, title: meta.item?.title || '',
1820
+ id: fullId, prNumber: found.prNumber, title: meta.item?.title || '',
1814
1821
  agent: agentId, branch: meta.branch, reviewStatus: 'pending',
1815
- status: PR_STATUS.ACTIVE, created: ts(), url: found[0].url,
1822
+ status: PR_STATUS.ACTIVE, created: ts(), url: found.url,
1816
1823
  prdItems: meta.item?.id ? [meta.item.id] : [],
1817
1824
  sourcePlan: meta.item?.sourcePlan || '', itemType: meta.item?.itemType || '',
1818
1825
  });
1819
1826
  return prs;
1820
1827
  });
1821
1828
  if (meta.item?.id) addPrLink(fullId, meta.item.id);
1822
- log('info', `Auto-linked existing GH PR ${fullId} on branch ${meta.branch} for ${meta.item?.id}`);
1829
+ log('info', `Auto-linked existing PR ${fullId} on branch ${meta.branch} for ${meta.item?.id}`);
1823
1830
  existingPrFound = true;
1824
1831
  }
1825
- } catch (e) { log('warn', `GH PR lookup for branch ${meta.branch}: ${e.message}`); }
1832
+ } catch (e) { log('warn', `PR lookup for branch ${meta.branch}: ${e.message}`); }
1826
1833
  }
1827
1834
  }
1828
1835
  if (!existingPrFound) {
package/engine.js CHANGED
@@ -3081,18 +3081,24 @@ async function discoverWork(config) {
3081
3081
  } catch (e) { log('warn', 'plan completion sweep: ' + e.message); }
3082
3082
  }
3083
3083
 
3084
- // Gate reviews and fixes: do not dispatch until all implement items are complete
3084
+ // Gate reviews and fixes: only when at max concurrency idle agents should pick up reviews
3085
+ // even if implement items are in progress (implements get priority via sort order, not by blocking)
3085
3086
  const hasIncompleteImplements = queries.getWorkItems(config).some(i =>
3086
3087
  ['queued', 'pending', 'dispatched'].includes(i.status) && (i.type || '').startsWith('implement')
3087
3088
  );
3088
3089
  if (hasIncompleteImplements) {
3089
- if (allReviews.length > 0) {
3090
- log('info', `Gating ${allReviews.length} reviews — implement items still in progress`);
3091
- allReviews = [];
3092
- }
3093
- if (allFixes.length > 0) {
3094
- log('info', `Gating ${allFixes.length} fixes — implement items still in progress`);
3095
- allFixes = [];
3090
+ const activeCount = (getDispatch().active || []).length;
3091
+ const maxConcurrent = config.engine?.maxConcurrent ?? DEFAULTS.maxConcurrent;
3092
+ const freeSlots = Math.max(0, maxConcurrent - activeCount);
3093
+ if (freeSlots === 0) {
3094
+ if (allReviews.length > 0) {
3095
+ log('info', `Gating ${allReviews.length} reviews — implement items in progress and no free slots`);
3096
+ allReviews = [];
3097
+ }
3098
+ if (allFixes.length > 0) {
3099
+ log('info', `Gating ${allFixes.length} fixes — implement items in progress and no free slots`);
3100
+ allFixes = [];
3101
+ }
3096
3102
  }
3097
3103
  }
3098
3104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1001",
3
+ "version": "0.1.1003",
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"