@yemi33/minions 0.1.949 → 0.1.950

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.949 (2026-04-14)
3
+ ## 0.1.950 (2026-04-14)
4
4
 
5
5
  ### Features
6
6
  - add PR/build status CLI shim and agent guidance
@@ -47,6 +47,7 @@
47
47
  - prioritize error_max_turns over permission-blocked in classifyFailure (#1001)
48
48
 
49
49
  ### Other
50
+ - refactor(ado): simplify ado-status and extract build/review status helpers
50
51
  - refactor(ado): extract stripRefsHeads helper, deduplicate refs/heads/ stripping
51
52
  - refactor(ado): simplify comments, fix declaration order, promote ado import
52
53
  - refactor(ado): move PR enrichment fetch into fetchAdoPrMetadata helper
@@ -23,7 +23,7 @@
23
23
 
24
24
  const path = require('path');
25
25
  const shared = require('./shared');
26
- const { safeJsonArr, projectPrPath, getProjects } = shared;
26
+ const { safeJson, safeJsonArr, projectPrPath, getProjects } = shared;
27
27
 
28
28
  // ── Arg parsing ──────────────────────────────────────────────────────────────
29
29
 
@@ -54,13 +54,6 @@ const prNumber = parseInt(prNumberArg, 10);
54
54
 
55
55
  // ── Helpers ──────────────────────────────────────────────────────────────────
56
56
 
57
- function getConfig() {
58
- // Avoid pulling in queries.js to keep startup fast; read config directly.
59
- const configPath = path.join(__dirname, '..', 'config.json');
60
- try { return JSON.parse(require('fs').readFileSync(configPath, 'utf8')); }
61
- catch { return {}; }
62
- }
63
-
64
57
  function findInCache(projects) {
65
58
  for (const project of projects) {
66
59
  const prs = safeJsonArr(projectPrPath(project));
@@ -88,7 +81,8 @@ function findInCache(projects) {
88
81
  // ── Main ─────────────────────────────────────────────────────────────────────
89
82
 
90
83
  async function main() {
91
- const config = getConfig();
84
+ // Use safeJson directly — pulling in queries.js would load all cached state unnecessarily
85
+ const config = safeJson(path.join(__dirname, '..', 'config.json')) || {};
92
86
  const allProjects = getProjects(config).filter(p => p.repoHost === 'ado' || !p.repoHost);
93
87
  const projects = projectName
94
88
  ? allProjects.filter(p => p.name === projectName)
@@ -103,7 +97,6 @@ async function main() {
103
97
  }
104
98
 
105
99
  if (!live) {
106
- // Fast path: read engine-maintained pull-requests.json
107
100
  const result = findInCache(projects);
108
101
  if (result) { console.log(JSON.stringify(result, null, 2)); return; }
109
102
  console.error(`PR #${prNumber} not found in cached pull-requests.json`);
@@ -111,26 +104,25 @@ async function main() {
111
104
  process.exit(1);
112
105
  }
113
106
 
114
- // Live path: call ADO API via ado.js (handles auth, retry, circuit breaking)
107
+ // Live path: query all projects in parallel, return first match
115
108
  const ado = require('./ado');
109
+ const settled = await Promise.allSettled(
110
+ projects.map(p => ado.fetchSinglePrBuildStatus(p, prNumber))
111
+ );
116
112
 
117
- // Try each project until we find the PR
118
- for (const project of projects) {
119
- try {
120
- const result = await ado.fetchSinglePrBuildStatus(project, prNumber);
121
- if (result) { console.log(JSON.stringify(result, null, 2)); return; }
122
- } catch (e) {
123
- // 404 or similar — PR not in this project, try the next one
124
- if (!e.message?.includes('404') && !e.message?.includes('not found')) {
125
- console.error(`Error querying ${project.name}: ${e.message}`);
126
- }
113
+ const found = settled.find(r => r.status === 'fulfilled' && r.value);
114
+ if (found) { console.log(JSON.stringify(found.value, null, 2)); return; }
115
+
116
+ // Log unexpected errors (not 404s — those just mean PR isn't in that project)
117
+ for (const { status, reason } of settled) {
118
+ if (status === 'rejected' && !reason?.message?.includes('404') && !reason?.message?.includes('not found')) {
119
+ console.error(`ADO query error: ${reason.message}`);
127
120
  }
128
121
  }
129
122
 
130
- // Live lookup failed fall back to cache
123
+ // Fall back to cache with a note that live lookup failed
131
124
  const cached = findInCache(projects);
132
125
  if (cached) {
133
- cached._liveFailed = true;
134
126
  cached.source = 'cached (live lookup failed — check ADO auth)';
135
127
  console.log(JSON.stringify(cached, null, 2));
136
128
  return;
package/engine/ado.js CHANGED
@@ -18,6 +18,30 @@ function engine() {
18
18
 
19
19
  const stripRefsHeads = s => (s || '').replace('refs/heads/', '');
20
20
 
21
+ // ── Build/Review Status Helpers ───────────────────────────────────────────────
22
+
23
+ /** Classify an array of ADO build records into a single status string. */
24
+ function classifyBuildStatus(prBuilds) {
25
+ if (!prBuilds.length) return 'none';
26
+ // partiallySucceeded = warnings, not failures — counts as passing
27
+ const hasFailed = prBuilds.some(b => b.result === 'failed' || b.result === 'canceled');
28
+ const allDone = prBuilds.every(b => b.status === 'completed');
29
+ const allPassed = prBuilds.every(b => b.result === 'succeeded' || b.result === 'partiallySucceeded');
30
+ const hasRunning = prBuilds.some(b => b.status === 'inProgress' || b.status === 'notStarted');
31
+ if (hasFailed && allDone) return 'failing';
32
+ if (allDone && allPassed) return 'passing';
33
+ if (hasRunning) return 'running';
34
+ return 'none';
35
+ }
36
+
37
+ /** Map ADO reviewer vote array to a review status string. */
38
+ function votesToReviewStatus(votes) {
39
+ if (votes.some(v => v === -10)) return 'changes-requested';
40
+ if (votes.some(v => v >= 5)) return 'approved';
41
+ if (votes.some(v => v === -5)) return 'waiting';
42
+ return 'pending';
43
+ }
44
+
21
45
  // ─── ADO Token Cache ─────────────────────────────────────────────────────────
22
46
 
23
47
  let _adoTokenCache = { token: null, expiresAt: 0 };
@@ -390,14 +414,8 @@ async function pollPrStatus(config) {
390
414
  const prBuilds = (buildsData?.value || []).filter(b => b.sourceVersion === mergeCommitId);
391
415
 
392
416
  if (prBuilds.length > 0) {
393
- // partiallySucceeded = warnings, not failures — counts as passing
394
- const hasFailed = prBuilds.some(b => b.result === 'failed' || b.result === 'canceled');
395
- const allDone = prBuilds.every(b => b.status === 'completed');
396
- const allPassed = prBuilds.every(b => b.result === 'succeeded' || b.result === 'partiallySucceeded');
397
- const hasRunning = prBuilds.some(b => b.status === 'inProgress' || b.status === 'notStarted');
398
-
399
- if (hasFailed && allDone) {
400
- buildStatus = 'failing';
417
+ buildStatus = classifyBuildStatus(prBuilds);
418
+ if (buildStatus === 'failing') {
401
419
  const failed = prBuilds.find(b => b.result === 'failed');
402
420
  buildFailReason = failed?.definition?.name || 'Build failed';
403
421
  // Build fake status objects for error log fetching
@@ -405,10 +423,6 @@ async function pollPrStatus(config) {
405
423
  state: 'failed', targetUrl: `${orgBase}/${project.adoProject}/_build/results?buildId=${b.id}`,
406
424
  _buildId: String(b.id),
407
425
  }));
408
- } else if (allDone && allPassed) {
409
- buildStatus = 'passing';
410
- } else if (hasRunning) {
411
- buildStatus = 'running';
412
426
  }
413
427
  }
414
428
  } catch (e) { log('warn', `ADO build query for ${pr.id}: ${e.message}`); }
@@ -758,10 +772,7 @@ async function checkLiveReviewStatus(pr, project) {
758
772
  const prData = JSON.parse(result);
759
773
  const votes = (prData.reviewers || []).map(r => r.vote).filter(v => v !== undefined);
760
774
  if (votes.length === 0) return 'pending';
761
- if (votes.some(v => v === -10)) return 'changes-requested';
762
- if (votes.some(v => v >= 5)) return 'approved';
763
- if (votes.some(v => v === -5)) return 'waiting';
764
- return 'pending';
775
+ return votesToReviewStatus(votes);
765
776
  } catch (e) {
766
777
  log('warn', `Live review check for ${pr.id}: ${e.message}`);
767
778
  return null;
@@ -794,55 +805,42 @@ async function fetchSinglePrBuildStatus(project, prNumber) {
794
805
 
795
806
  const orgBase = getAdoOrgBase(project);
796
807
  const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${project.repositoryId}`;
797
- const prData = await adoFetch(`${repoBase}/pullrequests/${prNumber}?api-version=7.1`, token);
808
+ const mergeRef = encodeURIComponent(`refs/pull/${prNumber}/merge`);
809
+ const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${project.repositoryId}&repositoryType=TfsGit&$top=25&api-version=7.1`;
810
+
811
+ // Fetch PR metadata and builds in parallel
812
+ const [prData, buildsData] = await Promise.all([
813
+ adoFetch(`${repoBase}/pullrequests/${prNumber}?api-version=7.1`, token),
814
+ adoFetch(buildsUrl, token).catch(() => null),
815
+ ]);
798
816
  if (!prData) return null;
799
817
 
800
818
  const mergeCommitId = prData.lastMergeCommit?.commitId;
801
- let buildStatus = 'none';
819
+ const prBuilds = mergeCommitId
820
+ ? (buildsData?.value || []).filter(b => b.sourceVersion === mergeCommitId)
821
+ : [];
822
+
823
+ let buildStatus = classifyBuildStatus(prBuilds);
802
824
  let buildErrorLog = null;
803
825
 
804
- if (mergeCommitId) {
826
+ if (buildStatus === 'failing') {
805
827
  try {
806
- const mergeRef = encodeURIComponent(`refs/pull/${prNumber}/merge`);
807
- const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${project.repositoryId}&repositoryType=TfsGit&$top=25&api-version=7.1`;
808
- const buildsData = await adoFetch(buildsUrl, token);
809
- const prBuilds = (buildsData?.value || []).filter(b => b.sourceVersion === mergeCommitId);
810
-
811
- if (prBuilds.length > 0) {
812
- const hasFailed = prBuilds.some(b => b.result === 'failed' || b.result === 'canceled');
813
- const allDone = prBuilds.every(b => b.status === 'completed');
814
- const allPassed = prBuilds.every(b => b.result === 'succeeded' || b.result === 'partiallySucceeded');
815
- const hasRunning = prBuilds.some(b => b.status === 'inProgress' || b.status === 'notStarted');
816
-
817
- if (hasFailed && allDone) {
818
- buildStatus = 'failing';
819
- const failedBuilds = prBuilds.filter(b => b.result === 'failed').map(b => ({
820
- state: 'failed', _buildId: String(b.id),
821
- targetUrl: `${orgBase}/${project.adoProject}/_build/results?buildId=${b.id}`,
822
- }));
823
- const logParts = [];
824
- const seenBuildIds = new Set();
825
- for (const fb of failedBuilds.slice(0, 3)) {
826
- const errorLog = await fetchAdoBuildErrorLog(orgBase, project, fb, token, { id: `PR-${prNumber}` }, seenBuildIds);
827
- if (errorLog) logParts.push(errorLog);
828
- }
829
- if (logParts.length > 0) buildErrorLog = logParts.join('\n\n');
830
- } else if (allDone && allPassed) {
831
- buildStatus = 'passing';
832
- } else if (hasRunning) {
833
- buildStatus = 'running';
834
- }
828
+ const failedBuilds = prBuilds.filter(b => b.result === 'failed').map(b => ({
829
+ state: 'failed', _buildId: String(b.id),
830
+ targetUrl: `${orgBase}/${project.adoProject}/_build/results?buildId=${b.id}`,
831
+ }));
832
+ const logParts = [];
833
+ const seenBuildIds = new Set();
834
+ const pr = { id: `PR-${prNumber}`, branch: stripRefsHeads(prData.sourceRefName) };
835
+ for (const fb of failedBuilds.slice(0, 3)) {
836
+ const errorLog = await fetchAdoBuildErrorLog(orgBase, project, fb, token, pr, seenBuildIds);
837
+ if (errorLog) logParts.push(errorLog);
835
838
  }
836
- } catch (e) { log('warn', `fetchSinglePrBuildStatus build query for PR #${prNumber}: ${e.message}`); }
839
+ if (logParts.length > 0) buildErrorLog = logParts.join('\n\n');
840
+ } catch (e) { log('warn', `fetchSinglePrBuildStatus error log for PR #${prNumber}: ${e.message}`); }
837
841
  }
838
842
 
839
843
  const votes = (prData.reviewers || []).map(r => r.vote);
840
- let reviewStatus = 'pending';
841
- if (votes.some(v => v === -10)) reviewStatus = 'changes-requested';
842
- else if (votes.some(v => v >= 5)) reviewStatus = 'approved';
843
- else if (votes.some(v => v === -5)) reviewStatus = 'waiting';
844
-
845
- // Reconstruct a human-readable PR URL from the API URL
846
844
  const prUrl = `https://dev.azure.com/${project.adoOrg}/${project.adoProject}/_git/${project.repositoryId}/pullrequest/${prNumber}`;
847
845
 
848
846
  return {
@@ -850,9 +848,9 @@ async function fetchSinglePrBuildStatus(project, prNumber) {
850
848
  title: prData.title || '',
851
849
  branch: stripRefsHeads(prData.sourceRefName),
852
850
  status: prData.status || 'unknown',
853
- reviewStatus,
851
+ reviewStatus: votesToReviewStatus(votes),
854
852
  buildStatus,
855
- buildErrorLog: buildErrorLog || undefined,
853
+ ...(buildErrorLog ? { buildErrorLog } : {}),
856
854
  mergeConflict: prData.mergeStatus === 'conflicts',
857
855
  url: prUrl,
858
856
  project: project.name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.949",
3
+ "version": "0.1.950",
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"