@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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.554 (2026-04-07)
4
+
5
+ ### Features
6
+ - add pin/unpin button to inbox and KB document modals
7
+
8
+ ### Fixes
9
+ - fetch actual build error logs instead of just reason string (#489)
10
+
3
11
  ## 0.1.552 (2026-04-07)
4
12
 
5
13
  ### Features
@@ -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 = renderMd(display);
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"><button class="pr-pager-btn" style="font-size:10px;padding:3px 10px" onclick="promoteToKB(\'' + escHtml(item.name) + '\')">Add to Knowledge Base</button></div>' +
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') delete pr._buildFailNotified;
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') delete pr._buildFailNotified;
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: `Build is failing: ${pr.buildFailReason || 'Check CI pipeline for details'}. Fix the build errors and push.`,
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
- writeInboxAlert(`build-fail-${pr.agent}-${pr.id}`,
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
- `A fix agent has been dispatched to address this. Review the fix when complete.\n`
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.552",
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"