@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 +3 -2
- package/engine/ado-status.js +135 -0
- package/engine/ado.js +93 -16
- package/package.json +1 -1
- package/playbooks/shared-rules.md +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
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
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|