@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
@@ -0,0 +1,79 @@
1
+ // dashboard/shared/cc-limits.js
2
+ //
3
+ // Shared client-side mirror of the server's Command Center API length limits
4
+ // (engine/api/cc-api-validation.js CC_API_LIMITS). Used by BOTH the classic
5
+ // dashboard (dashboard/js/command-center.js) and Slim
6
+ // (dashboard/slim/js/command-send.js) so an oversized message is rejected on
7
+ // the client BEFORE it is cleared from the composer, queued, rendered, or
8
+ // persisted — and so every transcript entry is bounded to the server limit,
9
+ // meaning a previously-persisted oversized message can no longer poison future
10
+ // sends via the carried transcript (W-mscjjqdh).
11
+ //
12
+ // The REAL server values are injected into the page as window.MINIONS_CC_LIMITS
13
+ // by both serve paths (dashboard.js buildDashboardHtml + serveSlimUx, sourced
14
+ // from ccApiValidation.CC_API_LIMITS). The FALLBACK_LIMITS below only apply when
15
+ // that global is missing (e.g. a stale cached page) and MUST match the server
16
+ // constant — the whole point of the injection is that client and server limits
17
+ // do not silently drift, so never bump a number here without changing the
18
+ // server. Every helper reads the limits at CALL time so a late injection is
19
+ // still honored.
20
+
21
+ (function () {
22
+ 'use strict';
23
+
24
+ var FALLBACK_LIMITS = {
25
+ messageMaxChars: 100000,
26
+ transcriptTextMaxChars: 65536,
27
+ transcriptMaxEntries: 20,
28
+ };
29
+
30
+ function _positive(value, fallback) {
31
+ return (typeof value === 'number' && isFinite(value) && value > 0) ? value : fallback;
32
+ }
33
+
34
+ function _limits() {
35
+ var injected = (typeof window !== 'undefined' && window.MINIONS_CC_LIMITS) || {};
36
+ return {
37
+ messageMaxChars: _positive(injected.messageMaxChars, FALLBACK_LIMITS.messageMaxChars),
38
+ transcriptTextMaxChars: _positive(injected.transcriptTextMaxChars, FALLBACK_LIMITS.transcriptTextMaxChars),
39
+ transcriptMaxEntries: _positive(injected.transcriptMaxEntries, FALLBACK_LIMITS.transcriptMaxEntries),
40
+ };
41
+ }
42
+
43
+ // Returns { ok, length, limit }. A message longer than messageMaxChars is
44
+ // rejected by the server as `body.message has an invalid length`; guard it on
45
+ // the client so the composer is never cleared and the turn is never persisted.
46
+ function checkMessage(text) {
47
+ var s = typeof text === 'string' ? text : '';
48
+ var limit = _limits().messageMaxChars;
49
+ return { ok: s.length <= limit, length: s.length, limit: limit };
50
+ }
51
+
52
+ // Human-readable, actionable validation message for an oversized send. The
53
+ // rejected text stays in the composer, so tell the user that explicitly.
54
+ function messageTooLongText(length, limit) {
55
+ var lim = _positive(limit, _limits().messageMaxChars);
56
+ var len = (typeof length === 'number' && isFinite(length)) ? length : 0;
57
+ return 'Message is too long (' + len.toLocaleString() + ' characters; limit is '
58
+ + lim.toLocaleString() + '). Shorten it and send again — your text is still in the box.';
59
+ }
60
+
61
+ // Bound a single transcript entry's text to the server's transcriptTextMaxChars
62
+ // so a previously-persisted oversized message cannot poison future sends
63
+ // (`body.transcript[N].text has an invalid length`). Truncation is safe: the
64
+ // transcript is best-effort carry-over context, not the authoritative turn.
65
+ function boundTranscriptText(text) {
66
+ var s = typeof text === 'string' ? text : '';
67
+ var limit = _limits().transcriptTextMaxChars;
68
+ return s.length > limit ? s.slice(0, limit) : s;
69
+ }
70
+
71
+ window.MinionsCcLimits = {
72
+ get messageMaxChars() { return _limits().messageMaxChars; },
73
+ get transcriptTextMaxChars() { return _limits().transcriptTextMaxChars; },
74
+ get transcriptMaxEntries() { return _limits().transcriptMaxEntries; },
75
+ checkMessage: checkMessage,
76
+ messageTooLongText: messageTooLongText,
77
+ boundTranscriptText: boundTranscriptText,
78
+ };
79
+ })();
@@ -1,6 +1,10 @@
1
1
  // Shared PR filter semantics. Slim embeds the classic /prs screen, and both
2
2
  // bundles load this helper so lifecycle and legacy-field normalization stay
3
3
  // identical across surfaces.
4
+ //
5
+ // Only the PR-specific normalizers live here; the dimension/match/options
6
+ // machinery comes from shared/record-filters.js, which the classic Work Items
7
+ // filter bar reuses too.
4
8
  var PR_FILTER_DIMENSIONS = ['author', 'lifecycle', 'project', 'review', 'build'];
5
9
  var PR_LIFECYCLE_FILTER_LABELS = {
6
10
  open: 'Open / active',
@@ -14,7 +18,7 @@ var PR_FILTER_OPTION_ORDER = {
14
18
  };
15
19
 
16
20
  function _prFilterText(value) {
17
- return typeof value === 'string' ? value.trim() : '';
21
+ return window.MinionsRecordFilters.text(value);
18
22
  }
19
23
 
20
24
  function _prFilterName(value) {
@@ -28,15 +32,17 @@ function _prFilterName(value) {
28
32
  }
29
33
 
30
34
  function _prFilterKey(value) {
31
- return _prFilterText(value).toLowerCase();
35
+ return window.MinionsRecordFilters.key(value);
32
36
  }
33
37
 
34
38
  function normalizePrAuthor(pr) {
35
39
  if (!pr || typeof pr !== 'object') return '';
40
+ // Real author only. Deliberately NOT falling back to pr.agent: the Minions
41
+ // agent is a separate identity (W-mscibegy005e8b74) and a legacy record with
42
+ // no author must group/render as Unknown rather than borrow the agent name.
36
43
  return _prFilterName(pr.author)
37
44
  || _prFilterName(pr.createdBy)
38
- || _prFilterName(pr.created_by)
39
- || _prFilterName(pr.agent);
45
+ || _prFilterName(pr.created_by);
40
46
  }
41
47
 
42
48
  function normalizePrLifecycleStatus(pr) {
@@ -108,52 +114,29 @@ function _prFilterEntry(pr, dimension) {
108
114
  }
109
115
 
110
116
  function _formatPrFilterLabel(value) {
111
- var text = _prFilterText(value).replace(/[-_]+/g, ' ');
112
- return text ? text.charAt(0).toUpperCase() + text.slice(1) : '';
117
+ return window.MinionsRecordFilters.label(value);
113
118
  }
114
119
 
120
+ var _prFilterEngine = window.MinionsRecordFilters.create({
121
+ dimensions: PR_FILTER_DIMENSIONS,
122
+ entry: _prFilterEntry,
123
+ order: PR_FILTER_OPTION_ORDER,
124
+ });
125
+
115
126
  function getPrFilterValue(pr, dimension) {
116
- var entry = _prFilterEntry(pr, dimension);
117
- return entry ? entry.value : '';
127
+ return _prFilterEngine.value(pr, dimension);
118
128
  }
119
129
 
120
130
  function matchesPullRequestFilters(pr, filters) {
121
- filters = filters || {};
122
- for (var i = 0; i < PR_FILTER_DIMENSIONS.length; i++) {
123
- var dimension = PR_FILTER_DIMENSIONS[i];
124
- var selected = _prFilterKey(filters[dimension]);
125
- if (selected && getPrFilterValue(pr, dimension) !== selected) return false;
126
- }
127
- return true;
131
+ return _prFilterEngine.matches(pr, filters);
128
132
  }
129
133
 
130
134
  function filterPullRequests(prs, filters) {
131
- if (!Array.isArray(prs)) return [];
132
- return prs.filter(function(pr) { return matchesPullRequestFilters(pr, filters); });
135
+ return _prFilterEngine.filter(prs, filters);
133
136
  }
134
137
 
135
138
  function getPrFilterOptions(prs) {
136
- var result = {};
137
- var records = Array.isArray(prs) ? prs : [];
138
- PR_FILTER_DIMENSIONS.forEach(function(dimension) {
139
- var seen = new Map();
140
- records.forEach(function(pr) {
141
- var entry = _prFilterEntry(pr, dimension);
142
- if (entry && !seen.has(entry.value)) seen.set(entry.value, entry);
143
- });
144
- var order = PR_FILTER_OPTION_ORDER[dimension] || [];
145
- result[dimension] = Array.from(seen.values()).sort(function(a, b) {
146
- var ai = order.indexOf(a.value);
147
- var bi = order.indexOf(b.value);
148
- if (ai !== -1 || bi !== -1) {
149
- if (ai === -1) return 1;
150
- if (bi === -1) return -1;
151
- if (ai !== bi) return ai - bi;
152
- }
153
- return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' });
154
- });
155
- });
156
- return result;
139
+ return _prFilterEngine.options(prs);
157
140
  }
158
141
 
159
142
  window.MinionsPrFilters = {
@@ -161,7 +161,7 @@ function summarizeProjectGit(p) {
161
161
  var branchDiffersFromMain = !!(mainBranch && branch !== mainBranch);
162
162
  var comparator = mainBranch || remoteDefault;
163
163
  // Comparator for ahead/behind is origin/<mainBranch || remoteDefaultBranch>
164
- // (engine/queries.js _probeProjectGitStatus), never the branch's own upstream.
164
+ // (engine/core/queries.js _probeProjectGitStatus), never the branch's own upstream.
165
165
  var comparatorRef = mainBranch || remoteDefault;
166
166
  var branchDiffersFromComparator = !!(comparator && branch !== comparatorRef);
167
167
  var showAheadBehind = ahead !== null && behind !== null && (ahead > 0 || behind > 0);
@@ -0,0 +1,169 @@
1
+ // Generic record-filter semantics shared by every filterable classic table.
2
+ //
3
+ // Both filterable classic tables (Pull Requests, Work Items) need exactly the
4
+ // same machinery: normalize a record to a comparable key per dimension, AND the
5
+ // selected dimensions together, derive the <select> options from the records
6
+ // actually present, and drive the filter-bar DOM. Only the per-dimension
7
+ // normalizers differ. This module owns the common half so the two surfaces
8
+ // cannot drift ("reuse, don't fork"); callers supply just an `entry` function.
9
+ //
10
+ // Option VALUES are always derived from the records present, so no surface
11
+ // maintains a taxonomy that can fall out of sync with the engine. `order` only
12
+ // supplies the display ORDER; unknown values sort alphabetically after it.
13
+
14
+ function recordFilterText(value) {
15
+ return typeof value === 'string' ? value.trim() : '';
16
+ }
17
+
18
+ function recordFilterKey(value) {
19
+ return recordFilterText(value).toLowerCase();
20
+ }
21
+
22
+ function formatRecordFilterLabel(value) {
23
+ var text = recordFilterText(value).replace(/[-_]+/g, ' ');
24
+ return text ? text.charAt(0).toUpperCase() + text.slice(1) : '';
25
+ }
26
+
27
+ // `entry(record, dimension)` returns `{ value, label }` or a falsy value when
28
+ // the record carries nothing for that dimension. A record with no value is
29
+ // never given a selectable option (it would read as an "empty" choice) but is
30
+ // still retained under "All".
31
+ //
32
+ // `order` maps a dimension to an array of values defining display order, or to
33
+ // a function returning one — a function lets a caller defer to a vocabulary
34
+ // that is bootstrapped onto `window` after this module is evaluated.
35
+ function createRecordFilters(spec) {
36
+ var dimensions = (spec && spec.dimensions) || [];
37
+ var entry = (spec && spec.entry) || function () { return null; };
38
+ var order = (spec && spec.order) || {};
39
+
40
+ function resolveOrder(dimension) {
41
+ var configured = order[dimension];
42
+ if (typeof configured === 'function') configured = configured();
43
+ return Array.isArray(configured) ? configured : [];
44
+ }
45
+
46
+ function value(record, dimension) {
47
+ var resolved = entry(record, dimension);
48
+ return resolved ? resolved.value : '';
49
+ }
50
+
51
+ function matches(record, filters) {
52
+ filters = filters || {};
53
+ for (var i = 0; i < dimensions.length; i++) {
54
+ var dimension = dimensions[i];
55
+ var selected = recordFilterKey(filters[dimension]);
56
+ if (selected && value(record, dimension) !== selected) return false;
57
+ }
58
+ return true;
59
+ }
60
+
61
+ function filter(records, filters) {
62
+ if (!Array.isArray(records)) return [];
63
+ return records.filter(function (record) { return matches(record, filters); });
64
+ }
65
+
66
+ function options(records) {
67
+ var result = {};
68
+ var list = Array.isArray(records) ? records : [];
69
+ dimensions.forEach(function (dimension) {
70
+ var seen = new Map();
71
+ list.forEach(function (record) {
72
+ var resolved = entry(record, dimension);
73
+ if (resolved && resolved.value && !seen.has(resolved.value)) {
74
+ seen.set(resolved.value, { value: resolved.value, label: resolved.label || resolved.value });
75
+ }
76
+ });
77
+ var sequence = resolveOrder(dimension);
78
+ result[dimension] = Array.from(seen.values()).sort(function (a, b) {
79
+ var ai = sequence.indexOf(a.value);
80
+ var bi = sequence.indexOf(b.value);
81
+ if (ai !== -1 || bi !== -1) {
82
+ if (ai === -1) return 1;
83
+ if (bi === -1) return -1;
84
+ if (ai !== bi) return ai - bi;
85
+ }
86
+ return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' });
87
+ });
88
+ });
89
+ return result;
90
+ }
91
+
92
+ return { dimensions: dimensions, value: value, matches: matches, filter: filter, options: options };
93
+ }
94
+
95
+ // Drives a filter bar built from one <select> per dimension plus a reset button.
96
+ //
97
+ // `syncOptions` is called on EVERY render, so it short-circuits on an unchanged
98
+ // option signature: rebuilding the <option> nodes would collapse an open
99
+ // dropdown and drop keyboard focus every time the 4s poll lands.
100
+ function createFilterBarController(spec) {
101
+ var controlIds = (spec && spec.controlIds) || {};
102
+ var allLabels = (spec && spec.allLabels) || {};
103
+ var getOptions = spec && spec.getOptions;
104
+ var dimensions = Object.keys(controlIds);
105
+ var signature = '';
106
+
107
+ function selectFor(dimension) {
108
+ return document.getElementById(controlIds[dimension]);
109
+ }
110
+
111
+ function syncOptions(records) {
112
+ var options = getOptions(records);
113
+ var controls = dimensions.map(function (dimension) {
114
+ return { dimension: dimension, select: selectFor(dimension) };
115
+ }).filter(function (control) { return control.select; });
116
+ if (!controls.length) return;
117
+ var next = JSON.stringify(options);
118
+ if (next === signature) return;
119
+ signature = next;
120
+ controls.forEach(function (control) {
121
+ var select = control.select;
122
+ var selected = select.value;
123
+ var all = document.createElement('option');
124
+ all.value = '';
125
+ all.textContent = allLabels[control.dimension];
126
+ var nodes = [all].concat((options[control.dimension] || []).map(function (option) {
127
+ var node = document.createElement('option');
128
+ node.value = option.value;
129
+ node.textContent = option.label;
130
+ return node;
131
+ }));
132
+ select.replaceChildren.apply(select, nodes);
133
+ // A selection whose option disappeared (e.g. the last record of a project
134
+ // was archived) falls back to "All" rather than stranding the table on a
135
+ // permanently empty result.
136
+ if (nodes.some(function (node) { return node.value === selected; })) select.value = selected;
137
+ });
138
+ }
139
+
140
+ function read() {
141
+ var filters = {};
142
+ dimensions.forEach(function (dimension) {
143
+ var select = selectFor(dimension);
144
+ filters[dimension] = select ? select.value : '';
145
+ });
146
+ return filters;
147
+ }
148
+
149
+ function hasActive(filters) {
150
+ return dimensions.some(function (dimension) { return Boolean(filters[dimension]); });
151
+ }
152
+
153
+ function clear() {
154
+ dimensions.forEach(function (dimension) {
155
+ var select = selectFor(dimension);
156
+ if (select) select.value = '';
157
+ });
158
+ }
159
+
160
+ return { syncOptions: syncOptions, read: read, hasActive: hasActive, clear: clear };
161
+ }
162
+
163
+ window.MinionsRecordFilters = {
164
+ text: recordFilterText,
165
+ key: recordFilterKey,
166
+ label: formatRecordFilterLabel,
167
+ create: createRecordFilters,
168
+ createBar: createFilterBarController,
169
+ };
@@ -3,7 +3,7 @@
3
3
  // Watches are SQL-backed: migration 007 moved `engine/watches.json` into the
4
4
  // `watches` table, and the SQL-only cutover retired the JSON mirror entirely
5
5
  // (see engine/db/index.js#_retiredJsonTargets). The engine mutates watch
6
- // status/_history through engine/watches-store.js — nothing writes the old
6
+ // status/_history through engine/watches/store.js — nothing writes the old
7
7
  // file, so serving it over the static `/state/` passthrough 404s forever and
8
8
  // any UI reading it renders an empty list. `GET /api/watches` is the canonical
9
9
  // read surface.
@@ -1,7 +1,7 @@
1
1
  // Shared internal-distribution welcome popup for the classic and Slim dashboards.
2
2
  //
3
3
  // W-ms1jgr95000ke749 — the first dashboard launch on an INTERNAL Minions
4
- // distribution (`@yemishin_microsoft/minions` et al, see engine/distribution.js)
4
+ // distribution (`@yemishin_microsoft/minions` et al, see engine/operations/distribution.js)
5
5
  // shows a one-time modal. Public `@yemi33/minions` installs never see it: the
6
6
  // server decides via installed-package metadata and simply answers
7
7
  // `{ show: false }`, so this module renders nothing there.
@@ -0,0 +1,302 @@
1
+ // Shared Work-Item filter + sort semantics. Modeled on shared/pr-filters.js so
2
+ // the Work Items table (dashboard/js/render-work-items.js) reuses the same
3
+ // option-derivation / filter / signature pattern the PR table uses, rather than
4
+ // forking a divergent copy. Loaded as a dashboard/shared bundle before
5
+ // render-work-items.js (dashboard-build.js DASHBOARD_SHARED_JS). (W-ms7kutzv003q17ad)
6
+ var WI_FILTER_DIMENSIONS = ['status', 'type', 'project', 'agent', 'priority'];
7
+
8
+ // Rank orders drive both the filter-option ordering AND the status/priority
9
+ // sorts (so sorting is by semantic rank, not raw string compare).
10
+ var WI_STATUS_ORDER = ['dispatched', 'pending', 'queued', 'failed', 'paused', 'done', 'decomposed', 'cancelled'];
11
+ var WI_PRIORITY_ORDER = ['critical', 'high', 'medium', 'low'];
12
+
13
+ var WI_FILTER_OPTION_ORDER = {
14
+ status: WI_STATUS_ORDER,
15
+ priority: WI_PRIORITY_ORDER,
16
+ };
17
+
18
+ function _wiFilterText(value) {
19
+ return typeof value === 'string' ? value.trim() : '';
20
+ }
21
+
22
+ function _wiFilterKey(value) {
23
+ return _wiFilterText(value).toLowerCase();
24
+ }
25
+
26
+ function _wiFormatLabel(value) {
27
+ var text = _wiFilterText(value).replace(/[-_]+/g, ' ');
28
+ return text ? text.charAt(0).toUpperCase() + text.slice(1) : '';
29
+ }
30
+
31
+ function normalizeWorkItemStatus(item) {
32
+ if (!item || typeof item !== 'object') return '';
33
+ return _wiFilterKey(item.status);
34
+ }
35
+
36
+ function normalizeWorkItemType(item) {
37
+ if (!item || typeof item !== 'object') return '';
38
+ return _wiFilterKey(item.type);
39
+ }
40
+
41
+ function normalizeWorkItemProject(item) {
42
+ if (!item || typeof item !== 'object') return '';
43
+ return _wiFilterText(item.project) || _wiFilterText(item._source);
44
+ }
45
+
46
+ function normalizeWorkItemAgent(item) {
47
+ if (!item || typeof item !== 'object') return '';
48
+ // Prefer a completed agent (multiple → first), then the assigned/dispatched
49
+ // agent. Mirrors the Agent column in wiRow().
50
+ if (Array.isArray(item.completedAgents) && item.completedAgents.length) {
51
+ var first = _wiFilterText(item.completedAgents[0]);
52
+ if (first) return first;
53
+ }
54
+ return _wiFilterText(item.dispatched_to) || _wiFilterText(item.agent);
55
+ }
56
+
57
+ function normalizeWorkItemPriority(item) {
58
+ if (!item || typeof item !== 'object') return '';
59
+ return _wiFilterKey(item.priority);
60
+ }
61
+
62
+ function _wiFilterEntry(item, dimension) {
63
+ var label = '';
64
+ var value = '';
65
+ if (dimension === 'status') {
66
+ value = normalizeWorkItemStatus(item);
67
+ label = _wiFormatLabel(value);
68
+ } else if (dimension === 'type') {
69
+ value = normalizeWorkItemType(item);
70
+ label = _wiFormatLabel(value);
71
+ } else if (dimension === 'project') {
72
+ label = normalizeWorkItemProject(item);
73
+ value = _wiFilterKey(label);
74
+ } else if (dimension === 'agent') {
75
+ label = normalizeWorkItemAgent(item);
76
+ value = _wiFilterKey(label);
77
+ } else if (dimension === 'priority') {
78
+ value = normalizeWorkItemPriority(item);
79
+ label = _wiFormatLabel(value);
80
+ }
81
+ return value ? { value: value, label: label || value } : null;
82
+ }
83
+
84
+ function getWorkItemFilterValue(item, dimension) {
85
+ var entry = _wiFilterEntry(item, dimension);
86
+ return entry ? entry.value : '';
87
+ }
88
+
89
+ function matchesWorkItemFilters(item, filters) {
90
+ filters = filters || {};
91
+ for (var i = 0; i < WI_FILTER_DIMENSIONS.length; i++) {
92
+ var dimension = WI_FILTER_DIMENSIONS[i];
93
+ var selected = _wiFilterKey(filters[dimension]);
94
+ if (selected && getWorkItemFilterValue(item, dimension) !== selected) return false;
95
+ }
96
+ return true;
97
+ }
98
+
99
+ function filterWorkItems(items, filters) {
100
+ if (!Array.isArray(items)) return [];
101
+ return items.filter(function(item) { return matchesWorkItemFilters(item, filters); });
102
+ }
103
+
104
+ function getWorkItemFilterOptions(items) {
105
+ var result = {};
106
+ var records = Array.isArray(items) ? items : [];
107
+ WI_FILTER_DIMENSIONS.forEach(function(dimension) {
108
+ var seen = new Map();
109
+ records.forEach(function(item) {
110
+ var entry = _wiFilterEntry(item, dimension);
111
+ if (entry && !seen.has(entry.value)) seen.set(entry.value, entry);
112
+ });
113
+ var order = WI_FILTER_OPTION_ORDER[dimension] || [];
114
+ result[dimension] = Array.from(seen.values()).sort(function(a, b) {
115
+ var ai = order.indexOf(a.value);
116
+ var bi = order.indexOf(b.value);
117
+ if (ai !== -1 || bi !== -1) {
118
+ if (ai === -1) return 1;
119
+ if (bi === -1) return -1;
120
+ if (ai !== bi) return ai - bi;
121
+ }
122
+ return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' });
123
+ });
124
+ });
125
+ return result;
126
+ }
127
+
128
+ // --- Sorting -------------------------------------------------------------
129
+
130
+ // Lifecycle groups: a COARSER partition of WI_STATUS_ORDER that drives the
131
+ // default view, so the table reads active -> waiting -> needs-attention ->
132
+ // halted -> terminal instead of interleaving statuses by raw recency
133
+ // (W-ms9bmp8k003if9cf).
134
+ // Every member must also appear in WI_STATUS_ORDER and the groups must be
135
+ // disjoint (both test-enforced), so this module stays the single definition of
136
+ // status ordering — dashboard/js/render-work-items.js no longer carries a
137
+ // divergent copy. WI_STATUS_ORDER in turn must cover every canonical engine
138
+ // status (engine/core/shared.js WI_STATUS), which is also test-enforced: the
139
+ // group ordering here is direction-independent, so an unranked status would be
140
+ // pinned behind the entire terminal tail with no way for the user to surface
141
+ // it. A status outside every group ranks last, matching the "unknown sorts
142
+ // last" convention _wiRank() already uses.
143
+ var WI_LIFECYCLE_GROUPS = [
144
+ ['dispatched'], // active — an agent is working on it
145
+ ['pending', 'queued'], // waiting to be dispatched
146
+ ['failed'], // needs attention — may still auto-retry
147
+ ['paused'], // halted by an operator (POST /api/plans/pause);
148
+ // non-terminal and resumable, but only a human
149
+ // can un-block it, so it trails the groups the
150
+ // engine progresses on its own
151
+ ['done', 'decomposed', 'cancelled'], // terminal
152
+ ];
153
+
154
+ var WI_SORT_KEYS = ['lifecycle', 'updated', 'created', 'priority', 'status', 'type'];
155
+ var WI_SORT_DIRECTIONS = ['asc', 'desc'];
156
+ // Default view: lifecycle groups in their inherent order, last-updated DESC
157
+ // inside each group. Semantics of the Asc/Desc toggle under 'lifecycle': it
158
+ // controls ONLY the within-group recency ordering. The group ordering is
159
+ // inherent to the lifecycle and is never inverted, so terminal items can't
160
+ // float to the top of the default view. Picking 'updated' explicitly still
161
+ // yields pure, status-agnostic recency.
162
+ var WI_SORT_DEFAULT = { sortBy: 'lifecycle', dir: 'desc' };
163
+
164
+ // Sort keys whose primary comparator is coarse-grained (many items share one
165
+ // rank). These tie-break by last-updated DESC before falling through to the id
166
+ // tie-break, so a status group reads newest-first instead of id-first.
167
+ // 'lifecycle' is absent by design: recency is already its within-group primary.
168
+ var WI_RECENCY_TIEBREAK_KEYS = ['status'];
169
+
170
+ function _wiTimeMs(value) {
171
+ if (value == null || value === '') return null;
172
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
173
+ var parsed = Date.parse(value);
174
+ return Number.isFinite(parsed) ? parsed : null;
175
+ }
176
+
177
+ // Canonical last-updated timestamp. Prefer the real updatedAt surfaced by the
178
+ // store, then completedAt, dispatched_at, created (legacy fallback only — never
179
+ // faked from created when a real updatedAt exists). Returns ms epoch or null.
180
+ function workItemLastUpdatedMs(item) {
181
+ if (!item || typeof item !== 'object') return null;
182
+ return _wiTimeMs(item.updatedAt)
183
+ ?? _wiTimeMs(item.updated_at)
184
+ ?? _wiTimeMs(item.completedAt)
185
+ ?? _wiTimeMs(item.completed_at)
186
+ ?? _wiTimeMs(item.dispatched_at)
187
+ ?? _wiTimeMs(item.created)
188
+ ?? _wiTimeMs(item.created_at);
189
+ }
190
+
191
+ function _wiCreatedMs(item) {
192
+ if (!item || typeof item !== 'object') return null;
193
+ return _wiTimeMs(item.created) ?? _wiTimeMs(item.created_at);
194
+ }
195
+
196
+ function _wiRank(order, value) {
197
+ var i = order.indexOf(_wiFilterKey(value));
198
+ return i === -1 ? order.length : i;
199
+ }
200
+
201
+ // Lifecycle group index for an item (0 = active ... N = unknown/last).
202
+ function workItemLifecycleRank(item) {
203
+ var status = normalizeWorkItemStatus(item);
204
+ for (var i = 0; i < WI_LIFECYCLE_GROUPS.length; i++) {
205
+ if (WI_LIFECYCLE_GROUPS[i].indexOf(status) !== -1) return i;
206
+ }
207
+ return WI_LIFECYCLE_GROUPS.length;
208
+ }
209
+
210
+ // Grouping whose ordering is INHERENT rather than user-chosen, and therefore
211
+ // direction-independent. Returns 0 for sort keys that have no fixed grouping.
212
+ function _wiFixedGroupCompare(a, b, sortBy) {
213
+ if (sortBy !== 'lifecycle') return 0;
214
+ return workItemLifecycleRank(a) - workItemLifecycleRank(b);
215
+ }
216
+
217
+ // Newest-first recency comparator used as a tie-break for coarse sort keys.
218
+ // Missing timestamps sort last (treated as oldest), mirroring _wiCompare.
219
+ function _wiRecencyDesc(a, b) {
220
+ var av = workItemLastUpdatedMs(a);
221
+ var bv = workItemLastUpdatedMs(b);
222
+ if (av == null && bv == null) return 0;
223
+ if (av == null) return 1;
224
+ if (bv == null) return -1;
225
+ return bv - av;
226
+ }
227
+
228
+ function _wiCompare(a, b, sortBy) {
229
+ if (sortBy === 'priority') {
230
+ return _wiRank(WI_PRIORITY_ORDER, a.priority) - _wiRank(WI_PRIORITY_ORDER, b.priority);
231
+ }
232
+ if (sortBy === 'status') {
233
+ return _wiRank(WI_STATUS_ORDER, a.status) - _wiRank(WI_STATUS_ORDER, b.status);
234
+ }
235
+ if (sortBy === 'type') {
236
+ return _wiFilterKey(a.type).localeCompare(_wiFilterKey(b.type), undefined, { sensitivity: 'base' });
237
+ }
238
+ var av, bv;
239
+ if (sortBy === 'created') {
240
+ av = _wiCreatedMs(a); bv = _wiCreatedMs(b);
241
+ } else {
242
+ // 'lifecycle' (within-group ordering, after _wiFixedGroupCompare has
243
+ // separated the groups), 'updated' (default) and any unknown key fall
244
+ // through to last-updated.
245
+ av = workItemLastUpdatedMs(a); bv = workItemLastUpdatedMs(b);
246
+ }
247
+ // Missing timestamps sort last in ascending order (i.e. treated as oldest).
248
+ if (av == null && bv == null) return 0;
249
+ if (av == null) return -1;
250
+ if (bv == null) return 1;
251
+ return av - bv;
252
+ }
253
+
254
+ // Stable sort: applies any fixed (direction-independent) grouping first, then
255
+ // the primary comparator ascending, then reverses for desc, with a recency and
256
+ // finally an id tie-break so equal keys keep a deterministic order.
257
+ function sortWorkItems(items, sort) {
258
+ if (!Array.isArray(items)) return [];
259
+ var sortBy = sort && WI_SORT_KEYS.indexOf(sort.sortBy) !== -1 ? sort.sortBy : WI_SORT_DEFAULT.sortBy;
260
+ var dir = sort && WI_SORT_DIRECTIONS.indexOf(sort.dir) !== -1 ? sort.dir : WI_SORT_DEFAULT.dir;
261
+ var mult = dir === 'asc' ? 1 : -1;
262
+ var recencyTieBreak = WI_RECENCY_TIEBREAK_KEYS.indexOf(sortBy) !== -1;
263
+ var withIdx = items.map(function(item, idx) { return { item: item, idx: idx }; });
264
+ withIdx.sort(function(a, b) {
265
+ // Inherent group ordering — never multiplied by `mult`, so toggling the
266
+ // direction cannot float terminal work above active work.
267
+ var group = _wiFixedGroupCompare(a.item, b.item, sortBy);
268
+ if (group !== 0) return group;
269
+ var primary = _wiCompare(a.item, b.item, sortBy);
270
+ if (primary !== 0) return primary * mult;
271
+ // Coarse keys tie-break by recency (newest first, direction-independent —
272
+ // same contract as the id tie-break) so a whole status group doesn't read
273
+ // in id order.
274
+ if (recencyTieBreak) {
275
+ var recency = _wiRecencyDesc(a.item, b.item);
276
+ if (recency !== 0) return recency;
277
+ }
278
+ // Deterministic tie-break by id (ascending, direction-independent) so ties
279
+ // are stable regardless of the primary direction; fall back to input index.
280
+ var ida = _wiFilterText(a.item && a.item.id);
281
+ var idb = _wiFilterText(b.item && b.item.id);
282
+ if (ida !== idb) return ida < idb ? -1 : 1;
283
+ return a.idx - b.idx;
284
+ });
285
+ return withIdx.map(function(entry) { return entry.item; });
286
+ }
287
+
288
+ window.MinionsWorkItemFilters = {
289
+ dimensions: WI_FILTER_DIMENSIONS,
290
+ value: getWorkItemFilterValue,
291
+ matches: matchesWorkItemFilters,
292
+ filter: filterWorkItems,
293
+ options: getWorkItemFilterOptions,
294
+ sort: sortWorkItems,
295
+ lastUpdatedMs: workItemLastUpdatedMs,
296
+ lifecycleRank: workItemLifecycleRank,
297
+ sortKeys: WI_SORT_KEYS,
298
+ sortDefault: WI_SORT_DEFAULT,
299
+ statusOrder: WI_STATUS_ORDER,
300
+ lifecycleGroups: WI_LIFECYCLE_GROUPS,
301
+ priorityOrder: WI_PRIORITY_ORDER,
302
+ };