@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
@@ -28,7 +28,7 @@
28
28
 
29
29
  const fs = require('fs');
30
30
  const path = require('path');
31
- const shared = require('./shared');
31
+ const shared = require('../core/shared');
32
32
  const { mutateQaRuns, uid, ts, log } = shared;
33
33
 
34
34
  // Cap qa_runs so the table doesn't grow unboundedly
@@ -42,6 +42,13 @@ const QA_ARTIFACT_PATH_MAX = 2048;
42
42
  const QA_ARTIFACT_SEGMENT_MAX = 255;
43
43
  const QA_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
44
44
 
45
+ // W-msb9kgs402813a97-c — journey coverage reported by the EXECUTE agent in its
46
+ // qa-run-result sidecar. Caps mirror the artifact segment cap so a runaway
47
+ // agent cannot inflate the run's `data` blob.
48
+ const QA_JOURNEY_COVERAGE_MAX = 50;
49
+ const QA_JOURNEY_COVERAGE_LIST_MAX = 50;
50
+ const QA_JOURNEY_COVERAGE_STATUSES = Object.freeze(new Set(['passed', 'failed', 'skipped']));
51
+
45
52
  const QA_RUN_STATUS = Object.freeze({
46
53
  PENDING: 'pending',
47
54
  RUNNING: 'running',
@@ -59,7 +66,7 @@ const TERMINAL_STATUSES = new Set([
59
66
  // Allowed forward transitions. Anything not enumerated here is rejected.
60
67
  //
61
68
  // PR #2697 review fix (W-mpeiwz6k0005bf34-c — Ripley): the lifecycle hook in
62
- // engine/lifecycle.js parses the agent's qa-run-result.json sidecar and calls
69
+ // engine/orchestration/lifecycle.js parses the agent's qa-run-result.json sidecar and calls
63
70
  // completeRun({status: 'passed'|'failed'|'errored'}) directly. It never calls
64
71
  // markRunning, because the agent may crash before writing the sidecar (in
65
72
  // which case the hook still needs to mark the run errored from `pending`).
@@ -183,17 +190,133 @@ function validateTransition(from, to) {
183
190
  }
184
191
  }
185
192
 
193
+ /**
194
+ * Normalize one artifact record from the agent's qa-run-result sidecar.
195
+ *
196
+ * `journeyId` and `project` (W-msb9kgs402813a97-c) are OPTIONAL and only
197
+ * persisted when the agent actually declared them, so legacy qa-validate runs
198
+ * keep their historical record shape instead of gaining empty keys.
199
+ */
186
200
  function normalizeArtifact(a) {
187
201
  if (!a || typeof a !== 'object') return null;
188
202
  const type = typeof a.type === 'string' ? a.type : '';
189
203
  const p = typeof a.path === 'string' ? a.path : '';
190
204
  if (!type || !p) return null;
191
- return {
205
+ const out = {
192
206
  type,
193
207
  path: p,
194
208
  label: typeof a.label === 'string' ? a.label : '',
195
209
  capturedAt: typeof a.capturedAt === 'string' && a.capturedAt ? a.capturedAt : ts(),
196
210
  };
211
+ const journeyId = _normalizeCoverageName(a.journeyId);
212
+ if (journeyId) out.journeyId = journeyId;
213
+ const project = _normalizeCoverageName(a.project);
214
+ if (project) out.project = project;
215
+ return out;
216
+ }
217
+
218
+ function _normalizeCoverageName(value) {
219
+ if (typeof value !== 'string') return '';
220
+ const trimmed = value.trim();
221
+ if (!trimmed || trimmed.length > QA_ARTIFACT_SEGMENT_MAX) return '';
222
+ return trimmed;
223
+ }
224
+
225
+ function _normalizeCoverageList(value) {
226
+ if (!Array.isArray(value)) return [];
227
+ const out = [];
228
+ for (const entry of value) {
229
+ const name = _normalizeCoverageName(entry);
230
+ if (name && !out.includes(name)) out.push(name);
231
+ if (out.length >= QA_JOURNEY_COVERAGE_LIST_MAX) break;
232
+ }
233
+ return out;
234
+ }
235
+
236
+ /**
237
+ * Normalize the sidecar's `journeyCoverage` array — what the run actually
238
+ * exercised, per journey declared by the DRAFT visual-journey manifest.
239
+ * Malformed entries are DROPPED rather than repaired: a coverage claim the
240
+ * engine cannot read is not a coverage claim, and the EXECUTE evidence gate
241
+ * (engine/qa/visual-journey.js#validateEvidenceCoverage) then fails closed on
242
+ * the journey it can no longer prove.
243
+ *
244
+ * @param {Array} entries
245
+ * @returns {Array<{journeyId:string,status:string,projects:string[],services:string[]}>}
246
+ */
247
+ function normalizeJourneyCoverage(entries) {
248
+ if (!Array.isArray(entries)) return [];
249
+ const out = [];
250
+ const seen = new Set();
251
+ for (const entry of entries) {
252
+ if (out.length >= QA_JOURNEY_COVERAGE_MAX) break;
253
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
254
+ const journeyId = _normalizeCoverageName(entry.journeyId);
255
+ if (!journeyId || seen.has(journeyId)) continue;
256
+ const status = typeof entry.status === 'string' ? entry.status : '';
257
+ if (!QA_JOURNEY_COVERAGE_STATUSES.has(status)) continue;
258
+ seen.add(journeyId);
259
+ out.push({
260
+ journeyId,
261
+ status,
262
+ projects: _normalizeCoverageList(entry.projects),
263
+ services: _normalizeCoverageList(entry.services),
264
+ });
265
+ }
266
+ return out;
267
+ }
268
+
269
+ // W-msb9kgs402813a97-d — "primary" artifacts are the ones that ARE the visible
270
+ // result of a QA run: screenshots and video. Everything else (logs, json, md,
271
+ // traces) is supporting material. This classifier lives here because runs.js
272
+ // owns the artifact contract (see normalizeArtifact above); the /qa Recent Runs
273
+ // strip applies the same partition client-side in
274
+ // dashboard/js/qa.js#_qaRenderArtifactPreviews — keep the two extension lists
275
+ // in sync, same convention as _qaIsTerminalStatus ↔ TERMINAL_STATUSES.
276
+ const PRIMARY_IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg']);
277
+ const PRIMARY_VIDEO_EXTS = new Set(['mp4', 'webm', 'ogg', 'mov']);
278
+
279
+ // A session card is a summary, not a gallery — a handful of previews is enough
280
+ // to answer "did this run capture anything?" without turning a 50-session poll
281
+ // response into a payload problem.
282
+ const QA_PRIMARY_ARTIFACT_DEFAULT_MAX = 4;
283
+
284
+ function isPrimaryArtifact(a) {
285
+ if (!a || typeof a !== 'object') return false;
286
+ const p = typeof a.path === 'string' ? a.path : '';
287
+ if (!p) return false;
288
+ const type = String(a.type || '').toLowerCase();
289
+ if (type === 'screenshot' || type === 'image' || type === 'video') return true;
290
+ const ext = (p.match(/\.([a-z0-9]+)$/i) || ['', ''])[1].toLowerCase();
291
+ return PRIMARY_IMAGE_EXTS.has(ext) || PRIMARY_VIDEO_EXTS.has(ext);
292
+ }
293
+
294
+ /**
295
+ * Project a run's artifacts down to the bounded primary set a summary surface
296
+ * can render. Order is preserved; the return shape is exactly
297
+ * { type, path, label } so no internal bookkeeping (capturedAt) leaks into an
298
+ * API payload. Never throws on hostile input.
299
+ *
300
+ * @param {Array<object>} artifacts
301
+ * @param {number} [limit=QA_PRIMARY_ARTIFACT_DEFAULT_MAX]
302
+ * @returns {Array<{type: string, path: string, label: string}>}
303
+ */
304
+ function selectPrimaryArtifacts(artifacts, limit = QA_PRIMARY_ARTIFACT_DEFAULT_MAX) {
305
+ if (!Array.isArray(artifacts)) return [];
306
+ const cap = Number.isFinite(limit) && limit > 0
307
+ ? Math.floor(limit)
308
+ : QA_PRIMARY_ARTIFACT_DEFAULT_MAX;
309
+ const out = [];
310
+ for (const a of artifacts) {
311
+ if (out.length >= cap) break;
312
+ if (!isPrimaryArtifact(a)) continue;
313
+ out.push({
314
+ type: typeof a.type === 'string' ? a.type : '',
315
+ path: a.path,
316
+ label: typeof a.label === 'string' ? a.label : '',
317
+ });
318
+ }
319
+ return out;
197
320
  }
198
321
 
199
322
  function createRunId() {
@@ -303,10 +426,13 @@ function markRunning(id) {
303
426
  * @param {object} opts
304
427
  * @param {string} opts.status - 'passed' | 'failed' | 'errored'
305
428
  * @param {string} [opts.summary] - human-readable summary
306
- * @param {Array<object>} [opts.artifacts] - artifact records {type, path, label?, capturedAt?}
429
+ * @param {Array<object>} [opts.artifacts] - artifact records {type, path, label?, capturedAt?, journeyId?, project?}
430
+ * @param {Array<object>} [opts.journeyCoverage] - per-journey outcome records
431
+ * {journeyId, status, projects[], services[]}. Omit entirely for legacy
432
+ * qa-validate runs — the field is only stamped when the caller supplies it.
307
433
  * @returns {object} updated run
308
434
  */
309
- function completeRun(id, { status, summary, artifacts } = {}) {
435
+ function completeRun(id, { status, summary, artifacts, journeyCoverage } = {}) {
310
436
  if (!id) throw new Error('qa-runs: id is required');
311
437
  if (!TERMINAL_STATUSES.has(status)) {
312
438
  throw new Error(`qa-runs: completeRun status must be one of ${[...TERMINAL_STATUSES].join(', ')}`);
@@ -314,6 +440,9 @@ function completeRun(id, { status, summary, artifacts } = {}) {
314
440
  const normalized = Array.isArray(artifacts)
315
441
  ? artifacts.map(normalizeArtifact).filter(Boolean)
316
442
  : [];
443
+ const normalizedCoverage = Array.isArray(journeyCoverage)
444
+ ? normalizeJourneyCoverage(journeyCoverage)
445
+ : null;
317
446
 
318
447
  let captured = null;
319
448
  let transitionError = null;
@@ -330,6 +459,7 @@ function completeRun(id, { status, summary, artifacts } = {}) {
330
459
  if (!Array.isArray(run.artifacts)) run.artifacts = [];
331
460
  run.artifacts.push(...normalized);
332
461
  }
462
+ if (normalizedCoverage) run.journeyCoverage = normalizedCoverage;
333
463
  captured = run;
334
464
  return runs;
335
465
  });
@@ -350,7 +480,7 @@ function getRun(id) {
350
480
  }
351
481
 
352
482
  function _readRuns() {
353
- return require('./small-state-store').readQaRuns();
483
+ return require('../persistence/small-state-store').readQaRuns();
354
484
  }
355
485
 
356
486
  /**
@@ -436,36 +566,169 @@ function _isSafeRunId(id) {
436
566
  return QA_RUN_ID_RE.test(id);
437
567
  }
438
568
 
569
+ // W-msaklqb701jy584d — evidence-based cleanup for abandoned pending runs.
570
+ //
571
+ // deleteQaRun is terminal-only by design so an operator can't vacuum a live QA
572
+ // agent out from under itself. That left one class of record permanently
573
+ // undeletable through the API: a `pending` run created by a stray process,
574
+ // never dispatched, owning nothing, whose runbook and managed target don't
575
+ // exist. `evaluateOrphanedPendingRun` is the narrow escape hatch — EVERY
576
+ // condition below must be independently provable before
577
+ // deleteQaRun(id, { allowOrphanedPending: true }) will remove the record. Any
578
+ // unreadable ownership source fails CLOSED (not orphaned), because leaving a
579
+ // stale row costs nothing while deleting an owned one loses QA history.
580
+ const QA_ORPHAN_MIN_AGE_MS = 24 * 60 * 60 * 1000;
581
+
582
+ function _parseTsMs(value) {
583
+ if (typeof value !== 'string' || !value) return NaN;
584
+ const ms = Date.parse(value);
585
+ return Number.isFinite(ms) ? ms : NaN;
586
+ }
587
+
588
+ // { known } is false when the source could not be read at all — the caller
589
+ // must then refuse the delete rather than assume "no owner".
590
+ function _runSessionOwnership(id) {
591
+ try {
592
+ const sessions = require('./sessions').listSessions({});
593
+ if (!Array.isArray(sessions)) return { known: false, owned: false };
594
+ return { known: true, owned: sessions.some(s => s && s.qaRunId === id) };
595
+ } catch (_e) {
596
+ return { known: false, owned: false };
597
+ }
598
+ }
599
+
600
+ function _runWorkItemOwnership(id) {
601
+ try {
602
+ const items = require('../planning/work-items-store').readAllWorkItems();
603
+ if (!Array.isArray(items)) return { known: false, owned: false };
604
+ return {
605
+ known: true,
606
+ owned: items.some(wi => wi && wi.meta && wi.meta.qaRunId === id),
607
+ };
608
+ } catch (_e) {
609
+ return { known: false, owned: false };
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Decide whether a run is a provably-orphaned abandoned pending record.
615
+ *
616
+ * @param {string} id - run id
617
+ * @param {object} [opts]
618
+ * @param {number} [opts.now] - epoch ms, injectable for tests
619
+ * @param {number} [opts.minAgeMs] - minimum age before a pending run qualifies
620
+ * @returns {{id: string, exists: boolean, status: string|null, orphaned: boolean,
621
+ * reason: string|null, ageMs?: number}}
622
+ * `reason` names the first condition that failed, and is null when orphaned.
623
+ */
624
+ function evaluateOrphanedPendingRun(id, { now = Date.now(), minAgeMs = QA_ORPHAN_MIN_AGE_MS } = {}) {
625
+ const result = { id, exists: false, status: null, orphaned: false, reason: null };
626
+ if (!_isSafeRunId(id)) { result.reason = 'invalid_id'; return result; }
627
+
628
+ const run = getRun(id);
629
+ if (!run) { result.reason = 'not_found'; return result; }
630
+ result.exists = true;
631
+ result.status = run.status;
632
+
633
+ // Never touch an active run: `running` is live, and any pending run that
634
+ // already started, completed, captured artifacts, or carries a work item is
635
+ // owned, not abandoned.
636
+ if (run.status !== QA_RUN_STATUS.PENDING) { result.reason = 'status_not_pending'; return result; }
637
+ if (run.startedAt) { result.reason = 'already_started'; return result; }
638
+ if (run.completedAt) { result.reason = 'already_completed'; return result; }
639
+ if (Array.isArray(run.artifacts) && run.artifacts.length) { result.reason = 'has_artifacts'; return result; }
640
+ if (run.workItemId) { result.reason = 'work_item_owned'; return result; }
641
+
642
+ // Age gate: covers the window where a dispatch created the run and is still
643
+ // about to back-fill its work item / session linkage.
644
+ const createdMs = _parseTsMs(run.createdAt);
645
+ if (!Number.isFinite(createdMs)) { result.reason = 'unknown_age'; return result; }
646
+ result.ageMs = now - createdMs;
647
+ if (result.ageMs < minAgeMs) { result.reason = 'too_recent'; return result; }
648
+
649
+ const session = _runSessionOwnership(id);
650
+ if (!session.known) { result.reason = 'session_ownership_unknown'; return result; }
651
+ if (session.owned) { result.reason = 'session_owned'; return result; }
652
+
653
+ const workItem = _runWorkItemOwnership(id);
654
+ if (!workItem.known) { result.reason = 'work_item_ownership_unknown'; return result; }
655
+ if (workItem.owned) { result.reason = 'work_item_owned'; return result; }
656
+
657
+ let runbook;
658
+ try { runbook = require('./runbooks').getRunbook(run.runbookId); }
659
+ catch (_e) { result.reason = 'runbook_lookup_failed'; return result; }
660
+ if (runbook) { result.reason = 'runbook_exists'; return result; }
661
+
662
+ let target;
663
+ try { target = require('../processes/managed-spawn').getManagedSpecByName(run.targetName); }
664
+ catch (_e) { result.reason = 'target_lookup_failed'; return result; }
665
+ if (target) { result.reason = 'target_exists'; return result; }
666
+
667
+ result.orphaned = true;
668
+ return result;
669
+ }
670
+
439
671
  /**
440
672
  * Hard-delete a terminal-status run record and best-effort remove its
441
673
  * artifact directory. Result envelope (matches PR #3008 shape so the
442
674
  * dashboard DELETE /api/qa/runs/<id> handler stays generic):
443
675
  *
444
- * { ok: true, id, artifactsRemoved: bool }
445
- * { ok: false, error: 'invalid_id' | 'not_found' | 'not_terminal',
446
- * currentStatus?: string }
676
+ * { ok: true, id, artifactsRemoved: bool, deletedAs: 'terminal' | 'orphaned-pending' }
677
+ * { ok: false, error: 'invalid_id' | 'not_found' | 'not_terminal' | 'not_orphaned',
678
+ * currentStatus?: string, reason?: string }
447
679
  *
448
680
  * Caller must cancel/complete the run via the lifecycle helpers before
449
681
  * deletion — running/pending runs are refused so an operator can't vacuum a
450
- * live QA agent out from under itself. The artifact rmSync runs OUTSIDE
451
- * the lock, with a path.resolve + startsWith() belt-and-braces sandbox.
682
+ * live QA agent out from under itself. `allowOrphanedPending` opts into the
683
+ * single narrow exception documented on evaluateOrphanedPendingRun; a pending
684
+ * run that fails ANY of its conditions is still refused, with `not_orphaned`
685
+ * naming the failed condition. The artifact rmSync runs OUTSIDE the lock, with
686
+ * a path.resolve + startsWith() belt-and-braces sandbox.
452
687
  *
453
688
  * @param {string} id - run id (must pass _isSafeRunId)
689
+ * @param {object} [opts]
690
+ * @param {boolean} [opts.allowOrphanedPending] - allow provably-orphaned pending runs
454
691
  * @returns {object} envelope (see above)
455
692
  */
456
- function deleteQaRun(id) {
693
+ function deleteQaRun(id, { allowOrphanedPending = false } = {}) {
457
694
  if (!_isSafeRunId(id)) return { ok: false, error: 'invalid_id' };
695
+
696
+ // Orphan evidence gathering reads QA sessions, work items, runbook files and
697
+ // managed specs — none of that may run inside a mutateQaRuns callback. So
698
+ // evaluate first, then re-assert the cheap in-record invariants inside the
699
+ // transaction: a concurrent markRunning() between the two still wins.
700
+ let orphanEval = null;
701
+ if (allowOrphanedPending) {
702
+ orphanEval = evaluateOrphanedPendingRun(id);
703
+ if (orphanEval.exists && !orphanEval.orphaned && !TERMINAL_STATUSES.has(orphanEval.status)) {
704
+ return {
705
+ ok: false,
706
+ error: 'not_orphaned',
707
+ currentStatus: orphanEval.status,
708
+ reason: orphanEval.reason,
709
+ };
710
+ }
711
+ }
712
+
458
713
  let captured = null;
459
714
  let notFound = false;
460
715
  let notTerminal = null;
716
+ let deletedAs = 'terminal';
461
717
  mutateQaRuns((runs) => {
462
718
  if (!Array.isArray(runs)) return runs;
463
719
  const idx = runs.findIndex(r => r && r.id === id);
464
720
  if (idx < 0) { notFound = true; return runs; }
465
721
  const run = runs[idx];
466
722
  if (!TERMINAL_STATUSES.has(run.status)) {
467
- notTerminal = run.status;
468
- return runs;
723
+ const orphanCleanup = allowOrphanedPending
724
+ && !!orphanEval && orphanEval.orphaned
725
+ && run.status === QA_RUN_STATUS.PENDING
726
+ && !run.startedAt && !run.completedAt && !run.workItemId;
727
+ if (!orphanCleanup) {
728
+ notTerminal = run.status;
729
+ return runs;
730
+ }
731
+ deletedAs = 'orphaned-pending';
469
732
  }
470
733
  captured = run;
471
734
  runs.splice(idx, 1);
@@ -488,7 +751,7 @@ function deleteQaRun(id) {
488
751
  } catch (e) {
489
752
  log('warn', `qa-runs: rm artifacts dir failed for ${id}: ${e.message}`);
490
753
  }
491
- return { ok: true, id, artifactsRemoved, run: captured };
754
+ return { ok: true, id, artifactsRemoved, deletedAs, run: captured };
492
755
  }
493
756
 
494
757
  /**
@@ -527,16 +790,24 @@ module.exports = {
527
790
  completeRun,
528
791
  setRunWorkItemId,
529
792
  deleteQaRun,
793
+ evaluateOrphanedPendingRun,
794
+ QA_ORPHAN_MIN_AGE_MS,
530
795
  _isSafeRunId,
531
796
  getRun,
532
797
  listRuns,
533
798
  getRunsForWorkItem,
534
799
  summarizeRunsForStatus,
800
+ isPrimaryArtifact,
801
+ selectPrimaryArtifacts,
802
+ QA_PRIMARY_ARTIFACT_DEFAULT_MAX,
535
803
  // Exposed for tests:
536
804
  validateTransition,
537
805
  isValidStatus,
806
+ normalizeArtifact, normalizeJourneyCoverage, // exported for testing
538
807
  QA_RUNS_MAX_RECORDS,
539
808
  QA_RUN_ID_MAX,
540
809
  QA_ARTIFACT_PATH_MAX,
541
810
  QA_ARTIFACT_SEGMENT_MAX,
811
+ QA_JOURNEY_COVERAGE_MAX,
812
+ QA_JOURNEY_COVERAGE_STATUSES,
542
813
  };