@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/timeout.js — Runtime timeout, stale-orphan cleanup, steering, and idle checks.
2
+ * engine/orchestration/timeout.js — Runtime timeout, stale-orphan cleanup, steering, and idle checks.
3
3
  *
4
4
  * Live-checkout dispatches (project.checkoutMode === 'live') are killed by PID
5
5
  * exactly like isolated-mode dispatches — no special handling, no in-place
@@ -9,9 +9,9 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
- const shared = require('./shared');
13
- const queries = require('./queries');
14
- const steering = require('./steering');
12
+ const shared = require('../core/shared');
13
+ const queries = require('../core/queries');
14
+ const steering = require('../agents/steering');
15
15
 
16
16
  const { safeRead, safeWrite, safeJson, mutateWorkItems, getProjects, log, ts,
17
17
  ENGINE_DEFAULTS, ENGINE_DIR, WI_STATUS, WORK_TYPE, DISPATCH_RESULT, AGENT_STATUS, FAILURE_CLASS } = shared;
@@ -22,7 +22,7 @@ const MINIONS_DIR = shared.MINIONS_DIR;
22
22
  // Lazy require to break circular dependency with engine.js
23
23
  // Only needed for engine().activeProcesses and engine().engineRestartGraceUntil — log/ts come from shared.js
24
24
  let _engine = null;
25
- function engine() { if (!_engine) _engine = require('../engine'); return _engine; }
25
+ function engine() { if (!_engine) _engine = require('../../engine'); return _engine; }
26
26
 
27
27
  function resolveDispatchBranchPath(item, config) {
28
28
  if (item?.worktreePath) return item.worktreePath;
@@ -57,7 +57,7 @@ function resolveDispatchProject(item, config) {
57
57
 
58
58
  function getWorktreeProcessAnchorReason(item, managedSpawnSpawnedCount = 0) {
59
59
  if (!item?.worktreePath) return null;
60
- const decision = require('./worktree-gc').shouldGcDispatchWorktree({
60
+ const decision = require('../worktrees/gc').shouldGcDispatchWorktree({
61
61
  worktreePath: item.worktreePath,
62
62
  agentId: item.agent,
63
63
  managedSpawnSpawnedCount,
@@ -74,7 +74,7 @@ function gcRecoveredDispatchWorktree(item, project, config) {
74
74
  project.localPath,
75
75
  config.engine?.worktreeRoot || ENGINE_DEFAULTS.worktreeRoot,
76
76
  );
77
- require('./worktree-gc').gcDispatchWorktreeIfOrphan({
77
+ require('../worktrees/gc').gcDispatchWorktreeIfOrphan({
78
78
  worktreePath: item.worktreePath,
79
79
  gitRoot: project.localPath,
80
80
  worktreeRoot,
@@ -87,15 +87,28 @@ function gcRecoveredDispatchWorktree(item, project, config) {
87
87
  async function _finalizeDeadDispatch(item, reason, failureClass, config, deps = {}) {
88
88
  const preserveBranch = deps.preserveBranch || preserveDispatchBranch;
89
89
  const restoreLiveCheckout = deps.restoreLiveCheckout ||
90
- require('./live-checkout').maybeRestoreLiveCheckoutFromRecord;
90
+ require('../worktrees/live-checkout').maybeRestoreLiveCheckoutFromRecord;
91
91
  const complete = deps.complete || dispatch().completeDispatch;
92
+ // W-ms9tcs3o01y03383 — optional observability label describing HOW this
93
+ // dispatch reached its terminal state (shared.DISPATCH_COMPLETION_PATH).
94
+ const completionPath = deps.completionPath || null;
92
95
  const deferredTerminal = item?._worktreeProtectionDeferred?.terminal || {};
93
96
  const terminalReason = deferredTerminal.reason || reason;
94
97
  const terminalResultSummary = deferredTerminal.resultSummary || '';
95
- const terminalCompleteOpts = deferredTerminal.completeOpts
98
+ const terminalBaseCompleteOpts = deferredTerminal.completeOpts
96
99
  && typeof deferredTerminal.completeOpts === 'object'
97
100
  ? deferredTerminal.completeOpts
98
- : (failureClass ? { failureClass } : {});
101
+ : (failureClass
102
+ ? {
103
+ failureClass,
104
+ ...(failureClass === FAILURE_CLASS.ENGINE_RESTART
105
+ ? { retryBudget: 'infrastructure', countAgentRetryFailure: false }
106
+ : {}),
107
+ }
108
+ : {});
109
+ const terminalCompleteOpts = completionPath
110
+ ? { ...terminalBaseCompleteOpts, completionPath }
111
+ : terminalBaseCompleteOpts;
99
112
 
100
113
  if (resolveDispatchBranchPath(item, config) && item.meta?.branch) {
101
114
  try {
@@ -203,7 +216,7 @@ const STEERING_KILL_INTERVALS_MS = [30000, 60000, 120000];
203
216
  // have already terminated (delivered/dropped/etc).
204
217
  const STEERING_ACTIVE_STATUSES = new Set(['queued', 'live_kill', 'deferred', 're_spawning']);
205
218
 
206
- // W-mq066js7000fff1f-c (Gap G): lazy require for engine/steering-store.js.
219
+ // W-mq066js7000fff1f-c (Gap G): lazy require for engine/persistence/steering-store.js.
207
220
  // Sibling branch -d ships the store; my branch ships independently and no-ops
208
221
  // when the module is absent. Only swallows MODULE_NOT_FOUND for the exact
209
222
  // resolution — real syntax/runtime errors propagate so a broken store fails
@@ -214,11 +227,11 @@ function getSteeringStore() {
214
227
  if (_steeringStoreLoaded) return _steeringStore;
215
228
  _steeringStoreLoaded = true;
216
229
  try {
217
- _steeringStore = require('./steering-store');
230
+ _steeringStore = require('../persistence/steering-store');
218
231
  } catch (err) {
219
232
  const isMissing = err && err.code === 'MODULE_NOT_FOUND' && /steering-store/.test(String(err.message || ''));
220
233
  if (!isMissing) {
221
- try { log('warn', `Steering: failed to load engine/steering-store.js: ${err.message}`); } catch {}
234
+ try { log('warn', `Steering: failed to load engine/persistence/steering-store.js: ${err.message}`); } catch {}
222
235
  }
223
236
  _steeringStore = null;
224
237
  }
@@ -281,7 +294,7 @@ function runtimeSupportsMidRunSessionId(info) {
281
294
  if (typeof info?.runtime?.capabilities?.midRunSessionId === 'boolean') return info.runtime.capabilities.midRunSessionId;
282
295
  if (info?.runtimeName) {
283
296
  try {
284
- const { resolveRuntime } = require('./runtimes');
297
+ const { resolveRuntime } = require('../runtimes');
285
298
  const runtime = resolveRuntime(info.runtimeName);
286
299
  if (typeof runtime.capabilities?.midRunSessionId === 'boolean') return runtime.capabilities.midRunSessionId;
287
300
  } catch {
@@ -316,7 +329,7 @@ function deferSteeringUntilCheckpoint(id, info, steerEntry) {
316
329
  // once the resumed turn produces output evidence.
317
330
  if (steerEntry?.steerId) {
318
331
  try {
319
- const store = require('./steering-store');
332
+ const store = require('../persistence/steering-store');
320
333
  store.updateStatus(steerEntry.steerId, 'deferred', { dispatchId: id, runtime: info?.runtimeName || null });
321
334
  } catch { /* best-effort */ }
322
335
  }
@@ -369,29 +382,46 @@ function checkDeferredStranded(activeProcesses, config) {
369
382
  }
370
383
  }
371
384
 
372
- // P-6e2a4c15 — cancel-vs-kill for pooled dispatches. A pooled dispatch's
373
- // `activeProcesses` record (`info`) carries `_pooled: true` (stamped by
374
- // engine.js#spawnAgent, P-1d8f0b93) and its `.proc` is a PooledAgentProcess
375
- // facade whose `.pid` is the SHARED, long-lived ACP worker's real OS pid —
376
- // not a per-dispatch child process. Every OS-level kill helper in this file
377
- // (`shared.killGracefully`, `shared.killImmediate`, `_escalatePlatformKill`)
378
- // ultimately runs `taskkill /F /T /PID <pid>` or SIGKILLs that pid's whole
379
- // process tree. Doing that to a pooled dispatch would kill the shared worker
380
- // out from under every OTHER dispatch waiting to reuse it from the pool —
381
- // exactly the collateral-damage risk `PooledAgentProcess#kill()` (P-1d8f0b93)
382
- // was built to avoid: it cancels the in-flight ACP turn and releases the
383
- // worker back to the pool without touching the OS process. Route every kill
384
- // request here through this helper so no call site can accidentally bypass
385
- // that primitive for a pooled record.
386
- function killTrackedProcess(info, { graceMs } = {}) {
387
- if (!info || !info.proc) return;
385
+ // A pooled dispatch PID belongs to its shared ACP worker. Its registered
386
+ // gateway site cancels the facade; cold sites verify the tracked PID identity.
387
+ function killTrackedProcess(info, { graceMs, processes } = {}) {
388
+ if (!info || !info.proc) return false;
388
389
  if (info._pooled && typeof info.proc.kill === 'function') {
389
- try { info.proc.kill(); }
390
+ try { return shared.terminateProcess('engine.timeout.tracked-pooled', info.proc); }
390
391
  catch (err) { try { log('warn', `Pooled dispatch cancel failed for ${info.agentId || '?'}: ${err.message}`); } catch {} }
391
- return;
392
+ return false;
393
+ }
394
+ const ownership = {
395
+ recordedAtMs: info.runtimePidObservedAt,
396
+ tracked: true,
397
+ ...(Array.isArray(processes) ? { processes } : {}),
398
+ };
399
+ if (graceMs == null) {
400
+ return shared.terminateProcess('engine.timeout.tracked-cold', info.proc, ownership);
392
401
  }
393
- if (graceMs == null) shared.killImmediate(info.proc);
394
- else shared.killGracefully(info.proc, graceMs);
402
+ return shared.terminateProcess('engine.timeout.tracked-cold-graceful', info.proc, {
403
+ ...ownership,
404
+ graceMs,
405
+ });
406
+ }
407
+
408
+ function cancelTrackedProcessesWithoutDispatch(activeProcesses, activeDispatchItems, opts = {}) {
409
+ const activeIds = new Set((activeDispatchItems || []).map(item => item?.id).filter(Boolean));
410
+ let cancelled = 0;
411
+ let processSnapshot = Array.isArray(opts.processes) ? opts.processes : null;
412
+ for (const [id, info] of activeProcesses.entries()) {
413
+ if (activeIds.has(id)) continue;
414
+ if (!info?._pooled && !processSnapshot) processSnapshot = shared.listAllProcesses();
415
+ const stopped = killTrackedProcess(info, { processes: processSnapshot });
416
+ if (stopped) {
417
+ log('warn', `Cancelling tracked process ${id} after its dispatch record was removed`);
418
+ cancelled++;
419
+ } else if (!info?._pooled) {
420
+ log('warn', `Dropping unowned tracked process ${id} without killing its PID`);
421
+ }
422
+ activeProcesses.delete(id);
423
+ }
424
+ return cancelled;
395
425
  }
396
426
 
397
427
  // W-mq066js7000fff1f-c (Gap C): runs the kill-retry escalation ladder for an
@@ -451,49 +481,16 @@ function _escalatePlatformKill(info) {
451
481
  // ACP turn and release the worker instead of taskkill/SIGKILLing the
452
482
  // shared worker process. See killTrackedProcess's comment above.
453
483
  if (info?._pooled && info.proc && typeof info.proc.kill === 'function') {
454
- try { info.proc.kill(); } catch { /* best-effort */ }
455
- return;
456
- }
457
- const pid = info?.proc?.pid;
458
- if (!pid) return;
459
- if (process.platform === 'win32') {
460
- try { shared.exec(`taskkill /F /T /PID ${pid}`, { timeout: 3000 }); }
461
- catch { /* may already be dead */ }
484
+ try { shared.terminateProcess('engine.timeout.tracked-pooled', info.proc); } catch { /* best-effort */ }
462
485
  return;
463
486
  }
464
- // Unix: collect descendant PIDs (deepest first), SIGKILL each, then a final
465
- // `pkill -KILL -P <pid>` sweep for anything pgrep missed.
466
- const descendants = _collectDescendantPids(pid);
467
- for (const child of descendants) {
468
- try { process.kill(child, 'SIGKILL'); } catch { /* gone */ }
469
- }
470
- try { shared.exec(`pkill -KILL -P ${pid}`, { timeout: 3000 }); }
471
- catch { /* children may already be dead */ }
472
- try { process.kill(pid, 'SIGKILL'); } catch { /* parent may already be dead */ }
473
- }
474
-
475
- function _collectDescendantPids(rootPid) {
476
- // Returns descendant PIDs ordered deepest-first so leaves die before parents
477
- // (otherwise grandchildren get re-parented to init and disappear from pgrep -P
478
- // <pid>). BFS the tree, then reverse.
479
- const visited = new Set();
480
- const queue = [rootPid];
481
- const order = [];
482
- while (queue.length) {
483
- const current = queue.shift();
484
- if (visited.has(current)) continue;
485
- visited.add(current);
486
- let out = '';
487
- try { out = String(shared.exec(`pgrep -P ${current}`, { timeout: 2000 }) || ''); }
488
- catch { /* no children or pgrep missing */ continue; }
489
- const children = out.split(/\s+/).map(s => Number(s)).filter(n => Number.isFinite(n) && n > 0);
490
- for (const child of children) {
491
- if (visited.has(child)) continue;
492
- order.push(child);
493
- queue.push(child);
494
- }
495
- }
496
- return order.reverse();
487
+ if (!info?.proc?.pid) return;
488
+ try {
489
+ shared.terminateProcess('engine.timeout.tracked-cold-escalation', info.proc, {
490
+ recordedAtMs: info.runtimePidObservedAt,
491
+ tracked: true,
492
+ });
493
+ } catch { /* may already be dead */ }
497
494
  }
498
495
 
499
496
  function checkSteering(config) {
@@ -604,7 +601,7 @@ function checkSteering(config) {
604
601
  // process to deliver this message via session resume. Best-effort.
605
602
  if (steerEntry?.steerId) {
606
603
  try {
607
- const store = require('./steering-store');
604
+ const store = require('../persistence/steering-store');
608
605
  store.updateStatus(steerEntry.steerId, 'live_kill', { dispatchId: id, runtime: info?.runtimeName || null });
609
606
  } catch { /* best-effort */ }
610
607
  }
@@ -657,7 +654,7 @@ function trackedProcessPid(procInfo) {
657
654
  return Number.isFinite(pid) && pid > 0 ? pid : null;
658
655
  }
659
656
 
660
- function isTrackedProcessAlive(procInfo) {
657
+ function isTrackedProcessAlive(procInfo, opts = {}) {
661
658
  if (!procInfo) return false;
662
659
  const proc = procInfo.proc;
663
660
  if (proc && Object.prototype.hasOwnProperty.call(proc, 'exitCode') && proc.exitCode !== null) {
@@ -666,12 +663,9 @@ function isTrackedProcessAlive(procInfo) {
666
663
 
667
664
  const pid = trackedProcessPid(procInfo);
668
665
  if (!pid) return !!proc && proc.killed !== true;
669
- try {
670
- process.kill(pid, 0);
671
- return true;
672
- } catch {
673
- return false;
674
- }
666
+ // W-ms6dmv99005o26f4 — EPERM means the kernel FOUND the process and refused the
667
+ // signal; only ESRCH proves absence. See process-utils#inspectPidLiveness.
668
+ return shared.inspectPidLiveness(pid, opts).alive;
675
669
  }
676
670
 
677
671
  // Read the on-disk PID for a dispatch ID, or null if missing/invalid. Shared by
@@ -689,16 +683,43 @@ function _readDispatchPid(itemId) {
689
683
  return Number.isFinite(pid) && pid > 0 ? pid : null;
690
684
  }
691
685
 
686
+ // W-ms6dmv99005o26f4 — decide whether an ALIVE OS PID may be demoted to "dead"
687
+ // because its command line does not look like an agent runtime.
688
+ //
689
+ // The recycled-PID cross-check this backs is real and must stay (Windows reuses
690
+ // PIDs aggressively, and a coincidentally-alive unrelated process would otherwise
691
+ // park the dispatch as `active` forever). But `isProcessCommandLineMatchingAgent`
692
+ // returns a bare `false` for BOTH "I read the command line and it isn't an agent"
693
+ // and "I could not read the command line at all". Collapsing those made a probe
694
+ // outage — PowerShell Get-CimInstance timing out under fleet load, a `/proc` read
695
+ // racing teardown, `ps` failing to spawn — read as proof of death for every silent
696
+ // dispatch simultaneously. That is the second link of the 2026-07-29T17:44Z
697
+ // false-orphan storm, and unlike the restart link it needs no engine restart to
698
+ // fire.
699
+ //
700
+ // Only a CONCLUSIVE mismatch demotes the PID. An unobservable one is kept, exactly
701
+ // as if the cross-check had not run — the dispatch stays on its normal bounded
702
+ // orphan path and is re-evaluated next tick.
703
+ function evaluateLivePidCommandLine(livePid, opts = {}) {
704
+ if (!livePid) return { treatAsDead: false, inconclusive: true, reason: 'no-pid-file' };
705
+ const inspect = opts.inspect || shared.inspectProcessCommandLineForAgent;
706
+ const evidence = inspect(livePid, opts.inspectOpts || {}) || {};
707
+ if (!evidence.conclusive) {
708
+ return { treatAsDead: false, inconclusive: true, reason: evidence.reason || 'unobservable' };
709
+ }
710
+ return { treatAsDead: !evidence.match, inconclusive: false, reason: evidence.reason || 'conclusive' };
711
+ }
712
+
692
713
  // Last-resort liveness check via the on-disk PID file (engine/tmp/pid-<safeId>.pid).
693
714
  // Used by orphan detection to avoid false-positive kills when the engine has lost the
694
715
  // tracked process handle (engine restart, never-tracked spawn, etc.) but the OS-level
695
716
  // child process is still alive and healthy. The safeId here mirrors engine.js spawn
696
- // (id.replace(/[:\\/*?"<>|]/g, '-')) — same pattern engine/cli.js uses to re-attach.
697
- function isOsPidAliveForDispatch(itemId) {
717
+ // (id.replace(/[:\\/*?"<>|]/g, '-')) — same pattern engine/operations/cli.js uses to re-attach.
718
+ function isOsPidAliveForDispatch(itemId, opts = {}) {
698
719
  const pid = _readDispatchPid(itemId);
699
720
  if (!pid) return false;
700
- try { process.kill(pid, 0); return true; }
701
- catch { return false; }
721
+ // W-ms6dmv99005o26f4 — EPERM is proof of existence, not of death.
722
+ return shared.inspectPidLiveness(pid, opts).alive;
702
723
  }
703
724
 
704
725
  function readFileTail(filePath, maxBytes) {
@@ -733,10 +754,10 @@ function readAcceptedCompletionReport(item, procInfo) {
733
754
  const expectedNonce = procInfo?._completionNonce || null;
734
755
  if (!expectedNonce) return null;
735
756
  try {
736
- const report = require('./lifecycle').parseCompletionReportFile(item);
737
- if (!report) return null;
738
- if (typeof report.nonce !== 'string' || report.nonce !== expectedNonce) return null;
739
- return report;
757
+ return shared.acceptCompletionReportForNonce(
758
+ require('./lifecycle').parseCompletionReportFile(item),
759
+ expectedNonce,
760
+ );
740
761
  } catch (e) {
741
762
  log('warn', `resume-stall completion-report probe failed for ${item?.id}: ${e.message}`);
742
763
  return null;
@@ -745,12 +766,14 @@ function readAcceptedCompletionReport(item, procInfo) {
745
766
 
746
767
  function parseProcessExitCode(logText) {
747
768
  if (!logText) return null;
748
- const exitPattern = /(?:^|\n)\[process-exit\]\s+(?:code=)?(-?\d+|spawn-failed)(?=\s|$)/g;
749
- let lastMatch = null;
750
- let m;
751
- while ((m = exitPattern.exec(logText)) !== null) lastMatch = m;
752
- if (!lastMatch) return null;
753
- return lastMatch[1] === 'spawn-failed' ? -1 : parseInt(lastMatch[1], 10);
769
+ // A sentinel is terminal evidence only while it remains the final non-empty
770
+ // line. Agent live-output files survive engine restarts and can therefore
771
+ // contain an old sentinel followed by a newer attempt's active output.
772
+ const match = String(logText).match(
773
+ /(?:^|\n)\[process-exit\]\s+(?:code=)?(-?\d+|spawn-failed)[^\S\r\n]*(?:\r?\n)?\s*$/
774
+ );
775
+ if (!match) return null;
776
+ return match[1] === 'spawn-failed' ? -1 : parseInt(match[1], 10);
754
777
  }
755
778
 
756
779
  function terminalResultIndicatesError(obj) {
@@ -817,11 +840,12 @@ function checkTimeouts(config) {
817
840
  // The exception is a resumed runtime that has not produced its first stdout/stderr
818
841
  // heartbeat after spawn; that is the "alive but stuck in --resume" failure mode.
819
842
  const dispatchData = getDispatch();
843
+ cancelTrackedProcessesWithoutDispatch(activeProcesses, dispatchData.active);
820
844
  const deadItems = [];
821
845
  const legacyAnnotationClears = new Set();
822
846
  const finalizationPromises = [];
823
847
 
824
- function completeFromOutput(item, liveLogPath, processExitCode, detectedLogText, hasProcess, expectedProcInfo = activeProcesses.get(item.id)) {
848
+ function completeFromOutput(item, liveLogPath, processExitCode, detectedLogText, hasProcess, expectedProcInfo = activeProcesses.get(item.id), completionPath = shared.DISPATCH_COMPLETION_PATH.OUTPUT_DETECTION) {
825
849
  if (hasProcess && (
826
850
  expectedProcInfo?._steeringAt
827
851
  || expectedProcInfo?._steeringMessage
@@ -951,6 +975,7 @@ function checkTimeouts(config) {
951
975
  }
952
976
 
953
977
  const completionOpts = {
978
+ completionPath,
954
979
  ...(structuredCompletion?._path ? { completionReportPath: structuredCompletion._path } : {}),
955
980
  ...(structuredCompletion ? { structuredCompletion } : {}),
956
981
  };
@@ -1040,7 +1065,7 @@ function checkTimeouts(config) {
1040
1065
 
1041
1066
  if (shared.isLiveCheckoutDispatchRecord(item, config)) {
1042
1067
  try {
1043
- await require('./live-checkout').maybeRestoreLiveCheckoutFromRecord({
1068
+ await require('../worktrees/live-checkout').maybeRestoreLiveCheckoutFromRecord({
1044
1069
  item,
1045
1070
  config,
1046
1071
  isTerminalFailure: effectiveResult === DISPATCH_RESULT.ERROR,
@@ -1210,7 +1235,8 @@ function checkTimeouts(config) {
1210
1235
  // race against our own cleanup. procInfo is still passed as the
1211
1236
  // expected proc info so the per-spawn nonce is available to the
1212
1237
  // lifecycle hooks.
1213
- completeFromOutput(item, liveLogPath, 0, liveLogTailForItem(liveLogPath), false, procInfo);
1238
+ completeFromOutput(item, liveLogPath, 0, liveLogTailForItem(liveLogPath), false, procInfo,
1239
+ shared.DISPATCH_COMPLETION_PATH.RESUME_STALL_REPORT);
1214
1240
  activeProcesses.delete(item.id);
1215
1241
  killTrackedProcess(procInfo, { graceMs: 5000 });
1216
1242
  continue;
@@ -1223,7 +1249,12 @@ function checkTimeouts(config) {
1223
1249
  try { shared.safeUnlink(path.join(AGENTS_DIR, item.agent, 'session.json')); } catch {}
1224
1250
  activeProcesses.delete(item.id);
1225
1251
  killTrackedProcess(procInfo, { graceMs: 5000 });
1226
- deadItems.push({ item, reason, failureClass: FAILURE_CLASS.TIMEOUT });
1252
+ deadItems.push({
1253
+ item,
1254
+ reason,
1255
+ failureClass: FAILURE_CLASS.TIMEOUT,
1256
+ completionPath: shared.DISPATCH_COMPLETION_PATH.RESUME_STALL_TIMEOUT,
1257
+ });
1227
1258
  }
1228
1259
  }
1229
1260
  continue;
@@ -1276,7 +1307,7 @@ function checkTimeouts(config) {
1276
1307
  // Last-resort PID check: lost tracked handle but OS process may still be alive.
1277
1308
  // P-3f5b7d26 — SKIPPED for pooled dispatches (item.pooled, persisted by
1278
1309
  // engine.js#spawnAgent). The PID recorded for a pooled dispatch is the
1279
- // shared ACP worker's real OS pid (engine/acp-transport.js#spawnAcp),
1310
+ // shared ACP worker's real OS pid (engine/agents/acp-transport.js#spawnAcp),
1280
1311
  // whose command line is literally `copilot --acp ...` — it would always
1281
1312
  // pass isProcessCommandLineMatchingAgent's `cmdline.includes('copilot')`
1282
1313
  // check below even though that worker has no relationship to THIS
@@ -1290,11 +1321,23 @@ function checkTimeouts(config) {
1290
1321
  // under an unrelated process (Windows reuses PIDs aggressively). Without
1291
1322
  // this cmdline gate, the dispatch is parked indefinitely as 'active'
1292
1323
  // pinned to a process that has nothing to do with the agent.
1324
+ //
1325
+ // W-ms6dmv99005o26f4 — this gate must fire on OBSERVED mismatch only. The
1326
+ // boolean helper also returns false when the per-PID command-line probe
1327
+ // itself fails (PowerShell Get-CimInstance timing out under fleet load,
1328
+ // /proc read racing a namespace, `ps` spawn EAGAIN), which turned a probe
1329
+ // outage into "treating as dead" for every silent dispatch at once — the
1330
+ // second link of the 2026-07-29T17:44Z false-orphan storm. An alive OS PID
1331
+ // we merely failed to inspect is kept, exactly as if the probe had not run.
1293
1332
  const livePid = _readDispatchPid(item.id);
1294
- if (livePid && !shared.isProcessCommandLineMatchingAgent(livePid)) {
1333
+ const cmdlineEvidence = evaluateLivePidCommandLine(livePid);
1334
+ if (cmdlineEvidence.treatAsDead) {
1295
1335
  log('warn', `Orphan check: ${item.agent} (${item.id}) — PID ${livePid} alive but command line doesn't match agent, treating as dead`);
1296
1336
  processAlive = false;
1297
1337
  } else {
1338
+ if (cmdlineEvidence.inconclusive) {
1339
+ log('warn', `Orphan check: ${item.agent} (${item.id}) — PID ${livePid} alive but command line could not be observed (${cmdlineEvidence.reason}); keeping the dispatch rather than inferring death`);
1340
+ }
1298
1341
  log('info', `Orphan check: ${item.agent} (${item.id}) silent ${silentSec}s but OS PID is alive — keeping [${_logState}]`);
1299
1342
  continue;
1300
1343
  }
@@ -1324,16 +1367,20 @@ function checkTimeouts(config) {
1324
1367
  dispatch().updateAgentStatus(item.id, AGENT_STATUS.TIMED_OUT, `Orphaned — no process, silent for ${silentSec}s`);
1325
1368
  // Clear session so retry starts fresh
1326
1369
  try { shared.safeUnlink(path.join(AGENTS_DIR, item.agent, 'session.json')); } catch {}
1327
- deadItems.push({ item, reason: `Orphaned — no process, silent for ${silentSec}s` });
1370
+ deadItems.push({
1371
+ item,
1372
+ reason: `Orphaned — no process, silent for ${silentSec}s`,
1373
+ failureClass: confirmedDeadAtRestart ? FAILURE_CLASS.ENGINE_RESTART : FAILURE_CLASS.TIMEOUT,
1374
+ });
1328
1375
  activeProcesses.delete(item.id);
1329
1376
  }
1330
1377
  }
1331
1378
 
1332
1379
  // Clean up dead items
1333
- for (const { item, reason, failureClass } of deadItems) {
1380
+ for (const { item, reason, failureClass, completionPath } of deadItems) {
1334
1381
  const finalizationOwner = 'timeout-orphan';
1335
1382
  if (!shared.claimDispatchFinalization(item.id, finalizationOwner)) continue;
1336
- const finalizationPromise = _finalizeDeadDispatch(item, reason, failureClass, config, { complete: completeDispatch })
1383
+ const finalizationPromise = _finalizeDeadDispatch(item, reason, failureClass, config, { complete: completeDispatch, completionPath })
1337
1384
  .catch(err => log('warn', `Orphan cleanup failed for ${item.id}: ${err.message}`))
1338
1385
  .finally(() => shared.releaseDispatchFinalization(item.id, finalizationOwner));
1339
1386
  finalizationPromises.push(finalizationPromise);
@@ -1413,14 +1460,16 @@ module.exports = {
1413
1460
  checkIdleThreshold,
1414
1461
  dropSteeringForPurgedSession,
1415
1462
  isOsPidAliveForDispatch,
1463
+ evaluateLivePidCommandLine, // exported for testing
1464
+ _isTrackedProcessAlive: isTrackedProcessAlive, // exported for testing
1416
1465
  parseProcessExitCode, terminalResultIndicatesError, parseTerminalResultFallbackExitCode, // exported for testing
1417
1466
  readFileTail, runtimeSupportsMidRunSessionId, // exported for testing
1418
1467
  readAcceptedCompletionReport, liveLogTailForItem, // exported for testing (W-ms0vk9nv000z985b)
1419
1468
  // exported for testing
1420
- rememberDeferredSteering, checkDeferredStranded, _runSteeringKillLadder, _collectDescendantPids,
1469
+ rememberDeferredSteering, checkDeferredStranded, _runSteeringKillLadder,
1421
1470
  _resetSteeringStoreCacheForTest, _setSteeringStoreForTest,
1422
1471
  // exported for testing — steering clamp helpers and deferred checkpoint
1423
1472
  _clampSteeringDeferredMaxMs, _clampSteeringMaxKillRetries, _appendLiveOutputLine, deferSteeringUntilCheckpoint,
1424
- killTrackedProcess, _escalatePlatformKill, // exported for testing — P-6e2a4c15 cancel-vs-kill
1473
+ killTrackedProcess, cancelTrackedProcessesWithoutDispatch, _escalatePlatformKill, // exported for testing — P-6e2a4c15 cancel-vs-kill
1425
1474
  _finalizeDeadDispatch, // exported for testing
1426
1475
  };
@@ -1,4 +1,4 @@
1
- // engine/db-events.js — `emitStateEvent(topic, payload?)` helper.
1
+ // engine/persistence/db-events.js — `emitStateEvent(topic, payload?)` helper.
2
2
  //
3
3
  // Append a row to the `events` table so the dashboard's status cache (and
4
4
  // any future topic-scoped subscriber) sees the change on its next poll.
@@ -16,7 +16,7 @@ let _logged = false;
16
16
 
17
17
  function emitStateEvent(topic, payload) {
18
18
  try {
19
- const { getDb } = require('./db');
19
+ const { getDb } = require('../db');
20
20
  const db = getDb();
21
21
  if (payload !== undefined && payload !== null) {
22
22
  db.prepare('INSERT INTO events (topic, payload) VALUES (?, ?)')
@@ -1,4 +1,4 @@
1
- // engine/dispatch-store.js — SQL-backed section-shaped dispatch state:
1
+ // engine/persistence/dispatch-store.js — SQL-backed section-shaped dispatch state:
2
2
  //
3
3
  // readDispatchSectioned() -> { pending, active, completed, review }
4
4
  // applyDispatchMutation(fn) -> runs fn against the section object,
@@ -9,8 +9,8 @@
9
9
  // (event emission, cache invalidation)
10
10
  // when nothing actually changed.
11
11
  //
12
- // All callers go through engine/dispatch.js#mutateDispatch and
13
- // engine/queries.js#getDispatch as before — those are thin wrappers
12
+ // All callers go through engine/orchestration/dispatch.js#mutateDispatch and
13
+ // engine/core/queries.js#getDispatch as before — those are thin wrappers
14
14
  // over the helpers here. Zero changes at any call site.
15
15
 
16
16
  const SECTIONS = ['pending', 'active', 'completed', 'review'];
@@ -33,7 +33,7 @@ function _parseRow(row) {
33
33
  }
34
34
 
35
35
  function readDispatchSectioned() {
36
- const { getDb } = require('./db');
36
+ const { getDb } = require('../db');
37
37
  const db = getDb();
38
38
 
39
39
  // Return the complete dispatch state regardless of how old
@@ -59,7 +59,7 @@ function readDispatchSectioned() {
59
59
  // not just the recent window. Mutators that prune old completed
60
60
  // dispatches MUST see those rows or the prune is a no-op.
61
61
  function _readDispatchSectionedFull() {
62
- const { getDb } = require('./db');
62
+ const { getDb } = require('../db');
63
63
  let db;
64
64
  try { db = getDb(); }
65
65
  catch { return _emptySectioned(); }
@@ -169,11 +169,11 @@ function _applyDispatchDiff(db, diff) {
169
169
  // mutator mutated in place + returned undefined). Same
170
170
  // contract as the legacy mutateJsonFileLocked-based path.
171
171
  function applyDispatchMutation(mutator) {
172
- const { getDb, withTransaction } = require('./db');
172
+ const { getDb, withTransaction } = require('../db');
173
173
  let db;
174
174
  try { db = getDb(); }
175
175
  catch (e) {
176
- throw new Error(`engine/dispatch-store: SQLite unavailable (${e.message}); cannot mutate dispatch state`);
176
+ throw new Error(`engine/persistence/dispatch-store: SQLite unavailable (${e.message}); cannot mutate dispatch state`);
177
177
  }
178
178
 
179
179
  // withTransaction is nestable: shared.mutateWorkItems → mutator → cli
@@ -1,4 +1,4 @@
1
- // engine/inbox-store.js — SQL-backed inbox_entries table handlers.
1
+ // engine/persistence/inbox-store.js — SQL-backed inbox_entries table handlers.
2
2
  //
3
3
  // Provides read/write helpers for the operator and agent inbox message queue.
4
4
  // Inboxes are decoupled from the runtime work-item/dispatch lifecycle and
@@ -7,7 +7,7 @@
7
7
  // Schema: docs/design-inbox-entries-schema.md
8
8
 
9
9
  const crypto = require('node:crypto');
10
- const { getDb, withTransaction } = require('./db');
10
+ const { getDb, withTransaction } = require('../db');
11
11
 
12
12
  // Per CLAUDE.md's "State storage" mutator contract, every mutator wrapper
13
13
  // must call emitStateEvent() after a successful write so the dashboard's
@@ -1,4 +1,4 @@
1
- // engine/note-link-backfill.js
1
+ // engine/persistence/note-link-backfill.js
2
2
  //
3
3
  // One-time repair for work-item note links that broke BEFORE the live
4
4
  // rewrite landed (W-mq1j85cj00055a8f extended to _artifacts.notes / artifacts).
@@ -18,7 +18,7 @@
18
18
  // notes.md) are left untouched. Idempotent: a second pass finds nothing because
19
19
  // the repaired links no longer reference `notes/inbox/`.
20
20
  //
21
- // Wired as an idempotent engine-boot sweep (engine/cli.js, alongside the other
21
+ // Wired as an idempotent engine-boot sweep (engine/operations/cli.js, alongside the other
22
22
  // one-shot boot migrations). It runs through shared.mutateWorkItems — the same
23
23
  // write path the live rewriter uses — so the canonical SQL store and its JSON
24
24
  // mirror stay consistent, and it's a no-op once every link is healed.
@@ -53,7 +53,7 @@ function buildNoteResolver(dirs) {
53
53
  const kbCategories = Array.isArray(dirs.kbCategories) ? dirs.kbCategories : [];
54
54
  // Reuse the live rewriter's encoder so the backfill and the live path emit
55
55
  // byte-identical pill tokens (one source for the archive:/kb: grammar).
56
- const tokenFor = require('./shared')._artifactNoteFileToken;
56
+ const tokenFor = require('../core/shared')._artifactNoteFileToken;
57
57
 
58
58
  const archiveByBase = new Map();
59
59
  let archiveFiles = [];
@@ -150,7 +150,7 @@ function repointItemNoteLinks(item, resolve) {
150
150
 
151
151
  // Repair broken note links across every project scope plus central.
152
152
  function runNoteLinkBackfill(opts = {}) {
153
- const shared = require('./shared');
153
+ const shared = require('../core/shared');
154
154
  const minionsDir = opts.minionsDir || shared.MINIONS_DIR;
155
155
  const resolve = buildNoteResolver({
156
156
  inboxDir: path.join(minionsDir, 'notes', 'inbox'),