@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
package/bin/minions.js CHANGED
@@ -9,12 +9,15 @@
9
9
  * minions remove <project-dir> Unlink a project
10
10
  * minions list List linked projects
11
11
  * minions update [--no-wait] Update to latest version (--no-wait backgrounds the post-update restart)
12
+ * Channel-gated: refuses a public install over an internal runtime.
13
+ * --allow-downgrade overrides the semver downgrade guard.
12
14
  * minions version Show installed and package versions
13
15
  * minions doctor [--harness] Check prerequisites and runtime health (--harness: print runtime harness inventory)
14
16
  * minions state <subcmd> Check, backup, or export SQLite state
15
17
  * minions restart [--open] Stop + start engine + dashboard (--open forces a new browser tab)
16
18
  * minions start [--open] Start engine + dashboard if not already running (no-op when up)
17
- * minions stop Stop the engine
19
+ * minions stop Request engine shutdown and return (engine only)
20
+ * minions stop --all [--wait] Stop the whole stack (supervisor + dashboard + engine)
18
21
  * minions status Show engine status
19
22
  * minions pause / resume Pause/resume dispatching
20
23
  * minions queue Show dispatch queue
@@ -62,9 +65,28 @@ const net = require('net');
62
65
  const { spawn, spawnSync, execSync } = require('child_process');
63
66
 
64
67
  const PKG_ROOT = path.resolve(__dirname, '..');
65
- const shared = require(path.join(PKG_ROOT, 'engine', 'shared'));
68
+ const shared = require(path.join(PKG_ROOT, 'engine', 'core', 'shared'));
66
69
  const { openUrlInBrowser } = shared;
67
- const { waitForRestartHealth, formatRestartHealthError } = require(path.join(PKG_ROOT, 'engine', 'restart-health'));
70
+ const {
71
+ confirmRestartHealthBeforeCleanup,
72
+ waitForRestartHealth,
73
+ formatRestartHealthError,
74
+ } = require(path.join(PKG_ROOT, 'engine', 'recovery', 'restart-health'));
75
+ const { removeSupersededModuleFiles } = require('./install-layout');
76
+ const {
77
+ terminateProcess,
78
+ listNodePidsMatchingScripts,
79
+ } = require(path.join(PKG_ROOT, 'engine', 'processes', 'process-utils'));
80
+ // P-b28e4f60 — the ordered whole-stack teardown lives in exactly one place.
81
+ // Every CLI path that stops the runtime (stop --all, restart, nuke, uninstall,
82
+ // the init upgrade) calls stopWholeRuntimeStack() below, which wraps this.
83
+ const {
84
+ stopRuntimeStack,
85
+ stopStackExitCode,
86
+ holderIsLiveDaemon,
87
+ STOP_STACK_EXIT,
88
+ DEFAULT_STOP_TIMEOUT_MS: STOP_STACK_DEFAULT_TIMEOUT_MS,
89
+ } = require(path.join(PKG_ROOT, 'engine', 'recovery', 'stop-stack'));
68
90
  // Dev-mode dashboard port. `--dev` mode (see argv parser below) retargets
69
91
  // MINIONS_HOME at PKG_ROOT and routes the dashboard onto this port so a dev
70
92
  // checkout coexists with a global npm install on the normal port (7331).
@@ -126,16 +148,6 @@ function getListeningPids(port) {
126
148
  } catch { return []; }
127
149
  }
128
150
 
129
- /** Kill process(es) listening on a given port. Works cross-platform. */
130
- function killByPort(port) {
131
- const pids = getListeningPids(port);
132
- if (process.platform === 'win32') {
133
- for (const pid of pids) try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore', timeout: 5000, windowsHide: true }); } catch {}
134
- } else {
135
- for (const pid of pids) try { process.kill(Number(pid), 'SIGKILL'); } catch {}
136
- }
137
- }
138
-
139
151
  const isPortListening = (port) => getListeningPids(port).length > 0;
140
152
 
141
153
  /** Authoritative "is something accepting TCP connections on this port?" probe.
@@ -157,15 +169,13 @@ function tcpPortAccepts(port, timeoutMs = 1500) {
157
169
  }
158
170
 
159
171
  /**
160
- * Wait until no process is listening on `port`, retrying a kill on each tick
161
- * for any stragglers that re-appeared (e.g. orphan child the original kill
162
- * missed, or a process that respawned itself). Returns true when the port is
163
- * free, false on timeout.
172
+ * Wait until no process is listening on `port`. Daemons are terminated only
173
+ * after exact runtime-root ownership checks before this helper is entered.
164
174
  *
165
175
  * Rationale: `taskkill /F` and `SIGKILL` return immediately while the OS does
166
176
  * the actual termination asynchronously, and the port doesn't transition to
167
177
  * available until the kernel finalises the socket close. Without this wait,
168
- * the new dashboard race-spawned just after `killByPort` can hit EADDRINUSE
178
+ * the new dashboard race-spawned just after termination can hit EADDRINUSE
169
179
  * and exit silently — the bug pattern that surfaced as "Dashboard failed
170
180
  * health check, port=7331 listening=no" while the OLD dashboard PID was
171
181
  * still bound to the port.
@@ -175,9 +185,6 @@ function waitForPortRelease(port, timeoutMs = 10000, pollMs = 200) {
175
185
  while (Date.now() - start < timeoutMs) {
176
186
  const pids = getListeningPids(port);
177
187
  if (pids.length === 0) return { ok: true, waitedMs: Date.now() - start };
178
- // Retry the kill — covers the case where the original killByPort missed a
179
- // sibling listener or a new orphan appeared mid-wait.
180
- killByPort(port);
181
188
  const sleepUntil = Date.now() + pollMs;
182
189
  while (Date.now() < sleepUntil) { /* spin */ }
183
190
  }
@@ -264,42 +271,50 @@ function readEnginePid(minionsHome) {
264
271
  * they can be re-attached on next start (CLAUDE.md timeouts/liveness section).
265
272
  * Tree-kill (`taskkill /T`, `pgrep -P` walk) would orphan in-flight work.
266
273
  */
267
- function killPidOnly(pid) {
268
- if (!pid || pid === process.pid) return;
269
- try {
270
- if (process.platform === 'win32') {
271
- execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore', timeout: 5000, windowsHide: true });
272
- } else {
273
- try { process.kill(pid, 'SIGKILL'); } catch {}
274
+ function killPidOnly(pid, scriptPaths = daemonScriptPaths()) {
275
+ if (!pid || pid === process.pid) return false;
276
+ return terminateProcess('bin.daemon-stop-process', { pid }, { scriptPaths });
277
+ }
278
+
279
+ function daemonScriptPaths({ includeAgents = false } = {}) {
280
+ const scripts = [
281
+ path.join(MINIONS_HOME, 'engine.js'),
282
+ path.join(MINIONS_HOME, 'dashboard.js'),
283
+ path.join(MINIONS_HOME, 'engine', 'recovery', 'supervisor.js'),
284
+ ];
285
+ if (includeAgents) scripts.push(path.join(MINIONS_HOME, 'engine', 'agents', 'spawn-agent.js'));
286
+ return scripts;
287
+ }
288
+
289
+ /** Kill only exact daemon scripts owned by this runtime root. */
290
+ function killMinionsProcesses(scriptPaths, { includeDescendants = false } = {}) {
291
+ const pids = listNodePidsMatchingScripts(scriptPaths);
292
+ for (const pid of pids) {
293
+ if (pid === process.pid) continue;
294
+ if (includeDescendants) {
295
+ shared.terminateProcess('bin.daemon-stop-tree', { pid }, { scriptPaths });
274
296
  }
275
- } catch {}
297
+ else killPidOnly(pid, scriptPaths);
298
+ }
299
+ return pids.filter(pid => pid !== process.pid);
276
300
  }
277
301
 
278
- /** Kill minions processes by command-line pattern matching (wmic on Windows, pkill on Unix). */
279
- function killMinionsProcesses(patterns) {
280
- try {
281
- if (process.platform === 'win32') {
282
- // Use PowerShell Get-CimInstance (works on Win11 where wmic is removed)
283
- let out;
284
- try {
285
- out = execSync('powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name=\'node.exe\'\\" | Select-Object ProcessId,CommandLine | ConvertTo-Csv -NoTypeInformation"', { encoding: 'utf8', timeout: 10000, windowsHide: true });
286
- } catch {
287
- // Fallback to wmic for older Windows
288
- try { out = execSync('wmic process where "name=\'node.exe\'" get processid,commandline /format:csv', { encoding: 'utf8', timeout: 10000, windowsHide: true }); } catch { return; }
289
- }
290
- for (const line of out.split('\n')) {
291
- if (patterns.some(p => line.includes(p))) {
292
- const pidMatch = line.match(/(\d{2,})/);
293
- const pid = pidMatch ? pidMatch[1] : null;
294
- if (pid && pid !== String(process.pid)) killPidOnly(pid);
295
- }
296
- }
297
- } else {
298
- for (const p of patterns) {
299
- try { execSync(`pkill -f "${p}" 2>/dev/null`, { timeout: 5000 }); } catch {}
300
- }
302
+ function retryKillVerifiedPids(pids, scriptPaths, { attempts = 3, pollMs = 200 } = {}) {
303
+ const remaining = new Set(
304
+ (pids || []).map(Number).filter(pid => Number.isInteger(pid) && pid > 0 && pid !== process.pid),
305
+ );
306
+ const killed = [];
307
+ for (let attempt = 0; attempt < attempts && remaining.size > 0; attempt++) {
308
+ for (const pid of [...remaining]) {
309
+ if (!killPidOnly(pid, scriptPaths)) continue;
310
+ remaining.delete(pid);
311
+ killed.push(pid);
301
312
  }
302
- } catch {}
313
+ if (remaining.size > 0 && attempt + 1 < attempts) {
314
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pollMs);
315
+ }
316
+ }
317
+ return killed;
303
318
  }
304
319
 
305
320
  /** Open append-mode FDs under engine/ for detached-process stdio. If the file
@@ -363,7 +378,7 @@ function spawnDashboard(requestedPort) {
363
378
  return proc;
364
379
  }
365
380
 
366
- /** Spawn the external supervisor (engine/supervisor.js). The supervisor
381
+ /** Spawn the external supervisor (engine/recovery/supervisor.js). The supervisor
367
382
  * watches BOTH the engine and the dashboard and respawns whichever dies,
368
383
  * unless `engine/stop-intent.json` is present (set by `minions stop`).
369
384
  * Single-instance guard inside supervisor.js means double-spawn during
@@ -372,18 +387,13 @@ function spawnDashboard(requestedPort) {
372
387
  function spawnSupervisor() {
373
388
  const out = _openStdioLog('supervisor-stdio.log');
374
389
  const err = _openStdioLog('supervisor-stdio.log');
375
- const proc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine', 'supervisor.js')], {
390
+ const proc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine', 'recovery', 'supervisor.js')], {
376
391
  cwd: MINIONS_HOME, stdio: ['ignore', out, err], detached: true, windowsHide: true,
377
392
  });
378
393
  proc.unref();
379
394
  return proc;
380
395
  }
381
396
 
382
- // Path getter — evaluated lazily inside the helper so MINIONS_HOME, which
383
- // is initialized later in module load, is bound at call time rather than
384
- // at function-definition time.
385
- const _supervisorPidPath = () => path.join(MINIONS_HOME, 'engine', 'supervisor.pid');
386
-
387
397
  /**
388
398
  * Resolve the cold-start restart-health budget (ms) from config, falling back
389
399
  * to ENGINE_DEFAULTS.restartHealthTimeoutMs. Mirrors the Settings clamp range
@@ -449,7 +459,7 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
449
459
  void (async () => {
450
460
  const healthTimeoutMs = _resolveRestartHealthTimeoutMs();
451
461
  const verificationStartedAt = Date.now();
452
- const startupResult = await waitForRestartHealth({
462
+ let startupResult = await waitForRestartHealth({
453
463
  minionsHome: MINIONS_HOME,
454
464
  expectedEnginePid: engineProc.pid,
455
465
  dashboardPid: dashProc.pid,
@@ -460,13 +470,14 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
460
470
  });
461
471
  if (!startupResult.ok) {
462
472
  console.error(formatRestartHealthError(startupResult));
463
- _cleanupFailedStackSpawn({
473
+ const cleanup = await _cleanupFailedStackSpawn({
464
474
  enginePid: engineProc.pid,
465
475
  dashboardPid: dashProc.pid,
466
476
  supervisorPid: null,
467
477
  dashboardPort: requested.port,
468
478
  });
469
- process.exit(1);
479
+ if (!cleanup.skipped) process.exit(1);
480
+ startupResult = cleanup.health;
470
481
  }
471
482
 
472
483
  // The CLI owns startup recovery until the exact dashboard PID has bound the
@@ -476,10 +487,10 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
476
487
  const supProc = spawnSupervisor();
477
488
  console.log(` Supervisor started (PID: ${supProc.pid})`);
478
489
  console.log(' Verifying restart health...');
479
- const supervisorInternals = require(path.join(PKG_ROOT, 'engine', 'supervisor'))._internals;
490
+ const supervisorInternals = require(path.join(PKG_ROOT, 'engine', 'recovery', 'supervisor'))._internals;
480
491
  const startupElapsedMs = Date.now() - verificationStartedAt;
481
492
  const topologyTimeoutMs = Math.max(MIN_TOPOLOGY_VERIFY_TIMEOUT_MS, healthTimeoutMs - startupElapsedMs);
482
- const result = await waitForRestartHealth({
493
+ let result = await waitForRestartHealth({
483
494
  minionsHome: MINIONS_HOME,
484
495
  expectedEnginePid: engineProc.pid,
485
496
  dashboardPid: dashProc.pid,
@@ -495,18 +506,19 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
495
506
  topologyScripts: {
496
507
  engine: path.join(MINIONS_HOME, 'engine.js'),
497
508
  dashboard: path.join(MINIONS_HOME, 'dashboard.js'),
498
- supervisor: path.join(MINIONS_HOME, 'engine', 'supervisor.js'),
509
+ supervisor: path.join(MINIONS_HOME, 'engine', 'recovery', 'supervisor.js'),
499
510
  },
500
511
  });
501
512
  if (!result.ok) {
502
513
  console.error(formatRestartHealthError(result));
503
- _cleanupFailedStackSpawn({
514
+ const cleanup = await _cleanupFailedStackSpawn({
504
515
  enginePid: engineProc.pid,
505
516
  dashboardPid: dashProc.pid,
506
517
  supervisorPid: supProc.pid,
507
518
  dashboardPort: requested.port,
508
519
  });
509
- process.exit(1);
520
+ if (!cleanup.skipped) process.exit(1);
521
+ result = cleanup.health;
510
522
  }
511
523
  console.log(` Restart verified: engine PID ${result.engine.pid}; dashboard healthy.`);
512
524
 
@@ -528,15 +540,39 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
528
540
  });
529
541
  }
530
542
 
531
- function _cleanupFailedStackSpawn({ enginePid, dashboardPid, supervisorPid, dashboardPort }) {
532
- writeStopIntent('restart verification failed');
533
- for (const pid of [supervisorPid, dashboardPid, enginePid]) killPidOnly(pid);
534
- for (const pid of [supervisorPid, dashboardPid, enginePid]) waitForPidDeath(pid, 5000);
535
- if (dashboardPort) {
536
- killByPort(dashboardPort);
537
- waitForPortRelease(dashboardPort, 5000);
543
+ async function _cleanupFailedStackSpawn({ enginePid, dashboardPid, supervisorPid, dashboardPort }) {
544
+ const finalHealth = await confirmRestartHealthBeforeCleanup({
545
+ minionsHome: MINIONS_HOME,
546
+ expectedEnginePid: enginePid,
547
+ dashboardPid,
548
+ dashboardPort,
549
+ supervisorPid,
550
+ requireBeaconOwner: true,
551
+ });
552
+ if (finalHealth.ok) {
553
+ console.error(
554
+ ' Cleanup skipped: the expected engine, dashboard, and supervisor are currently healthy and owned.'
555
+ );
556
+ return { skipped: true, health: finalHealth };
538
557
  }
539
- shared.clearDashboardPortFile(MINIONS_HOME);
558
+
559
+ const cleanupSource = `restart verification failed (${process.pid})`;
560
+ writeStopIntent(cleanupSource);
561
+ try {
562
+ for (const [pid, scriptPath] of [
563
+ [supervisorPid, path.join(MINIONS_HOME, 'engine', 'recovery', 'supervisor.js')],
564
+ [dashboardPid, path.join(MINIONS_HOME, 'dashboard.js')],
565
+ [enginePid, path.join(MINIONS_HOME, 'engine.js')],
566
+ ]) {
567
+ killPidOnly(pid, [scriptPath]);
568
+ }
569
+ for (const pid of [supervisorPid, dashboardPid, enginePid]) waitForPidDeath(pid, 5000);
570
+ if (dashboardPort) waitForPortRelease(dashboardPort, 5000);
571
+ shared.clearDashboardPortFile(MINIONS_HOME);
572
+ } finally {
573
+ _clearStopIntentIfSource(cleanupSource);
574
+ }
575
+ return { skipped: false, health: finalHealth };
540
576
  }
541
577
 
542
578
  /** Poll engine/dashboard-port.json until it appears (dashboard wrote it on
@@ -578,7 +614,7 @@ async function _waitForDashboardPortFile(home, timeoutMs = 12000, pollMs = 150,
578
614
 
579
615
  /** Clear the stop-intent flag so the supervisor resumes guarding the engine
580
616
  * and dashboard. Called at the top of every start/restart path.
581
- * Delegates to engine/shared.js when available so engine.js, dashboard.js,
617
+ * Delegates to engine/core/shared.js when available so engine.js, dashboard.js,
582
618
  * and the CLI all share one source of truth. Falls back to a direct unlink
583
619
  * during early init when shared.js may not have loaded yet. */
584
620
  function clearStopIntent() {
@@ -603,19 +639,62 @@ function writeStopIntent(source) {
603
639
  }
604
640
  }
605
641
 
606
- /** Kill the supervisor process if its PID file points at a live process.
607
- * Used by `minions stop` and `minions restart` after writing stop-intent. */
608
- function killSupervisor() {
642
+ function _clearStopIntentIfSource(source) {
609
643
  try {
610
- const pid = Number(fs.readFileSync(_supervisorPidPath(), 'utf8').trim());
611
- if (!pid || pid === process.pid) return;
612
- if (process.platform === 'win32') {
613
- try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore', timeout: 5000, windowsHide: true }); } catch {}
614
- } else {
615
- try { process.kill(pid, 'SIGTERM'); } catch {}
616
- }
617
- try { fs.unlinkSync(_supervisorPidPath()); } catch {}
618
- } catch { /* no PID file supervisor not running */ }
644
+ const intent = JSON.parse(fs.readFileSync(shared.STOP_INTENT_PATH, 'utf8'));
645
+ if (intent && intent.source === source) shared.clearStopIntent();
646
+ } catch { /* missing, replaced, or unreadable intent is preserved */ }
647
+ }
648
+
649
+ /**
650
+ * The CLI's ONE whole-stack teardown call site (P-b28e4f60).
651
+ *
652
+ * The ordered sequence stop-intent supervisor dashboard engine drain →
653
+ * verified reap → WAL-index check → late-respawn sweep → verification — is
654
+ * owned by engine/recovery/stop-stack.js. `minions stop --all`, `restart`,
655
+ * `nuke`, `uninstall` and the init upgrade path all route through here, so
656
+ * exactly one implementation of that sequence exists in the tree.
657
+ *
658
+ * `waitForRelease` is opt-in and defaults OFF: only a caller that must PROVE
659
+ * the database handles are gone (the pre-migration gate behind
660
+ * `minions stop --all --wait`) blocks on `engine/state.db-shm`. Callers that
661
+ * immediately replace the stack, or delete the runtime root outright, run the
662
+ * identical teardown and get the same evidence reported without the wait.
663
+ *
664
+ * Never throws for an expected outcome — the caller decides what a refusal or
665
+ * an expired budget means for ITS command.
666
+ */
667
+ function stopWholeRuntimeStack({ source, timeoutMs, waitForRelease = false }) {
668
+ const verdict = stopRuntimeStack({
669
+ minionsHome: MINIONS_HOME,
670
+ source,
671
+ timeoutMs,
672
+ waitForRelease,
673
+ });
674
+ for (const svc of verdict.stopped) {
675
+ console.log(` Stopped ${svc.name} (PID ${svc.pid}, ${svc.via})`);
676
+ }
677
+ for (const holder of verdict.holders) {
678
+ console.error(` Still running: ${holder.name} (PID ${holder.pid}) — ${holder.reason}`);
679
+ }
680
+ console.log(` ${verdict.reason} (${verdict.waitedMs}ms)`);
681
+ return verdict;
682
+ }
683
+
684
+ /** `--timeout <ms>` / `--timeout=<ms>` for `minions stop --all`. Defaults to
685
+ * the shared module's budget, which matches the internal installer's
686
+ * quiescence window so a stop can never be shorter than the wait after it. */
687
+ function _resolveStopAllTimeoutMs(argv) {
688
+ const flagIdx = argv.indexOf('--timeout');
689
+ const inline = argv.find(a => typeof a === 'string' && a.startsWith('--timeout='));
690
+ if (flagIdx < 0 && !inline) return STOP_STACK_DEFAULT_TIMEOUT_MS;
691
+ const raw = flagIdx >= 0 ? argv[flagIdx + 1] : inline.slice('--timeout='.length);
692
+ const n = Number(raw);
693
+ if (!Number.isInteger(n) || n <= 0) {
694
+ console.error(`\n --timeout requires a positive integer in milliseconds, got: ${raw || '(missing)'}\n`);
695
+ process.exit(2);
696
+ }
697
+ return n;
619
698
  }
620
699
 
621
700
  const LEGACY_DEFAULT_SQUAD_HOME = path.join(os.homedir(), '.squad');
@@ -823,14 +902,19 @@ if (devMode) process.env.MINIONS_DASHBOARD_PORT = String(devPort);
823
902
  // dynamic chain re-introduces the 7331 bug W-mq5nwl9l was opened to fix.
824
903
  const DASHBOARD_PORT = resolveDashboardPort(rest).port;
825
904
  // Propagate the port to every child (engine, dashboard, delegated subcommands).
826
- // engine/cli.js status reads MINIONS_PORT; dashboard.js reads PORT. We export
905
+ // engine/operations/cli.js status reads MINIONS_PORT; dashboard.js reads PORT. We export
827
906
  // MINIONS_PORT as the *requested* port for legacy consumers, but loopback callers
828
- // (watch-actions / pipeline / managed-spawn) must read the *actual* bound port
907
+ // (watch action handlers / pipeline / managed-spawn) must read the *actual* bound port
829
908
  // from the dashboard-port.json beacon via shared.readDashboardPortFile() — the
830
909
  // dashboard may bind to a fallback port (7332+) on EADDRINUSE, and MINIONS_PORT
831
910
  // stays frozen at the originally-requested value in that case.
832
911
  process.env.MINIONS_PORT = String(DASHBOARD_PORT);
833
912
  const POST_UPDATE_INIT_TIMEOUT_MS = 120000;
913
+ // Teardown budget for the paths that immediately REPLACE the stack (upgrade)
914
+ // or DELETE the runtime root (nuke, uninstall). Mirrors the 10s timeout the
915
+ // inline `engine.js stop` call used to carry, so a hung engine cannot stall
916
+ // them — whatever survives the budget is force-reaped or deleted next.
917
+ const FORCED_STOP_BUDGET_MS = 10000;
834
918
  // W-mqpx0tpi — the post-update `minions restart` runs as a child of `minions
835
919
  // update` under this wall-clock cap. It must outlive the restart-health budget
836
920
  // (now up to ~60s for a cold post-`npm install` dashboard boot) plus spawn +
@@ -859,7 +943,7 @@ function getPkgVersionFresh() {
859
943
 
860
944
  // Which npm distribution is this CLI? Derived from the installed package
861
945
  // manifest (public `@yemi33/minions` vs an internal rename produced by
862
- // tools/prepare-package.js) so `minions version` compares against the right
946
+ // scripts/prepare-package.js) so `minions version` compares against the right
863
947
  // registry channel and `minions update` reinstalls ITSELF instead of pulling
864
948
  // in a second package that fights over the shared `minions` bin (npm EEXIST).
865
949
  function getPkgName() {
@@ -875,6 +959,18 @@ function getInstalledVersion() {
875
959
 
876
960
  function saveInstalledVersion(version) {
877
961
  fs.writeFileSync(path.join(MINIONS_HOME, '.minions-version'), version);
962
+ // Record WHICH npm distribution produced this runtime root (W-ms6b7prf006tef7f-b).
963
+ // `.minions-version` alone cannot tell an internal `@opg-microsoft/minions`
964
+ // root apart from a public `@yemi33/minions` one, so a channel gate has
965
+ // nothing to compare against. Read back via shared.readRuntimeChannel();
966
+ // best-effort, because failing to stamp the channel must never fail an init.
967
+ try {
968
+ fs.writeFileSync(path.join(MINIONS_HOME, shared.RUNTIME_CHANNEL_MARKER), `${JSON.stringify({
969
+ name: getPkgName(),
970
+ version,
971
+ installedAt: new Date().toISOString(),
972
+ }, null, 2)}\n`);
973
+ } catch {}
878
974
  // Persist source commit so dashboard can detect repo-based installs
879
975
  try {
880
976
  const commit = execSync('git rev-parse --short HEAD', { cwd: PKG_ROOT, encoding: 'utf8', timeout: 5000, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
@@ -942,6 +1038,9 @@ function init() {
942
1038
 
943
1039
  // Copy with smart merge logic
944
1040
  copyDir(PKG_ROOT, MINIONS_HOME, excludeTop, alwaysUpdate, neverOverwrite, isUpgrade, actions);
1041
+ for (const removed of removeSupersededModuleFiles(MINIONS_HOME)) {
1042
+ actions.updated.push(`removed ${removed}`);
1043
+ }
945
1044
  shared.syncBundledPersonalSkills(path.join(MINIONS_HOME, 'skills'), { homeDir: os.homedir() });
946
1045
 
947
1046
  // Create config from template if it doesn't exist
@@ -989,7 +1088,7 @@ function init() {
989
1088
  // current config only references one. Missing optional runtimes surface as
990
1089
  // warns, not failures.
991
1090
  try {
992
- const { runPreflight, printPreflight } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
1091
+ const { runPreflight, printPreflight } = require(path.join(MINIONS_HOME, 'engine', 'agents', 'preflight'));
993
1092
  let preflightConfig = null;
994
1093
  try { preflightConfig = JSON.parse(fs.readFileSync(path.join(MINIONS_HOME, 'config.json'), 'utf8')); }
995
1094
  catch { /* config may not exist on first init — fine, preflight handles null */ }
@@ -997,22 +1096,43 @@ function init() {
997
1096
  printPreflight(results, { label: 'Preflight checks' });
998
1097
  } catch {}
999
1098
 
1000
- // Update flow passes --skip-start so it can perform a single visible restart afterwards.
1001
- if (isUpgrade && skipStart) return;
1099
+ // Constellation companion report (P-e93c85a1). Purely informational and
1100
+ // strictly non-fatal: init/update must succeed identically whether or not a
1101
+ // Constellation companion exists. The SYNC probe is used deliberately —
1102
+ // init() is synchronous, and the marker-only path is bounded by a single file
1103
+ // read, so a hung or absent companion can never stall an install or upgrade.
1104
+ // Minions never downloads, executes, or installs Constellation; when the
1105
+ // companion is missing or incompatible this only PRINTS the documented
1106
+ // install command for the operator to run. The whole block is wrapped so a
1107
+ // probe defect can never break `minions init` / `minions update`.
1108
+ try {
1109
+ const companion = require(path.join(MINIONS_HOME, 'engine', 'api', 'companion'));
1110
+ const companionLines = companion.formatCompanionLines(
1111
+ companion.probeCompanionSync({ config: _readConfigJsonSafe() })
1112
+ );
1113
+ if (companionLines.length > 0) {
1114
+ console.log('');
1115
+ for (const line of companionLines) console.log(` ${line}`);
1116
+ console.log('');
1117
+ }
1118
+ } catch { /* companion reporting is optional — never block init/update */ }
1119
+
1120
+ // The caller owns process lifecycle whenever it requests copy/sync-only mode.
1121
+ // This must also hold for a fresh runtime root; otherwise isolated installs
1122
+ // unexpectedly launch a second engine/dashboard/supervisor stack.
1123
+ if (skipStart) return;
1002
1124
 
1003
1125
  // Auto-start on fresh install; direct force-upgrade restarts automatically.
1004
1126
  const upgradeRequested = resolveDashboardPort([]);
1005
1127
  const dashWasUp = isPortListening(upgradeRequested.port);
1006
1128
  const restartStartMs = Date.now();
1007
1129
  if (isUpgrade) {
1008
- // Pre-write stop-intent so the supervisor doesn't race-respawn the
1009
- // engine/dashboard we're about to kill for upgrade.
1010
- writeStopIntent('minions upgrade');
1011
- killSupervisor();
1012
- try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
1013
- // Free the dashboard port too — without this the new dashboard EADDRINUSE-dies
1014
- // silently and the user keeps running stale code from the old dashboard process.
1015
- killByPort(upgradeRequested.port);
1130
+ // Ordered teardown (stop-intent supervisor dashboard → engine drain →
1131
+ // verified reap late-respawn sweep) is owned by
1132
+ // engine/recovery/stop-stack.js. Best-effort: an upgrade must not be
1133
+ // blocked by a refusal, and the spawn below replaces whatever survives.
1134
+ // Budget mirrors the 10s the inline `engine.js stop` call used to get.
1135
+ stopWholeRuntimeStack({ source: 'minions upgrade', timeoutMs: FORCED_STOP_BUDGET_MS });
1016
1136
  // Wipe the runtime port file so the new dashboard's freshly-written
1017
1137
  // beacon is the source of truth (avoids polling-back a stale entry).
1018
1138
  shared.clearDashboardPortFile(MINIONS_HOME);
@@ -1278,10 +1398,17 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
1278
1398
  Setup:
1279
1399
  minions init Bootstrap ~/.minions/ (first time)
1280
1400
  minions update [--no-wait] Update to latest version (--no-wait backgrounds the restart)
1401
+ Refuses a public install over an internal runtime.
1402
+ An internal install updates from its own feed (Azure CLI auth).
1403
+ --allow-downgrade Permit a target version older than the installed one
1404
+ --registry <url> Override the internal feed (internal channel only)
1281
1405
  minions version Show installed vs package version
1282
1406
  minions doctor [--harness] Check prerequisites and runtime health (--harness: print runtime harness inventory)
1283
1407
  minions state check Run SQLite integrity and invariant checks
1284
1408
  minions state backup <file> Checkpoint and create a consistent SQLite backup
1409
+ minions state summary [--json] Row-count census + fingerprint of the SQL state
1410
+ minions state verify-backup <file>
1411
+ Verify a backup file's integrity and contents
1285
1412
  minions state export <file> Write a human-readable diagnostic JSON export
1286
1413
  minions state import-legacy-json <dir> --confirm
1287
1414
  Offline recovery import from a legacy home
@@ -1295,7 +1422,13 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
1295
1422
  Engine:
1296
1423
  minions restart [--open] Stop + start engine + dashboard (--open forces a new browser tab)
1297
1424
  minions start [--open] Start engine + dashboard if not already running (idempotent; --open forces a new tab)
1298
- minions stop Stop the engine
1425
+ minions stop Stop the engine (requests shutdown and returns immediately)
1426
+ minions stop --all [--wait] Stop the WHOLE stack: supervisor + dashboard + engine
1427
+ --wait Block until the engine/state.db handles are
1428
+ released (quiescence gate before a migration)
1429
+ --timeout <ms> Budget for the teardown (default 60000)
1430
+ Exit codes: 0 stopped · 3 not quiesced within the budget
1431
+ · 4 refused (a live PID could not be verified)
1299
1432
  minions status Show agents, projects, queue
1300
1433
  minions pause / resume Pause/resume dispatching
1301
1434
  minions dispatch Force a dispatch cycle
@@ -1351,6 +1484,13 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1351
1484
  init();
1352
1485
  } else if (cmd === 'update') {
1353
1486
  const noWait = rest.includes('--no-wait') || rest.includes('--detach');
1487
+ const allowDowngrade = rest.includes('--allow-downgrade');
1488
+ const registryIdx = rest.indexOf('--registry');
1489
+ const registryOverride = registryIdx !== -1 ? rest[registryIdx + 1] : null;
1490
+ if (registryIdx !== -1 && (!registryOverride || registryOverride.startsWith('--'))) {
1491
+ console.error('\n ERROR: --registry requires a feed URL.\n');
1492
+ process.exit(1);
1493
+ }
1354
1494
  console.log('\n Updating Minions...\n');
1355
1495
  // Dev/symlink installs: PKG_ROOT === MINIONS_HOME — npm update is a no-op (symlink already
1356
1496
  // points to the repo), and `minions init --force` would fail (cwd/.minions is inside PKG_ROOT).
@@ -1361,6 +1501,26 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1361
1501
  console.log(` Version synced to ${getPkgVersion()} (dev/symlink install — pull from git to update code)`);
1362
1502
  } else {
1363
1503
  const preVersion = getPkgVersionFresh();
1504
+ // CHANNEL GATE (W-ms6b7prf006tef7f-d). The runtime root — not PKG_ROOT — is
1505
+ // the channel authority. PKG_ROOT only says which package an install WOULD
1506
+ // pull; it resolves PUBLIC both from an opg repo checkout (whose own
1507
+ // package.json is named `@yemi33/minions`) and from a stale public global
1508
+ // shim standing in front of an internal runtime. Gating on the runtime's
1509
+ // `.minions-package` marker is what stops a stale public build from landing
1510
+ // on top of a newer internal one. Runs BEFORE any npm call so a refusal
1511
+ // never touches the registry or the install.
1512
+ const channel = shared.resolveUpdateChannel({ pkgRoot: PKG_ROOT, runtimeRoot: MINIONS_HOME });
1513
+ if (!channel.allowed) {
1514
+ console.error(`\n REFUSED: this would install the public package over an internal runtime.`);
1515
+ console.error(` Runtime channel: ${channel.runtimeName}${channel.runtimeVersion ? `@${channel.runtimeVersion}` : ''} (${MINIONS_HOME})`);
1516
+ console.error(` Would install: ${channel.pkgName} (resolved from ${PKG_ROOT})`);
1517
+ console.error(` A public install is never a valid upgrade for an internal runtime.`);
1518
+ console.error(`\n Use the internal installer instead:`);
1519
+ for (const line of channel.remediation) console.error(` ${line}`);
1520
+ console.error('');
1521
+ process.exit(1);
1522
+ }
1523
+ if (channel.warning) console.log(` ${channel.warning}`);
1364
1524
  // Resolve the ACTIVE distribution first: an internal build must update
1365
1525
  // itself, not the public package. Installing `@yemi33/minions` on top of an
1366
1526
  // `@opg-microsoft/minions` install adds a second package claiming the same
@@ -1368,6 +1528,52 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1368
1528
  // Reinstalling the same package name is an in-place upgrade, so no bin
1369
1529
  // collision is possible.
1370
1530
  const pkgName = getPkgName();
1531
+
1532
+ // INTERNAL FEED ROUTING (W-ms9bhf9k001j29cb). `npm view` / `npm install -g`
1533
+ // with no registry of their own resolve a SCOPED package through whatever
1534
+ // `@scope:registry` the caller's npm config carries — which routed an
1535
+ // `@opg-microsoft/minions` update to a public proxy and answered E404 for a
1536
+ // package that exists in the ISS ProjectFeed-ISS feed. A package served by a
1537
+ // private feed is therefore updated through that feed, using the SAME
1538
+ // auth/config seams as the bootstrap installer (short-lived `az` token →
1539
+ // 0600 temporary userconfig → npm, with the scoped registry pinned on the
1540
+ // command line so ambient config cannot redirect it). Anything not on a
1541
+ // private channel — the public package above all — keeps the plain npm path
1542
+ // below unchanged.
1543
+ const internalInstaller = require('./install-internal-minions');
1544
+ let feed = null;
1545
+ try {
1546
+ feed = internalInstaller.resolveInternalFeed(pkgName, { registry: registryOverride || undefined });
1547
+ } catch (e) {
1548
+ console.error(`\n ERROR: invalid --registry value: ${e.message}\n`);
1549
+ process.exit(1);
1550
+ }
1551
+ if (registryOverride && !feed) {
1552
+ console.error(`\n ERROR: --registry applies only to an internal channel install; ${pkgName} is served by your own npm registry.\n`);
1553
+ process.exit(1);
1554
+ }
1555
+
1556
+ let feedSession = null;
1557
+ // Every failure below exits the process, and process.exit() skips `finally`,
1558
+ // so the staged credential is removed explicitly here. The session also
1559
+ // registers its own process-exit hook as a backstop.
1560
+ const exitUpdate = (code) => {
1561
+ if (feedSession) { feedSession.close(); feedSession = null; }
1562
+ process.exit(code);
1563
+ };
1564
+ // Feed failures arrive pre-formatted as operator-actionable lines (which
1565
+ // feed, which auth flow, what a 404 there actually means); anything else is
1566
+ // a plain npm/exec failure.
1567
+ const reportUpdateFailure = (e, fallback) => {
1568
+ if (e && Array.isArray(e.lines)) {
1569
+ console.error('');
1570
+ for (const line of e.lines) console.error(` ${line}`);
1571
+ console.error('');
1572
+ } else {
1573
+ console.error(fallback, e && e.message ? e.message : e);
1574
+ }
1575
+ };
1576
+
1371
1577
  // Resolve the target version explicitly BEFORE installing, and pin the
1372
1578
  // install to that exact version instead of a floating `@latest` tag. This
1373
1579
  // avoids a race where `npm view` (used here and by `minions version`) and
@@ -1379,21 +1585,51 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1379
1585
  // though `npm view pkg version` reports a newer one (W-mr9k4a5x00090184).
1380
1586
  let targetVersion;
1381
1587
  try {
1382
- targetVersion = execSync(shared.buildNpmViewVersionCommand(pkgName), { encoding: 'utf8', timeout: 15000, windowsHide: true }).trim();
1588
+ if (feed) {
1589
+ console.log(` Internal channel — resolving ${pkgName} from ${feed.registry}`);
1590
+ feedSession = internalInstaller.openInternalFeedSession(feed);
1591
+ targetVersion = feedSession.resolveVersion();
1592
+ } else {
1593
+ targetVersion = execSync(shared.buildNpmViewVersionCommand(pkgName), { encoding: 'utf8', timeout: 15000, windowsHide: true }).trim();
1594
+ }
1383
1595
  } catch (e) {
1384
- console.error(` Failed to resolve the latest version of ${pkgName} from the npm registry:`, e.message);
1385
- process.exit(1);
1596
+ reportUpdateFailure(e, ` Failed to resolve the latest version of ${pkgName} from the npm registry:`);
1597
+ exitUpdate(1);
1386
1598
  }
1387
1599
  if (!targetVersion) {
1388
- console.error(` Failed to resolve the latest version of ${pkgName} from the npm registry (empty response).`);
1389
- process.exit(1);
1600
+ console.error(` Failed to resolve the latest version of ${pkgName} from the ${feed ? 'internal feed' : 'npm registry'} (empty response).`);
1601
+ exitUpdate(1);
1602
+ }
1603
+ // The public path validates the version inside its install-command builder;
1604
+ // a feed-resolved version reaches npm as an argument the same way, so gate
1605
+ // it identically rather than trusting registry output.
1606
+ if (feed && !shared.isValidNpmVersion(targetVersion)) {
1607
+ console.error(` Refusing to install an invalid version resolved from the internal feed: ${JSON.stringify(targetVersion)}`);
1608
+ exitUpdate(1);
1390
1609
  }
1391
1610
 
1611
+ // DOWNGRADE GUARD (W-ms6b7prf006tef7f-d). The same stale-packument failure
1612
+ // described above can also resolve BACKWARDS, in which case the install
1613
+ // succeeds and the post-install check below passes (on-disk == target) while
1614
+ // the runtime silently moves to an older build. Compare numerically against
1615
+ // what is on disk and refuse by default.
1616
+ const downgrade = shared.evaluateUpdateDowngrade({ installedVersion: preVersion, targetVersion, allowDowngrade });
1617
+ if (!downgrade.allowed) {
1618
+ console.error(`\n REFUSED: ${pkgName}@${targetVersion} would downgrade the installed ${pkgName}@${preVersion}.`);
1619
+ for (const line of downgrade.message.split('\n')) console.error(` ${line}`);
1620
+ console.error('');
1621
+ exitUpdate(1);
1622
+ }
1623
+ if (downgrade.message) console.log(` ${downgrade.message}`);
1624
+
1392
1625
  try {
1393
- execSync(shared.buildNpmGlobalInstallCommand(pkgName, targetVersion), { stdio: 'inherit', timeout: 120000 });
1626
+ if (feedSession) feedSession.install(targetVersion);
1627
+ else execSync(shared.buildNpmGlobalInstallCommand(pkgName, targetVersion), { stdio: 'inherit', timeout: 120000 });
1394
1628
  } catch (e) {
1395
- console.error(' npm update failed:', e.message);
1396
- process.exit(1);
1629
+ reportUpdateFailure(e, ' npm update failed:');
1630
+ exitUpdate(1);
1631
+ } finally {
1632
+ if (feedSession) { feedSession.close(); feedSession = null; }
1397
1633
  }
1398
1634
  // Equivalent to `minions init --force --skip-start`, but avoids recursing through
1399
1635
  // the global shim while npm is still settling the updated install.
@@ -1470,6 +1706,36 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1470
1706
  }
1471
1707
  console.log('\n Starting Minions (engine + dashboard + supervisor)...');
1472
1708
  spawnFullStackAndVerify({ rest, forceOpen, dashWasUp: false, restartStartMs: Date.now() });
1709
+ } else if (cmd === 'stop' && rest.includes('--all')) {
1710
+ // Opt-in WHOLE-STACK stop. Bare `minions stop` deliberately keeps its
1711
+ // engine-only, non-blocking delegation (engineCmds → `engine.js stop`, which
1712
+ // requests shutdown and returns) because recovery paths, operator scripts
1713
+ // and the engine's own callers depend on that semantic. This branch is the
1714
+ // additive contract on top of it.
1715
+ //
1716
+ // minions stop --all full ordered teardown; does not block on
1717
+ // the engine/state.db handle release
1718
+ // minions stop --all --wait additionally waits for the WAL index to be
1719
+ // released, i.e. proves quiescence
1720
+ //
1721
+ // The exit code is the machine-readable result (STOP_STACK_EXIT): 0 stopped,
1722
+ // 3 not quiesced within the budget, 4 refused on an unverified holder. No
1723
+ // caller should ever have to scrape this output.
1724
+ ensureInstalled();
1725
+ const stopAllWait = rest.includes('--wait');
1726
+ const stopAllVerdict = stopWholeRuntimeStack({
1727
+ source: stopAllWait ? 'minions stop --all --wait' : 'minions stop --all',
1728
+ timeoutMs: _resolveStopAllTimeoutMs(rest),
1729
+ waitForRelease: stopAllWait,
1730
+ });
1731
+ process.exit(stopStackExitCode(stopAllVerdict));
1732
+ } else if (cmd === 'stop' && rest.includes('--wait')) {
1733
+ // `--wait` alone would silently wait for nothing: the engine-only delegation
1734
+ // returns as soon as shutdown is REQUESTED. Fail loudly instead.
1735
+ console.error('\n ERROR: `--wait` only applies to the whole-stack stop.');
1736
+ console.error(' Run `minions stop --all --wait` to wait for the runtime handles to be released,');
1737
+ console.error(' or `minions stop` to request engine shutdown and return immediately.\n');
1738
+ process.exit(1);
1473
1739
  } else if (cmd === 'restart') {
1474
1740
  // `--cli` / `--model` flags forward to `engine.js start` so the runtime
1475
1741
  // fleet flips before the daemon spawns (P-6b3f9c2e AC: works on restart).
@@ -1480,16 +1746,6 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1480
1746
  const restartResolved = resolveDashboardPort(rest);
1481
1747
  const dashWasUp = isPortListening(restartResolved.port);
1482
1748
  const restartStartMs = Date.now();
1483
- // Pre-write stop-intent so the supervisor doesn't race-respawn the
1484
- // engine/dashboard we're about to kill. Killed before the engine/dashboard
1485
- // so its 30s tick doesn't fire mid-restart against a half-dead engine.
1486
- writeStopIntent('minions restart');
1487
- killSupervisor();
1488
- // Layered kill — each step is best-effort, so the next still runs if one
1489
- // fails. Goal: the old engine is gone before we spawn a new one, even if
1490
- // PowerShell is unavailable, the engine is hung, or its cmdline doesn't match.
1491
- const oldEnginePid = readEnginePid(MINIONS_HOME);
1492
- try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
1493
1749
  // Give the engine's graceful handler time to drain non-reattachable pooled
1494
1750
  // leases. Detached cold agents are not part of that drain and remain alive
1495
1751
  // for normal PID-file reattachment by the replacement engine.
@@ -1499,34 +1755,40 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1499
1755
  return Number(cfg?.engine?.shutdownTimeout) || shared.ENGINE_DEFAULTS.shutdownTimeout;
1500
1756
  } catch { return shared.ENGINE_DEFAULTS.shutdownTimeout; }
1501
1757
  })();
1502
- const gracefulEngineStop = waitForPidDeath(oldEnginePid, shutdownTimeout + 5000);
1503
- if (!gracefulEngineStop.ok) {
1504
- // Fallback for a hung engine. Kill only the engine PID, never its detached
1505
- // cold-agent children; pooled leases have already exceeded their drain budget.
1506
- killPidOnly(oldEnginePid);
1758
+ // Ordered teardown stop-intent first so the supervisor stands down and
1759
+ // cannot race-respawn what we are about to stop, then supervisor, dashboard,
1760
+ // graceful engine drain, verified reap, and the late-respawn sweep that
1761
+ // closes the watchdog race window. All of it lives in
1762
+ // engine/recovery/stop-stack.js; restart owns only what follows.
1763
+ // The WAL-index release is deliberately NOT awaited: restart replaces the
1764
+ // stack, and the port gate below is its real readiness condition.
1765
+ const teardown = stopWholeRuntimeStack({
1766
+ source: 'minions restart',
1767
+ timeoutMs: shutdownTimeout + 5000,
1768
+ waitForRelease: false,
1769
+ });
1770
+ // A holder that is provably NOT one of our daemons (a stale PID file, a
1771
+ // recycled PID) must not wedge the command restart exists to heal — the port
1772
+ // gate below still refuses to spawn over a live dashboard. Anything else
1773
+ // means a daemon survived, and spawning a second stack over it is exactly
1774
+ // the multi-engine state we must never create.
1775
+ const blockingHolders = teardown.holders.filter(h => holderIsLiveDaemon(h) || h.name === 'engine');
1776
+ if (blockingHolders.length > 0) {
1777
+ console.error(`\n ERROR: Refusing restart — ${teardown.reason}`);
1778
+ console.error(' The new stack cannot safely take over control.json while those processes are alive.');
1779
+ process.exit(1);
1507
1780
  }
1508
- killByPort(restartResolved.port);
1509
- killMinionsProcesses(['engine.js', 'dashboard.js', 'supervisor.js']);
1510
- // Confirm the OS finished the asynchronous termination before we spawn new
1511
- // processes. Without this, `taskkill /F` returns immediately while the
1512
- // kernel is still releasing the dashboard's port; the new dashboard spawned
1513
- // ~10ms later hits EADDRINUSE and exits, producing the "Restart verification
1514
- // failed, port=7331 listening=no" symptom against an orphan PID that's
1515
- // STILL bound to the port a heartbeat later.
1516
- if (oldEnginePid) {
1517
- const engineDead = waitForPidDeath(oldEnginePid, 5000);
1518
- if (!engineDead.ok) {
1519
- console.error(`\n ERROR: Old engine (PID ${oldEnginePid}) did not exit within 5s after kill.`);
1520
- console.error(` The new engine cannot safely take over control.json. Aborting restart.`);
1521
- process.exit(1);
1781
+ let portFree = waitForPortRelease(restartResolved.port, 10000);
1782
+ if (!portFree.ok) {
1783
+ const retriedPids = retryKillVerifiedPids(portFree.stillBound, daemonScriptPaths());
1784
+ if (retriedPids.length > 0) {
1785
+ portFree = waitForPortRelease(restartResolved.port, 5000);
1522
1786
  }
1523
1787
  }
1524
- const portFree = waitForPortRelease(restartResolved.port, 10000);
1525
1788
  if (!portFree.ok) {
1526
- console.error(`\n ERROR: Port ${restartResolved.port} still in use after 10s killing failed.`);
1789
+ console.error(`\n ERROR: Port ${restartResolved.port} still in use after verified stop retries.`);
1527
1790
  console.error(` Bound by PID(s): ${portFree.stillBound.join(', ')}`);
1528
- console.error(` Manually free the port: taskkill /F /PID ${portFree.stillBound.join(' /PID ')}`);
1529
- console.error(` Then retry: minions restart`);
1791
+ console.error(' Refusing to terminate an unverified port owner. Stop that application explicitly, then retry.');
1530
1792
  process.exit(1);
1531
1793
  }
1532
1794
  // Wipe the runtime port file so a stale entry from the old dashboard
@@ -1539,11 +1801,17 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1539
1801
  // fresh engine/dashboard during the kill→spawn window — those become ORPHANS
1540
1802
  // the new stack never reaps, and orphan engines keep dispatching agents +
1541
1803
  // spawning copilot (the root of the recurring multi-engine / MCP-auth storms).
1542
- // stop-intent is STILL set here, so any respawn has already stood down or is
1543
- // about to; reap once more so the stack we spawn below is the ONLY one. Scoped
1544
- // by command-line to engine/dashboard/supervisor never touches agent/copilot
1545
- // children, preserving the re-attach invariant.
1546
- killMinionsProcesses(['engine.js', 'dashboard.js', 'supervisor.js']);
1804
+ // The sweep itself is step 8 of the shared teardown above (it runs with
1805
+ // stop-intent still set, so any respawn has already stood down, and a
1806
+ // survivor comes back as a holder and aborts this command). What remains
1807
+ // restart-specific is proving the port was not reclaimed in the window
1808
+ // between that sweep and the spawn below.
1809
+ const finalPortFree = waitForPortRelease(restartResolved.port, 5000);
1810
+ if (!finalPortFree.ok) {
1811
+ console.error(`\n ERROR: Port ${restartResolved.port} was reclaimed during the restart race window.`);
1812
+ console.error(` Bound by PID(s): ${finalPortFree.stillBound.join(', ')}`);
1813
+ process.exit(1);
1814
+ }
1547
1815
  // Clear stop-intent so the freshly-spawned supervisor resumes guarding.
1548
1816
  clearStopIntent();
1549
1817
  spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs });
@@ -1576,9 +1844,16 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1576
1844
  console.log('\n Minions Factory Reset\n');
1577
1845
 
1578
1846
  // 1. Kill all processes
1579
- try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME }); } catch {}
1580
- killByPort(resolveDashboardPort([]).port);
1581
- killMinionsProcesses(['engine.js', 'dashboard.js', 'spawn-agent.js']);
1847
+ // The ordered teardown (stop-intent supervisor dashboard engine drain
1848
+ // → verified reap) stands the supervisor down FIRST, so it cannot
1849
+ // race-respawn the engine while we delete the state database underneath it.
1850
+ stopWholeRuntimeStack({ source: 'minions nuke', timeoutMs: FORCED_STOP_BUDGET_MS });
1851
+ // Agents are deliberately spared by the shared teardown so restarts can
1852
+ // reattach them; a factory reset deletes their state, so they go too.
1853
+ killMinionsProcesses(daemonScriptPaths({ includeAgents: true }), { includeDescendants: true });
1854
+ // The nested `init` below re-spawns the stack, so this root must not be left
1855
+ // carrying a stop-intent that would stand the new supervisor down.
1856
+ clearStopIntent();
1582
1857
  shared.clearDashboardPortFile(MINIONS_HOME);
1583
1858
  console.log(' Killed all processes');
1584
1859
 
@@ -1664,12 +1939,12 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1664
1939
  // 1. Kill all processes
1665
1940
  // Stop-intent FIRST so the supervisor stands down before we kill it. Without
1666
1941
  // this the supervisor could race-respawn the engine/dashboard between our
1667
- // engine-stop and the supervisor kill.
1668
- writeStopIntent('minions uninstall');
1669
- killSupervisor();
1670
- try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000 }); } catch {}
1671
- killByPort(resolveDashboardPort([]).port);
1672
- killMinionsProcesses(['engine.js', 'dashboard.js', 'spawn-agent.js', 'supervisor.js']);
1942
+ // engine-stop and the supervisor kill. That ordering — and the graceful
1943
+ // engine drain that follows it — is owned by engine/recovery/stop-stack.js.
1944
+ stopWholeRuntimeStack({ source: 'minions uninstall', timeoutMs: FORCED_STOP_BUDGET_MS });
1945
+ // Agents are deliberately spared by the shared teardown so restarts can
1946
+ // reattach them; an uninstall deletes the runtime root, so they go too.
1947
+ killMinionsProcesses(daemonScriptPaths({ includeAgents: true }), { includeDescendants: true });
1673
1948
  console.log(' Killed all processes');
1674
1949
 
1675
1950
  // 2. Remove minions-authored skills from ~/.claude/skills/
@@ -1680,7 +1955,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1680
1955
  const skillFile = path.join(claudeSkills, dir, 'SKILL.md');
1681
1956
  try {
1682
1957
  const content = fs.readFileSync(skillFile, 'utf8');
1683
- if (content.includes('Auto-extracted skill') || content.includes('author:')) {
1958
+ if (/<!-- Auto-extracted by [^>]+ on \d{4}-\d{2}-\d{2} -->/.test(content)) {
1684
1959
  fs.rmSync(path.join(claudeSkills, dir), { recursive: true, force: true });
1685
1960
  }
1686
1961
  } catch {}
@@ -1704,13 +1979,27 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1704
1979
  console.log(` npm install -g ${uninstallPkgName} && minions init\n`);
1705
1980
  } else if (cmd === 'state') {
1706
1981
  ensureInstalled();
1707
- const operations = require(path.join(MINIONS_HOME, 'engine', 'state-operations'));
1982
+ const operations = require(path.join(MINIONS_HOME, 'engine', 'persistence', 'state-operations'));
1708
1983
  const sub = rest[0];
1709
1984
  try {
1710
1985
  let result;
1711
1986
  if (sub === 'check') result = operations.checkState();
1712
1987
  else if (sub === 'backup') result = operations.backupState(rest[1]);
1713
1988
  else if (sub === 'export') result = operations.exportState(rest[1]);
1989
+ else if (sub === 'summary') {
1990
+ result = operations.summarizeState();
1991
+ if (!rest.includes('--json')) {
1992
+ console.log(`\n Schema version: ${result.schemaVersion}`);
1993
+ console.log(` Fingerprint: ${result.fingerprint}`);
1994
+ console.log(' Row counts:');
1995
+ for (const [table, count] of Object.entries(result.tables)) {
1996
+ console.log(` ${String(table).padEnd(30)} ${count === null ? 'n/a' : count}`);
1997
+ }
1998
+ console.log('');
1999
+ process.exit(0);
2000
+ }
2001
+ }
2002
+ else if (sub === 'verify-backup') result = operations.verifyBackup(rest[1]);
1714
2003
  else if (sub === 'import-legacy-json') {
1715
2004
  if (!rest.includes('--confirm')) throw new Error('legacy import requires --confirm');
1716
2005
  const enginePid = readEnginePid(MINIONS_HOME);
@@ -1725,7 +2014,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1725
2014
  result = operations.importLegacyJson(rest[1]);
1726
2015
  }
1727
2016
  else {
1728
- console.error('\n Usage: minions state (check|backup <file>|export <file>|import-legacy-json <dir> --confirm)\n');
2017
+ console.error('\n Usage: minions state (check|backup <file>|summary [--json]|verify-backup <file>|export <file>|import-legacy-json <dir> --confirm)\n');
1729
2018
  process.exit(2);
1730
2019
  }
1731
2020
  console.log(JSON.stringify(result, null, 2));
@@ -1765,7 +2054,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1765
2054
  }
1766
2055
  }
1767
2056
  try {
1768
- const { backfillReviewLearnings } = require(path.join(MINIONS_HOME, 'engine', 'review-learning-backfill'));
2057
+ const { backfillReviewLearnings } = require(path.join(MINIONS_HOME, 'engine', 'memory', 'review-learning-backfill'));
1769
2058
  const result = backfillReviewLearnings({ dryRun, limit, project });
1770
2059
  console.log(JSON.stringify(result, null, 2));
1771
2060
  process.exit(0);
@@ -1775,7 +2064,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1775
2064
  }
1776
2065
  } else if (cmd === 'doctor') {
1777
2066
  ensureInstalled();
1778
- const { doctor, runHarnessDoctor } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
2067
+ const { doctor, runHarnessDoctor } = require(path.join(MINIONS_HOME, 'engine', 'agents', 'preflight'));
1779
2068
  if (rest.includes('--harness')) {
1780
2069
  // Print the read-only runtime harness inventory and exit. The
1781
2070
  // legacy `minions doctor` still runs the full preflight + runtime fleet
@@ -1791,11 +2080,15 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1791
2080
  // the in-process supervisor can't (Windows job-object teardown when the
1792
2081
  // parent terminal closes, Linux OOM-killer, cgroup limits, pkill, reboot).
1793
2082
  ensureInstalled();
1794
- const watchdog = require(path.join(MINIONS_HOME, 'engine', 'watchdog'));
2083
+ const watchdog = require(path.join(MINIONS_HOME, 'engine', 'recovery', 'watchdog'));
1795
2084
  const sub = rest[0];
1796
- const minionsBin = __filename;
2085
+ // Source checkouts should recover with the same code they run, not an older
2086
+ // globally installed package. npm-state installs have no local bin and keep
2087
+ // using this executable as the fallback.
2088
+ const localMinionsBin = path.join(MINIONS_HOME, 'bin', 'minions.js');
2089
+ const minionsBin = fs.existsSync(localMinionsBin) ? localMinionsBin : __filename;
1797
2090
  if (sub === 'tick') {
1798
- const shared = require(path.join(MINIONS_HOME, 'engine', 'shared'));
2091
+ const shared = require(path.join(MINIONS_HOME, 'engine', 'core', 'shared'));
1799
2092
  // Resolve the live dashboard port via the in-file resolver, which honors
1800
2093
  // the runtime beacon (written by dashboard.js after bind), env, and
1801
2094
  // config — so the watchdog probes whatever port the dashboard actually