@yemi33/minions 0.1.2448 → 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 +58 -4
  7. package/dashboard/js/detail-panel.js +36 -0
  8. package/dashboard/js/memory-panel.js +59 -12
  9. package/dashboard/js/qa.js +179 -20
  10. package/dashboard/js/refresh.js +148 -12
  11. package/dashboard/js/render-dispatch.js +3 -4
  12. package/dashboard/js/render-inbox.js +2 -2
  13. package/dashboard/js/render-other.js +3 -3
  14. package/dashboard/js/render-pipelines.js +14 -0
  15. package/dashboard/js/render-plans.js +57 -9
  16. package/dashboard/js/render-prd.js +132 -23
  17. package/dashboard/js/render-prs.js +195 -166
  18. package/dashboard/js/render-schedules.js +63 -3
  19. package/dashboard/js/render-utils.js +3 -3
  20. package/dashboard/js/render-watches.js +19 -3
  21. package/dashboard/js/render-work-items.js +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 +13 -2
  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,4 +1,4 @@
1
- // engine/prd-store.js
1
+ // engine/planning/prd-store.js
2
2
  //
3
3
  // SQL-authoritative PRD state.
4
4
 
@@ -192,11 +192,11 @@ function writePrd(filename, prd, { archived = false } = {}) {
192
192
  throw new TypeError('prd-store.writePrd requires a filename and object');
193
193
  }
194
194
  const normalizedPrd = _normalizePrdForStorage(prd);
195
- const { getDb, withTransaction } = require('./db');
195
+ const { getDb, withTransaction } = require('../db');
196
196
  const db = getDb();
197
197
  const archivedValue = archived ? 1 : 0;
198
198
  const prdId = withTransaction(db, () => _upsertPrd(db, filename, archivedValue, normalizedPrd, Date.now()));
199
- require('./db-events').emitStateEvent('prds', { filename, archived: archivedValue });
199
+ require('../persistence/db-events').emitStateEvent('prds', { filename, archived: archivedValue });
200
200
  return { ok: true, id: prdId, plan: normalizedPrd };
201
201
  }
202
202
 
@@ -207,7 +207,7 @@ function createPrdUnique(baseFilename, prd) {
207
207
  }
208
208
  const normalizedPrd = _normalizePrdForStorage(prd);
209
209
  const stem = baseFilename.slice(0, -'.json'.length);
210
- const { getDb, withTransaction } = require('./db');
210
+ const { getDb, withTransaction } = require('../db');
211
211
  const db = getDb();
212
212
  const filename = withTransaction(db, () => {
213
213
  const exists = db.prepare('SELECT 1 FROM prds WHERE filename=? LIMIT 1');
@@ -219,7 +219,7 @@ function createPrdUnique(baseFilename, prd) {
219
219
  }
220
220
  throw new Error('Could not reserve unique PRD filename after 100 attempts');
221
221
  });
222
- require('./db-events').emitStateEvent('prds', { filename, archived: 0 });
222
+ require('../persistence/db-events').emitStateEvent('prds', { filename, archived: 0 });
223
223
  return filename;
224
224
  }
225
225
 
@@ -228,7 +228,7 @@ function mutatePrd(filename, mutator, { archived = false, defaultValue = {} } =
228
228
  if (typeof mutator !== 'function') {
229
229
  throw new TypeError('prd-store.mutatePrd requires a filename and mutator');
230
230
  }
231
- const { getDb, withTransaction } = require('./db');
231
+ const { getDb, withTransaction } = require('../db');
232
232
  const db = getDb();
233
233
  const result = withTransaction(db, () => {
234
234
  const archivedValue = archived ? 1 : 0;
@@ -244,13 +244,13 @@ function mutatePrd(filename, mutator, { archived = false, defaultValue = {} } =
244
244
  _upsertPrd(db, filename, archivedValue, normalizedPrd, Date.now());
245
245
  return normalizedPrd;
246
246
  });
247
- require('./db-events').emitStateEvent('prds', { filename, archived: archived ? 1 : 0 });
247
+ require('../persistence/db-events').emitStateEvent('prds', { filename, archived: archived ? 1 : 0 });
248
248
  return result;
249
249
  }
250
250
 
251
251
  function deletePrd(filename, { archived = false } = {}) {
252
252
  validatePrdFilename(filename);
253
- const { getDb, withTransaction } = require('./db');
253
+ const { getDb, withTransaction } = require('../db');
254
254
  const db = getDb();
255
255
  const removed = withTransaction(db, () => {
256
256
  const archivedValue = archived ? 1 : 0;
@@ -259,13 +259,13 @@ function deletePrd(filename, { archived = false } = {}) {
259
259
  _deletePrdRow(db, row.id);
260
260
  return true;
261
261
  });
262
- if (removed) require('./db-events').emitStateEvent('prds', { filename, archived: archived ? 1 : 0, removed: true });
262
+ if (removed) require('../persistence/db-events').emitStateEvent('prds', { filename, archived: archived ? 1 : 0, removed: true });
263
263
  return removed;
264
264
  }
265
265
 
266
266
  function readPrd(filename, { archived = false } = {}) {
267
267
  validatePrdFilename(filename);
268
- const row = require('./db').getDb()
268
+ const row = require('../db').getDb()
269
269
  .prepare('SELECT data FROM prds WHERE filename=? AND archived=?')
270
270
  .get(filename, archived ? 1 : 0);
271
271
  return row ? JSON.parse(row.data) : null;
@@ -284,7 +284,7 @@ function restorePrdFromArchive(filename) {
284
284
  if (!filename || typeof filename !== 'string') {
285
285
  throw new TypeError('prd-store.restorePrdFromArchive requires a filename');
286
286
  }
287
- const { getDb, withTransaction } = require('./db');
287
+ const { getDb, withTransaction } = require('../db');
288
288
  const db = getDb();
289
289
  const restored = withTransaction(db, () => {
290
290
  const row = db.prepare('SELECT id, data FROM prds WHERE filename=? AND archived=1').get(filename);
@@ -295,7 +295,7 @@ function restorePrdFromArchive(filename) {
295
295
  return prd;
296
296
  });
297
297
  if (restored) {
298
- require('./db-events').emitStateEvent('prds', { filename, archived: 0, restored: true });
298
+ require('../persistence/db-events').emitStateEvent('prds', { filename, archived: 0, restored: true });
299
299
  }
300
300
  return restored;
301
301
  }
@@ -309,7 +309,7 @@ function movePrdToArchive(filename, replacementPrd) {
309
309
  && (!replacementPrd || typeof replacementPrd !== 'object' || Array.isArray(replacementPrd))) {
310
310
  throw new TypeError('prd-store.movePrdToArchive replacement must be a PRD object');
311
311
  }
312
- const { getDb, withTransaction } = require('./db');
312
+ const { getDb, withTransaction } = require('../db');
313
313
  const db = getDb();
314
314
  const archived = withTransaction(db, () => {
315
315
  const row = db.prepare('SELECT id, data FROM prds WHERE filename=? AND archived=0').get(filename);
@@ -322,7 +322,7 @@ function movePrdToArchive(filename, replacementPrd) {
322
322
  return prd;
323
323
  });
324
324
  if (archived) {
325
- require('./db-events').emitStateEvent('prds', {
325
+ require('../persistence/db-events').emitStateEvent('prds', {
326
326
  filename,
327
327
  archived: 1,
328
328
  compensated: true,
@@ -338,9 +338,9 @@ function movePrdToArchive(filename, replacementPrd) {
338
338
  // Returns { ok, byWorkItemId, bySourcePlan } counts.
339
339
  function backfillWorkItemPrdItemIds() {
340
340
  let db;
341
- try { db = require('./db').getDb(); } catch { return { ok: false }; }
341
+ try { db = require('../db').getDb(); } catch { return { ok: false }; }
342
342
  try {
343
- return require('./db').withTransaction(db, () => {
343
+ return require('../db').withTransaction(db, () => {
344
344
  // Path 1 — explicit prd_items.work_item_id backref (most precise).
345
345
  const r1 = db.prepare(`
346
346
  UPDATE work_items SET prd_item_id = (
@@ -378,7 +378,7 @@ function backfillWorkItemPrdItemIds() {
378
378
  // data blob. Sorted by (archived, filename) for deterministic consumer order
379
379
  // (matches the disk-scan order once the caller also sorts). Read-flip source.
380
380
  function listPrdRows() {
381
- const db = require('./db').getDb();
381
+ const db = require('../db').getDb();
382
382
  const rows = db.prepare('SELECT filename, archived, data, updated_at FROM prds ORDER BY archived, filename').all();
383
383
  const out = [];
384
384
  for (const r of rows) {
@@ -5,7 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { execFileSync: defaultExecFileSync } = require('child_process');
8
- const shared = require('./shared');
8
+ const shared = require('../core/shared');
9
9
 
10
10
  function decodeUrlSegment(segment) {
11
11
  try { return decodeURIComponent(String(segment || '')); } catch { return String(segment || ''); }
@@ -196,7 +196,7 @@ function runAzJson(execFileSync, args, timeoutMs) {
196
196
  }
197
197
 
198
198
  // W-mq8li79a000889fa — official ADO REST endpoint for repo lookup.
199
- // Matches engine/ado.js#resolveAdoBuildRepositoryGuid (the canonical
199
+ // Matches engine/ado/index.js#resolveAdoBuildRepositoryGuid (the canonical
200
200
  // PR-poll path). Path-style encoding mirrors that helper so a project
201
201
  // named with reserved chars (e.g. "Project With Spaces") routes the
202
202
  // same way at link time as at poll time. The repo segment is URL-
@@ -212,7 +212,7 @@ function _buildAdoRepoLookupUrl({ orgUrl, project, repoName }) {
212
212
  // this AFTER `az repos show/list` to fill in the GUID when the CLI is
213
213
  // unavailable (typical agent box without Azure CLI installed).
214
214
  //
215
- // Token acquisition uses engine/ado-token.js#acquireAdoToken (az CLI
215
+ // Token acquisition uses engine/ado/token.js#acquireAdoToken (az CLI
216
216
  // then azureauth fallback). Tests inject `options.fetch` and
217
217
  // `options.acquireAdoToken` directly.
218
218
  async function resolveAdoRemoteMetadataViaRest(remote, options = {}) {
@@ -221,7 +221,7 @@ async function resolveAdoRemoteMetadataViaRest(remote, options = {}) {
221
221
  if (typeof fetchImpl !== 'function') return null;
222
222
  let acquireToken = options.acquireAdoToken;
223
223
  if (typeof acquireToken !== 'function') {
224
- try { acquireToken = require('./ado-token').acquireAdoToken; }
224
+ try { acquireToken = require('../ado/token').acquireAdoToken; }
225
225
  catch { return null; }
226
226
  }
227
227
  let token;
@@ -387,7 +387,7 @@ function discoverProjectMetadata(targetDir, options = {}) {
387
387
 
388
388
  // W-mq8li79a000889fa — async wrapper that runs the sync `az`-based
389
389
  // discovery first, then falls back to the official ADO REST endpoint
390
- // (via engine/ado-token.js + globalThis.fetch) when the remote is ADO
390
+ // (via engine/ado/token.js + globalThis.fetch) when the remote is ADO
391
391
  // but no repositoryId was resolved. Non-ADO remotes are returned
392
392
  // unchanged. Failure to resolve the GUID via REST leaves
393
393
  // `repositoryId` empty — callers (engine/projects.js#addProject) MUST
@@ -7,12 +7,12 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
- const shared = require('./shared');
11
- const dispatch = require('./dispatch');
12
- const queries = require('./queries');
10
+ const shared = require('../core/shared');
11
+ const dispatch = require('../orchestration/dispatch');
12
+ const queries = require('../core/queries');
13
13
  const projectDiscovery = require('./project-discovery');
14
- const scheduleBootstrap = require('./schedule-bootstrap');
15
- const apiValidation = require('./api-validation');
14
+ const scheduleBootstrap = require('../orchestration/schedule-bootstrap');
15
+ const apiValidation = require('../api/api-validation');
16
16
  const { MINIONS_DIR } = shared;
17
17
 
18
18
  const PROJECT_PATH_MAX_LENGTH = 4096;
@@ -195,7 +195,7 @@ function resolveProjectRemovalDataMode(options = {}) {
195
195
  // Windows EPERM/EBUSY can hit fs.renameSync transiently when antivirus,
196
196
  // indexer, or another scanner has the directory open. Retry with backoff
197
197
  // before giving up — same pattern used by shared.safeWrite for atomic
198
- // JSON writes (engine/shared.js:424).
198
+ // JSON writes (engine/core/shared.js:424).
199
199
  function _renameWithRetry(src, dest) {
200
200
  for (let attempt = 0; attempt < 5; attempt++) {
201
201
  try {
@@ -377,7 +377,7 @@ function removeProject(target, options = {}) {
377
377
  // Kills + unlinks every spec owned by this project, including the .log
378
378
  // and .log.1 rotated sibling. Best-effort: failures only warn.
379
379
  try {
380
- const managedSpawn = require('./managed-spawn');
380
+ const managedSpawn = require('../processes/managed-spawn');
381
381
  const result = managedSpawn.removeManagedSpecsForProject(project.name);
382
382
  summary.killedManagedProcesses = result.killed || 0;
383
383
  } catch (e) {
@@ -423,7 +423,7 @@ function removeProject(target, options = {}) {
423
423
  const archivedSourcePlans = new Set();
424
424
  try {
425
425
  {
426
- const { archivePlan } = require('./lifecycle');
426
+ const { archivePlan } = require('../orchestration/lifecycle');
427
427
  for (const { filename: f, plan: prd, archived } of require('./prd-store').listPrdRows()) {
428
428
  if (archived) continue;
429
429
  try {
@@ -458,7 +458,7 @@ function removeProject(target, options = {}) {
458
458
  // 6. Surface pipelines that reference this project so the user can review
459
459
  // them. Don't auto-modify — user intent there is unclear.
460
460
  try {
461
- const { getPipelines } = require('./pipeline');
461
+ const { getPipelines } = require('../orchestration/pipeline');
462
462
  for (const p of getPipelines() || []) {
463
463
  const refs = [
464
464
  ...(p.monitoredResources || []),
@@ -602,7 +602,7 @@ function removeProject(target, options = {}) {
602
602
 
603
603
  // 9. Drop SQL rows for the removed project scope.
604
604
  try { require('./work-items-store').dropScope(project.name); } catch { /* best-effort */ }
605
- try { require('./pull-requests-store').dropScope(project.name); } catch { /* best-effort */ }
605
+ try { require('../persistence/pull-requests-store').dropScope(project.name); } catch { /* best-effort */ }
606
606
 
607
607
  summary.ok = true;
608
608
  return summary;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * engine/resolve-area.js — Reverse "component name → subproject path" resolver.
2
+ * engine/planning/resolve-area.js — Reverse component-to-path resolver.
3
3
  *
4
4
  * WI W-mrpbsbs7000ce8ac. Command Center (and other callers) frequently receive
5
5
  * a monorepo component name in a natural-language request ("audit OCM changes",
@@ -1,4 +1,4 @@
1
- const apiValidation = require('./api-validation');
1
+ const apiValidation = require('../api/api-validation');
2
2
 
3
3
  const WORK_ITEM_SCOPES = Object.freeze(['fan-out']);
4
4
  const WORK_ITEM_FEEDBACK_RATINGS = Object.freeze(['up', 'down']);
@@ -323,6 +323,21 @@ function _normalizeWorkdir(value, field, validateWorkdir) {
323
323
  return result.value;
324
324
  }
325
325
 
326
+ function _normalizeExecutionSurface(value, resolvedType, validateExecutionSurface) {
327
+ if (typeof validateExecutionSurface !== 'function') {
328
+ throw new TypeError('normalize work-item input requires validateExecutionSurface');
329
+ }
330
+ const result = validateExecutionSurface(value, resolvedType);
331
+ if (!result || result.valid !== true) {
332
+ _inputError(`meta.executionSurface: ${result?.error || 'invalid execution surface'}`, {
333
+ code: result?.code || 'invalid-execution-surface',
334
+ field: 'meta.executionSurface',
335
+ rejectedValue: value,
336
+ });
337
+ }
338
+ return result.value;
339
+ }
340
+
326
341
  function _normalizeMeta(value, options = {}) {
327
342
  if (value === undefined) return undefined;
328
343
  apiValidation.validatePlainObject(value, {
@@ -341,6 +356,20 @@ function _normalizeMeta(value, options = {}) {
341
356
  if (workdir === null) delete normalized.workdir;
342
357
  else normalized.workdir = workdir;
343
358
  }
359
+ // W-ms5tb6ha016fda10 — explicit control-plane opt-in, validated fail-closed
360
+ // against the resolved work type. An ineligible (repo-reading/mutating) type
361
+ // is REJECTED at the API boundary rather than silently downgraded, so an
362
+ // operator cannot accidentally strip a checkout from work that needs one.
363
+ // The default 'repo' value is dropped so unchanged items keep today's shape.
364
+ if (_hasOwn(value, 'executionSurface')) {
365
+ const surface = _normalizeExecutionSurface(
366
+ value.executionSurface,
367
+ options.resolvedType,
368
+ options.validateExecutionSurface,
369
+ );
370
+ if (!surface || surface === 'repo') delete normalized.executionSurface;
371
+ else normalized.executionSurface = surface;
372
+ }
344
373
  for (const field of META_BOOLEAN_FIELDS) {
345
374
  if (_hasOwn(value, field)) {
346
375
  normalized[field] = _normalizeBoolean(value[field], `meta.${field}`);
@@ -445,7 +474,7 @@ function normalizeCreateInput(body, options = {}) {
445
474
  for (const field of ['skipPr', 'oneShot', 'agentLock', 'hardAgent']) {
446
475
  if (_hasOwn(body, field)) normalized[field] = _normalizeBoolean(body[field], field);
447
476
  }
448
- normalized.meta = _normalizeMeta(body.meta, options);
477
+ normalized.meta = _normalizeMeta(body.meta, { ...options, resolvedType: normalized.type });
449
478
  for (const field of ['planFile', 'branch', 'branchStrategy', 'featureBranch']) {
450
479
  if (_hasOwn(body, field)) {
451
480
  normalized[field] = _normalizeText(body[field], field, {
@@ -510,7 +539,14 @@ function normalizeUpdateInput(body, options = {}) {
510
539
  if (_hasOwn(body, 'workdir')) {
511
540
  normalized.workdir = _normalizeWorkdir(body.workdir, 'workdir', options.validateWorkdir);
512
541
  }
513
- normalized.meta = _normalizeMeta(body.meta, options);
542
+ // W-ms5tb6ha016fda10 — an update may change type, meta.executionSurface, or
543
+ // both. Prefer the incoming type; fall back to the stored item's type
544
+ // (`options.currentType`, supplied by the dashboard handler) so a
545
+ // meta-only edit is still checked against the type the item will run as.
546
+ normalized.meta = _normalizeMeta(body.meta, {
547
+ ...options,
548
+ resolvedType: _hasOwn(body, 'type') ? normalized.type : options.currentType,
549
+ });
514
550
  return normalized;
515
551
  }
516
552
 
@@ -1,4 +1,4 @@
1
- // engine/work-items-store.js — SQL-backed scoped work-item storage.
1
+ // engine/planning/work-items-store.js — SQL-backed scoped work-item storage.
2
2
  //
3
3
  // readWorkItemsForScope(scope) -> [wi, wi, ...]
4
4
  // applyWorkItemsMutation(scope, fn) -> runs fn against the array,
@@ -22,7 +22,7 @@ function _parseRow(row) {
22
22
  }
23
23
 
24
24
  function readWorkItemsForScope(scope) {
25
- const { getDb } = require('./db');
25
+ const { getDb } = require('../db');
26
26
  const db = getDb();
27
27
 
28
28
  const rows = db.prepare(`
@@ -43,16 +43,24 @@ function readWorkItemsForScope(scope) {
43
43
  // needs to surface central + every project's items in a single shot,
44
44
  // tagged with their source scope.
45
45
  function readAllWorkItems() {
46
- const { getDb } = require('./db');
46
+ const { getDb } = require('../db');
47
47
  const db = getDb();
48
48
 
49
- const rows = db.prepare('SELECT data, scope FROM work_items WHERE archived = 0 ORDER BY rowid').all();
49
+ const rows = db.prepare('SELECT data, scope, updated_at FROM work_items WHERE archived = 0 ORDER BY rowid').all();
50
50
  const out = [];
51
51
  for (const row of rows) {
52
52
  const wi = _parseRow(row);
53
53
  if (!wi) continue;
54
54
  // Preserve the legacy `_source` convention from queries.getWorkItems.
55
55
  wi._source = row.scope;
56
+ // Surface the authoritative last-mutation time (updated_at column, ms epoch)
57
+ // as an ISO `updatedAt` so /api/work-items consumers can sort on real
58
+ // last-updated rather than faking it from created/dispatched_at. The column
59
+ // wins over any stale in-blob value. (W-ms7kutzv003q17ad)
60
+ if (row.updated_at != null) {
61
+ const ms = Number(row.updated_at);
62
+ if (Number.isFinite(ms)) wi.updatedAt = new Date(ms).toISOString();
63
+ }
56
64
  out.push(wi);
57
65
  }
58
66
  return out;
@@ -136,7 +144,7 @@ function _applyWorkItemsDiff(db, diff) {
136
144
  // result — the post-mutation array (legacy return shape of
137
145
  // mutateJsonFileLocked)
138
146
  function applyWorkItemsMutation(scope, mutator, opts = {}) {
139
- const { getDb, withTransaction } = require('./db');
147
+ const { getDb, withTransaction } = require('../db');
140
148
  const db = getDb();
141
149
 
142
150
  return withTransaction(db, () => {
@@ -170,17 +178,17 @@ function applyWorkItemsMutation(scope, mutator, opts = {}) {
170
178
  // applyWorkItemsMutation's callers (see shared.js mutateWorkItems).
171
179
  function dropScope(scope) {
172
180
  try {
173
- const { getDb } = require('./db');
181
+ const { getDb } = require('../db');
174
182
  const result = getDb().prepare('DELETE FROM work_items WHERE scope = ?').run(scope);
175
183
  if (result && result.changes > 0) {
176
- try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
177
- try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
184
+ try { require('../persistence/db-events').emitStateEvent('work_items'); } catch { /* optional */ }
185
+ try { require('../core/queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
178
186
  }
179
187
  } catch { /* db unavailable */ }
180
188
  }
181
189
 
182
190
  function readArchivedWorkItemsForScope(scope) {
183
- const rows = require('./db').getDb().prepare(`
191
+ const rows = require('../db').getDb().prepare(`
184
192
  SELECT data FROM work_items
185
193
  WHERE scope = ? AND archived = 1
186
194
  ORDER BY updated_at DESC, rowid DESC
@@ -194,7 +202,7 @@ function readArchivedWorkItemsForScope(scope) {
194
202
  }
195
203
 
196
204
  function archiveWorkItem(scope, id, opts = {}) {
197
- const { getDb, withTransaction } = require('./db');
205
+ const { getDb, withTransaction } = require('../db');
198
206
  const db = getDb();
199
207
  const archived = withTransaction(db, () => {
200
208
  const row = db.prepare('SELECT data FROM work_items WHERE scope=? AND id=? AND archived=0').get(scope, String(id));
@@ -219,14 +227,14 @@ function archiveWorkItem(scope, id, opts = {}) {
219
227
  return item;
220
228
  });
221
229
  if (archived) {
222
- try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
223
- try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
230
+ try { require('../persistence/db-events').emitStateEvent('work_items'); } catch { /* optional */ }
231
+ try { require('../core/queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
224
232
  }
225
233
  return archived;
226
234
  }
227
235
 
228
236
  function replaceArchivedWorkItems(scope, items) {
229
- const { getDb, withTransaction } = require('./db');
237
+ const { getDb, withTransaction } = require('../db');
230
238
  const db = getDb();
231
239
  const count = withTransaction(db, () => {
232
240
  db.prepare('DELETE FROM work_items WHERE scope=? AND archived=1').run(scope);
@@ -257,13 +265,13 @@ function replaceArchivedWorkItems(scope, items) {
257
265
  }
258
266
  return inserted;
259
267
  });
260
- try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
261
- try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
268
+ try { require('../persistence/db-events').emitStateEvent('work_items'); } catch { /* optional */ }
269
+ try { require('../core/queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
262
270
  return count;
263
271
  }
264
272
 
265
273
  function moveWorkItem(sourceScope, targetScope, id, item) {
266
- const { getDb, withTransaction } = require('./db');
274
+ const { getDb, withTransaction } = require('../db');
267
275
  const db = getDb();
268
276
  const moved = withTransaction(db, () => {
269
277
  const source = db.prepare('SELECT 1 FROM work_items WHERE scope=? AND id=? AND archived=0')
@@ -292,15 +300,15 @@ function moveWorkItem(sourceScope, targetScope, id, item) {
292
300
  return Number(result.changes) > 0;
293
301
  });
294
302
  if (moved) {
295
- try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
296
- try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
303
+ try { require('../persistence/db-events').emitStateEvent('work_items'); } catch { /* optional */ }
304
+ try { require('../core/queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
297
305
  }
298
306
  return moved;
299
307
  }
300
308
 
301
309
  function moveWorkItemWithUpdate(sourceScope, targetScope, id, updater) {
302
310
  if (typeof updater !== 'function') throw new TypeError('moveWorkItemWithUpdate requires an updater');
303
- const { getDb, withTransaction } = require('./db');
311
+ const { getDb, withTransaction } = require('../db');
304
312
  const db = getDb();
305
313
  const outcome = withTransaction(db, () => {
306
314
  const source = db.prepare(
@@ -340,8 +348,8 @@ function moveWorkItemWithUpdate(sourceScope, targetScope, id, updater) {
340
348
  return { moved: true, reason: null, item: updated };
341
349
  });
342
350
  if (outcome.moved) {
343
- try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
344
- try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
351
+ try { require('../persistence/db-events').emitStateEvent('work_items'); } catch { /* optional */ }
352
+ try { require('../core/queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
345
353
  }
346
354
  return outcome;
347
355
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * engine/keep-process-sweep.js -- opt-in `meta.keep_processes` reaper.
2
+ * engine/processes/keep-process-sweep.js -- opt-in `meta.keep_processes` reaper.
3
3
  *
4
4
  * Walks agents/<id>/keep-pids.json files and, for each:
5
5
  * - Validates the file (bounded JSON shape, see validateKeepPidsRecord).
@@ -19,8 +19,8 @@
19
19
 
20
20
  const fs = require('fs');
21
21
  const path = require('path');
22
- const shared = require('./shared');
23
- const queries = require('./queries');
22
+ const shared = require('../core/shared');
23
+ const queries = require('../core/queries');
24
24
  const processUtils = require('./process-utils');
25
25
 
26
26
  const { log, ENGINE_DEFAULTS } = shared;
@@ -262,14 +262,16 @@ function readKeepPidsFile(agentId, opts) {
262
262
  let raw;
263
263
  try { raw = fs.readFileSync(filePath, 'utf8'); }
264
264
  catch (_e) { return null; }
265
+ let mtimeMs = 0;
266
+ try { mtimeMs = fs.statSync(filePath).mtimeMs; } catch {}
265
267
  let parsed = null;
266
268
  try { parsed = JSON.parse(raw); }
267
269
  catch (e) {
268
- return { agentId: agentId, filePath: filePath, valid: false, reason: 'json-parse: ' + e.message, parsed: null };
270
+ return { agentId: agentId, filePath: filePath, mtimeMs, valid: false, reason: 'json-parse: ' + e.message, parsed: null };
269
271
  }
270
272
  const v = validateKeepPidsRecord(parsed, opts);
271
- if (!v.ok) return { agentId: agentId, filePath: filePath, valid: false, reason: v.reason, parsed: parsed };
272
- return { agentId: agentId, filePath: filePath, valid: true, value: v.value, parsed: parsed };
273
+ if (!v.ok) return { agentId: agentId, filePath: filePath, mtimeMs, valid: false, reason: v.reason, parsed: parsed };
274
+ return { agentId: agentId, filePath: filePath, mtimeMs, valid: true, value: v.value, parsed: parsed };
273
275
  }
274
276
 
275
277
  function listAllKeepPidsFiles(opts) {
@@ -305,12 +307,28 @@ function _audit(eventType, payload) {
305
307
  } catch (_e) { /* logging best-effort */ }
306
308
  }
307
309
 
310
+ function _identityMatchesDeclaration(rec, pid, opts) {
311
+ const getIdentity = typeof opts.getIdentity === 'function'
312
+ ? opts.getIdentity
313
+ : getKeepProcessIdentity;
314
+ let identity = null;
315
+ try { identity = getIdentity(pid, opts); } catch {}
316
+ return isKeepProcessIdentityCompatible(identity, rec.mtimeMs);
317
+ }
318
+
319
+ function _withIdentitySnapshot(opts) {
320
+ if (typeof opts.getIdentity === 'function' || Array.isArray(opts.processes)) return opts;
321
+ return { ...opts, processes: processUtils.listAllProcesses() };
322
+ }
323
+
308
324
  function sweepKeepProcesses(opts) {
309
325
  opts = opts || {};
310
326
  const now = Number.isFinite(opts.now) ? opts.now : Date.now();
311
327
  const killOne = typeof opts.killOne === 'function'
312
328
  ? opts.killOne
313
- : function (pid) { return shared.killByPidImmediate(pid); };
329
+ : function (pid, proof) {
330
+ return shared.terminateProcess('engine.keep-process.ttl', pid, proof);
331
+ };
314
332
  const stats = { scanned: 0, expiredFiles: 0, killedPids: 0, deadFiles: 0, malformed: 0 };
315
333
  // W-mp6k7ywi000fa33c — sweep ignores workdir validation so TTL/dead-PID
316
334
  // cleanup still drains files whose cwd no longer exists (e.g., the agent
@@ -318,6 +336,7 @@ function sweepKeepProcesses(opts) {
318
336
  // getActiveAnchorPids*) keep the default-on validation so invalid records
319
337
  // are not honored as anchors.
320
338
  const records = listAllKeepPidsFiles({ now: now, requireGitWorkdir: false });
339
+ if (records.length > 0) opts = _withIdentitySnapshot(opts);
321
340
  for (const rec of records) {
322
341
  stats.scanned++;
323
342
  if (!rec.valid) {
@@ -328,20 +347,30 @@ function sweepKeepProcesses(opts) {
328
347
  const value = rec.value;
329
348
  const filePath = rec.filePath;
330
349
  const agentId = rec.agentId;
350
+ const killDeclaredPid = (pid, reason) => {
351
+ if (!_identityMatchesDeclaration(rec, pid, opts)) {
352
+ _audit('identity-mismatch-skip', { agentId, pid, reason, wi_id: value.wi_id });
353
+ return false;
354
+ }
355
+ return killOne(pid, {
356
+ recordedAtMs: rec.mtimeMs,
357
+ skewMs: KEEP_PROCESS_IDENTITY_CLOCK_SKEW_MS,
358
+ });
359
+ };
331
360
  const executionContext = validateKeepProcessExecutionContext(value, opts);
332
361
  if (!executionContext.ok) {
333
362
  for (const pid of value.pids) {
334
- if (killOne(pid)) stats.killedPids++;
335
- _audit('unsafe-context-kill', { agentId, pid, reason: executionContext.reason, wi_id: value.wi_id });
363
+ _audit('unsafe-context-skip', { agentId, pid, reason: executionContext.reason, wi_id: value.wi_id });
336
364
  }
337
365
  try { fs.unlinkSync(filePath); } catch {}
338
- log('warn', 'keep-processes: removed unsafe execution context for ' + agentId + ' — ' + executionContext.reason);
366
+ log('warn', 'keep-processes: removed unsafe execution context without trusting its PIDs for ' +
367
+ agentId + ' — ' + executionContext.reason);
339
368
  continue;
340
369
  }
341
370
  if (value.expiresAtMs < now) {
342
371
  let killed = 0;
343
372
  for (const pid of value.pids) {
344
- const ok = killOne(pid);
373
+ const ok = killDeclaredPid(pid, 'ttl_expired');
345
374
  if (ok) killed++;
346
375
  _audit('ttl-expired-kill', { agentId: agentId, pid: pid, reason: 'ttl_expired', wi_id: value.wi_id });
347
376
  }
@@ -350,7 +379,8 @@ function sweepKeepProcesses(opts) {
350
379
  try { fs.unlinkSync(filePath); } catch (_e) { /* gone already */ }
351
380
  continue;
352
381
  }
353
- const stillAlive = alivePids(value.pids, opts);
382
+ const stillAlive = alivePids(value.pids, opts)
383
+ .filter(pid => _identityMatchesDeclaration(rec, pid, opts));
354
384
  if (stillAlive.length === 0) {
355
385
  stats.deadFiles++;
356
386
  try { fs.unlinkSync(filePath); } catch (_e) { /* gone already */ }
@@ -364,11 +394,15 @@ function getActiveAnchorPids(opts) {
364
394
  opts = opts || {};
365
395
  const now = Number.isFinite(opts.now) ? opts.now : Date.now();
366
396
  const out = new Set();
367
- for (const rec of listAllKeepPidsFiles({ now: now })) {
397
+ const records = listAllKeepPidsFiles({ now: now });
398
+ if (records.length > 0) opts = _withIdentitySnapshot(opts);
399
+ for (const rec of records) {
368
400
  if (!rec.valid) continue;
369
401
  if (rec.value.expiresAtMs < now) continue;
370
402
  if (!validateKeepProcessExecutionContext(rec.value, opts).ok) continue;
371
- for (const pid of rec.value.pids) out.add(pid);
403
+ for (const pid of rec.value.pids) {
404
+ if (_identityMatchesDeclaration(rec, pid, opts)) out.add(pid);
405
+ }
372
406
  }
373
407
  return out;
374
408
  }
@@ -388,7 +422,10 @@ function getActiveAnchorPidsForAgent(agentId, opts) {
388
422
  if (rec.value.expiresAtMs < now) return { pids: out, record: rec.value, reason: 'expired' };
389
423
  const executionContext = validateKeepProcessExecutionContext(rec.value, opts);
390
424
  if (!executionContext.ok) return { pids: out, record: rec.value, reason: executionContext.reason };
391
- for (const pid of rec.value.pids) out.add(pid);
425
+ opts = _withIdentitySnapshot(opts);
426
+ for (const pid of rec.value.pids) {
427
+ if (_identityMatchesDeclaration(rec, pid, opts)) out.add(pid);
428
+ }
392
429
  return { pids: out, record: rec.value };
393
430
  }
394
431
 
@@ -396,10 +433,13 @@ function getActiveKeepProcessCwds(opts) {
396
433
  opts = opts || {};
397
434
  const now = Number.isFinite(opts.now) ? opts.now : Date.now();
398
435
  const out = [];
399
- for (const rec of listAllKeepPidsFiles({ now })) {
436
+ const records = listAllKeepPidsFiles({ now });
437
+ if (records.length > 0) opts = _withIdentitySnapshot(opts);
438
+ for (const rec of records) {
400
439
  if (!rec.valid || rec.value.expiresAtMs < now || !rec.value.cwd) continue;
401
440
  if (!validateKeepProcessExecutionContext(rec.value, opts).ok) continue;
402
- if (alivePids(rec.value.pids, opts).length === 0) continue;
441
+ if (!alivePids(rec.value.pids, opts)
442
+ .some(pid => _identityMatchesDeclaration(rec, pid, opts))) continue;
403
443
  try { out.push(shared.realPathForComparison(rec.value.cwd)); }
404
444
  catch { /* malformed/unreachable cwd contributes no anchor */ }
405
445
  }
@@ -2,6 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  const { spawn } = require('child_process');
5
+ const { terminateProcess } = require('./process-utils');
5
6
 
6
7
  const [cmd, ...args] = process.argv.slice(2);
7
8
  if (!cmd) {
@@ -37,10 +38,9 @@ if (!cmd) {
37
38
  console.error('[managed-spawn-launcher] target exited (code='
38
39
  + (code == null ? 'null' : code) + ', signal=' + (signal || 'null') + ')');
39
40
  if (signal) {
40
- try {
41
- process.kill(process.pid, signal);
41
+ if (terminateProcess('engine.managed-launcher.self-signal', { pid: process.pid }, { signal })) {
42
42
  return;
43
- } catch {
43
+ } else {
44
44
  process.exitCode = 1;
45
45
  return;
46
46
  }