@yemi33/minions 0.1.549 → 0.1.551

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,14 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.549 (2026-04-07)
3
+ ## 0.1.551 (2026-04-07)
4
4
 
5
5
  ### Fixes
6
+ - mark stale build status on ADO auth failure, bypass 6-tick cadence for recovery (#483)
7
+
8
+ ## 0.1.550 (2026-04-07)
9
+
10
+ ### Fixes
11
+ - use isGitRepo flag for update banner instead of diskCommit presence
6
12
  - prefer git rev-parse over .minions-commit for disk version detection
7
13
 
8
14
  ## 0.1.548 (2026-04-07)
@@ -12,8 +12,8 @@ function prRow(pr) {
12
12
  const reviewSource = sq.status || effectiveReviewStatus || 'pending';
13
13
  const reviewClass = reviewSource === 'approved' ? 'approved' : (reviewSource === 'changes-requested' || reviewSource === 'rejected') ? 'rejected' : reviewSource === 'waiting' ? 'building' : 'draft';
14
14
  const reviewLabel = sq.status === 'waiting' ? 'reviewing (minions)' : sq.status ? sq.status + ' (minions)' : (effectiveReviewStatus || 'pending');
15
- const buildClass = pr.buildStatus === 'passing' ? 'build-pass' : pr.buildStatus === 'failing' ? 'build-fail' : pr.buildStatus === 'running' ? 'building' : 'no-build';
16
- const buildLabel = pr.buildStatus || 'none';
15
+ const buildClass = pr._buildStatusStale ? 'build-stale' : pr.buildStatus === 'passing' ? 'build-pass' : pr.buildStatus === 'failing' ? 'build-fail' : pr.buildStatus === 'running' ? 'building' : 'no-build';
16
+ const buildLabel = (pr.buildStatus || 'none') + (pr._buildStatusStale ? ' (stale)' : '');
17
17
  const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
18
18
  const statusLabel = pr.status || 'active';
19
19
  const url = pr.url || '#';
@@ -251,6 +251,7 @@
251
251
  .pr-badge.build-pass { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
252
252
  .pr-badge.build-fail { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
253
253
  .pr-badge.no-build { background: var(--surface); color: var(--muted); border: 1px solid var(--border); }
254
+ .pr-badge.build-stale { background: rgba(210,153,34,0.15); color: var(--orange); border: 1px dashed var(--orange); }
254
255
  .error-details-btn { font-size: var(--text-xs); padding: var(--space-1) var(--space-3); margin-left: var(--space-2); background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); border-radius: var(--radius-lg); cursor: pointer; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; }
255
256
  .error-details-btn:hover { background: rgba(248,81,73,0.3); }
256
257
  .pr-empty { color: var(--muted); font-style: italic; font-size: var(--text-md); padding: var(--space-6) 0; }
package/dashboard.js CHANGED
@@ -230,12 +230,13 @@ function getDiskVersion() {
230
230
  try { diskVersion = require('@yemi33/minions/package.json').version; } catch {}
231
231
  }
232
232
  let diskCommit = null;
233
+ let isGitRepo = false;
233
234
  // Prefer git (authoritative for repo-based dev), fall back to .minions-commit (installed copies)
234
- try { diskCommit = require('child_process').execSync('git rev-parse --short HEAD', { cwd: MINIONS_DIR, encoding: 'utf8', timeout: 5000, windowsHide: true }).trim(); } catch {}
235
+ try { diskCommit = require('child_process').execSync('git rev-parse --short HEAD', { cwd: MINIONS_DIR, encoding: 'utf8', timeout: 5000, windowsHide: true }).trim(); isGitRepo = true; } catch {}
235
236
  if (!diskCommit) {
236
237
  try { diskCommit = fs.readFileSync(path.join(MINIONS_DIR, '.minions-commit'), 'utf8').trim() || null; } catch {}
237
238
  }
238
- _diskVersionCache = { diskVersion, diskCommit };
239
+ _diskVersionCache = { diskVersion, diskCommit, isGitRepo };
239
240
  _diskVersionCacheTs = now;
240
241
  return _diskVersionCache;
241
242
  }
@@ -372,7 +373,7 @@ function getStatus() {
372
373
  installId: safeRead(path.join(MINIONS_DIR, '.install-id')).trim() || null,
373
374
  version: (() => {
374
375
  const engine = getEngineState();
375
- const { diskVersion, diskCommit } = getDiskVersion();
376
+ const { diskVersion, diskCommit, isGitRepo } = getDiskVersion();
376
377
  const engineStale = !!(engine.codeVersion && diskVersion && engine.codeVersion !== diskVersion) ||
377
378
  !!(engine.codeCommit && diskCommit && engine.codeCommit !== diskCommit);
378
379
  const dashboardStale = !!(diskVersion && _dashboardVersion.codeVersion && diskVersion !== _dashboardVersion.codeVersion) ||
@@ -390,7 +391,7 @@ function getStatus() {
390
391
  stale: engineStale || dashboardStale,
391
392
  latest: _npmVersionCache?.latest || null,
392
393
  // Only show "update available" for npm installs (no git repo) — repo users manage their own updates
393
- updateAvailable: !diskCommit && !!(diskVersion && _npmVersionCache?.latest && _npmVersionCache.latest !== diskVersion && _compareVersions(_npmVersionCache.latest, diskVersion) > 0),
394
+ updateAvailable: !isGitRepo && !!(diskVersion && _npmVersionCache?.latest && _npmVersionCache.latest !== diskVersion && _compareVersions(_npmVersionCache.latest, diskVersion) > 0),
394
395
  _npmCheckError: _npmVersionCache?.error || null,
395
396
  };
396
397
  })(),
@@ -3658,7 +3659,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3658
3659
  // Routes endpoint (self-describing API)
3659
3660
  { method: 'GET', path: '/api/version', desc: 'Current + latest version info with update check', handler: async (req, res) => {
3660
3661
  const npm = await checkNpmVersion();
3661
- const { diskVersion, diskCommit } = getDiskVersion();
3662
+ const { diskVersion, diskCommit, isGitRepo } = getDiskVersion();
3662
3663
  const engine = getEngineState();
3663
3664
  const engineStale = !!(engine.codeVersion && diskVersion && engine.codeVersion !== diskVersion) ||
3664
3665
  !!(engine.codeCommit && diskCommit && engine.codeCommit !== diskCommit);
@@ -3672,7 +3673,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3672
3673
  dashboardRunning: _dashboardVersion.codeVersion,
3673
3674
  dashboardRunningCommit: _dashboardVersion.codeCommit,
3674
3675
  latest: npm.latest,
3675
- updateAvailable: !diskCommit && !!(diskVersion && npm.latest && _compareVersions(npm.latest, diskVersion) > 0),
3676
+ updateAvailable: !isGitRepo && !!(diskVersion && npm.latest && _compareVersions(npm.latest, diskVersion) > 0),
3676
3677
  engineStale,
3677
3678
  dashboardStale,
3678
3679
  stale: engineStale || dashboardStale,
package/engine/ado.js CHANGED
@@ -21,6 +21,20 @@ function engine() {
21
21
  let _adoTokenCache = { token: null, expiresAt: 0 };
22
22
  let _adoTokenFailedUntil = 0; // backoff: skip azureauth calls until this timestamp
23
23
 
24
+ // ─── Auth Failure Tracking ──────────────────────────────────────────────────
25
+ // Set when pollPrStatus encounters auth errors mid-loop. The engine checks this
26
+ // to bypass the normal 6-tick cadence and re-poll on the next tick.
27
+ let _adoPollHadAuthFailure = false;
28
+
29
+ /** Check if auth failure during PR poll means an early re-poll is needed. */
30
+ function needsAdoPollRetry() { return _adoPollHadAuthFailure; }
31
+
32
+ /** Detect auth-related errors from adoFetch (HTML redirect, 401, 403). */
33
+ function isAdoAuthError(err) {
34
+ const msg = err?.message || '';
35
+ return msg.includes('auth redirect') || msg.includes('HTML instead of JSON') || /ADO API (401|403)/.test(msg);
36
+ }
37
+
24
38
  async function getAdoToken() {
25
39
  if (_adoTokenCache.token && Date.now() < _adoTokenCache.expiresAt) {
26
40
  return _adoTokenCache.token;
@@ -133,16 +147,23 @@ async function forEachActivePr(config, token, callback) {
133
147
  // ─── PR Status Polling ───────────────────────────────────────────────────────
134
148
 
135
149
  async function pollPrStatus(config) {
150
+ _adoPollHadAuthFailure = false; // reset before polling — set again if errors recur
151
+
136
152
  const token = await getAdoToken();
137
153
  if (!token) {
138
154
  log('warn', 'Skipping PR status poll — no ADO token available');
155
+ _adoPollHadAuthFailure = true; // trigger retry on next tick
139
156
  return;
140
157
  }
141
158
 
142
159
  const totalUpdated = await forEachActivePr(config, token, async (project, pr, prNum, orgBase) => {
160
+ try {
143
161
  const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${project.repositoryId}/pullrequests/${prNum}`;
144
162
  let updated = false;
145
163
 
164
+ // Clear stale flag — we're attempting a fresh poll
165
+ if (pr._buildStatusStale) { delete pr._buildStatusStale; updated = true; }
166
+
146
167
  const prData = await adoFetch(`${repoBase}?api-version=7.1`, token);
147
168
 
148
169
  let newStatus = pr.status;
@@ -278,6 +299,17 @@ async function pollPrStatus(config) {
278
299
  }
279
300
 
280
301
  return updated;
302
+ } catch (err) {
303
+ // Auth errors → mark build status stale so dashboard shows uncertainty
304
+ // and engine re-polls on next tick instead of waiting 6 ticks
305
+ if (isAdoAuthError(err)) {
306
+ pr._buildStatusStale = true;
307
+ _adoPollHadAuthFailure = true;
308
+ log('warn', `PR ${pr.id}: build status marked stale (auth error: ${err.message})`);
309
+ return true; // count as updated to persist the stale flag
310
+ }
311
+ throw err; // re-throw non-auth errors for forEachActivePr to handle
312
+ }
281
313
  });
282
314
 
283
315
  if (totalUpdated > 0) {
@@ -520,5 +552,7 @@ module.exports = {
520
552
  pollPrHumanComments,
521
553
  reconcilePrs,
522
554
  checkLiveReviewStatus,
555
+ needsAdoPollRetry,
556
+ isAdoAuthError, // exported for testing
523
557
  };
524
558
 
package/engine.js CHANGED
@@ -937,7 +937,7 @@ function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
937
937
  // ─── Inbox Consolidation (extracted to engine/consolidation.js) ──────────────
938
938
 
939
939
  const { consolidateInbox } = require('./engine/consolidation');
940
- const { pollPrStatus, pollPrHumanComments, reconcilePrs, checkLiveReviewStatus: adoCheckLiveReview } = require('./engine/ado');
940
+ const { pollPrStatus, pollPrHumanComments, reconcilePrs, checkLiveReviewStatus: adoCheckLiveReview, needsAdoPollRetry } = require('./engine/ado');
941
941
  const { pollPrStatus: ghPollPrStatus, pollPrHumanComments: ghPollPrHumanComments, reconcilePrs: ghReconcilePrs, checkLiveReviewStatus: ghCheckLiveReview } = require('./engine/github');
942
942
 
943
943
  // ─── State Snapshot ─────────────────────────────────────────────────────────
@@ -2447,7 +2447,8 @@ async function tickInner() {
2447
2447
 
2448
2448
  // 2.6. Poll PR status: build, review, merge (every 6 ticks = ~3 minutes)
2449
2449
  // Awaited so PR state is consistent before discoverWork reads it
2450
- if (tickCount % 6 === 0) {
2450
+ // Also re-polls early if previous tick had ADO auth failures (stale build status recovery)
2451
+ if (tickCount % 6 === 0 || needsAdoPollRetry()) {
2451
2452
  try { await pollPrStatus(config); } catch (err) { log('warn', `ADO PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
2452
2453
  try { await ghPollPrStatus(config); } catch (err) { log('warn', `GitHub PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
2453
2454
  // Sync PR status back to PRD items (missing → done when active PR exists)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.549",
3
+ "version": "0.1.551",
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"