@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
@@ -47,6 +47,34 @@ dashboard page renders a per-row Delete button conditionally (terminal
47
47
  only), confirms with the user, and surfaces success/error via
48
48
  `showToast()` rather than `alert()`.
49
49
 
50
+ Success replies carry `deletedAs`: `terminal` for the normal path,
51
+ `orphaned-pending` for the escape hatch below.
52
+
53
+ #### Orphaned pending runs (`?orphaned=1`)
54
+
55
+ Terminal-only deletion leaves one record class permanently stuck: a
56
+ `pending` run written by a stray process that was never dispatched and owns
57
+ nothing. `DELETE /api/qa/runs/<id>?orphaned=1` opts into removing it, but
58
+ only when `engine/qa/runs.js#evaluateOrphanedPendingRun` can prove *every*
59
+ one of these:
60
+
61
+ - status is `pending` (a `running` run is never eligible);
62
+ - `startedAt` and `completedAt` are unset and `artifacts` is empty;
63
+ - `workItemId` is unset and no non-archived work item carries
64
+ `meta.qaRunId === <id>`;
65
+ - no QA session carries `qaRunId === <id>`;
66
+ - the record is at least `QA_ORPHAN_MIN_AGE_MS` old (24h), which covers the
67
+ window where a live dispatch is still back-filling its linkage;
68
+ - `getRunbook(run.runbookId)` returns null **and**
69
+ `getManagedSpecByName(run.targetName)` returns null.
70
+
71
+ Any unreadable ownership source fails closed (treated as *not* orphaned).
72
+ A pending run that misses any condition returns
73
+ `409 { error: 'not_orphaned', currentStatus, reason }`, where `reason` names
74
+ the first failed condition (`too_recent`, `runbook_exists`, `session_owned`,
75
+ …). `?orphaned=1` is additive only — it never relaxes the refusal for
76
+ `running` runs or for ambiguously-owned pending runs.
77
+
50
78
  ## Artifact contract
51
79
 
52
80
  Artifacts live at `<MINIONS_DIR>/engine/qa-artifacts/<runId>/<path>`, served via
@@ -105,7 +133,7 @@ runner-native test file, and (with user approval) a second agent executes
105
133
  it. Sessions are a thin orchestration layer on top of the same
106
134
  `managed-spawn` + `qa-run-result.json` infrastructure that powers
107
135
  runbooks above — they reuse the SQL QA-run store, `engine/qa-artifacts/`, and
108
- the existing `engine/lifecycle.js#runPostCompletionHooks` qa-run sidecar
136
+ the existing `engine/orchestration/lifecycle.js#runPostCompletionHooks` qa-run sidecar
109
137
  hook. Surfaced on `/qa` (sessions card list above the runbooks/runs
110
138
  tables) and proxied by the Command Center natural-language shortcut.
111
139
 
@@ -127,7 +155,7 @@ runbooks for repeat traffic.
127
155
 
128
156
  ## State machine
129
157
 
130
- Eight states, source of truth `engine/qa-sessions.js#QA_SESSION_STATE`:
158
+ Eight states, source of truth `engine/qa/sessions.js#QA_SESSION_STATE`:
131
159
 
132
160
  ```
133
161
  pending ──▶ spawning ──▶ drafting ──▶ awaiting-approval ──▶ executing ──▶ done
@@ -168,9 +196,9 @@ routing default):
168
196
  **Multi-project fan-out (W-mpq6xqzj000606d0):** when `spec.projects`
169
197
  contains more than one project name, SETUP is fanned out — one work item
170
198
  per project, queued in parallel into each project's own
171
- the target project's SQL work-item scope. The first project (`spec.projects[0]`) is the
172
- **primary** (`meta.qaSession.primary === true`); the rest are
173
- **co-services** (`primary === false`). Each WI's
199
+ the target project's SQL work-item scope. One project is the **primary**
200
+ (`meta.qaSession.primary === true`); the rest are **co-services**
201
+ (`primary === false`). Each WI's
174
202
  `meta.qaSession.coServices` lists the co-service project names (only the
175
203
  primary carries the full list; co-services see `[]`) and
176
204
  `meta.qaSession.primaryProject` carries the canonical primary name.
@@ -185,6 +213,35 @@ routing default):
185
213
  Single-project sessions skip `setupStatus` entirely (fast path uses
186
214
  `session.state` directly).
187
215
 
216
+ **Explicit primary + per-project targets (W-msb9kgs402813a97-a).** Which
217
+ project is primary and which target each project is checked out at are
218
+ **declared**, not positional:
219
+
220
+ - `spec.primaryProject: string` — must be a member of the resolved
221
+ `projects[]` (`validateSpec` rejects anything else with an error naming
222
+ the value and the known projects). Defaults to `projects[0]`.
223
+ `session.coServices` is then *every other* project, so a primary in the
224
+ middle of the list still fans the rest out correctly.
225
+ - `spec.projectTargets: { [projectName]: target }` — per-project target
226
+ overrides. Each key must be a member of `projects[]`, and each value is
227
+ validated by the same `_validateTarget` rules as `spec.target`. Each
228
+ SETUP WI is built with `spec.projectTargets[project] || spec.target`
229
+ (`meta.qaSession.target`, the prompt body, and the WI title all reflect
230
+ the per-project target), so a co-service is no longer forced onto the
231
+ primary's branch/PR/commit. DRAFT and EXECUTE inherit the primary's
232
+ target.
233
+
234
+ Both fields ride the normal request allowlist
235
+ (`engine/qa/process-validation.js#validateQaSessionRequest`), are ordered
236
+ and marked by the shared `orderTargetsByPrimary` seam in both project
237
+ resolvers (`dashboard.js#_qaSessionsResolveTargets` and
238
+ `engine/qa/prd-session.js#resolveTargetsFromConfig`), and are rewritten to
239
+ canonical project names by `canonicalizeResolvedProjects`. `queueSetup`
240
+ reorders the resolved targets so the declared primary leads, and throws
241
+ before `markSpawning` when project-bound targets cannot host it — a
242
+ contract disagreement surfaces as a 4xx instead of a session stranded in
243
+ `spawning`.
244
+
188
245
  2. **DRAFT** (`playbooks/qa-session-draft.md`) reads the live spawn
189
246
  metadata via `/api/managed-processes/by-name/qa-session-<id>`, calls the
190
247
  resolved runner's `generateBrief({target, flowsRaw, capture})` hook, and
@@ -192,7 +249,56 @@ routing default):
192
249
  the injected absolute `engine/qa-tests/<sessionId>/test.<ext>` path.
193
250
  The completion report's `testFile` value is stored on the session. In
194
251
  `confirm` mode the session parks at `awaiting-approval`; in `auto` mode
195
- it auto-chains to EXECUTE.
252
+ it auto-chains to EXECUTE — but only after the visual-journey gate below
253
+ accepts the draft.
254
+
255
+ ### Visual-journey manifest + pre-EXECUTE gate (W-msb9kgs402813a97-b)
256
+
257
+ `structuredCompletion.testFile` used to be DRAFT's only structured output, so
258
+ an API-only test file passed straight through `handleDraftComplete` into
259
+ EXECUTE: a session that asked for screenshots or video could reach a green
260
+ `qa-run` having never opened a browser, and a multi-project session could
261
+ ignore every co-service origin.
262
+
263
+ `engine/qa/visual-journey.js` owns the fix. It is a **declaration** validated by
264
+ the session layer, never a source-string scan of the drafted test file (a guess
265
+ cannot be a gate).
266
+
267
+ - **Sidecar.** DRAFT writes `agents/<agentId>/qa-session-draft-result.json`
268
+ (injected as the `{{qa_draft_result_sidecar}}` template var). The contract
269
+ mirrors `engine/qa/runs.js#prepareResultSidecar` / `#consumeResultSidecar`:
270
+ `engine.js#spawnAgent` clears stale content before a DRAFT dispatch (load-
271
+ bearing on the `editDraft` re-draft path, which reuses the session id), and
272
+ `engine/orchestration/lifecycle.js` does one atomic read-then-unlink on every
273
+ DRAFT completion — success or failure — so a manifest can never be inherited
274
+ by the next attempt. Missing / unreadable / malformed / session-id-mismatched
275
+ files return `{ ok: false, reason }`.
276
+ - **Shape.** `{ sessionId, journeys: [{ id, name, kind: 'browser'|'api',
277
+ projects[], services[], origins[], steps[], assertions[],
278
+ evidence: { screenshots[], video[] } }] }`. Journey count, per-list length,
279
+ per-entry byte size, and the encoded manifest itself are all capped
280
+ (`visual-journey.js` `LIMITS`), so a runaway agent cannot inflate the session
281
+ record.
282
+ - **Gate.** Enforced by `validateVisualJourneyManifest(session, manifest,
283
+ { services })` **only** when `session.spec.capture.video ||
284
+ session.spec.capture.screenshots` — a logs-only (or capture-less) session
285
+ keeps its historical pass-through behaviour. It rejects when: there is no
286
+ manifest at all; no journey has `kind: 'browser'` (an API-only draft cannot
287
+ produce visual evidence); some project in `session.spec.projects` appears in
288
+ no journey's `projects`; some name from
289
+ `managedSpawnNamesForSession(session)` appears in no journey's `services`; or
290
+ a requested capture type has no planned artifact of its extension
291
+ (`screenshots` ⇒ `.png`, `video` ⇒ `.webm`). Each rejection is a distinct,
292
+ actionable message naming the missing projects / services / evidence type.
293
+ - **Placement.** The gate runs inside `handleDraftComplete` **before**
294
+ `markAwaitingApproval` and **before** the auto-mode EXECUTE queue, so a
295
+ rejected draft is neither parked for human approval nor chained into
296
+ EXECUTE. A rejection calls `markFailed` with
297
+ `failure_class: 'qa-session-draft-visual-coverage'` and the joined error; the
298
+ auto-path `qa-run` created moments earlier is terminalized as `errored`
299
+ rather than left pending. An accepted manifest is persisted as
300
+ `session.visualJourneyManifest` for the EXECUTE-side evidence check to
301
+ validate against.
196
302
 
197
303
  3. **EXECUTE** (`playbooks/qa-session-execute.md`) runs the drafted test
198
304
  against the live spawn via the runner's `executeBrief` hook, captures
@@ -200,34 +306,141 @@ routing default):
200
306
  the injected absolute `qa_result_sidecar` path — the same single-use sidecar
201
307
  the `qa-validate` runbook flow uses. Artifacts go to the injected absolute
202
308
  `qa_artifacts_dir` keyed by the linked QA run id, not the session id. The
203
- `engine/lifecycle.js#runPostCompletionHooks` `meta.qaRunId` hook ingests
309
+ `engine/orchestration/lifecycle.js#runPostCompletionHooks` `meta.qaRunId` hook ingests
204
310
  the sidecar and marks the linked `qa-runs` record terminal; the
205
311
  session-level `handleExecuteComplete` then reads the `qa-run` terminal
206
312
  status and transitions `executing → done` (or `failed`).
207
313
 
314
+ ### EXECUTE evidence coverage (W-msb9kgs402813a97-c)
315
+
316
+ A green `qa-run` is necessary but **not sufficient** for a session that asked
317
+ for screenshots or video. Before this gate, `handleExecuteComplete` went to
318
+ `done` on `qaRunStatus === 'passed'` with no evidence check at all, and
319
+ `normalizeArtifact` accepted any `{type, path}` without touching the
320
+ filesystem — so a run whose only green assertions were API calls closed a
321
+ visual session, and a multi-project session could pass on evidence from a
322
+ single project.
323
+
324
+ - **Sidecar extension.** The `qa-run-result.json` contract gains
325
+ `journeyCoverage: [{ journeyId, status: 'passed'|'failed'|'skipped',
326
+ projects[], services[] }]` plus optional per-artifact `journeyId` and
327
+ `project`. `engine/qa/runs.js#normalizeJourneyCoverage` drops malformed
328
+ entries (unknown status, missing id, duplicates) rather than repairing them,
329
+ and `completeRun` stamps `run.journeyCoverage` **only** when the caller
330
+ supplied it, so legacy `qa-validate` runs keep their historical record shape.
331
+ - **Validator.** `engine/qa/visual-journey.js#validateEvidenceCoverage({
332
+ session, manifest, run, artifactsDir })` returns
333
+ `{ ok, required, missing: { files, journeys, projects, services,
334
+ evidenceTypes }, errors, error }`. For each requested capture type it asserts
335
+ (a) at least one artifact of that type is registered on the run,
336
+ (b) every such artifact resolves **inside** `qaArtifactsDirForRun(runId)`
337
+ (`shared.isPathInside`) and exists on disk as a non-zero-size regular file,
338
+ and (c) the union of `journeyCoverage[].projects` over **passed** journeys
339
+ covers every `session.spec.projects`, the union of `.services` covers every
340
+ `managedSpawnNamesForSession(session)` name, and every journey the DRAFT
341
+ manifest declared actually reported `passed`.
342
+ - **Manifest delivery.** Check (c) grades the run against journey ids the DRAFT
343
+ agent invented, so the EXECUTE prompt has to carry them: the DRAFT sidecar is
344
+ read-then-unlinked and the accepted manifest survives only on the session
345
+ record. `engine.js#buildQaExecuteJourneyContract(session)` projects
346
+ `session.visualJourneyManifest` into the sidecar's own coverage-entry shape
347
+ (`[{journeyId, name, kind, projects, services}]`) and
348
+ `_qaVisualJourneyManifestJson(item)` renders it as the
349
+ `{{visual_journey_manifest_json}}` template var consumed by
350
+ `playbooks/qa-session-execute.md`. It is EXECUTE-only (the manifest does not
351
+ exist before DRAFT produces it, and re-draft must not be shown the previous
352
+ attempt's ids), bounded independently of the DRAFT-side manifest caps, and
353
+ degrades to `''` for logs-only, legacy, and non-QA dispatches. Without it a
354
+ flawless visual run would always fail `qa-session-evidence-incomplete`.
355
+ - **Gate.** Run inside `handleExecuteComplete` **before** DONE is chosen. A
356
+ coverage failure **overrides** a green `qaRunStatus`: the session transitions
357
+ to `failed` with `failure_class: 'qa-session-evidence-incomplete'` and an
358
+ `error` naming exactly what was missing. An already-failing run keeps its own
359
+ `qa-session-execute-failed` / `-errored` class — the gate only ever downgrades
360
+ a would-be `done`. It is skipped entirely when both capture flags are false
361
+ or no `visualJourneyManifest` is persisted (a legacy session), so non-visual
362
+ and pre-manifest completions are untouched. An internal error inside the gate
363
+ fails **closed**.
364
+
208
365
  Each phase WI carries `meta.sessionId`, `meta.sessionPhase`, `meta.qaSession`
209
366
  (target + flowsRaw + mode + capture + runner), and `meta.playbook`. The
210
367
  EXECUTE WI additionally carries `meta.qaRunId` so the existing qa-run
211
368
  lifecycle hook fires.
212
369
 
370
+ ### Co-service spawn metadata (W-msb9kgs402813a97-a)
371
+
372
+ A multi-project session owns one managed-spawn per project, but DRAFT/EXECUTE
373
+ historically only saw the PRIMARY one — a co-service origin was unaddressable
374
+ from the drafted test. `engine.js#buildQaSessionServices` now iterates
375
+ `qaSessions.managedSpawnNamesForSession(session)` and builds a bounded array
376
+ (cap 8) of
377
+
378
+ ```json
379
+ [{ "name": "qa-session-<id>-api", "project": "api", "primary": false,
380
+ "health": "healthy", "baseUrl": "http://localhost:4000", "ports": [4000] }]
381
+ ```
382
+
383
+ `health` is `healthy` | `unhealthy` (alive, failing its healthcheck) | `down`
384
+ (registered, not alive) | `missing` (no spec registered yet — the normal SETUP
385
+ state). It is surfaced two ways:
386
+
387
+ - as the `{{session_services_json}}` template var (registered in
388
+ `PLAYBOOK_OPTIONAL_VARS`, rendered by `playbooks/qa-session-draft.md` and
389
+ `playbooks/qa-session-execute.md` with prose stating each entry is a **real,
390
+ separately-reachable origin**, not a route on the primary), and
391
+ - as `briefOpts.services` on the runner adapter `generateBrief()` /
392
+ `executeBrief()` calls, so an adapter can address a co-service directly.
393
+
394
+ `managed_spawn_name` is unchanged and still names the **primary** spawn.
395
+ Discovery never fails a render: an unreadable managed-process store degrades to
396
+ `missing` rows and an empty string.
397
+
398
+ ### Who dispatches a phase (W-ms9nq2pu00cd60f3)
399
+
400
+ `engine/qa/sessions.js` **persists** each phase work item and requests an
401
+ engine wakeup (`control.json._wakeupAt`); it never enqueues a dispatch record
402
+ of its own. The engine's normal work-discovery pass
403
+ (`engine.js#discoverFromWorkItems` for project scopes,
404
+ `#discoverCentralWorkItems` for central) builds the dispatch, and it is the
405
+ only place that stamps `meta.project`, sets `meta.source` (which
406
+ `lifecycle.js#resolveWorkItemScope` needs to route the completion back to the
407
+ session), routes an agent, derives the branch, honours the branch mutex, and
408
+ renders the phase playbook prompt.
409
+
410
+ Phase work items are `WORK_TYPE.TEST`, which is in
411
+ `shared.WORKTREE_REQUIRING_TYPES`, so their project association is
412
+ load-bearing: `spawnAgent` resolves the worktree rootDir from it, and without
413
+ one it falls back to MINIONS_DIR's parent — which collapses to a drive root on
414
+ installs where MINIONS_DIR sits one level below it (`D:\squad-opg` → `D:\`) and
415
+ fails non-retryably with `failure_class: worktree-preflight`. So
416
+ `_persistWorkItem` **binds** `wi.project` to the SQL scope the item is stored
417
+ in (a scope is `project.name` or the `central` sentinel) rather than trusting
418
+ the builders' `opts.project || spec.project || null` fallback chain, and a
419
+ project/scope disagreement throws instead of persisting an item the engine can
420
+ never place. Central scope has no owning project to bind: a declared project
421
+ there is preserved (that is how `discoverCentralWorkItems` still resolves a
422
+ rootDir), and a genuinely project-less session stays project-less — the
423
+ preflight is not masked.
424
+
213
425
  ## Endpoints
214
426
 
215
427
  Documented in `dashboard.js`; routes are visible at `GET /api/routes`.
216
428
 
217
429
  | Method | Path | Behavior |
218
430
  |--------|--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
219
- | POST | `/api/qa/session` | Create session; validates spec, calls `createSession` + `queueSetup` (`pending → spawning`). Body accepts `project: string` (single) OR `projects: string[]` (multi, ≤5; first is primary, rest are co-services). Returns `sessionId`, `setupWorkItemId` (primary's WI), `managedSpawnName`, and (multi only) `projects: string[]`. |
220
- | POST | `/api/qa/from-prd` | "qa this PRD" bridge. Build a QA Session spec from a completed/approved PRD via `engine/qa-from-prd.js` (pure) then reuse `createSession` + `queueSetup`. Body: `{ prd, mode?, runner? }`. Non-ready / unknown / malformed PRD → **400** (`QaFromPrdError.reason`). Returns `sessionId`, `state`, `setupWorkItemId`, `managedSpawnName`, `projects`, `flowSource`, `warnings[]`, `note` (repo-native-harness copy), and optional `runnerHint`. The test is drafted on the repo's native test harness. |
221
- | GET | `/api/qa/sessions` | List sessions newest-first. Optional `?limit=N` and `?state=pending\|spawning\|drafting\|awaiting-approval\|executing\|done\|failed\|killed`. |
431
+ | POST | `/api/qa/session` | Create session; validates spec, calls `createSession` + `queueSetup` (`pending → spawning`). Body accepts `project: string` (single) OR `projects: string[]` (multi, ≤5), plus the optional `primaryProject: string` (a member of `projects`; defaults to `projects[0]`) and `projectTargets: { [project]: target }` (per-project target overrides; each key a member of `projects`, each value validated like `target`). Returns `sessionId`, `setupWorkItemId` (primary's WI), `managedSpawnName`, and (multi only) `projects: string[]`, primary-first. |
432
+ | POST | `/api/qa/from-prd` | "qa this PRD" bridge. Build a QA Session spec from a completed/approved PRD via `engine/qa/from-prd.js` (pure) then reuse `createSession` + `queueSetup`. Body: `{ prd, mode?, runner? }`. Non-ready / unknown / malformed PRD → **400** (`QaFromPrdError.reason`). Returns `sessionId`, `state`, `setupWorkItemId`, `managedSpawnName`, `projects`, `flowSource`, `warnings[]`, `note` (repo-native-harness copy), and optional `runnerHint`. The test is drafted on the repo's native test harness. |
433
+ | GET | `/api/qa/sessions` | List sessions newest-first. Optional `?limit=N` and `?state=pending\|spawning\|drafting\|awaiting-approval\|executing\|done\|failed\|killed`. Each record carries a bounded `linkedRun` projection — `{ id, status, artifacts: [{type, path, label}] }` for a session with a `qaRunId` (primary image/video artifacts only, capped at `QA_PRIMARY_ARTIFACT_DEFAULT_MAX`), else `null`. Built from ONE `listRuns` read indexed by id, and fully try/catch'd: a deleted run or an unreadable runs store degrades to `linkedRun: null`, never a 500. |
222
434
  | GET | `/api/qa/sessions/<id>` | Fetch a single session record by id. |
223
435
  | POST | `/api/qa/sessions/<id>/approve` | `awaiting-approval → executing`. Accepts no request body. Server-side creates the linked `qa-runs` record (synthetic `runbookId='qa-session-<id>'`), queues EXECUTE WI, stamps `qaRunId` on the session. |
224
436
  | POST | `/api/qa/sessions/<id>/edit` | `awaiting-approval → drafting`. Body: `{ feedback }`. Re-fires DRAFT with the reviewer feedback threaded into the prompt. |
225
437
  | POST | `/api/qa/sessions/<id>/cancel` | Non-terminal → `killed`. Optional `{ reason }`. Does NOT touch the managed-spawn — use `/kill` for that. |
226
438
  | POST | `/api/qa/sessions/<id>/kill` | Non-terminal → `killed`, then generation-safely removes the primary and every multi-project co-service managed-spawn. Best-effort when a spawn is absent or concurrently replaced. |
227
439
  | POST | `/api/qa/sessions/<id>/dismiss` | Non-terminal → `done`. Accept the draft as final; leaves spawn alive. Optional `{ summary }`. |
440
+ | DELETE | `/api/qa/sessions/<id>` | Hard-delete a terminal session record (`done`/`failed`/`killed` only). Accepts no request body. Returns `{ ok: true, id, state, testDirRemoved, qaRun, managedSpawnsLive, managedSpawnsKnown }`. `409 not_terminal` (with `currentState`) for an in-flight session, `404 not_found` for unknown ids, `400` for an unsafe id. See "Deleting sessions". |
228
441
  | GET | `/api/qa/runners` | List registered runner adapters (built-ins + `qa-runners.d/` plugins). Metadata only — hooks (functions) are stripped. |
229
442
  | POST | `/api/qa/runners/reload` | Accepts no request body. Clears the in-process registry, re-registers built-ins, re-scans `qa-runners.d/` for plugin edits, and returns the fresh runner list. |
230
- | DELETE | `/api/qa/runs/<id>` | Hard-delete a single QA run record (terminal-status runs only) and best-effort wipe its artifact directory under `engine/qa-artifacts/<runId>/`. Returns `{ ok: true, id, artifactsRemoved }` on success, `409 not_terminal` for in-flight runs, `404 not_found` for unknown ids. |
443
+ | DELETE | `/api/qa/runs/<id>` | Hard-delete a single QA run record (terminal-status runs only) and best-effort wipe its artifact directory under `engine/qa-artifacts/<runId>/`. Returns `{ ok: true, id, artifactsRemoved, deletedAs }` on success, `409 not_terminal` for in-flight runs, `404 not_found` for unknown ids. `?orphaned=1` additionally removes a provably-orphaned abandoned pending run, or returns `409 not_orphaned` with the failed condition in `reason`. |
231
444
 
232
445
  The audited contracts returned by `GET /api/routes` expose the canonical enum,
233
446
  length, list, nested-field, path, and lifecycle constraints for these routes.
@@ -243,6 +456,38 @@ errors are mapped to HTTP via `_qaSessionsErrorToStatus`:
243
456
  - `'unsafe sessionId'` / `'invalid spec'` / `'requires …'` / `'exceeds …'` (spec or feedback validation) → 400
244
457
  - `'illegal state transition'` / `'requires state …'` / `'requires non-terminal'` → 409
245
458
 
459
+ ## Deleting sessions
460
+
461
+ `DELETE /api/qa/sessions/<id>` (`engine/qa/sessions.js#deleteSession`) exists so
462
+ an operator can clear failed/completed/killed rows out of the dashboard's
463
+ "Recent Sessions" list without hand-editing `engine/state.db`. It is the session
464
+ counterpart of `DELETE /api/qa/runs/<id>` and follows the same shape: the record
465
+ is removed inside `shared.mutateQaSessions` (which emits the `qa_sessions` state
466
+ event), and every filesystem / cross-store side effect runs outside the lock.
467
+
468
+ **Terminal-only.** Only `done`, `failed`, and `killed` are deletable. Any other
469
+ state returns `409 { error: 'not_terminal', currentState }` — cancel or kill the
470
+ session first (`/cancel`, `/kill`), then delete. A refused delete touches no
471
+ linked resource.
472
+
473
+ **Linked resources** are cleaned up only where ownership is unambiguous:
474
+
475
+ | Resource | Behavior |
476
+ |----------|----------|
477
+ | `engine/qa-tests/<id>/` | Removed. The directory is session-owned by construction (`createSession` creates it and the directory name *is* the session id), and removal repeats the `path.resolve` + prefix sandbox used for artifact directories, so only the per-session directory can be reclaimed — never the `qa-tests` root. Reported as `testDirRemoved`. |
478
+ | `session.qaRunId` | Cascaded through `qaRuns.deleteQaRun` **only** when the run still exists and carries the synthetic `runbookId === 'qa-session-<id>'` stamped by `/approve` and the auto-mode chain. Otherwise left in place, with the reason reported in `qaRun.reason`: `no_linked_run`, `not_found` (already deleted), `not_session_owned` (a real runbook owns it), `not_terminal` (+ `currentStatus` — a live run is never vacuumed out from under its agent), `lookup_failed`, or `delete_failed`. `deleteQaRun`'s own terminal-only guard is the second line of defence. |
479
+ | Managed-spawn processes | **Never killed.** A QA managed-spawn can outlive its session, so killing a live process as a side effect of deleting a history record would be a surprise. Live spawn names are reported in `managedSpawnsLive` (with `managedSpawnsKnown: false` when the managed-process store could not be read, so an empty list is not mistaken for "nothing live"); kill them deliberately via `POST /api/managed-processes/kill`. |
480
+ | Work items | Untouched. The SETUP/DRAFT/EXECUTE work items are dispatch history and are not owned by the session record. |
481
+
482
+ **Repeat and missing-link behavior is safe.** Deleting twice returns a plain
483
+ `404 not_found`; a linked run that a previous call (or a manual
484
+ `DELETE /api/qa/runs/<id>`) already removed reports
485
+ `qaRun.reason: 'not_found'` instead of failing the request. Note that removing a
486
+ session also removes the `session_owned` evidence
487
+ `evaluateOrphanedPendingRun` consults, so a still-`pending` run left behind
488
+ becomes eligible for the explicit `DELETE /api/qa/runs/<id>?orphaned=1` escape
489
+ hatch once its other conditions hold.
490
+
246
491
  ## File locations
247
492
 
248
493
  - **Session state**: `engine/state.db` `qa_sessions` (all projects,
@@ -262,7 +507,7 @@ errors are mapped to HTTP via `_qaSessionsErrorToStatus`:
262
507
 
263
508
  ## Runner adapters (P-c4a9e7f3 / P-b8e1d4a6)
264
509
 
265
- Pluggable test-runner registry at `engine/qa-runners.js`. Built-in
510
+ Pluggable test-runner registry at `engine/qa-runners/`. Built-in
266
511
  adapters: `playwright` (priority 50, always-true safe default),
267
512
  `maestro` (priority 80, detects `.maestro/` and therefore wins when
268
513
  present). Each adapter exports five hooks:
@@ -282,7 +527,7 @@ present). Each adapter exports five hooks:
282
527
  Resolution order in `detectRunner(target, project, explicitRunner)`:
283
528
  explicit-name (no `detect` call, unknown names return null), then
284
529
  priority-desc iteration. Plugin folder: `<MINIONS_DIR>/qa-runners.d/*.js`
285
- (same trust level as `playbooks/` and `watches.d/`). Hot-reload via
530
+ (same trust level as `playbooks/` and `watch-plugins/`). Hot-reload via
286
531
  `POST /api/qa/runners/reload` (clears registry → re-registers built-ins →
287
532
  re-scans plugin dir) so plugin edits take effect without an engine
288
533
  restart.
@@ -290,7 +535,7 @@ restart.
290
535
  ## Fast-state slice
291
536
 
292
537
  `/api/status.qaSessions = { total, sig }` — the unsorted summary helper
293
- `engine/qa-sessions.js#summarizeSessionsForStatus()`. Mirrors `qaRuns` so
538
+ `engine/qa/sessions.js#summarizeSessionsForStatus()`. Mirrors `qaRuns` so
294
539
  the sidebar activity-dot lights up on any new session or state
295
540
  transition within one `/api/status` poll cycle (~4s). Do NOT call
296
541
  `listSessions({limit:50})` from this hot path — it sorts O(N log N) on
@@ -310,6 +555,18 @@ every fast-state rebuild.
310
555
  chip classes (`--done` / `--active` / `--pending` / `--failed` /
311
556
  `--killed`) per `session.state`. State-driven left-border color
312
557
  (red=failed, green=done, yellow=awaiting-approval, blue=active).
558
+ - **Linked run + evidence** (W-msb9kgs402813a97-d) — a card carrying a
559
+ `qaRunId` renders a `run:` chip in the meta row that scrolls the matching
560
+ Recent Runs row (`#qa-run-<id>`) into view, and a **terminal** card renders
561
+ the run's primary (image/video) artifacts through the same
562
+ `_qaRenderArtifactPreviews` helper the Recent Runs strip uses — so artifact
563
+ URLs always go through `/api/qa/artifacts/<runId>/<path>` and no filesystem
564
+ path is ever exposed. A terminal card whose run captured nothing says
565
+ "No screenshots or video captured for this run." rather than rendering
566
+ nothing: a session that captured plenty and one that captured nothing used
567
+ to be indistinguishable. The data comes from the bounded `linkedRun`
568
+ projection on `GET /api/qa/sessions` (see the endpoint table), not from the
569
+ Recent Runs poll, so card content does not depend on poll ordering.
313
570
  - **Action buttons** —
314
571
  `awaiting-approval` cards show `[Approve & run]` `[Edit]` `[Cancel]`;
315
572
  every non-terminal card shows `[Dismiss]` `[Kill spawn]` in the footer;
@@ -338,15 +595,36 @@ field gets the right audit trail.
338
595
 
339
596
  A completed/approved PRD can be QA'd in one shot — no hand-built spec. The
340
597
  bridge is `POST /api/qa/from-prd`, which turns the PRD into a QA Session
341
- spec via the **pure** spec-builder `engine/qa-from-prd.js`
598
+ spec via the **pure** spec-builder `engine/qa/from-prd.js`
342
599
  (`buildQaSessionSpecFromPrd`) and then reuses the **exact same**
343
600
  orchestration as `POST /api/qa/session` (`createSession` + `queueSetup`,
344
601
  `pending → spawning`). No forked dispatch logic — the SETUP → DRAFT →
345
602
  EXECUTE chain above runs verbatim.
346
603
 
604
+ That orchestration lives in **`engine/qa/prd-session.js`**
605
+ (`createQaSessionFromPrd`), not in the HTTP handler, so the automatic
606
+ post-verify path below creates identical sessions instead of forking the
607
+ route. The handler keeps only what is genuinely HTTP-specific (body
608
+ validation, header identity, status-code mapping, the runner hint). The one
609
+ injected seam is `resolveTargets`: the dashboard runs each project name
610
+ through its HTTP-input validator (bad project → 400 with an `ApiInputError`
611
+ path), while the engine uses `resolveTargetsFromConfig`, which walks the same
612
+ `shared.resolveProjectSource` seam against the live config.
613
+
614
+ Both resolvers stamp the caller's `requested` name next to the canonical
615
+ `project` and pipe the list through the shared
616
+ `orderTargetsByPrimary(spec, targets)`, so a spec's `primaryProject` leads the
617
+ resolved list and every entry carries a `primary` boolean. That lets
618
+ `canonicalizeResolvedProjects` rewrite `primaryProject` and the
619
+ `projectTargets` keys to canonical project names — without it, a PRD naming
620
+ `"Web"` would produce `projects: ["web"]` alongside `primaryProject: "Web"` and
621
+ `validateSpec` would reject a spec the caller wrote correctly.
622
+
347
623
  **Body:** `{ prd: "<file>.json", mode?, runner? }`. `prd` is a PRD filename
348
- resolved under `prd/`, then `prd/archive/` (traversal/absolute/drive-letter
349
- shapes are rejected). `mode` and `runner` are optional overrides.
624
+ resolved against the SQL PRD store (live bucket, then the archive bucket),
625
+ with a legacy `prd/` + `prd/archive/` filesystem fallback for pre-migration
626
+ installs. Traversal / absolute / drive-letter shapes are rejected. `mode` and
627
+ `runner` are optional overrides.
350
628
 
351
629
  **Completed/approved guard rail.** The builder rejects any PRD whose
352
630
  `status` is not in `{completed, approved}` (`QA_READY_STATUSES`) — drafting
@@ -407,6 +685,59 @@ alongside the usual `sessionId` / `state` / `setupWorkItemId` /
407
685
  Command Center exposes this as a natural-language shortcut — "qa this PRD"
408
686
  maps to `POST /api/qa/from-prd` (see `prompts/cc-system.md`).
409
687
 
688
+ ## Automatic QA for verified PRDs (`engine.autoQaCompletedPrds`, W-msal3ser01q4ccb3)
689
+
690
+ `engine.autoQaCompletedPrds` (default **false**, Settings → Plan & PR
691
+ Workflow → Plan) makes the engine start a PRD-driven QA Session by itself.
692
+ Resolved through `shared.resolveAutoQaCompletedPrds(engine)`; only an explicit
693
+ boolean counts, so a malformed `config.json` cannot silently enable an
694
+ automatic dispatch. When OFF, behaviour is byte-identical to today and the
695
+ manual endpoint above is unaffected either way.
696
+
697
+ **Trigger — verification, not the status flip.** The hook lives in
698
+ `engine/qa/auto-prd-qa.js#maybeAutoQaAfterVerify` and fires from
699
+ `engine/orchestration/lifecycle.js#runPostCompletionHooks` on a **successful,
700
+ non-skipped verify work item**. It deliberately does *not* key off the PRD's
701
+ top-level `status: completed`: `checkPlanCompletion` sets that flag **before**
702
+ creating the verify work item(s), so triggering there would race the verify
703
+ agent and miss the canonical guide. All four gates must hold:
704
+
705
+ 1. `engine.autoQaCompletedPrds` is on.
706
+ 2. The completing work item is a `verify` type with a `sourcePlan`.
707
+ 3. `prd/guides/verify-<plan>.md` exists — the artifact that proves
708
+ verification ran to completion. Flow *priority* is untouched: the builder
709
+ still prefers the verify guide, then `manual-qa-<plan>.md`, then
710
+ acceptance criteria.
711
+ 4. **Every** verify work item for that plan (across all project scopes) is in
712
+ a `DONE_STATUSES` state. A cross-repo plan therefore waits for its whole
713
+ per-project fan-out and launches **one** coherent session — not one per
714
+ verify completion. Any `failed` / `cancelled` / still-pending sibling
715
+ blocks QA entirely.
716
+
717
+ Automatic sessions use `mode: 'auto'` (SETUP → DRAFT → EXECUTE with no
718
+ approval step — nobody is standing by to approve the draft) and
719
+ `createdBy: 'engine:auto-qa-verified-prd'`. Their provenance carries an extra
720
+ `autoTrigger: { reason: 'verified-prd', generation, verifyWorkItemIds[],
721
+ triggeredBy, at }` block alongside the usual PRD provenance.
722
+
723
+ **Idempotency — a durable verify generation, not an in-memory flag.** The
724
+ dedupe key is `sha256(planFile + every verify WI's "id@completedAt")`,
725
+ truncated to 16 chars. It is CLAIMED inside a `prdStore.mutatePrd` transaction
726
+ (compare-and-set) before any session is created and stamped on the PRD row as
727
+ `_autoQa.launches[]` (bounded to the last 5 entries), so it survives ticks,
728
+ engine restarts, lifecycle retries, duplicate completion callbacks, and
729
+ concurrent cross-repo completions landing in the same tick. `completedAt` is
730
+ what makes a **reopened** PRD re-verifiable: `shared.reopenWorkItem` drops it,
731
+ so the next successful completion produces a different generation and may
732
+ legitimately launch a new session — while the *same* generation always
733
+ collapses to exactly one.
734
+
735
+ **Failure is bounded and never regresses the PRD.** A rejected launch records
736
+ `status: 'error'` on the launch entry and **keeps** the claim; not releasing it
737
+ is what stops a broken QA path from re-firing every tick. The hook never
738
+ throws (every failure degrades to a warn), never writes PRD completion state,
739
+ and QA failure stays visible in QA state only.
740
+
410
741
  ## When something goes wrong
411
742
 
412
743
  - **SETUP managed-spawn won't validate** → session lands in `failed` with
@@ -419,10 +750,29 @@ maps to `POST /api/qa/from-prd` (see `prompts/cc-system.md`).
419
750
  the injected
420
751
  `engine/qa-tests/<sessionId>/` directory. The session failure class is
421
752
  `qa-session-draft-failed` when the agent reports that contract failure.
753
+ - **DRAFT succeeded but the session failed anyway** → `failure_class:
754
+ 'qa-session-draft-visual-coverage'`. The session asked for screenshots or
755
+ video and the agent's visual-journey manifest
756
+ (`agents/<agentId>/qa-session-draft-result.json`, see
757
+ `engine/qa/visual-journey.js`) was missing, API-only, or did not cover every
758
+ project / managed-spawn / requested capture type. `session.error` names
759
+ exactly what is missing; POST `/api/qa/sessions/<id>/edit` is not available
760
+ (the session is terminal), so fix the flows/capture spec and create a new
761
+ session, or re-run DRAFT via a new session with the same target.
422
762
  - **EXECUTE qa-run terminal status is `failed`/`errored`** →
423
763
  `failure_class: 'qa-session-execute-failed'` /
424
764
  `'qa-session-execute-errored'`. The linked `qa-runs` record (joined via
425
765
  `session.qaRunId`) carries the agent's `summary` and artifact list.
766
+ - **EXECUTE qa-run passed but the session failed anyway** → `failure_class:
767
+ 'qa-session-evidence-incomplete'`. The session asked for screenshots or video
768
+ and `validateEvidenceCoverage` could not prove the evidence: a requested
769
+ capture type had no registered artifact, a registered artifact was missing /
770
+ zero-byte / outside `engine/qa-artifacts/<runId>/`, a project or managed-spawn
771
+ appeared in no **passed** `journeyCoverage` entry, or a journey the DRAFT
772
+ manifest declared never reported `passed`. `session.error` names exactly what
773
+ was missing; the linked run keeps its own `passed` status, so compare
774
+ `run.artifacts` + `run.journeyCoverage` against
775
+ `session.visualJourneyManifest` to see the gap.
426
776
  - **Want to start over after seeing a bad draft** → POST
427
777
  `/api/qa/sessions/<id>/edit` with `{ feedback: "…" }`; do NOT
428
778
  `/cancel` + create a new session unless the original spec was wrong
@@ -16,7 +16,7 @@ scoped to a single project lives under its `projects/<name>/` state dir
16
16
  rather than a root-level `runbooks/` directory. Two reasons:
17
17
 
18
18
  1. **Lifecycle parity with the project.** When a project is removed via
19
- `engine/projects.js removeProject`, its `projects/<name>/` dir is
19
+ `engine/planning/projects.js removeProject`, its `projects/<name>/` dir is
20
20
  archived as one unit. Co-locating runbooks under that dir means they
21
21
  travel with the project rather than dangling in a global `runbooks/`
22
22
  that has no relationship to the project being removed.
@@ -76,7 +76,7 @@ Responses:
76
76
 
77
77
  ## Module
78
78
 
79
- `engine/qa-runbooks.js` exports:
79
+ `engine/qa/runbooks.js` exports:
80
80
 
81
81
  ```js
82
82
  {
@@ -99,7 +99,7 @@ unlink).
99
99
 
100
100
  The deferred follow-up items (W-mpeiwz6k0005bf34-b/c/d) have since landed. Brief pointers — see [CLAUDE.md](../CLAUDE.md) → "QA validation runs" for the deep dive:
101
101
 
102
- - **Run dispatch + persistence** (`engine/qa-runs.js`): `POST /api/qa/runbooks/run` creates a SQL QA-run record with `status ∈ pending|running|passed|failed|errored` and dispatches a `qa-validate` work item against the runbook's `targetName`. Read via `GET /api/qa/runs?limit=N&status=...` and `GET /api/qa/runs/<id>`.
102
+ - **Run dispatch + persistence** (`engine/qa/runs.js`): `POST /api/qa/runbooks/run` creates a SQL QA-run record with `status ∈ pending|running|passed|failed|errored` and dispatches a `qa-validate` work item against the runbook's `targetName`. Read via `GET /api/qa/runs?limit=N&status=...` and `GET /api/qa/runs/<id>`.
103
103
  - **Artifact contract**: the engine pre-creates `engine/qa-artifacts/<runId>/`; the `qa-validate` agent writes files there and records run-root-relative paths in the injected absolute `agents/<id>/qa-run-result.json` path. The engine clears that sidecar before dispatch, verifies its `runId`, and removes it after consumption. Lifecycle persists the metadata, and `GET /api/qa/artifacts/<runId>/<path>` serves root-level or nested files behind traversal guards (403 on escape). There is no `engine.qaArtifactsMaxBytes` setting or copy-on-completion gate.
104
104
  - **UI**: `/qa` dashboard page (`dashboard/pages/qa.html`, `dashboard/js/qa.js`) polls `GET /api/qa/runs` every 5s while active; auto-detects screenshots/videos/logs for inline preview.
105
105
  - **Playbook**: `playbooks/qa-validate.md` (routed via the synthetic `qa-validate` task-type in `routing.md`).