@yemi33/minions 0.1.2447 → 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 +66 -7
  7. package/dashboard/js/detail-panel.js +36 -0
  8. package/dashboard/js/memory-panel.js +59 -12
  9. package/dashboard/js/qa.js +186 -45
  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 +52 -6
  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
@@ -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,7 @@ function invalidateWorkItemsCache() {
1993
2106
  _workItemsLeanCacheAt = 0;
1994
2107
  }
1995
2108
 
1996
- // getWorkItems(config, { enrich })
2109
+ // getWorkItems(config, { enrich, onlyId })
1997
2110
  // enrich (default true): also cross-reference PRs and populate the
1998
2111
  // detail-modal _artifacts/_notes fields. The /api/work-items list payload
1999
2112
  // needs these (PR badges + note chips).
@@ -2003,11 +2116,26 @@ function invalidateWorkItemsCache() {
2003
2116
  // never read _artifacts/_notes (getAgentStatus fallback, count-only callers)
2004
2117
  // so the agent roster / CC preamble don't pay the enrichment cost on every
2005
2118
  // cold rebuild.
2119
+ // onlyId (W-mscmqk6b03ha5673, enrich-only) — narrow the per-item detail
2120
+ // enrichment to the row(s) carrying this work-item id. The single-record
2121
+ // GET /api/work-items/<id> handler needs ONE fully-enriched row, but paid
2122
+ // the whole-history enrichment on every detail-modal open (at ~2.3k items
2123
+ // that's the same multi-second synchronous block the polled list endpoint
2124
+ // was fixed for, just triggered by a click). The full list is still
2125
+ // returned and the cheap pre-enrich dispatch cross-reference still runs
2126
+ // over everything, so _pendingReason/_skipReason/_blockedBy resolve
2127
+ // identically and the caller can still resolve the complete id-matched set
2128
+ // for its ambiguity check before reading the enriched row.
2006
2129
  function getWorkItems(config, opts) {
2007
2130
  const enrich = !(opts && opts.enrich === false);
2131
+ const onlyId = enrich && opts && typeof opts.onlyId === 'string' && opts.onlyId !== ''
2132
+ ? opts.onlyId
2133
+ : null;
2008
2134
  const now = Date.now();
2009
2135
  config = config || getConfig();
2010
2136
  if (enrich) {
2137
+ // An onlyId caller can be served the fully-enriched cache verbatim — it is
2138
+ // a strict superset of what the caller asked for.
2011
2139
  if (_workItemsCache && (now - _workItemsCacheAt) < 1000) return _workItemsCache;
2012
2140
  } else if (_workItemsLeanCache && (now - _workItemsLeanCacheAt) < 1000) {
2013
2141
  return _workItemsLeanCache;
@@ -2015,7 +2143,7 @@ function getWorkItems(config, opts) {
2015
2143
  let allItems = [];
2016
2144
 
2017
2145
  // SQL is the canonical (and only) work-items store after Phase 9.
2018
- const store = require('./work-items-store');
2146
+ const store = require('../planning/work-items-store');
2019
2147
  allItems = store.readAllWorkItems() || [];
2020
2148
 
2021
2149
  // Cross-reference with dispatch (fill in agent from active dispatch if missing on work item)
@@ -2063,13 +2191,22 @@ function getWorkItems(config, opts) {
2063
2191
  }
2064
2192
  }
2065
2193
 
2194
+ // Rows that pay the per-item detail enrichment below. Without `onlyId` this
2195
+ // is the whole list, exactly as before.
2196
+ const enrichTargets = onlyId ? allItems.filter(it => it && it.id === onlyId) : allItems;
2197
+ // An `onlyId` lookup that matches nothing (the caller's 404 path) skips the
2198
+ // block wholesale, so a miss pays none of the once-per-call prep either.
2199
+ // Callers without `onlyId` keep today's behaviour exactly, empty store
2200
+ // included.
2201
+ const skipEnrichment = onlyId !== null && enrichTargets.length === 0;
2202
+
2066
2203
  // Detail-modal enrichment (PR cross-ref + _artifacts/_notes). Skipped for
2067
2204
  // enrich:false callers — see the getWorkItems header.
2068
- if (enrich) {
2205
+ if (enrich && !skipEnrichment) {
2069
2206
  const projects = getProjects(config);
2070
2207
  // Cross-reference with PRs
2071
2208
  const allPrs = getPullRequests(config);
2072
- for (const item of allItems) {
2209
+ for (const item of enrichTargets) {
2073
2210
  if (item._pr && !item._prUrl) {
2074
2211
  const project = shared.resolveProjectSource(item.project || item._source, projects, { allowCentral: false }).project || null;
2075
2212
  const canonicalPrId = shared.getCanonicalPrId(project, item._pr);
@@ -2135,11 +2272,14 @@ function getWorkItems(config, opts) {
2135
2272
  // P-34fa5d79 — pre-build the wiId → notes map so each item's _notes lookup
2136
2273
  // is O(1) instead of re-scanning inbox+archive per item.
2137
2274
  const _notesByWi = _buildNotesByWiMap(dispatch);
2275
+ // `_workItemIdCounts` stays a full-list tally even under `onlyId` — the
2276
+ // `hasUniqueId` note-linkage guard below asks whether an id collides
2277
+ // ANYWHERE, so narrowing it would change the enriched row's output.
2138
2278
  const _workItemIdCounts = new Map();
2139
2279
  for (const item of allItems) {
2140
2280
  _workItemIdCounts.set(item.id, (_workItemIdCounts.get(item.id) || 0) + 1);
2141
2281
  }
2142
- for (const item of allItems) {
2282
+ for (const item of enrichTargets) {
2143
2283
  const arts = shared.isPlainObject(item._artifacts) ? { ...item._artifacts } : {};
2144
2284
  const agentId = item.dispatched_to || item.agent;
2145
2285
  // W-ms0ytfdd000d5a26 — the model this item actually ran on (or, for items
@@ -2235,10 +2375,14 @@ function getWorkItems(config, opts) {
2235
2375
  // >= 1 000 ms and miss the cache, defeating the cache entirely.
2236
2376
  // (W-mr00aooc000i1d6b — queries-work-items-cache CI failure on slow runners)
2237
2377
  const cacheSetAt = Date.now();
2238
- if (enrich) {
2378
+ // An `onlyId` result is only PARTIALLY enriched, so it must never be
2379
+ // published as either module cache: as the enriched cache it would serve
2380
+ // unenriched rows to the next list caller, and as the lean cache it would
2381
+ // carry enrichment the lean shape does not promise.
2382
+ if (enrich && !onlyId) {
2239
2383
  _workItemsCache = allItems;
2240
2384
  _workItemsCacheAt = cacheSetAt;
2241
- } else {
2385
+ } else if (!enrich) {
2242
2386
  _workItemsLeanCache = allItems;
2243
2387
  _workItemsLeanCacheAt = cacheSetAt;
2244
2388
  }
@@ -2258,7 +2402,7 @@ let _prdResultInputHash = ''; // SQL event version + authored-plan mtim
2258
2402
  function _getPrdInputHash() {
2259
2403
  const mtimes = [];
2260
2404
  try {
2261
- const row = require('./db').getDb().prepare('SELECT COALESCE(MAX(id), 0) AS version FROM events').get();
2405
+ const row = require('../db').getDb().prepare('SELECT COALESCE(MAX(id), 0) AS version FROM events').get();
2262
2406
  mtimes.push(Number(row?.version) || 0);
2263
2407
  } catch { mtimes.push(0); }
2264
2408
  // W-mqacrzis0003df4a — Source-plan markdown mtimes. Without this, the
@@ -2294,6 +2438,10 @@ function getPrdInfo(config) {
2294
2438
  // filename so render-prd.js#renderE2eSection can merge them with the live
2295
2439
  // tracker and keep rendering a merged aggregate after its live record is swept.
2296
2440
  const verifyPrsByPlan = {};
2441
+ // W-msa0mrus00mh466f — live (non-archived) PRD objects keyed by filename so
2442
+ // the revision-state derivation below can run once, server-side, after the
2443
+ // work-item index is built.
2444
+ const livePlansByFile = {};
2297
2445
  let latestUpdatedAt = 0;
2298
2446
 
2299
2447
  // Phase 10 step 4.3 — FK-based WI↔PRD-item join scaffolding. `prdItemIdByKey`
@@ -2332,6 +2480,7 @@ function getPrdInfo(config) {
2332
2480
  completedAt: plan.completedAt || '',
2333
2481
  _archived: isArch,
2334
2482
  });
2483
+ if (!isArch) livePlansByFile[pf] = plan;
2335
2484
  if (Array.isArray(plan.verifyPrs) && plan.verifyPrs.length > 0) {
2336
2485
  verifyPrsByPlan[pf] = plan.verifyPrs.map(r => ({
2337
2486
  id: r.id, url: r.url || '', title: r.title || '',
@@ -2365,7 +2514,7 @@ function getPrdInfo(config) {
2365
2514
  // feature-id string match.
2366
2515
  if (useFkJoin) {
2367
2516
  try {
2368
- const db = require('./db').getDb();
2517
+ const db = require('../db').getDb();
2369
2518
  const map = new Map();
2370
2519
  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
2520
  map.set(`${r.archived ? 1 : 0} ${r.filename} ${r.feature_id}`, r.id);
@@ -2375,7 +2524,7 @@ function getPrdInfo(config) {
2375
2524
  }
2376
2525
 
2377
2526
  // SQLite is the unconditional PRD source.
2378
- const prdStore = require('./prd-store');
2527
+ const prdStore = require('../planning/prd-store');
2379
2528
  for (const { filename, archived, plan, updatedAt } of prdStore.listPrdRows()) {
2380
2529
  try {
2381
2530
  latestUpdatedAt = Math.max(latestUpdatedAt, updatedAt);
@@ -2394,7 +2543,7 @@ function getPrdInfo(config) {
2394
2543
  // so they don't masquerade as a 2nd PRD item in the aggregate guard (W-mpem52qn).
2395
2544
  const wiById = {};
2396
2545
  const allWiById = {};
2397
- for (const wi of require('./work-items-store').readAllWorkItems()) {
2546
+ for (const wi of require('../planning/work-items-store').readAllWorkItems()) {
2398
2547
  if (!wi?.id) {
2399
2548
  console.warn(`[queries] Skipping work item without id in ${wi?._source || 'unknown'}:`, JSON.stringify(wi).slice(0, 120));
2400
2549
  continue;
@@ -2403,13 +2552,27 @@ function getPrdInfo(config) {
2403
2552
  if (wi.sourcePlan && !wiById[wi.id]) wiById[wi.id] = wi;
2404
2553
  }
2405
2554
 
2555
+ // W-msa0mrus00mh466f — canonical PRD revision state, derived ONCE here.
2556
+ // `revision-requested` and `planStale` can both be true on the same PRD; the
2557
+ // dashboard must not offer competing recovery paths, so the server decides
2558
+ // which state applies and the renderer just paints it.
2559
+ const planRevisions = {};
2560
+ const allWorkItemList = Object.values(allWiById);
2561
+ for (const [pf, plan] of Object.entries(livePlansByFile)) {
2562
+ try {
2563
+ const revisionWi = shared.findPrdRevisionWorkItem(plan, pf, allWorkItemList);
2564
+ const revision = shared.derivePrdRevisionState(plan, revisionWi);
2565
+ if (revision.state !== shared.PRD_REVISION_STATE.NONE) planRevisions[pf] = revision;
2566
+ } catch { /* a malformed PRD must not break the whole PRD read */ }
2567
+ }
2568
+
2406
2569
  // Index work items by their stable SQL FK so the per-item join can resolve
2407
2570
  // PRD item → WI by prd_items.id rather than the feature-id string.
2408
2571
  // Best-effort: null map → the join falls back to the legacy string match.
2409
2572
  let wiByPrdItemId = null;
2410
2573
  if (useFkJoin && prdItemIdByKey) {
2411
2574
  try {
2412
- const db = require('./db').getDb();
2575
+ const db = require('../db').getDb();
2413
2576
  const fkByWiId = new Map();
2414
2577
  for (const r of db.prepare('SELECT id, prd_item_id FROM work_items WHERE prd_item_id IS NOT NULL').all()) {
2415
2578
  if (!fkByWiId.has(r.id)) fkByWiId.set(r.id, r.prd_item_id);
@@ -2578,7 +2741,7 @@ function getPrdInfo(config) {
2578
2741
 
2579
2742
  const progress = {
2580
2743
  total, complete, inProgress, paused, missing, donePercent, planTimings,
2581
- verifyPrsByPlan,
2744
+ verifyPrsByPlan, planRevisions,
2582
2745
  items: items.map(i => ({
2583
2746
  id: i.id, name: i.name || i.title, priority: i.priority,
2584
2747
  complexity: i.estimated_complexity || i.size, status: i.status || 'missing',
@@ -2733,7 +2896,9 @@ function _gitExec(localPath, args) {
2733
2896
  // Belt-and-suspenders: explicitly kill the stuck child so it can't
2734
2897
  // linger as an orphaned git.exe. SIGKILL on Unix, forced termination
2735
2898
  // on Windows (Node maps both to TerminateProcess via libuv).
2736
- try { if (child && typeof child.kill === 'function') child.kill('SIGKILL'); }
2899
+ try {
2900
+ if (child) shared.terminateProcess('engine.query.git-probe-timeout', child);
2901
+ }
2737
2902
  catch { /* child may already be dead */ }
2738
2903
  reject(new Error(`git probe watchdog: \`git ${args.join(' ')}\` did not settle within ${watchdogMs}ms`));
2739
2904
  }, watchdogMs);
@@ -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