@yemi33/minions 0.1.2223 → 0.1.2225

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/dashboard.js CHANGED
@@ -512,7 +512,64 @@ function inferActionPrRecord(action, prs, project = null) {
512
512
  return findPrRecordReferencedByText(prs, text, project);
513
513
  }
514
514
 
515
- function copyWorkItemPrFields(item, input, pr = null) {
515
+ // issue #246 verify a LOOSE (description/title-scanned) PR ref points at a
516
+ // real pull request before `copyWorkItemPrFields` promotes it to structured
517
+ // targetPr/pr_id/prNumber fields. GitHub & ADO issues share the PR number
518
+ // namespace, so a bare `owner/repo#244` / `#244` can be an ISSUE. Stamping an
519
+ // issue mints a phantom pull-requests.json record (agent 'human', goes
520
+ // abandoned) AND routes the dispatch through the PR-fix path instead of
521
+ // implement-and-open-a-fresh-PR.
522
+ //
523
+ // Resolution order (cheap → expensive, fail-safe):
524
+ // 1. An already-tracked PR record for the ref ⇒ it's a known PR ⇒ true (no net).
525
+ // 2. Host existence check (gh/ado prExists) ⇒ true | false | null.
526
+ // Returns true only when a PR is positively confirmed; false / null / unknown
527
+ // all mean "do not stamp" (prefer a fresh PR over a phantom one).
528
+ const _PR_REF_VERIFY_TTL_MS = 5 * 60 * 1000;
529
+ const _prRefVerifyCache = new Map(); // cacheKey → { value: boolean, at: ms }
530
+ let _prRefVerifierOverride = null; // test seam
531
+
532
+ // Test seam (issue #246) — inject a fake verifier so handler tests can assert
533
+ // the stamp decision without a live gh/ado call. The override receives
534
+ // (prRef, project) and returns true | false | null.
535
+ function _setPrRefVerifierForTest(fn) { _prRefVerifierOverride = (typeof fn === 'function') ? fn : null; }
536
+
537
+ function _findTrackedPrRecord(prRef, project) {
538
+ if (!project) return null;
539
+ try {
540
+ const prs = shared.safeJsonArr(shared.projectPrPath(project));
541
+ return shared.findPrRecord(prs, prRef, project);
542
+ } catch { return null; }
543
+ }
544
+
545
+ async function verifyLoosePrRefIsPr(prRef, project) {
546
+ if (_prRefVerifierOverride) {
547
+ try { return (await _prRefVerifierOverride(prRef, project)) === true; }
548
+ catch { return false; }
549
+ }
550
+ // 1. Known PR record ⇒ definitely a PR (no network call needed).
551
+ if (_findTrackedPrRecord(prRef, project)) return true;
552
+ // 2. Host existence check.
553
+ const info = shared.classifyPrRefForVerification(prRef, project);
554
+ if (!info) return false; // can't resolve host/number → fail-safe, do not stamp
555
+ const cacheKey = info.host === 'github'
556
+ ? `github:${info.slug}#${info.prNumber}`
557
+ : `ado:${info.adoOrg}/${info.adoProject}/${info.adoRepo}#${info.prNumber}`;
558
+ const cached = _prRefVerifyCache.get(cacheKey);
559
+ if (cached && (Date.now() - cached.at) < _PR_REF_VERIFY_TTL_MS) return cached.value;
560
+ let verdict = null;
561
+ try {
562
+ verdict = info.host === 'github'
563
+ ? await gh.prExists(info.slug, info.prNumber)
564
+ : await ado.prExists(info.adoOrg, info.adoProject, info.adoRepo, info.prNumber);
565
+ } catch { verdict = null; }
566
+ const value = verdict === true;
567
+ // Only cache a definitive answer; an unknown (null) may resolve next time.
568
+ if (verdict === true || verdict === false) _prRefVerifyCache.set(cacheKey, { value, at: Date.now() });
569
+ return value;
570
+ }
571
+
572
+ async function copyWorkItemPrFields(item, input, pr = null, opts = {}) {
516
573
  // W-mqbaby2a000pa8ee: Gate the LOOSE description/title scan in
517
574
  // `shared.extractWorkItemPrRef` on `type: "fix"`. Without this gate,
518
575
  // an implement/explore/test WI that merely mentions an existing PR
@@ -526,13 +583,30 @@ function copyWorkItemPrFields(item, input, pr = null) {
526
583
  // operator intent — they stamp on EVERY type. An explicit `pr` record
527
584
  // (caller already resolved the PR) also bypasses the gate. Only the
528
585
  // last-resort description/title regex scan is type-gated.
529
- if (!pr) {
586
+ //
587
+ // issue #246: a loose `github:owner/repo#NNN` / `#NNN` token can be an
588
+ // ISSUE — issues and PRs share one number namespace. Before stamping a
589
+ // loose ref we verify it points at a real PR (already-tracked record or a
590
+ // gh/ado existence check); structured refs are trusted without a network
591
+ // call. Fail-safe: if existence can't be confirmed, do NOT stamp — the WI
592
+ // then dispatches as a normal fix that opens a fresh PR.
593
+ let prRef;
594
+ if (pr) {
595
+ prRef = getWorkItemPrRef(input);
596
+ } else {
530
597
  const structuredRef = shared.extractStructuredWorkItemPrRef(input);
531
- if (!structuredRef && String(item?.type || '').toLowerCase() !== WORK_TYPE.FIX) {
532
- return;
598
+ if (structuredRef) {
599
+ prRef = structuredRef; // explicit operator intent — trusted, no verification
600
+ } else {
601
+ if (String(item?.type || '').toLowerCase() !== WORK_TYPE.FIX) return;
602
+ const looseRef = getWorkItemPrRef(input);
603
+ if (!looseRef) return;
604
+ // issue #246 — confirm the loose ref is a PR (not an issue) before stamping.
605
+ const confirmed = await verifyLoosePrRefIsPr(looseRef, opts.project || null);
606
+ if (!confirmed) return; // issue / unverifiable → dispatch as a fresh fix
607
+ prRef = looseRef;
533
608
  }
534
609
  }
535
- const prRef = getWorkItemPrRef(input);
536
610
  if (!prRef && !pr) return;
537
611
  const prNumber = pr ? shared.getPrNumber(pr) : shared.getPrNumber(prRef);
538
612
  item.targetPr = pr?.id || prRef;
@@ -6361,7 +6435,7 @@ const server = http.createServer(async (req, res) => {
6361
6435
  const originWi = extractMinionsOriginWiHeader(req);
6362
6436
  if (originAgent) item._originAgent = originAgent;
6363
6437
  if (originWi) item._originWi = originWi;
6364
- copyWorkItemPrFields(item, body);
6438
+ await copyWorkItemPrFields(item, body, null, { project: targetProject });
6365
6439
  // W-mq5wfh1v000e0da9 — Auto-enroll the PR into pull-requests.json when
6366
6440
  // this is a `type: fix` WI carrying a structured PR pointer and the PR
6367
6441
  // isn't tracked yet. Without this, the engine's `pr_not_found` gate
@@ -13857,6 +13931,7 @@ function _installCrashHandlers() {
13857
13931
  // Production entry points use the closures directly; tests import via require('./dashboard').
13858
13932
  module.exports = {
13859
13933
  getMcpServers,
13934
+ _setPrRefVerifierForTest, // issue #246 — inject a fake loose-PR-ref verifier in handler tests
13860
13935
  _parseClaudeMcpListLine,
13861
13936
  _parseCopilotMcpListJson,
13862
13937
  _readWorkspaceMcpServers,
@@ -56,6 +56,40 @@ PR-fix dispatch path, fail the PR-branch lookup, and stick in
56
56
  (`targetPr` / `prUrl` / `references[].url` / etc.) still stamps on
57
57
  every type — only the loose regex scan is gated.
58
58
 
59
+ ## Loose refs are PR-verified before stamping (issue #246)
60
+
61
+ A loose `github:owner/repo#NNN` / `ado:org/proj/repo#NNN` / bare `#NNN` token
62
+ carries **no issue-vs-PR discriminator** — GitHub issues and PRs share one
63
+ number namespace, and ADO work items share theirs. Before this guard, a
64
+ `type: "fix"` WI titled `Fix github:owner/repo#244: …` where **#244 is an
65
+ issue** got `targetPr` / `pr_id` / `prNumber` stamped by
66
+ `dashboard.js#copyWorkItemPrFields`, which then (a) minted a phantom
67
+ `pull-requests.json` record (`agent: 'human'`, went abandoned, polluted the PR
68
+ count) via `shared.autoEnrollPrFromFixWorkItem`, and (b) routed the dispatch
69
+ through the PR-fix path (`targetPr`/`pr_id`/`prNumber`) instead of
70
+ implement-and-open-a-fresh-PR.
71
+
72
+ `copyWorkItemPrFields` now verifies a **loose** ref points at a real PR before
73
+ stamping:
74
+
75
+ 1. **Structured refs are trusted** — `targetPr` / `prUrl` / `references[].url`
76
+ / `meta.pr_followup.parent_pr_url` are explicit operator intent and stamp
77
+ with **no** network call.
78
+ 2. **Loose (description/title-scanned) refs on a fix WI** are confirmed via:
79
+ - an already-tracked `pull-requests.json` record for the ref (cheap, no
80
+ network), else
81
+ - a host existence check — `engine/github.js#prExists` (`gh api
82
+ repos/:owner/:repo/pulls/NNN`; 404 ⇒ issue) or `engine/ado.js#prExists`
83
+ (`…/pullrequests/NNN`), routed via `engine/gh-token.js` per-account token.
84
+ `shared.classifyPrRefForVerification(prRef, project)` does the pure host/slug
85
+ /number routing; the result is TTL-cached.
86
+ 3. **Fail-safe:** when existence can't be confirmed (issue, 404, network/auth
87
+ error, unresolvable host) the ref is **not** stamped — the WI dispatches as a
88
+ normal fix that opens a NEW PR. Prefer a fresh PR over a phantom one.
89
+
90
+ The `type: "fix"` gate from W-mqbaby2a000pa8ee still applies first: non-fix WIs
91
+ are never stamped from loose prose.
92
+
59
93
  ## Structured-vs-loose split (W-mq18ec6h000p7b87)
60
94
 
61
95
  The PR-ref extractor has **two** variants — pick the right one for the
@@ -119,8 +119,8 @@
119
119
  { "file": "engine.js", "note": "discoverFromPrs reads canonical `pr.contextOnly` only." }
120
120
  ],
121
121
  "deprecated": "2026-06-08",
122
- "targetRemovalDate": "2026-06-16",
123
- "notes": "The write side already shipped out — there is no dual-write to retire. What remains is the read-fallback (`_contextOnly === true`) plus the one-shot boot migration migratePrGateFlags (engine/shared.js:6833) that copies + deletes the alias from any pre-migration on-disk record. Pair this entry's removal with `pr-record-autoObserve-underscore-alias`, `pr-record-manual-underscore-alias`, and `pr-link-autoObserve-body-param` — they share the same boot migration. Removal scope (DEFERRED — gated on a positive signal that every live pull-requests.json has been swept of underscore keys, not on the expired calendar date): delete the `_contextOnly` read-fallback in engine/shared.js and the migratePrGateFlags handling of it."
122
+ "targetRemovalDate": "2026-06-25",
123
+ "notes": "The write side already shipped out — there is no dual-write to retire. What remains is the read-fallback (`_contextOnly === true`) plus the one-shot boot migration migratePrGateFlags (engine/shared.js:6833) that copies + deletes the alias from any pre-migration on-disk record. Pair this entry's removal with `pr-record-autoObserve-underscore-alias`, `pr-record-manual-underscore-alias`, and `pr-link-autoObserve-body-param` — they share the same boot migration. Removal scope (DEFERRED — gated on a positive signal that every live pull-requests.json has been swept of underscore keys, not on the expired calendar date): delete the `_contextOnly` read-fallback in engine/shared.js and the migratePrGateFlags handling of it. On-disk sweep confirmed CLEAN on 2026-06-18: grep of every live projects/*/pull-requests.json (budget-test-project, constellation, minions-opg, office-bohemia) for the `_contextOnly`/`_autoObserve`/`_manual` record keys returned zero matches, so the deferral gate is now cleared and a deliberate, human-reviewed code removal can be scheduled (this calendar bump 2026-06-16 → 2026-06-25 is the docs-only consistency half; the MEDIUM/>3-file code removal stays a separate PR)."
124
124
  },
125
125
  {
126
126
  "id": "pr-record-autoObserve-underscore-alias",
@@ -130,8 +130,8 @@
130
130
  { "file": "dashboard.js", "note": "Zero `_autoObserve` references — link/observe handlers accept the canonical `contextOnly` body param and write no underscore alias onto the record." }
131
131
  ],
132
132
  "deprecated": "2026-06-08",
133
- "targetRemovalDate": "2026-06-16",
134
- "notes": "The write side already shipped out — there is no dual-write to retire. Removal is paired with `pr-record-contextOnly-underscore-alias` — same boot migration, same read-bridge. Removal scope (DEFERRED — gated on a sweep confirming no live record retains `_autoObserve`, not on the expired calendar date): delete the `_autoObserve === true` read in _prRecordIsLegacyManaged (engine/shared.js:6823) and the migratePrGateFlags handling of it."
133
+ "targetRemovalDate": "2026-06-25",
134
+ "notes": "The write side already shipped out — there is no dual-write to retire. Removal is paired with `pr-record-contextOnly-underscore-alias` — same boot migration, same read-bridge. Removal scope (DEFERRED — gated on a sweep confirming no live record retains `_autoObserve`, not on the expired calendar date): delete the `_autoObserve === true` read in _prRecordIsLegacyManaged (engine/shared.js:6823) and the migratePrGateFlags handling of it. On-disk sweep confirmed CLEAN on 2026-06-18: grep of every live projects/*/pull-requests.json (budget-test-project, constellation, minions-opg, office-bohemia) for the `_contextOnly`/`_autoObserve`/`_manual` record keys returned zero matches, so the deferral gate is now cleared and a deliberate, human-reviewed code removal can be scheduled (this calendar bump 2026-06-16 → 2026-06-25 is the docs-only consistency half; the MEDIUM/>3-file code removal stays a separate PR)."
135
135
  },
136
136
  {
137
137
  "id": "pr-record-manual-underscore-alias",
@@ -142,8 +142,8 @@
142
142
  { "file": "dashboard.js", "note": "Zero `_manual` references — the link handler no longer writes the provenance alias onto the record." }
143
143
  ],
144
144
  "deprecated": "2026-06-08",
145
- "targetRemovalDate": "2026-06-16",
146
- "notes": "Disambiguation: this entry covers ONLY the PR-record `_manual` flag (engine/<scope>/pull-requests.json). Other `_manual`-named flags elsewhere in the engine (work items, dispatch records, etc.) are NOT covered by this entry and remain in active use. The write side already shipped out — there is no write site or dashboard badge left to remove. Removal scope (DEFERRED — gated on a sweep confirming no live record retains `_manual`, not on the expired calendar date): drop the `_manual` handling in normalizePrRecord and migratePrGateFlags (engine/shared.js) once the on-disk sweep is confirmed."
145
+ "targetRemovalDate": "2026-06-25",
146
+ "notes": "Disambiguation: this entry covers ONLY the PR-record `_manual` flag (engine/<scope>/pull-requests.json). Other `_manual`-named flags elsewhere in the engine (work items, dispatch records, etc.) are NOT covered by this entry and remain in active use. The write side already shipped out — there is no write site or dashboard badge left to remove. Removal scope (DEFERRED — gated on a sweep confirming no live record retains `_manual`, not on the expired calendar date): drop the `_manual` handling in normalizePrRecord and migratePrGateFlags (engine/shared.js) once the on-disk sweep is confirmed. On-disk sweep confirmed CLEAN on 2026-06-18: grep of every live projects/*/pull-requests.json (budget-test-project, constellation, minions-opg, office-bohemia) for the `_contextOnly`/`_autoObserve`/`_manual` record keys returned zero matches, so the deferral gate is now cleared and a deliberate, human-reviewed code removal can be scheduled (this calendar bump 2026-06-16 → 2026-06-25 is the docs-only consistency half; the MEDIUM/>3-file code removal stays a separate PR)."
147
147
  },
148
148
  {
149
149
  "id": "pr-link-autoObserve-body-param",
@@ -152,8 +152,8 @@
152
152
  { "file": "dashboard.js", "note": "linkPullRequestForTracking resolves `contextOnly` from `body.contextOnly` when boolean, else `autoObserve === undefined ? false : !autoObserve` (dashboard.js:1055-1057). Route registry params string still lists `autoObserve?` (dashboard.js:12680)." }
153
153
  ],
154
154
  "deprecated": "2026-06-08",
155
- "targetRemovalDate": "2026-06-16",
156
- "notes": "Unlike the three record-field aliases, nothing is written here — this is purely an input read-fallback. Removal scope (DEFERRED — gated on confirming no client still POSTs `autoObserve`, not on the expired calendar date): drop the `!body.autoObserve` fallback in the link handler in dashboard.js, drop `autoObserve?` from the route registry `params` string, and update any client (dashboard JS, ops scripts) that still POSTs `autoObserve`. After removal, callers that still send `autoObserve` will see their value silently ignored."
155
+ "targetRemovalDate": "2026-06-25",
156
+ "notes": "Unlike the three record-field aliases, nothing is written here — this is purely an input read-fallback. Removal scope (DEFERRED — gated on confirming no client still POSTs `autoObserve`, not on the expired calendar date): drop the `!body.autoObserve` fallback in the link handler in dashboard.js, drop `autoObserve?` from the route registry `params` string, and update any client (dashboard JS, ops scripts) that still POSTs `autoObserve`. After removal, callers that still send `autoObserve` will see their value silently ignored. On-disk sweep confirmed CLEAN on 2026-06-18: grep of every live projects/*/pull-requests.json (budget-test-project, constellation, minions-opg, office-bohemia) for the `_contextOnly`/`_autoObserve`/`_manual` record keys returned zero matches, so the deferral gate is now cleared and a deliberate, human-reviewed removal can be scheduled (this calendar bump 2026-06-16 → 2026-06-25 is the docs-only consistency half; the actual code removal stays a separate human-reviewed PR)."
157
157
  },
158
158
  {
159
159
  "id": "worktreemode-field-rename",
package/engine/ado.js CHANGED
@@ -2511,6 +2511,25 @@ async function fetchAdoPrMetadata(prNum, adoOrg, adoProj, adoRepo) {
2511
2511
  };
2512
2512
  }
2513
2513
 
2514
+ /**
2515
+ * issue #246 — confirm a number points at a real ADO PULL REQUEST. ADO work
2516
+ * items (issues) live in a different namespace, so a non-PR number 404s on the
2517
+ * pullrequests endpoint. `adoFetch` can't distinguish 404 from auth/network
2518
+ * failure, so a missing/erroring PR returns null (not false) — and the caller
2519
+ * treats both false and null as "do not stamp" (fail-safe). Returns true only
2520
+ * on a positively-fetched PR.
2521
+ */
2522
+ async function prExists(adoOrg, adoProject, adoRepo, prNumber) {
2523
+ const n = parseInt(prNumber, 10);
2524
+ if (!adoOrg || !adoProject || !adoRepo || !Number.isInteger(n) || n <= 0) return null;
2525
+ try {
2526
+ const meta = await fetchAdoPrMetadata(n, adoOrg, adoProject, adoRepo);
2527
+ return meta ? true : null;
2528
+ } catch {
2529
+ return null;
2530
+ }
2531
+ }
2532
+
2514
2533
  /**
2515
2534
  * Fetch live PR and build status for a single PR number.
2516
2535
  * Used by engine/ado-status.js so agents can check CI without raw curl calls.
@@ -2918,6 +2937,7 @@ module.exports = {
2918
2937
  getAdoThrottleState,
2919
2938
  getAdoThrottleStateAll,
2920
2939
  fetchAdoPrMetadata,
2940
+ prExists, // issue #246 — confirm a loose ref points at a PR (not a work item) before stamping
2921
2941
  fetchSinglePrBuildStatus,
2922
2942
  findOpenPrOnBranch,
2923
2943
  applyAdoPrMetadata, // #3079 — exported for unit tests of targetRefName + retarget reset
package/engine/github.js CHANGED
@@ -400,6 +400,23 @@ async function ghApi(endpoint, slug, opts = {}) {
400
400
  }
401
401
  }
402
402
 
403
+ /**
404
+ * issue #246 — confirm a number points at a real PULL REQUEST (GitHub issues
405
+ * and PRs share one number namespace, so `owner/repo#244` may be an issue).
406
+ * Hits GET /repos/{slug}/pulls/{n}, which 404s for issue numbers. Returns:
407
+ * true — the PR exists
408
+ * false — 404 (the number is an issue or doesn't exist)
409
+ * null — could not determine (network / auth / throttle) → caller fail-safe
410
+ */
411
+ async function prExists(slug, prNumber, opts = {}) {
412
+ const n = parseInt(prNumber, 10);
413
+ if (!slug || !Number.isInteger(n) || n <= 0) return null;
414
+ const result = await ghApi(`/pulls/${n}`, slug, opts);
415
+ if (result === GH_NOT_FOUND) return false;
416
+ if (result && typeof result === 'object' && result._notFound !== true) return true;
417
+ return null;
418
+ }
419
+
403
420
  const BUILD_ERROR_LOG_MAX_LINES = 150;
404
421
 
405
422
  /**
@@ -1885,6 +1902,7 @@ module.exports = {
1885
1902
  isGhThrottled,
1886
1903
  getGhThrottleState,
1887
1904
  ghApi, // P-8c1b6e45 — used by engine/shared-branch-pr-reconcile.js to list open PRs on a feature branch
1905
+ prExists, // issue #246 — confirm a loose ref points at a PR (not an issue) before stamping
1888
1906
  // Exported for testing
1889
1907
  isGitHub,
1890
1908
  getRepoSlug,
package/engine/shared.js CHANGED
@@ -6163,15 +6163,11 @@ function parsePrUrl(url) {
6163
6163
  // `isPrTargeted` paths intact and prevents PR-feedback fix WIs from being
6164
6164
  // dispatched on a fresh `work/<wi-id>` branch when only the description /
6165
6165
  // references[] carried the PR pointer (issue #2999).
6166
- function _trimTrailingPrRefPunctuation(value) {
6167
- return String(value || '').replace(/[),.;:]+$/g, '');
6168
- }
6169
-
6170
6166
  function extractPrRefFromText(value) {
6171
6167
  const text = String(value || '');
6172
6168
  if (!text.trim()) return null;
6173
6169
  const urlMatch = text.match(/https?:\/\/[^\s<>()]+(?:\/pull\/\d+|\/pullrequest\/\d+)[^\s<>()]*/i);
6174
- if (urlMatch) return _trimTrailingPrRefPunctuation(urlMatch[0]);
6170
+ if (urlMatch) return String(urlMatch[0] || '').replace(/[),.;:]+$/g, '');
6175
6171
  const canonicalMatch = text.match(/\b(?:github|ado):[^\s#]+#\d+\b/i);
6176
6172
  if (canonicalMatch) return canonicalMatch[0];
6177
6173
  const legacyMatch = text.match(/\bPR-(\d+)\b/i);
@@ -7053,6 +7049,38 @@ function deriveUrlForPrRef(prRef, project) {
7053
7049
  return null;
7054
7050
  }
7055
7051
 
7052
+ // issue #246 — classify a PR ref into the host + identifiers needed to verify
7053
+ // it actually points at a PULL REQUEST (not an ISSUE, which shares the number
7054
+ // namespace on GitHub and is a distinct endpoint on ADO). Pure: no network.
7055
+ //
7056
+ // Resolution: prefer the scope embedded in the ref (canonical `github:…#N` /
7057
+ // `ado:…#N` id, or a PR URL); fall back to the project's own scope for a bare
7058
+ // `#N` / `PR-N` / number ref. Returns null when the ref can't be resolved to a
7059
+ // concrete host + number — the caller treats null as "cannot verify" and, per
7060
+ // the fail-safe contract, declines to stamp.
7061
+ //
7062
+ // Returns one of:
7063
+ // { host: 'github', slug: 'owner/repo', prNumber }
7064
+ // { host: 'ado', adoOrg, adoProject, adoRepo, prNumber }
7065
+ // null
7066
+ function classifyPrRefForVerification(prRef, project = null) {
7067
+ const scoped = getPrScopeInfo(prRef);
7068
+ let scope = scoped?.scope || '';
7069
+ const prNumber = scoped?.prNumber ?? getPrNumber(prRef);
7070
+ if (!scope && project) scope = getProjectPrScope(project);
7071
+ if (!scope || prNumber == null) return null;
7072
+ if (scope.startsWith('github:')) {
7073
+ const slug = scope.slice('github:'.length);
7074
+ return slug.includes('/') ? { host: 'github', slug, prNumber } : null;
7075
+ }
7076
+ if (scope.startsWith('ado:')) {
7077
+ const parts = scope.slice('ado:'.length).split('/');
7078
+ if (parts.length < 3 || !parts[0] || !parts[1] || !parts[2]) return null;
7079
+ return { host: 'ado', adoOrg: parts[0], adoProject: parts[1], adoRepo: parts[2], prNumber };
7080
+ }
7081
+ return null;
7082
+ }
7083
+
7056
7084
  // W-mq5wfh1v000e0da9 — Auto-enroll a PR into pull-requests.json when a
7057
7085
  // `type: fix` work item is created with a structured PR pointer. Without
7058
7086
  // this, fix WIs against untracked PRs bypass the polling / review / build
@@ -8775,6 +8803,7 @@ module.exports = {
8775
8803
  migratePrGateFlags, // W-mq5s5ttx000j7ab8-a — boot migration wired from engine/cli.js
8776
8804
  autoEnrollPrFromFixWorkItem,
8777
8805
  deriveUrlForPrRef, // exported for testing
8806
+ classifyPrRefForVerification, // issue #246 — host routing for loose PR-ref verification
8778
8807
  nextWorkItemId,
8779
8808
  getProjectOrg,
8780
8809
  getAdoOrgBase,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2223",
3
+ "version": "0.1.2225",
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"