@yemi33/minions 0.1.2265 → 0.1.2266

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.
Files changed (2) hide show
  1. package/dashboard.js +145 -4
  2. package/package.json +1 -1
package/dashboard.js CHANGED
@@ -590,6 +590,126 @@ let _prRefVerifierOverride = null; // test seam
590
590
  // (prRef, project) and returns true | false | null.
591
591
  function _setPrRefVerifierForTest(fn) { _prRefVerifierOverride = (typeof fn === 'function') ? fn : null; }
592
592
 
593
+ // ── Live PR fetch helpers (W-mqtrnp7y00056bc8) ───────────────────────────────
594
+ //
595
+ // When GET /api/prs/:id is called for a PR that is not in the local tracker
596
+ // (e.g. merged before the engine started tracking, or from an unpolled repo),
597
+ // attempt a live fetch from the platform API and return a shaped record with
598
+ // _liveOnly: true. Falls back to the existing 404 when both lookups fail.
599
+
600
+ const LIVE_PR_FETCH_TIMEOUT_MS = 8000;
601
+
602
+ // Test seam: when set, replaces the real _fetchLivePrRecord call so unit tests
603
+ // can inject a mock without standing up a real gh CLI or ADO endpoint.
604
+ let _livePrFetchForTest = null;
605
+ function _setLivePrFetchForTest(fn) {
606
+ _livePrFetchForTest = typeof fn === 'function' ? fn : null;
607
+ }
608
+
609
+ function _mapGhPrToRecord(ghPr, canonicalId) {
610
+ const isMerged = !!ghPr.merged_at;
611
+ const status = isMerged ? 'merged' : ghPr.state === 'closed' ? 'closed' : 'active';
612
+ return {
613
+ id: canonicalId,
614
+ prNumber: ghPr.number,
615
+ title: (ghPr.title || `PR #${ghPr.number}`).slice(0, 120),
616
+ agent: (ghPr.user && ghPr.user.login) ? String(ghPr.user.login).toLowerCase() : 'unknown',
617
+ branch: (ghPr.head && ghPr.head.ref) || '',
618
+ status,
619
+ url: ghPr.html_url || '',
620
+ description: typeof ghPr.body === 'string' ? ghPr.body.slice(0, 500) : '',
621
+ _liveOnly: true,
622
+ };
623
+ }
624
+
625
+ function _mapAdoPrToRecord(adoPr, canonicalId, ref) {
626
+ const status = adoPr.status === 'completed' ? 'merged'
627
+ : adoPr.status === 'abandoned' ? 'abandoned'
628
+ : 'active';
629
+ const branch = typeof adoPr.sourceRefName === 'string'
630
+ ? adoPr.sourceRefName.replace(/^refs\/heads\//, '')
631
+ : '';
632
+ const author = (adoPr.createdBy && (adoPr.createdBy.uniqueName || adoPr.createdBy.displayName)) || 'unknown';
633
+ const prUrl = `https://dev.azure.com/${encodeURIComponent(ref.org)}/${encodeURIComponent(ref.project)}/_git/${encodeURIComponent(ref.repo)}/pullrequest/${ref.number}`;
634
+ return {
635
+ id: canonicalId,
636
+ prNumber: adoPr.pullRequestId || ref.number,
637
+ title: (adoPr.title || `PR #${ref.number}`).slice(0, 120),
638
+ agent: String(author).toLowerCase(),
639
+ branch,
640
+ status,
641
+ url: prUrl,
642
+ description: typeof adoPr.description === 'string' ? adoPr.description.slice(0, 500) : '',
643
+ _liveOnly: true,
644
+ };
645
+ }
646
+
647
+ // Exported for unit testing — fetch a PR live from the platform API and return
648
+ // a tracker-shaped record. Returns null when the canonical id is not parseable
649
+ // or the host is unrecognized. Throws on fetch errors so callers can decide
650
+ // whether to log-and-degrade or propagate.
651
+ //
652
+ // opts: test seams only — production callers pass no opts.
653
+ // _resolveTokenForSlug(slug) → token string or null
654
+ // _shellSafeGh(args, opts) → stdout string (argv-form gh CLI)
655
+ // _adoFetch(url, token) → parsed JSON object
656
+ // _adoToken → ADO bearer token string (skips ado.getAdoToken())
657
+ async function _fetchLivePrRecord(canonicalId, opts) {
658
+ const o = opts || {};
659
+ const parsed = shared.parseCanonicalPrId(canonicalId);
660
+ if (!parsed) return null; // bare number or unknown format
661
+ const { scope, prNumber } = parsed;
662
+ const colonIdx = scope.indexOf(':');
663
+ const host = scope.slice(0, colonIdx).toLowerCase();
664
+ const scopeSlug = scope.slice(colonIdx + 1);
665
+
666
+ if (host === 'github') {
667
+ const [owner, repo] = scopeSlug.split('/');
668
+ if (!owner || !repo) return null;
669
+ const ghSlug = shared.validateGhSlug(`${owner}/${repo}`);
670
+ const num = String(prNumber);
671
+ const resolveToken = o._resolveTokenForSlug || ghToken.resolveTokenForSlug;
672
+ const token = resolveToken(`${owner}/${repo}`);
673
+ const ghOpts = { timeout: LIVE_PR_FETCH_TIMEOUT_MS };
674
+ if (token) ghOpts.env = { ...process.env, GH_TOKEN: token };
675
+ const shellGh = o._shellSafeGh || shared.shellSafeGh;
676
+ const raw = await shellGh(['api', `repos/${ghSlug}/pulls/${num}`], ghOpts);
677
+ const ghPr = JSON.parse(raw);
678
+ return _mapGhPrToRecord(ghPr, canonicalId);
679
+ }
680
+
681
+ if (host === 'ado') {
682
+ const segs = scopeSlug.split('/');
683
+ if (segs.length < 3) return null;
684
+ const [org, project, repo] = segs;
685
+ const ref = { host: 'ado', slug: scopeSlug, org, project, repo, number: prNumber, id: canonicalId };
686
+ const doFetch = o._adoFetch || (async (url, adoToken) => {
687
+ const res = await fetch(url, {
688
+ headers: { Authorization: `Bearer ${adoToken}`, 'Content-Type': 'application/json' },
689
+ signal: AbortSignal.timeout(LIVE_PR_FETCH_TIMEOUT_MS),
690
+ });
691
+ if (!res.ok) throw new Error(`ADO API ${res.status}: ${res.statusText}`);
692
+ const text = await res.text();
693
+ if (!text || text.trimStart().startsWith('<')) {
694
+ throw new Error(`ADO returned HTML instead of JSON for ${url.split('?')[0]}`);
695
+ }
696
+ return JSON.parse(text);
697
+ });
698
+ const adoTok = o._adoToken != null ? o._adoToken : await ado.getAdoToken();
699
+ if (!adoTok) throw new Error(`Could not acquire ADO token for ${canonicalId}`);
700
+ const orgBase = `https://dev.azure.com/${encodeURIComponent(org)}`;
701
+ const projEnc = encodeURIComponent(project);
702
+ const repoEnc = encodeURIComponent(repo);
703
+ const adoPr = await doFetch(
704
+ `${orgBase}/${projEnc}/_apis/git/repositories/${repoEnc}/pullRequests/${prNumber}?api-version=7.1`,
705
+ adoTok,
706
+ );
707
+ return _mapAdoPrToRecord(adoPr, canonicalId, ref);
708
+ }
709
+
710
+ return null; // unknown host
711
+ }
712
+
593
713
  function _findTrackedPrRecord(prRef, project) {
594
714
  if (!project) return null;
595
715
  try {
@@ -6398,16 +6518,35 @@ const server = http.createServer(async (req, res) => {
6398
6518
 
6399
6519
  // GET /api/prs/<id> — return a single fully-enriched PR record by canonical
6400
6520
  // id (`<host>:<slug>#<number>`) or by bare number (P-79b47b0c). The in-stack
6401
- // PR modal (renderPrs.openPrDetail) calls this on demand. Always returns the
6402
- // record exactly as queries.getPullRequests() produces it (no slimming).
6521
+ // PR modal (renderPrs.openPrDetail) calls this on demand. Returns the local
6522
+ // tracker record when found; falls back to a live platform fetch (GitHub REST
6523
+ // or ADO) for PRs that were never added to the tracker. The live record
6524
+ // carries _liveOnly: true so the UI can optionally surface a "start tracking"
6525
+ // offer. Returns {"error":"pr not found"} when both lookups fail.
6403
6526
  async function handlePrsById(req, res, match) {
6404
6527
  try {
6405
6528
  const id = decodeURIComponent(match[1] || '').trim();
6406
6529
  if (!id) return jsonReply(res, 400, { error: 'id required' });
6407
6530
  const prs = queries.getPullRequests();
6408
6531
  const found = prs.find(p => p && (p.id === id || String(p.number) === id));
6409
- if (!found) return jsonReply(res, 404, { error: 'pr not found' });
6410
- return jsonReply(res, 200, { pr: found });
6532
+ if (found) return jsonReply(res, 200, { pr: found });
6533
+
6534
+ // Not in local tracker — attempt live fetch from platform API.
6535
+ const liveFetch = _livePrFetchForTest || _fetchLivePrRecord;
6536
+ let liveRecord = null;
6537
+ try {
6538
+ liveRecord = await Promise.race([
6539
+ liveFetch(id),
6540
+ new Promise((_, rej) => {
6541
+ const t = setTimeout(() => rej(new Error(`live PR fetch timed out for ${id}`)), LIVE_PR_FETCH_TIMEOUT_MS + 1000);
6542
+ if (t.unref) t.unref();
6543
+ }),
6544
+ ]);
6545
+ } catch (e) {
6546
+ shared.log('warn', `handlePrsById: live fetch failed for ${id}: ${e.message}`);
6547
+ }
6548
+ if (liveRecord) return jsonReply(res, 200, { pr: liveRecord });
6549
+ return jsonReply(res, 404, { error: 'pr not found' });
6411
6550
  } catch (e) { return jsonReply(res, 500, { error: e.message }); }
6412
6551
  }
6413
6552
 
@@ -14166,6 +14305,8 @@ function _installCrashHandlers() {
14166
14305
  module.exports = {
14167
14306
  getMcpServers,
14168
14307
  _setPrRefVerifierForTest, // issue #246 — inject a fake loose-PR-ref verifier in handler tests
14308
+ _setLivePrFetchForTest, // W-mqtrnp7y00056bc8 — inject a mock live fetch for unit tests
14309
+ _fetchLivePrRecord, // W-mqtrnp7y00056bc8 — exported for direct unit testing
14169
14310
  _parseClaudeMcpListLine,
14170
14311
  _parseCopilotMcpListJson,
14171
14312
  _readWorkspaceMcpServers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2265",
3
+ "version": "0.1.2266",
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"