@yemi33/minions 0.1.2448 → 0.1.2449

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 (315) hide show
  1. package/bin/cli-api-client.js +1 -1
  2. package/bin/install-internal-minions.js +1382 -44
  3. package/bin/install-layout.js +150 -0
  4. package/bin/minions.js +460 -167
  5. package/dashboard/docs/typography.md +65 -12
  6. package/dashboard/js/command-center.js +58 -4
  7. package/dashboard/js/detail-panel.js +36 -0
  8. package/dashboard/js/memory-panel.js +59 -12
  9. package/dashboard/js/qa.js +179 -20
  10. package/dashboard/js/refresh.js +148 -12
  11. package/dashboard/js/render-dispatch.js +3 -4
  12. package/dashboard/js/render-inbox.js +2 -2
  13. package/dashboard/js/render-other.js +3 -3
  14. package/dashboard/js/render-pipelines.js +14 -0
  15. package/dashboard/js/render-plans.js +57 -9
  16. package/dashboard/js/render-prd.js +132 -23
  17. package/dashboard/js/render-prs.js +195 -166
  18. package/dashboard/js/render-schedules.js +63 -3
  19. package/dashboard/js/render-utils.js +3 -3
  20. package/dashboard/js/render-watches.js +19 -3
  21. package/dashboard/js/render-work-items.js +238 -30
  22. package/dashboard/js/settings.js +205 -54
  23. package/dashboard/js/utils.js +51 -1
  24. package/dashboard/pages/home.html +1 -1
  25. package/dashboard/pages/qa.html +1 -16
  26. package/dashboard/pages/work.html +40 -0
  27. package/dashboard/shared/cc-limits.js +79 -0
  28. package/dashboard/shared/pr-filters.js +21 -38
  29. package/dashboard/shared/project-git-summary.js +1 -1
  30. package/dashboard/shared/record-filters.js +169 -0
  31. package/dashboard/shared/watches-source.js +1 -1
  32. package/dashboard/shared/welcome-popup.js +1 -1
  33. package/dashboard/shared/wi-filters.js +302 -0
  34. package/dashboard/slim/body.html +1 -0
  35. package/dashboard/slim/js/command-send.js +26 -0
  36. package/dashboard/slim/js/modals-tiles.js +380 -39
  37. package/dashboard/slim/js/status.js +13 -21
  38. package/dashboard/slim/layout.html +1 -0
  39. package/dashboard/slim/panel-bootstrap.js +6 -2
  40. package/dashboard/slim/styles.css +38 -0
  41. package/dashboard/styles.css +159 -55
  42. package/dashboard-build.js +13 -2
  43. package/dashboard.js +956 -423
  44. package/docs/README.md +11 -6
  45. package/docs/api-errors.md +2 -2
  46. package/docs/architecture-review-2026-07-09.md +1 -1
  47. package/docs/architecture.excalidraw +2 -2
  48. package/docs/auto-discovery.md +18 -9
  49. package/docs/branch-derivation.md +4 -4
  50. package/docs/capture-demos.js +39 -2
  51. package/docs/ci-runner-canary.md +123 -0
  52. package/docs/claude-md-propagation.md +2 -2
  53. package/docs/cloud-agent-dispatch.md +204 -0
  54. package/docs/command-center.md +7 -7
  55. package/docs/completion-reports.md +43 -20
  56. package/docs/constants.md +10 -3
  57. package/docs/constellation-bridge.md +134 -6
  58. package/docs/constellation-style-telemetry.md +4 -4
  59. package/docs/contracts/capability-protocol.v1.json +165 -0
  60. package/docs/cooldown-merge-semantics.md +12 -12
  61. package/docs/copilot-cli-schema.md +7 -7
  62. package/docs/cross-repo-plans.md +10 -10
  63. package/docs/dead-code-audit-retractions.md +5 -5
  64. package/docs/default-branch-ci.md +173 -0
  65. package/docs/deprecated.json +31 -31
  66. package/docs/design-inbox-entries-schema.md +3 -3
  67. package/docs/design-language.md +1051 -0
  68. package/docs/design-state-storage.md +11 -11
  69. package/docs/diagnostics-crash-reports.md +9 -9
  70. package/docs/diagnostics-memory.md +5 -5
  71. package/docs/documentation-audit-2026-07-09.md +7 -7
  72. package/docs/engine-restart.md +90 -5
  73. package/docs/harness-mode.md +1 -1
  74. package/docs/internal-install.md +338 -39
  75. package/docs/kb-dedup-duplicate-pair-investigation.md +5 -5
  76. package/docs/kb-pr3223-cascade-archiving.md +1 -1
  77. package/docs/kb-pr696-merge-conflict-docs.md +6 -6
  78. package/docs/kb-sweep.md +35 -35
  79. package/docs/keep-processes.md +1 -1
  80. package/docs/live-checkout-mode.md +30 -30
  81. package/docs/managed-spawn.md +18 -14
  82. package/docs/named-agents.md +7 -7
  83. package/docs/plan-lifecycle.md +69 -2
  84. package/docs/pr-author-identity.md +114 -0
  85. package/docs/pr-auto-fix-dispatch.md +19 -4
  86. package/docs/pr-comment-followup.md +6 -6
  87. package/docs/pr-review-fix-loop.md +59 -10
  88. package/docs/process-termination.md +40 -0
  89. package/docs/proposals/repo-pool-for-live-checkout.md +13 -13
  90. package/docs/qa-runbook-lifecycle.md +367 -17
  91. package/docs/qa-runbooks.md +3 -3
  92. package/docs/rfc-completion-json.md +18 -18
  93. package/docs/runtime-adapters.md +26 -21
  94. package/docs/security.md +6 -6
  95. package/docs/self-improvement.md +4 -4
  96. package/docs/shared-lifecycle-module-map.md +473 -472
  97. package/docs/skills.md +52 -3
  98. package/docs/slim-ux/concepts.md +121 -116
  99. package/docs/specs/agent-configurability.md +18 -18
  100. package/docs/specs/agent-rename.md +18 -18
  101. package/docs/team-memory.md +38 -21
  102. package/docs/timeouts-and-liveness.md +118 -10
  103. package/docs/tutorials/01-install-and-connect.md +1 -1
  104. package/docs/watches.md +40 -39
  105. package/docs/workspace-manifests.md +4 -4
  106. package/docs/worktree-lifecycle.md +293 -14
  107. package/engine/README.md +46 -0
  108. package/engine/{ado-comment.js → ado/comment.js} +8 -8
  109. package/engine/{ado-git-auth.js → ado/git-auth.js} +4 -4
  110. package/engine/{ado.js → ado/index.js} +417 -63
  111. package/engine/{ado-status.js → ado/status.js} +6 -8
  112. package/engine/{ado-token.js → ado/token.js} +1 -1
  113. package/engine/{acp-transport.js → agents/acp-transport.js} +62 -22
  114. package/engine/{agent-worker-pool.js → agents/agent-worker-pool.js} +17 -8
  115. package/engine/{cc-worker-pool.js → agents/cc-worker-pool.js} +16 -6
  116. package/engine/{claude-md-context.js → agents/claude-md-context.js} +5 -5
  117. package/engine/{harness-context.js → agents/harness-context.js} +5 -5
  118. package/engine/{harness.js → agents/harness.js} +3 -3
  119. package/engine/{llm.js → agents/llm.js} +18 -14
  120. package/engine/{model-discovery.js → agents/model-discovery.js} +2 -2
  121. package/engine/{playbook.js → agents/playbook.js} +155 -22
  122. package/engine/{pooled-agent-process.js → agents/pooled-agent-process.js} +14 -12
  123. package/engine/{preflight.js → agents/preflight.js} +29 -10
  124. package/engine/{spawn-agent.js → agents/spawn-agent.js} +25 -14
  125. package/engine/{spawn-phase-watchdog.js → agents/spawn-phase-watchdog.js} +16 -7
  126. package/engine/{steering.js → agents/steering.js} +5 -5
  127. package/engine/{tools-inventory.js → agents/tools-inventory.js} +2 -2
  128. package/engine/{agent-api-validation.js → api/agent-api-validation.js} +2 -2
  129. package/engine/{api-validation.js → api/api-validation.js} +1 -1
  130. package/engine/api/bridge.js +787 -0
  131. package/engine/{cc-api-validation.js → api/cc-api-validation.js} +1 -1
  132. package/engine/api/companion.js +560 -0
  133. package/engine/{content-api-validation.js → api/content-api-validation.js} +2 -2
  134. package/engine/{pr-issue-validation.js → api/pr-issue-validation.js} +33 -6
  135. package/engine/{settings-validation.js → api/settings-validation.js} +32 -4
  136. package/engine/api-contracts/agent-content.js +4 -4
  137. package/engine/api-contracts/capability-manifest.js +236 -0
  138. package/engine/api-contracts/capability-protocol.js +333 -0
  139. package/engine/api-contracts/cc-ops.js +1 -1
  140. package/engine/api-contracts/config-runtime.js +5 -0
  141. package/engine/api-contracts/core.js +28 -1
  142. package/engine/api-contracts/index.js +100 -0
  143. package/engine/api-contracts/orchestration.js +18 -5
  144. package/engine/api-contracts/pull-requests.js +37 -6
  145. package/engine/api-contracts/qa-process.js +29 -6
  146. package/engine/api-contracts/work-plan-prd.js +21 -1
  147. package/engine/cloud/contract.js +212 -0
  148. package/engine/cloud/index.js +159 -0
  149. package/engine/{execution-model.js → core/execution-model.js} +1 -1
  150. package/engine/{features.js → core/features.js} +4 -4
  151. package/engine/{operator-identity.js → core/operator-identity.js} +1 -1
  152. package/engine/{queries.js → core/queries.js} +201 -36
  153. package/engine/{safe-expr.js → core/safe-expr.js} +1 -1
  154. package/engine/{shared.js → core/shared.js} +1637 -175
  155. package/engine/{stdio-timestamps.js → core/stdio-timestamps.js} +1 -1
  156. package/engine/{untrusted-fence.js → core/untrusted-fence.js} +3 -3
  157. package/engine/db/index.js +11 -2
  158. package/engine/db/migrations/002-dispatches.js +3 -3
  159. package/engine/db/migrations/003-work-items.js +1 -1
  160. package/engine/db/migrations/004-pull-requests.js +1 -1
  161. package/engine/db/migrations/006-metrics.js +1 -1
  162. package/engine/db/migrations/007-watches.js +2 -2
  163. package/engine/db/migrations/008-small-state.js +1 -1
  164. package/engine/db/migrations/009-qa.js +1 -1
  165. package/engine/db/migrations/010-pr-links.js +1 -1
  166. package/engine/db/migrations/011-remaining-state.js +1 -1
  167. package/engine/db/migrations/012-steering-deliveries.js +2 -2
  168. package/engine/db/migrations/013-backfill-broken-note-links.js +1 -1
  169. package/engine/db/migrations/014-pr-fix-target-prefs.js +2 -2
  170. package/engine/db/migrations/015-plans-prds.js +0 -0
  171. package/engine/db/migrations/018-sql-only-cutover.js +2 -2
  172. package/engine/db/migrations/021-archived-work-items.js +1 -1
  173. package/engine/db/migrations/022-global-cc-session.js +1 -1
  174. package/engine/db/migrations/023-engine-state.js +1 -1
  175. package/engine/db/migrations/025-malformed-work-item-phantoms.js +1 -1
  176. package/engine/db/migrations/027-review-learning-lifecycle.js +1 -1
  177. package/engine/db/migrations/029-repair-reused-versions.js +20 -0
  178. package/engine/db/migrations/031-pr-author-identity.js +137 -0
  179. package/engine/{consolidation.js → memory/consolidation.js} +6 -6
  180. package/engine/{kb-sweep-runner.js → memory/kb-sweep-runner.js} +2 -2
  181. package/engine/{kb-sweep.js → memory/kb-sweep.js} +9 -7
  182. package/engine/{memory-retrieval.js → memory/memory-retrieval.js} +46 -4
  183. package/engine/{memory-store.js → memory/memory-store.js} +3 -3
  184. package/engine/{promotion.js → memory/promotion.js} +3 -3
  185. package/engine/{review-learning-backfill.js → memory/review-learning-backfill.js} +6 -6
  186. package/engine/{review-learning.js → memory/review-learning.js} +10 -5
  187. package/engine/{diagnostics-memory.js → observability/diagnostics-memory.js} +1 -1
  188. package/engine/{logs-store.js → observability/logs-store.js} +5 -5
  189. package/engine/{metrics-store.js → observability/metrics-store.js} +4 -4
  190. package/engine/{check-status.js → operations/check-status.js} +3 -3
  191. package/engine/{cli.js → operations/cli.js} +271 -113
  192. package/engine/{distribution.js → operations/distribution.js} +5 -6
  193. package/engine/{cleanup.js → orchestration/cleanup.js} +72 -45
  194. package/engine/{cooldown.js → orchestration/cooldown.js} +5 -5
  195. package/engine/{dispatch-events.js → orchestration/dispatch-events.js} +2 -2
  196. package/engine/{dispatch.js → orchestration/dispatch.js} +129 -36
  197. package/engine/orchestration/failed-scheduled-cleanup.js +274 -0
  198. package/engine/{lifecycle.js → orchestration/lifecycle.js} +198 -90
  199. package/engine/{meeting.js → orchestration/meeting.js} +6 -16
  200. package/engine/{pipeline.js → orchestration/pipeline.js} +12 -12
  201. package/engine/{pre-dispatch-eval.js → orchestration/pre-dispatch-eval.js} +10 -9
  202. package/engine/{routing.js → orchestration/routing.js} +3 -3
  203. package/engine/{schedule-bootstrap.js → orchestration/schedule-bootstrap.js} +4 -4
  204. package/engine/{scheduler.js → orchestration/scheduler.js} +38 -8
  205. package/engine/{timeout.js → orchestration/timeout.js} +158 -109
  206. package/engine/{db-events.js → persistence/db-events.js} +2 -2
  207. package/engine/{dispatch-store.js → persistence/dispatch-store.js} +7 -7
  208. package/engine/{inbox-store.js → persistence/inbox-store.js} +2 -2
  209. package/engine/{note-link-backfill.js → persistence/note-link-backfill.js} +4 -4
  210. package/engine/{pr-fix-target-store.js → persistence/pr-fix-target-store.js} +8 -8
  211. package/engine/{pull-requests-store.js → persistence/pull-requests-store.js} +21 -7
  212. package/engine/{small-state-store.js → persistence/small-state-store.js} +31 -31
  213. package/engine/persistence/state-operations.js +350 -0
  214. package/engine/{steering-store.js → persistence/steering-store.js} +6 -6
  215. package/engine/{issues.js → planning/issues.js} +2 -2
  216. package/engine/{plan-prd-validation.js → planning/plan-prd-validation.js} +8 -2
  217. package/engine/planning/prd-result-sidecar.js +190 -0
  218. package/engine/{prd-store.js → planning/prd-store.js} +17 -17
  219. package/engine/{project-discovery.js → planning/project-discovery.js} +5 -5
  220. package/engine/{projects.js → planning/projects.js} +10 -10
  221. package/engine/{resolve-area.js → planning/resolve-area.js} +1 -1
  222. package/engine/{work-item-validation.js → planning/work-item-validation.js} +39 -3
  223. package/engine/{work-items-store.js → planning/work-items-store.js} +29 -21
  224. package/engine/{keep-process-sweep.js → processes/keep-process-sweep.js} +57 -17
  225. package/engine/{managed-spawn-launcher.js → processes/managed-spawn-launcher.js} +3 -3
  226. package/engine/{managed-spawn.js → processes/managed-spawn.js} +97 -46
  227. package/engine/{process-utils.js → processes/process-utils.js} +599 -55
  228. package/engine/{abandoned-pr-reconciliation.js → providers/abandoned-pr-reconciliation.js} +17 -7
  229. package/engine/{comment-classifier.js → providers/comment-classifier.js} +85 -17
  230. package/engine/{comment-format.js → providers/comment-format.js} +5 -5
  231. package/engine/{gh-comment.js → providers/gh-comment.js} +15 -15
  232. package/engine/{gh-token.js → providers/gh-token.js} +4 -4
  233. package/engine/{github.js → providers/github.js} +131 -54
  234. package/engine/{pr-action.js → providers/pr-action.js} +13 -12
  235. package/engine/{pr-clone-keep.js → providers/pr-clone-keep.js} +7 -7
  236. package/engine/{pr-devbox.js → providers/pr-devbox.js} +6 -6
  237. package/engine/{pr-fix-target.js → providers/pr-fix-target.js} +13 -13
  238. package/engine/{pr-remote-patch.js → providers/pr-remote-patch.js} +4 -4
  239. package/engine/{pr-resolve.js → providers/pr-resolve.js} +7 -7
  240. package/engine/{pr-temp-clone.js → providers/pr-temp-clone.js} +5 -5
  241. package/engine/{pr-track.js → providers/pr-track.js} +11 -13
  242. package/engine/{shared-branch-pr-reconcile.js → providers/shared-branch-pr-reconcile.js} +4 -4
  243. package/engine/qa/auto-prd-qa.js +313 -0
  244. package/engine/{qa-from-prd.js → qa/from-prd.js} +42 -12
  245. package/engine/qa/prd-session.js +240 -0
  246. package/engine/{qa-process-validation.js → qa/process-validation.js} +14 -9
  247. package/engine/{qa-runbooks.js → qa/runbooks.js} +1 -1
  248. package/engine/{qa-runs.js → qa/runs.js} +286 -15
  249. package/engine/{qa-sessions.js → qa/sessions.js} +595 -49
  250. package/engine/qa/visual-journey.js +654 -0
  251. package/engine/{qa-runners.js → qa-runners/index.js} +7 -7
  252. package/engine/qa-runners/maestro.js +3 -3
  253. package/engine/qa-runners/playwright.js +2 -2
  254. package/engine/{restart-health.js → recovery/restart-health.js} +48 -4
  255. package/engine/recovery/stop-stack.js +607 -0
  256. package/engine/{supervisor.js → recovery/supervisor.js} +105 -175
  257. package/engine/{watchdog.js → recovery/watchdog.js} +136 -13
  258. package/engine/runtimes/claude.js +14 -12
  259. package/engine/runtimes/codex.js +8 -6
  260. package/engine/runtimes/copilot.js +17 -16
  261. package/engine/{watch-actions.js → watches/actions.js} +13 -13
  262. package/engine/{watches.js → watches/index.js} +43 -32
  263. package/engine/{watches-store.js → watches/store.js} +4 -4
  264. package/engine/{create-pr-worktree.js → worktrees/create-pr.js} +1 -1
  265. package/engine/{worktree-gc.js → worktrees/gc.js} +70 -22
  266. package/engine/worktrees/inventory.js +671 -0
  267. package/engine/{live-checkout.js → worktrees/live-checkout.js} +4 -4
  268. package/engine/{worktree-pool.js → worktrees/pool.js} +2 -2
  269. package/engine/{worktree-preflight.js → worktrees/preflight.js} +1 -0
  270. package/engine/worktrees/quarantine-refs.js +173 -0
  271. package/engine.js +1137 -208
  272. package/minions.js +147 -77
  273. package/package.json +10 -6
  274. package/playbooks/_pr-description-audit.md +110 -78
  275. package/playbooks/build-fix-complex.md +2 -0
  276. package/playbooks/fix.md +16 -12
  277. package/playbooks/implement-shared.md +2 -0
  278. package/playbooks/implement.md +19 -20
  279. package/playbooks/plan-to-prd.md +18 -3
  280. package/playbooks/qa-session-draft.md +136 -1
  281. package/playbooks/qa-session-execute.md +80 -2
  282. package/playbooks/qa-session-setup.md +17 -1
  283. package/playbooks/qa-validate.md +1 -1
  284. package/playbooks/setup.md +2 -0
  285. package/playbooks/shared-rules.md +25 -32
  286. package/playbooks/templates/followup-dispatch.md +4 -3
  287. package/playbooks/verify.md +1 -1
  288. package/prompts/cc-system.md +19 -27
  289. package/watch-plugins/README.md +92 -0
  290. package/watch-plugins/ado-author-prs.js +336 -0
  291. package/watch-plugins/gh-author-prs.js +375 -0
  292. package/watch-plugins/http.js +474 -0
  293. package/watch-plugins/teams-channel.js +869 -0
  294. package/docs/dev-composite-workflow.md +0 -101
  295. package/docs/pr-screenshots/pr-886/after-single-header.png +0 -0
  296. package/docs/pr-screenshots/pr-886/before-duplicate-header.png +0 -0
  297. package/docs/pr-screenshots/pr-895/01-cancellation-reason-detail.png +0 -0
  298. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
  299. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
  300. package/docs/pr-screenshots/pr-901/projects-tab-default.png +0 -0
  301. package/docs/pr-screenshots/pr-901/projects-tab-fmf-selected.png +0 -0
  302. package/docs/pr-screenshots/pr-916/model-picker-AFTER-crop.png +0 -0
  303. package/docs/pr-screenshots/pr-916/model-picker-AFTER.png +0 -0
  304. package/docs/pr-screenshots/pr-916/model-picker-BEFORE-crop.png +0 -0
  305. package/docs/pr-screenshots/pr-916/model-picker-BEFORE.png +0 -0
  306. package/docs/pr-screenshots/pr-916/model-picker-dropdown-AFTER.png +0 -0
  307. package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
  308. package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
  309. package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
  310. package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
  311. package/docs/visual-evidence-ci.md +0 -103
  312. package/engine/bridge.js +0 -379
  313. package/engine/quarantine-refs.js +0 -103
  314. package/engine/state-operations.js +0 -178
  315. /package/engine/{steering-constraints.js → agents/steering-constraints.js} +0 -0
@@ -1,39 +1,41 @@
1
1
  /**
2
- * engine/ado.js — Azure DevOps integration for Minions engine.
2
+ * engine/ado/index.js — Azure DevOps integration for Minions engine.
3
3
  * Extracted from engine.js: ADO token management, PR status polling, human comment polling.
4
4
  */
5
5
 
6
6
  const path = require('path');
7
7
  const childProcess = require('child_process');
8
- const shared = require('./shared');
8
+ const shared = require('../core/shared');
9
9
  const { exec, execAsync, getAdoOrgBase, log, ts, dateStamp, PR_STATUS, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, createThrottleTracker, createBackoffTracker, writeToInbox } = shared;
10
- const { getPrs } = require('./queries');
10
+ const { getPrs } = require('../core/queries');
11
11
  const { mutateJsonFileLocked } = shared;
12
- const { acquireAdoToken } = require('./ado-token');
12
+ const { acquireAdoToken } = require('./token');
13
13
  const {
14
- hasMinionsMarker,
15
14
  hasVerdictPrefix,
15
+ classifyMarkerAuthorship,
16
+ isMarkerAuthored,
17
+ MARKER_AUTHORSHIP,
16
18
  classifyNonActionableAutomationComment,
17
- } = require('./comment-classifier');
18
- const { wrapUntrusted, buildSource } = require('./untrusted-fence');
19
- const reviewLearning = require('./review-learning');
19
+ } = require('../providers/comment-classifier');
20
+ const { wrapUntrusted, buildSource } = require('../core/untrusted-fence');
21
+ const reviewLearning = require('../memory/review-learning');
20
22
 
21
23
  // Lazy require to avoid circular dependency — only needed for engine().handlePostMerge
22
24
  let _engine = null;
23
25
  function engine() {
24
- if (!_engine) _engine = require('../engine');
26
+ if (!_engine) _engine = require('../../engine');
25
27
  return _engine;
26
28
  }
27
29
 
28
30
  // Lazy require for dispatch module (avoids circular dependency via engine)
29
31
  let _dispatch = null;
30
- function dispatchModule() { if (!_dispatch) _dispatch = require('./dispatch'); return _dispatch; }
32
+ function dispatchModule() { if (!_dispatch) _dispatch = require('../orchestration/dispatch'); return _dispatch; }
31
33
 
32
34
  // Lazy require for lifecycle module — only needed for the stale-noopfix sweep
33
- // in pollPrStatus (W-mpx44p05000ze8d8). lifecycle.js lazy-requires ado.js back,
35
+ // in pollPrStatus (W-mpx44p05000ze8d8). lifecycle.js lazy-requires ado/ back,
34
36
  // so this MUST stay lazy to avoid a circular import at module-load time.
35
37
  let _lifecycle = null;
36
- function lifecycleModule() { if (!_lifecycle) _lifecycle = require('./lifecycle'); return _lifecycle; }
38
+ function lifecycleModule() { if (!_lifecycle) _lifecycle = require('../orchestration/lifecycle'); return _lifecycle; }
37
39
 
38
40
  const stripRefsHeads = s => (s || '').replace('refs/heads/', '');
39
41
  const getAdoPrUrl = (project, prNumber) => {
@@ -408,10 +410,17 @@ function applyAdoPrMetadata(pr, prData) {
408
410
  updated = true;
409
411
  }
410
412
 
411
- const author = String(prData.createdBy?.displayName || '').trim();
412
- if (author && pr.agent === 'human') {
413
- pr.agent = author;
414
- updated = true;
413
+ // Populate the real author identity from the ADO PR creator. Never overwrite
414
+ // pr.agent (the Minions author-agent) with a human display name.
415
+ {
416
+ const adoAuthor = shared.buildAdoAuthorIdentity(prData.createdBy);
417
+ if (adoAuthor) {
418
+ const mergedAuthor = shared.mergePrAuthorIdentity(pr.author, adoAuthor);
419
+ if (JSON.stringify(mergedAuthor) !== JSON.stringify(pr.author || null)) {
420
+ pr.author = mergedAuthor;
421
+ updated = true;
422
+ }
423
+ }
415
424
  }
416
425
 
417
426
  // W-mpej044m00076d63 / W-mrezh0yb0007f733: keep `pr.created` aligned with
@@ -462,26 +471,103 @@ function classifyBuildStatus(prBuilds) {
462
471
  return BUILD_STATUS.NONE;
463
472
  }
464
473
 
474
+ /**
475
+ * Recency ordering key for one ADO build record, compared as a tuple so a
476
+ * weaker signal never outranks a stronger one:
477
+ * queueTime (→ startTime → finishTime) → numeric build id → response order.
478
+ * ADO always stamps `queueTime` and returns builds newest-first, but neither
479
+ * is guaranteed by the API contract, so the key degrades instead of picking an
480
+ * arbitrary generation.
481
+ */
482
+ function buildRecencyKey(build, index) {
483
+ const time = Date.parse(build?.queueTime || build?.startTime || build?.finishTime || '');
484
+ const id = Number(build?.id);
485
+ return [
486
+ Number.isFinite(time) ? time : -Infinity,
487
+ Number.isFinite(id) ? id : -Infinity,
488
+ -index,
489
+ ];
490
+ }
491
+
492
+ function compareBuildRecency(a, b) {
493
+ for (let i = 0; i < a.length; i++) {
494
+ if (a[i] > b[i]) return 1;
495
+ if (a[i] < b[i]) return -1;
496
+ }
497
+ return 0;
498
+ }
499
+
500
+ /**
501
+ * Narrow a stale build set to the NEWEST merge commit it covers
502
+ * (W-msaux21y00g79c48).
503
+ *
504
+ * `refs/pull/<n>/merge` accumulates builds for EVERY push to the PR and the
505
+ * poller reads `$top=25` of them, so classifying that whole list ORs a single
506
+ * historical failure across the PR's entire life: once any build failed, every
507
+ * later stale evaluation reported 'failing' forever, even after the newest
508
+ * merge commit built green and the failures had been fixed on-branch. Group by
509
+ * `sourceVersion` and keep only the most recently built generation, so the
510
+ * stale classification says "the most recent thing CI actually told us about
511
+ * this PR" instead of OR-ing across already-superseded history.
512
+ *
513
+ * Draft PRs feel this hardest: ADO does not auto-run branch policies on drafts
514
+ * (see queueDraftPrValidationBuilds), so a draft spends most of its life on an
515
+ * unbuilt merge commit — i.e. inside this stale path.
516
+ */
517
+ function selectNewestMergeCommitBuilds(allBuilds) {
518
+ const builds = Array.isArray(allBuilds) ? allBuilds : [];
519
+ if (builds.length < 2) return builds.slice();
520
+ const generations = new Map(); // sourceVersion → { key, builds }
521
+ builds.forEach((build, index) => {
522
+ const sourceVersion = String(build?.sourceVersion ?? '');
523
+ const key = buildRecencyKey(build, index);
524
+ const generation = generations.get(sourceVersion);
525
+ if (!generation) {
526
+ generations.set(sourceVersion, { key, builds: [build] });
527
+ return;
528
+ }
529
+ generation.builds.push(build);
530
+ if (compareBuildRecency(key, generation.key) > 0) generation.key = key;
531
+ });
532
+ if (generations.size < 2) return builds.slice();
533
+ let newest = null;
534
+ for (const generation of generations.values()) {
535
+ if (!newest || compareBuildRecency(generation.key, newest.key) > 0) newest = generation;
536
+ }
537
+ return newest.builds;
538
+ }
539
+
465
540
  /**
466
541
  * Stale merge-commit fallback classifier (Issue #2747). When ADO returns
467
542
  * builds on a PR's merge ref but none target the current merge commit
468
543
  * (ADO recomputed merge-commit after a target-branch advance / rebase
469
- * without re-queueing), classify the stale set:
544
+ * without re-queueing), classify the NEWEST merge commit in that stale set
545
+ * (W-msaux21y00g79c48 — older generations are superseded history):
470
546
  * - Stale-failing → 'failing' + buildStaleMergeCommit:true. A stale
471
547
  * failing signal is strictly more actionable than 'none'; a fix WI
472
548
  * either does real work (failure still in the branch) or verifies
473
549
  * green via push (rebase resolved it).
550
+ * - Stale-succeeded that contradicts a cached 'failing' → 'none' +
551
+ * supersededCachedFailure:true. The current merge commit is unbuilt, so
552
+ * the cached failure can only have come from an OLDER signal than the
553
+ * green one we just read; re-asserting it pins the PR to 'failing'
554
+ * forever and keeps dispatching build fixes against a green branch.
555
+ * 'none' (unknown) is the honest answer — 'passing' would be false
556
+ * confidence about a merge commit nothing has built.
474
557
  * - Stale-succeeded/other → preserve cached pr.buildStatus (or 'none')
475
558
  * and do NOT set the stale-merge flag. Succeeded-on-pre-rebase is
476
559
  * ambiguous and could be false confidence.
477
- * Returns { buildStatus, buildStaleMergeCommit }.
560
+ * Returns { buildStatus, buildStaleMergeCommit, supersededCachedFailure }.
478
561
  */
479
562
  function classifyStaleBuilds(allBuilds, cachedBuildStatus) {
480
- const staleStatus = classifyBuildStatus(allBuilds);
563
+ const staleStatus = classifyBuildStatus(selectNewestMergeCommitBuilds(allBuilds));
481
564
  if (staleStatus === BUILD_STATUS.FAILING) {
482
- return { buildStatus: BUILD_STATUS.FAILING, buildStaleMergeCommit: true };
565
+ return { buildStatus: BUILD_STATUS.FAILING, buildStaleMergeCommit: true, supersededCachedFailure: false };
566
+ }
567
+ if (staleStatus === BUILD_STATUS.PASSING && cachedBuildStatus === BUILD_STATUS.FAILING) {
568
+ return { buildStatus: BUILD_STATUS.NONE, buildStaleMergeCommit: false, supersededCachedFailure: true };
483
569
  }
484
- return { buildStatus: cachedBuildStatus || BUILD_STATUS.NONE, buildStaleMergeCommit: false };
570
+ return { buildStatus: cachedBuildStatus || BUILD_STATUS.NONE, buildStaleMergeCommit: false, supersededCachedFailure: false };
485
571
  }
486
572
 
487
573
  /**
@@ -526,6 +612,130 @@ async function queueFreshAdoBuild({ orgBase, project, prNumber, definitionId, to
526
612
  return adoFetch(url, token, { method: 'POST', body });
527
613
  }
528
614
 
615
+ // ─── Draft-PR branch-policy build queueing (W-ms7kklp8002zad24) ───────────────
616
+ //
617
+ // On Azure DevOps, branch build policies do NOT run automatically while a PR is
618
+ // a draft — the required policy builds sit un-queued, so a Minions-managed draft
619
+ // PR never gets a build status and the existing build-fix loop has nothing to
620
+ // act on. GitHub already fires CI on drafts, so this whole path is ADO-only.
621
+ //
622
+ // For a draft PR with buildStatus === NONE there are NO existing builds to read
623
+ // `build.definition.id` from, so the stale-requeue path cannot supply a
624
+ // definitionId. We instead DISCOVER the required build definition(s) from the
625
+ // target branch's ADO branch-policy configurations.
626
+
627
+ // ADO "Build" policy type id — every branch build policy carries this type.id.
628
+ const ADO_BUILD_POLICY_TYPE_ID = '0609b952-1397-4640-95ec-e00a01b2c241';
629
+
630
+ /**
631
+ * Does an ADO policy `settings.scope[]` entry apply to this PR's target branch
632
+ * (and repo)? ADO scope entries carry `{ repositoryId, refName, matchKind }`:
633
+ * - `repositoryId` null/empty → the policy applies to every repo in the
634
+ * project; a concrete GUID must match this PR's repo (case-insensitive).
635
+ * - `matchKind` 'Exact' (default) → target refName must equal the scope
636
+ * refName; 'Prefix' → target refName must start with the scope refName
637
+ * (ADO's prefix branch policies, e.g. scope `refs/heads/release/` covering
638
+ * `refs/heads/release/x`).
639
+ */
640
+ function adoPolicyScopeMatches(scopeEntry, targetRefName, repositoryId) {
641
+ if (!scopeEntry || typeof scopeEntry !== 'object') return false;
642
+ const scopeRepo = String(scopeEntry.repositoryId || '').trim();
643
+ if (scopeRepo) {
644
+ const prRepo = String(repositoryId || '').trim();
645
+ if (!prRepo || scopeRepo.toLowerCase() !== prRepo.toLowerCase()) return false;
646
+ }
647
+ const scopeRef = String(scopeEntry.refName || '').trim();
648
+ if (!scopeRef) return false; // no branch scope → not a branch-targeted policy we can match
649
+ const target = String(targetRefName || '').trim();
650
+ if (!target) return false;
651
+ const matchKind = String(scopeEntry.matchKind || 'exact').toLowerCase();
652
+ if (matchKind === 'prefix') return target === scopeRef || target.startsWith(scopeRef);
653
+ return target === scopeRef; // Exact (default)
654
+ }
655
+
656
+ /**
657
+ * Parse an ADO policy/configurations `value[]` into the DISTINCT set of required
658
+ * build definition ids that apply to `targetRefName` on `repositoryId`. Only
659
+ * enabled + blocking (required) Build policies whose scope matches the target
660
+ * branch + repo are considered. Pure/testable — no network. Returns number[].
661
+ */
662
+ function extractDraftPrBuildDefinitionIds(policyConfigs, targetRefName, repositoryId) {
663
+ const ids = [];
664
+ const seen = new Set();
665
+ if (!Array.isArray(policyConfigs)) return ids;
666
+ for (const cfg of policyConfigs) {
667
+ if (!cfg || typeof cfg !== 'object') continue;
668
+ if (String(cfg?.type?.id || '').toLowerCase() !== ADO_BUILD_POLICY_TYPE_ID) continue;
669
+ if (cfg.isEnabled !== true || cfg.isBlocking !== true) continue;
670
+ const settings = cfg.settings || {};
671
+ const rawDefId = settings.buildDefinitionId;
672
+ if (rawDefId == null || rawDefId === '') continue;
673
+ if (typeof rawDefId !== 'number' && typeof rawDefId !== 'string') continue;
674
+ // ADO normally serializes buildDefinitionId as a number, but a string id
675
+ // would silently defeat a raw Set dedupe (1234 !== '1234') and queue the
676
+ // SAME pipeline twice. Normalize before comparing and before emitting.
677
+ const defId = Number(rawDefId);
678
+ if (!Number.isFinite(defId)) continue;
679
+ const scopes = Array.isArray(settings.scope) ? settings.scope : [];
680
+ if (!scopes.some(s => adoPolicyScopeMatches(s, targetRefName, repositoryId))) continue;
681
+ if (seen.has(defId)) continue;
682
+ seen.add(defId);
683
+ ids.push(defId);
684
+ }
685
+ return ids;
686
+ }
687
+
688
+ /**
689
+ * Discover the required branch-policy build definitions for a draft PR's target
690
+ * branch and queue a fresh validation build for each distinct definition against
691
+ * refs/pull/<prNumber>/merge. Non-fatal: the policy-config fetch and every queue
692
+ * POST are wrapped so a failure logs a warn and returns without throwing (mirrors
693
+ * queueFreshAdoBuild's non-fatal contract).
694
+ *
695
+ * Returns `{ queued, discovered }`:
696
+ * - `queued` — how many builds were actually POSTed.
697
+ * - `discovered` — whether policy discovery COMPLETED and produced a definitive
698
+ * answer for this branch. True even when zero definitions matched, because
699
+ * "this branch has no required Build policies" is a stable result: the caller
700
+ * uses it to close the 30-min rate-limit window so it stops re-GETting
701
+ * policy/configurations on every single poll. False only when we short-circuit
702
+ * on a malformed PR or the policy fetch itself failed (transient — the caller
703
+ * leaves the window open and retries on the next poll).
704
+ */
705
+ async function queueDraftPrValidationBuilds(pr, project, token, orgBase) {
706
+ const prNumber = pr?.prNumber;
707
+ const targetRefName = pr?.targetRefName;
708
+ if (!prNumber || !targetRefName) return { queued: 0, discovered: false };
709
+ const repositoryId = getConfiguredAdoRepositoryGuid(project) || getAdoRepositoryId(project);
710
+
711
+ let defIds = [];
712
+ try {
713
+ const url = `${orgBase}/${project.adoProject}/_apis/policy/configurations?api-version=7.1`;
714
+ const data = await adoFetch(url, token);
715
+ defIds = extractDraftPrBuildDefinitionIds(data?.value || [], targetRefName, repositoryId);
716
+ } catch (e) {
717
+ log('warn', `PR ${pr.id} draft-build: failed to fetch policy configurations: ${e.message}`);
718
+ return { queued: 0, discovered: false };
719
+ }
720
+
721
+ if (defIds.length === 0) {
722
+ log('info', `PR ${pr.id} draft-build: no required Build policies matched target ${targetRefName}; nothing to queue`);
723
+ return { queued: 0, discovered: true };
724
+ }
725
+
726
+ let queued = 0;
727
+ for (const definitionId of defIds) {
728
+ try {
729
+ await queueFreshAdoBuild({ orgBase, project, prNumber, definitionId, token });
730
+ queued++;
731
+ log('info', `PR ${pr.id} draft-build: queued validation build for definition id=${definitionId} on refs/pull/${prNumber}/merge (draft PR — ADO does not auto-trigger policy builds)`);
732
+ } catch (e) {
733
+ log('warn', `PR ${pr.id} draft-build: failed to queue validation build for definition id=${definitionId}: ${e.message}`);
734
+ }
735
+ }
736
+ return { queued, discovered: true };
737
+ }
738
+
529
739
  /** Map ADO reviewer vote array to a review status string. */
530
740
  function votesToReviewStatus(votes) {
531
741
  if (votes.some(v => v === -10)) return REVIEW_STATUS.CHANGES_REQUESTED;
@@ -589,8 +799,8 @@ function _voteSnapshotsPath() {
589
799
  // Resolve lazily so tests that swap MINIONS_DIR via createTestMinionsDir()
590
800
  // see the fresh value. shared.MINIONS_DIR is reassigned when MINIONS_TEST_DIR
591
801
  // is set, but only when shared is re-required — which the helper does via
592
- // `require('./shared').MINIONS_DIR`.
593
- return path.join(require('./shared').MINIONS_DIR, 'engine', VOTE_SNAPSHOT_FILE);
802
+ // `require('../core/shared').MINIONS_DIR`.
803
+ return path.join(require('../core/shared').MINIONS_DIR, 'engine', VOTE_SNAPSHOT_FILE);
594
804
  }
595
805
 
596
806
  function _normalizeSnapshotKey(prId, reviewerId) {
@@ -760,7 +970,7 @@ function _scanStuckVote({ pr, reviewerId, vote, sourceCommit, snapshot, now }) {
760
970
  function _hasPendingReReviewWi(pr) {
761
971
  if (!pr?.id) return false;
762
972
  try {
763
- const dispatchData = require('./queries').getDispatch();
973
+ const dispatchData = require('../core/queries').getDispatch();
764
974
  const all = [...(dispatchData?.pending || []), ...(dispatchData?.active || [])];
765
975
  return all.some(d => {
766
976
  if (d?.type !== 'review') return false;
@@ -1203,16 +1413,26 @@ async function fetchAdoBuildErrorLog(orgBase, project, failedStatus, token, pr,
1203
1413
  /**
1204
1414
  * Iterate active PRs across all projects. Calls `callback(project, pr, prNum, orgBase, adoRepositoryId)`
1205
1415
  * for each active PR. If callback returns truthy, the PR file is saved after the project loop.
1416
+ *
1417
+ * @param {object} [options]
1418
+ * @param {number} [options.abandonedRepollWindowMs] — W-msav5edk00g81710. Mirror of
1419
+ * engine/providers/github.js:forEachActiveGhPr. Opt-in bounded re-probe of
1420
+ * recently-abandoned records so a reopened PR can leave the terminal stamp.
1421
+ * Defaults to 0 (strict PR_POLLABLE_STATUSES) — only the status poll opts in.
1206
1422
  */
1207
- async function forEachActivePr(config, token, callback) {
1423
+ async function forEachActivePr(config, token, callback, options = {}) {
1208
1424
  const projects = shared.getProjects(config);
1209
1425
  let totalUpdated = 0;
1426
+ const pollOpts = {
1427
+ windowMs: Number(options.abandonedRepollWindowMs) || 0,
1428
+ now: Date.now(),
1429
+ };
1210
1430
 
1211
1431
  for (const project of projects) {
1212
1432
  if (isGitHubProject(project)) continue;
1213
1433
  // W-mp625n27000m6e78 — Honor per-project workSources.pullRequests.enabled.
1214
1434
  // Default-ON: only skip when explicitly disabled (=== false). Same gate as
1215
- // engine/github.js:forEachActiveGhPr — without this, disabling PR polling
1435
+ // engine/providers/github.js:forEachActiveGhPr — without this, disabling PR polling
1216
1436
  // for an ADO project via the dashboard Settings UI was cosmetic.
1217
1437
  const src = project?.workSources?.pullRequests || config?.workSources?.pullRequests;
1218
1438
  if (src && src.enabled === false) continue;
@@ -1221,7 +1441,7 @@ async function forEachActivePr(config, token, callback) {
1221
1441
  if (!project.adoOrg || !project.adoProject) continue;
1222
1442
 
1223
1443
  const prs = getPrs(project);
1224
- const activePrs = prs.filter(pr => shared.PR_POLLABLE_STATUSES.has(pr.status)
1444
+ const activePrs = prs.filter(pr => shared.isPrPollable(pr, pollOpts)
1225
1445
  && shared.isPrCompatibleWithProject(project, pr, pr.url || ''));
1226
1446
  if (activePrs.length === 0) continue;
1227
1447
 
@@ -1309,13 +1529,13 @@ async function forEachActivePr(config, token, callback) {
1309
1529
  }
1310
1530
 
1311
1531
  // BUG-C2 — Also poll manually linked ADO PRs from the central PR store.
1312
- // Mirror of engine/github.js:540-633. Without this block, any ADO PR enrolled
1532
+ // Mirror of engine/providers/github.js:540-633. Without this block, any ADO PR enrolled
1313
1533
  // into the central PR store via the dashboard attach UI or
1314
1534
  // lifecycle.enrollPrFromCanonicalId is never polled (status, build, review,
1315
1535
  // comments, abandoned-reconciliation all freeze permanently).
1316
1536
  //
1317
1537
  // MIRROR-SYMMETRY CONTRACT: any future divergence between
1318
- // engine/github.js:540-633 and this branch is itself a bug class. The
1538
+ // engine/providers/github.js:540-633 and this branch is itself a bug class. The
1319
1539
  // canonical 'mirror-ado-github-poll-fix' skill applies. Keep filter, gating,
1320
1540
  // per-PR iteration, and merge-back semantics symmetric.
1321
1541
  //
@@ -1340,7 +1560,7 @@ async function forEachActivePr(config, token, callback) {
1340
1560
  const configuredAdoProjects = projects.filter(p => !isGitHubProject(p) && p.adoOrg && p.adoProject);
1341
1561
 
1342
1562
  const activeCentral = centralPrs.filter(pr => {
1343
- if (!shared.PR_POLLABLE_STATUSES.has(pr.status)) return false;
1563
+ if (!shared.isPrPollable(pr, pollOpts)) return false;
1344
1564
  if (String(pr.repoHost || 'ado').toLowerCase() !== 'ado') return false;
1345
1565
  if (!parseCanonicalAdoPrId(pr.id, 'central ADO PR poll')) return false;
1346
1566
  // Only defer to the project loop when the PR is ACTUALLY present in that
@@ -1473,13 +1693,13 @@ async function pollPrStatus(config) {
1473
1693
 
1474
1694
  let newStatus = pr.status;
1475
1695
  // W-mp5trwh60008386d: ADO does NOT need the `prAbandonConfirmCount` confirmation logic
1476
- // that engine/github.js uses. ADO only flips a PR to `abandoned` when a *successful*
1696
+ // that engine/providers/github.js uses. ADO only flips a PR to `abandoned` when a *successful*
1477
1697
  // adoFetch returns `prData.status === 'abandoned'` (below). On 404/auth failure adoFetch
1478
1698
  // throws, the throw propagates to forEachActivePr's `Promise.allSettled`, and the PR
1479
1699
  // record is left untouched — equivalent to "infinite confirmations required". The shared
1480
- // ENGINE_DEFAULTS.prAbandonConfirmCount constant lives in engine/shared.js so a future
1700
+ // ENGINE_DEFAULTS.prAbandonConfirmCount constant lives in engine/core/shared.js so a future
1481
1701
  // ADO change can adopt the same semantics if the failure model ever maps 404 to
1482
- // abandonment directly. See engine/github.js pollPrStatus for the GitHub implementation.
1702
+ // abandonment directly. See engine/providers/github.js pollPrStatus for the GitHub implementation.
1483
1703
  if (prData.status === 'completed') newStatus = PR_STATUS.MERGED;
1484
1704
  else if (prData.status === 'abandoned') newStatus = PR_STATUS.ABANDONED;
1485
1705
  else if (prData.status === 'active') newStatus = PR_STATUS.ACTIVE;
@@ -1501,6 +1721,12 @@ async function pollPrStatus(config) {
1501
1721
  if ((newStatus === PR_STATUS.ABANDONED || newStatus === PR_STATUS.CLOSED) && !pr.closedAt) {
1502
1722
  pr.closedAt = prData.closedDate || ts();
1503
1723
  }
1724
+ // W-msav5edk00g81710: anchor the bounded re-probe window (mirror of
1725
+ // engine/providers/github.js). ADO abandonment is fully reversible via
1726
+ // "Reactivate", so the record must stay observable for a bounded window.
1727
+ if (newStatus === PR_STATUS.ABANDONED) {
1728
+ shared.stampPrAbandonedAt(pr, prData.closedDate || pr.closedAt || ts());
1729
+ }
1504
1730
 
1505
1731
  if (newStatus === PR_STATUS.MERGED || newStatus === PR_STATUS.ABANDONED) {
1506
1732
  if (pr.reviewStatus === REVIEW_STATUS.WAITING) {
@@ -1537,7 +1763,7 @@ async function pollPrStatus(config) {
1537
1763
  // references it. Invariant #14 ('approved is permanent terminal') is
1538
1764
  // preserved: we keep reviewStatus when it was 'approved' rather than
1539
1765
  // carving a wasReopened exception into the 8+ guard sites in
1540
- // engine/lifecycle.js, engine.js, engine/github.js, and engine/ado.js.
1766
+ // engine/orchestration/lifecycle.js, engine.js, engine/providers/github.js, and engine/ado/index.js.
1541
1767
  // Re-review safety on new commits is handled by the pre-existing
1542
1768
  // pr.lastPushedAt > pr.lastReviewedAt path (engine.js alreadyReviewed
1543
1769
  // gate) plus the changes-requested override in lifecycle.js.
@@ -1550,6 +1776,10 @@ async function pollPrStatus(config) {
1550
1776
  // F6 not yet shipped (field absent).
1551
1777
  delete pr.humanFeedback.editsSeen;
1552
1778
  pr.fixDispatched = false;
1779
+ // W-msav5edk00g81710: record left `abandoned` — drop the re-probe
1780
+ // anchor and the stale close timestamp it falls back to.
1781
+ shared.clearPrAbandonedAt(pr);
1782
+ delete pr.closedAt;
1553
1783
  if (pr.reviewStatus !== REVIEW_STATUS.APPROVED) pr.reviewStatus = REVIEW_STATUS.PENDING;
1554
1784
  log('info', `PR ${pr.id} reopened — reset transient state (reviewStatus=${pr.reviewStatus})`);
1555
1785
  }
@@ -1578,7 +1808,7 @@ async function pollPrStatus(config) {
1578
1808
  }
1579
1809
 
1580
1810
  // P-w1a3f9b2 — Phase 1.1: plumb mergeable / isDraft / mergeStatus /
1581
- // headRefOid onto the PR object so watches captureState (engine/watches.js)
1811
+ // headRefOid onto the PR object so watches captureState (engine/watches/index.js)
1582
1812
  // and future predicates (Phase 2.1: head-commit-change, mergeable-flipped,
1583
1813
  // ready-for-merge, draft-flipped) can read them.
1584
1814
  //
@@ -1787,15 +2017,21 @@ async function pollPrStatus(config) {
1787
2017
  // builds for this PR's merge ref but none target the current
1788
2018
  // `mergeCommitId` — most likely the target branch moved (rebase
1789
2019
  // or push), ADO recomputed the merge commit, but no new
1790
- // source-side changes triggered a rebuild. Classify the stale
1791
- // set: stale-failing flips to 'failing' + buildStaleMergeCommit
1792
- // (strictly more actionable than 'none'); stale-succeeded/other
1793
- // preserves cached state (no false confidence).
2020
+ // source-side changes triggered a rebuild. Classify the NEWEST
2021
+ // merge commit in that stale set (W-msaux21y00g79c48): stale-
2022
+ // failing flips to 'failing' + buildStaleMergeCommit (strictly
2023
+ // more actionable than 'none'); stale-succeeded/other preserves
2024
+ // cached state (no false confidence) unless it contradicts a
2025
+ // cached failure, which it then drops.
1794
2026
  const stale = classifyStaleBuilds(allBuilds, pr.buildStatus);
1795
2027
  buildStatus = stale.buildStatus;
1796
2028
  buildStaleMergeCommit = stale.buildStaleMergeCommit;
2029
+ // Same narrowing classifyStaleBuilds applies, so the reported
2030
+ // reason and the re-queued definitions describe the generation we
2031
+ // actually classified rather than a superseded one.
2032
+ const newestStaleBuilds = selectNewestMergeCommitBuilds(allBuilds);
1797
2033
  if (buildStaleMergeCommit) {
1798
- const failed = allBuilds.find(b => b.result === 'failed') || allBuilds.find(b => b.result === 'canceled');
2034
+ const failed = newestStaleBuilds.find(b => b.result === 'failed') || newestStaleBuilds.find(b => b.result === 'canceled');
1799
2035
  buildFailReason = failed?.definition?.name || 'Build failed (stale merge commit)';
1800
2036
  buildFailureSignature = shared.safeSlugComponent([
1801
2037
  failed?.definition?.name,
@@ -1805,7 +2041,7 @@ async function pollPrStatus(config) {
1805
2041
  ].filter(Boolean).join('\n') || buildFailReason, 80);
1806
2042
  // Collect PR-validation definitions with failures for fresh-build queue.
1807
2043
  const seenDefIds = new Set();
1808
- for (const b of allBuilds) {
2044
+ for (const b of newestStaleBuilds) {
1809
2045
  if ((b.result === 'failed' || b.result === 'canceled') && isPrValidationBuild(b)) {
1810
2046
  const defId = b?.definition?.id;
1811
2047
  if (defId != null && !seenDefIds.has(defId)) {
@@ -1817,8 +2053,8 @@ async function pollPrStatus(config) {
1817
2053
  } else if (pr.buildFailReason && buildStatus === pr.buildStatus) {
1818
2054
  buildFailReason = pr.buildFailReason;
1819
2055
  }
1820
- const sampleSv = (allBuilds[0]?.sourceVersion || '').slice(0, 8);
1821
- log('warn', `PR ${pr.id} build: merge-commit mismatch — ${allBuilds.length} build(s) on merge ref, none target current merge commit ${String(mergeCommitId).slice(0, 8)} (sample sourceVersion ${sampleSv}); classified '${buildStatus}' from stale set${buildStaleMergeCommit ? ' (stale failing — will queue fresh build if rate limit allows)' : ''}`);
2056
+ const sampleSv = (newestStaleBuilds[0]?.sourceVersion || '').slice(0, 8);
2057
+ log('warn', `PR ${pr.id} build: merge-commit mismatch — ${allBuilds.length} build(s) on merge ref, none target current merge commit ${String(mergeCommitId).slice(0, 8)} (newest built merge commit ${sampleSv}, ${newestStaleBuilds.length} build(s)); classified '${buildStatus}' from that generation${buildStaleMergeCommit ? ' (stale failing — will queue fresh build if rate limit allows)' : ''}${stale.supersededCachedFailure ? ` (dropped cached '${BUILD_STATUS.FAILING}' — superseded by the newer green build)` : ''}`);
1822
2058
  }
1823
2059
  } catch (e) {
1824
2060
  buildStatusResolved = false;
@@ -1930,6 +2166,43 @@ async function pollPrStatus(config) {
1930
2166
  }
1931
2167
  }
1932
2168
 
2169
+ // Draft-PR branch-policy build queueing (W-ms7kklp8002zad24). ADO does NOT
2170
+ // auto-trigger branch build policies while a PR is a draft, so an
2171
+ // auto-managed draft PR sits at buildStatus === NONE forever and the
2172
+ // build-fix loop has nothing to act on. Discover the required Build
2173
+ // policies from the target branch and queue a fresh build per distinct
2174
+ // definition against refs/pull/<n>/merge; the next poll then picks up
2175
+ // RUNNING/FAILING/PASSING via the existing classifyBuildStatus path and the
2176
+ // existing auto-fix loop iterates. Gated by engine.adoQueueDraftPrBuilds
2177
+ // (default true). Rate-limited via the shared _lastBuildRequeueAt window and
2178
+ // restricted to auto-managed (non-context-only) PRs. Non-fatal by contract.
2179
+ const adoQueueDraftPrBuilds = config?.engine?.adoQueueDraftPrBuilds ?? shared.ENGINE_DEFAULTS.adoQueueDraftPrBuilds;
2180
+ if (adoQueueDraftPrBuilds && buildStatusResolved && buildStatus === BUILD_STATUS.NONE
2181
+ && pr.isDraft === true && shared.isAutoManagedPrRecord(pr)) {
2182
+ if (shouldRequeueStaleBuild(pr)) {
2183
+ try {
2184
+ const { queued, discovered } = await queueDraftPrValidationBuilds(pr, project, token, orgBase);
2185
+ // Close the shared rate-limit window whenever policy discovery
2186
+ // COMPLETED — including the zero-match case. A target branch with no
2187
+ // required Build policies is a stable answer, so stamping only on
2188
+ // queued > 0 left every subsequent poll re-GETting
2189
+ // policy/configurations for that PR forever (PR #1099 review). A
2190
+ // transient policy-fetch failure reports discovered:false and
2191
+ // deliberately leaves the window open so the next poll retries.
2192
+ if (discovered) {
2193
+ pr._lastBuildRequeueAt = Date.now();
2194
+ updated = true; // persist the stamp, otherwise the back-off is lost
2195
+ }
2196
+ } catch (e) {
2197
+ log('warn', `PR ${pr.id} draft-build: queueDraftPrValidationBuilds threw unexpectedly: ${e.message}`);
2198
+ }
2199
+ } else {
2200
+ const lastAt = Number(pr._lastBuildRequeueAt) || 0;
2201
+ const ageMin = Math.floor((Date.now() - lastAt) / 60000);
2202
+ log('info', `PR ${pr.id} draft-build: buildStatus none on draft PR but skipping queue — last requeue ${ageMin}m ago (rate limit: 30 min)`);
2203
+ }
2204
+ }
2205
+
1933
2206
  // Auto-complete: set auto-complete on PR when builds green + review approved
1934
2207
  if (pr.status === PR_STATUS.ACTIVE && pr.reviewStatus === REVIEW_STATUS.APPROVED && pr.buildStatus === BUILD_STATUS.PASSING && !pr._autoCompleted) {
1935
2208
  const autoComplete = config.engine?.autoCompletePrs === true; // opt-in
@@ -1995,6 +2268,11 @@ async function pollPrStatus(config) {
1995
2268
  }
1996
2269
  throw err; // re-throw non-auth errors for forEachActivePr to handle
1997
2270
  }
2271
+ }, {
2272
+ // W-msav5edk00g81710 — only the STATUS poll re-probes recently-abandoned
2273
+ // records (mirror of engine/providers/github.js#pollPrStatus). ADO's
2274
+ // "Reactivate" is the exact analogue of a GitHub reopen.
2275
+ abandonedRepollWindowMs: shared.resolveAbandonedPrRepollWindowMs(config),
1998
2276
  });
1999
2277
 
2000
2278
  if (totalUpdated > 0) {
@@ -2004,6 +2282,32 @@ async function pollPrStatus(config) {
2004
2282
 
2005
2283
  // ─── Poll Human Comments on PRs ──────────────────────────────────────────────
2006
2284
 
2285
+ // W-msbf1kf6033f46cb — acceptance criterion 2: an unconfigured
2286
+ // `config.engine.minionsAdoIdentities` leaves the marker spoof guard inactive
2287
+ // (marker-presence alone then classifies as agent-authored). That is the safe
2288
+ // default, but it must not be SILENT — an operator who assumed the spoof guard
2289
+ // was live deserves to be told it is not. Latched once per process because
2290
+ // pollPrHumanComments runs every `prPollCommentsEvery` ticks over every active
2291
+ // ADO PR; warning per comment would bury the engine log.
2292
+ let _adoMarkerSpoofGuardWarned = false;
2293
+
2294
+ function _warnAdoMarkerSpoofGuardInactiveOnce() {
2295
+ if (_adoMarkerSpoofGuardWarned) return;
2296
+ _adoMarkerSpoofGuardWarned = true;
2297
+ log('warn',
2298
+ 'ADO comment classifier: config.engine.minionsAdoIdentities is empty — the Minions marker '
2299
+ + 'spoof guard is INACTIVE. Marker-bearing comments are trusted as agent-authored without an '
2300
+ + 'author cross-check (fail-safe: prevents agent fix-summaries from re-triggering fix '
2301
+ + 'dispatches). Populate engine.minionsAdoIdentities with the ADO identities Minions posts '
2302
+ + 'under to re-enable the cross-check.');
2303
+ }
2304
+
2305
+ // Test hook: clears the once-per-process latch so a test can assert the
2306
+ // warning fires exactly once.
2307
+ function _resetAdoMarkerSpoofGuardWarningForTest() {
2308
+ _adoMarkerSpoofGuardWarned = false;
2309
+ }
2310
+
2007
2311
  async function pollPrHumanComments(config) {
2008
2312
  // #3758 — inert when no project actually uses ADO.
2009
2313
  if (!hasAdoProjectConfigured(config)) return;
@@ -2024,7 +2328,7 @@ async function pollPrHumanComments(config) {
2024
2328
  // including bot/CI bodies and explicitly-ignored authors that get filtered
2025
2329
  // out before reaching `allHumanComments`. We need their dates so the
2026
2330
  // cutoff can advance past them on every successful poll, mirroring the
2027
- // GitHub poller convention (engine/github.js: `allCommentDates`). Without
2331
+ // GitHub poller convention (engine/providers/github.js: `allCommentDates`). Without
2028
2332
  // this, ignored/CI comments are re-fetched and re-filtered every tick.
2029
2333
  const allHumanComments = [];
2030
2334
  const newHumanComments = [];
@@ -2042,20 +2346,28 @@ async function pollPrHumanComments(config) {
2042
2346
  // P-f23classifier (F3, security note): ADO has no `viewerDidAuthor`
2043
2347
  // equivalent. A human pasting our `<!-- minions:agent=... -->` marker
2044
2348
  // (e.g. by copy-paste from a fix-summary) would otherwise be
2045
- // misclassified as agent-authored. Cross-check marker-presence against
2046
- // the configured Minions identity (uniqueName / displayName / id /
2047
- // descriptor) so marker-presence alone NEVER suffices on ADO. When
2048
- // `minionsAdoIdentities` is empty (default), marker-presence is ignored
2049
- // entirely on ADO — the body still has to match one of the legacy
2050
- // regexes (Minions(...), by Minions, [minions], VERDICT:) to classify
2051
- // as agent-authored.
2349
+ // misclassified as agent-authored. `config.engine.minionsAdoIdentities`
2350
+ // (uniqueName / displayName / id / descriptor) is the ADO stand-in for
2351
+ // that identity signal.
2352
+ //
2353
+ // W-msbf1kf6033f46cb: an EMPTY allowlist means the identity signal is
2354
+ // UNAVAILABLE, not that the author is someone else. Those two used to
2355
+ // collapse to the same answer here, so with the default empty allowlist
2356
+ // `markerAuthored` was ALWAYS false and the hidden marker had zero
2357
+ // filtering power on ADO. Every agent fix-summary was then reclassified
2358
+ // as new human feedback and re-triggered a fix dispatch on the same PR
2359
+ // (ADO PR 5530936: ten consecutive zero-change dispatches). Resolution
2360
+ // now runs through the shared `classifyMarkerAuthorship` seam, which
2361
+ // fails SAFE on an unavailable signal while keeping the spoof guard at
2362
+ // full strength on a real mismatch. See the module header of
2363
+ // engine/providers/comment-classifier.js for the cost asymmetry.
2052
2364
  const minionsAdoIdentities = new Set(
2053
2365
  (Array.isArray(config.engine?.minionsAdoIdentities) ? config.engine.minionsAdoIdentities : [])
2054
2366
  .map(s => String(s || '').toLowerCase())
2055
2367
  .filter(Boolean)
2056
2368
  );
2369
+ const adoIdentityAvailable = minionsAdoIdentities.size > 0;
2057
2370
  const isAdoMinionsAuthor = (comment) => {
2058
- if (minionsAdoIdentities.size === 0) return false;
2059
2371
  const author = comment?.author || {};
2060
2372
  const candidates = [author.uniqueName, author.displayName, author.id, author.descriptor];
2061
2373
  for (const cand of candidates) {
@@ -2071,7 +2383,7 @@ async function pollPrHumanComments(config) {
2071
2383
  // fresh human replies (humans do reply on resolved threads after a
2072
2384
  // teammate marks them done). Classification below — ignoredAuthors,
2073
2385
  // CI/coverage report regex, agent (Minions) detection — runs on every
2074
- // collected comment, mirroring engine/github.js pollPrHumanComments
2386
+ // collected comment, mirroring engine/providers/github.js pollPrHumanComments
2075
2387
  // which has never filtered by thread state.
2076
2388
  for (const comment of (thread.comments || [])) {
2077
2389
  if (!comment.content || comment.commentType === 'system') continue;
@@ -2110,13 +2422,28 @@ async function pollPrHumanComments(config) {
2110
2422
  if (automationReason) continue;
2111
2423
  // P-f23classifier (F3): detect agent comments (included in context
2112
2424
  // but don't trigger fix). Structurally symmetric to
2113
- // engine/github.js _isAgentComment + _isMinionsAuthoredComment:
2114
- // - marker-presence + ADO identity cross-check (spoof guard), OR
2425
+ // engine/providers/github.js _isAgentComment + _isMinionsAuthoredComment:
2426
+ // - marker authorship via the shared classifyMarkerAuthorship seam
2427
+ // (marker + ADO identity signal, fail-safe when unconfigured), OR
2115
2428
  // - legacy body patterns (Minions(...), by Minions, [minions]), OR
2116
2429
  // - hasVerdictPrefix(content) — engine-owned structured-output
2117
2430
  // contract emitted by playbooks/review.md.
2118
- const markerAuthored = hasMinionsMarker(content) && isAdoMinionsAuthor(comment);
2119
- const isAgent = markerAuthored
2431
+ //
2432
+ // W-msbf1kf6033f46cb: the legacy patterns are a weak fallback, not a
2433
+ // safety net. `/\bMinions\s*\(/i` does NOT match `[Minions](url)` and
2434
+ // `/\bby\s+Minions\b/i` does NOT match `by [Minions]` (the bracket
2435
+ // intervenes), so on ADO only a literal `[Minions]` ever fires. An
2436
+ // agent posting through the raw `az`/REST fallback omits that brand
2437
+ // sign-off entirely — the marker is what has to carry the weight.
2438
+ const markerAuthorship = classifyMarkerAuthorship({
2439
+ body: content,
2440
+ identityAvailable: adoIdentityAvailable,
2441
+ identityMatches: adoIdentityAvailable && isAdoMinionsAuthor(comment),
2442
+ });
2443
+ if (markerAuthorship === MARKER_AUTHORSHIP.IDENTITY_UNAVAILABLE) {
2444
+ _warnAdoMarkerSpoofGuardInactiveOnce();
2445
+ }
2446
+ const isAgent = isMarkerAuthored(markerAuthorship)
2120
2447
  || /\bMinions\s*\(/i.test(content)
2121
2448
  || /\bby\s+Minions\b/i.test(content)
2122
2449
  || /\[minions\]/i.test(content)
@@ -2359,7 +2686,8 @@ async function reconcilePrs(config) {
2359
2686
  id: prId,
2360
2687
  prNumber: adoPr.pullRequestId,
2361
2688
  title: (adoPr.title || `PR #${adoPr.pullRequestId}`).slice(0, 120),
2362
- agent: (linkedItem?.dispatched_to || adoPr.createdBy?.displayName || 'unknown').toLowerCase(),
2689
+ agent: (linkedItem?.dispatched_to || shared.PR_AGENT_HUMAN).toLowerCase(),
2690
+ author: shared.buildAdoAuthorIdentity(adoPr.createdBy),
2363
2691
  branch,
2364
2692
  reviewStatus: REVIEW_STATUS.PENDING,
2365
2693
  status: PR_STATUS.ACTIVE,
@@ -2403,7 +2731,8 @@ async function reconcilePrs(config) {
2403
2731
  id: prId,
2404
2732
  prNumber: adoPr.pullRequestId,
2405
2733
  title: (adoPr.title || `PR #${adoPr.pullRequestId}`).slice(0, 120),
2406
- agent: (linkedItem?.dispatched_to || adoPr.createdBy?.displayName || 'unknown').toLowerCase(),
2734
+ agent: (linkedItem?.dispatched_to || shared.PR_AGENT_HUMAN).toLowerCase(),
2735
+ author: shared.buildAdoAuthorIdentity(adoPr.createdBy),
2407
2736
  branch,
2408
2737
  reviewStatus: REVIEW_STATUS.PENDING,
2409
2738
  status: PR_STATUS.ACTIVE,
@@ -2502,7 +2831,7 @@ async function checkLiveReviewStatus(pr, project) {
2502
2831
  *
2503
2832
  * Ensures the *authenticated reviewer's own* vote is positive (+10) when an agent
2504
2833
  * flips their verdict from request_changes → approved on a re-review. Mirrors the
2505
- * existing target-branch re-approval pattern at engine/ado.js:837-852 (PUT
2834
+ * existing target-branch re-approval pattern in engine/ado/index.js (PUT
2506
2835
  * reviewers/{myId} {vote:10}). Covers both:
2507
2836
  * - prior negative vote (-5 wait-for-author, -10 rejected) → cleared to +10, and
2508
2837
  * - prior neutral vote (0 — never cast, or reset by another path and never
@@ -2604,7 +2933,10 @@ async function resetReviewerNegativeVote(pr, project) {
2604
2933
  * buildStatus" semantics from issue #1233; the caller must trust the cached
2605
2934
  * value). When the stale set IS failing, buildStatus flips to 'failing' and
2606
2935
  * `buildStaleMergeCommit: true` is returned (Issue #2747) — a stale failing
2607
- * signal is strictly more actionable than cache fallback.
2936
+ * signal is strictly more actionable than cache fallback. When the newest built
2937
+ * merge commit PASSED but the cached status is 'failing', buildStatus is 'none'
2938
+ * (W-msaux21y00g79c48): the cached failure is provably older than that green
2939
+ * build, so the caller must skip the build-fix instead of falling back to it.
2608
2940
  */
2609
2941
  async function checkLiveBuildAndConflict(pr, project) {
2610
2942
  try {
@@ -2668,6 +3000,14 @@ async function checkLiveBuildAndConflict(pr, project) {
2668
3000
  if (stale.buildStaleMergeCommit) {
2669
3001
  buildStatus = stale.buildStatus;
2670
3002
  buildStaleMergeCommit = true;
3003
+ } else if (stale.supersededCachedFailure) {
3004
+ // W-msaux21y00g79c48: the newest built merge commit passed, so
3005
+ // the cached 'failing' the caller would otherwise fall back to
3006
+ // is a superseded signal. Report 'none' (not null) so the
3007
+ // pre-dispatch gate skips the build-fix and clears the cache
3008
+ // instead of re-dispatching against a green branch. Not
3009
+ // 'passing' — the current merge commit still has no build.
3010
+ buildStatus = stale.buildStatus;
2671
3011
  }
2672
3012
  // else: leave buildStatus null — caller falls back to cached state (issue #1233).
2673
3013
  }
@@ -2710,6 +3050,9 @@ async function fetchAdoPrMetadata(prNum, adoOrg, adoProj, adoRepo, project) {
2710
3050
  description: pr.description || '',
2711
3051
  branch: stripRefsHeads(pr.sourceRefName),
2712
3052
  author: pr.createdBy?.displayName || '',
3053
+ // Structured author identity (provider/id/descriptor/login/displayName/url)
3054
+ // for callers that persist pr.author separately from pr.agent.
3055
+ authorIdentity: shared.buildAdoAuthorIdentity(pr.createdBy),
2713
3056
  // W-mrezh0yb0007f733 — surface ADO's real creationDate so manual-link paths
2714
3057
  // can seed `pr.created` with the true value instead of link-time now().
2715
3058
  created: pr.creationDate || '',
@@ -2771,7 +3114,7 @@ async function getPrChangedFiles(project, prNumber) {
2771
3114
 
2772
3115
  /**
2773
3116
  * Fetch live PR and build status for a single PR number.
2774
- * Used by engine/ado-status.js so agents can check CI without raw curl calls.
3117
+ * Used by engine/ado/status.js so agents can check CI without raw curl calls.
2775
3118
  * Returns { prNumber, title, branch, status, reviewStatus, buildStatus,
2776
3119
  * buildStatusStale?, buildStatusDetail?, buildErrorLog?,
2777
3120
  * mergeConflict, url, project } or null on auth failure.
@@ -2988,7 +3331,7 @@ function _setAdoTokenAuthNoteWriterForTest(fn) {
2988
3331
 
2989
3332
  // ─── One-Shot Startup Reconciliation for Abandoned PRs (W-mp60tw0u000j3931) ───
2990
3333
  //
2991
- // ADO equivalent of engine/github.js reconcileAbandonedPrs. Same shape:
3334
+ // ADO equivalent of engine/providers/github.js reconcileAbandonedPrs. Same shape:
2992
3335
  // per-(org, project, repo) base probe, cached for the duration of the pass;
2993
3336
  // then per-PR re-probe via adoFetch. ADO PRs use `prData.status` directly
2994
3337
  // ('active' | 'abandoned' | 'completed'), which we map back to PR_STATUS.
@@ -3126,6 +3469,9 @@ async function reconcileAbandonedPrs(config) {
3126
3469
  delete pr._consecutive404s;
3127
3470
  delete pr._404Count;
3128
3471
  delete pr._404FirstAt;
3472
+ // W-msav5edk00g81710: the record left `abandoned` — drop the
3473
+ // bounded-re-probe anchor with it.
3474
+ if (upd.newStatus !== shared.PR_STATUS.ABANDONED) shared.clearPrAbandonedAt(pr);
3129
3475
  pr._reconciledAt = reconciledAt;
3130
3476
  pr._reconciledFrom = 'startup-pass';
3131
3477
  } else if (upd.action === 'confirm404') {
@@ -3177,6 +3523,7 @@ module.exports = {
3177
3523
  _resetAdoThrottle, // exported for testing
3178
3524
  _setAdoThrottleForTest, // exported for testing
3179
3525
  _setAdoTokenForTest, // exported for testing
3526
+ _resetAdoMarkerSpoofGuardWarningForTest, // exported for testing
3180
3527
  _setAdoFetchForTest, // exported for testing
3181
3528
  _resetAdoFetchForTest, // exported for testing
3182
3529
  // Exported for unit tests — engine code MUST go through pollPrStatus / reconcilePrs.
@@ -3203,9 +3550,16 @@ module.exports = {
3203
3550
  // Issue #2747 — stale merge-commit classifier + auto-queue helpers, exported for unit tests.
3204
3551
  classifyBuildStatus,
3205
3552
  classifyStaleBuilds,
3553
+ // W-msaux21y00g79c48 — newest-merge-commit narrowing for stale build sets.
3554
+ selectNewestMergeCommitBuilds,
3206
3555
  isPrValidationBuild,
3207
3556
  shouldRequeueStaleBuild,
3208
3557
  PR_BUILD_REQUEUE_INTERVAL_MS,
3558
+ // W-ms7kklp8002zad24 — draft-PR branch-policy build queueing, exported for unit tests.
3559
+ extractDraftPrBuildDefinitionIds,
3560
+ adoPolicyScopeMatches,
3561
+ queueDraftPrValidationBuilds,
3562
+ ADO_BUILD_POLICY_TYPE_ID,
3209
3563
  // #3758 — exported for unit tests of GitHub-only-config inertness.
3210
3564
  isGitHubProject,
3211
3565
  hasAdoProjectConfigured,