@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/watch-actions.js — Follow-up action registry for watches.
2
+ * engine/watches/actions.js — Follow-up action registry for watches.
3
3
  *
4
4
  * When a watch fires, the engine first writes the inbox alert (the existing
5
5
  * `notify` field) and then — if the watch carries a structured `action` —
@@ -11,7 +11,7 @@
11
11
  * Handlers MUST swallow their own errors and return `{ ok: false, summary }`
12
12
  * rather than throwing — this keeps a single bad action from poisoning the
13
13
  * watches tick (other watches should still fire). The caller in
14
- * engine/watches.js wraps the invocation in try/catch as a backstop.
14
+ * engine/watches/index.js wraps the invocation in try/catch as a backstop.
15
15
  *
16
16
  * Templating: action params support {{var}} substitution from `ctx`. Strings
17
17
  * and one level of nested string fields are templated recursively. Built-in
@@ -29,7 +29,7 @@ const https = require('https');
29
29
  const crypto = require('crypto');
30
30
  const { URL } = require('url');
31
31
 
32
- const shared = require('./shared');
32
+ const shared = require('../core/shared');
33
33
  const {
34
34
  WATCH_ACTION_TYPE, WI_STATUS, WORK_TYPE, DONE_STATUSES, PLAN_STATUS,
35
35
  log, ts, uid, mutateWorkItems,
@@ -53,7 +53,7 @@ const WATCH_DEDUP_ACTIVE_STATUSES = new Set([
53
53
  // must NOT include Date.now()/uid()/triggerCount so the same logical work
54
54
  // produces the same key across ticks and engine restarts.
55
55
  //
56
- // Stored on the dispatched WI under `meta._watchDedupKey`; engine/watch-actions.js
56
+ // Stored on the dispatched WI under `meta._watchDedupKey`; this module
57
57
  // scans existing pending+active WIs and skips the push if a match is found.
58
58
  function computeWatchDedupKey(watch, dispatchedItemShape) {
59
59
  const stable = {
@@ -77,7 +77,7 @@ function computeWatchDedupKey(watch, dispatchedItemShape) {
77
77
  // `false` on parse/eval errors (with a `[safe-expr]` warn). That contract
78
78
  // is what keeps the "invalid expression skips + warns" acceptance criterion
79
79
  // a one-liner here — we trust safe-expr to do the logging.
80
- const safeExpr = require('./safe-expr');
80
+ const safeExpr = require('../core/safe-expr');
81
81
 
82
82
  // ── Action registry ──────────────────────────────────────────────────────────
83
83
 
@@ -362,7 +362,7 @@ async function runWatchActionChain(watch, ctx) {
362
362
  //
363
363
  // P-w11a4c9e — Phase 6.1: NOTIFY is now the single registered code path for
364
364
  // inbox notifications. The legacy `watch.notify === 'inbox'` branch in
365
- // engine/watches.js no longer writes to the inbox itself — it instead
365
+ // engine/watches/index.js no longer writes to the inbox itself — it instead
366
366
  // synthesizes an implicit `{type:'notify'}` step prepended to the action
367
367
  // chain. To keep the legacy slug shape `watch-<id>-<triggerCount>` (so
368
368
  // existing dedup / readers see no change), this handler defaults to that
@@ -720,7 +720,7 @@ registerActionType(WATCH_ACTION_TYPE.TRIGGER_PIPELINE, {
720
720
  const pipelineId = String(p.pipelineId || ctx.pipelineId || '').trim();
721
721
  if (!pipelineId) return { ok: false, summary: 'trigger-pipeline: pipelineId is required' };
722
722
  let pipelineMod;
723
- try { pipelineMod = require('./pipeline'); }
723
+ try { pipelineMod = require('../orchestration/pipeline'); }
724
724
  catch (err) { return { ok: false, summary: `trigger-pipeline: pipeline module unavailable (${err.message})` }; }
725
725
  let pipeline;
726
726
  try { pipeline = pipelineMod.getPipeline(pipelineId); }
@@ -749,7 +749,7 @@ registerActionType(WATCH_ACTION_TYPE.ARCHIVE_PLAN, {
749
749
  const p = ctx.params || {};
750
750
  const planFile = String(p.planFile || ctx.planFile || '').trim();
751
751
  if (!planFile) return { ok: false, summary: 'archive-plan: planFile is required' };
752
- const store = require('./prd-store');
752
+ const store = require('../planning/prd-store');
753
753
  if (!store.readPrd(planFile)) return { ok: false, summary: `archive-plan: ${planFile} not found` };
754
754
  let updated = false;
755
755
  store.mutatePrd(planFile, (data) => {
@@ -776,7 +776,7 @@ registerActionType(WATCH_ACTION_TYPE.RESUME_PLAN, {
776
776
  const p = ctx.params || {};
777
777
  const planFile = String(p.planFile || ctx.planFile || '').trim();
778
778
  if (!planFile) return { ok: false, summary: 'resume-plan: planFile is required' };
779
- const store = require('./prd-store');
779
+ const store = require('../planning/prd-store');
780
780
  if (!store.readPrd(planFile)) return { ok: false, summary: `resume-plan: ${planFile} not found` };
781
781
  let updated = false;
782
782
  store.mutatePrd(planFile, (data) => {
@@ -844,7 +844,7 @@ function _ccTriageReadFileTail(filePath, maxBytes) {
844
844
  function _ccTriageLookupLatestDispatch(workItemId) {
845
845
  if (!workItemId) return null;
846
846
  try {
847
- const queries = require('./queries');
847
+ const queries = require('../core/queries');
848
848
  const dispatch = queries.getDispatch() || {};
849
849
  let latest = null;
850
850
  for (const listName of ['completed', 'active', 'pending']) {
@@ -862,7 +862,7 @@ function _ccTriageLookupLatestDispatch(workItemId) {
862
862
  }
863
863
 
864
864
  function _ccTriageBuildArtifactSection(watch, ctx, kinds) {
865
- const fence = require('./untrusted-fence');
865
+ const fence = require('../core/untrusted-fence');
866
866
  const attachments = [];
867
867
  const requested = Array.isArray(kinds) && kinds.length > 0
868
868
  ? kinds.map(k => String(k || '').trim().toLowerCase()).filter(k => CC_TRIAGE_ALLOWED_ARTIFACTS.has(k))
@@ -879,7 +879,7 @@ function _ccTriageBuildArtifactSection(watch, ctx, kinds) {
879
879
  dispatchEntry = _ccTriageLookupLatestDispatch(workItemId);
880
880
  if (dispatchEntry?.id && requested.includes('completion-report')) {
881
881
  try {
882
- const queries = require('./queries');
882
+ const queries = require('../core/queries');
883
883
  completionReport = queries.getDispatchCompletionReport(dispatchEntry.id);
884
884
  } catch { completionReport = null; }
885
885
  }
@@ -936,7 +936,7 @@ registerActionType(WATCH_ACTION_TYPE.CC_TRIAGE, {
936
936
  { key: 'timeoutMs', required: false, description: `Engine-side HTTP timeout in ms. Default ${CC_TRIAGE_DEFAULT_TIMEOUT_MS} (10 min), capped at ${CC_TRIAGE_MAX_TIMEOUT_MS} (1 h).` },
937
937
  ],
938
938
  handler: async (watch, ctx) => {
939
- const fence = require('./untrusted-fence');
939
+ const fence = require('../core/untrusted-fence');
940
940
  const p = ctx.params || {};
941
941
  const promptRaw = String(p.prompt || '').trim();
942
942
  if (!promptRaw) return { ok: false, summary: 'cc-triage: prompt is required' };
@@ -1,6 +1,6 @@
1
1
  /**
2
- * engine/watches.js -- Persistent watch jobs that survive engine restarts.
3
- * Zero dependencies -- uses only Node.js built-ins + engine/shared.js.
2
+ * engine/watches/index.js -- Persistent watch jobs that survive engine restarts.
3
+ * Zero dependencies -- uses only Node.js built-ins + engine/core/shared.js.
4
4
  *
5
5
  * Watches monitor targets (PRs, work items, meetings, plans, schedules,
6
6
  * pipelines, dispatches, agents, ...) for conditions (merged, build-fail,
@@ -29,18 +29,19 @@
29
29
  * Built-in types: pr, work-item, meeting, plan, schedule, pipeline, dispatch,
30
30
  * agent. Additional types can be added at runtime via registerTargetType().
31
31
  *
32
- * Canonical plugin example: `watches.d/http.js` (W-mp7i22mu00191b07) — a
32
+ * Canonical plugin example: `watch-plugins/http.js` (W-mp7i22mu00191b07) — a
33
33
  * generic HTTP poller that demonstrates the full plugin contract including
34
34
  * `contextVars` (exposing `{{httpStatus}}`, `{{extracted}}`, `{{prevExtracted}}`,
35
35
  * etc. to action templates) and a background-fetch cache (because the engine
36
36
  * tick path is synchronous; plugins that need async I/O maintain their own
37
37
  * cache and return cached entities from the synchronous fetchEntity).
38
38
  *
39
- * ── Plugin folder (`watches.d/`) — W-mp7hg58e000b5212 ────────────────────────
39
+ * ── Plugin folder (`watch-plugins/`) ─────────────────────────────────────────
40
40
  *
41
- * At engine boot, every `*.js` file in `<MINIONS_DIR>/watches.d/` is loaded
41
+ * At engine boot, every `*.js` file in `<MINIONS_DIR>/watch-plugins/` is loaded
42
42
  * and its exports are passed to registerTargetType(). This lets users add
43
- * new watch target types without editing engine source.
43
+ * new watch target types without editing engine source. Plugins cannot replace
44
+ * built-in or previously registered target types.
44
45
  *
45
46
  * Each plugin file exports either:
46
47
  *
@@ -51,7 +52,7 @@
51
52
  *
52
53
  * or an array of those objects for multi-target plugins.
53
54
  *
54
- * Resolution: `path.join(MINIONS_DIR, 'watches.d')` so it works in both dev
55
+ * Resolution: `path.join(MINIONS_DIR, 'watch-plugins')` so it works in both dev
55
56
  * (D:/squad) and installed (~/.minions) layouts. A missing folder is a silent
56
57
  * no-op (not an error). Each file is required inside its own try/catch — one
57
58
  * bad plugin must not break boot or block other plugins. Successful loads
@@ -66,11 +67,11 @@
66
67
 
67
68
  const fs = require('fs');
68
69
  const path = require('path');
69
- const shared = require('./shared');
70
+ const shared = require('../core/shared');
70
71
  const { mutateWatches, ts, uid, log, writeToInbox,
71
72
  WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS } = shared;
72
- const watchActions = require('./watch-actions');
73
- const apiValidation = require('./api-validation');
73
+ const watchActions = require('./actions');
74
+ const apiValidation = require('../api/api-validation');
74
75
 
75
76
  // Default check interval: 5 minutes (300000ms). Engine tick runs every 10s
76
77
  // (W-mpxpckey000laa4f), so watches are checked every N ticks where
@@ -483,7 +484,7 @@ function validateWatchId(value) {
483
484
  * @returns {Array<object>}
484
485
  */
485
486
  function getWatches() {
486
- return require('./watches-store').readWatches();
487
+ return require('./store').readWatches();
487
488
  }
488
489
 
489
490
  /**
@@ -507,7 +508,7 @@ function createWatch(input, config) {
507
508
  notify: validated.notify,
508
509
  stopAfter: validated.stopAfter, // 0 = run forever; N = expire after N triggers
509
510
  onNotMet: validated.onNotMet, // null | 'notify' — action per poll when condition not met
510
- action: validated.action, // optional follow-up action — see engine/watch-actions.js
511
+ action: validated.action, // optional follow-up action — see engine/watches/actions.js
511
512
  // P-w10b6e2d — Phase 5: optional requires[] — when present every entry
512
513
  // must also evaluate truthy for the watch to fire. See evaluateWatch.
513
514
  requires: validated.requires,
@@ -1118,7 +1119,7 @@ registerTargetType(WATCH_TARGET_TYPE.PR, {
1118
1119
  return { triggered: changed, message: changed ? `PR ${target} head commit changed: ${prev} → ${cur}` : '' };
1119
1120
  }
1120
1121
  case WATCH_CONDITION.MERGEABLE_FLIPPED: {
1121
- // engine/github.js:902-923 documents that mergeable=null means
1122
+ // engine/providers/github.js:902-923 documents that mergeable=null means
1122
1123
  // "GitHub still computing" — NOT a positive signal. Only fire on
1123
1124
  // transitions between defined boolean values (true↔false). Any
1124
1125
  // transition involving null or undefined is ignored.
@@ -1130,7 +1131,7 @@ registerTargetType(WATCH_TARGET_TYPE.PR, {
1130
1131
  }
1131
1132
  case WATCH_CONDITION.READY_FOR_MERGE: {
1132
1133
  // Compound predicate — fires when ALL gating signals line up.
1133
- // Registered as an absolute condition (engine/shared.js
1134
+ // Registered as an absolute condition (engine/core/shared.js
1134
1135
  // WATCH_ABSOLUTE_CONDITIONS) so it auto-expires on first trigger
1135
1136
  // when stopAfter=0. Treat isDraft !== true (catches null/undefined
1136
1137
  // legacy PRs that don't expose the field).
@@ -1431,7 +1432,7 @@ registerTargetType(WATCH_TARGET_TYPE.SCHEDULE, {
1431
1432
  validateTarget: _validateStringTarget,
1432
1433
  fetchEntity: _findSchedule,
1433
1434
  // P-w2c8d1e7 — Phase 1.2: surface next_run as null. Cron parsing in
1434
- // engine/scheduler.js exposes parseCronExpr/.matches() but not a
1435
+ // engine/orchestration/scheduler.js exposes parseCronExpr/.matches() but not a
1435
1436
  // next-fire calculator, so the snapshot field is reserved (always null
1436
1437
  // today) for forward-compat with future predicates that gain a helper.
1437
1438
  captureState: (s) => ({
@@ -1490,7 +1491,7 @@ function _completedStageCount(run) {
1490
1491
  }
1491
1492
  // P-w2c8d1e7 — Phase 1.2: report the id of the first non-terminal stage
1492
1493
  // in run.stages iteration order. run.stages is built from pipeline.stages
1493
- // in declaration order (engine/pipeline.js startRun ~line 84) so the first
1494
+ // in declaration order (engine/orchestration/pipeline.js startRun ~line 84) so the first
1494
1495
  // non-terminal entry is the currently-executing or next-pending stage.
1495
1496
  const _PIPELINE_TERMINAL = new Set([shared.PIPELINE_STATUS.COMPLETED, shared.PIPELINE_STATUS.FAILED, shared.PIPELINE_STATUS.STOPPED]);
1496
1497
  function _currentPipelineStageId(run) {
@@ -1688,22 +1689,24 @@ registerTargetType(WATCH_TARGET_TYPE.AGENT, {
1688
1689
  },
1689
1690
  });
1690
1691
 
1691
- // ── Plugin folder discovery (W-mp7hg58e000b5212) ─────────────────────────────
1692
- // Auto-load `<MINIONS_DIR>/watches.d/*.js` at module load time, AFTER the 8
1693
- // built-in registerTargetType() calls so plugins can override built-ins by
1694
- // re-using their key (last-write-wins). Per the README block at the top of
1695
- // this file: missing folder is silent, per-file try/catch isolates failures,
1696
- // and INFO/WARN logs trace what loaded.
1697
- function _loadPluginTargetTypes() {
1698
- const dir = path.join(shared.MINIONS_DIR, 'watches.d');
1692
+ // ── Plugin folder discovery ───────────────────────────────────────────────────
1693
+ // Auto-load `<MINIONS_DIR>/watch-plugins/*.js` at module load time, AFTER the 8
1694
+ // built-in registerTargetType() calls. Duplicate target names are rejected so
1695
+ // a plugin cannot silently replace core behavior. Missing folder is silent;
1696
+ // per-file try/catch isolates failures and INFO/WARN logs trace what loaded.
1697
+ function _loadPluginDirectory(directoryName, { legacy = false } = {}) {
1698
+ const dir = path.join(shared.MINIONS_DIR, directoryName);
1699
1699
  let entries;
1700
1700
  try {
1701
1701
  entries = fs.readdirSync(dir);
1702
1702
  } catch (err) {
1703
1703
  if (err && err.code === 'ENOENT') return; // silent no-op
1704
- log('warn', `watches.d/ readdir failed: ${err.message}`);
1704
+ log('warn', `${directoryName}/ readdir failed: ${err.message}`);
1705
1705
  return;
1706
1706
  }
1707
+ if (legacy && entries.some(name => name.endsWith('.js'))) {
1708
+ log('warn', 'Legacy watches.d/ plugins are still loaded for compatibility; move them to watch-plugins/');
1709
+ }
1707
1710
  for (const fname of entries.sort()) {
1708
1711
  if (!fname.endsWith('.js')) continue;
1709
1712
  const fp = path.join(dir, fname);
@@ -1714,21 +1717,29 @@ function _loadPluginTargetTypes() {
1714
1717
  const list = Array.isArray(mod) ? mod : [mod];
1715
1718
  for (const entry of list) {
1716
1719
  if (!entry || !entry.name || !entry.spec) {
1717
- log('warn', `watches.d/${fname}: export missing { name, spec } — skipping entry`);
1720
+ log('warn', `${directoryName}/${fname}: export missing { name, spec } — skipping entry`);
1721
+ continue;
1722
+ }
1723
+ if (Object.prototype.hasOwnProperty.call(TARGET_TYPES, entry.name)) {
1724
+ log('warn', `${directoryName}/${fname}: target type '${entry.name}' already registered — skipping duplicate`);
1718
1725
  continue;
1719
1726
  }
1720
1727
  try {
1721
1728
  registerTargetType(entry.name, entry.spec);
1722
- log('info', `Loaded watch target type '${entry.name}' from watches.d/${fname}`);
1729
+ log('info', `Loaded watch target type '${entry.name}' from ${directoryName}/${fname}`);
1723
1730
  } catch (regErr) {
1724
- log('warn', `watches.d/${fname}: registerTargetType('${entry.name}') failed: ${regErr.message}`);
1731
+ log('warn', `${directoryName}/${fname}: registerTargetType('${entry.name}') failed: ${regErr.message}`);
1725
1732
  }
1726
1733
  }
1727
1734
  } catch (err) {
1728
- log('warn', `watches.d/${fname}: load failed: ${err.message}`);
1735
+ log('warn', `${directoryName}/${fname}: load failed: ${err.message}`);
1729
1736
  }
1730
1737
  }
1731
1738
  }
1739
+ function _loadPluginTargetTypes() {
1740
+ _loadPluginDirectory('watch-plugins');
1741
+ _loadPluginDirectory('watches.d', { legacy: true });
1742
+ }
1732
1743
  _loadPluginTargetTypes();
1733
1744
 
1734
1745
  module.exports = {
@@ -1748,8 +1759,8 @@ module.exports = {
1748
1759
  registerTargetType,
1749
1760
  getTargetType,
1750
1761
  listTargetTypes,
1751
- // Re-exported from engine/watch-actions for callers that prefer a single
1752
- // import surface (dashboard.js handlers, tests). See engine/watch-actions.js
1762
+ // Re-exported from engine/watches/actions for callers that prefer a single
1763
+ // import surface (dashboard.js handlers, tests).
1753
1764
  // for the canonical implementations and the action registry.
1754
1765
  registerActionType: watchActions.registerActionType,
1755
1766
  getActionType: watchActions.getActionType,
@@ -1761,5 +1772,5 @@ module.exports = {
1761
1772
  _captureState, // exported for testing
1762
1773
  _runActionTask, // exported for testing — invoked by checkWatches per fired action
1763
1774
  _TARGET_TYPES: TARGET_TYPES, // exported for testing — direct registry access
1764
- _loadPluginTargetTypes, // exported for testing — re-scans watches.d/
1775
+ _loadPluginTargetTypes, // exported for testing — re-scans watch-plugins/
1765
1776
  };
@@ -1,4 +1,4 @@
1
- // engine/watches-store.js — SQL-backed watches.
1
+ // engine/watches/store.js — SQL-backed watches.
2
2
  //
3
3
  // A single SQL table (no per-scope split), with a diff-then-apply mutator.
4
4
  //
@@ -29,7 +29,7 @@ function _readWatchesFromSqlOnly(db) {
29
29
  }
30
30
 
31
31
  function readWatches() {
32
- const { getDb } = require('./db');
32
+ const { getDb } = require('../db');
33
33
  return _readWatchesFromSqlOnly(getDb());
34
34
  }
35
35
 
@@ -100,11 +100,11 @@ function _applyWatchesDiff(db, diff) {
100
100
  }
101
101
 
102
102
  function applyWatchesMutation(mutator) {
103
- const { getDb, withTransaction } = require('./db');
103
+ const { getDb, withTransaction } = require('../db');
104
104
  let db;
105
105
  try { db = getDb(); }
106
106
  catch (e) {
107
- throw new Error(`engine/watches-store: SQLite unavailable (${e.message}); cannot mutate watches`);
107
+ throw new Error(`engine/watches/store: SQLite unavailable (${e.message}); cannot mutate watches`);
108
108
  }
109
109
 
110
110
  return withTransaction(db, () => {
@@ -21,7 +21,7 @@
21
21
 
22
22
  const fs = require('fs');
23
23
  const path = require('path');
24
- const shared = require('./shared');
24
+ const shared = require('../core/shared');
25
25
 
26
26
  const PATCH_APPLY_TIMEOUT_MS = 120000;
27
27
 
@@ -9,7 +9,7 @@
9
9
  // worktree was borrowed from the pool, returned to the pool, or is
10
10
  // anchored by live keep_processes PIDs / managed_spawn cwds.
11
11
  //
12
- // 2. On engine boot (engine/cli.js): scan each project's worktree root
12
+ // 2. On engine boot (engine/operations/cli.js): scan each project's worktree root
13
13
  // for orphan W-* dirs whose dispatch is not active
14
14
  // or pending) AND that are not pool entries. Such dirs are leftovers
15
15
  // from a prior crash that block retries with
@@ -24,21 +24,21 @@
24
24
  const fs = require('fs');
25
25
  const path = require('path');
26
26
 
27
- const shared = require('./shared');
28
- const worktreePool = require('./worktree-pool');
27
+ const shared = require('../core/shared');
28
+ const worktreePool = require('./pool');
29
29
 
30
30
  const DEFAULT_ORPHAN_RETENTION_MS = 2 * 60 * 60 * 1000;
31
31
  const DEFAULT_QUARANTINE_RETENTION_MS = 24 * 60 * 60 * 1000;
32
32
 
33
33
  let keepProcessSweep = null;
34
34
  function _keepProcessSweep() {
35
- if (!keepProcessSweep) keepProcessSweep = require('./keep-process-sweep');
35
+ if (!keepProcessSweep) keepProcessSweep = require('../processes/keep-process-sweep');
36
36
  return keepProcessSweep;
37
37
  }
38
38
 
39
39
  let managedSpawn = null;
40
40
  function _managedSpawn() {
41
- if (!managedSpawn) managedSpawn = require('./managed-spawn');
41
+ if (!managedSpawn) managedSpawn = require('../processes/managed-spawn');
42
42
  return managedSpawn;
43
43
  }
44
44
 
@@ -263,12 +263,13 @@ function _maybeEscalateStuck(resolvedPath, errorMsg, opts = {}) {
263
263
  const minAgeMs = agentTimeoutMs * 2;
264
264
  const killFn = typeof opts.killImmediate === 'function'
265
265
  ? opts.killImmediate
266
- : shared.killImmediate;
266
+ : (proc, holder) => shared.terminateProcess('engine.worktree-gc.orphan-holder', proc, {
267
+ expectedIdentity: { pid: holder.pid, startedAt: holder.startedAt },
268
+ });
267
269
  for (const h of holders) {
268
270
  if (!_isSafeReapHolder(h, basename, minAgeMs)) continue;
269
271
  try {
270
- killFn({ pid: h.pid });
271
- reapedPids.push(h.pid);
272
+ if (killFn({ pid: h.pid }, h) !== false) reapedPids.push(h.pid);
272
273
  } catch { /* fail-open — keep trying other holders */ }
273
274
  }
274
275
  if (reapedPids.length > 0) {
@@ -869,6 +870,15 @@ function reconcileStaleWorktreeDirectories(opts) {
869
870
  Number.isFinite(Number(opts.quarantineRetentionMs))
870
871
  ? Number(opts.quarantineRetentionMs)
871
872
  : DEFAULT_QUARANTINE_RETENTION_MS);
873
+ // W-ms5tb6ha016fda10 — bounded (but longest) window for PROTECTED quarantine
874
+ // dirs, whose dirty WIP has no durable recovery ref. 0 keeps them forever.
875
+ // W-ms87rbe601a66d13 — the config→default resolution goes through
876
+ // `shared.resolveQuarantineRetentionMs` so this sweep and the recovery-REF
877
+ // sweep read the operator override through ONE seam and cannot diverge.
878
+ const protectedQuarantineRetentionMs = Math.max(0,
879
+ Number.isFinite(Number(opts.protectedQuarantineRetentionMs))
880
+ ? Number(opts.protectedQuarantineRetentionMs)
881
+ : shared.resolveQuarantineRetentionMs(opts.config));
872
882
 
873
883
  const result = {
874
884
  scanned: 0,
@@ -1120,9 +1130,29 @@ function reconcileStaleWorktreeDirectories(opts) {
1120
1130
  }
1121
1131
  let protectedQuarantine = true;
1122
1132
  try { protectedQuarantine = !!_hasProtectedQuarantineMarker(candidate); } catch {}
1133
+ // W-ms5tb6ha016fda10 — a protected quarantine keeps the LONGEST window,
1134
+ // but no longer an infinite one. `protectedQuarantineRetentionMs === 0`
1135
+ // is the explicit operator opt-out that restores forever-retention.
1136
+ // Non-quarantine dirs are never affected: the marker probe fails open to
1137
+ // `true`, so an unreadable dir is still preserved.
1123
1138
  if (protectedQuarantine) {
1124
- keep('protected', candidate);
1125
- continue;
1139
+ if (protectedQuarantineRetentionMs === 0) {
1140
+ keep('protected', candidate);
1141
+ continue;
1142
+ }
1143
+ let protectedStat;
1144
+ try { protectedStat = _fs.statSync(candidate); }
1145
+ catch {
1146
+ keep('protected', candidate);
1147
+ continue;
1148
+ }
1149
+ const protectedCreatedAt = _isQuarantineDirName(name)
1150
+ ? _quarantineCreatedAt(name, protectedStat.mtimeMs)
1151
+ : protectedStat.mtimeMs;
1152
+ if ((now - protectedCreatedAt) < protectedQuarantineRetentionMs) {
1153
+ keep('protected', candidate);
1154
+ continue;
1155
+ }
1126
1156
  }
1127
1157
 
1128
1158
  let owned = false;
@@ -1222,7 +1252,7 @@ function reconcileStaleWorktreeDirectories(opts) {
1222
1252
  * 2. Normalized abs path is currently in any `worktreePool` entry (idle
1223
1253
  * or borrowed) — see `extraPoolPaths` for the test override.
1224
1254
  * 3. Worktree contains (or equals) the cwd of any live managed_spawn
1225
- * spec — matches `engine/cleanup.js`'s tick-time protection so a
1255
+ * spec — matches `engine/orchestration/cleanup.js`'s tick-time protection so a
1226
1256
  * completed dispatch with long-running services survives engine
1227
1257
  * restart. (PR #2627 review — Issue 1)
1228
1258
  *
@@ -1394,16 +1424,21 @@ function pruneOrphanWorktrees(opts) {
1394
1424
  continue;
1395
1425
  }
1396
1426
  // Quarantine directories are recovery artifacts, not ordinary crash
1397
- // residue. Keep them for a deterministic recovery window, and keep
1398
- // protected quarantines indefinitely when no durable backup ref exists.
1427
+ // residue. Keep them for a deterministic recovery window.
1428
+ //
1429
+ // W-ms5tb6ha016fda10 — PROTECTED quarantines (dirty WIP that could NOT be
1430
+ // snapshotted to a durable `refs/minions/quarantine-wip/*` ref) used to be
1431
+ // kept FOREVER. That is why a busy install accumulated 20 quarantine dirs
1432
+ // with no expiry. They still get the LONGEST window — `engine
1433
+ // .worktreeProtectedQuarantineRetentionMs`, whose default is DERIVED from
1434
+ // `shared.QUARANTINE_REF_RETENTION_MS` (30 days) so directories and
1435
+ // recovery refs expire together — but the window is now finite. Setting the
1436
+ // config value to 0 restores indefinite retention (for the recovery refs
1437
+ // too, since W-ms87rbe601a66d13 routes both sweeps through
1438
+ // `shared.resolveQuarantineRetentionMs`).
1399
1439
  if (_isQuarantineDirName(name)) {
1400
1440
  let protectedQuarantine = true;
1401
1441
  try { protectedQuarantine = shared.hasProtectedQuarantineMarker(wtPath); } catch {}
1402
- if (protectedQuarantine) {
1403
- projStats.kept++; result.kept++;
1404
- projStats.preserved++; result.preserved++;
1405
- continue;
1406
- }
1407
1442
  let quarantineStat;
1408
1443
  try { quarantineStat = _fs.statSync(wtPath); }
1409
1444
  catch {
@@ -1412,10 +1447,23 @@ function pruneOrphanWorktrees(opts) {
1412
1447
  continue;
1413
1448
  }
1414
1449
  const now = Number.isFinite(Number(opts.now)) ? Number(opts.now) : Date.now();
1415
- const retentionMs = Math.max(0,
1416
- Number.isFinite(Number(opts.quarantineRetentionMs))
1417
- ? Number(opts.quarantineRetentionMs)
1418
- : DEFAULT_QUARANTINE_RETENTION_MS);
1450
+ const protectedRetentionMs = Math.max(0,
1451
+ Number.isFinite(Number(opts.protectedQuarantineRetentionMs))
1452
+ ? Number(opts.protectedQuarantineRetentionMs)
1453
+ : shared.resolveQuarantineRetentionMs(opts.config));
1454
+ const retentionMs = protectedQuarantine
1455
+ ? protectedRetentionMs
1456
+ : Math.max(0,
1457
+ Number.isFinite(Number(opts.quarantineRetentionMs))
1458
+ ? Number(opts.quarantineRetentionMs)
1459
+ : DEFAULT_QUARANTINE_RETENTION_MS);
1460
+ // 0 == keep forever (explicit operator opt-out), and only for the
1461
+ // protected class — an unprotected quarantine always has a durable ref.
1462
+ if (protectedQuarantine && retentionMs === 0) {
1463
+ projStats.kept++; result.kept++;
1464
+ projStats.preserved++; result.preserved++;
1465
+ continue;
1466
+ }
1419
1467
  const createdAt = _quarantineCreatedAt(name, quarantineStat.mtimeMs);
1420
1468
  if ((now - createdAt) < retentionMs) {
1421
1469
  projStats.kept++; result.kept++;