@yemi33/minions 0.1.2448 → 0.1.2450

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 +246 -34
  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 +974 -430
  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} +283 -69
  153. package/engine/{safe-expr.js → core/safe-expr.js} +1 -1
  154. package/engine/{shared.js → core/shared.js} +1637 -175
  155. package/engine/{stdio-timestamps.js → core/stdio-timestamps.js} +1 -1
  156. package/engine/{untrusted-fence.js → core/untrusted-fence.js} +3 -3
  157. package/engine/db/index.js +11 -2
  158. package/engine/db/migrations/002-dispatches.js +3 -3
  159. package/engine/db/migrations/003-work-items.js +1 -1
  160. package/engine/db/migrations/004-pull-requests.js +1 -1
  161. package/engine/db/migrations/006-metrics.js +1 -1
  162. package/engine/db/migrations/007-watches.js +2 -2
  163. package/engine/db/migrations/008-small-state.js +1 -1
  164. package/engine/db/migrations/009-qa.js +1 -1
  165. package/engine/db/migrations/010-pr-links.js +1 -1
  166. package/engine/db/migrations/011-remaining-state.js +1 -1
  167. package/engine/db/migrations/012-steering-deliveries.js +2 -2
  168. package/engine/db/migrations/013-backfill-broken-note-links.js +1 -1
  169. package/engine/db/migrations/014-pr-fix-target-prefs.js +2 -2
  170. package/engine/db/migrations/015-plans-prds.js +0 -0
  171. package/engine/db/migrations/018-sql-only-cutover.js +2 -2
  172. package/engine/db/migrations/021-archived-work-items.js +1 -1
  173. package/engine/db/migrations/022-global-cc-session.js +1 -1
  174. package/engine/db/migrations/023-engine-state.js +1 -1
  175. package/engine/db/migrations/025-malformed-work-item-phantoms.js +1 -1
  176. package/engine/db/migrations/027-review-learning-lifecycle.js +1 -1
  177. package/engine/db/migrations/029-repair-reused-versions.js +20 -0
  178. package/engine/db/migrations/031-pr-author-identity.js +137 -0
  179. package/engine/{consolidation.js → memory/consolidation.js} +6 -6
  180. package/engine/{kb-sweep-runner.js → memory/kb-sweep-runner.js} +2 -2
  181. package/engine/{kb-sweep.js → memory/kb-sweep.js} +9 -7
  182. package/engine/{memory-retrieval.js → memory/memory-retrieval.js} +46 -4
  183. package/engine/{memory-store.js → memory/memory-store.js} +3 -3
  184. package/engine/{promotion.js → memory/promotion.js} +3 -3
  185. package/engine/{review-learning-backfill.js → memory/review-learning-backfill.js} +6 -6
  186. package/engine/{review-learning.js → memory/review-learning.js} +10 -5
  187. package/engine/{diagnostics-memory.js → observability/diagnostics-memory.js} +1 -1
  188. package/engine/{logs-store.js → observability/logs-store.js} +5 -5
  189. package/engine/{metrics-store.js → observability/metrics-store.js} +4 -4
  190. package/engine/{check-status.js → operations/check-status.js} +3 -3
  191. package/engine/{cli.js → operations/cli.js} +271 -113
  192. package/engine/{distribution.js → operations/distribution.js} +5 -6
  193. package/engine/{cleanup.js → orchestration/cleanup.js} +72 -45
  194. package/engine/{cooldown.js → orchestration/cooldown.js} +5 -5
  195. package/engine/{dispatch-events.js → orchestration/dispatch-events.js} +2 -2
  196. package/engine/{dispatch.js → orchestration/dispatch.js} +129 -36
  197. package/engine/orchestration/failed-scheduled-cleanup.js +274 -0
  198. package/engine/{lifecycle.js → orchestration/lifecycle.js} +198 -90
  199. package/engine/{meeting.js → orchestration/meeting.js} +6 -16
  200. package/engine/{pipeline.js → orchestration/pipeline.js} +12 -12
  201. package/engine/{pre-dispatch-eval.js → orchestration/pre-dispatch-eval.js} +10 -9
  202. package/engine/{routing.js → orchestration/routing.js} +3 -3
  203. package/engine/{schedule-bootstrap.js → orchestration/schedule-bootstrap.js} +4 -4
  204. package/engine/{scheduler.js → orchestration/scheduler.js} +38 -8
  205. package/engine/{timeout.js → orchestration/timeout.js} +158 -109
  206. package/engine/{db-events.js → persistence/db-events.js} +2 -2
  207. package/engine/{dispatch-store.js → persistence/dispatch-store.js} +7 -7
  208. package/engine/{inbox-store.js → persistence/inbox-store.js} +2 -2
  209. package/engine/{note-link-backfill.js → persistence/note-link-backfill.js} +4 -4
  210. package/engine/{pr-fix-target-store.js → persistence/pr-fix-target-store.js} +8 -8
  211. package/engine/{pull-requests-store.js → persistence/pull-requests-store.js} +21 -7
  212. package/engine/{small-state-store.js → persistence/small-state-store.js} +31 -31
  213. package/engine/persistence/state-operations.js +350 -0
  214. package/engine/{steering-store.js → persistence/steering-store.js} +6 -6
  215. package/engine/{issues.js → planning/issues.js} +2 -2
  216. package/engine/{plan-prd-validation.js → planning/plan-prd-validation.js} +8 -2
  217. package/engine/planning/prd-result-sidecar.js +190 -0
  218. package/engine/{prd-store.js → planning/prd-store.js} +17 -17
  219. package/engine/{project-discovery.js → planning/project-discovery.js} +5 -5
  220. package/engine/{projects.js → planning/projects.js} +10 -10
  221. package/engine/{resolve-area.js → planning/resolve-area.js} +1 -1
  222. package/engine/{work-item-validation.js → planning/work-item-validation.js} +39 -3
  223. package/engine/{work-items-store.js → planning/work-items-store.js} +29 -21
  224. package/engine/{keep-process-sweep.js → processes/keep-process-sweep.js} +57 -17
  225. package/engine/{managed-spawn-launcher.js → processes/managed-spawn-launcher.js} +3 -3
  226. package/engine/{managed-spawn.js → processes/managed-spawn.js} +97 -46
  227. package/engine/{process-utils.js → processes/process-utils.js} +599 -55
  228. package/engine/{abandoned-pr-reconciliation.js → providers/abandoned-pr-reconciliation.js} +17 -7
  229. package/engine/{comment-classifier.js → providers/comment-classifier.js} +85 -17
  230. package/engine/{comment-format.js → providers/comment-format.js} +5 -5
  231. package/engine/{gh-comment.js → providers/gh-comment.js} +15 -15
  232. package/engine/{gh-token.js → providers/gh-token.js} +4 -4
  233. package/engine/{github.js → providers/github.js} +131 -54
  234. package/engine/{pr-action.js → providers/pr-action.js} +13 -12
  235. package/engine/{pr-clone-keep.js → providers/pr-clone-keep.js} +7 -7
  236. package/engine/{pr-devbox.js → providers/pr-devbox.js} +6 -6
  237. package/engine/{pr-fix-target.js → providers/pr-fix-target.js} +13 -13
  238. package/engine/{pr-remote-patch.js → providers/pr-remote-patch.js} +4 -4
  239. package/engine/{pr-resolve.js → providers/pr-resolve.js} +7 -7
  240. package/engine/{pr-temp-clone.js → providers/pr-temp-clone.js} +5 -5
  241. package/engine/{pr-track.js → providers/pr-track.js} +11 -13
  242. package/engine/{shared-branch-pr-reconcile.js → providers/shared-branch-pr-reconcile.js} +4 -4
  243. package/engine/qa/auto-prd-qa.js +313 -0
  244. package/engine/{qa-from-prd.js → qa/from-prd.js} +42 -12
  245. package/engine/qa/prd-session.js +240 -0
  246. package/engine/{qa-process-validation.js → qa/process-validation.js} +14 -9
  247. package/engine/{qa-runbooks.js → qa/runbooks.js} +1 -1
  248. package/engine/{qa-runs.js → qa/runs.js} +286 -15
  249. package/engine/{qa-sessions.js → qa/sessions.js} +595 -49
  250. package/engine/qa/visual-journey.js +654 -0
  251. package/engine/{qa-runners.js → qa-runners/index.js} +7 -7
  252. package/engine/qa-runners/maestro.js +3 -3
  253. package/engine/qa-runners/playwright.js +2 -2
  254. package/engine/{restart-health.js → recovery/restart-health.js} +48 -4
  255. package/engine/recovery/stop-stack.js +607 -0
  256. package/engine/{supervisor.js → recovery/supervisor.js} +105 -175
  257. package/engine/{watchdog.js → recovery/watchdog.js} +136 -13
  258. package/engine/runtimes/claude.js +14 -12
  259. package/engine/runtimes/codex.js +8 -6
  260. package/engine/runtimes/copilot.js +17 -16
  261. package/engine/{watch-actions.js → watches/actions.js} +13 -13
  262. package/engine/{watches.js → watches/index.js} +43 -32
  263. package/engine/{watches-store.js → watches/store.js} +4 -4
  264. package/engine/{create-pr-worktree.js → worktrees/create-pr.js} +1 -1
  265. package/engine/{worktree-gc.js → worktrees/gc.js} +70 -22
  266. package/engine/worktrees/inventory.js +671 -0
  267. package/engine/{live-checkout.js → worktrees/live-checkout.js} +4 -4
  268. package/engine/{worktree-pool.js → worktrees/pool.js} +2 -2
  269. package/engine/{worktree-preflight.js → worktrees/preflight.js} +1 -0
  270. package/engine/worktrees/quarantine-refs.js +173 -0
  271. package/engine.js +1137 -208
  272. package/minions.js +147 -77
  273. package/package.json +10 -6
  274. package/playbooks/_pr-description-audit.md +110 -78
  275. package/playbooks/build-fix-complex.md +2 -0
  276. package/playbooks/fix.md +16 -12
  277. package/playbooks/implement-shared.md +2 -0
  278. package/playbooks/implement.md +19 -20
  279. package/playbooks/plan-to-prd.md +18 -3
  280. package/playbooks/qa-session-draft.md +136 -1
  281. package/playbooks/qa-session-execute.md +80 -2
  282. package/playbooks/qa-session-setup.md +17 -1
  283. package/playbooks/qa-validate.md +1 -1
  284. package/playbooks/setup.md +2 -0
  285. package/playbooks/shared-rules.md +25 -32
  286. package/playbooks/templates/followup-dispatch.md +4 -3
  287. package/playbooks/verify.md +1 -1
  288. package/prompts/cc-system.md +19 -27
  289. package/watch-plugins/README.md +92 -0
  290. package/watch-plugins/ado-author-prs.js +336 -0
  291. package/watch-plugins/gh-author-prs.js +375 -0
  292. package/watch-plugins/http.js +474 -0
  293. package/watch-plugins/teams-channel.js +869 -0
  294. package/docs/dev-composite-workflow.md +0 -101
  295. package/docs/pr-screenshots/pr-886/after-single-header.png +0 -0
  296. package/docs/pr-screenshots/pr-886/before-duplicate-header.png +0 -0
  297. package/docs/pr-screenshots/pr-895/01-cancellation-reason-detail.png +0 -0
  298. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
  299. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
  300. package/docs/pr-screenshots/pr-901/projects-tab-default.png +0 -0
  301. package/docs/pr-screenshots/pr-901/projects-tab-fmf-selected.png +0 -0
  302. package/docs/pr-screenshots/pr-916/model-picker-AFTER-crop.png +0 -0
  303. package/docs/pr-screenshots/pr-916/model-picker-AFTER.png +0 -0
  304. package/docs/pr-screenshots/pr-916/model-picker-BEFORE-crop.png +0 -0
  305. package/docs/pr-screenshots/pr-916/model-picker-BEFORE.png +0 -0
  306. package/docs/pr-screenshots/pr-916/model-picker-dropdown-AFTER.png +0 -0
  307. package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
  308. package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
  309. package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
  310. package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
  311. package/docs/visual-evidence-ci.md +0 -103
  312. package/engine/bridge.js +0 -379
  313. package/engine/quarantine-refs.js +0 -103
  314. package/engine/state-operations.js +0 -178
  315. /package/engine/{steering-constraints.js → agents/steering-constraints.js} +0 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * engine/queries.js — Shared read-only state queries for Minions engine + dashboard.
2
+ * engine/core/queries.js — Shared read-only state queries for Minions engine + dashboard.
3
3
  * Single source of truth for all data reading/aggregation.
4
4
  * Both engine.js and dashboard.js require() this module.
5
5
  */
@@ -9,7 +9,7 @@ const fsp = require('fs').promises;
9
9
  const path = require('path');
10
10
  const os = require('os');
11
11
  const shared = require('./shared');
12
- const steering = require('./steering');
12
+ const steering = require('../agents/steering');
13
13
  const { normalizeExecutionModel, resolveWorkItemModel } = require('./execution-model');
14
14
 
15
15
  const { safeRead, safeReadDir, safeJson, safeJsonObj, safeWrite, getProjects, mutateJsonFileLocked,
@@ -32,7 +32,7 @@ const _prEnrichmentInFlight = new Set();
32
32
  let _lifecycleModule = null;
33
33
  function _lifecycle() {
34
34
  if (!_lifecycleModule) {
35
- try { _lifecycleModule = require('./lifecycle'); }
35
+ try { _lifecycleModule = require('../orchestration/lifecycle'); }
36
36
  catch { _lifecycleModule = null; }
37
37
  }
38
38
  return _lifecycleModule;
@@ -248,7 +248,7 @@ function getDispatch() {
248
248
  const now = Date.now();
249
249
  if (_dispatchCache && (now - _dispatchCacheAt) < 2000) return _dispatchCache;
250
250
  // SQL is the canonical (and only) dispatch store after Phase 9.
251
- const store = require('./dispatch-store');
251
+ const store = require('../persistence/dispatch-store');
252
252
  _dispatchCache = store.readDispatchSectioned();
253
253
  _dispatchCacheAt = now;
254
254
  return _dispatchCache;
@@ -388,7 +388,7 @@ function getDispatchQueue() {
388
388
  const d = getDispatch();
389
389
  const allCompleted = d.completed || [];
390
390
  // Lifetime total from metrics (dispatch.completed is capped at 100)
391
- const metrics = require('./metrics-store').readMetrics() || {};
391
+ const metrics = require('../observability/metrics-store').readMetrics() || {};
392
392
  // Periodically prune cache entries for dispatches that have rotated out of the queue.
393
393
  if (_completionReportCache.size >= 200) {
394
394
  const activeIds = new Set();
@@ -420,18 +420,18 @@ function getNotesWithMeta() {
420
420
 
421
421
  function getEngineLog() {
422
422
  // SQL is the canonical (and only) log store after Phase 9.
423
- const store = require('./logs-store');
423
+ const store = require('../observability/logs-store');
424
424
  return store.readRecentLogsChronological(50) || [];
425
425
  }
426
426
 
427
427
  function getMetrics() {
428
428
  // SQL is the canonical (and only) metrics store after Phase 9.
429
- const store = require('./metrics-store');
429
+ const store = require('../observability/metrics-store');
430
430
  const metrics = store.readMetrics() || {};
431
431
  const config = getConfig();
432
432
  const engineConfig = config.engine || {};
433
433
  const configuredAgents = config.agents || {};
434
- const { resolveRuntime } = require('./runtimes');
434
+ const { resolveRuntime } = require('../runtimes');
435
435
 
436
436
  for (const [agentId, m] of Object.entries(metrics)) {
437
437
  if (agentId.startsWith('_')) continue;
@@ -448,7 +448,7 @@ function getMetrics() {
448
448
  const prRejectedByAgent = {};
449
449
  for (const pr of allPrs) {
450
450
  const agent = (pr.agent || '').toLowerCase();
451
- if (!agent || agent.startsWith('temp-')) continue;
451
+ if (!agent || agent === shared.PR_AGENT_HUMAN || agent.startsWith('temp-')) continue;
452
452
  prCountByAgent[agent] = (prCountByAgent[agent] || 0) + 1;
453
453
  if (pr.reviewStatus === shared.REVIEW_STATUS.APPROVED || pr.status === shared.PR_STATUS.MERGED) prApprovedByAgent[agent] = (prApprovedByAgent[agent] || 0) + 1;
454
454
  // BUG-H13 / P-h13-rejected: this used to compare to the literal 'rejected',
@@ -519,7 +519,7 @@ function getInboxFiles() {
519
519
  // P-34fa5d79 — Note ↔ Work Item ↔ KB Entry linkage.
520
520
  //
521
521
  // Parse the YAML frontmatter from a note body and return the parsed fields.
522
- // Same minimal contract as engine/kb-sweep.js#_parseFrontmatter — a single
522
+ // Same minimal contract as engine/memory/kb-sweep.js#_parseFrontmatter — a single
523
523
  // shared regex would have been nicer but importing kb-sweep from queries
524
524
  // would form a circular dep with lifecycle (queries → kb-sweep → … →
525
525
  // queries), so we keep a local copy of the four-line parser.
@@ -737,7 +737,7 @@ function resolveRuntimeModelDisplay({ runtime, configuredModel, agentId, dispatc
737
737
  * model). An active dispatch is the live run and always beats a completed one;
738
738
  * among entries of the same kind the most recently started wins.
739
739
  *
740
- * Note `dispatch.completed` is capped (engine/dispatch.js), so an old work item
740
+ * Note `dispatch.completed` is capped (engine/orchestration/dispatch.js), so an old work item
741
741
  * can legitimately have no entry here — callers must handle that.
742
742
  */
743
743
  function buildWorkItemDispatchIndex(dispatch) {
@@ -773,7 +773,7 @@ function resolveConfiguredAgentModel(config, agentId, cache) {
773
773
  const configured = shared.resolveAgentModel(agentConfig, engine) || null;
774
774
  let model = configured;
775
775
  try {
776
- const runtime = require('./runtimes').resolveRuntime(runtimeName);
776
+ const runtime = require('../runtimes').resolveRuntime(runtimeName);
777
777
  if (typeof runtime?.resolveModel === 'function') model = runtime.resolveModel(configured) || configured;
778
778
  } catch { /* registry unavailable — fall back to the raw configured value */ }
779
779
  const resolved = { runtimeName, model };
@@ -1017,7 +1017,7 @@ function getAgentDetail(id) {
1017
1017
 
1018
1018
  function getPrs(project) {
1019
1019
  if (project) {
1020
- const store = require('./pull-requests-store');
1020
+ const store = require('../persistence/pull-requests-store');
1021
1021
  const prs = store.readPullRequestsForScope(project.name) || [];
1022
1022
  shared.normalizePrRecords(prs, project);
1023
1023
  return prs;
@@ -1137,7 +1137,7 @@ function _pickPrDedupeWinner(group, projects) {
1137
1137
  // W-mr3mvgfe — terminal PR statuses (done being polled/reviewed) get pushed
1138
1138
  // to the bottom of the PR tab's default sort. Includes `CLOSED` (a PR closed
1139
1139
  // without merging, distinct from `ABANDONED` but equally inactive — see
1140
- // engine/github.js `newStatus = PR_STATUS.CLOSED` on `prData.state === 'closed'`
1140
+ // engine/providers/github.js `newStatus = PR_STATUS.CLOSED` on `prData.state === 'closed'`
1141
1141
  // without `merged`) alongside `MERGED`/`ABANDONED`.
1142
1142
  function _isTerminalPrStatus(status) {
1143
1143
  return status === PR_STATUS.MERGED || status === PR_STATUS.ABANDONED || status === PR_STATUS.CLOSED;
@@ -1152,7 +1152,7 @@ function getPullRequests(config) {
1152
1152
  const allPrs = [];
1153
1153
 
1154
1154
  // SQL is the canonical (and only) PR store after Phase 9.
1155
- const store = require('./pull-requests-store');
1155
+ const store = require('../persistence/pull-requests-store');
1156
1156
  const sqlPrs = store.readAllPullRequests() || [];
1157
1157
 
1158
1158
  // W-mqba5ulq000nd255 — Read-side de-dupe across project scopes. When the
@@ -1361,6 +1361,92 @@ let _getSkillsResultCacheTs = 0;
1361
1361
  let _getSkillsResultCacheKey = null;
1362
1362
  const SKILLS_CACHE_TTL = 30000; // 30s — skill files change rarely (agent extraction, manual authoring)
1363
1363
 
1364
+ // ── Nested project-embedded skill discovery (W-ms3a68j2) ────────────────────
1365
+ // A configured project can be a massive monorepo (e.g. `src` = C:/office/src),
1366
+ // and project-embedded skills can live in deeply nested `.claude/skills` /
1367
+ // `.github/skills` / `.agents/skills` dirs far below the repo root (observed:
1368
+ // `loop/app/android/android/.claude/skills/*`). The repo-root-only project skill
1369
+ // roots exposed by the runtime adapters miss these. `collectSkillFiles` does a
1370
+ // single bounded walk per project to find every nested skill root. The defensive
1371
+ // caps mirror engine/agents/harness-context.js's "Defensive caps" pattern so the walk
1372
+ // can never balloon or wander into node_modules/.git/build output.
1373
+ const SKILL_WALK_MAX_DEPTH = 12; // directory levels below the project root before we stop descending
1374
+ const SKILL_WALK_MAX_ENTRIES = 40000; // directory dirents inspected across one project walk (files are free)
1375
+
1376
+ // Test seam: allow lowering the directory-dirent budget so the entry cap can be
1377
+ // exercised without materializing 40k directories. Falls back to the real cap.
1378
+ function _skillWalkMaxEntries() {
1379
+ const override = Number(process.env.MINIONS_SKILL_WALK_MAX_ENTRIES);
1380
+ return Number.isFinite(override) && override > 0 ? override : SKILL_WALK_MAX_ENTRIES;
1381
+ }
1382
+ const SKILL_WALK_PRUNE_DIRS = new Set([
1383
+ '.git', 'node_modules', 'bin', 'obj', 'build', 'dist', 'out', 'output',
1384
+ 'target', '.gradle', '.venv', 'venv', 'vendor', '.next', '.nuxt', 'coverage',
1385
+ '.idea', '.vs', '.cache', '__pycache__', '.terraform', 'Pods', 'DerivedData',
1386
+ ]);
1387
+
1388
+ // Sort skill dirs shallowest-first so repo-root skills keep priority over deeper
1389
+ // ones during the per-project name dedup.
1390
+ function _sortSkillDirsShallowFirst(dirs) {
1391
+ return dirs.sort((a, b) => {
1392
+ const da = a.split(/[\\/]/).length;
1393
+ const db = b.split(/[\\/]/).length;
1394
+ return da - db || a.localeCompare(b);
1395
+ });
1396
+ }
1397
+
1398
+ // Bounded recursive walk of a project checkout that returns every nested
1399
+ // skill-root directory (absolute path) of the form `**/<marker>/skills`, where
1400
+ // `<marker>` is one of `markerNames` (e.g. `.claude`, `.github`, `.agents`).
1401
+ // Marker names are derived from the runtime adapters' own project skill roots so
1402
+ // discovery stays runtime-agnostic (no hard-coded runtime name checks). The walk
1403
+ // is bounded by depth, total dirents, and a heavy-dir prune set. Marker dirs are
1404
+ // never descended into, so we never surface skills nested inside build output.
1405
+ function _findNestedProjectSkillDirs(projectRoot, markerNames) {
1406
+ const found = [];
1407
+ if (!projectRoot || !markerNames || markerNames.size === 0) return found;
1408
+ const seenDirs = new Set();
1409
+ const maxEntries = _skillWalkMaxEntries();
1410
+ let dirsScanned = 0;
1411
+ // Breadth-first drain (FIFO via an index cursor) so the directory budget is
1412
+ // spent shallowest-first — matching _sortSkillDirsShallowFirst's declared
1413
+ // priority. A depth-first walk would let a single fat subtree exhaust the
1414
+ // budget before sibling subtrees are ever visited, so nested discovery would
1415
+ // silently no-op depending on directory-name ordering (e.g. a monorepo whose
1416
+ // skills sort after a 40k-file tree). Only *directory* dirents count toward
1417
+ // SKILL_WALK_MAX_ENTRIES — a file can never be a skill root, so files are free
1418
+ // and don't starve the walk.
1419
+ const queue = [{ dir: projectRoot, depth: 0 }];
1420
+ for (let head = 0; head < queue.length; head++) {
1421
+ const { dir, depth } = queue[head];
1422
+ let entries;
1423
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
1424
+ for (const dirent of entries) {
1425
+ let isDir = false;
1426
+ try { isDir = dirent.isDirectory(); } catch { isDir = false; }
1427
+ if (!isDir) continue;
1428
+ if (dirsScanned >= maxEntries) return _sortSkillDirsShallowFirst(found);
1429
+ dirsScanned++;
1430
+ const name = dirent.name;
1431
+ const childPath = path.join(dir, name);
1432
+ if (markerNames.has(name)) {
1433
+ // `<marker>/skills` — record if present; never descend into the marker dir.
1434
+ const skillsDir = path.join(childPath, 'skills');
1435
+ let st = null;
1436
+ try { st = fs.statSync(skillsDir); } catch { st = null; }
1437
+ if (st && st.isDirectory()) {
1438
+ const abs = path.resolve(skillsDir);
1439
+ if (!seenDirs.has(abs)) { seenDirs.add(abs); found.push(abs); }
1440
+ }
1441
+ continue;
1442
+ }
1443
+ if (SKILL_WALK_PRUNE_DIRS.has(name)) continue;
1444
+ if (depth + 1 <= SKILL_WALK_MAX_DEPTH) queue.push({ dir: childPath, depth: depth + 1 });
1445
+ }
1446
+ }
1447
+ return _sortSkillDirsShallowFirst(found);
1448
+ }
1449
+
1364
1450
  function invalidateSkillsCache() {
1365
1451
  _skillsCache = null;
1366
1452
  _skillsCacheTs = 0;
@@ -1422,7 +1508,7 @@ function _collectHarnessesScope(scopeFilter, runtimeOpts) {
1422
1508
  const mcpsByPath = new Map();
1423
1509
  let runtimeNames = [];
1424
1510
  try {
1425
- const { listRuntimes, resolveRuntime } = require('./runtimes');
1511
+ const { listRuntimes, resolveRuntime } = require('../runtimes');
1426
1512
  runtimeNames = listRuntimes();
1427
1513
  for (const runtimeName of runtimeNames) {
1428
1514
  let runtime;
@@ -1511,7 +1597,7 @@ function collectSkillFiles(config) {
1511
1597
  return seenByScope.get(key);
1512
1598
  }
1513
1599
  try {
1514
- const { listRuntimes, resolveRuntime } = require('./runtimes');
1600
+ const { listRuntimes, resolveRuntime } = require('../runtimes');
1515
1601
  for (const runtimeName of listRuntimes()) {
1516
1602
  const runtime = resolveRuntime(runtimeName);
1517
1603
  if (typeof runtime.getSkillRoots !== 'function') continue;
@@ -1525,15 +1611,42 @@ function collectSkillFiles(config) {
1525
1611
 
1526
1612
  // Project-scope skill roots: aggregate across runtimes via the shared
1527
1613
  // getProjectHarnesses helper so dedup-by-absolute-path happens once
1528
- // (e.g. `<repo>/.agents/skills` is exposed by both Codex and Copilot).
1614
+ // (e.g. `<repo>/.agents/skills` is exposed by both Codex and Copilot). In
1615
+ // addition to the repo-root roots the adapters expose, walk each checkout for
1616
+ // nested project-embedded skill dirs (`**/.claude/skills`, `**/.github/skills`,
1617
+ // `**/.agents/skills`) so deeply-nested skills (e.g.
1618
+ // `loop/app/android/android/.claude/skills/*` in a monorepo) are discovered too.
1529
1619
  for (const project of projects) {
1530
1620
  if (!project.localPath) continue;
1531
- for (const root of getProjectHarnesses(project).skills) {
1532
- if (root.scope !== 'project') continue;
1533
- _collectNativeSkillsDir(root.dir, root.scope, seenFor(root.scope, root.projectName), skillFiles, {
1621
+ const projectSkillRoots = getProjectHarnesses(project).skills.filter(r => r.scope === 'project');
1622
+ if (projectSkillRoots.length === 0) continue;
1623
+ const projectName = projectSkillRoots[0].projectName;
1624
+ const dedup = seenFor('project', projectName);
1625
+
1626
+ // Marker dir names (`.claude` / `.github` / `.agents`) come from the adapter
1627
+ // roots themselves, keeping nested discovery runtime-agnostic. Track the
1628
+ // repo-root skill dirs so the nested walk doesn't re-enumerate them.
1629
+ const markerNames = new Set();
1630
+ const rootLevelDirs = new Set();
1631
+ for (const root of projectSkillRoots) {
1632
+ rootLevelDirs.add(path.resolve(root.dir));
1633
+ const rel = path.relative(project.localPath, root.dir);
1634
+ const firstSeg = rel.split(/[\\/]/)[0];
1635
+ if (firstSeg && !firstSeg.startsWith('..')) markerNames.add(firstSeg);
1636
+ }
1637
+
1638
+ // 1. Repo-root skill roots exposed by the adapters (`<repo>/.claude/skills`).
1639
+ for (const root of projectSkillRoots) {
1640
+ _collectNativeSkillsDir(root.dir, root.scope, dedup, skillFiles, {
1534
1641
  projectName: root.projectName,
1535
1642
  });
1536
1643
  }
1644
+
1645
+ // 2. Nested project-embedded skill roots anywhere under the checkout.
1646
+ for (const nestedDir of _findNestedProjectSkillDirs(project.localPath, markerNames)) {
1647
+ if (rootLevelDirs.has(path.resolve(nestedDir))) continue; // already enumerated above
1648
+ _collectNativeSkillsDir(nestedDir, 'project', dedup, skillFiles, { projectName });
1649
+ }
1537
1650
  }
1538
1651
 
1539
1652
  // 1b. Installed plugin skills: ~/.claude/plugins/installed_plugins.json
@@ -1717,7 +1830,7 @@ function collectCommandFiles(config) {
1717
1830
  // `~/.claude/commands`; Copilot exposes `~/.copilot/commands` and Codex
1718
1831
  // currently returns nothing — adapters fall in/out as their CLIs evolve).
1719
1832
  try {
1720
- const { listRuntimes, resolveRuntime } = require('./runtimes');
1833
+ const { listRuntimes, resolveRuntime } = require('../runtimes');
1721
1834
  for (const runtimeName of listRuntimes()) {
1722
1835
  const runtime = resolveRuntime(runtimeName);
1723
1836
  if (typeof runtime.getCommandRoots !== 'function') continue;
@@ -1993,7 +2106,83 @@ function invalidateWorkItemsCache() {
1993
2106
  _workItemsLeanCacheAt = 0;
1994
2107
  }
1995
2108
 
1996
- // getWorkItems(config, { enrich })
2109
+ // attachWorkItemPrLinks(items, config) — backfill the _pr / _prUrl fields the
2110
+ // /api/work-items LIST rows render (dashboard/js/render-work-items.js `wiRow`).
2111
+ // This is the ONLY cross-reference the list rows need; the far more expensive
2112
+ // _artifacts/_notes/_model enrichment stays detail-only (GET /api/work-items/<id>).
2113
+ //
2114
+ // Root-cause perf note (W-mscir7fv000u6321): the list endpoint used to run the
2115
+ // FULL enrich over the entire work-item history on every 4 s poll, whose
2116
+ // _artifacts/_notes step is an O(items × files) synchronous KB/inbox/archive/
2117
+ // agent-dir scan that hard-froze the single dashboard event loop (~26-30 s
2118
+ // requests at ~2,281 completed items), starving /api/health and Command Center
2119
+ // SSE. Extracting the cheap PR link-up lets the endpoint paginate FIRST and then
2120
+ // attach links to just the page slice. This helper is deliberately map-based —
2121
+ // O(items + prs), never O(items × prs) — and performs NO filesystem access, so
2122
+ // it is safe to call on the full list or a single page. Mutates and returns
2123
+ // `items` (same semantics the enrich block relied on).
2124
+ function attachWorkItemPrLinks(items, config) {
2125
+ if (!Array.isArray(items) || items.length === 0) return items || [];
2126
+ config = config || getConfig();
2127
+ const projects = getProjects(config);
2128
+ const allPrs = getPullRequests(config);
2129
+ // Prebuilt lookups so each item is O(1) instead of scanning allPrs.
2130
+ const prById = new Map();
2131
+ const prsByDisplayId = new Map();
2132
+ const prByProjectPrdItem = new Map();
2133
+ const prByProjectBranch = new Map();
2134
+ for (const p of allPrs) {
2135
+ if (p.id != null && !prById.has(p.id)) prById.set(p.id, p);
2136
+ const disp = shared.getPrDisplayId(p);
2137
+ if (disp != null) {
2138
+ const bucket = prsByDisplayId.get(disp);
2139
+ if (bucket) bucket.push(p); else prsByDisplayId.set(disp, [p]);
2140
+ }
2141
+ if (p._project) {
2142
+ for (const wid of (p.prdItems || [])) {
2143
+ const key = p._project + '\0' + wid;
2144
+ if (!prByProjectPrdItem.has(key)) prByProjectPrdItem.set(key, p);
2145
+ }
2146
+ if (p.branch) {
2147
+ const bkey = p._project + '\0' + p.branch;
2148
+ if (!prByProjectBranch.has(bkey)) prByProjectBranch.set(bkey, p);
2149
+ }
2150
+ }
2151
+ }
2152
+ for (const item of items) {
2153
+ if (item._pr && !item._prUrl) {
2154
+ const project = shared.resolveProjectSource(item.project || item._source, projects, { allowCentral: false }).project || null;
2155
+ const canonicalPrId = shared.getCanonicalPrId(project, item._pr);
2156
+ const displayPrId = shared.getPrDisplayId(item._pr);
2157
+ const exactPr = prById.get(canonicalPrId);
2158
+ const displayMatches = exactPr ? [] : (prsByDisplayId.get(displayPrId) || []);
2159
+ const pr = exactPr || (displayMatches.length === 1 ? displayMatches[0] : null);
2160
+ if (pr) {
2161
+ item._pr = pr.id;
2162
+ item._prUrl = pr.url;
2163
+ }
2164
+ }
2165
+ if (!item._pr) {
2166
+ // Derive from PR.prdItems (single source of truth), then fall back to
2167
+ // branch identity. The branch fallback heals decomposed-child WIs whose
2168
+ // PR-opening agent only stamped the parent id into prdItems — branch
2169
+ // names (`work/<wi-id>`) are unique per WI within a project, so a
2170
+ // branch hit is as authoritative as a prdItems hit. Scoped to the same
2171
+ // project so cross-repo branch collisions don't false-link.
2172
+ // (W-mpn0b76200044eed)
2173
+ const itemProject = item.project || item._source;
2174
+ const linkedPr = prByProjectPrdItem.get(itemProject + '\0' + item.id)
2175
+ || (item.branch ? prByProjectBranch.get(itemProject + '\0' + item.branch) : null);
2176
+ if (linkedPr) {
2177
+ item._pr = linkedPr.id;
2178
+ item._prUrl = linkedPr.url;
2179
+ }
2180
+ }
2181
+ }
2182
+ return items;
2183
+ }
2184
+
2185
+ // getWorkItems(config, { enrich, onlyId })
1997
2186
  // enrich (default true): also cross-reference PRs and populate the
1998
2187
  // detail-modal _artifacts/_notes fields. The /api/work-items list payload
1999
2188
  // needs these (PR badges + note chips).
@@ -2003,11 +2192,26 @@ function invalidateWorkItemsCache() {
2003
2192
  // never read _artifacts/_notes (getAgentStatus fallback, count-only callers)
2004
2193
  // so the agent roster / CC preamble don't pay the enrichment cost on every
2005
2194
  // cold rebuild.
2195
+ // onlyId (W-mscmqk6b03ha5673, enrich-only) — narrow the per-item detail
2196
+ // enrichment to the row(s) carrying this work-item id. The single-record
2197
+ // GET /api/work-items/<id> handler needs ONE fully-enriched row, but paid
2198
+ // the whole-history enrichment on every detail-modal open (at ~2.3k items
2199
+ // that's the same multi-second synchronous block the polled list endpoint
2200
+ // was fixed for, just triggered by a click). The full list is still
2201
+ // returned and the cheap pre-enrich dispatch cross-reference still runs
2202
+ // over everything, so _pendingReason/_skipReason/_blockedBy resolve
2203
+ // identically and the caller can still resolve the complete id-matched set
2204
+ // for its ambiguity check before reading the enriched row.
2006
2205
  function getWorkItems(config, opts) {
2007
2206
  const enrich = !(opts && opts.enrich === false);
2207
+ const onlyId = enrich && opts && typeof opts.onlyId === 'string' && opts.onlyId !== ''
2208
+ ? opts.onlyId
2209
+ : null;
2008
2210
  const now = Date.now();
2009
2211
  config = config || getConfig();
2010
2212
  if (enrich) {
2213
+ // An onlyId caller can be served the fully-enriched cache verbatim — it is
2214
+ // a strict superset of what the caller asked for.
2011
2215
  if (_workItemsCache && (now - _workItemsCacheAt) < 1000) return _workItemsCache;
2012
2216
  } else if (_workItemsLeanCache && (now - _workItemsLeanCacheAt) < 1000) {
2013
2217
  return _workItemsLeanCache;
@@ -2015,7 +2219,7 @@ function getWorkItems(config, opts) {
2015
2219
  let allItems = [];
2016
2220
 
2017
2221
  // SQL is the canonical (and only) work-items store after Phase 9.
2018
- const store = require('./work-items-store');
2222
+ const store = require('../planning/work-items-store');
2019
2223
  allItems = store.readAllWorkItems() || [];
2020
2224
 
2021
2225
  // Cross-reference with dispatch (fill in agent from active dispatch if missing on work item)
@@ -2063,42 +2267,24 @@ function getWorkItems(config, opts) {
2063
2267
  }
2064
2268
  }
2065
2269
 
2270
+ // Rows that pay the per-item detail enrichment below. Without `onlyId` this
2271
+ // is the whole list, exactly as before.
2272
+ const enrichTargets = onlyId ? allItems.filter(it => it && it.id === onlyId) : allItems;
2273
+ // An `onlyId` lookup that matches nothing (the caller's 404 path) skips the
2274
+ // block wholesale, so a miss pays none of the once-per-call prep either.
2275
+ // Callers without `onlyId` keep today's behaviour exactly, empty store
2276
+ // included.
2277
+ const skipEnrichment = onlyId !== null && enrichTargets.length === 0;
2278
+
2066
2279
  // Detail-modal enrichment (PR cross-ref + _artifacts/_notes). Skipped for
2067
2280
  // enrich:false callers — see the getWorkItems header.
2068
- if (enrich) {
2069
- const projects = getProjects(config);
2070
- // Cross-reference with PRs
2071
- const allPrs = getPullRequests(config);
2072
- for (const item of allItems) {
2073
- if (item._pr && !item._prUrl) {
2074
- const project = shared.resolveProjectSource(item.project || item._source, projects, { allowCentral: false }).project || null;
2075
- const canonicalPrId = shared.getCanonicalPrId(project, item._pr);
2076
- const displayPrId = shared.getPrDisplayId(item._pr);
2077
- const exactPr = allPrs.find(p => p.id === canonicalPrId);
2078
- const displayMatches = exactPr ? [] : allPrs.filter(p => shared.getPrDisplayId(p) === displayPrId);
2079
- const pr = exactPr || (displayMatches.length === 1 ? displayMatches[0] : null);
2080
- if (pr) {
2081
- item._pr = pr.id;
2082
- item._prUrl = pr.url;
2083
- }
2084
- }
2085
- if (!item._pr) {
2086
- // Derive from PR.prdItems (single source of truth), then fall back to
2087
- // branch identity. The branch fallback heals decomposed-child WIs whose
2088
- // PR-opening agent only stamped the parent id into prdItems — branch
2089
- // names (`work/<wi-id>`) are unique per WI within a project, so a
2090
- // branch hit is as authoritative as a prdItems hit. Scoped to the same
2091
- // project so cross-repo branch collisions don't false-link.
2092
- // (W-mpn0b76200044eed)
2093
- const itemProject = item.project || item._source;
2094
- const linkedPr = allPrs.find(p => p._project === itemProject && (p.prdItems || []).includes(item.id))
2095
- || (item.branch && allPrs.find(p => p.branch === item.branch && p._project === itemProject));
2096
- if (linkedPr) {
2097
- item._pr = linkedPr.id;
2098
- item._prUrl = linkedPr.url;
2099
- }
2100
- }
2101
- }
2281
+ if (enrich && !skipEnrichment) {
2282
+ // Cross-reference with PRs (the _pr/_prUrl LIST-row fields). Extracted to a
2283
+ // shared, map-based helper so the /api/work-items list endpoint can attach
2284
+ // the same links to a PAGINATED slice without the expensive _artifacts/_notes
2285
+ // filesystem scan below (W-mscir7fv000u6321). Narrowed to enrichTargets so an
2286
+ // onlyId detail lookup only links its own row (W-mscmqk6b03ha5673).
2287
+ attachWorkItemPrLinks(enrichTargets, config);
2102
2288
 
2103
2289
  // Populate _artifacts for the work item detail modal
2104
2290
  // Build dispatch ID → work item ID lookup from completed dispatches
@@ -2135,11 +2321,14 @@ function getWorkItems(config, opts) {
2135
2321
  // P-34fa5d79 — pre-build the wiId → notes map so each item's _notes lookup
2136
2322
  // is O(1) instead of re-scanning inbox+archive per item.
2137
2323
  const _notesByWi = _buildNotesByWiMap(dispatch);
2324
+ // `_workItemIdCounts` stays a full-list tally even under `onlyId` — the
2325
+ // `hasUniqueId` note-linkage guard below asks whether an id collides
2326
+ // ANYWHERE, so narrowing it would change the enriched row's output.
2138
2327
  const _workItemIdCounts = new Map();
2139
2328
  for (const item of allItems) {
2140
2329
  _workItemIdCounts.set(item.id, (_workItemIdCounts.get(item.id) || 0) + 1);
2141
2330
  }
2142
- for (const item of allItems) {
2331
+ for (const item of enrichTargets) {
2143
2332
  const arts = shared.isPlainObject(item._artifacts) ? { ...item._artifacts } : {};
2144
2333
  const agentId = item.dispatched_to || item.agent;
2145
2334
  // W-ms0ytfdd000d5a26 — the model this item actually ran on (or, for items
@@ -2235,10 +2424,14 @@ function getWorkItems(config, opts) {
2235
2424
  // >= 1 000 ms and miss the cache, defeating the cache entirely.
2236
2425
  // (W-mr00aooc000i1d6b — queries-work-items-cache CI failure on slow runners)
2237
2426
  const cacheSetAt = Date.now();
2238
- if (enrich) {
2427
+ // An `onlyId` result is only PARTIALLY enriched, so it must never be
2428
+ // published as either module cache: as the enriched cache it would serve
2429
+ // unenriched rows to the next list caller, and as the lean cache it would
2430
+ // carry enrichment the lean shape does not promise.
2431
+ if (enrich && !onlyId) {
2239
2432
  _workItemsCache = allItems;
2240
2433
  _workItemsCacheAt = cacheSetAt;
2241
- } else {
2434
+ } else if (!enrich) {
2242
2435
  _workItemsLeanCache = allItems;
2243
2436
  _workItemsLeanCacheAt = cacheSetAt;
2244
2437
  }
@@ -2258,7 +2451,7 @@ let _prdResultInputHash = ''; // SQL event version + authored-plan mtim
2258
2451
  function _getPrdInputHash() {
2259
2452
  const mtimes = [];
2260
2453
  try {
2261
- const row = require('./db').getDb().prepare('SELECT COALESCE(MAX(id), 0) AS version FROM events').get();
2454
+ const row = require('../db').getDb().prepare('SELECT COALESCE(MAX(id), 0) AS version FROM events').get();
2262
2455
  mtimes.push(Number(row?.version) || 0);
2263
2456
  } catch { mtimes.push(0); }
2264
2457
  // W-mqacrzis0003df4a — Source-plan markdown mtimes. Without this, the
@@ -2294,6 +2487,10 @@ function getPrdInfo(config) {
2294
2487
  // filename so render-prd.js#renderE2eSection can merge them with the live
2295
2488
  // tracker and keep rendering a merged aggregate after its live record is swept.
2296
2489
  const verifyPrsByPlan = {};
2490
+ // W-msa0mrus00mh466f — live (non-archived) PRD objects keyed by filename so
2491
+ // the revision-state derivation below can run once, server-side, after the
2492
+ // work-item index is built.
2493
+ const livePlansByFile = {};
2297
2494
  let latestUpdatedAt = 0;
2298
2495
 
2299
2496
  // Phase 10 step 4.3 — FK-based WI↔PRD-item join scaffolding. `prdItemIdByKey`
@@ -2332,6 +2529,7 @@ function getPrdInfo(config) {
2332
2529
  completedAt: plan.completedAt || '',
2333
2530
  _archived: isArch,
2334
2531
  });
2532
+ if (!isArch) livePlansByFile[pf] = plan;
2335
2533
  if (Array.isArray(plan.verifyPrs) && plan.verifyPrs.length > 0) {
2336
2534
  verifyPrsByPlan[pf] = plan.verifyPrs.map(r => ({
2337
2535
  id: r.id, url: r.url || '', title: r.title || '',
@@ -2365,7 +2563,7 @@ function getPrdInfo(config) {
2365
2563
  // feature-id string match.
2366
2564
  if (useFkJoin) {
2367
2565
  try {
2368
- const db = require('./db').getDb();
2566
+ const db = require('../db').getDb();
2369
2567
  const map = new Map();
2370
2568
  for (const r of db.prepare('SELECT p.filename AS filename, p.archived AS archived, pi.feature_id AS feature_id, pi.id AS id FROM prd_items pi JOIN prds p ON p.id = pi.prd_id WHERE pi.feature_id IS NOT NULL').all()) {
2371
2569
  map.set(`${r.archived ? 1 : 0} ${r.filename} ${r.feature_id}`, r.id);
@@ -2375,7 +2573,7 @@ function getPrdInfo(config) {
2375
2573
  }
2376
2574
 
2377
2575
  // SQLite is the unconditional PRD source.
2378
- const prdStore = require('./prd-store');
2576
+ const prdStore = require('../planning/prd-store');
2379
2577
  for (const { filename, archived, plan, updatedAt } of prdStore.listPrdRows()) {
2380
2578
  try {
2381
2579
  latestUpdatedAt = Math.max(latestUpdatedAt, updatedAt);
@@ -2394,7 +2592,7 @@ function getPrdInfo(config) {
2394
2592
  // so they don't masquerade as a 2nd PRD item in the aggregate guard (W-mpem52qn).
2395
2593
  const wiById = {};
2396
2594
  const allWiById = {};
2397
- for (const wi of require('./work-items-store').readAllWorkItems()) {
2595
+ for (const wi of require('../planning/work-items-store').readAllWorkItems()) {
2398
2596
  if (!wi?.id) {
2399
2597
  console.warn(`[queries] Skipping work item without id in ${wi?._source || 'unknown'}:`, JSON.stringify(wi).slice(0, 120));
2400
2598
  continue;
@@ -2403,13 +2601,27 @@ function getPrdInfo(config) {
2403
2601
  if (wi.sourcePlan && !wiById[wi.id]) wiById[wi.id] = wi;
2404
2602
  }
2405
2603
 
2604
+ // W-msa0mrus00mh466f — canonical PRD revision state, derived ONCE here.
2605
+ // `revision-requested` and `planStale` can both be true on the same PRD; the
2606
+ // dashboard must not offer competing recovery paths, so the server decides
2607
+ // which state applies and the renderer just paints it.
2608
+ const planRevisions = {};
2609
+ const allWorkItemList = Object.values(allWiById);
2610
+ for (const [pf, plan] of Object.entries(livePlansByFile)) {
2611
+ try {
2612
+ const revisionWi = shared.findPrdRevisionWorkItem(plan, pf, allWorkItemList);
2613
+ const revision = shared.derivePrdRevisionState(plan, revisionWi);
2614
+ if (revision.state !== shared.PRD_REVISION_STATE.NONE) planRevisions[pf] = revision;
2615
+ } catch { /* a malformed PRD must not break the whole PRD read */ }
2616
+ }
2617
+
2406
2618
  // Index work items by their stable SQL FK so the per-item join can resolve
2407
2619
  // PRD item → WI by prd_items.id rather than the feature-id string.
2408
2620
  // Best-effort: null map → the join falls back to the legacy string match.
2409
2621
  let wiByPrdItemId = null;
2410
2622
  if (useFkJoin && prdItemIdByKey) {
2411
2623
  try {
2412
- const db = require('./db').getDb();
2624
+ const db = require('../db').getDb();
2413
2625
  const fkByWiId = new Map();
2414
2626
  for (const r of db.prepare('SELECT id, prd_item_id FROM work_items WHERE prd_item_id IS NOT NULL').all()) {
2415
2627
  if (!fkByWiId.has(r.id)) fkByWiId.set(r.id, r.prd_item_id);
@@ -2578,7 +2790,7 @@ function getPrdInfo(config) {
2578
2790
 
2579
2791
  const progress = {
2580
2792
  total, complete, inProgress, paused, missing, donePercent, planTimings,
2581
- verifyPrsByPlan,
2793
+ verifyPrsByPlan, planRevisions,
2582
2794
  items: items.map(i => ({
2583
2795
  id: i.id, name: i.name || i.title, priority: i.priority,
2584
2796
  complexity: i.estimated_complexity || i.size, status: i.status || 'missing',
@@ -2733,7 +2945,9 @@ function _gitExec(localPath, args) {
2733
2945
  // Belt-and-suspenders: explicitly kill the stuck child so it can't
2734
2946
  // linger as an orphaned git.exe. SIGKILL on Unix, forced termination
2735
2947
  // on Windows (Node maps both to TerminateProcess via libuv).
2736
- try { if (child && typeof child.kill === 'function') child.kill('SIGKILL'); }
2948
+ try {
2949
+ if (child) shared.terminateProcess('engine.query.git-probe-timeout', child);
2950
+ }
2737
2951
  catch { /* child may already be dead */ }
2738
2952
  reject(new Error(`git probe watchdog: \`git ${args.join(' ')}\` did not settle within ${watchdogMs}ms`));
2739
2953
  }, watchdogMs);
@@ -3444,7 +3658,7 @@ module.exports = {
3444
3658
  getKnowledgeBaseEntries, getKnowledgeBaseEntriesSnapshot, getKnowledgeBaseIndex,
3445
3659
 
3446
3660
  // Work items & PRD
3447
- getWorkItems, invalidateWorkItemsCache, getPrdInfo,
3661
+ getWorkItems, attachWorkItemPrLinks, invalidateWorkItemsCache, getPrdInfo,
3448
3662
 
3449
3663
  // W-mq5uzmc6001d708f — test hooks for the defensive PR enrichment cache.
3450
3664
  _resetPrEnrichmentCacheForTest,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * engine/safe-expr.js — Hand-rolled recursive-descent expression evaluator.
2
+ * engine/core/safe-expr.js — Hand-rolled recursive-descent expression evaluator.
3
3
  *
4
4
  * Phase 3.1 of feat/plan-resilient-watches (P-w6f4a3e8). Used by watch action
5
5
  * guards (P-w7c5d8b3) and predicate-based watches (Phase 2.x) to evaluate