@yemi33/minions 0.1.948 → 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,8 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.948 (2026-04-14)
3
+ ## 0.1.950 (2026-04-14)
4
4
 
5
5
  ### Features
6
+ - add PR/build status CLI shim and agent guidance
6
7
  - make ADO poll frequency configurable and ungate reconcilePrs
7
8
  - update render-work-items.js output viewer to use renderAgentOutput
8
9
  - update detail-panel.js Output Log tab to use renderAgentOutput
@@ -22,7 +23,6 @@
22
23
  - fix stall recovery nested lock violation (#965)
23
24
  - fix pending-rebases.json race condition with file locking (#964)
24
25
  - add missing resolveWorkItemPath import in engine.js (#963)
25
- - bump plan-to-prd max turns from 20 to 35
26
26
 
27
27
  ### Fixes
28
28
  - tighten build query guard to require mergeCommitId
@@ -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
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * engine/ado-status.js — CLI shim for querying PR and build status.
4
+ *
5
+ * Agents steered to "check on the builds" or "is CI green for PR #123" should
6
+ * use this instead of raw curl + azureauth calls. All ADO auth and retry logic
7
+ * is handled by ado.js internally.
8
+ *
9
+ * Usage:
10
+ * node engine/ado-status.js <prNumber>
11
+ * node engine/ado-status.js <prNumber> --live
12
+ * node engine/ado-status.js <prNumber> --project MyProject
13
+ * node engine/ado-status.js <prNumber> --live --project MyProject
14
+ *
15
+ * Output: JSON to stdout. Exit 0 on success, 1 on error/not-found.
16
+ *
17
+ * Cached (default): reads pull-requests.json maintained by the engine (~3 min stale).
18
+ * Live (--live): makes fresh ADO API calls — use when engine isn't running or you
19
+ * just pushed and need up-to-the-moment build status.
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const path = require('path');
25
+ const shared = require('./shared');
26
+ const { safeJson, safeJsonArr, projectPrPath, getProjects } = shared;
27
+
28
+ // ── Arg parsing ──────────────────────────────────────────────────────────────
29
+
30
+ const args = process.argv.slice(2);
31
+ const prNumberArg = args.find(a => /^\d+$/.test(a));
32
+ const live = args.includes('--live');
33
+ const projectIdx = args.indexOf('--project');
34
+ const projectName = projectIdx >= 0 ? args[projectIdx + 1] : null;
35
+
36
+ if (!prNumberArg) {
37
+ console.error([
38
+ 'Usage: node engine/ado-status.js <prNumber> [--live] [--project <name>]',
39
+ '',
40
+ ' <prNumber> PR number to look up (required)',
41
+ ' --live Make a fresh ADO API call instead of reading cached state',
42
+ ' --project <name> Scope search to one project (optional)',
43
+ '',
44
+ 'Output: JSON with fields: prNumber, title, branch, status, reviewStatus,',
45
+ ' buildStatus, buildErrorLog (if failing), mergeConflict, url, project, source',
46
+ '',
47
+ 'buildStatus values: passing | failing | running | none',
48
+ 'reviewStatus values: approved | changes-requested | waiting | pending',
49
+ ].join('\n'));
50
+ process.exit(1);
51
+ }
52
+
53
+ const prNumber = parseInt(prNumberArg, 10);
54
+
55
+ // ── Helpers ──────────────────────────────────────────────────────────────────
56
+
57
+ function findInCache(projects) {
58
+ for (const project of projects) {
59
+ const prs = safeJsonArr(projectPrPath(project));
60
+ const pr = prs.find(p => p.prNumber === prNumber || p.id === `PR-${prNumber}`);
61
+ if (!pr) continue;
62
+ const out = {
63
+ prNumber: pr.prNumber || prNumber,
64
+ id: pr.id,
65
+ title: pr.title || '',
66
+ branch: pr.branch || '',
67
+ status: pr.status || 'unknown',
68
+ reviewStatus: pr.reviewStatus || 'pending',
69
+ buildStatus: pr.buildStatus || 'none',
70
+ url: pr.url || '',
71
+ project: project.name,
72
+ source: 'cached',
73
+ };
74
+ if (pr.buildErrorLog) out.buildErrorLog = pr.buildErrorLog;
75
+ if (pr._mergeConflict) out.mergeConflict = true;
76
+ return out;
77
+ }
78
+ return null;
79
+ }
80
+
81
+ // ── Main ─────────────────────────────────────────────────────────────────────
82
+
83
+ async function main() {
84
+ // Use safeJson directly — pulling in queries.js would load all cached state unnecessarily
85
+ const config = safeJson(path.join(__dirname, '..', 'config.json')) || {};
86
+ const allProjects = getProjects(config).filter(p => p.repoHost === 'ado' || !p.repoHost);
87
+ const projects = projectName
88
+ ? allProjects.filter(p => p.name === projectName)
89
+ : allProjects;
90
+
91
+ if (projects.length === 0) {
92
+ const msg = projectName
93
+ ? `Project "${projectName}" not found in config.json`
94
+ : 'No ADO projects configured in config.json';
95
+ console.error(msg);
96
+ process.exit(1);
97
+ }
98
+
99
+ if (!live) {
100
+ const result = findInCache(projects);
101
+ if (result) { console.log(JSON.stringify(result, null, 2)); return; }
102
+ console.error(`PR #${prNumber} not found in cached pull-requests.json`);
103
+ console.error('Tip: try --live for a fresh ADO API call, or check the PR number is correct.');
104
+ process.exit(1);
105
+ }
106
+
107
+ // Live path: query all projects in parallel, return first match
108
+ const ado = require('./ado');
109
+ const settled = await Promise.allSettled(
110
+ projects.map(p => ado.fetchSinglePrBuildStatus(p, prNumber))
111
+ );
112
+
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}`);
120
+ }
121
+ }
122
+
123
+ // Fall back to cache with a note that live lookup failed
124
+ const cached = findInCache(projects);
125
+ if (cached) {
126
+ cached.source = 'cached (live lookup failed — check ADO auth)';
127
+ console.log(JSON.stringify(cached, null, 2));
128
+ return;
129
+ }
130
+
131
+ console.error(`PR #${prNumber} not found (tried live ADO API and cached pull-requests.json).`);
132
+ process.exit(1);
133
+ }
134
+
135
+ main().catch(e => { console.error(e.message); process.exit(1); });
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;
@@ -782,6 +793,71 @@ async function fetchAdoPrMetadata(prNum, adoOrg, adoProj, adoRepo) {
782
793
  };
783
794
  }
784
795
 
796
+ /**
797
+ * Fetch live PR and build status for a single PR number.
798
+ * Used by engine/ado-status.js so agents can check CI without raw curl calls.
799
+ * Returns { prNumber, title, branch, status, reviewStatus, buildStatus, buildErrorLog?,
800
+ * mergeConflict, url, project } or null on auth failure.
801
+ */
802
+ async function fetchSinglePrBuildStatus(project, prNumber) {
803
+ const token = await getAdoToken();
804
+ if (!token) return null;
805
+
806
+ const orgBase = getAdoOrgBase(project);
807
+ const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${project.repositoryId}`;
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
+ ]);
816
+ if (!prData) return null;
817
+
818
+ const mergeCommitId = prData.lastMergeCommit?.commitId;
819
+ const prBuilds = mergeCommitId
820
+ ? (buildsData?.value || []).filter(b => b.sourceVersion === mergeCommitId)
821
+ : [];
822
+
823
+ let buildStatus = classifyBuildStatus(prBuilds);
824
+ let buildErrorLog = null;
825
+
826
+ if (buildStatus === 'failing') {
827
+ try {
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);
838
+ }
839
+ if (logParts.length > 0) buildErrorLog = logParts.join('\n\n');
840
+ } catch (e) { log('warn', `fetchSinglePrBuildStatus error log for PR #${prNumber}: ${e.message}`); }
841
+ }
842
+
843
+ const votes = (prData.reviewers || []).map(r => r.vote);
844
+ const prUrl = `https://dev.azure.com/${project.adoOrg}/${project.adoProject}/_git/${project.repositoryId}/pullrequest/${prNumber}`;
845
+
846
+ return {
847
+ prNumber,
848
+ title: prData.title || '',
849
+ branch: stripRefsHeads(prData.sourceRefName),
850
+ status: prData.status || 'unknown',
851
+ reviewStatus: votesToReviewStatus(votes),
852
+ buildStatus,
853
+ ...(buildErrorLog ? { buildErrorLog } : {}),
854
+ mergeConflict: prData.mergeStatus === 'conflicts',
855
+ url: prUrl,
856
+ project: project.name,
857
+ source: 'live',
858
+ };
859
+ }
860
+
785
861
  module.exports = {
786
862
  getAdoToken,
787
863
  adoFetch,
@@ -792,5 +868,6 @@ module.exports = {
792
868
  needsAdoPollRetry,
793
869
  isAdoAuthError, // exported for testing
794
870
  fetchAdoPrMetadata,
871
+ fetchSinglePrBuildStatus,
795
872
  };
796
873
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.948",
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"
@@ -32,3 +32,25 @@ Your context window may be compacted or summarized mid-task by Claude's automati
32
32
  ````
33
33
  The `name` and `description` fields are required. `scope` defaults to `minions` (global). Use `scope: project` + `project: ProjectName` for project-specific skills.
34
34
  - Do TDD where it makes sense — write failing tests first, then implement, then verify tests pass. Especially for bug fixes (write a test that reproduces the bug) and new utility functions.
35
+
36
+ ## Checking PR and Build Status
37
+
38
+ When asked to check build status, CI results, or review state for a PR:
39
+
40
+ **Preferred — read cached state (always fresh within ~3 min when engine is running):**
41
+ Find the PR in `projects/<project-name>/pull-requests.json` by `prNumber`. Key fields:
42
+ - `buildStatus` — `passing` | `failing` | `running` | `none`
43
+ - `buildErrorLog` — compiler/pipeline errors when `buildStatus` is `failing`
44
+ - `reviewStatus` — `approved` | `changes-requested` | `waiting` | `pending`
45
+ - `status` — `active` | `merged` | `abandoned`
46
+ - `url` — link to the PR in ADO
47
+
48
+ **Live status (when engine isn't running or you need up-to-the-moment results):**
49
+ ```bash
50
+ node engine/ado-status.js <prNumber> # reads cached pull-requests.json
51
+ node engine/ado-status.js <prNumber> --live # fresh ADO API call
52
+ node engine/ado-status.js <prNumber> --live --project MyProject
53
+ ```
54
+ Output is JSON with the same fields. Exit 0 on success, 1 if not found.
55
+
56
+ **Never make raw `curl` calls to ADO APIs directly.** Use `node engine/ado-status.js` which routes through `ado.js` — authenticated, retried, circuit-broken. Raw `azureauth` + curl bypasses all of that.