@yemi33/minions 0.1.552 → 0.1.554
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 +8 -0
- package/dashboard/js/render-kb.js +5 -1
- package/dashboard/js/render-prs.js +6 -1
- package/engine/ado.js +82 -1
- package/engine/github.js +70 -1
- package/engine.js +15 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -216,9 +216,13 @@ async function kbOpenItem(category, file) {
|
|
|
216
216
|
try {
|
|
217
217
|
const content = await fetch('/api/knowledge/' + category + '/' + encodeURIComponent(file)).then(r => r.text());
|
|
218
218
|
const display = content.replace(/^---[\s\S]*?---\n*/m, '');
|
|
219
|
+
var pk = kbPinKey(category, file);
|
|
220
|
+
var pinned = isPinned(pk);
|
|
219
221
|
document.getElementById('modal-title').textContent = file;
|
|
220
222
|
const modalBody = document.getElementById('modal-body');
|
|
221
|
-
modalBody.innerHTML =
|
|
223
|
+
modalBody.innerHTML =
|
|
224
|
+
'<div style="margin-bottom:12px"><button class="pr-pager-btn pin-btn' + (pinned ? ' pinned' : '') + '" style="font-size:10px;padding:3px 10px" data-pin-key="' + escHtml(pk) + '" onclick="_togglePinAndRefresh(this.dataset.pinKey,\'kb\');kbOpenItem(\'' + escHtml(category) + '\',\'' + escHtml(file) + '\')">' + (pinned ? 'Unpin' : 'Pin to top') + '</button></div>' +
|
|
225
|
+
renderMd(display);
|
|
222
226
|
_modalDocContext = { title: file, content: display, selection: '' };
|
|
223
227
|
_modalFilePath = 'knowledge/' + category + '/' + file; showModalQa();
|
|
224
228
|
// Clear notification badge when opening this document
|
|
@@ -91,9 +91,14 @@ function openAllPrs() {
|
|
|
91
91
|
function openModal(i) {
|
|
92
92
|
const item = inboxData[i];
|
|
93
93
|
if (!item) return;
|
|
94
|
+
var pk = inboxPinKey(item.name);
|
|
95
|
+
var pinned = isPinned(pk);
|
|
94
96
|
document.getElementById('modal-title').textContent = item.name;
|
|
95
97
|
document.getElementById('modal-body').innerHTML =
|
|
96
|
-
'<div style="margin-bottom:12px
|
|
98
|
+
'<div style="margin-bottom:12px;display:flex;gap:8px;align-items:center">' +
|
|
99
|
+
'<button class="pr-pager-btn pin-btn' + (pinned ? ' pinned' : '') + '" style="font-size:10px;padding:3px 10px" data-pin-key="' + escHtml(pk) + '" onclick="_togglePinAndRefresh(this.dataset.pinKey,\'inbox\');openModal(' + i + ')">' + (pinned ? 'Unpin' : 'Pin to top') + '</button>' +
|
|
100
|
+
'<button class="pr-pager-btn" style="font-size:10px;padding:3px 10px" onclick="promoteToKB(\'' + escHtml(item.name) + '\')">Add to Knowledge Base</button>' +
|
|
101
|
+
'</div>' +
|
|
97
102
|
'<div style="font-size:12px;line-height:1.7;color:var(--muted)">' + renderMd(item.content) + '</div>';
|
|
98
103
|
_modalDocContext = { title: item.name, content: item.content, selection: '' };
|
|
99
104
|
_modalFilePath = 'notes/inbox/' + item.name; showModalQa();
|
package/engine/ado.js
CHANGED
|
@@ -82,6 +82,73 @@ async function adoFetch(url, token, _retryCount = 0) {
|
|
|
82
82
|
return JSON.parse(text);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/** Fetch raw text from ADO API (for build logs which aren't JSON). */
|
|
86
|
+
async function adoFetchText(url, token) {
|
|
87
|
+
const res = await fetch(url, {
|
|
88
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) throw new Error(`ADO API ${res.status}: ${res.statusText}`);
|
|
91
|
+
return res.text();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const BUILD_ERROR_LOG_MAX_LINES = 150;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Fetch actual build/compiler error logs from ADO when a build fails.
|
|
98
|
+
* Extracts buildId from the failed status's targetUrl, queries the build timeline
|
|
99
|
+
* for failed tasks, and fetches their logs.
|
|
100
|
+
* Returns truncated log text or null if unavailable.
|
|
101
|
+
*/
|
|
102
|
+
async function fetchAdoBuildErrorLog(orgBase, project, failedStatus, token) {
|
|
103
|
+
try {
|
|
104
|
+
// Extract buildId from the targetUrl (e.g. .../_build/results?buildId=12345)
|
|
105
|
+
const targetUrl = failedStatus?.targetUrl || '';
|
|
106
|
+
const buildIdMatch = targetUrl.match(/buildId=(\d+)/);
|
|
107
|
+
if (!buildIdMatch) {
|
|
108
|
+
log('debug', `No buildId in targetUrl: ${targetUrl.slice(0, 120)}`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const buildId = buildIdMatch[1];
|
|
112
|
+
|
|
113
|
+
// Fetch build timeline to find failed tasks
|
|
114
|
+
const timelineUrl = `${orgBase}/${project.adoProject}/_apis/build/builds/${buildId}/timeline?api-version=7.1`;
|
|
115
|
+
const timeline = await adoFetch(timelineUrl, token);
|
|
116
|
+
if (!timeline?.records) return null;
|
|
117
|
+
|
|
118
|
+
// Find failed records that have logs
|
|
119
|
+
const failedRecords = timeline.records.filter(r =>
|
|
120
|
+
r.result === 'failed' && r.log?.id
|
|
121
|
+
);
|
|
122
|
+
if (failedRecords.length === 0) return null;
|
|
123
|
+
|
|
124
|
+
// Fetch logs for failed tasks (cap at 3 to limit API calls)
|
|
125
|
+
const logParts = [];
|
|
126
|
+
for (const record of failedRecords.slice(0, 3)) {
|
|
127
|
+
try {
|
|
128
|
+
const logUrl = `${orgBase}/${project.adoProject}/_apis/build/builds/${buildId}/logs/${record.log.id}?api-version=7.1`;
|
|
129
|
+
const text = await adoFetchText(logUrl, token);
|
|
130
|
+
if (text) {
|
|
131
|
+
logParts.push(`--- ${record.name || 'Task'} ---\n${text}`);
|
|
132
|
+
}
|
|
133
|
+
} catch { /* skip individual log fetch failures */ }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (logParts.length === 0) return null;
|
|
137
|
+
|
|
138
|
+
// Join and truncate to last N lines
|
|
139
|
+
const combined = logParts.join('\n\n');
|
|
140
|
+
const lines = combined.split('\n');
|
|
141
|
+
if (lines.length > BUILD_ERROR_LOG_MAX_LINES) {
|
|
142
|
+
return `... (truncated, showing last ${BUILD_ERROR_LOG_MAX_LINES} lines)\n` +
|
|
143
|
+
lines.slice(-BUILD_ERROR_LOG_MAX_LINES).join('\n');
|
|
144
|
+
}
|
|
145
|
+
return combined;
|
|
146
|
+
} catch (e) {
|
|
147
|
+
log('warn', `Failed to fetch ADO build error log: ${e.message}`);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
85
152
|
// ─── Shared PR Polling Loop ──────────────────────────────────────────────────
|
|
86
153
|
|
|
87
154
|
/**
|
|
@@ -185,6 +252,7 @@ async function pollPrStatus(config) {
|
|
|
185
252
|
if (pr.buildStatus && pr.buildStatus !== 'none') {
|
|
186
253
|
delete pr.buildStatus;
|
|
187
254
|
delete pr.buildFailReason;
|
|
255
|
+
delete pr.buildErrorLog;
|
|
188
256
|
delete pr._buildFailNotified;
|
|
189
257
|
}
|
|
190
258
|
await engine().handlePostMerge(pr, project, config, newStatus);
|
|
@@ -294,8 +362,21 @@ async function pollPrStatus(config) {
|
|
|
294
362
|
pr.buildStatus = buildStatus;
|
|
295
363
|
if (buildFailReason) pr.buildFailReason = buildFailReason;
|
|
296
364
|
else delete pr.buildFailReason;
|
|
297
|
-
if (buildStatus !== 'failing')
|
|
365
|
+
if (buildStatus !== 'failing') {
|
|
366
|
+
delete pr._buildFailNotified;
|
|
367
|
+
delete pr.buildErrorLog;
|
|
368
|
+
}
|
|
298
369
|
updated = true;
|
|
370
|
+
|
|
371
|
+
// Fetch actual compiler/build error logs when transitioning to failing
|
|
372
|
+
if (buildStatus === 'failing') {
|
|
373
|
+
const failedStatusObj = buildStatuses.find(s => s.state === 'failed' || s.state === 'error');
|
|
374
|
+
const errorLog = await fetchAdoBuildErrorLog(orgBase, project, failedStatusObj, token);
|
|
375
|
+
if (errorLog) {
|
|
376
|
+
pr.buildErrorLog = errorLog;
|
|
377
|
+
log('info', `PR ${pr.id}: fetched ${errorLog.split('\n').length} lines of build error log`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
299
380
|
}
|
|
300
381
|
|
|
301
382
|
return updated;
|
package/engine/github.js
CHANGED
|
@@ -94,6 +94,61 @@ async function ghApiWithBackoff(endpoint, slug) {
|
|
|
94
94
|
return result;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
const BUILD_ERROR_LOG_MAX_LINES = 150;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Fetch actual build/compiler error logs from GitHub when a check run fails.
|
|
101
|
+
* Tries annotations first (structured error messages), then falls back to the
|
|
102
|
+
* Actions job log. Returns truncated log text or null if unavailable.
|
|
103
|
+
*/
|
|
104
|
+
async function fetchGhBuildErrorLog(slug, failedRuns) {
|
|
105
|
+
try {
|
|
106
|
+
const logParts = [];
|
|
107
|
+
|
|
108
|
+
for (const run of (failedRuns || []).slice(0, 3)) {
|
|
109
|
+
if (!run?.id) continue;
|
|
110
|
+
|
|
111
|
+
// Try annotations first — these contain structured compiler/lint errors
|
|
112
|
+
try {
|
|
113
|
+
const annotations = await ghApi(`/check-runs/${run.id}/annotations`, slug);
|
|
114
|
+
if (Array.isArray(annotations) && annotations.length > 0) {
|
|
115
|
+
const formatted = annotations
|
|
116
|
+
.filter(a => a.annotation_level === 'failure' || a.annotation_level === 'warning')
|
|
117
|
+
.map(a => `${a.path || ''}:${a.start_line || ''} [${a.annotation_level}] ${a.message || ''}`)
|
|
118
|
+
.join('\n');
|
|
119
|
+
if (formatted) {
|
|
120
|
+
logParts.push(`--- ${run.name || 'Check'} (annotations) ---\n${formatted}`);
|
|
121
|
+
continue; // annotations are sufficient for this run
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch { /* fall through to job log */ }
|
|
125
|
+
|
|
126
|
+
// Fallback: fetch the full Actions job log
|
|
127
|
+
try {
|
|
128
|
+
const cmd = `gh api "repos/${slug}/actions/jobs/${run.id}/logs" 2>&1`;
|
|
129
|
+
const result = await execAsync(cmd, { timeout: 15000, encoding: 'utf-8' });
|
|
130
|
+
if (result && !result.includes('Not Found')) {
|
|
131
|
+
logParts.push(`--- ${run.name || 'Check'} ---\n${result}`);
|
|
132
|
+
}
|
|
133
|
+
} catch { /* skip individual log fetch failures */ }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (logParts.length === 0) return null;
|
|
137
|
+
|
|
138
|
+
// Join and truncate to last N lines
|
|
139
|
+
const combined = logParts.join('\n\n');
|
|
140
|
+
const lines = combined.split('\n');
|
|
141
|
+
if (lines.length > BUILD_ERROR_LOG_MAX_LINES) {
|
|
142
|
+
return `... (truncated, showing last ${BUILD_ERROR_LOG_MAX_LINES} lines)\n` +
|
|
143
|
+
lines.slice(-BUILD_ERROR_LOG_MAX_LINES).join('\n');
|
|
144
|
+
}
|
|
145
|
+
return combined;
|
|
146
|
+
} catch (e) {
|
|
147
|
+
log('warn', `Failed to fetch GitHub build error log: ${e.message}`);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
97
152
|
// ─── Shared PR Polling Loop ─────────────────────────────────────────────────
|
|
98
153
|
|
|
99
154
|
async function forEachActiveGhPr(config, callback) {
|
|
@@ -237,6 +292,7 @@ async function pollPrStatus(config) {
|
|
|
237
292
|
if (pr.buildStatus && pr.buildStatus !== 'none') {
|
|
238
293
|
delete pr.buildStatus;
|
|
239
294
|
delete pr.buildFailReason;
|
|
295
|
+
delete pr.buildErrorLog;
|
|
240
296
|
delete pr._buildFailNotified;
|
|
241
297
|
}
|
|
242
298
|
await engine().handlePostMerge(pr, project, config, newStatus);
|
|
@@ -334,8 +390,21 @@ async function pollPrStatus(config) {
|
|
|
334
390
|
pr.buildStatus = buildStatus;
|
|
335
391
|
if (buildFailReason) pr.buildFailReason = buildFailReason;
|
|
336
392
|
else delete pr.buildFailReason;
|
|
337
|
-
if (buildStatus !== 'failing')
|
|
393
|
+
if (buildStatus !== 'failing') {
|
|
394
|
+
delete pr._buildFailNotified;
|
|
395
|
+
delete pr.buildErrorLog;
|
|
396
|
+
}
|
|
338
397
|
updated = true;
|
|
398
|
+
|
|
399
|
+
// Fetch actual compiler/build error logs when transitioning to failing
|
|
400
|
+
if (buildStatus === 'failing') {
|
|
401
|
+
const failedRuns = runs.filter(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
|
|
402
|
+
const errorLog = await fetchGhBuildErrorLog(slug, failedRuns);
|
|
403
|
+
if (errorLog) {
|
|
404
|
+
pr.buildErrorLog = errorLog;
|
|
405
|
+
log('info', `PR ${pr.id}: fetched ${errorLog.split('\n').length} lines of build error log`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
339
408
|
}
|
|
340
409
|
}
|
|
341
410
|
}
|
package/engine.js
CHANGED
|
@@ -1503,20 +1503,29 @@ async function discoverFromPrs(config, project) {
|
|
|
1503
1503
|
const agentId = resolveAgent('fix', config, pr.agent);
|
|
1504
1504
|
if (!agentId) continue;
|
|
1505
1505
|
|
|
1506
|
+
let reviewNote = `Build is failing: ${pr.buildFailReason || 'Check CI pipeline for details'}. Fix the build errors and push.`;
|
|
1507
|
+
if (pr.buildErrorLog) {
|
|
1508
|
+
reviewNote += `\n\n## Build Error Log\n\n\`\`\`\n${pr.buildErrorLog}\n\`\`\``;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1506
1511
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
1507
1512
|
pr_id: pr.id, pr_branch: pr.branch || '',
|
|
1508
|
-
review_note:
|
|
1513
|
+
review_note: reviewNote,
|
|
1509
1514
|
}, `Fix build failure on PR ${pr.id}`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
|
|
1510
1515
|
if (item) { newWork.push(item); setCooldown(key); }
|
|
1511
1516
|
|
|
1512
1517
|
// Notify the author agent about the build failure
|
|
1513
1518
|
if (pr.agent && !pr._buildFailNotified) {
|
|
1514
|
-
|
|
1515
|
-
`# Build Failure Notification\n\n` +
|
|
1519
|
+
let alertBody = `# Build Failure Notification\n\n` +
|
|
1516
1520
|
`**Your PR ${pr.id}** on branch \`${pr.branch || 'unknown'}\` has a failing build.\n` +
|
|
1517
|
-
`**Reason:** ${pr.buildFailReason || 'Check CI pipeline for details'}\n\n
|
|
1518
|
-
|
|
1519
|
-
|
|
1521
|
+
`**Reason:** ${pr.buildFailReason || 'Check CI pipeline for details'}\n\n`;
|
|
1522
|
+
if (pr.buildErrorLog) {
|
|
1523
|
+
// Include first 30 lines of error log in notification (full log in fix agent prompt)
|
|
1524
|
+
const logPreview = pr.buildErrorLog.split('\n').slice(0, 30).join('\n');
|
|
1525
|
+
alertBody += `**Error preview:**\n\`\`\`\n${logPreview}\n\`\`\`\n\n`;
|
|
1526
|
+
}
|
|
1527
|
+
alertBody += `A fix agent has been dispatched to address this. Review the fix when complete.\n`;
|
|
1528
|
+
writeInboxAlert(`build-fail-${pr.agent}-${pr.id}`, alertBody);
|
|
1520
1529
|
// Mark notified to prevent duplicate alerts
|
|
1521
1530
|
try {
|
|
1522
1531
|
const prPath = projectPrPath(project);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.554",
|
|
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"
|