@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
@@ -7,8 +7,8 @@
7
7
  // happening and exposes per-row operational controls.
8
8
  //
9
9
  // Sections (top → bottom):
10
- // 1. Active QA Sessions — non-terminal sessions only, with a CC-pitch card
11
- // at the top and a collapsed disclosure for terminal sessions (last 10).
10
+ // 1. Active QA Sessions — non-terminal sessions only, opening straight on the
11
+ // session list, with a collapsed disclosure for terminal sessions (last 10).
12
12
  // Per-card controls: approve / edit / cancel / dismiss / kill.
13
13
  // 2. Live Targets — slim dedup'd list of /api/managed-processes +
14
14
  // /api/keep-processes with a health badge and a link to /engine
@@ -17,8 +17,11 @@
17
17
  // while the QA page is active. Each row links to the live agent stream
18
18
  // and renders inline artifact previews via /api/qa/artifacts/<runId>/<file>
19
19
  // (screenshots as <img>, videos as <video>, logs as 40-line text preview
20
- // with a "View full" link). No direct filesystem paths are exposed
21
- // artifact URLs always go through /api/qa/artifacts/.
20
+ // with a "View full" link). For a run in a terminal state only the
21
+ // screenshots + video stay inline; every other artifact collapses behind a
22
+ // "Show all artifacts (N)" disclosure (W-ms9p62od00kt5b58). No direct
23
+ // filesystem paths are exposed — artifact URLs always go through
24
+ // /api/qa/artifacts/.
22
25
  // 4. Validation Runbooks — collapsed disclosure with a small CC-pitch and
23
26
  // the runbook list. Each row has a Run + Delete button.
24
27
  //
@@ -30,6 +33,27 @@
30
33
  let _qaRunsPollInterval = null;
31
34
  let _qaRunbooksCache = [];
32
35
 
36
+ // W-ms9p62od00kt5b58 — state that must survive loadQaRuns()'s 5s
37
+ // replaceChildren() of the whole runs list.
38
+ // - _qaOpenArtifactGroups: run ids whose "Show all artifacts" disclosure the
39
+ // user opened. Without it the re-render would snap every group shut.
40
+ // - _qaLogPreviewCache: fetched preview text keyed by artifact URL. The
41
+ // per-element `__qaLoaded` flag dies with the node on every poll, so an
42
+ // opened group would otherwise refetch its previews every 5 seconds.
43
+ const _qaOpenArtifactGroups = new Set();
44
+ const QA_LOG_PREVIEW_CACHE_MAX = 200;
45
+ const _qaLogPreviewCache = new Map();
46
+
47
+ function _qaCacheLogPreview(url, text) {
48
+ if (!url) return;
49
+ // Re-insert so the Map's insertion order doubles as an LRU queue.
50
+ if (_qaLogPreviewCache.has(url)) _qaLogPreviewCache.delete(url);
51
+ _qaLogPreviewCache.set(url, text);
52
+ while (_qaLogPreviewCache.size > QA_LOG_PREVIEW_CACHE_MAX) {
53
+ _qaLogPreviewCache.delete(_qaLogPreviewCache.keys().next().value);
54
+ }
55
+ }
56
+
33
57
  function _stopQaRunsPoll() {
34
58
  if (_qaRunsPollInterval) {
35
59
  clearInterval(_qaRunsPollInterval);
@@ -306,7 +330,7 @@ async function loadQaRuns() {
306
330
  return;
307
331
  }
308
332
  const rows = items.map(_qaRenderRunRow).join('');
309
- // eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: run id, status, runbook, target, timestamp, workItemId, agentId, artifact file names)
333
+ // eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: run id, status, runbook, target, timestamp, workItemId, agentId, artifact file names); the secondary-artifact count is a computed number
310
334
  const frag = document.createRange().createContextualFragment(rows);
311
335
  root.replaceChildren(frag);
312
336
  }
@@ -346,8 +370,8 @@ function _qaRenderRunRow(run) {
346
370
  const actionsHtml = (liveLink || deleteBtn)
347
371
  ? '<span class="qa-run-actions">' + liveLink + deleteBtn + '</span>'
348
372
  : '';
349
- const artifactsHtml = _qaRenderArtifactPreviews(id, run.artifacts || []);
350
- return '<div class="qa-run-row" data-run-id="' + escHtml(id) + '">' +
373
+ const artifactsHtml = _qaRenderArtifactPreviews(id, run.artifacts || [], status);
374
+ return '<div class="qa-run-row" id="qa-run-' + escHtml(id) + '" data-run-id="' + escHtml(id) + '">' +
351
375
  '<div class="qa-run-head">' +
352
376
  '<span class="qa-run-status ' + escHtml(statusClass) + '">' + escHtml(status) + '</span>' +
353
377
  '<span class="qa-run-name">' + escHtml(runbook) + '</span>' +
@@ -360,7 +384,7 @@ function _qaRenderRunRow(run) {
360
384
  }
361
385
 
362
386
  // W-mpxp90xs000i5732 — terminal status set kept in sync with
363
- // engine/qa-runs.js#TERMINAL_STATUSES. The Delete button only renders for
387
+ // engine/qa/runs.js#TERMINAL_STATUSES. The Delete button only renders for
364
388
  // runs in one of these states; the engine's deleteQaRun returns 409 for
365
389
  // anything else (defense in depth).
366
390
  function _qaIsTerminalStatus(status) {
@@ -408,9 +432,30 @@ async function qaDeleteRun(id) {
408
432
  }
409
433
  }
410
434
 
411
- function _qaRenderArtifactPreviews(runId, artifacts) {
435
+ // W-ms9p62od00kt5b58 artifact triage.
436
+ //
437
+ // A COMPLETED run used to render every artifact inline at equal weight:
438
+ // screenshots and video next to .json blobs, .md reports, and a binary
439
+ // trace.zip garbled into a <pre> by the text-preview fetcher. The final
440
+ // screenshots + video ARE the result of a QA run; everything else is
441
+ // supporting material.
442
+ //
443
+ // So for a run in a terminal state (see _qaIsTerminalStatus) primary artifacts
444
+ // render inline as before and the rest collapse behind one native <details>.
445
+ // A non-terminal run keeps the old flat layout on purpose — while a run is
446
+ // still going the logs are the signal, so hiding them would be a regression.
447
+ function _qaRenderArtifactPreviews(runId, artifacts, status) {
412
448
  if (!runId || !Array.isArray(artifacts) || !artifacts.length) return '';
413
- const parts = [];
449
+ const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'];
450
+ const VIDEO_EXTS = ['mp4', 'webm', 'ogg', 'mov'];
451
+ // Extensions whose bytes are NOT text. Fetching these into a <pre> is what
452
+ // rendered trace.zip as mojibake; they get a download link and no preview
453
+ // fetch at all. An artifact with no extension is treated the same way —
454
+ // unknown content is not assumed to be safe to inline as text.
455
+ const BINARY_EXTS = ['zip', 'gz', 'tgz', 'tar', 'bz2', 'xz', 'zst', 'br', '7z', 'rar',
456
+ 'pdf', 'wasm', 'exe', 'dll', 'so', 'dylib', 'bin', 'db', 'sqlite', 'ico'];
457
+ const primary = [];
458
+ const secondary = [];
414
459
  for (const a of artifacts) {
415
460
  // Canonical records use { path, type }; retain the retired { file, kind }
416
461
  // shape for older persisted runs.
@@ -428,25 +473,36 @@ function _qaRenderArtifactPreviews(runId, artifacts) {
428
473
  const typeHint = String(typeRaw).toLowerCase();
429
474
  const ext = (file.match(/\.([a-z0-9]+)$/i) || ['', ''])[1].toLowerCase();
430
475
  const url = '/api/qa/artifacts/' + encodeURIComponent(runId) + '/' + encodeURIComponent(artifactPath);
431
- if (typeHint === 'screenshot' || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {
432
- parts.push(
476
+ if (typeHint === 'screenshot' || IMAGE_EXTS.includes(ext)) {
477
+ primary.push(
433
478
  '<div class="qa-artifact qa-artifact-image">' +
434
479
  '<img src="' + escHtml(url) + '" alt="' + escHtml(artifactPath) + '" loading="lazy">' +
435
480
  '<div class="qa-artifact-caption">' + escHtml(artifactPath) + '</div>' +
436
481
  '</div>'
437
482
  );
438
- } else if (typeHint === 'video' || ['mp4', 'webm', 'ogg', 'mov'].includes(ext)) {
439
- parts.push(
483
+ } else if (typeHint === 'video' || VIDEO_EXTS.includes(ext)) {
484
+ primary.push(
440
485
  '<div class="qa-artifact qa-artifact-video">' +
441
486
  '<video controls src="' + escHtml(url) + '"></video>' +
442
487
  '<div class="qa-artifact-caption">' + escHtml(artifactPath) + '</div>' +
443
488
  '</div>'
444
489
  );
490
+ } else if (!ext || BINARY_EXTS.includes(ext)) {
491
+ // Binary / unknown — link only. Deliberately carries no data-qa-log-url,
492
+ // so _qaFillLogPreviews never fetches it.
493
+ secondary.push(
494
+ '<div class="qa-artifact qa-artifact-binary">' +
495
+ '<div class="qa-artifact-caption">' +
496
+ escHtml(artifactPath) + ' ' +
497
+ '<a href="' + escHtml(url) + '" target="_blank" rel="noopener" download class="qa-artifact-fulllink">View full</a>' +
498
+ '</div>' +
499
+ '</div>'
500
+ );
445
501
  } else {
446
502
  // Log / text — render an asynchronously-filled <pre>; first 40 lines
447
503
  // fetched lazily so we don't N+1-flood the network on every poll.
448
504
  const blockId = 'qa-log-' + runId + '-' + artifactPath.replace(/[^a-zA-Z0-9_-]/g, '_');
449
- parts.push(
505
+ secondary.push(
450
506
  '<div class="qa-artifact qa-artifact-log">' +
451
507
  '<div class="qa-artifact-caption">' +
452
508
  escHtml(artifactPath) + ' ' +
@@ -457,28 +513,91 @@ function _qaRenderArtifactPreviews(runId, artifacts) {
457
513
  );
458
514
  }
459
515
  }
460
- return parts.join('');
516
+ const primaryHtml = primary.join('');
517
+ if (!secondary.length) return primaryHtml;
518
+ if (!_qaIsTerminalStatus(status)) return primaryHtml + secondary.join('');
519
+ // Open-state is tracked outside the DOM because loadQaRuns() replaces the
520
+ // whole list every 5s. Resolved defensively so the renderer stays usable in
521
+ // isolation (same pattern as qaFocusCommandCenter's _ccOpen lookup).
522
+ const openGroups = (typeof _qaOpenArtifactGroups !== 'undefined') ? _qaOpenArtifactGroups : null;
523
+ const isOpen = !!(openGroups && openGroups.has(runId));
524
+ return primaryHtml +
525
+ '<details class="qa-artifact-secondary" data-run-id="' + escHtml(runId) + '"' + (isOpen ? ' open' : '') + '>' +
526
+ '<summary class="qa-artifact-secondary-summary">Show all artifacts (' + secondary.length + ')</summary>' +
527
+ '<div class="qa-artifact-secondary-body">' + secondary.join('') + '</div>' +
528
+ '</details>';
461
529
  }
462
530
 
463
531
  // Lazily fetch log previews after the runs DOM is in place. Runs every time
464
532
  // loadQaRuns finishes; cheap because the `data-qa-log-url` lookup short-
465
533
  // circuits on already-loaded blocks.
534
+ //
535
+ // W-ms9p62od00kt5b58 — two rules now bound the fetching:
536
+ // 1. Previews inside a COLLAPSED <details> are not fetched. A completed run
537
+ // hides its logs/json behind one disclosure, and filling them eagerly
538
+ // would fire N background requests for content nobody is looking at.
539
+ // Each group is instead wired once to fill on its first open (`toggle`).
540
+ // 2. Preview text is memoized by URL, so re-opening a group — or the 5s
541
+ // poll re-rendering an already-open one — reuses the fetched text
542
+ // instead of refetching it.
466
543
  function _qaFillLogPreviews() {
467
- const blocks = document.querySelectorAll('#qa-runs-content .qa-artifact-log-preview[data-qa-log-url]');
468
- blocks.forEach(function (el) {
544
+ const fill = function (el) {
469
545
  const url = el.getAttribute('data-qa-log-url');
470
546
  if (!url || el.__qaLoaded) return;
471
547
  el.__qaLoaded = true;
548
+ if (_qaLogPreviewCache.has(url)) {
549
+ el.textContent = _qaLogPreviewCache.get(url);
550
+ return;
551
+ }
472
552
  fetch(url)
473
553
  .then(function (r) { return r.ok ? r.text() : ''; })
474
554
  .then(function (txt) {
475
555
  const head = (txt || '').split(/\r?\n/).slice(0, 40).join('\n');
556
+ _qaCacheLogPreview(url, head || '(empty)');
476
557
  el.textContent = head || '(empty)';
477
558
  })
478
559
  .catch(function () { el.textContent = '(failed to load)'; });
560
+ };
561
+ const groups = document.querySelectorAll('#qa-runs-content details.qa-artifact-secondary');
562
+ groups.forEach(function (d) {
563
+ if (d.__qaToggleWired) return;
564
+ d.__qaToggleWired = true;
565
+ d.addEventListener('toggle', function () {
566
+ const runId = d.getAttribute ? d.getAttribute('data-run-id') : '';
567
+ if (runId) {
568
+ if (d.open) _qaOpenArtifactGroups.add(runId);
569
+ else _qaOpenArtifactGroups.delete(runId);
570
+ }
571
+ if (!d.open) return;
572
+ d.querySelectorAll('.qa-artifact-log-preview[data-qa-log-url]').forEach(fill);
573
+ });
574
+ });
575
+ const blocks = document.querySelectorAll('#qa-runs-content .qa-artifact-log-preview[data-qa-log-url]');
576
+ blocks.forEach(function (el) {
577
+ // Collapsed group → defer to the toggle handler wired above.
578
+ const group = el.closest ? el.closest('details.qa-artifact-secondary') : null;
579
+ if (group && !group.open) return;
580
+ fill(el);
479
581
  });
480
582
  }
481
583
 
584
+ // W-msb9kgs402813a97-d — a session card's run chip scrolls the matching Recent
585
+ // Runs row into view instead of navigating. The default fragment jump is
586
+ // suppressed by the chip's onclick because the URL hash is owned by the
587
+ // modal-stack router (state.js#getModalStackFromUrl); the href stays as the
588
+ // no-JS fallback and as the accessible link target.
589
+ function qaScrollToRun(runId) {
590
+ if (!runId) return;
591
+ const sel = (window.CSS && CSS.escape) ? CSS.escape(runId) : runId.replace(/"/g, '\\"');
592
+ const row = document.querySelector('.qa-run-row[data-run-id="' + sel + '"]');
593
+ if (!row) return;
594
+ try { row.scrollIntoView({ behavior: 'smooth', block: 'center' }); }
595
+ catch { row.scrollIntoView(); }
596
+ // Brief highlight so the eye lands on the right row after the scroll.
597
+ row.classList.add('qa-run-row--flash');
598
+ setTimeout(function () { row.classList.remove('qa-run-row--flash'); }, 1600);
599
+ }
600
+
482
601
  function qaOpenRunAgent(workItemId, agentId) {
483
602
  // Prefer the agent live-detail panel if available; fall back to the
484
603
  // work-item detail; fall back to a hard navigation as a last resort.
@@ -670,7 +789,7 @@ async function loadQaSessions() {
670
789
  } else {
671
790
  let html = '';
672
791
  for (const s of active) html += _qaRenderSessionCard(s);
673
- // eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: session id, state, target sub-fields, mode, runner, error/summary/failureClass)
792
+ // eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: session id, state, target sub-fields, mode, runner, error/summary/failureClass, linked run id + run status)
674
793
  const frag = document.createRange().createContextualFragment(html);
675
794
  activeRoot.replaceChildren(frag);
676
795
  }
@@ -684,7 +803,7 @@ async function loadQaSessions() {
684
803
  } else {
685
804
  let html = '';
686
805
  for (const s of terminal) html += _qaRenderSessionCard(s);
687
- // eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: session id, state, target sub-fields, mode, runner, error/summary/failureClass)
806
+ // eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: session id, state, target sub-fields, mode, runner, error/summary/failureClass, linked run id + run status + artifact file names via _qaRenderArtifactPreviews)
688
807
  const frag = document.createRange().createContextualFragment(html);
689
808
  recentRoot.replaceChildren(frag);
690
809
  }
@@ -728,6 +847,15 @@ function _qaRenderSessionCard(s) {
728
847
  const summary = s && s.summary || '';
729
848
  const createdAt = s && s.createdAt ? String(s.createdAt).slice(0, 19).replace('T', ' ') : '';
730
849
  const isTerminal = QA_SESSION_TERMINAL_STATES.has(state);
850
+ // W-msb9kgs402813a97-d — `linkedRun` is the bounded read-side projection the
851
+ // sessions endpoint attaches (engine/qa/sessions.js#listSessions with
852
+ // includeLinkedRun): { id, status, artifacts:[{type,path,label}] }, primary
853
+ // (image/video) artifacts only. It is absent when the run was deleted or the
854
+ // runs store was unreadable, so every read below is defensive.
855
+ const qaRunId = (s && s.qaRunId) || '';
856
+ const linkedRun = (s && s.linkedRun) || null;
857
+ const linkedRunStatus = (linkedRun && linkedRun.status) || '';
858
+ const linkedArtifacts = (linkedRun && Array.isArray(linkedRun.artifacts)) ? linkedRun.artifacts : [];
731
859
 
732
860
  const phases = _qaRenderPhaseChips(state);
733
861
 
@@ -752,6 +880,20 @@ function _qaRenderSessionCard(s) {
752
880
  meta += '<span class="qa-session-meta-item">target: <code>' + escHtml(targetSummary) + '</code></span>';
753
881
  meta += '<span class="qa-session-meta-item">mode: <code>' + escHtml(mode) + '</code></span>';
754
882
  meta += '<span class="qa-session-meta-item">runner: <code>' + escHtml(runner) + '</code></span>';
883
+ // W-msb9kgs402813a97-d — the linked qa-run. Without it a session that DID
884
+ // capture screenshots reads as if it produced nothing, because the run (and
885
+ // its artifacts) only ever appeared in the separate Recent Runs list. The
886
+ // anchor is the run row's id (see _qaRenderRunRow); the click is intercepted
887
+ // so the URL hash stays owned by the modal-stack router in state.js.
888
+ if (qaRunId) {
889
+ meta += '<span class="qa-session-meta-item">run: ' +
890
+ '<a class="qa-session-run-link" href="#qa-run-' + escHtml(qaRunId) + '"' +
891
+ ' data-run-id="' + escHtml(qaRunId) + '"' +
892
+ ' onclick="event.preventDefault();qaScrollToRun(this.dataset.runId);">' +
893
+ '<code>' + escHtml(qaRunId) + '</code></a>' +
894
+ (linkedRunStatus ? ' <span class="qa-session-run-status">' + escHtml(linkedRunStatus) + '</span>' : '') +
895
+ '</span>';
896
+ }
755
897
  if (createdAt) meta += '<span class="qa-session-meta-item qa-session-meta-ts">' + escHtml(createdAt) + '</span>';
756
898
 
757
899
  let body = '';
@@ -775,6 +917,23 @@ function _qaRenderSessionCard(s) {
775
917
  } else if (summary) {
776
918
  body += '<div class="qa-session-summary">' + escHtml(summary) + '</div>';
777
919
  }
920
+ // W-msb9kgs402813a97-d — evidence for a finished session. Only terminal
921
+ // sessions get this block: a still-executing run has not finished capturing,
922
+ // so "none captured" would be a lie. The screenshots/video render through the
923
+ // SAME helper the Recent Runs strip uses, so artifact URLs keep going through
924
+ // /api/qa/artifacts/<runId>/<path> and no filesystem path is ever exposed.
925
+ // The empty case is stated out loud — silence is what made a terminal session
926
+ // that captured nothing indistinguishable from one that captured plenty.
927
+ if (isTerminal && qaRunId) {
928
+ const previews = (linkedArtifacts.length && typeof _qaRenderArtifactPreviews === 'function')
929
+ ? _qaRenderArtifactPreviews(linkedRun.id || qaRunId, linkedArtifacts, linkedRunStatus)
930
+ : '';
931
+ if (previews) {
932
+ body += '<div class="qa-session-artifacts">' + previews + '</div>';
933
+ } else if (!linkedArtifacts.length) {
934
+ body += '<div class="qa-session-artifacts-empty">No screenshots or video captured for this run.</div>';
935
+ }
936
+ }
778
937
 
779
938
  const stateClass = 'qa-session-state-' + escHtml(state);
780
939
  return (
@@ -138,7 +138,10 @@ const RENDER_VERSIONS = {
138
138
  // Bumped 2→3 for client-ticked relative last-action timestamps.
139
139
  // Bumped 3→4 to show the runtime-reported model when no model is pinned.
140
140
  agents: 4,
141
- prdProgress: 2,
141
+ // Bumped 3→4: `missing` no longer counts as active, so the group summary
142
+ // reports only genuinely in-progress items and surfaces "to do" separately
143
+ // (W-msa2tagf01cse21c).
144
+ prdProgress: 4,
142
145
  prdPrs: 1,
143
146
  inbox: 2,
144
147
  // Bumped 4→5 for the clickable checkout-mode pill + picker (W-mr1b67zi0006b788).
@@ -146,11 +149,23 @@ const RENDER_VERSIONS = {
146
149
  // Bumped 6→7 for the liveValidation.autoDispatch toggle + pill "· auto-validate" suffix (W-mr2q361a00097e5c).
147
150
  projects: 9,
148
151
  notes: 1,
149
- prd: 3,
152
+ // Bumped 3→4: an approved all-missing PRD no longer derives as
153
+ // dispatched/"In Progress" in the header badge (W-msa2tagf01cse21c).
154
+ prd: 4,
150
155
  // Bumped 4→5 for shared author/lifecycle/project/review/build filters.
151
- prs: 5,
156
+ // Bumped 5→6 for draft/ready pill + Merge column removal (W-ms8z6l8z).
157
+ // Bumped 6→7 for the dedicated Project column (W-ms90po5j).
158
+ // Bumped 8→9 for independent Auto Fix and Auto Review row controls.
159
+ // Bumped 9→10: Status cell badges moved into a .pr-status-badges flex
160
+ // container so a wrapped conflict badge no longer overlaps the lifecycle
161
+ // badge (W-msakop2o).
162
+ // Bumped 10→11: Agent column replaced by Author (real PR author identity,
163
+ // separate from the Minions agent) + Author/Minions-agent split in PR detail
164
+ // (W-mscibegy005e8b74).
165
+ prs: 11,
152
166
  archivedPrds: 1,
153
- engine: 4,
167
+ // Bumped 4→5 for the lifecycle-owned worktree count + foreign suffix (W-ms9q3dql00vxd4a8).
168
+ engine: 5,
154
169
  version: 1,
155
170
  adoThrottle: 1,
156
171
  ghThrottle: 1,
@@ -175,7 +190,19 @@ const RENDER_VERSIONS = {
175
190
  // a client reload past both.
176
191
  // Bumped 17→18: WI detail modal shows the effective model (persisted dispatch
177
192
  // evidence, else the current-config projection) — W-ms0ytfdd000d5a26.
178
- workItems: 18,
193
+ // Bumped 18→19 (main via #1092): merge of dependency-conflict needs-attention
194
+ // renderer change with main's row-render changes — both touch the row render.
195
+ // Bumped 19→20 (#1092): Work Items filter bar (project/type/status).
196
+ // Bumped 20→21 (#1100): Work Items table gains the richer filter + sort controls
197
+ // and an Updated column (supersedes the #1092 filter bar); row/table markup
198
+ // changed for byte-identical input — W-ms7kutzv003q17ad.
199
+ // Bumped 21→22: default sort is now lifecycle-grouped (active → pending →
200
+ // failed → terminal, last-updated desc inside each group), so row ORDER
201
+ // changes for byte-identical input — W-ms9bmp8k003if9cf.
202
+ // Bumped 22→23: 'paused' joins the lifecycle order between failed and the
203
+ // terminal tail (it was unranked, so it sorted behind every done row in both
204
+ // directions) — row ORDER changes again for byte-identical input.
205
+ workItems: 23,
179
206
  skills: 1,
180
207
  commands: 1,
181
208
  mcpServers: 1,
@@ -190,6 +217,16 @@ const RENDER_VERSIONS = {
190
217
  pipelines: 2,
191
218
  pinned: 1,
192
219
  kbPayload: 2,
220
+ // The QA runs list renders on its own 5s poll (loadQaRuns → replaceChildren)
221
+ // rather than through the tick-driven _changed() cache, so — like `settings`
222
+ // above — this entry is the section's cache-bust knob rather than a live
223
+ // cache key today. Registered by W-ms9p62od00kt5b58, which changed
224
+ // _qaRenderArtifactPreviews' output for byte-identical input (terminal runs
225
+ // now collapse non-screenshot artifacts behind a <details>).
226
+ // Bumped 1→2 by W-msb9kgs402813a97-d: the session card now renders the linked
227
+ // qa-run chip and (for terminal sessions) its primary artifacts, so the card
228
+ // output changed for byte-identical session input.
229
+ qa: 2,
193
230
  // settings is a modal-driven section, not a tick-driven render — but the
194
231
  // cache-bust entry exists so future tick-driven code (or feature-flag-aware
195
232
  // renderers) can call `_changed('settings', …)` and bump on visual revamps.
@@ -200,7 +237,9 @@ const RENDER_VERSIONS = {
200
237
  // Bumped to 6 for the fleet ACP pool opt-in default and warning copy.
201
238
  // Bumped to 7 on origin/main.
202
239
  // Bumped to 8 for the Auto-fix pane tile-grid revamp (W-mryyzhug000454b7).
203
- settings: 8,
240
+ // Bumped to 11 for the Cloud dispatch control in Agents Runtime & Models
241
+ // (registry-populated provider selector, disabled when none are registered).
242
+ settings: 11,
204
243
  };
205
244
  const _sectionCache = {};
206
245
  const _lastValueByKey = {};
@@ -772,6 +811,14 @@ function _processStatusUpdate(data, opts) {
772
811
  var qs = document.getElementById('engine-quick-stats');
773
812
  if (qs && data.engine) {
774
813
  var wt = data.engine.worktreeCount != null ? data.engine.worktreeCount : '-';
814
+ // W-ms9q3dql00vxd4a8 — `worktreeCount` counts only lifecycle-owned
815
+ // worktrees. Operator-created scratch checkouts arrive separately as
816
+ // `worktreeForeignCount` and are shown as a dimmed suffix so they stay
817
+ // visible without being presented as active Minions worktrees.
818
+ var foreignWt = Number(data.engine.worktreeForeignCount) || 0;
819
+ var foreignChip = foreignWt > 0
820
+ ? ' <span style="opacity:.65" title="Operator-created or unclassifiable directories under the worktree root. Minions never auto-removes these.">+' + foreignWt + ' foreign</span>'
821
+ : '';
775
822
  var pid = data.engine.pid || '-';
776
823
  // W-mpnc4u8c001d9d6c — replace the dead "Tick: -" chip (control.json
777
824
  // never carried a `tick` field) with a live "Next tick in Xs" countdown
@@ -788,7 +835,7 @@ function _processStatusUpdate(data, opts) {
788
835
  // eslint-disable-next-line no-unsanitized/property -- reason: composed from internal engine metrics (pid, lastTickAt/tickInterval, worktreeCount) and a literal id; no user data flows in
789
836
  qs.innerHTML = '<span>PID: <b>' + pid + '</b></span>' +
790
837
  '<span>Next tick in: <b id="engine-next-tick">' + _formatNextTickText() + '</b></span>' +
791
- '<span>Worktrees: <b>' + wt + '</b></span>';
838
+ '<span>Worktrees: <b>' + wt + '</b>' + foreignChip + '</span>';
792
839
  _startNextTickTicker();
793
840
  }
794
841
  });
@@ -1010,7 +1057,7 @@ function _processStatusUpdate(data, opts) {
1010
1057
  });
1011
1058
  });
1012
1059
  // Watches come from GET /api/watches — the canonical SQL-backed read
1013
- // surface (engine/watches-store.js). The retired engine/watches.json mirror
1060
+ // surface (engine/watches/store.js). The retired engine/watches.json mirror
1014
1061
  // is never written, so /state/ has nothing to serve.
1015
1062
  _safeRender('watches', function() {
1016
1063
  // Seq guard — the watches table is mutated every 3 ticks by
@@ -1109,7 +1156,7 @@ function _processStatusUpdate(data, opts) {
1109
1156
  // (dashboard.js _plansCache / invalidatePlansCache) — back-to-back polls
1110
1157
  // hit the cache; external file edits surface within 5s.
1111
1158
  // - /api/knowledge: 30s in-memory TTL + invalidation on POST /api/knowledge
1112
- // and after every kb-sweep (engine/queries.js _kbCache / kb-sweep.js).
1159
+ // and after every kb-sweep (engine/core/queries.js _kbCache / kb-sweep.js).
1113
1160
  // Previously throttled to every 3rd cycle (~12s) — see W-mphfb6ss000a3b9e
1114
1161
  // for the cadence audit + Playwright coverage.
1115
1162
  _safeRender('refreshKnowledgeBase', function() { refreshKnowledgeBase(); });
@@ -1175,6 +1222,18 @@ let _embedPollSuspended = false;
1175
1222
  const d = ev.data;
1176
1223
  if (!d || d.source !== 'minions-slim-embed' || d.type !== 'visibility') return;
1177
1224
  _embedPollSuspended = !d.visible;
1225
+ // Also pause/resume the /engine screen's heavy diagnostics pollers
1226
+ // (memory-panel.js: 10s /api/diagnostics/memory + 60s memory/history)
1227
+ // through the SAME message — a hidden engine embed must not keep those
1228
+ // running forever (W-ms4odb8h009a5d16). Guarded: the panel is only present
1229
+ // on the /engine page, and its hooks are no-ops before mount.
1230
+ try {
1231
+ const mp = window.MinionsMemoryPanel;
1232
+ if (mp) {
1233
+ if (d.visible) { if (typeof mp.resumeMemoryPanel === 'function') mp.resumeMemoryPanel(); }
1234
+ else if (typeof mp.suspendMemoryPanel === 'function') { mp.suspendMemoryPanel(); }
1235
+ }
1236
+ } catch (e) { /* memory panel not on this embedded page — ignore */ }
1178
1237
  if (d.visible) { try { refresh(); } catch (e) { /* refresh not yet defined / transient */ } }
1179
1238
  });
1180
1239
  } catch (e) { /* defensive — never block boot on this bridge */ }
@@ -1198,8 +1257,19 @@ let _embedPollSuspended = false;
1198
1257
  // (~12s) because connection-refused rejects fast rather than waiting out the
1199
1258
  // 15s timeout. The old "> N seconds since last success" OR side was REMOVED:
1200
1259
  // aborts during a healthy cold rebuild pushed elapsed-time past the budget and
1201
- // surfaced the banner while the dashboard was alive-but-busy. Consecutive
1202
- // failures are the only reliable "actually unreachable" signal.
1260
+ // surfaced the banner while the dashboard was alive-but-busy.
1261
+ //
1262
+ // Liveness confirmation (W-ms95wh14): consecutive /api/status failures alone are
1263
+ // still NOT proof the process is gone. During a long-running Command Center turn
1264
+ // the status-cache rebuild is starved on the event loop, so every /api/status
1265
+ // poll aborts even though the dashboard is alive and the CC stream is flowing —
1266
+ // reading that as "unreachable" is the false "Dashboard connection lost" bug.
1267
+ // So on the 3rd consecutive status failure we CONFIRM with a lightweight
1268
+ // /api/health probe (no status-cache rebuild, no per-agent file IO): if health
1269
+ // answers we treat the dashboard as alive-but-slow (reset counter, no banner, no
1270
+ // backoff); only a health-probe failure trips the banner. While the banner is up
1271
+ // an independent /api/health recovery probe (fixed 3s cadence, NOT gated by the
1272
+ // status-poll backoff) clears it the instant the process answers again.
1203
1273
  // On trip we show a sticky red banner with two recovery actions:
1204
1274
  // 1. "Restart Minions" → ccRestartMinions (works when dashboard is
1205
1275
  // alive-but-stale; falls through to copy-to-clipboard when the POST
@@ -1216,6 +1286,48 @@ const _UNREACHABLE_FAIL_THRESHOLD = 3;
1216
1286
  // Once the banner is up we throttle polls (exponential, capped at 30s) to
1217
1287
  // avoid hammering a struggling dashboard / network. Reset on recovery.
1218
1288
  let _nextPollAllowedAt = 0;
1289
+ // Recovery probe — a short, fixed-cadence /api/health poll that runs ONLY while
1290
+ // the unreachable banner is up. It is deliberately independent of the status
1291
+ // backoff above: once the dashboard process answers /api/health again we clear
1292
+ // the banner immediately and resume steady status polling, instead of waiting
1293
+ // out the (up to 30s) exponential status-poll backoff. /api/health does no
1294
+ // status-cache rebuild, so it recovers the instant the process is reachable.
1295
+ let _recoveryProbeTimer = null;
1296
+ const _RECOVERY_PROBE_INTERVAL_MS = 3000;
1297
+ const _LIVENESS_PROBE_TIMEOUT_MS = 4000;
1298
+
1299
+ // Lightweight liveness probe. A /api/status timeout/abort is NOT proof the
1300
+ // dashboard process is down — the status-cache rebuild can be starved on the
1301
+ // event loop by a long-running Command Center turn. /api/health rebuilds no
1302
+ // status cache and does no per-agent file IO, so a fast 200 proves the process
1303
+ // is alive and servicing requests. Resolves true only on an ok response.
1304
+ function _probeDashboardAlive() {
1305
+ return safeFetch('/api/health', { timeout: _LIVENESS_PROBE_TIMEOUT_MS })
1306
+ .then(function(r) { return !!(r && r.ok); })
1307
+ .catch(function() { return false; });
1308
+ }
1309
+
1310
+ function _startRecoveryProbe() {
1311
+ if (_recoveryProbeTimer) return;
1312
+ _recoveryProbeTimer = setInterval(function() {
1313
+ _probeDashboardAlive().then(function(alive) {
1314
+ if (!alive) return;
1315
+ _stopRecoveryProbe();
1316
+ // Process is reachable again. Snap back to steady status polling (no
1317
+ // backoff) and kick an immediate refresh so the frozen UI repopulates
1318
+ // and the badge/banner restore on the next successful poll — don't wait
1319
+ // for the status cache to warm to CLEAR the "unreachable" state.
1320
+ _consecutiveStatusFails = 0;
1321
+ _nextPollAllowedAt = 0;
1322
+ _markDashboardReachable();
1323
+ try { refresh(); } catch (e) { /* transient — next tick retries */ }
1324
+ });
1325
+ }, _RECOVERY_PROBE_INTERVAL_MS);
1326
+ }
1327
+
1328
+ function _stopRecoveryProbe() {
1329
+ if (_recoveryProbeTimer) { clearInterval(_recoveryProbeTimer); _recoveryProbeTimer = null; }
1330
+ }
1219
1331
 
1220
1332
  function _formatAge(ms) {
1221
1333
  if (ms < 1000) return 'just now';
@@ -1282,6 +1394,7 @@ function _markDashboardReachable() {
1282
1394
  _unreachableSince = 0;
1283
1395
  delete window._dashboardUnreachable;
1284
1396
  if (_unreachableAgeTimer) { clearInterval(_unreachableAgeTimer); _unreachableAgeTimer = null; }
1397
+ _stopRecoveryProbe();
1285
1398
  // Hand #engine-alert back to renderEngineAlert. We can't leave our content
1286
1399
  // sitting in there because the next renderEngineAlert call only runs when
1287
1400
  // `_changed('engine', data.engine)` returns true — if engine state hasn't
@@ -1300,6 +1413,7 @@ window._resetDashboardUnreachableForTest = function() {
1300
1413
  _unreachableSince = 0;
1301
1414
  _nextPollAllowedAt = 0;
1302
1415
  if (_unreachableAgeTimer) { clearInterval(_unreachableAgeTimer); _unreachableAgeTimer = null; }
1416
+ _stopRecoveryProbe();
1303
1417
  delete window._dashboardUnreachable;
1304
1418
  };
1305
1419
 
@@ -1474,8 +1588,30 @@ async function refresh(opts) {
1474
1588
  _consecutiveStatusFails++;
1475
1589
  // Sustained failure only — see the detector header for why elapsed time
1476
1590
  // alone must not trip (it flashed the banner on healthy-but-slow rebuilds).
1477
- if (_consecutiveStatusFails >= _UNREACHABLE_FAIL_THRESHOLD) {
1591
+ // But a sustained run of /api/status timeouts is STILL not proof the
1592
+ // process is gone: the ~2 KB status snapshot rebuild can be starved on the
1593
+ // event loop by a long-running Command Center turn, so every poll aborts at
1594
+ // safeFetch's 15s budget while the dashboard is perfectly alive. Before
1595
+ // tripping the banner, confirm true unreachability with a lightweight
1596
+ // /api/health probe that rebuilds no status cache. If health answers, the
1597
+ // dashboard is alive-but-slow — reset to steady polling (no banner, no
1598
+ // backoff) so we recover the instant the cache warms; only a health-probe
1599
+ // failure is treated as "actually unreachable".
1600
+ let _statusFailConfirmedDead = false;
1601
+ if (_consecutiveStatusFails >= _UNREACHABLE_FAIL_THRESHOLD && !_unreachableSince) {
1602
+ const _aliveViaHealth = await _probeDashboardAlive();
1603
+ if (_aliveViaHealth) {
1604
+ _consecutiveStatusFails = 0;
1605
+ _nextPollAllowedAt = 0;
1606
+ } else {
1607
+ _statusFailConfirmedDead = true;
1608
+ }
1609
+ }
1610
+ if (_consecutiveStatusFails >= _UNREACHABLE_FAIL_THRESHOLD && _statusFailConfirmedDead) {
1478
1611
  _markDashboardUnreachable(e);
1612
+ // Start the independent /api/health recovery probe so we clear the banner
1613
+ // promptly on recovery instead of waiting out the status-poll backoff.
1614
+ _startRecoveryProbe();
1479
1615
  }
1480
1616
  // Backoff: once we've tripped the banner, throttle subsequent polls
1481
1617
  // (4s → 8s → 16s → 30s cap). Reset to 0 in the success path below so
@@ -349,8 +349,8 @@ function renderDispatch(dispatch, opts) {
349
349
  const compStart = _completedPage * COMPLETED_PER_PAGE;
350
350
  const pageCompleted = completed.slice(compStart, compStart + COMPLETED_PER_PAGE);
351
351
 
352
- // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: dispatch id, type, agent, task, result, reason)
353
- completedEl.innerHTML = '<table class="pr-table"><thead><tr><th>ID</th><th>Type</th><th>Agent</th><th>Task</th><th>Result</th><th>Completed</th></tr></thead><tbody>' +
352
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: type, agent, task, result, reason)
353
+ completedEl.innerHTML = '<table class="pr-table"><thead><tr><th>Type</th><th>Agent</th><th>Task</th><th>Result</th><th>Completed</th></tr></thead><tbody>' +
354
354
  pageCompleted.map(d => {
355
355
  const isError = d.result === 'error';
356
356
  const agentId = (d.agent || '').toLowerCase();
@@ -358,10 +358,9 @@ function renderDispatch(dispatch, opts) {
358
358
  ? ' <button class="error-details-btn" data-agent="' + escHtml(agentId) + '" data-reason="' + escHtml(d.reason || 'No reason recorded') + '" data-task="' + escHtml((d.task || '').slice(0, 100)) + '" onclick="showErrorDetails(this.dataset.agent, this.dataset.reason, this.dataset.task)" title="View error details">details</button>'
359
359
  : '';
360
360
  return '<tr>' +
361
- '<td style="font-family:Consolas;font-size:var(--text-sm)" title="' + escHtml(d.id || '') + '">' + escHtml(d.id || '') + '</td>' +
362
361
  '<td><span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span></td>' +
363
362
  '<td>' + escHtml(d.agentName || d.agent || '') + '</td>' +
364
- '<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml((d.task || '').slice(0, 60)) + '</td>' +
363
+ '<td style="width:100%;max-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml((d.task || '').slice(0, 60)) + '</td>' +
365
364
  '<td style="color:' + (d.result === 'success' ? 'var(--green)' : 'var(--red)') + '">' + escHtml(d.result || '') + errorBtn + '</td>' +
366
365
  '<td class="pr-date">' + shortTime(d.completed_at) + '</td>' +
367
366
  '</tr>';
@@ -3,7 +3,7 @@
3
3
  const INBOX_PER_PAGE = 15;
4
4
  let _inboxPage = 0;
5
5
 
6
- // Quick relative-time formatter — mirrors engine/queries.js timeSince so the
6
+ // Quick relative-time formatter — mirrors engine/core/queries.js timeSince so the
7
7
  // inbox-item "age" chip reads the same as it did via /api/status.
8
8
  function _timeSinceMs(mtimeMs) {
9
9
  if (!mtimeMs) return '';
@@ -78,7 +78,7 @@ async function fetchNotesFromDisk() {
78
78
  // Promote a human-readable title to the inbox card heading. Prefers an
79
79
  // explicit `title` field from the API, then a `title:` YAML frontmatter key,
80
80
  // then the first markdown heading, then the first non-empty body line. Mirrors
81
- // the engine's own title derivation (engine/consolidation.js — first `# ` ATX
81
+ // the engine's own title derivation (engine/memory/consolidation.js — first `# ` ATX
82
82
  // heading). Returns '' when nothing readable is extractable so the caller can
83
83
  // fall back to the raw filename.
84
84
  function _extractInboxTitle(item) {